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 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 getFreeDiskStorageAsync(): Promise<number> {
139  if (!ExponentFileSystem.getFreeDiskStorageAsync) {
140    throw new UnavailabilityError('expo-file-system', 'getFreeDiskStorageAsync');
141  }
142  return await ExponentFileSystem.getFreeDiskStorageAsync();
143}
144
145export async function getTotalDiskCapacityAsync(): Promise<number> {
146  if (!ExponentFileSystem.getTotalDiskCapacityAsync) {
147    throw new UnavailabilityError('expo-file-system', 'getTotalDiskCapacityAsync');
148  }
149  return await ExponentFileSystem.getTotalDiskCapacityAsync();
150}
151
152export async function downloadAsync(
153  uri: string,
154  fileUri: string,
155  options: DownloadOptions = {}
156): Promise<DownloadResult> {
157  if (!ExponentFileSystem.downloadAsync) {
158    throw new UnavailabilityError('expo-file-system', 'downloadAsync');
159  }
160  return await ExponentFileSystem.downloadAsync(uri, fileUri, options);
161}
162
163export function createDownloadResumable(
164  uri: string,
165  fileUri: string,
166  options?: DownloadOptions,
167  callback?: DownloadProgressCallback,
168  resumeData?: string
169): DownloadResumable {
170  return new DownloadResumable(uri, fileUri, options, callback, resumeData);
171}
172
173export class DownloadResumable {
174  _uuid: string;
175  _url: string;
176  _fileUri: string;
177  _options: DownloadOptions;
178  _resumeData?: string;
179  _callback?: DownloadProgressCallback;
180  _subscription?: Subscription | null;
181  _emitter: EventEmitter;
182
183  constructor(
184    url: string,
185    fileUri: string,
186    options: DownloadOptions = {},
187    callback?: DownloadProgressCallback,
188    resumeData?: string
189  ) {
190    this._uuid = UUID.create(4).toString();
191    this._url = url;
192    this._fileUri = fileUri;
193    this._options = options;
194    this._resumeData = resumeData;
195    this._callback = callback;
196    this._subscription = null;
197    this._emitter = new EventEmitter(ExponentFileSystem);
198  }
199
200  async downloadAsync(): Promise<DownloadResult | undefined> {
201    if (!ExponentFileSystem.downloadResumableStartAsync) {
202      throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
203    }
204    this._addSubscription();
205    return await ExponentFileSystem.downloadResumableStartAsync(
206      this._url,
207      this._fileUri,
208      this._uuid,
209      this._options,
210      this._resumeData
211    );
212  }
213
214  async pauseAsync(): Promise<DownloadPauseState> {
215    if (!ExponentFileSystem.downloadResumablePauseAsync) {
216      throw new UnavailabilityError('expo-file-system', 'downloadResumablePauseAsync');
217    }
218    const pauseResult = await ExponentFileSystem.downloadResumablePauseAsync(this._uuid);
219    this._removeSubscription();
220    if (pauseResult) {
221      this._resumeData = pauseResult.resumeData;
222      return this.savable();
223    } else {
224      throw new Error('Unable to generate a savable pause state');
225    }
226  }
227
228  async resumeAsync(): Promise<DownloadResult | undefined> {
229    if (!ExponentFileSystem.downloadResumableStartAsync) {
230      throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
231    }
232    this._addSubscription();
233    return await ExponentFileSystem.downloadResumableStartAsync(
234      this._url,
235      this._fileUri,
236      this._uuid,
237      this._options,
238      this._resumeData
239    );
240  }
241
242  savable(): DownloadPauseState {
243    return {
244      url: this._url,
245      fileUri: this._fileUri,
246      options: this._options,
247      resumeData: this._resumeData,
248    };
249  }
250
251  _addSubscription(): void {
252    if (this._subscription) {
253      return;
254    }
255    this._subscription = this._emitter.addListener(
256      'Exponent.downloadProgress',
257      (event: ProgressEvent) => {
258        if (event.uuid === this._uuid) {
259          const callback = this._callback;
260          if (callback) {
261            callback(event.data);
262          }
263        }
264      }
265    );
266  }
267
268  _removeSubscription(): void {
269    if (!this._subscription) {
270      return;
271    }
272    this._emitter.removeSubscription(this._subscription);
273    this._subscription = null;
274  }
275}
276