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