1import { EventEmitter, Subscription, UnavailabilityError } from 'expo-modules-core'; 2import { Platform } from 'react-native'; 3import { v4 as uuidv4 } from 'uuid'; 4 5import ExponentFileSystem from './ExponentFileSystem'; 6import { 7 DownloadOptions, 8 DownloadPauseState, 9 DownloadProgressCallback, 10 FileSystemNetworkTaskProgressCallback, 11 DownloadProgressData, 12 UploadProgressData, 13 DownloadResult, 14 EncodingType, 15 FileInfo, 16 FileSystemAcceptedUploadHttpMethod, 17 FileSystemDownloadResult, 18 FileSystemRequestDirectoryPermissionsResult, 19 FileSystemSessionType, 20 FileSystemUploadOptions, 21 FileSystemUploadResult, 22 FileSystemUploadType, 23 ProgressEvent, 24 ReadingOptions, 25 WritingOptions, 26} from './FileSystem.types'; 27 28if (!ExponentFileSystem) { 29 console.warn( 30 "No native ExponentFileSystem module found, are you sure the expo-file-system's module is linked properly?" 31 ); 32} 33// Prevent webpack from pruning this. 34const _unused = new EventEmitter(ExponentFileSystem); // eslint-disable-line 35 36export { 37 DownloadOptions, 38 DownloadPauseState, 39 DownloadProgressCallback, 40 DownloadProgressData, 41 DownloadResult, 42 EncodingType, 43 FileInfo, 44 FileSystemDownloadResult, 45 FileSystemRequestDirectoryPermissionsResult, 46 FileSystemAcceptedUploadHttpMethod, 47 FileSystemSessionType, 48 FileSystemUploadOptions, 49 FileSystemUploadResult, 50 FileSystemUploadType, 51 FileSystemNetworkTaskProgressCallback, 52 ReadingOptions, 53 WritingOptions, 54}; 55 56function normalizeEndingSlash(p: string | null): string | null { 57 if (p != null) { 58 return p.replace(/\/*$/, '') + '/'; 59 } 60 return null; 61} 62 63export const documentDirectory = normalizeEndingSlash(ExponentFileSystem.documentDirectory); 64export const cacheDirectory = normalizeEndingSlash(ExponentFileSystem.cacheDirectory); 65 66export const { bundledAssets, bundleDirectory } = ExponentFileSystem; 67 68export async function getInfoAsync( 69 fileUri: string, 70 options: { md5?: boolean; size?: boolean } = {} 71): Promise<FileInfo> { 72 if (!ExponentFileSystem.getInfoAsync) { 73 throw new UnavailabilityError('expo-file-system', 'getInfoAsync'); 74 } 75 return await ExponentFileSystem.getInfoAsync(fileUri, options); 76} 77 78export async function readAsStringAsync( 79 fileUri: string, 80 options?: ReadingOptions 81): Promise<string> { 82 if (!ExponentFileSystem.readAsStringAsync) { 83 throw new UnavailabilityError('expo-file-system', 'readAsStringAsync'); 84 } 85 return await ExponentFileSystem.readAsStringAsync(fileUri, options || {}); 86} 87 88export async function getContentUriAsync(fileUri: string): Promise<string> { 89 if (Platform.OS === 'android') { 90 if (!ExponentFileSystem.getContentUriAsync) { 91 throw new UnavailabilityError('expo-file-system', 'getContentUriAsync'); 92 } 93 return await ExponentFileSystem.getContentUriAsync(fileUri); 94 } else { 95 return new Promise(function (resolve, reject) { 96 resolve(fileUri); 97 }); 98 } 99} 100 101export async function writeAsStringAsync( 102 fileUri: string, 103 contents: string, 104 options: WritingOptions = {} 105): Promise<void> { 106 if (!ExponentFileSystem.writeAsStringAsync) { 107 throw new UnavailabilityError('expo-file-system', 'writeAsStringAsync'); 108 } 109 return await ExponentFileSystem.writeAsStringAsync(fileUri, contents, options); 110} 111 112export async function deleteAsync( 113 fileUri: string, 114 options: { idempotent?: boolean } = {} 115): Promise<void> { 116 if (!ExponentFileSystem.deleteAsync) { 117 throw new UnavailabilityError('expo-file-system', 'deleteAsync'); 118 } 119 return await ExponentFileSystem.deleteAsync(fileUri, options); 120} 121 122export async function deleteLegacyDocumentDirectoryAndroid(): Promise<void> { 123 if (Platform.OS !== 'android' || documentDirectory == null) { 124 return; 125 } 126 const legacyDocumentDirectory = `${documentDirectory}ExperienceData/`; 127 return await deleteAsync(legacyDocumentDirectory, { idempotent: true }); 128} 129 130export async function moveAsync(options: { from: string; to: string }): Promise<void> { 131 if (!ExponentFileSystem.moveAsync) { 132 throw new UnavailabilityError('expo-file-system', 'moveAsync'); 133 } 134 return await ExponentFileSystem.moveAsync(options); 135} 136 137export async function copyAsync(options: { from: string; to: string }): Promise<void> { 138 if (!ExponentFileSystem.copyAsync) { 139 throw new UnavailabilityError('expo-file-system', 'copyAsync'); 140 } 141 return await ExponentFileSystem.copyAsync(options); 142} 143 144export async function makeDirectoryAsync( 145 fileUri: string, 146 options: { intermediates?: boolean } = {} 147): Promise<void> { 148 if (!ExponentFileSystem.makeDirectoryAsync) { 149 throw new UnavailabilityError('expo-file-system', 'makeDirectoryAsync'); 150 } 151 return await ExponentFileSystem.makeDirectoryAsync(fileUri, options); 152} 153 154export async function readDirectoryAsync(fileUri: string): Promise<string[]> { 155 if (!ExponentFileSystem.readDirectoryAsync) { 156 throw new UnavailabilityError('expo-file-system', 'readDirectoryAsync'); 157 } 158 return await ExponentFileSystem.readDirectoryAsync(fileUri, {}); 159} 160 161export async function getFreeDiskStorageAsync(): Promise<number> { 162 if (!ExponentFileSystem.getFreeDiskStorageAsync) { 163 throw new UnavailabilityError('expo-file-system', 'getFreeDiskStorageAsync'); 164 } 165 return await ExponentFileSystem.getFreeDiskStorageAsync(); 166} 167 168export async function getTotalDiskCapacityAsync(): Promise<number> { 169 if (!ExponentFileSystem.getTotalDiskCapacityAsync) { 170 throw new UnavailabilityError('expo-file-system', 'getTotalDiskCapacityAsync'); 171 } 172 return await ExponentFileSystem.getTotalDiskCapacityAsync(); 173} 174 175export async function downloadAsync( 176 uri: string, 177 fileUri: string, 178 options: DownloadOptions = {} 179): Promise<FileSystemDownloadResult> { 180 if (!ExponentFileSystem.downloadAsync) { 181 throw new UnavailabilityError('expo-file-system', 'downloadAsync'); 182 } 183 184 return await ExponentFileSystem.downloadAsync(uri, fileUri, { 185 sessionType: FileSystemSessionType.BACKGROUND, 186 ...options, 187 }); 188} 189 190export async function uploadAsync( 191 url: string, 192 fileUri: string, 193 options: FileSystemUploadOptions = {} 194): Promise<FileSystemUploadResult> { 195 if (!ExponentFileSystem.uploadAsync) { 196 throw new UnavailabilityError('expo-file-system', 'uploadAsync'); 197 } 198 199 return await ExponentFileSystem.uploadAsync(url, fileUri, { 200 sessionType: FileSystemSessionType.BACKGROUND, 201 uploadType: FileSystemUploadType.BINARY_CONTENT, 202 ...options, 203 httpMethod: (options.httpMethod || 'POST').toUpperCase(), 204 }); 205} 206 207export function createDownloadResumable( 208 uri: string, 209 fileUri: string, 210 options?: DownloadOptions, 211 callback?: FileSystemNetworkTaskProgressCallback<DownloadProgressData>, 212 resumeData?: string 213): DownloadResumable { 214 return new DownloadResumable(uri, fileUri, options, callback, resumeData); 215} 216 217export function createUploadTask( 218 url: string, 219 fileUri: string, 220 options?: FileSystemUploadOptions, 221 callback?: FileSystemNetworkTaskProgressCallback<UploadProgressData> 222): UploadTask { 223 return new UploadTask(url, fileUri, options, callback); 224} 225 226export abstract class FileSystemCancellableNetworkTask< 227 T extends DownloadProgressData | UploadProgressData 228> { 229 private _uuid = uuidv4(); 230 protected taskWasCanceled = false; 231 private emitter = new EventEmitter(ExponentFileSystem); 232 private subscription?: Subscription | null; 233 234 public async cancelAsync(): Promise<void> { 235 if (!ExponentFileSystem.networkTaskCancelAsync) { 236 throw new UnavailabilityError('expo-file-system', 'networkTaskCancelAsync'); 237 } 238 239 this.removeSubscription(); 240 this.taskWasCanceled = true; 241 return await ExponentFileSystem.networkTaskCancelAsync(this.uuid); 242 } 243 244 protected isTaskCancelled(): boolean { 245 if (this.taskWasCanceled) { 246 console.warn('This task was already canceled.'); 247 return true; 248 } 249 250 return false; 251 } 252 253 protected get uuid(): string { 254 return this._uuid; 255 } 256 257 protected abstract getEventName(): string; 258 259 protected abstract getCallback(): FileSystemNetworkTaskProgressCallback<T> | undefined; 260 261 protected addSubscription() { 262 if (this.subscription) { 263 return; 264 } 265 266 this.subscription = this.emitter.addListener(this.getEventName(), (event: ProgressEvent<T>) => { 267 if (event.uuid === this.uuid) { 268 const callback = this.getCallback(); 269 if (callback) { 270 callback(event.data); 271 } 272 } 273 }); 274 } 275 276 protected removeSubscription() { 277 if (!this.subscription) { 278 return; 279 } 280 this.emitter.removeSubscription(this.subscription); 281 this.subscription = null; 282 } 283} 284 285export class UploadTask extends FileSystemCancellableNetworkTask<UploadProgressData> { 286 private options: FileSystemUploadOptions; 287 288 constructor( 289 private url: string, 290 private fileUri: string, 291 options?: FileSystemUploadOptions, 292 private callback?: FileSystemNetworkTaskProgressCallback<UploadProgressData> 293 ) { 294 super(); 295 296 const httpMethod = (options?.httpMethod?.toUpperCase || 297 'POST') as FileSystemAcceptedUploadHttpMethod; 298 299 this.options = { 300 sessionType: FileSystemSessionType.BACKGROUND, 301 uploadType: FileSystemUploadType.BINARY_CONTENT, 302 ...options, 303 httpMethod, 304 }; 305 } 306 307 protected getEventName(): string { 308 return 'expo-file-system.uploadProgress'; 309 } 310 protected getCallback(): FileSystemNetworkTaskProgressCallback<UploadProgressData> | undefined { 311 return this.callback; 312 } 313 314 public async uploadAsync(): Promise<FileSystemUploadResult | undefined> { 315 if (!ExponentFileSystem.uploadTaskStartAsync) { 316 throw new UnavailabilityError('expo-file-system', 'uploadTaskStartAsync'); 317 } 318 319 if (this.isTaskCancelled()) { 320 return; 321 } 322 323 this.addSubscription(); 324 const result = await ExponentFileSystem.uploadTaskStartAsync( 325 this.url, 326 this.fileUri, 327 this.uuid, 328 this.options 329 ); 330 this.removeSubscription(); 331 332 return result; 333 } 334} 335 336export class DownloadResumable extends FileSystemCancellableNetworkTask<DownloadProgressData> { 337 constructor( 338 private url: string, 339 private _fileUri: string, 340 private options: DownloadOptions = {}, 341 private callback?: FileSystemNetworkTaskProgressCallback<DownloadProgressData>, 342 private resumeData?: string 343 ) { 344 super(); 345 } 346 347 public get fileUri(): string { 348 return this._fileUri; 349 } 350 351 protected getEventName(): string { 352 return 'expo-file-system.downloadProgress'; 353 } 354 355 protected getCallback(): FileSystemNetworkTaskProgressCallback<DownloadProgressData> | undefined { 356 return this.callback; 357 } 358 359 async downloadAsync(): Promise<FileSystemDownloadResult | undefined> { 360 if (!ExponentFileSystem.downloadResumableStartAsync) { 361 throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync'); 362 } 363 364 if (this.isTaskCancelled()) { 365 return; 366 } 367 368 this.addSubscription(); 369 return await ExponentFileSystem.downloadResumableStartAsync( 370 this.url, 371 this._fileUri, 372 this.uuid, 373 this.options, 374 this.resumeData 375 ); 376 } 377 378 async pauseAsync(): Promise<DownloadPauseState> { 379 if (!ExponentFileSystem.downloadResumablePauseAsync) { 380 throw new UnavailabilityError('expo-file-system', 'downloadResumablePauseAsync'); 381 } 382 383 if (this.isTaskCancelled()) { 384 return { 385 fileUri: this._fileUri, 386 options: this.options, 387 url: this.url, 388 }; 389 } 390 391 const pauseResult = await ExponentFileSystem.downloadResumablePauseAsync(this.uuid); 392 this.removeSubscription(); 393 if (pauseResult) { 394 this.resumeData = pauseResult.resumeData; 395 return this.savable(); 396 } else { 397 throw new Error('Unable to generate a savable pause state'); 398 } 399 } 400 401 async resumeAsync(): Promise<FileSystemDownloadResult | undefined> { 402 if (!ExponentFileSystem.downloadResumableStartAsync) { 403 throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync'); 404 } 405 406 if (this.isTaskCancelled()) { 407 return; 408 } 409 410 this.addSubscription(); 411 return await ExponentFileSystem.downloadResumableStartAsync( 412 this.url, 413 this.fileUri, 414 this.uuid, 415 this.options, 416 this.resumeData 417 ); 418 } 419 420 savable(): DownloadPauseState { 421 return { 422 url: this.url, 423 fileUri: this.fileUri, 424 options: this.options, 425 resumeData: this.resumeData, 426 }; 427 } 428} 429 430const baseReadAsStringAsync = readAsStringAsync; 431const baseWriteAsStringAsync = writeAsStringAsync; 432const baseDeleteAsync = deleteAsync; 433const baseMoveAsync = moveAsync; 434const baseCopyAsync = copyAsync; 435/** 436 * Android only 437 */ 438export namespace StorageAccessFramework { 439 export function getUriForDirectoryInRoot(folderName: string) { 440 return `content://com.android.externalstorage.documents/tree/primary:${folderName}/document/primary:${folderName}`; 441 } 442 443 export async function requestDirectoryPermissionsAsync( 444 initialFileUrl: string | null = null 445 ): Promise<FileSystemRequestDirectoryPermissionsResult> { 446 if (!ExponentFileSystem.requestDirectoryPermissionsAsync) { 447 throw new UnavailabilityError( 448 'expo-file-system', 449 'StorageAccessFramework.requestDirectoryPermissionsAsync' 450 ); 451 } 452 453 return await ExponentFileSystem.requestDirectoryPermissionsAsync(initialFileUrl); 454 } 455 456 export async function readDirectoryAsync(dirUri: string): Promise<string[]> { 457 if (!ExponentFileSystem.readSAFDirectoryAsync) { 458 throw new UnavailabilityError( 459 'expo-file-system', 460 'StorageAccessFramework.readDirectoryAsync' 461 ); 462 } 463 return await ExponentFileSystem.readSAFDirectoryAsync(dirUri, {}); 464 } 465 466 export async function makeDirectoryAsync(parentUri: string, dirName: string): Promise<string> { 467 if (!ExponentFileSystem.makeSAFDirectoryAsync) { 468 throw new UnavailabilityError( 469 'expo-file-system', 470 'StorageAccessFramework.makeDirectoryAsync' 471 ); 472 } 473 return await ExponentFileSystem.makeSAFDirectoryAsync(parentUri, dirName); 474 } 475 476 export async function createFileAsync( 477 parentUri: string, 478 fileName: string, 479 mimeType: string 480 ): Promise<string> { 481 if (!ExponentFileSystem.createSAFFileAsync) { 482 throw new UnavailabilityError('expo-file-system', 'StorageAccessFramework.createFileAsync'); 483 } 484 return await ExponentFileSystem.createSAFFileAsync(parentUri, fileName, mimeType); 485 } 486 487 export const writeAsStringAsync = baseWriteAsStringAsync; 488 export const readAsStringAsync = baseReadAsStringAsync; 489 export const deleteAsync = baseDeleteAsync; 490 export const moveAsync = baseMoveAsync; 491 export const copyAsync = baseCopyAsync; 492} 493