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