Skip to content

Commit f652d1a

Browse files
committed
chore: add legacy RF to /legacy
1 parent 78ba728 commit f652d1a

6 files changed

Lines changed: 512 additions & 0 deletions

File tree

‎packages/expo-resource-fetcher/package.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
".": {
99
"import": "./lib/index.js",
1010
"types": "./lib/index.d.ts"
11+
},
12+
"./legacy": {
13+
"import": "./lib/legacy/index.js",
14+
"types": "./lib/legacy/index.d.ts"
1115
}
1216
},
1317
"files": [
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/**
2+
* Resource Fetcher for Expo applications (legacy `expo-file-system` API).
3+
*
4+
* This module provides functions to download and manage files stored in the application's document directory
5+
* inside the `react-native-executorch/` directory. These utilities help manage storage and clean up downloaded
6+
* files when they are no longer needed.
7+
*
8+
* It is functionally equivalent to the default `ExpoResourceFetcher` export, but uses the legacy
9+
* `expo-file-system/legacy` APIs under the hood. Prefer this build only if you cannot adopt the new
10+
* file-system API yet.
11+
* @category Utilities - General
12+
* @remarks
13+
* **Key Functionality:**
14+
* - **Download Control**: Pause, resume, and cancel operations through:
15+
* - {@link pauseFetching} - Pause ongoing downloads
16+
* - {@link resumeFetching} - Resume paused downloads
17+
* - {@link cancelFetching} - Cancel ongoing or paused downloads
18+
* - **File Management**:
19+
* - {@link getFilesTotalSize} - Get total size of resources
20+
* - {@link listDownloadedFiles} - List all downloaded files
21+
* - {@link listDownloadedModels} - List downloaded model files (.pte)
22+
* - {@link deleteResources} - Delete downloaded resources
23+
*
24+
* **Important Notes:**
25+
* - Pause/resume/cancel operations work only for remote resources
26+
* - Most functions accept multiple `ResourceSource` arguments (string, number, or object)
27+
* - The {@link fetch} method accepts a progress callback (0-1) and returns file paths or throws if interrupted
28+
*/
29+
30+
import {
31+
deleteAsync,
32+
readDirectoryAsync,
33+
readAsStringAsync,
34+
moveAsync,
35+
} from 'expo-file-system/legacy';
36+
import { RNEDirectory } from './constants/directories';
37+
import {
38+
ResourceSource,
39+
RnExecutorchErrorCode,
40+
RnExecutorchError,
41+
BaseResourceFetcherClass,
42+
} from 'react-native-executorch';
43+
import {
44+
ResourceFetcherUtils,
45+
HTTP_CODE,
46+
DownloadStatus,
47+
} from './ResourceFetcherUtils';
48+
import {
49+
type ActiveDownload,
50+
handleObject,
51+
handleLocalFile,
52+
handleAsset,
53+
handleRemote,
54+
} from './handlers';
55+
56+
class ExpoResourceFetcherClass extends BaseResourceFetcherClass<ActiveDownload> {
57+
protected downloads = new Map<ResourceSource, ActiveDownload>();
58+
59+
protected async getFilesSizes(sources: ResourceSource[]) {
60+
return ResourceFetcherUtils.getFilesSizes(sources);
61+
}
62+
63+
protected async handleObject(source: object): Promise<string> {
64+
return handleObject(source);
65+
}
66+
67+
protected handleLocalFile(source: string): string {
68+
return handleLocalFile(source);
69+
}
70+
71+
protected handleAsset(
72+
source: number,
73+
progressCallback: (progress: number) => void
74+
): Promise<string> {
75+
return handleAsset(source, progressCallback, this.downloads);
76+
}
77+
78+
protected handleRemote(
79+
uri: string,
80+
source: ResourceSource,
81+
progressCallback: (progress: number) => void
82+
): Promise<{ path: string; wasDownloaded: boolean }> {
83+
return handleRemote(uri, source, progressCallback, this.downloads);
84+
}
85+
86+
protected async pause(source: ResourceSource): Promise<void> {
87+
const downloadHandle = this.downloads.get(source)!;
88+
if (downloadHandle.status === DownloadStatus.PAUSED) {
89+
throw new RnExecutorchError(
90+
RnExecutorchErrorCode.ResourceFetcherAlreadyPaused,
91+
"The file download is currently paused. Can't pause the download of the same file twice."
92+
);
93+
}
94+
downloadHandle.status = DownloadStatus.PAUSED;
95+
await downloadHandle.downloadResumable.pauseAsync();
96+
}
97+
98+
protected async resume(source: ResourceSource): Promise<void> {
99+
const downloadHandle = this.downloads.get(source)!;
100+
if (downloadHandle.status === DownloadStatus.ONGOING) {
101+
throw new RnExecutorchError(
102+
RnExecutorchErrorCode.ResourceFetcherAlreadyOngoing,
103+
"The file download is currently ongoing. Can't resume the ongoing download."
104+
);
105+
}
106+
downloadHandle.status = DownloadStatus.ONGOING;
107+
const result = await downloadHandle.downloadResumable.resumeAsync();
108+
const current = this.downloads.get(source);
109+
// Paused again or canceled during resume — resolve/reject handled elsewhere.
110+
if (!current || current.status === DownloadStatus.PAUSED) return;
111+
112+
if (
113+
!result ||
114+
(result.status !== HTTP_CODE.OK &&
115+
result.status !== HTTP_CODE.PARTIAL_CONTENT)
116+
) {
117+
this.downloads.delete(source);
118+
downloadHandle.reject(
119+
new RnExecutorchError(
120+
RnExecutorchErrorCode.ResourceFetcherDownloadFailed,
121+
`Failed to resume download from '${downloadHandle.uri}', status: ${result?.status}`
122+
)
123+
);
124+
return;
125+
}
126+
127+
await moveAsync({
128+
from: downloadHandle.cacheFileUri,
129+
to: downloadHandle.fileUri,
130+
});
131+
this.downloads.delete(source);
132+
downloadHandle.resolve(
133+
ResourceFetcherUtils.removeFilePrefix(downloadHandle.fileUri)
134+
);
135+
}
136+
137+
protected async cancel(source: ResourceSource): Promise<void> {
138+
const downloadHandle = this.downloads.get(source)!;
139+
await downloadHandle.downloadResumable.cancelAsync();
140+
this.downloads.delete(source);
141+
downloadHandle.reject(
142+
new RnExecutorchError(
143+
RnExecutorchErrorCode.DownloadInterrupted,
144+
'Download was canceled.'
145+
)
146+
);
147+
}
148+
149+
/**
150+
* Reads the contents of a file as a string.
151+
* @param path - Absolute file path or file URI to read.
152+
* @returns A promise that resolves to the file contents as a string.
153+
*/
154+
async readAsString(path: string): Promise<string> {
155+
const uri = path.startsWith('file://') ? path : `file://${path}`;
156+
return readAsStringAsync(uri);
157+
}
158+
159+
/**
160+
* Lists all the downloaded files used by React Native ExecuTorch.
161+
* @returns A promise that resolves to an array of URIs for all the downloaded files.
162+
*/
163+
async listDownloadedFiles(): Promise<string[]> {
164+
const files = await readDirectoryAsync(RNEDirectory);
165+
return files.map((file: string) => `${RNEDirectory}${file}`);
166+
}
167+
168+
/**
169+
* Deletes downloaded resources from the local filesystem.
170+
* @param sources - The resource identifiers used when calling `fetch`.
171+
* @returns A promise that resolves once all specified resources have been removed.
172+
*/
173+
async deleteResources(...sources: ResourceSource[]): Promise<void> {
174+
for (const source of sources) {
175+
const filename = ResourceFetcherUtils.getFilenameFromUri(
176+
source as string
177+
);
178+
const fileUri = `${RNEDirectory}${filename}`;
179+
if (await ResourceFetcherUtils.checkFileExists(fileUri)) {
180+
await deleteAsync(fileUri);
181+
}
182+
}
183+
}
184+
185+
/**
186+
* Fetches the total size of remote files. Works only for remote files.
187+
* @param sources - The resource identifiers (URLs).
188+
* @returns A promise that resolves to the combined size of files in bytes.
189+
*/
190+
async getFilesTotalSize(...sources: ResourceSource[]): Promise<number> {
191+
return (await ResourceFetcherUtils.getFilesSizes(sources)).totalLength;
192+
}
193+
}
194+
195+
export const ExpoResourceFetcher = new ExpoResourceFetcherClass();
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { RNEDirectory } from './constants/directories';
2+
import {
3+
ResourceSource,
4+
Logger,
5+
ResourceFetcherUtils as CoreUtils,
6+
HTTP_CODE,
7+
DownloadStatus,
8+
SourceType,
9+
RnExecutorchError,
10+
RnExecutorchErrorCode,
11+
} from 'react-native-executorch';
12+
import { Asset } from 'expo-asset';
13+
14+
/**
15+
* @internal
16+
*/
17+
import { getInfoAsync, makeDirectoryAsync } from 'expo-file-system/legacy';
18+
19+
export { HTTP_CODE, DownloadStatus, SourceType };
20+
21+
/**
22+
* Utility functions for fetching and managing resources.
23+
* @category Utilities - General
24+
*/
25+
export namespace ResourceFetcherUtils {
26+
export const removeFilePrefix = CoreUtils.removeFilePrefix;
27+
export const hashObject = CoreUtils.hashObject;
28+
export const calculateDownloadProgress = CoreUtils.calculateDownloadProgress;
29+
export const triggerHuggingFaceDownloadCounter =
30+
CoreUtils.triggerHuggingFaceDownloadCounter;
31+
export const triggerDownloadEvent = CoreUtils.triggerDownloadEvent;
32+
export const getFilenameFromUri = CoreUtils.getFilenameFromUri;
33+
34+
export function getType(source: ResourceSource): SourceType {
35+
if (typeof source === 'object') {
36+
return SourceType.OBJECT;
37+
} else if (typeof source === 'number') {
38+
const uri = Asset.fromModule(source).uri;
39+
if (uri.startsWith('http')) {
40+
return SourceType.DEV_MODE_FILE;
41+
}
42+
return SourceType.RELEASE_MODE_FILE;
43+
}
44+
// typeof source == 'string'
45+
if (source.startsWith('file://')) {
46+
return SourceType.LOCAL_FILE;
47+
}
48+
return SourceType.REMOTE_FILE;
49+
}
50+
51+
export async function getFilesSizes(sources: ResourceSource[]) {
52+
const results: Array<{
53+
source: ResourceSource;
54+
type: SourceType;
55+
length: number;
56+
previousFilesTotalLength: number;
57+
}> = [];
58+
let totalLength = 0;
59+
60+
for (const source of sources) {
61+
const type = ResourceFetcherUtils.getType(source);
62+
let length = 0;
63+
64+
if (type === SourceType.REMOTE_FILE && typeof source === 'string') {
65+
try {
66+
const response = await fetch(source, { method: 'HEAD' });
67+
if (!response.ok) {
68+
Logger.warn(
69+
`Failed to fetch HEAD for ${source}: ${response.status}`
70+
);
71+
continue;
72+
}
73+
74+
const contentLength = response.headers.get('content-length');
75+
if (!contentLength) {
76+
Logger.warn(`No content-length header for ${source}`);
77+
}
78+
79+
length = contentLength ? parseInt(contentLength, 10) : 0;
80+
} catch (error) {
81+
Logger.warn(`Error fetching HEAD for ${source}:`, error);
82+
continue;
83+
}
84+
}
85+
86+
const previousFilesTotalLength = totalLength;
87+
totalLength += length;
88+
results.push({ source, type, length, previousFilesTotalLength });
89+
}
90+
91+
return { results, totalLength };
92+
}
93+
94+
export async function createDirectoryIfNoExists() {
95+
if (!(await checkFileExists(RNEDirectory))) {
96+
try {
97+
await makeDirectoryAsync(RNEDirectory, { intermediates: true });
98+
} catch (error) {
99+
throw new RnExecutorchError(
100+
RnExecutorchErrorCode.FileWriteFailed,
101+
`Failed to create directory at ${RNEDirectory}`,
102+
error
103+
);
104+
}
105+
}
106+
}
107+
108+
export async function checkFileExists(fileUri: string) {
109+
const fileInfo = await getInfoAsync(fileUri);
110+
return fileInfo.exists;
111+
}
112+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { documentDirectory } from 'expo-file-system/legacy';
2+
3+
export const RNEDirectory = `${documentDirectory}react-native-executorch/`;

0 commit comments

Comments
 (0)