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