UNPKG

expo-file-system

Version:

Provides access to the local file system on the device.

393 lines (366 loc) 13.5 kB
import { uuid, type EventSubscription } from 'expo-modules-core'; import { Directory } from './Directory'; import ExpoFileSystem from './ExpoFileSystem'; import { FileMode, type PickFileOptions, type PickMultipleFilesOptions, type PickMultipleFilesResult, type PickSingleFileOptions, type PickSingleFileResult, } from './File.types'; import { type WatchEvent, type WatchOptions, type WatchSubscription, } from './FileSystemWatcher.types'; import { DownloadTask, UploadTask } from './NetworkTasks'; import { type DownloadOptions, type DownloadProgress, type DownloadTaskOptions, type UploadOptions, type UploadResult, } from './NetworkTasks.types'; import { Paths } from './Paths'; import { FileSystemWatcher } from './internal/FileSystemWatcher'; import { FileSystemReadableStreamSource, FileSystemWritableSink } from './internal/streams'; /** * Represents a file on the filesystem. * * A `File` instance can be created for any path, and does not need to exist on the filesystem during creation. * * The constructor accepts an array of strings that are joined to create the file URI. The first argument can also be a `Directory` instance (like `Paths.cache`) or a `File` instance (which creates a new reference to the same file). * @example * ```ts * const file = new File(Paths.cache, "subdirName", "file.txt"); * ``` */ export class File extends ExpoFileSystem.FileSystemFile implements Blob { /** * A static method that downloads a file from the network. * * On Android, the response body streams directly into the target file. If the download fails after * it starts, a partially written file may remain at the destination. On iOS, the download first * completes in a temporary location and the file is moved into place only after success, so no * file is left behind when the request fails. * * @param url - The URL of the file to download. * @param destination - The destination directory or file. If a directory is provided, the resulting filename will be determined based on the response headers. * @param options - Download options. When the destination already contains a file, the promise rejects with a `DestinationAlreadyExists` error unless `options.idempotent` is set to `true`. With `idempotent: true`, the download overwrites the existing file instead of failing. * * @returns A promise that resolves to the downloaded file. When the server responds with * a non-2xx HTTP status, the promise rejects with an `UnableToDownload` error whose * message includes the status code. No file is created in that scenario. * * @example * ```ts * const file = await File.downloadFileAsync("https://example.com/image.png", new Directory(Paths.document)); * ``` */ static downloadFileAsync: ( url: string, destination: Directory | File, options?: DownloadOptions ) => Promise<File>; /** * Opens the system file picker for selecting a single file. * * This overload requires `options.multipleFiles` to be `undefined` or `false`. * * @param options File picker options. */ static pickFileAsync(options?: PickSingleFileOptions): Promise<PickSingleFileResult>; /** * Opens the system file picker for selecting multiple files. * * This overload requires `options.multipleFiles` to be `true`. * * @param options File picker options. * * @example * ```ts * const result = await File.pickFileAsync({ * multipleFiles: true, * mimeTypes: ['image/*', 'application/pdf'], * }); * * if (!result.canceled) { * for (const file of result.result) { * console.log(file.uri); * } * } * ``` */ static pickFileAsync(options?: PickMultipleFilesOptions): Promise<PickMultipleFilesResult>; /** * A static method that opens a file picker to select a single file of specified type. On iOS, it returns a temporary copy of the file leaving the original file untouched. * * Selecting multiple files is not supported yet. * * @deprecated Use `pickFileAsync({initialUri, mimeTypes: mimeType})` instead. * @param initialUri An optional URI pointing to an initial folder on which the file picker is opened. * @param mimeType A mime type that is used to filter out files that can be picked out. * @returns A `File` instance or an array of `File` instances. */ static pickFileAsync(initialUri?: string, mimeType?: string): Promise<File | File[]>; static async pickFileAsync( initialUriOrOptions?: string | PickFileOptions, mimeType?: string ): Promise<File | File[] | PickSingleFileResult | PickMultipleFilesResult> { const { options, usingOldAPI } = parsePickFileOptions(initialUriOrOptions, mimeType); try { if (options.multipleFiles) { const files = await ExpoFileSystem.pickFileAsync(options); return { result: files.map((file) => new File(file.uri)), canceled: false }; } const file = await ExpoFileSystem.pickFileAsync(options); if (usingOldAPI) { return new File(file.uri); } return { result: new File(file.uri), canceled: false, }; } catch (e) { if (usingOldAPI) { throw e; } return { result: null, canceled: true, }; } } /** * Creates an instance of a file. It can be created for any path, and does not need to exist on the filesystem during creation. * * The constructor accepts an array of strings that are joined to create the file URI. The first argument can also be a `Directory` instance (like `Paths.cache`) or a `File` instance (which creates a new reference to the same file). * @param uris An array of: `file:///` string URIs, `File` instances, and `Directory` instances representing an arbitrary location on the file system. * @example * ```ts * const file = new File(Paths.cache, "subdirName", "file.txt"); * ``` */ constructor(...uris: (string | File | Directory)[]) { super(Paths.join(...uris)); this.validatePath(); } /* * Directory containing the file. */ get parentDirectory() { return new Directory(Paths.dirname(this.uri)); } /** * File extension. * @example '.png' */ get extension() { return Paths.extname(this.uri); } /** * File name. Includes the extension. */ get name() { return Paths.basename(this.uri); } readableStream() { return new ReadableStream(new FileSystemReadableStreamSource(super.open(FileMode.ReadOnly))); } writableStream() { return new WritableStream<Uint8Array>( new FileSystemWritableSink(super.open(FileMode.WriteOnly)) ); } async arrayBuffer(): Promise<ArrayBuffer> { const bytes = await this.bytes(); return bytes.buffer as ArrayBuffer; } async json(): Promise<any> { return JSON.parse(await this.text()); } async formData(): ReturnType<Response['formData']> { return new Response(await this.arrayBuffer(), { headers: this.type ? { 'Content-Type': this.type } : undefined, }).formData(); } stream(): ReadableStream<Uint8Array<ArrayBuffer>> { return this.readableStream(); } slice(start?: number, end?: number, contentType?: string): Blob { return new Blob([this.bytesSync().slice(start, end)], { type: contentType }); } /** * Uploads this file to a server and starts the request immediately. * * The promise resolves with the HTTP response metadata and body for any completed response, * including non-2xx status codes. It is rejected only when the file cannot be read, the * request fails, or the upload is cancelled. * * @param url The URL to upload the file to. * @param options Upload options. * @returns A promise that resolves to the upload result. */ upload(url: string, options?: UploadOptions): Promise<UploadResult> { return new UploadTask(this, url, options).uploadAsync(); } /** * Creates an upload task for this file without starting it. * * Call `uploadAsync()` on the returned task to start the upload. Use this when you need to * inspect task state, cancel the upload, or subscribe to progress manually. * * @param url The URL to upload the file to. * @param options Upload options. * @returns An upload task that can be started with `uploadAsync()`. * * @example * ```ts * const file = new File(Paths.document, 'photo.jpg'); * const task = file.createUploadTask('https://example.com/upload', { * uploadType: UploadType.MULTIPART, * onProgress: ({ bytesSent, totalBytes }) => { * console.log(`${bytesSent} / ${totalBytes}`); * }, * }); * * const result = await task.uploadAsync(); * ``` */ createUploadTask(url: string, options?: UploadOptions): UploadTask { return new UploadTask(this, url, options); } /** * Creates a download task without starting it. * * Call `downloadAsync()` on the returned task to start the download. Use this when you need * pause/resume support, task state, cancellation, or manual progress subscriptions. * * @param url The URL of the file to download. * @param destination The destination file or directory. If a directory is provided, the * resulting filename is determined from the response headers or URL. * @param options Download task options. * @returns A download task that can be started with `downloadAsync()`. * * @example * ```ts * const destination = new File(Paths.document, 'video.mp4'); * const task = File.createDownloadTask('https://example.com/video.mp4', destination, { * onProgress: ({ bytesWritten, totalBytes }) => { * console.log(`${bytesWritten} / ${totalBytes}`); * }, * }); * * const file = await task.downloadAsync(); * ``` */ static createDownloadTask( url: string, destination: File | Directory, options?: DownloadTaskOptions ): DownloadTask { return new DownloadTask(url, destination, options); } /** * Watches this file for changes on the filesystem. * * The watcher automatically stops when the file is deleted or renamed. To stop watching manually, * call `remove()` on the returned subscription. * * @param callback Invoked when a change is detected. Receives a `WatchEvent` describing what changed. * @param options Configuration for debouncing and filtering events. * @return A subscription handle. Call `remove()` to stop watching. * * @example * ```ts * const file = new File(Paths.cache, 'data.json'); * const subscription = file.watch((event) => { * console.log(`File ${event.type}`); * }); * * // Later, stop watching: * subscription.remove(); * ``` */ watch(callback: (event: WatchEvent<File>) => void, options?: WatchOptions): WatchSubscription { return new FileSystemWatcher<File>(this.uri, callback, options, (uri) => new File(uri)); } } function createAbortError(reason?: string): Error { const error = new Error(reason ?? 'The operation was aborted.'); error.name = 'AbortError'; return error; } // Cannot use `static` keyword in class declaration because of a runtime error. File.downloadFileAsync = async function downloadFileAsync( url: string, to: File | Directory, options?: DownloadOptions ) { const needsUuid = options?.onProgress || options?.signal; const downloadUuid = needsUuid ? uuid.v4() : undefined; let subscription: EventSubscription | undefined; let abortHandler: (() => void) | undefined; let lastProgress: DownloadProgress | undefined; try { if (options?.signal?.aborted) { throw createAbortError(options.signal.reason); } if (downloadUuid && options?.onProgress) { subscription = ExpoFileSystem.addListener('downloadProgress', (event) => { if (event.uuid === downloadUuid) { lastProgress = event.data; options.onProgress!(lastProgress); } }); } if (downloadUuid && options?.signal) { abortHandler = () => { ExpoFileSystem.cancelDownloadAsync(downloadUuid); }; options.signal.addEventListener('abort', abortHandler, { once: true }); } const outputURI = await ExpoFileSystem.downloadFileAsync(url, to, options, downloadUuid); const file = new File(outputURI); const fileSize = file.size ?? 0; if ( options?.onProgress && fileSize > 0 && (lastProgress?.bytesWritten !== fileSize || lastProgress?.totalBytes !== fileSize) ) { options.onProgress({ bytesWritten: fileSize, totalBytes: fileSize }); } return file; } catch (error: any) { if (options?.signal?.aborted) { throw createAbortError(options.signal.reason); } throw error; } finally { subscription?.remove(); if (abortHandler && options?.signal) { options.signal.removeEventListener('abort', abortHandler); } } }; /** * Used to parse different APIs merged together. * @hidden */ function parsePickFileOptions( initialUriOrOptions?: string | PickFileOptions, mimeType?: string ): { options: PickFileOptions; usingOldAPI: boolean } { if (typeof initialUriOrOptions === 'object') { return { options: initialUriOrOptions, usingOldAPI: false }; } return { options: { initialUri: initialUriOrOptions, mimeTypes: mimeType, multipleFiles: false, }, usingOldAPI: mimeType !== undefined || typeof initialUriOrOptions === 'string', }; }