1import UUID from 'uuid-js'; 2import { Platform } from 'react-native'; 3import { EventEmitter, Subscription, UnavailabilityError } from '@unimodules/core'; 4 5import ExponentFileSystem from './ExponentFileSystem'; 6 7import { 8 DownloadOptions, 9 DownloadResult, 10 DownloadProgressCallback, 11 DownloadProgressData, 12 DownloadPauseState, 13 FileInfo, 14 EncodingType, 15 ReadingOptions, 16 WritingOptions, 17 ProgressEvent, 18} from './FileSystem.types'; 19 20if (!ExponentFileSystem) { 21 console.warn( 22 "No native ExponentFileSystem module found, are you sure the expo-file-system's module is linked properly?" 23 ); 24} 25// Prevent webpack from pruning this. 26const _unused = new EventEmitter(ExponentFileSystem); // eslint-disable-line 27 28export { 29 DownloadOptions, 30 DownloadResult, 31 DownloadProgressCallback, 32 DownloadProgressData, 33 DownloadPauseState, 34 FileInfo, 35 EncodingType, 36 ReadingOptions, 37 WritingOptions, 38 ProgressEvent, 39}; 40 41function normalizeEndingSlash(p: string | null): string | null { 42 if (p != null) { 43 return p.replace(/\/*$/, '') + '/'; 44 } 45 return null; 46} 47 48export const documentDirectory = normalizeEndingSlash(ExponentFileSystem.documentDirectory); 49export const cacheDirectory = normalizeEndingSlash(ExponentFileSystem.cacheDirectory); 50 51export const { bundledAssets, bundleDirectory } = ExponentFileSystem; 52 53export async function getInfoAsync( 54 fileUri: string, 55 options: { md5?: boolean; size?: boolean } = {} 56): Promise<FileInfo> { 57 if (!ExponentFileSystem.getInfoAsync) { 58 throw new UnavailabilityError('expo-file-system', 'getInfoAsync'); 59 } 60 return await ExponentFileSystem.getInfoAsync(fileUri, options); 61} 62 63export async function readAsStringAsync( 64 fileUri: string, 65 options?: ReadingOptions 66): Promise<string> { 67 if (!ExponentFileSystem.readAsStringAsync) { 68 throw new UnavailabilityError('expo-file-system', 'readAsStringAsync'); 69 } 70 return await ExponentFileSystem.readAsStringAsync(fileUri, options || {}); 71} 72 73export async function getContentUriAsync(fileUri: string): Promise<string> { 74 if (Platform.OS === 'android') { 75 if (!ExponentFileSystem.getContentUriAsync) { 76 throw new UnavailabilityError('expo-file-system', 'getContentUriAsync'); 77 } 78 return await ExponentFileSystem.getContentUriAsync(fileUri); 79 } else { 80 return new Promise(function(resolve, reject) { 81 resolve(fileUri); 82 }); 83 } 84} 85 86export async function writeAsStringAsync( 87 fileUri: string, 88 contents: string, 89 options: WritingOptions = {} 90): Promise<void> { 91 if (!ExponentFileSystem.writeAsStringAsync) { 92 throw new UnavailabilityError('expo-file-system', 'writeAsStringAsync'); 93 } 94 return await ExponentFileSystem.writeAsStringAsync(fileUri, contents, options); 95} 96 97export async function deleteAsync( 98 fileUri: string, 99 options: { idempotent?: boolean } = {} 100): Promise<void> { 101 if (!ExponentFileSystem.deleteAsync) { 102 throw new UnavailabilityError('expo-file-system', 'deleteAsync'); 103 } 104 return await ExponentFileSystem.deleteAsync(fileUri, options); 105} 106 107export async function deleteLegacyDocumentDirectoryAndroid(): Promise<void> { 108 if (Platform.OS !== 'android' || documentDirectory == null) { 109 return; 110 } 111 const legacyDocumentDirectory = `${documentDirectory}ExperienceData/`; 112 return await deleteAsync(legacyDocumentDirectory, { idempotent: true }); 113} 114 115export async function moveAsync(options: { from: string; to: string }): Promise<void> { 116 if (!ExponentFileSystem.moveAsync) { 117 throw new UnavailabilityError('expo-file-system', 'moveAsync'); 118 } 119 return await ExponentFileSystem.moveAsync(options); 120} 121 122export async function copyAsync(options: { from: string; to: string }): Promise<void> { 123 if (!ExponentFileSystem.copyAsync) { 124 throw new UnavailabilityError('expo-file-system', 'copyAsync'); 125 } 126 return await ExponentFileSystem.copyAsync(options); 127} 128 129export async function makeDirectoryAsync( 130 fileUri: string, 131 options: { intermediates?: boolean } = {} 132): Promise<void> { 133 if (!ExponentFileSystem.makeDirectoryAsync) { 134 throw new UnavailabilityError('expo-file-system', 'makeDirectoryAsync'); 135 } 136 return await ExponentFileSystem.makeDirectoryAsync(fileUri, options); 137} 138 139export async function readDirectoryAsync(fileUri: string): Promise<string[]> { 140 if (!ExponentFileSystem.readDirectoryAsync) { 141 throw new UnavailabilityError('expo-file-system', 'readDirectoryAsync'); 142 } 143 return await ExponentFileSystem.readDirectoryAsync(fileUri, {}); 144} 145 146export async function getFreeDiskStorageAsync(): Promise<number> { 147 if (!ExponentFileSystem.getFreeDiskStorageAsync) { 148 throw new UnavailabilityError('expo-file-system', 'getFreeDiskStorageAsync'); 149 } 150 return await ExponentFileSystem.getFreeDiskStorageAsync(); 151} 152 153export async function getTotalDiskCapacityAsync(): Promise<number> { 154 if (!ExponentFileSystem.getTotalDiskCapacityAsync) { 155 throw new UnavailabilityError('expo-file-system', 'getTotalDiskCapacityAsync'); 156 } 157 return await ExponentFileSystem.getTotalDiskCapacityAsync(); 158} 159 160export async function downloadAsync( 161 uri: string, 162 fileUri: string, 163 options: DownloadOptions = {} 164): Promise<DownloadResult> { 165 if (!ExponentFileSystem.downloadAsync) { 166 throw new UnavailabilityError('expo-file-system', 'downloadAsync'); 167 } 168 return await ExponentFileSystem.downloadAsync(uri, fileUri, options); 169} 170 171export function createDownloadResumable( 172 uri: string, 173 fileUri: string, 174 options?: DownloadOptions, 175 callback?: DownloadProgressCallback, 176 resumeData?: string 177): DownloadResumable { 178 return new DownloadResumable(uri, fileUri, options, callback, resumeData); 179} 180 181export class DownloadResumable { 182 _uuid: string; 183 _url: string; 184 _fileUri: string; 185 _options: DownloadOptions; 186 _resumeData?: string; 187 _callback?: DownloadProgressCallback; 188 _subscription?: Subscription | null; 189 _emitter: EventEmitter; 190 191 constructor( 192 url: string, 193 fileUri: string, 194 options: DownloadOptions = {}, 195 callback?: DownloadProgressCallback, 196 resumeData?: string 197 ) { 198 this._uuid = UUID.create(4).toString(); 199 this._url = url; 200 this._fileUri = fileUri; 201 this._options = options; 202 this._resumeData = resumeData; 203 this._callback = callback; 204 this._subscription = null; 205 this._emitter = new EventEmitter(ExponentFileSystem); 206 } 207 208 async downloadAsync(): Promise<DownloadResult | undefined> { 209 if (!ExponentFileSystem.downloadResumableStartAsync) { 210 throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync'); 211 } 212 this._addSubscription(); 213 return await ExponentFileSystem.downloadResumableStartAsync( 214 this._url, 215 this._fileUri, 216 this._uuid, 217 this._options, 218 this._resumeData 219 ); 220 } 221 222 async pauseAsync(): Promise<DownloadPauseState> { 223 if (!ExponentFileSystem.downloadResumablePauseAsync) { 224 throw new UnavailabilityError('expo-file-system', 'downloadResumablePauseAsync'); 225 } 226 const pauseResult = await ExponentFileSystem.downloadResumablePauseAsync(this._uuid); 227 this._removeSubscription(); 228 if (pauseResult) { 229 this._resumeData = pauseResult.resumeData; 230 return this.savable(); 231 } else { 232 throw new Error('Unable to generate a savable pause state'); 233 } 234 } 235 236 async resumeAsync(): Promise<DownloadResult | undefined> { 237 if (!ExponentFileSystem.downloadResumableStartAsync) { 238 throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync'); 239 } 240 this._addSubscription(); 241 return await ExponentFileSystem.downloadResumableStartAsync( 242 this._url, 243 this._fileUri, 244 this._uuid, 245 this._options, 246 this._resumeData 247 ); 248 } 249 250 savable(): DownloadPauseState { 251 return { 252 url: this._url, 253 fileUri: this._fileUri, 254 options: this._options, 255 resumeData: this._resumeData, 256 }; 257 } 258 259 _addSubscription(): void { 260 if (this._subscription) { 261 return; 262 } 263 this._subscription = this._emitter.addListener( 264 'Exponent.downloadProgress', 265 (event: ProgressEvent) => { 266 if (event.uuid === this._uuid) { 267 const callback = this._callback; 268 if (callback) { 269 callback(event.data); 270 } 271 } 272 } 273 ); 274 } 275 276 _removeSubscription(): void { 277 if (!this._subscription) { 278 return; 279 } 280 this._emitter.removeSubscription(this._subscription); 281 this._subscription = null; 282 } 283} 284