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