Skip to content

Commit

Permalink
Restore from backup
Browse files Browse the repository at this point in the history
  • Loading branch information
belinde committed Apr 23, 2024
1 parent e837e5b commit 897ea02
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 60 deletions.
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
"react-native-svg": "14.1.0",
"react-native-zip-archive": "^6.1.0",
"react-qr-code": "^2.0.12",
"expo-sharing": "~11.10.0"
"expo-sharing": "~11.10.0",
"expo-document-picker": "~11.10.1",
"expo-system-ui": "~2.9.4"
},
"devDependencies": {
"@babel/core": "^7.23.7",
Expand Down
15 changes: 12 additions & 3 deletions src/Settings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FILENAME_SETTINGS } from "./constants";
import { readJsonFile, writeJsonFile } from "./functions";
import { InitialNote, NoteNameStyle } from "./types";

Expand All @@ -9,14 +10,22 @@ type SettingsProperties = {
standardSections: StandardSection[];
};

export const isSettingsProperties = (item: any): item is SettingsProperties => {
return (
typeof item === "object" &&
item !== null &&
"noteStyle" in item &&
"standardSections" in item
);
};

const FIELDS: (keyof SettingsProperties)[] = [
"noteStyle",
"concertMode",
"standardSections",
];

export class Settings {
private static fileName = "settings.json";
private noteStyle: NoteNameStyle = "latin";
private concertMode?: string;
private subscribers: Set<() => void> = new Set();
Expand All @@ -31,7 +40,7 @@ export class Settings {

public async load(): Promise<void> {
const content = await readJsonFile<SettingsProperties>(
Settings.fileName,
FILENAME_SETTINGS,
FIELDS.reduce((acc, field) => {
acc[field] = this[field] as any;
return acc;
Expand All @@ -48,7 +57,7 @@ export class Settings {
}

private async persist() {
await writeJsonFile(Settings.fileName, this);
await writeJsonFile(FILENAME_SETTINGS, this);
this.subscribers.forEach((callback) => callback());
}

Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { PentagramPreference, Section } from "./types";

export const DOCUMENT_DIRECTORY = `${documentDirectory}storage/`;
export const SCORE_PHOTO_RATIO = 5;
export const FILENAME_SONGS = "songs.json";
export const FILENAME_CONCERTS = "concerts.json";
export const FILENAME_SETTINGS = "settings.json";

export const SECTIONS = [
"tenori",
Expand Down
2 changes: 0 additions & 2 deletions src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export const readJsonFile = async <T>(
defaultValue: T
): Promise<T> => {
const file = DOCUMENT_DIRECTORY + fileName;
console.log("Reading file", file);
const info = await getInfoAsync(file);
if (!info.exists) {
return defaultValue;
Expand All @@ -52,7 +51,6 @@ export const writeJsonFile = async <T>(fileName: string, data: T) => {
(e) => console.warn("Cannot create directory", DOCUMENT_DIRECTORY, e)
);
const file = DOCUMENT_DIRECTORY + fileName;
console.log("Writing file", file);
await writeAsStringAsync(file, JSON.stringify(data), {
encoding: "utf8",
}).catch((e) => console.warn("Cannot write file", fileName, e));
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/useDataContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {
} from "react";
import { JsonCRUD } from "../JsonCRUD";
import { Settings } from "../Settings";
import { FILENAME_CONCERTS, FILENAME_SONGS } from "../constants";
import { deleteFile } from "../functions";
import { Concert, Song } from "../types";

const songs = new JsonCRUD<Song, "id">(
"songs.json",
FILENAME_SONGS,
"id",
(a, b) => a.title.localeCompare(b.title),
async (song) => {
Expand All @@ -24,7 +25,7 @@ const songs = new JsonCRUD<Song, "id">(
}
);

const concerts = new JsonCRUD<Concert, "id">("concerts.json", "id", (a, b) =>
const concerts = new JsonCRUD<Concert, "id">(FILENAME_CONCERTS, "id", (a, b) =>
a.title.localeCompare(b.title)
);

Expand Down
59 changes: 59 additions & 0 deletions src/pages/Settings/ArchiveExport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { cacheDirectory, deleteAsync } from "expo-file-system";
import { isAvailableAsync, shareAsync } from "expo-sharing";
import { FC, useCallback, useEffect, useState } from "react";
import { Button, ProgressBar, Text } from "react-native-paper";
import { subscribe, zip } from "react-native-zip-archive";
import { DOCUMENT_DIRECTORY } from "../../constants";

export const ArchiveExport: FC = () => {
const [canShare, setCanShare] = useState(true);
const [elaboratingBackup, setElaboratingBackup] = useState(false);
const [progress, setProgress] = useState(0);

const now = new Date();
const targetPath = `${cacheDirectory}backup_${now.getFullYear()}-${now.getMonth()}-${now.getDate()}.meistru.zip`;

useEffect(() => {
isAvailableAsync().then(setCanShare);
}, []);

useEffect(() => {
const observer = subscribe(
(evt) => evt.filePath === targetPath && setProgress(evt.progress)
);
return () => observer.remove();
}, [targetPath]);

const doBackup = useCallback(async () => {
if (!canShare || !cacheDirectory) return;
setElaboratingBackup(true);
setProgress(0);

try {
await zip(DOCUMENT_DIRECTORY, targetPath);
await shareAsync(targetPath, {
mimeType: "application/zip",
dialogTitle: "Esporta archivio",
});
} catch (err) {
console.error(err);
} finally {
await deleteAsync(targetPath);
setElaboratingBackup(false);
}
}, [canShare, targetPath]);

return (
<>
<Text variant="titleMedium">Esportazione dell'archivio</Text>
{elaboratingBackup ? <ProgressBar progress={progress} /> : null}
{canShare ? (
<Button onPress={doBackup} disabled={elaboratingBackup} icon="upload">
Invia copia a...
</Button>
) : (
<Text>"Condivisione non disponibile"</Text>
)}
</>
);
};
184 changes: 184 additions & 0 deletions src/pages/Settings/ArchiveImport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { getDocumentAsync } from "expo-document-picker";
import {
cacheDirectory,
deleteAsync,
getInfoAsync,
makeDirectoryAsync,
moveAsync,
readAsStringAsync,
readDirectoryAsync,
} from "expo-file-system";
import { FC, useCallback, useEffect, useState } from "react";
import { Button, Checkbox, Dialog, Portal, Text } from "react-native-paper";
import { unzip } from "react-native-zip-archive";
import { isSettingsProperties } from "../../Settings";
import {
DOCUMENT_DIRECTORY,
FILENAME_CONCERTS,
FILENAME_SETTINGS,
FILENAME_SONGS,
} from "../../constants";
import { useDataContext } from "../../hooks/useDataContext";
import { isConcert, isSong } from "../../types";

const BACKUP_FOLDER = `${cacheDirectory}backup`;

export const readJsonFile = async <T,>(fileName: string): Promise<T | null> => {
const file = `${BACKUP_FOLDER}/${fileName}`;
const info = await getInfoAsync(file);
if (!info.exists) {
return null;
}
const content = await readAsStringAsync(file, {
encoding: "utf8",
}).catch((e) => {
console.warn("Cannot open file", fileName, e);
return null;
});
try {
return content ? JSON.parse(content) : null;
} catch (e) {
console.warn("Cannot parse file", fileName, e);
return null;
}
};

const FolderInspector: FC<{ close: () => void }> = (props) => {
const data = useDataContext();
const [files, setFiles] = useState<string[]>([]);
const [mergeSongs, setMergeSongs] = useState(false);
const [mergeConcerts, setMergeConcerts] = useState(false);
const [mergeSettings, setMergeSettings] = useState(false);

const importBackup = useCallback(async () => {
if (mergeSongs) {
const songsJson = await readJsonFile<unknown[]>(FILENAME_SONGS);
if (Array.isArray(songsJson)) {
songsJson.forEach(async (s) => {
if (isSong(s)) {
data.songs.add(s);
if (s.image && files.includes(s.image)) {
await moveAsync({
from: `${BACKUP_FOLDER}/${s.image}`,
to: `${DOCUMENT_DIRECTORY}${s.image}`,
});
}
}
});
}
}

if (mergeConcerts) {
const concertsJson = await readJsonFile<unknown[]>(FILENAME_CONCERTS);
if (Array.isArray(concertsJson)) {
concertsJson.forEach(async (c) => {
if (isConcert(c)) {
data.concerts.add(c);
}
});
}
}

if (mergeSettings) {
const settingsJson = await readJsonFile(FILENAME_SETTINGS);
if (isSettingsProperties(settingsJson)) {
data.settings.setNoteStyle(settingsJson.noteStyle);
data.settings.setStandardSections(settingsJson.standardSections);
}
}

props.close();
}, [
mergeSongs,
mergeConcerts,
mergeSettings,
props,
data.songs,
data.concerts,
data.settings,
files,
]);

useEffect(() => {
readDirectoryAsync(BACKUP_FOLDER)
.then((f) => {
setFiles(f);
setMergeSongs(f.includes(FILENAME_SONGS));
setMergeConcerts(f.includes(FILENAME_CONCERTS));
setMergeSettings(f.includes(FILENAME_SETTINGS));
})
.catch((e) => {
console.warn("Cannot read directory", BACKUP_FOLDER, e);
});
}, []);
return (
<Dialog visible>
<Dialog.Title>Ripristino backup ({files.length})</Dialog.Title>
<Dialog.ScrollArea>
<Text>
Seleziona quali dati vuoi ripristinare. Brani e concerti verranno
uniti a quelli eventualmente già presenti su questo dispositivo.
</Text>
<Checkbox.Item
label="Repertorio"
status={mergeSongs ? "checked" : "unchecked"}
onPress={() => setMergeSongs(!mergeSongs)}
/>
<Checkbox.Item
label="Concerti"
status={mergeConcerts ? "checked" : "unchecked"}
onPress={() => setMergeConcerts(!mergeConcerts)}
/>
<Checkbox.Item
label="Impostazioni"
status={mergeSettings ? "checked" : "unchecked"}
onPress={() => setMergeSettings(!mergeSettings)}
/>
</Dialog.ScrollArea>
<Dialog.Actions>
<Button onPress={props.close}>Annulla</Button>
<Button onPress={importBackup} mode="contained" icon="import">
Importa
</Button>
</Dialog.Actions>
</Dialog>
);
};

export const ArchiveImport: FC = () => {
const [inspect, setInspect] = useState(false);

const pickBackup = useCallback(async () => {
setInspect(false);
await makeDirectoryAsync(BACKUP_FOLDER, { intermediates: true });
const res = await getDocumentAsync({
type: "application/zip",
copyToCacheDirectory: true,
multiple: false,
});
if (!res.canceled && res.assets && res.assets.length > 0) {
await unzip(res.assets[0].uri, BACKUP_FOLDER);
await deleteAsync(res.assets[0].uri);
}
setInspect(true);
}, []);

const removeBackup = useCallback(async () => {
await deleteAsync(BACKUP_FOLDER, { idempotent: true });
setInspect(false);
}, []);

return (
<>
<Text variant="titleMedium">Ripristino da backup</Text>
<Button onPress={pickBackup} icon="download">
Carica da...
</Button>
{inspect && (
<Portal>
<FolderInspector close={removeBackup} />
</Portal>
)}
</>
);
};
Loading

0 comments on commit 897ea02

Please sign in to comment.