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