Skip to content

Commit

Permalink
feat: Database export and restore from client
Browse files Browse the repository at this point in the history
  • Loading branch information
Gustl22 committed Mar 5, 2024
1 parent e78e02e commit 3bec47a
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 27 deletions.
4 changes: 4 additions & 0 deletions wrestling_scoreboard_client/lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
"themeModeDark": "Dunkler Modus",
"apiUrl": "Api-Url",
"wsUrl": "Websocket-Url",
"exportDatabase": "Datenbank exportieren",
"restoreDatabase": "Datenbank wiederherstellen",
"restoreDefaultDatabase": "Standard-Datenbank wiederherstellen",
"resetDatabase": "Datenbank zurücksetzen",
"bellSound": "Glocken-Sound",

"imprint": "Impressum",
Expand Down
4 changes: 4 additions & 0 deletions wrestling_scoreboard_client/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"themeModeDark": "Dark mode",
"apiUrl": "Api-Url",
"wsUrl": "Websocket-Url",
"exportDatabase": "Export database",
"restoreDatabase": "Restore database",
"restoreDefaultDatabase": "Restore default database",
"resetDatabase": "Reset database",
"bellSound": "Bell sound",

"imprint": "Imprint",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,30 @@ class MockDataManager extends DataManager {
// TODO: implement messageHandler
return null;
});

@override
Future<String> exportDatabase() {
// TODO: implement exportDatabase
throw UnimplementedError();
}

@override
Future<void> resetDatabase() {
// TODO: implement resetDatabase
throw UnimplementedError();
}

@override
Future<void> restoreDefaultDatabase() {
// TODO: implement restoreDatabase
throw UnimplementedError();
}

@override
Future<void> restoreDatabase(String sqlDump) {
// TODO: implement restoreDatabase
throw UnimplementedError();
}
}

