From d30d2a1212b25f941aed15c42d810108abc612da Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 8 Feb 2023 00:35:55 +0100 Subject: [PATCH] Add backup/restore functionality to example app --- .../pages/docs/Examples/existing_databases.md | 2 +- .../app/lib/database/connection/native.dart | 17 ++-- examples/app/lib/database/database.dart | 2 +- examples/app/lib/screens/backup/backup.dart | 1 + .../app/lib/screens/backup/supported.dart | 94 +++++++++++++++++++ .../app/lib/screens/backup/unsupported.dart | 10 ++ examples/app/lib/screens/home.dart | 2 + examples/app/pubspec.yaml | 1 + 8 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 examples/app/lib/screens/backup/backup.dart create mode 100644 examples/app/lib/screens/backup/supported.dart create mode 100644 examples/app/lib/screens/backup/unsupported.dart diff --git a/docs/pages/docs/Examples/existing_databases.md b/docs/pages/docs/Examples/existing_databases.md index 0cb44bc9..b176ad1d 100644 --- a/docs/pages/docs/Examples/existing_databases.md +++ b/docs/pages/docs/Examples/existing_databases.md @@ -60,7 +60,7 @@ LazyDatabase _openConnection() { await file.writeAsBytes(buffer.asUint8List(blob.offsetInBytes, blob.lengthInBytes)); } - return NativeDatabase.createInBackground(file);; + return NativeDatabase.createInBackground(file); }); } ``` diff --git a/examples/app/lib/database/connection/native.dart b/examples/app/lib/database/connection/native.dart index 3b61e994..fa20ad1b 100644 --- a/examples/app/lib/database/connection/native.dart +++ b/examples/app/lib/database/connection/native.dart @@ -5,17 +5,16 @@ import 'package:drift/native.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; +Future get databaseFile async { + // We use `path_provider` to find a suitable path to store our data in. + final appDir = await getApplicationDocumentsDirectory(); + final dbPath = p.join(appDir.path, 'todos.db'); + return File(dbPath); +} + /// Obtains a database connection for running drift in a Dart VM. DatabaseConnection connect() { return DatabaseConnection.delayed(Future(() async { - // Background isolates can't use platform channels, so let's use - // `path_provider` in the main isolate and just send the result containing - // the path over to the background isolate. - - // We use `path_provider` to find a suitable path to store our data in. - final appDir = await getApplicationDocumentsDirectory(); - final dbPath = p.join(appDir.path, 'todos.db'); - - return NativeDatabase.createBackgroundConnection(File(dbPath)); + return NativeDatabase.createBackgroundConnection(await databaseFile); })); } diff --git a/examples/app/lib/database/database.dart b/examples/app/lib/database/database.dart index 3a9b9dc1..94c3b9b2 100644 --- a/examples/app/lib/database/database.dart +++ b/examples/app/lib/database/database.dart @@ -119,7 +119,7 @@ class AppDatabase extends _$AppDatabase { }); } - static Provider provider = Provider((ref) { + static final StateProvider provider = StateProvider((ref) { final database = AppDatabase(); ref.onDispose(database.close); diff --git a/examples/app/lib/screens/backup/backup.dart b/examples/app/lib/screens/backup/backup.dart new file mode 100644 index 00000000..20b6e8c3 --- /dev/null +++ b/examples/app/lib/screens/backup/backup.dart @@ -0,0 +1 @@ +export 'unsupported.dart' if (dart.library.ffi) 'supported.dart'; diff --git a/examples/app/lib/screens/backup/supported.dart b/examples/app/lib/screens/backup/supported.dart new file mode 100644 index 00000000..a2a154e4 --- /dev/null +++ b/examples/app/lib/screens/backup/supported.dart @@ -0,0 +1,94 @@ +import 'dart:io'; + +import 'package:app/database/connection/native.dart'; +import 'package:app/database/database.dart'; +import 'package:drift/drift.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:sqlite3/sqlite3.dart'; + +class BackupIcon extends StatelessWidget { + const BackupIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () => + showDialog(context: context, builder: (_) => const BackupDialog()), + icon: const Icon(Icons.save), + tooltip: 'Backup', + ); + } +} + +class BackupDialog extends ConsumerWidget { + const BackupDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AlertDialog( + title: const Text('Database backup'), + content: const Text( + 'Here, you can save the database to a file or restore a created ' + 'backup.', + ), + actions: [ + TextButton( + onPressed: () { + createDatabaseBackup(ref.read(AppDatabase.provider)); + }, + child: const Text('Save'), + ), + TextButton( + onPressed: () async { + final db = ref.read(AppDatabase.provider); + await db.close(); + + // Open the selected database file + final backupFile = await FilePicker.platform.pickFiles(); + if (backupFile == null) return; + final backupDb = sqlite3.open(backupFile.files.single.path!); + + // Vacuum it into a temporary location first to make sure it's working. + final tempPath = await getTemporaryDirectory(); + final tempDb = p.join(tempPath.path, 'import.db'); + backupDb + ..execute('VACUUM INTO ?', [tempDb]) + ..dispose(); + + // Then replace the existing database file with it. + final tempDbFile = File(tempDb); + await tempDbFile.copy((await databaseFile).path); + await tempDbFile.delete(); + + // And now, re-open the database! + ref.read(AppDatabase.provider.notifier).state = AppDatabase(); + }, + child: const Text('Restore'), + ), + ], + ); + } +} + +Future createDatabaseBackup(DatabaseConnectionUser database) async { + final choosenDirectory = await FilePicker.platform.getDirectoryPath(); + if (choosenDirectory == null) return; + + final parent = Directory(choosenDirectory); + final file = File(p.join(choosenDirectory, 'drift_example_backup.db')); + + // Make sure the directory of the file exists + if (!await parent.exists()) { + await parent.create(recursive: true); + } + // However, the file itself must not exist + if (await file.exists()) { + await file.delete(); + } + + await database.customStatement('VACUUM INTO ?', [file.absolute.path]); +} diff --git a/examples/app/lib/screens/backup/unsupported.dart b/examples/app/lib/screens/backup/unsupported.dart new file mode 100644 index 00000000..6591ac08 --- /dev/null +++ b/examples/app/lib/screens/backup/unsupported.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class BackupIcon extends StatelessWidget { + const BackupIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} diff --git a/examples/app/lib/screens/home.dart b/examples/app/lib/screens/home.dart index 9d0c2f36..dd1050ea 100644 --- a/examples/app/lib/screens/home.dart +++ b/examples/app/lib/screens/home.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../database/database.dart'; +import 'backup/backup.dart'; import 'home/card.dart'; import 'home/drawer.dart'; import 'home/state.dart'; @@ -48,6 +49,7 @@ class _HomePageState extends ConsumerState { appBar: AppBar( title: const Text('Drift Todo list'), actions: [ + const BackupIcon(), IconButton( onPressed: () => context.go('/search'), icon: const Icon(Icons.search), diff --git a/examples/app/pubspec.yaml b/examples/app/pubspec.yaml index f8fb4c37..704a29e1 100644 --- a/examples/app/pubspec.yaml +++ b/examples/app/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: flutter: sdk: flutter drift: + file_picker: ^5.2.5 flutter_colorpicker: ^1.0.3 flutter_riverpod: ^1.0.3 go_router: ^3.0.6