xref: /MusicFree/src/pages/setting/settingTypes/backupSetting.tsx (revision 78ad703b21d9e7bbcafd7e14386bc12f78d2ff7f)
1import React from "react";
2import { ScrollView, StyleSheet } from "react-native";
3import ListItem, { ListItemHeader } from "@/components/base/listItem";
4import Toast from "@/utils/toast";
5import Backup from "@/core/backup";
6import backup from "@/core/backup";
7import { ROUTE_PATH, useNavigate } from "@/core/router";
8
9import axios from "axios";
10import { showDialog } from "@/components/dialogs/useDialog";
11import { showPanel } from "@/components/panels/usePanel";
12
13import { AuthType, createClient } from "webdav";
14import Config from "@/core/config.ts";
15import { writeInChunks } from "@/utils/fileUtils.ts";
16import { getDocumentAsync } from "expo-document-picker";
17import { readAsStringAsync } from "expo-file-system";
18import sleep from "@/utils/sleep";
19import { ResumeMode } from "@/constants/commonConst.ts";
20import strings from "@/constants/strings.ts";
21import { errorLog } from "@/utils/log.ts";
22
23export default function BackupSetting() {
24    const navigate = useNavigate();
25
26    const resumeMode = Config.useConfigValue('backup.resumeMode');
27    const webdavUrl = Config.useConfigValue('webdav.url');
28    const webdavUsername = Config.useConfigValue('webdav.username');
29    const webdavPassword = Config.useConfigValue('webdav.password');
30
31
32    const onBackupToLocal = async () => {
33        navigate(ROUTE_PATH.FILE_SELECTOR, {
34            fileType: 'folder',
35            multi: false,
36            actionText: '开始备份',
37            async onAction(selectedFiles) {
38                const raw = Backup.backup();
39                const folder = selectedFiles[0]?.path;
40                return new Promise(resolve => {
41                    showDialog('LoadingDialog', {
42                        title: '备份本地音乐',
43                        loadingText: '备份中...',
44                        promise: writeInChunks(
45                            `${folder}${
46                                folder?.endsWith('/') ? '' : '/'
47                            }backup.json`,
48                            raw,
49                        ),
50                        onResolve(_, hideDialog) {
51                            Toast.success('备份成功~');
52                            hideDialog();
53                            resolve(true);
54                        },
55                        onCancel(hideDialog) {
56                            hideDialog();
57                            resolve(false);
58                        },
59                        onReject(reason, hideDialog) {
60                            hideDialog();
61                            resolve(false);
62                            console.log(reason);
63                            Toast.warn(`备份失败 ${reason?.message ?? reason}`);
64                        },
65                    });
66                });
67            },
68        });
69    };
70
71    async function onResumeFromLocal() {
72        try {
73            const pickResult = await getDocumentAsync({
74                copyToCacheDirectory: true,
75                type: 'application/json',
76            });
77            if (pickResult.canceled) {
78                return;
79            }
80            const result = await readAsStringAsync(pickResult.assets[0].uri);
81            return new Promise(resolve => {
82                showDialog('LoadingDialog', {
83                    title: '从本地文件恢复',
84                    loadingText: '恢复中...',
85                    async task() {
86                        await sleep(300);
87                        return backup.resume(result, resumeMode);
88                    },
89                    onResolve(_, hideDialog) {
90                        Toast.success('恢复成功~');
91                        hideDialog();
92                        resolve(true);
93                    },
94                    onCancel(hideDialog) {
95                        hideDialog();
96                        resolve(false);
97                    },
98                    onReject(reason, hideDialog) {
99                        hideDialog();
100                        resolve(false);
101                        console.log(reason);
102                        Toast.warn(`恢复失败 ${reason?.message ?? reason}`);
103                    },
104                });
105            });
106        } catch (e: any) {
107            errorLog('恢复失败', e);
108            Toast.warn(`恢复失败 ${e?.message ?? e}`);
109        }
110    }
111
112    // const [webDavState, setWebDavState] = useState('');
113    async function onResumeFromUrl() {
114        showPanel('SimpleInput', {
115            title: '恢复数据',
116            placeholder: '输入以json或txt结尾的URL',
117            maxLength: 1024,
118            async onOk(text, closePanel) {
119                try {
120                    const url = text.trim();
121                    if (url.endsWith('.json') || url.endsWith('.txt')) {
122                        const raw = (await axios.get(text)).data;
123                        await Backup.resume(raw, resumeMode);
124                        Toast.success('恢复成功~');
125                        closePanel();
126                    } else {
127                        throw '无效的URL';
128                    }
129                } catch (e: any) {
130                    Toast.warn(`恢复失败 ${e?.message ?? e}`);
131                }
132            },
133        });
134    }
135
136    async function onResumeFromWebdav() {
137        const url = Config.getConfig('webdav.url');
138        const username = Config.getConfig('webdav.username');
139        const password = Config.getConfig('webdav.password');
140
141        if (!(username && password && url)) {
142            Toast.warn('请先在「Webdav设置」中完成配置,再执行恢复');
143            return;
144        }
145        const client = createClient(url, {
146            authType: AuthType.Password,
147            username: username,
148            password: password,
149        });
150
151        if (!(await client.exists('/MusicFree/MusicFreeBackup.json'))) {
152            Toast.warn('备份文件不存在');
153            return;
154        }
155
156        try {
157            const resumeData = await client.getFileContents(
158                '/MusicFree/MusicFreeBackup.json',
159                {
160                    format: 'text',
161                },
162            );
163            await Backup.resume(
164                resumeData,
165                Config.getConfig('backup.resumeMode'),
166            );
167            Toast.success('恢复成功~');
168        } catch (e: any) {
169            Toast.warn(`恢复失败: ${e?.message ?? e}`);
170        }
171    }
172
173    async function onBackupToWebdav() {
174        const username = Config.getConfig('webdav.username');
175        const password = Config.getConfig('webdav.password');
176        const url = Config.getConfig('webdav.url');
177        if (!(username && password && url)) {
178            Toast.warn('请先在「Webdav设置」中完成配置,再执行恢复');
179            return;
180        }
181        try {
182            const client = createClient(url, {
183                authType: AuthType.Password,
184                username: username,
185                password: password,
186            });
187
188            const raw = Backup.backup();
189            if (!(await client.exists('/MusicFree'))) {
190                await client.createDirectory('/MusicFree');
191            }
192            // 临时文件
193            await client.putFileContents(
194                '/MusicFree/MusicFreeBackup.json',
195                raw,
196                {
197                    overwrite: true,
198                },
199            );
200            Toast.success('备份成功');
201        } catch (e: any) {
202            Toast.warn(`备份失败: ${e?.message ?? e}`);
203        }
204    }
205
206    return (
207        <ScrollView style={style.wrapper}>
208            <ListItemHeader>备份&恢复设置</ListItemHeader>
209
210            <ListItem
211                withHorizontalPadding
212                onPress={() => {
213                    showDialog('RadioDialog', {
214                        title: '设置恢复方式',
215                        content: [
216                            {
217                                label: strings.settings[ResumeMode.Append],
218                                value: ResumeMode.Append,
219                            },
220                            {
221                                label: strings.settings[
222                                    ResumeMode.OverwriteDefault
223                                ],
224                                value: ResumeMode.OverwriteDefault,
225                            },
226                            {
227                                label: strings.settings[ResumeMode.Overwrite],
228                                value: ResumeMode.Overwrite,
229                            },
230                        ],
231                        onOk(value) {
232                            Config.setConfig(
233                                'backup.resumeMode',
234                                value as any,
235                            );
236                        },
237                    });
238                }}>
239                <ListItem.Content title="恢复方式" />
240                <ListItem.ListItemText>
241                    {
242                        strings.settings[
243                            (resumeMode as ResumeMode) ||
244                                ResumeMode.Append
245                        ]
246                    }
247                </ListItem.ListItemText>
248            </ListItem>
249            <ListItemHeader>本地备份</ListItemHeader>
250            <ListItem withHorizontalPadding onPress={onBackupToLocal}>
251                <ListItem.Content title="备份到本地" />
252            </ListItem>
253            <ListItem withHorizontalPadding onPress={onResumeFromLocal}>
254                <ListItem.Content title="从本地文件恢复" />
255            </ListItem>
256            <ListItem withHorizontalPadding onPress={onResumeFromUrl}>
257                <ListItem.Content title="从远程URL中恢复" />
258            </ListItem>
259            <ListItemHeader>Webdav</ListItemHeader>
260            <ListItem
261                withHorizontalPadding
262                onPress={() => {
263                    showPanel('SetUserVariables', {
264                        title: 'Webdav设置',
265                        initValues: {
266                            url: webdavUrl ?? '',
267                            username: webdavUsername ?? '',
268                            password: webdavPassword ?? '',
269                        },
270                        variables: [
271                            {
272                                key: 'url',
273                                name: 'URL',
274                                hint: 'webdav服务地址',
275                            },
276                            {
277                                key: 'username',
278                                name: '用户名',
279                            },
280                            {
281                                key: 'password',
282                                name: '密码',
283                            },
284                        ],
285                        onOk(values, closePanel) {
286                            Config.setConfig('webdav.url', values?.url);
287                            Config.setConfig('webdav.username', values?.username);
288                            Config.setConfig('webdav.password', values?.password);
289
290                            Toast.success('保存成功');
291                            closePanel();
292                        },
293                    });
294                }}>
295                <ListItem.Content title="Webdav设置" />
296            </ListItem>
297            <ListItem withHorizontalPadding onPress={onBackupToWebdav}>
298                <ListItem.Content title="备份到Webdav" />
299            </ListItem>
300            <ListItem withHorizontalPadding onPress={onResumeFromWebdav}>
301                <ListItem.Content title="从Webdav中恢复" />
302            </ListItem>
303        </ScrollView>
304    );
305}
306
307const style = StyleSheet.create({
308    wrapper: {
309        width: '100%',
310        flex: 1,
311    },
312});
313