1import AsyncStorage from '@react-native-async-storage/async-storage';
2import { Asset } from 'expo-asset';
3import * as FileSystem from 'expo-file-system';
4// import * as Progress from 'expo-progress';
5import type {
6  DownloadProgressData,
7  DownloadResumable,
8  FileSystemNetworkTaskProgressCallback,
9  UploadProgressData,
10  UploadTask,
11} from 'expo-file-system';
12import React from 'react';
13import { Alert, ScrollView, Text, Platform } from 'react-native';
14
15import HeadingText from '../components/HeadingText';
16import ListButton from '../components/ListButton';
17import SimpleActionDemo from '../components/SimpleActionDemo';
18
19const { StorageAccessFramework } = FileSystem;
20
21interface State {
22  downloadProgress: number;
23  uploadProgress: number;
24  permittedURI: string | null;
25  createdFileURI: string | null;
26}
27
28export default class FileSystemScreen extends React.Component<object, State> {
29  static navigationOptions = {
30    title: 'FileSystem',
31  };
32
33  readonly state: State = {
34    downloadProgress: 0,
35    uploadProgress: 0,
36    permittedURI: null,
37    createdFileURI: null,
38  };
39
40  download?: DownloadResumable;
41  upload?: UploadTask;
42
43  _download = async () => {
44    const url = 'http://ipv4.download.thinkbroadband.com/256KB.zip';
45    await FileSystem.downloadAsync(url, FileSystem.documentDirectory + '256KB.zip');
46    alert('Download complete!');
47  };
48
49  _startDownloading = async () => {
50    const url = 'http://ipv4.download.thinkbroadband.com/5MB.zip';
51    const fileUri = FileSystem.documentDirectory + '5MB.zip';
52    const callback: FileSystemNetworkTaskProgressCallback<DownloadProgressData> = (
53      downloadProgress
54    ) => {
55      const progress =
56        downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite;
57      this.setState({
58        downloadProgress: progress,
59      });
60    };
61    const options = { md5: true };
62    this.download = FileSystem.createDownloadResumable(url, fileUri, options, callback);
63
64    try {
65      const result = await this.download.downloadAsync();
66      if (result) {
67        this._downloadComplete();
68      }
69    } catch (e) {
70      console.log(e);
71    }
72  };
73
74  _pause = async () => {
75    if (!this.download) {
76      alert('Initiate a download first!');
77      return;
78    }
79    try {
80      const downloadSnapshot = await this.download.pauseAsync();
81      await AsyncStorage.setItem('pausedDownload', JSON.stringify(downloadSnapshot));
82      alert('Download paused...');
83    } catch (e) {
84      console.log(e);
85    }
86  };
87
88  _resume = async () => {
89    try {
90      if (this.download) {
91        const result = await this.download.resumeAsync();
92        if (result) {
93          this._downloadComplete();
94        }
95      } else {
96        this._fetchDownload();
97      }
98    } catch (e) {
99      console.log(e);
100    }
101  };
102
103  _cancel = async () => {
104    if (!this.download) {
105      alert('Initiate a download first!');
106      return;
107    }
108
109    try {
110      await this.download.cancelAsync();
111      delete this.download;
112      await AsyncStorage.removeItem('pausedDownload');
113      this.setState({
114        downloadProgress: 0,
115      });
116    } catch (e) {
117      console.log(e);
118    }
119  };
120
121  _downloadComplete = () => {
122    if (this.state.downloadProgress !== 1) {
123      this.setState({
124        downloadProgress: 1,
125      });
126    }
127    alert('Download complete!');
128  };
129
130  _fetchDownload = async () => {
131    try {
132      const downloadJson = await AsyncStorage.getItem('pausedDownload');
133      if (downloadJson !== null) {
134        const downloadFromStore = JSON.parse(downloadJson);
135        const callback: FileSystemNetworkTaskProgressCallback<DownloadProgressData> = (
136          downloadProgress
137        ) => {
138          const progress =
139            downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite;
140          this.setState({
141            downloadProgress: progress,
142          });
143        };
144        this.download = new FileSystem.DownloadResumable(
145          downloadFromStore.url,
146          downloadFromStore.fileUri,
147          downloadFromStore.options,
148          callback,
149          downloadFromStore.resumeData
150        );
151        await this.download.resumeAsync();
152        if (this.state.downloadProgress === 1) {
153          alert('Download complete!');
154        }
155      } else {
156        alert('Initiate a download first!');
157        return;
158      }
159    } catch (e) {
160      console.log(e);
161    }
162  };
163
164  _upload = async () => {
165    try {
166      const fileUri = FileSystem.documentDirectory + '5MB.zip';
167      const downloadUrl = 'https://xcal1.vodafone.co.uk/5MB.zip';
168      await FileSystem.downloadAsync(downloadUrl, fileUri);
169
170      const callback: FileSystemNetworkTaskProgressCallback<UploadProgressData> = (
171        uploadProgress
172      ) => {
173        // intentionally use deprecated property to test warning
174        const progress = uploadProgress.totalByteSent / uploadProgress.totalBytesExpectedToSend;
175        this.setState({
176          uploadProgress: progress,
177        });
178      };
179      const uploadUrl = 'http://httpbin.org/post';
180      this.upload = FileSystem.createUploadTask(uploadUrl, fileUri, {}, callback);
181
182      await this.upload.uploadAsync();
183    } catch (e) {
184      console.log(e);
185    }
186  };
187
188  _getInfo = async () => {
189    if (!this.download) {
190      alert('Initiate a download first!');
191      return;
192    }
193    try {
194      const info = await FileSystem.getInfoAsync(this.download.fileUri);
195      Alert.alert('File Info:', JSON.stringify(info), [{ text: 'OK', onPress: () => {} }]);
196    } catch (e) {
197      console.log(e);
198    }
199  };
200
201  _readAsset = async () => {
202    const asset = Asset.fromModule(require('../../assets/index.html'));
203    await asset.downloadAsync();
204    try {
205      const result = await FileSystem.readAsStringAsync(asset.localUri!);
206      Alert.alert('Result', result);
207    } catch (e) {
208      Alert.alert('Error', e.message);
209    }
210  };
211
212  _getInfoAsset = async () => {
213    const asset = Asset.fromModule(require('../../assets/index.html'));
214    await asset.downloadAsync();
215    try {
216      const result = await FileSystem.getInfoAsync(asset.localUri!);
217      Alert.alert('Result', JSON.stringify(result, null, 2));
218    } catch (e) {
219      Alert.alert('Error', e.message);
220    }
221  };
222
223  _copyAndReadAsset = async () => {
224    const asset = Asset.fromModule(require('../../assets/index.html'));
225    await asset.downloadAsync();
226    const tmpFile = FileSystem.cacheDirectory + 'test.html';
227    try {
228      await FileSystem.copyAsync({ from: asset.localUri!, to: tmpFile });
229      const result = await FileSystem.readAsStringAsync(tmpFile);
230      Alert.alert('Result', result);
231    } catch (e) {
232      Alert.alert('Error', e.message);
233    }
234  };
235
236  _createUTF8Uri = async () => {
237    const folderName = '中文';
238    const folderUri = FileSystem.documentDirectory + folderName;
239
240    const dirInfo = await FileSystem.getInfoAsync(folderUri);
241    if (dirInfo.exists) {
242      await FileSystem.deleteAsync(folderUri);
243    }
244
245    await FileSystem.makeDirectoryAsync(folderUri);
246    const newDirInfo = await FileSystem.getInfoAsync(folderUri);
247
248    if (newDirInfo.exists && newDirInfo.isDirectory) {
249      alert(`${folderName} directory was successfully created`);
250    }
251  };
252
253  _alertFreeSpace = async () => {
254    const freeBytes = await FileSystem.getFreeDiskStorageAsync();
255    alert(`${Math.round(freeBytes / 1024 / 1024)} MB available`);
256  };
257
258  _askForDirPermissions = async () => {
259    const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
260    if (permissions.granted) {
261      const url = permissions.directoryUri;
262      this.setState({
263        permittedURI: url,
264      });
265      alert(`You selected: ${url}`);
266    }
267  };
268
269  _readSAFDirAsync = async () => {
270    return await StorageAccessFramework.readDirectoryAsync(this.state.permittedURI!);
271  };
272
273  _creatSAFFileAsync = async () => {
274    const createdFile = await StorageAccessFramework.createFileAsync(
275      // eslint-disable-next-line react/no-access-state-in-setstate
276      this.state.permittedURI!,
277      'test',
278      'text/plain'
279    );
280
281    this.setState({
282      createdFileURI: createdFile,
283    });
284
285    return createdFile;
286  };
287
288  _writeToSAFFileAsync = async () => {
289    await StorageAccessFramework.writeAsStringAsync(
290      this.state.createdFileURI!,
291      'Expo is awesome ������'
292    );
293
294    return 'Done ��';
295  };
296
297  _readSAFFileAsync = async () => {
298    return await StorageAccessFramework.readAsStringAsync(this.state.createdFileURI!);
299  };
300
301  _deleteSAFFileAsync = async () => {
302    await StorageAccessFramework.deleteAsync(this.state.createdFileURI!);
303
304    this.setState({
305      createdFileURI: null,
306    });
307  };
308
309  _copySAFFileToInternalStorageAsync = async () => {
310    const outputDir = FileSystem.cacheDirectory! + '/SAFTest';
311    await StorageAccessFramework.copyAsync({
312      from: this.state.createdFileURI!,
313      to: outputDir,
314    });
315
316    return await FileSystem.readDirectoryAsync(outputDir);
317  };
318
319  _moveSAFFileToInternalStorageAsync = async () => {
320    await StorageAccessFramework.moveAsync({
321      from: this.state.createdFileURI!,
322      to: FileSystem.cacheDirectory!,
323    });
324
325    this.setState({
326      createdFileURI: null,
327    });
328  };
329
330  render() {
331    return (
332      <ScrollView style={{ padding: 10 }}>
333        <ListButton onPress={this._download} title="Download file (512KB)" />
334        <ListButton onPress={this._startDownloading} title="Start Downloading file (5MB)" />
335        {this.state.downloadProgress ? (
336          <Text style={{ paddingVertical: 15 }}>
337            Download progress: {this.state.downloadProgress * 100}%
338          </Text>
339        ) : null}
340        {/* Add back progress bar once deprecation warnings from reanimated 2 are resolved */}
341        {/* <Progress.Bar style={styles.progress} isAnimated progress={this.state.downloadProgress} /> */}
342        <ListButton onPress={this._pause} title="Pause Download" />
343        <ListButton onPress={this._resume} title="Resume Download" />
344        <ListButton onPress={this._cancel} title="Cancel Download" />
345        <ListButton onPress={this._upload} title="Download & Upload file (5MB)" />
346        {this.state.uploadProgress ? (
347          <Text style={{ paddingVertical: 15 }}>
348            Upload progress: {this.state.uploadProgress * 100}%
349          </Text>
350        ) : null}
351        <ListButton onPress={this._getInfo} title="Get Info" />
352        <ListButton onPress={this._readAsset} title="Read Asset" />
353        <ListButton onPress={this._getInfoAsset} title="Get Info Asset" />
354        <ListButton onPress={this._copyAndReadAsset} title="Copy and Read Asset" />
355        <ListButton onPress={this._createUTF8Uri} title="Create UTF-8 uri" />
356        <ListButton onPress={this._alertFreeSpace} title="Alert free space" />
357        {Platform.OS === 'android' && (
358          <>
359            <HeadingText>Storage Access Framework</HeadingText>
360            <ListButton
361              onPress={this._askForDirPermissions}
362              title="Ask for directory permissions"
363            />
364            {this.state.permittedURI && (
365              <>
366                <SimpleActionDemo title="Read directory" action={this._readSAFDirAsync} />
367                <SimpleActionDemo title="Create a file" action={this._creatSAFFileAsync} />
368
369                {this.state.createdFileURI && (
370                  <>
371                    <SimpleActionDemo
372                      title="Write to created file"
373                      action={this._writeToSAFFileAsync}
374                    />
375                    <SimpleActionDemo
376                      title="Read from created file"
377                      action={this._readSAFFileAsync}
378                    />
379                    <ListButton title="Delete created file" onPress={this._deleteSAFFileAsync} />
380                    <SimpleActionDemo
381                      title="Copy file to internal storage"
382                      action={this._copySAFFileToInternalStorageAsync}
383                    />
384                    <ListButton
385                      title="Move file to internal storage"
386                      onPress={this._moveSAFFileToInternalStorageAsync}
387                    />
388                  </>
389                )}
390              </>
391            )}
392          </>
393        )}
394      </ScrollView>
395    );
396  }
397}
398