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