1import { UnavailabilityError } from '@unimodules/core'; 2import { EventEmitter, Subscription } from '@unimodules/core'; 3import UUID from 'uuid-js'; 4import ExponentFileSystem from './ExponentFileSystem'; 5import { Platform } from 'react-native'; 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); 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 moveAsync(options: { from: string; to: string }): Promise<void> { 108 if (!ExponentFileSystem.moveAsync) { 109 throw new UnavailabilityError('expo-file-system', 'moveAsync'); 110 } 111 return await ExponentFileSystem.moveAsync(options); 112} 113 114export async function copyAsync(options: { from: string; to: string }): Promise<void> { 115 if (!ExponentFileSystem.copyAsync) { 116 throw new UnavailabilityError('expo-file-system', 'copyAsync'); 117 } 118 return await ExponentFileSystem.copyAsync(options); 119} 120 121export async function makeDirectoryAsync( 122 fileUri: string, 123 options: { intermediates?: boolean } = {} 124): Promise<void> { 125 if (!ExponentFileSystem.makeDirectoryAsync) { 126 throw new UnavailabilityError('expo-file-system', 'makeDirectoryAsync'); 127 } 128 return await ExponentFileSystem.makeDirectoryAsync(fileUri, options); 129} 130 131export async function readDirectoryAsync(fileUri: string): Promise<string[]> { 132 if (!ExponentFileSystem.readDirectoryAsync) { 133 throw new UnavailabilityError('expo-file-system', 'readDirectoryAsync'); 134 } 135 return await ExponentFileSystem.readDirectoryAsync(fileUri, {}); 136} 137 138export async function downloadAsync( 139 uri: string, 140 fileUri: string, 141 options: DownloadOptions = {} 142): Promise<DownloadResult> { 143 if (!ExponentFileSystem.downloadAsync) { 144 throw new UnavailabilityError('expo-file-system', 'downloadAsync'); 145 } 146 return await ExponentFileSystem.downloadAsync(uri, fileUri, options); 147} 148 149export function createDownloadResumable( 150 uri: string, 151 fileUri: string, 152 options?: DownloadOptions, 153 callback?: DownloadProgressCallback, 154 resumeData?: string 155): DownloadResumable { 156 return new DownloadResumable(uri, fileUri, options, callback, resumeData); 157} 158 159export class DownloadResumable { 160 _uuid: string; 161 _url: string; 162 _fileUri: string; 163 _options: DownloadOptions; 164 _resumeData?: string; 165 _callback?: DownloadProgressCallback; 166 _subscription?: Subscription | null; 167 _emitter: EventEmitter; 168 169 constructor( 170 url: string, 171 fileUri: string, 172 options: DownloadOptions = {}, 173 callback?: DownloadProgressCallback, 174 resumeData?: string 175 ) { 176 this._uuid = UUID.create(4).toString(); 177 this._url = url; 178 this._fileUri = fileUri; 179 this._options = options; 180 this._resumeData = resumeData; 181 this._callback = callback; 182 this._subscription = null; 183 this._emitter = new EventEmitter(ExponentFileSystem); 184 } 185 186 async downloadAsync(): Promise<DownloadResult | undefined> { 187 if (!ExponentFileSystem.downloadResumableStartAsync) { 188 throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync'); 189 } 190 this._addSubscription(); 191 return await ExponentFileSystem.downloadResumableStartAsync( 192 this._url, 193 this._fileUri, 194 this._uuid, 195 this._options, 196 this._resumeData 197 ); 198 } 199 200 async pauseAsync(): Promise<DownloadPauseState> { 201 if (!ExponentFileSystem.downloadResumablePauseAsync) { 202 throw new UnavailabilityError('expo-file-system', 'downloadResumablePauseAsync'); 203 } 204 const pauseResult = await ExponentFileSystem.downloadResumablePauseAsync(this._uuid); 205 this._removeSubscription(); 206 if (pauseResult) { 207 this._resumeData = pauseResult.resumeData; 208 return this.savable(); 209 } else { 210 throw new Error('Unable to generate a savable pause state'); 211 } 212 } 213 214 async resumeAsync(): Promise<DownloadResult | undefined> { 215 if (!ExponentFileSystem.downloadResumableStartAsync) { 216 throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync'); 217 } 218 this._addSubscription(); 219 return await ExponentFileSystem.downloadResumableStartAsync( 220 this._url, 221 this._fileUri, 222 this._uuid, 223 this._options, 224 this._resumeData 225 ); 226 } 227 228 savable(): DownloadPauseState { 229 return { 230 url: this._url, 231 fileUri: this._fileUri, 232 options: this._options, 233 resumeData: this._resumeData, 234 }; 235 } 236 237 _addSubscription(): void { 238 if (this._subscription) { 239 return; 240 } 241 this._subscription = this._emitter.addListener( 242 'Exponent.downloadProgress', 243 (event: ProgressEvent) => { 244 if (event.uuid === this._uuid) { 245 const callback = this._callback; 246 if (callback) { 247 callback(event.data); 248 } 249 } 250 } 251 ); 252 } 253 254 _removeSubscription(): void { 255 if (!this._subscription) { 256 return; 257 } 258 this._emitter.removeSubscription(this._subscription); 259 this._subscription = null; 260 } 261} 262