class MockWebSocketManager implements WebSocketManager {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ abstract class DataManager {
/// If [isReset] is true, then delete all previous Bouts and TeamMatchBouts, else reuse the states.
Future<void> generateBouts<S extends WrestlingEvent>(WrestlingEvent wrestlingEvent, [bool isReset = false]);

Future<String> exportDatabase();

Future<void> restoreDatabase(String sqlDump);

Future<void> restoreDefaultDatabase();

Future<void> resetDatabase();

final Map<Type, StreamController<DataObject>> _singleStreamControllers = {};
final Map<Type, Map<Type, StreamController<ManyDataObject<dynamic>>>> _manyStreamControllers = {};
final Map<Type, StreamController<Map<String, dynamic>>> _singleRawStreamControllers = {};
Expand Down
38 changes: 38 additions & 0 deletions wrestling_scoreboard_client/lib/services/network/remote/rest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,44 @@ class RestDataManager extends DataManager {
Future<void> deleteSingle<T extends DataObject>(T single) async {
webSocketManager.addToSink(jsonEncode(singleToJson(single, T, CRUD.delete)));
}

@override
Future<String> exportDatabase() async {
final uri = Uri.parse('$_apiUrl/database/export');
final response = await http.get(uri);
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Failed to export the database: ${response.reasonPhrase ?? response.statusCode.toString()}');
}
}

@override
Future<void> resetDatabase() async {
final uri = Uri.parse('$_apiUrl/database/reset');
final response = await http.post(uri);
if (response.statusCode != 200) {
throw Exception('Failed to reset the database: ${response.reasonPhrase ?? response.statusCode.toString()}');
}
}

@override
Future<void> restoreDefaultDatabase() async {
final uri = Uri.parse('$_apiUrl/database/restore_default');
final response = await http.post(uri);
if (response.statusCode != 200) {
throw Exception('Failed to restore the default database: ${response.reasonPhrase ?? response.statusCode.toString()}');
}
}

@override
Future<void> restoreDatabase(String sqlDump) async {
final uri = Uri.parse('$_apiUrl/database/restore');
final response = await http.post(uri, headers: headers, body: sqlDump);
if (response.statusCode != 200) {
throw Exception('Failed to restore the database: ${response.reasonPhrase ?? response.statusCode.toString()}');
}
}
}

class RestException implements Exception {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import 'dart:convert';
import 'dart:io';

import 'package:audioplayers/audioplayers.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:wrestling_scoreboard_client/provider/local_preferences.dart';
import 'package:wrestling_scoreboard_client/provider/local_preferences_provider.dart';
import 'package:wrestling_scoreboard_client/provider/network_provider.dart';
import 'package:wrestling_scoreboard_client/utils/asset.dart';
import 'package:wrestling_scoreboard_client/utils/environment.dart';
import 'package:wrestling_scoreboard_client/view/widgets/dialogs.dart';
Expand Down Expand Up @@ -148,7 +153,7 @@ class CustomSettingsScreen extends ConsumerWidget {
ListTile(
subtitle: Text(apiUrl),
title: Text(localizations.apiUrl),
leading: const Icon(Icons.storage),
leading: const Icon(Icons.link),
onTap: () async {
final val = await showDialog<String>(
context: context,
Expand All @@ -163,7 +168,7 @@ class CustomSettingsScreen extends ConsumerWidget {
},
),
ListTile(
leading: const Icon(Icons.storage),
leading: const Icon(Icons.link),
title: Text(localizations.wsUrl),
subtitle: Text(wsUrl),
onTap: () async {
Expand All @@ -179,6 +184,54 @@ class CustomSettingsScreen extends ConsumerWidget {
}
},
),
ListTile(
leading: const Icon(Icons.cloud_download),
title: Text(localizations.exportDatabase),
onTap: () async {
String? outputPath = await FilePicker.platform.saveFile(
fileName:
'${DateTime.now().toIso8601String().replaceAll(':', '-').replaceAll(RegExp(r'\.[0-9]{3}'), '')}-'
'PostgreSQL-wrestling_scoreboard-dump.sql',
);
if (outputPath != null) {
final dataManager = await ref.read(dataManagerNotifierProvider);
final sqlString = await dataManager.exportDatabase();
final outputFile = File(outputPath);
await outputFile.writeAsString(sqlString, encoding: const Utf8Codec());
}
},
),
ListTile(
leading: const Icon(Icons.settings_backup_restore),
title: Text(localizations.resetDatabase),
onTap: () async {
final dataManager = await ref.read(dataManagerNotifierProvider);
await dataManager.resetDatabase();
},
),
ListTile(
leading: const Icon(Icons.history),
title: Text(localizations.restoreDefaultDatabase),
onTap: () async {
final dataManager = await ref.read(dataManagerNotifierProvider);
await dataManager.restoreDefaultDatabase();
},
),
ListTile(
leading: const Icon(Icons.cloud_upload),
title: Text(localizations.restoreDatabase),
onTap: () async {
FilePickerResult? filePickerResult = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['sql'],
);
if (filePickerResult != null) {
File file = File(filePickerResult.files.single.path!);
final dataManager = await ref.read(dataManagerNotifierProvider);
await dataManager.restoreDatabase(await file.readAsString(encoding: const Utf8Codec()));
}
},
),
],
);
},
Expand Down
16 changes: 16 additions & 0 deletions wrestling_scoreboard_client/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: caa6bc229eab3e32eb2f37b53a5f9d22a6981474afd210c512a7546c1e1a04f6
url: "https://pub.dev"
source: hosted
version: "6.2.0"
fixnum:
dependency: transitive
description:
Expand Down Expand Up @@ -371,6 +379,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.20+1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da
url: "https://pub.dev"
source: hosted
version: "2.0.17"
flutter_riverpod:
dependency: "direct main"
description:
Expand Down
1 change: 1 addition & 0 deletions wrestling_scoreboard_client/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies:
flutter_riverpod: ^3.0.0-dev.3
riverpod_annotation: ^3.0.0-dev.3
collection: ^1.18.0
file_picker: ^6.2.0

dev_dependencies:
flutter_test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'package:wrestling_scoreboard_server/services/postgres_db.dart';
import 'entity_controller.dart';

class DatabaseController {
// TODO: migration should be handled automatically at server start.

/// Reset all tables
Future<Response> reset(Request request) async {
try {
Expand All @@ -20,18 +22,13 @@ class DatabaseController {
}
}

/// Upgrade the existing database
Future<Response> upgrade(Request request) async {
return Response.notFound('{"status": "not yet implemented"}');
}

/// Restore a database dump
/// Export a database to dump
Future<Response> export(Request request) async {
final db = PostgresDb();
final args = <String>[
'--file',
'./database/dump/${DateTime.now().toIso8601String().replaceAll(':', '-').replaceAll(RegExp(r'\.[0-9]{3}'), '')}-'
'PostgreSQL-wrestling_scoreboard-dump.sql',
// '--file',
// './database/dump/${DateTime.now().toIso8601String().replaceAll(':', '-').replaceAll(RegExp(r'\.[0-9]{3}'), '')}-'
// 'PostgreSQL-wrestling_scoreboard-dump.sql',
'--username',
db.dbUser,
'--host',
Expand All @@ -46,12 +43,26 @@ class DatabaseController {
final process = await Process.run('pg_dump', args, environment: {'PGPASSWORD': db.dbPW});

if (process.exitCode == 0) {
return Response.ok('{"status": "success"}');
return Response.ok(process.stdout);
} else {
return Response.internalServerError(body: '{"err": "${process.stderr}"}');
}
}

/// Restore a database dump
Future<Response> restore(Request request) async {
try {
final message = await request.readAsString();
File file =
File('${Directory.systemTemp.path}/${message.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0')}.sql');
await file.writeAsString(message);
await _restore(file.path);
return Response.ok('{"status": "success"}');
} catch (err) {
return Response.internalServerError(body: '{"err": "$err"}');
}
}

/// Restore the default database dump
Future<Response> restoreDefault(Request request) async {
try {
Expand All @@ -62,7 +73,9 @@ class DatabaseController {
}
}

Future<void> _restoreDefault() async {
Future<void> _restoreDefault() => _restore('./database/dump/PostgreSQL-wrestling_scoreboard-dump.sql');

Future<void> _restore(String dumpPath) async {
final db = PostgresDb();
final conn = db.connection;
{
Expand All @@ -72,7 +85,7 @@ class DatabaseController {

final args = <String>[
'--file',
'./database/dump/PostgreSQL-wrestling_scoreboard-dump.sql',
dumpPath,
'--username',
db.dbUser,
'--host',
Expand Down
6 changes: 3 additions & 3 deletions wrestling_scoreboard_server/lib/routes/api_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ class ApiRoute {
final router = Router();

final databaseController = DatabaseController();
router.get('/database/export', databaseController.export);
router.post('/database/reset', databaseController.reset);
router.post('/database/upgrade', databaseController.upgrade);
router.post('/database/export', databaseController.export);
router.post('/database/restoreDefault', databaseController.restoreDefault);
router.post('/database/restore', databaseController.restore);
router.post('/database/restore_default', databaseController.restoreDefault);

final boutConfigController = BoutConfigController();
router.post('/bout_config', boutConfigController.postSingle);
Expand Down
12 changes: 4 additions & 8 deletions wrestling_scoreboard_server/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,14 @@ <h1>Wrestling Scoreboard Server</h1>
<li><a href="/api/competitions">Competitions</a></li>
<li><a href="/api/competition_bouts">CompetitionBouts</a></li>
<li><a href="/api/weight_classs">WeightClasses</a></li>
<form action='/api/database/export' method='GET'
onsubmit="return confirm('Are you sure you want to export the database?');">
<input type='submit' value="Export">
</form>
<form action='/api/database/reset' method='POST'
onsubmit="return confirm('Are you sure you want to reset the database?');">
<input type='submit' value="Reset">
</form>
<form action='/api/database/upgrade' method='POST'
onsubmit="return confirm('Are you sure you want to upgrade the database?');">
<input type='submit' value="Upgrade">
</form>
<form action='/api/database/export' method='POST'
onsubmit="return confirm('Are you sure you want to export the database?');">
<input type='submit' value="Export">
</form>
<form action='/api/database/restore_default' method='POST'
onsubmit="return confirm('Are you sure you want to restore the default database?');">
<input type='submit' value="Restore Default">
Expand Down
4 changes: 2 additions & 2 deletions wrestling_scoreboard_server/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.11.0"
mime:
dependency: transitive
description:
Expand Down

0 comments on commit 3bec47a

Please sign in to comment.