@_nazmiforreal/flutter-ota 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +4 -3
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -2
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +22 -10
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +16 -5
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +3 -0
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +7 -8
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +12 -3
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +129 -6
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +2 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +23 -14
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +4 -0
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +266 -26
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1422 -16
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +4 -1
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +10 -9
- package/dart-src/packages/cli-tools/lib/src/config.dart +14 -19
- package/dart-src/packages/cli-tools/lib/src/operations.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +113 -10
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +59 -6
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +149 -0
- package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +14 -9
- package/dart-src/packages/core/CHANGELOG.md +6 -1
- package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +73 -1
- package/dart-src/packages/core/lib/src/bundle.dart +62 -17
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +7 -3
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +16 -0
- package/dart-src/packages/core/lib/src/changed_asset.dart +33 -3
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +24 -2
- package/dart-src/packages/core/lib/src/platform.dart +8 -0
- package/dart-src/packages/core/lib/src/semver.dart +0 -3
- package/dart-src/packages/core/lib/src/status.dart +20 -1
- package/dart-src/packages/core/lib/src/strategy.dart +10 -0
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +15 -0
- package/dart-src/packages/core/lib/src/uuid.dart +11 -0
- package/dart-src/plugins/aws/CHANGELOG.md +5 -1
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +2 -2
- package/dart-src/plugins/cloudflare/CHANGELOG.md +5 -1
- package/dart-src/plugins/plugin-core/CHANGELOG.md +5 -1
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +3 -0
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +17 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +94 -3
- package/dart-src/plugins/pocketbase/CHANGELOG.md +5 -1
- package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +2 -1
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +10 -2
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +617 -4
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +262 -42
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +9 -2
- package/dart-src/plugins/postgres/CHANGELOG.md +5 -1
- package/dart-src/plugins/supabase/CHANGELOG.md +6 -1
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +4 -0
- package/package.json +2 -2
- package/bin/flutter-ota-linux-x64 +0 -0
- package/bin/flutter-ota.js +0 -36
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -458
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -707
- package/dart-src/packages/cli-tools/pubspec.lock +0 -590
|
@@ -6,7 +6,10 @@ import '../ui/ui.dart';
|
|
|
6
6
|
/// `flutter-ota rollback` — roll a channel back to a previous bundle.
|
|
7
7
|
class RollbackCommand extends FlutterPatcherCommand {
|
|
8
8
|
RollbackCommand({this.config, this.backendOverride}) {
|
|
9
|
-
|
|
9
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
10
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null
|
|
11
|
+
? 'Backend provider [detected: $detected].'
|
|
12
|
+
: 'Backend provider.');
|
|
10
13
|
argParser.addOption('channel', abbr: 'c', help: 'Channel to roll back.');
|
|
11
14
|
argParser.addOption(
|
|
12
15
|
'bundle-id',
|
|
@@ -22,12 +22,15 @@ class StorageCommand extends FlutterPatcherCommand {
|
|
|
22
22
|
|
|
23
23
|
@override
|
|
24
24
|
String get description =>
|
|
25
|
-
'Inspect and manage bundle storage objects (
|
|
25
|
+
'Inspect and manage bundle storage objects (list / delete).';
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
class StorageListCommand extends FlutterPatcherCommand {
|
|
29
29
|
StorageListCommand({this.config, this.backendOverride}) {
|
|
30
|
-
|
|
30
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
31
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null
|
|
32
|
+
? 'Backend provider [detected: $detected].'
|
|
33
|
+
: 'Backend provider.');
|
|
31
34
|
argParser.addOption('prefix', help: 'Key prefix filter (e.g. bundles).');
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -57,12 +60,7 @@ class StorageListCommand extends FlutterPatcherCommand {
|
|
|
57
60
|
}
|
|
58
61
|
final rows = <List<String>>[];
|
|
59
62
|
for (final o in objects) {
|
|
60
|
-
|
|
61
|
-
? '${(o.size / (1024 * 1024)).toStringAsFixed(2)} MB'
|
|
62
|
-
: o.size >= 1024
|
|
63
|
-
? '${(o.size / 1024).toStringAsFixed(1)} KB'
|
|
64
|
-
: '${o.size} B';
|
|
65
|
-
rows.add([cyan(o.key), size]);
|
|
63
|
+
rows.add([cyan(o.key), humanSize(o.size)]);
|
|
66
64
|
}
|
|
67
65
|
table('${objects.length} objects', ['KEY', 'SIZE'], rows);
|
|
68
66
|
steps.summary();
|
|
@@ -71,7 +69,10 @@ class StorageListCommand extends FlutterPatcherCommand {
|
|
|
71
69
|
|
|
72
70
|
class StorageDeleteCommand extends FlutterPatcherCommand {
|
|
73
71
|
StorageDeleteCommand({this.config, this.backendOverride}) {
|
|
74
|
-
|
|
72
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
73
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null
|
|
74
|
+
? 'Backend provider [detected: $detected].'
|
|
75
|
+
: 'Backend provider.');
|
|
75
76
|
argParser.addMultiOption('key', help: 'Storage key to delete (repeatable).');
|
|
76
77
|
argParser.addOption('uri', help: 'Full storage URI to delete.');
|
|
77
78
|
}
|
|
@@ -232,8 +232,6 @@ SupabaseServiceRoleConfig resolveSupabaseConfig(
|
|
|
232
232
|
String? url,
|
|
233
233
|
String? serviceRoleKey,
|
|
234
234
|
String? anonKey,
|
|
235
|
-
String? bucket,
|
|
236
|
-
String? basePath,
|
|
237
235
|
SupabaseClientFactory? clientFactory,
|
|
238
236
|
}) {
|
|
239
237
|
final env = Platform.environment;
|
|
@@ -247,8 +245,8 @@ SupabaseServiceRoleConfig resolveSupabaseConfig(
|
|
|
247
245
|
|
|
248
246
|
if (resolvedUrl == null || resolvedUrl.isEmpty) {
|
|
249
247
|
throw StateError(
|
|
250
|
-
'Supabase URL is required. Set it via
|
|
251
|
-
|
|
248
|
+
'Supabase URL is required. Set it via SUPABASE_URL env var, '
|
|
249
|
+
'or `flutter-ota config set supabase.url <url>`.',
|
|
252
250
|
);
|
|
253
251
|
}
|
|
254
252
|
|
|
@@ -275,8 +273,6 @@ SupabaseStorageConfig resolveSupabaseStorageConfig(
|
|
|
275
273
|
url: url,
|
|
276
274
|
serviceRoleKey: serviceRoleKey,
|
|
277
275
|
anonKey: anonKey,
|
|
278
|
-
bucket: bucket,
|
|
279
|
-
basePath: basePath,
|
|
280
276
|
clientFactory: clientFactory,
|
|
281
277
|
);
|
|
282
278
|
return SupabaseStorageConfig(
|
|
@@ -284,12 +280,10 @@ SupabaseStorageConfig resolveSupabaseStorageConfig(
|
|
|
284
280
|
supabaseServiceRoleKey: db.supabaseServiceRoleKey,
|
|
285
281
|
supabaseAnonKey: db.supabaseAnonKey,
|
|
286
282
|
clientFactory: db.clientFactory,
|
|
287
|
-
bucketName:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
'bundles')
|
|
292
|
-
: (bucket ?? config.supabase.bucket ?? 'bundles'),
|
|
283
|
+
bucketName: bucket ??
|
|
284
|
+
Platform.environment['SUPABASE_BUCKET'] ??
|
|
285
|
+
config.supabase.bucket ??
|
|
286
|
+
'bundles',
|
|
293
287
|
basePath:
|
|
294
288
|
basePath ??
|
|
295
289
|
Platform.environment['SUPABASE_BASE_PATH'] ??
|
|
@@ -387,8 +381,8 @@ PostgresConfig resolvePostgresDatabaseConfig(
|
|
|
387
381
|
|
|
388
382
|
if (resolvedHost == null || resolvedHost.isEmpty) {
|
|
389
383
|
throw StateError(
|
|
390
|
-
'Postgres host is required. Set
|
|
391
|
-
|
|
384
|
+
'Postgres host is required. Set POSTGRES_HOST env var, '
|
|
385
|
+
'or `flutter-ota config set postgres.host <host>`.',
|
|
392
386
|
);
|
|
393
387
|
}
|
|
394
388
|
|
|
@@ -765,15 +759,16 @@ PocketBaseDatabaseConfig resolvePocketBaseDatabaseConfig(
|
|
|
765
759
|
|
|
766
760
|
if (resolvedUrl == null || resolvedUrl.isEmpty) {
|
|
767
761
|
throw StateError(
|
|
768
|
-
'PocketBase URL is required. Set
|
|
769
|
-
'`
|
|
762
|
+
'PocketBase URL is required. Set POCKETBASE_URL env var, '
|
|
763
|
+
'or `flutter-ota config set pocketbase.url <url>`.',
|
|
770
764
|
);
|
|
771
765
|
}
|
|
772
766
|
if (resolvedEmail == null || resolvedPassword == null) {
|
|
773
767
|
throw StateError(
|
|
774
|
-
'PocketBase admin credentials are required. Set
|
|
775
|
-
'
|
|
776
|
-
'`
|
|
768
|
+
'PocketBase admin credentials are required. Set '
|
|
769
|
+
'POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD env vars, or '
|
|
770
|
+
'`flutter-ota config set pocketbase.adminEmail/<email> '
|
|
771
|
+
'pocketbase.adminPassword/<password>`.',
|
|
777
772
|
);
|
|
778
773
|
}
|
|
779
774
|
|
|
@@ -62,7 +62,7 @@ class ListOptions {
|
|
|
62
62
|
Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
|
|
63
63
|
if (opts.targetAppVersion != null && opts.fingerprintHash != null) {
|
|
64
64
|
throw StateError(
|
|
65
|
-
'Use only one of target-app-version / fingerprint-hash
|
|
65
|
+
'Use only one of target-app-version / fingerprint-hash.',
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
if (opts.targetAppVersion == null && opts.fingerprintHash == null) {
|
|
@@ -17,7 +17,7 @@ import 'package:path/path.dart' as p;
|
|
|
17
17
|
|
|
18
18
|
/// Default PocketBase version managed by the CLI. Bumped in lockstep with
|
|
19
19
|
/// the rest of the stack.
|
|
20
|
-
const String kDefaultPocketBaseVersion = '0.
|
|
20
|
+
const String kDefaultPocketBaseVersion = '0.40.3';
|
|
21
21
|
|
|
22
22
|
/// Where the binary + extracted files live.
|
|
23
23
|
class PocketBaseInstallPaths {
|
|
@@ -4,31 +4,37 @@
|
|
|
4
4
|
/// PB reads its data directory from the `--dir` flag (default `./pb_data`).
|
|
5
5
|
/// The CLI's installer pre-creates that directory and copies hooks into
|
|
6
6
|
/// `pb_data/pb_hooks/` before the first start.
|
|
7
|
+
///
|
|
8
|
+
/// A PID file (`pb.pid`) is written to the data directory on start so that
|
|
9
|
+
/// `stop` can find and terminate the process even after the CLI restarts.
|
|
7
10
|
library;
|
|
8
11
|
|
|
9
12
|
import 'dart:async';
|
|
13
|
+
import 'dart:convert';
|
|
10
14
|
import 'dart:io';
|
|
11
15
|
|
|
12
16
|
import 'package:path/path.dart' as p;
|
|
13
17
|
|
|
14
18
|
class PocketBaseProcess {
|
|
15
|
-
PocketBaseProcess._(this.process, this.dataDir, this.binaryPath)
|
|
19
|
+
PocketBaseProcess._(this.process, this.dataDir, this.binaryPath)
|
|
20
|
+
: _exitCodeCompleted = false {
|
|
21
|
+
process.exitCode.then((_) => _exitCodeCompleted = true);
|
|
22
|
+
}
|
|
16
23
|
|
|
17
24
|
final Process process;
|
|
18
25
|
final Directory dataDir;
|
|
19
26
|
final File binaryPath;
|
|
27
|
+
bool _exitCodeCompleted;
|
|
20
28
|
|
|
21
|
-
bool get isRunning =>
|
|
29
|
+
bool get isRunning => !_exitCodeCompleted;
|
|
22
30
|
|
|
23
31
|
int get pid => process.pid;
|
|
24
32
|
|
|
25
33
|
Future<int> get exitCode => process.exitCode;
|
|
26
34
|
|
|
27
|
-
/// Returns the stdout/stderr streams merged.
|
|
28
35
|
Stream<List<int>> get output => process.stdout;
|
|
29
36
|
Stream<List<int>> get errors => process.stderr;
|
|
30
37
|
|
|
31
|
-
/// Send SIGINT (Ctrl+C) for a graceful shutdown, then SIGKILL if needed.
|
|
32
38
|
Future<void> stop({Duration timeout = const Duration(seconds: 5)}) async {
|
|
33
39
|
if (Platform.isWindows) {
|
|
34
40
|
process.kill();
|
|
@@ -59,6 +65,55 @@ class PocketBaseProcessManager {
|
|
|
59
65
|
PocketBaseProcess? _process;
|
|
60
66
|
PocketBaseProcess? get process => _process;
|
|
61
67
|
|
|
68
|
+
File get pidFile => File(p.join(dataDir.path, 'pb.pid'));
|
|
69
|
+
File get configFile => File(p.join(dataDir.path, 'pb.json'));
|
|
70
|
+
|
|
71
|
+
/// Read the stored PID from the PID file, or -1 if not present.
|
|
72
|
+
int readPid() {
|
|
73
|
+
if (!pidFile.existsSync()) return -1;
|
|
74
|
+
final content = pidFile.readAsStringSync().trim();
|
|
75
|
+
return int.tryParse(content) ?? -1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Check if the process identified by the PID file is still alive.
|
|
79
|
+
Future<bool> isRunningByPidFile() async {
|
|
80
|
+
final pid = readPid();
|
|
81
|
+
if (pid <= 0) return false;
|
|
82
|
+
try {
|
|
83
|
+
final result = await Process.run('kill', ['-0', '$pid']);
|
|
84
|
+
return result.exitCode == 0;
|
|
85
|
+
} catch (_) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
void _writePidFile(int pid) {
|
|
91
|
+
pidFile.writeAsStringSync(pid.toString());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
void _writeConfigFile(int pid) {
|
|
95
|
+
configFile.writeAsStringSync(jsonEncode({
|
|
96
|
+
'pid': pid,
|
|
97
|
+
'host': host,
|
|
98
|
+
'port': port,
|
|
99
|
+
'url': 'http://$host:$port',
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
Map<String, dynamic>? readConfig() {
|
|
104
|
+
if (!configFile.existsSync()) return null;
|
|
105
|
+
try {
|
|
106
|
+
return jsonDecode(configFile.readAsStringSync()) as Map<String, dynamic>;
|
|
107
|
+
} catch (_) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
void deletePidFile() {
|
|
113
|
+
if (pidFile.existsSync()) pidFile.deleteSync();
|
|
114
|
+
if (configFile.existsSync()) configFile.deleteSync();
|
|
115
|
+
}
|
|
116
|
+
|
|
62
117
|
/// Start PB in the background. Returns once PB is listening (or after a
|
|
63
118
|
/// short timeout waiting for it to come up).
|
|
64
119
|
Future<PocketBaseProcess> start({
|
|
@@ -82,11 +137,22 @@ class PocketBaseProcessManager {
|
|
|
82
137
|
|
|
83
138
|
final mergedEnv = <String, String>{
|
|
84
139
|
...Platform.environment,
|
|
85
|
-
if (adminEmail != null) 'PB_ADMIN_EMAIL': adminEmail,
|
|
86
|
-
if (adminPassword != null) 'PB_ADMIN_PASSWORD': adminPassword,
|
|
87
140
|
...?env,
|
|
88
141
|
};
|
|
89
142
|
|
|
143
|
+
// Create superuser if credentials provided (PB 0.40+ doesn't support
|
|
144
|
+
// PB_ADMIN_EMAIL env var, so we use the CLI directly).
|
|
145
|
+
if (adminEmail != null && adminPassword != null) {
|
|
146
|
+
final result = await Process.run(
|
|
147
|
+
binaryPath.path,
|
|
148
|
+
['superuser', 'upsert', '--dir=${dataDir.path}', adminEmail, adminPassword],
|
|
149
|
+
environment: mergedEnv,
|
|
150
|
+
);
|
|
151
|
+
if (result.exitCode != 0) {
|
|
152
|
+
// Non-fatal; serve will continue but schema install may fail.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
90
156
|
final proc = await Process.start(
|
|
91
157
|
binaryPath.path,
|
|
92
158
|
['serve', '--http=${host}:$port', '--dir=${dataDir.path}'],
|
|
@@ -94,8 +160,10 @@ class PocketBaseProcessManager {
|
|
|
94
160
|
mode: ProcessStartMode.detached,
|
|
95
161
|
);
|
|
96
162
|
|
|
97
|
-
|
|
98
|
-
|
|
163
|
+
_writePidFile(proc.pid);
|
|
164
|
+
_writeConfigFile(proc.pid);
|
|
165
|
+
|
|
166
|
+
// Wait for the health endpoint to come up.
|
|
99
167
|
final deadline = DateTime.now().add(readyTimeout);
|
|
100
168
|
while (DateTime.now().isBefore(deadline)) {
|
|
101
169
|
try {
|
|
@@ -114,10 +182,45 @@ class PocketBaseProcessManager {
|
|
|
114
182
|
return _process!;
|
|
115
183
|
}
|
|
116
184
|
|
|
185
|
+
/// Stop the running process by sending SIGINT, then SIGKILL if needed.
|
|
186
|
+
/// Also cleans up the PID file.
|
|
117
187
|
Future<void> stop() async {
|
|
188
|
+
final proc = _process;
|
|
189
|
+
if (proc != null) {
|
|
190
|
+
await proc.stop();
|
|
191
|
+
_process = null;
|
|
192
|
+
} else {
|
|
193
|
+
// Try stopping by PID file (for externally started processes).
|
|
194
|
+
final pid = readPid();
|
|
195
|
+
if (pid > 0) {
|
|
196
|
+
try {
|
|
197
|
+
Process.killPid(pid, ProcessSignal.sigint);
|
|
198
|
+
// Give it a moment to shut down gracefully.
|
|
199
|
+
await Future<void>.delayed(const Duration(seconds: 2));
|
|
200
|
+
// Force kill if still alive.
|
|
201
|
+
try {
|
|
202
|
+
final check = await Process.run('kill', ['-0', '$pid']);
|
|
203
|
+
if (check.exitCode == 0) {
|
|
204
|
+
Process.killPid(pid, ProcessSignal.sigkill);
|
|
205
|
+
}
|
|
206
|
+
} catch (_) {}
|
|
207
|
+
} catch (_) {}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
deletePidFile();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Get combined stdout+stderr log lines from the running process.
|
|
214
|
+
Stream<String> get logStream async* {
|
|
118
215
|
final proc = _process;
|
|
119
216
|
if (proc == null) return;
|
|
120
|
-
|
|
121
|
-
|
|
217
|
+
yield* proc.output
|
|
218
|
+
.transform(utf8.decoder)
|
|
219
|
+
.transform(const LineSplitter())
|
|
220
|
+
.map((line) => '[stdout] $line');
|
|
221
|
+
yield* proc.errors
|
|
222
|
+
.transform(utf8.decoder)
|
|
223
|
+
.transform(const LineSplitter())
|
|
224
|
+
.map((line) => '[stderr] $line');
|
|
122
225
|
}
|
|
123
226
|
}
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
/// Installs the flutter_ota_kit PocketBase schema (collections, indexes,
|
|
2
|
-
/// default
|
|
2
|
+
/// default superuser) on a running PB instance.
|
|
3
3
|
///
|
|
4
4
|
/// PocketBase is initialized with:
|
|
5
5
|
/// - `bundles` collection (the OTA bundle metadata)
|
|
6
6
|
/// - `channels` collection (release channels with a current_bundle pointer)
|
|
7
7
|
/// - `audit_log` collection (immutable event log)
|
|
8
|
-
/// - `
|
|
9
|
-
///
|
|
8
|
+
/// - `app_meta` collection (key-value app metadata)
|
|
9
|
+
/// - `bundles_patches` collection (per-bundle patch metadata)
|
|
10
|
+
/// - `users` is PB's built-in auth collection
|
|
11
|
+
///
|
|
12
|
+
/// The first superuser is created via `pocketbase superuser upsert`
|
|
13
|
+
/// before starting the server.
|
|
10
14
|
///
|
|
11
15
|
/// Idempotent: re-running the installer is a no-op when collections exist.
|
|
12
16
|
library;
|
|
13
17
|
|
|
14
18
|
import 'dart:convert';
|
|
15
19
|
|
|
16
|
-
import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
|
|
17
20
|
import 'package:http/http.dart' as http;
|
|
18
21
|
|
|
19
22
|
const _kBundlesCollection = r'''
|
|
@@ -95,6 +98,50 @@ const _kAuditLogCollection = r'''
|
|
|
95
98
|
}
|
|
96
99
|
''';
|
|
97
100
|
|
|
101
|
+
const _kBundlePatchesCollection = r'''
|
|
102
|
+
{
|
|
103
|
+
"name": "bundles_patches",
|
|
104
|
+
"type": "base",
|
|
105
|
+
"listRule": null,
|
|
106
|
+
"viewRule": null,
|
|
107
|
+
"createRule": null,
|
|
108
|
+
"updateRule": null,
|
|
109
|
+
"deleteRule": null,
|
|
110
|
+
"fields": [
|
|
111
|
+
{"name": "bundle_id", "type": "text", "required": true, "options": {"max": 64}},
|
|
112
|
+
{"name": "base_bundle_id", "type": "text", "required": true, "options": {"max": 64}},
|
|
113
|
+
{"name": "base_file_hash", "type": "text", "required": true, "options": {"max": 128}},
|
|
114
|
+
{"name": "patch_file_hash", "type": "text", "required": true, "options": {"max": 128}},
|
|
115
|
+
{"name": "patch_storage_uri", "type": "text", "required": true, "options": {"max": 512}},
|
|
116
|
+
{"name": "order_index", "type": "number", "options": {"min": 0}},
|
|
117
|
+
{"name": "artifact", "type": "file", "options": {"maxSelect": 1, "maxSize": 0, "mimeTypes": []}}
|
|
118
|
+
],
|
|
119
|
+
"indexes": [
|
|
120
|
+
"CREATE INDEX idx_patches_bundle ON bundles_patches (bundle_id)",
|
|
121
|
+
"CREATE INDEX idx_patches_base ON bundles_patches (base_bundle_id)"
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
''';
|
|
125
|
+
|
|
126
|
+
const _kMetaCollection = r'''
|
|
127
|
+
{
|
|
128
|
+
"name": "app_meta",
|
|
129
|
+
"type": "base",
|
|
130
|
+
"listRule": null,
|
|
131
|
+
"viewRule": null,
|
|
132
|
+
"createRule": null,
|
|
133
|
+
"updateRule": null,
|
|
134
|
+
"deleteRule": null,
|
|
135
|
+
"fields": [
|
|
136
|
+
{"name": "key", "type": "text", "required": true, "options": {"min": 1, "max": 128}},
|
|
137
|
+
{"name": "value", "type": "json"}
|
|
138
|
+
],
|
|
139
|
+
"indexes": [
|
|
140
|
+
"CREATE UNIQUE INDEX idx_meta_key ON app_meta (key)"
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
''';
|
|
144
|
+
|
|
98
145
|
class _RawAdminClient {
|
|
99
146
|
_RawAdminClient(this.baseUrl);
|
|
100
147
|
final String baseUrl;
|
|
@@ -102,7 +149,7 @@ class _RawAdminClient {
|
|
|
102
149
|
|
|
103
150
|
Future<String> authenticate(String email, String password) async {
|
|
104
151
|
final res = await http.post(
|
|
105
|
-
Uri.parse('$baseUrl/api/
|
|
152
|
+
Uri.parse('$baseUrl/api/collections/_superusers/auth-with-password'),
|
|
106
153
|
headers: {'content-type': 'application/json'},
|
|
107
154
|
body: jsonEncode({'identity': email, 'password': password}),
|
|
108
155
|
);
|
|
@@ -174,7 +221,6 @@ class PocketBaseSchemaInstaller {
|
|
|
174
221
|
required this.url,
|
|
175
222
|
required this.adminEmail,
|
|
176
223
|
required this.adminPassword,
|
|
177
|
-
http.Client? httpClient,
|
|
178
224
|
this.bundlesCollection = 'bundles',
|
|
179
225
|
this.channelsCollection = 'channels',
|
|
180
226
|
this.auditLogCollection = 'audit_log',
|
|
@@ -209,6 +255,10 @@ class PocketBaseSchemaInstaller {
|
|
|
209
255
|
String rename(String json) {
|
|
210
256
|
if (bundlesCollection != 'bundles') {
|
|
211
257
|
json = json.replaceAll('"bundles"', '"$bundlesCollection"');
|
|
258
|
+
json = json.replaceAll(
|
|
259
|
+
'bundles_patches',
|
|
260
|
+
'${bundlesCollection}_patches',
|
|
261
|
+
);
|
|
212
262
|
}
|
|
213
263
|
if (channelsCollection != 'channels') {
|
|
214
264
|
json = json.replaceAll('"channels"', '"$channelsCollection"');
|
|
@@ -219,10 +269,13 @@ class PocketBaseSchemaInstaller {
|
|
|
219
269
|
return json;
|
|
220
270
|
}
|
|
221
271
|
|
|
272
|
+
final patchesCollectionName = '${bundlesCollection}_patches';
|
|
222
273
|
return [
|
|
223
274
|
_CollectionSpec(bundlesCollection, rename(_kBundlesCollection)),
|
|
224
275
|
_CollectionSpec(channelsCollection, rename(_kChannelsCollection)),
|
|
225
276
|
_CollectionSpec(auditLogCollection, rename(_kAuditLogCollection)),
|
|
277
|
+
_CollectionSpec(patchesCollectionName, rename(_kBundlePatchesCollection)),
|
|
278
|
+
_CollectionSpec('app_meta', _kMetaCollection),
|
|
226
279
|
];
|
|
227
280
|
}
|
|
228
281
|
}
|
|
@@ -292,7 +292,7 @@ class Steps {
|
|
|
292
292
|
_echo(' ${dim('·')} $label');
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
/// Print
|
|
295
|
+
/// Print summary. Always inline — no box for a single line of text.
|
|
296
296
|
void summary() {
|
|
297
297
|
_sw.stop();
|
|
298
298
|
final ms = _sw.elapsedMilliseconds;
|
|
@@ -307,7 +307,7 @@ class Steps {
|
|
|
307
307
|
}
|
|
308
308
|
if (parts.isEmpty) parts.add('no steps');
|
|
309
309
|
parts.add(time);
|
|
310
|
-
|
|
310
|
+
_echo(' ${dim(parts.join(' · '))}');
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
/// Run a step that shows a spinner while working, then marks it
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
library;
|
|
7
7
|
|
|
8
8
|
import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
|
|
9
|
+
import 'package:http/http.dart' as http;
|
|
9
10
|
|
|
10
11
|
class MockPocketBaseClient implements PocketBaseClient {
|
|
11
12
|
MockPocketBaseClient(this.store);
|
|
@@ -182,6 +183,154 @@ class MockPocketBaseClient implements PocketBaseClient {
|
|
|
182
183
|
void close() {
|
|
183
184
|
_closed = true;
|
|
184
185
|
}
|
|
186
|
+
|
|
187
|
+
@override
|
|
188
|
+
Future<Map<String, dynamic>> healthDetailed() async => {
|
|
189
|
+
'code': 200,
|
|
190
|
+
'message': 'API is healthy.',
|
|
191
|
+
'data': {'canBackup': false},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
@override
|
|
195
|
+
Future<List<PocketBaseBackup>> listBackups() async => [];
|
|
196
|
+
|
|
197
|
+
@override
|
|
198
|
+
Future<void> createBackup({String? name}) async {}
|
|
199
|
+
|
|
200
|
+
@override
|
|
201
|
+
Future<void> deleteBackup(String key) async {}
|
|
202
|
+
|
|
203
|
+
@override
|
|
204
|
+
Future<void> restoreBackup(String key) async {}
|
|
205
|
+
|
|
206
|
+
@override
|
|
207
|
+
String backupDownloadUrl(String key, String token) => '';
|
|
208
|
+
|
|
209
|
+
@override
|
|
210
|
+
Future<List<Map<String, dynamic>>> listAdmins() async => [];
|
|
211
|
+
|
|
212
|
+
@override
|
|
213
|
+
Future<Map<String, dynamic>> createAdmin({
|
|
214
|
+
required String email,
|
|
215
|
+
required String password,
|
|
216
|
+
required String passwordConfirm,
|
|
217
|
+
}) async => {'id': 'mock_admin', 'email': email};
|
|
218
|
+
|
|
219
|
+
@override
|
|
220
|
+
Future<Map<String, dynamic>> updateAdmin(
|
|
221
|
+
String id,
|
|
222
|
+
Map<String, dynamic> data,
|
|
223
|
+
) async => data;
|
|
224
|
+
|
|
225
|
+
@override
|
|
226
|
+
Future<void> deleteAdmin(String id) async {}
|
|
227
|
+
|
|
228
|
+
@override
|
|
229
|
+
Future<List<Map<String, dynamic>>> exportCollection(String collection) async =>
|
|
230
|
+
[];
|
|
231
|
+
|
|
232
|
+
@override
|
|
233
|
+
Future<void> importCollection(
|
|
234
|
+
String collection,
|
|
235
|
+
List<Map<String, dynamic>> records,
|
|
236
|
+
) async {}
|
|
237
|
+
|
|
238
|
+
@override
|
|
239
|
+
Future<http.Response> rawRequest(
|
|
240
|
+
String method,
|
|
241
|
+
String path, {
|
|
242
|
+
Map<String, dynamic>? body,
|
|
243
|
+
}) async => http.Response('{}', 200);
|
|
244
|
+
|
|
245
|
+
@override
|
|
246
|
+
Future<List<int>> downloadFileUnauth(String url) async => [];
|
|
247
|
+
|
|
248
|
+
@override
|
|
249
|
+
Future<Map<String, dynamic>> listLogs({
|
|
250
|
+
String? filter, String? sort, int page = 1, int perPage = 30,
|
|
251
|
+
}) async => {'items': [], 'totalItems': 0};
|
|
252
|
+
|
|
253
|
+
@override
|
|
254
|
+
Future<Map<String, dynamic>> getLog(String id) async => {};
|
|
255
|
+
|
|
256
|
+
@override
|
|
257
|
+
Future<List<Map<String, dynamic>>> getLogStats({String? filter}) async => [];
|
|
258
|
+
|
|
259
|
+
@override
|
|
260
|
+
Future<void> truncateLogs() async {}
|
|
261
|
+
|
|
262
|
+
@override
|
|
263
|
+
Future<List<Map<String, dynamic>>> listCollections() async => [];
|
|
264
|
+
|
|
265
|
+
@override
|
|
266
|
+
Future<Map<String, dynamic>> getCollection(String nameOrId) async => {};
|
|
267
|
+
|
|
268
|
+
@override
|
|
269
|
+
Future<Map<String, dynamic>> createCollection(
|
|
270
|
+
Map<String, dynamic> body,
|
|
271
|
+
) async => body;
|
|
272
|
+
|
|
273
|
+
@override
|
|
274
|
+
Future<Map<String, dynamic>> updateCollection(
|
|
275
|
+
String nameOrId,
|
|
276
|
+
Map<String, dynamic> body,
|
|
277
|
+
) async => body;
|
|
278
|
+
|
|
279
|
+
@override
|
|
280
|
+
Future<void> deleteCollection(String nameOrId) async {}
|
|
281
|
+
|
|
282
|
+
@override
|
|
283
|
+
Future<void> truncateCollection(String nameOrId) async {}
|
|
284
|
+
|
|
285
|
+
@override
|
|
286
|
+
Future<void> importCollections(
|
|
287
|
+
List<Map<String, dynamic>> collections, {
|
|
288
|
+
bool deleteMissing = false,
|
|
289
|
+
}) async {}
|
|
290
|
+
|
|
291
|
+
@override
|
|
292
|
+
Future<void> uploadBackup(List<int> bytes, {String? name}) async {}
|
|
293
|
+
|
|
294
|
+
@override
|
|
295
|
+
Future<List<Map<String, dynamic>>> batch(
|
|
296
|
+
List<Map<String, dynamic>> requests,
|
|
297
|
+
) async => [];
|
|
298
|
+
|
|
299
|
+
@override
|
|
300
|
+
Future<Map<String, dynamic>> listSettings() async => {};
|
|
301
|
+
|
|
302
|
+
@override
|
|
303
|
+
Future<Map<String, dynamic>> updateSettings(
|
|
304
|
+
Map<String, dynamic> body,
|
|
305
|
+
) async => body;
|
|
306
|
+
|
|
307
|
+
@override
|
|
308
|
+
Future<void> testS3(String filesystem) async {}
|
|
309
|
+
|
|
310
|
+
@override
|
|
311
|
+
Future<void> testEmail({
|
|
312
|
+
required String email,
|
|
313
|
+
required String template,
|
|
314
|
+
String? collection,
|
|
315
|
+
}) async {}
|
|
316
|
+
|
|
317
|
+
@override
|
|
318
|
+
Future<Map<String, dynamic>> runSql(String query) async => {};
|
|
319
|
+
|
|
320
|
+
@override
|
|
321
|
+
Future<List<Map<String, dynamic>>> listCrons() async => [];
|
|
322
|
+
|
|
323
|
+
@override
|
|
324
|
+
Future<void> runCron(String jobId) async {}
|
|
325
|
+
|
|
326
|
+
@override
|
|
327
|
+
Future<Map<String, dynamic>> getCollectionScaffolds() async => {};
|
|
328
|
+
|
|
329
|
+
@override
|
|
330
|
+
Future<List<Map<String, dynamic>>> listOAuth2Providers() async => [];
|
|
331
|
+
|
|
332
|
+
@override
|
|
333
|
+
Future<Map<String, dynamic>> dryRunViewQuery(String query) async => {};
|
|
185
334
|
}
|
|
186
335
|
|
|
187
336
|
/// In-memory store backing [MockPocketBaseClient].
|