Skip to content

Commit 079f354

Browse files
authored
Merge pull request #10 from amarjanica/feature/import-export
add import and export
2 parents 36299b5 + 716bb9e commit 079f354

File tree

10 files changed

+1233
-646
lines changed

10 files changed

+1233
-646
lines changed

app/settings.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import React from 'react';
1+
import React, { useContext } from 'react';
22
import { View, Text, StyleSheet, Button } from 'react-native';
33
import { useAppDispatch } from '@/store';
44
import Header from '@/Header';
55
import globalStyles from '@/globalStyles';
66
import { clearData } from '@/store/globalReset';
77
import { useDataContext } from '@/data/DataContext';
8+
import { SQLiteContext } from '@/data/SQLiteProvider';
89

910
const SettingsPage = () => {
1011
const dispatch = useAppDispatch();
1112
const { opsClient } = useDataContext();
13+
const dbCtx = useContext(SQLiteContext);
1214

1315
return (
1416
<View style={globalStyles.root}>
@@ -24,6 +26,19 @@ const SettingsPage = () => {
2426
dispatch(clearData({ opsClient }));
2527
}}
2628
/>
29+
<Button
30+
title="Import"
31+
onPress={async () => {
32+
await opsClient.restore();
33+
await dbCtx.reload();
34+
}}
35+
/>
36+
<Button
37+
title="Export"
38+
onPress={async () => {
39+
await opsClient.backup('testbackup.sqlite');
40+
}}
41+
/>
2742
</View>
2843
</View>
2944
);

package-lock.json

