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