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