Lines changed: 1045 additions & 613 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
"eslint-plugin-prettier": "^5.2.1",
2323
"expo": "^51.0.34",
2424
"expo-constants": "~16.0.2",
25-
"expo-dev-client": "~4.0.29",
2625
"expo-linking": "~6.3.1",
2726
"expo-router": "~3.5.23",
2827
"expo-sqlite": "~14.0.6",
@@ -40,9 +39,9 @@
4039
"redux-devtools-expo-dev-plugin": "^0.2.1",
4140
"semver": "^7.6.3",
4241
"sql.js": "^1.12.0",
43-
"@expo/vector-icons": "^14.0.3",
44-
"expo-document-picker": "~12.0.2",
42+
"expo-dev-client": "~4.0.29",
4543
"expo-file-system": "~17.0.1",
44+
"expo-document-picker": "~12.0.2",
4645
"expo-sharing": "~12.0.1"
4746
},
4847
"devDependencies": {

src/clients/SQLiteOpsClient.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { OpsClient } from '@/clients/types';
2-
import { SQLiteDatabase } from '@/data/sqliteDatabase';
2+
import { backupDatabase, restoreDatabase, SQLiteDatabase } from '@/data/sqliteDatabase';
33

44
class SQLiteOpsClient implements OpsClient {
55
constructor(private db: SQLiteDatabase) {}
@@ -8,11 +8,11 @@ class SQLiteOpsClient implements OpsClient {
88
await this.db.execAsync(`
99
DELETE FROM task;`);
1010
}
11-
async backup(): Promise<void> {
12-
throw new Error('Method not implemented.');
11+
async backup(backupName: string): Promise<void> {
12+
await backupDatabase(this.db, backupName);
1313
}
1414
async restore(): Promise<void> {
15-
throw new Error('Method not implemented.');
15+
await restoreDatabase(this.db);
1616
}
1717
}
1818

src/clients/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ export interface TaskClient {
1313
*/
1414
export interface OpsClient {
1515
clear(): Promise<void>;
16-
backup(): Promise<void>;
16+
backup(backupName: string): Promise<void>;
1717
restore(): Promise<void>;
1818
}

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const dbName = 'test.db';
1+
export const dbName = 'test.sqlite';

src/data/SQLiteProvider.tsx

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { createContext, useEffect, useState } from 'react';
22
import logger from '@/logger';
33
import { SQLiteDatabase, openDatabaseAsync } from '@/data/sqliteDatabase';
44

5-
const SQLiteContext = createContext<SQLiteDatabase | null>(null);
5+
export const SQLiteContext = createContext<{ db: SQLiteDatabase; reload: () => Promise<void> } | null>(null);
66
function SQLiteProvider({
77
databaseName,
88
children,
@@ -17,28 +17,26 @@ function SQLiteProvider({
1717
const [error, setError] = useState<Error | null>(null);
1818
const isMounting = React.useRef(true);
1919
const isRunning = React.useRef(false); // New flag to track if the setup is still running
20+
const setup = React.useCallback(async () => {
21+
if (!isMounting.current || isRunning.current) return;
2022

21-
useEffect(() => {
22-
const setup = async () => {
23-
if (!isMounting.current || isRunning.current) return;
24-
25-
isMounting.current = false;
26-
isRunning.current = true;
27-
try {
28-
const _db = await openDatabaseAsync(databaseName, undefined);
29-
await onInit(_db);
30-
logger.log(`Mounted sqlite provider`);
31-
setDb(_db);
32-
setLoading(false);
33-
} catch (e: any) {
34-
setError(e);
35-
} finally {
36-
isRunning.current = false; // Mark setup as complete
37-
}
38-
};
23+
isMounting.current = false;
24+
isRunning.current = true;
25+
try {
26+
const _db = await openDatabaseAsync(databaseName, undefined);
27+
await onInit(_db);
28+
logger.log(`Mounted sqlite provider`);
29+
setDb(_db);
30+
setLoading(false);
31+
} catch (e: any) {
32+
setError(e);
33+
} finally {
34+
isRunning.current = false; // Mark setup as complete
35+
}
36+
}, []);
3937

38+
useEffect(() => {
4039
void setup();
41-
4240
return () => {
4341
isMounting.current = true;
4442
};
@@ -51,7 +49,21 @@ function SQLiteProvider({
5149

5250
const isFullyLoaded = !loading && !!db;
5351

54-
return isFullyLoaded && <SQLiteContext.Provider value={db}>{children}</SQLiteContext.Provider>;
52+
return (
53+
isFullyLoaded && (
54+
<SQLiteContext.Provider
55+
value={{
56+
db,
57+
reload: async () => {
58+
isMounting.current = true;
59+
isRunning.current = false;
60+
await setup();
61+
},
62+
}}>
63+
{children}
64+
</SQLiteContext.Provider>
65+
)
66+
);
5567
}
5668

5769
export default SQLiteProvider;

src/data/indexedDatabase.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export async function loadFromIndexedDB(dbName: string): Promise<Uint8Array | nu
1717
}
1818
export async function saveToIndexedDB(db: Database, dbName: string) {
1919
const dbData = db.export();
20+
await saveToIndexedDBBA(dbData, dbName);
21+
}
22+
export async function saveToIndexedDBBA(dbData: Uint8Array, dbName: string) {
2023
const idb = await idbPromise;
2124
await idb.put(STORE_NAME, dbData, dbName);
2225
}

src/data/sqliteDatabase.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,56 @@
1-
export { openDatabaseAsync, SQLiteDatabase } from 'expo-sqlite';
1+
import { openDatabaseAsync, SQLiteDatabase } from 'expo-sqlite';
2+
import * as FileSystem from 'expo-file-system';
3+
import * as Sharing from 'expo-sharing';
4+
import * as DocumentPicker from 'expo-document-picker';
5+
import logger from '@/logger';
6+
7+
export const backupDatabase = async (db: SQLiteDatabase, backupName: string) => {
8+
try {
9+
await db.execAsync('PRAGMA wal_checkpoint(FULL)');
10+
const appPath = FileSystem.documentDirectory;
11+
const dbPath = `${appPath}/SQLite/${db.databaseName}`;
12+
const backupPath = `${appPath}/SQLite/${backupName}`;
13+
14+
await FileSystem.copyAsync({
15+
from: dbPath,
16+
to: backupPath,
17+
});
18+
19+
await Sharing.shareAsync(backupPath, { mimeType: 'application/x-sqlite3' });
20+
await FileSystem.deleteAsync(backupPath, { idempotent: true });
21+
} catch (err) {
22+
logger.error('Failed to backup', err);
23+
}
24+
};
25+
26+
export const restoreDatabase = async (db: SQLiteDatabase) => {
27+
try {
28+
const appPath = FileSystem.documentDirectory;
29+
const result = await DocumentPicker.getDocumentAsync({
30+
type: '*/*',
31+
copyToCacheDirectory: true,
32+
multiple: false,
33+
});
34+
if (result.canceled) {
35+
return;
36+
}
37+
const backupPath = result.assets[0].uri;
38+
if (!(await FileSystem.getInfoAsync(backupPath)).exists) {
39+
return;
40+
}
41+
await db.execAsync('PRAGMA wal_checkpoint(FULL)');
42+
await db.closeAsync();
43+
const dbPath = `${appPath}/SQLite/${db.databaseName}`;
44+
await FileSystem.deleteAsync(`${dbPath}-wal`, { idempotent: true });
45+
await FileSystem.deleteAsync(`${dbPath}-shm`, { idempotent: true });
46+
47+
await FileSystem.copyAsync({
48+
to: dbPath,
49+
from: backupPath,
50+
});
51+
} catch (err) {
52+
logger.error('Failed to backup', err);
53+
}
54+
};
55+
56+
export { openDatabaseAsync, SQLiteDatabase };

src/data/sqliteDatabase.web.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { BindParams } from 'sql.js';
22
import initSqlJs from 'sql.js';
3-
import { loadFromIndexedDB, saveToIndexedDB } from '@/data/indexedDatabase';
3+
import { loadFromIndexedDB, saveToIndexedDB, saveToIndexedDBBA } from '@/data/indexedDatabase';
4+
import * as FileSystem from 'expo-file-system';
5+
import * as Sharing from 'expo-sharing';
6+
import logger from '@/logger';
7+
import * as DocumentPicker from 'expo-document-picker';
48
export type SQLiteDatabase = any;
59
const sqlPromise = initSqlJs({
610
locateFile: (file) => `https://sql.js.org/dist/${file}`,
@@ -60,5 +64,72 @@ export async function openDatabaseAsync(databaseName: string, options?: any): Pr
6064
closeAsync: async () => {
6165
db.close();
6266
},
67+
export: () => {
68+
return db.export();
69+
},
70+
import: async (data: Uint8Array) => {
71+
await saveToIndexedDBBA(data, databaseName);
72+
},
6373
};
6474
}
75+
76+
export const backupDatabase = async (db: SQLiteDatabase, backupName: string) => {
77+
try {
78+
const fileContent = await db.export();
79+
const blob = new Blob([fileContent], { type: 'application/x-sqlite3' });
80+
const url = URL.createObjectURL(blob);
81+
const link = document.createElement('a');
82+
link.href = url;
83+
link.download = backupName;
84+
link.click();
85+
URL.revokeObjectURL(url);
86+
} catch (err) {
87+
logger.error('Failed to backup', err);
88+
}
89+
};
90+
91+
export const restoreDatabase = async (db: SQLiteDatabase) => {
92+
try {
93+
const input = document.createElement('input');
94+
input.type = 'file';
95+
input.accept = '*/*';
96+
97+
return new Promise<void>((resolve, reject) => {
98+
input.onchange = async (event: any) => {
99+
const file = event.target.files[0];
100+
if (!file) {
101+
resolve(undefined);
102+
return;
103+
}
104+
105+
const reader = new FileReader();
106+
reader.onload = async (e) => {
107+
try {
108+
const fileContent = e.target?.result as ArrayBuffer | null;
109+
if (!fileContent) {
110+
reject('Corrupted file!');
111+
return;
112+
}
113+
114+
const uint8Array = new Uint8Array(fileContent);
115+
// @ts-ignore
116+
await db.import(uint8Array);
117+
resolve(undefined);
118+
} catch (error) {
119+
reject(error?.message || 'Unknown error!');
120+
}
121+
};
122+
123+
reader.onerror = () => {
124+
reject('Error reading the file!');
125+
};
126+
127+
reader.readAsArrayBuffer(file);
128+
};
129+
130+
input.click();
131+
});
132+
} catch (err) {
133+
logger.error('Failed to backup', err);
134+
}
135+
};

0 commit comments

Comments
 (0)