@_nazmiforreal/flutter-ota 0.1.20 → 0.1.22
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/bin/flutter-ota-linux-x64 +0 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +1 -1
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +340 -340
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +9 -5
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -2
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +106 -31
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +34 -7
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +12 -4
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +7 -8
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +41 -9
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +124 -13
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +2 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +58 -28
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +11 -5
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +283 -32
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +347 -121
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +18 -9
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +32 -15
- package/dart-src/packages/cli-tools/lib/src/config.dart +16 -22
- package/dart-src/packages/cli-tools/lib/src/operations.dart +1 -3
- package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +26 -17
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +0 -1
- package/dart-src/packages/cli-tools/lib/src/runner.dart +3 -1
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +10 -16
- package/dart-src/packages/cli-tools/pubspec.lock +7 -7
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +7 -3
- package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +13 -16
- package/dart-src/packages/core/CHANGELOG.md +15 -0
- package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +71 -1
- package/dart-src/packages/core/lib/src/bundle.dart +59 -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/packages/core/pubspec.yaml +1 -1
- package/dart-src/plugins/aws/CHANGELOG.md +18 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +0 -1
- package/dart-src/plugins/aws/pubspec.yaml +3 -3
- package/dart-src/plugins/cloudflare/CHANGELOG.md +18 -0
- package/dart-src/plugins/cloudflare/pubspec.yaml +3 -3
- package/dart-src/plugins/plugin-core/CHANGELOG.md +15 -0
- 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/plugin-core/pubspec.yaml +2 -2
- package/dart-src/plugins/pocketbase/CHANGELOG.md +18 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +3 -0
- package/dart-src/plugins/pocketbase/pubspec.yaml +3 -3
- package/dart-src/plugins/postgres/CHANGELOG.md +18 -0
- package/dart-src/plugins/postgres/pubspec.yaml +3 -3
- package/dart-src/plugins/supabase/CHANGELOG.md +18 -0
- package/dart-src/plugins/supabase/pubspec.yaml +3 -3
- package/package.json +2 -2
- package/scripts/postinstall.js +5 -1
- package/bin/flutter-ota.js +0 -36
|
@@ -10,8 +10,15 @@ import '../ui/ui.dart';
|
|
|
10
10
|
|
|
11
11
|
/// `flutter-ota migrate` — run backend SQL migrations.
|
|
12
12
|
class MigrateCommand extends FlutterPatcherCommand {
|
|
13
|
-
MigrateCommand() {
|
|
14
|
-
|
|
13
|
+
MigrateCommand({this.config, this.backendOverride}) {
|
|
14
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
15
|
+
argParser.addOption(
|
|
16
|
+
'backend',
|
|
17
|
+
abbr: 'b',
|
|
18
|
+
help: detected != null
|
|
19
|
+
? 'Backend provider [detected: $detected].'
|
|
20
|
+
: 'Backend provider.',
|
|
21
|
+
);
|
|
15
22
|
argParser.addOption(
|
|
16
23
|
'database-url',
|
|
17
24
|
help: 'Postgres connection string (or DATABASE_URL env).',
|
|
@@ -26,6 +33,34 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
26
33
|
'migrations-dir',
|
|
27
34
|
help: 'Directory of *.sql migration files (ordered by name).',
|
|
28
35
|
);
|
|
36
|
+
argParser.addOption(
|
|
37
|
+
'account-id',
|
|
38
|
+
help: 'Cloudflare account ID (or CLOUDFLARE_ACCOUNT_ID env).',
|
|
39
|
+
);
|
|
40
|
+
argParser.addOption(
|
|
41
|
+
'api-token',
|
|
42
|
+
help: 'Cloudflare API token (or CLOUDFLARE_API_TOKEN env).',
|
|
43
|
+
);
|
|
44
|
+
argParser.addOption(
|
|
45
|
+
'd1-database-id',
|
|
46
|
+
help: 'Cloudflare D1 database ID (or CLOUDFLARE_D1_DATABASE_ID env).',
|
|
47
|
+
);
|
|
48
|
+
argParser.addOption(
|
|
49
|
+
'r2-bucket',
|
|
50
|
+
help: 'Cloudflare R2 bucket name (or R2_BUCKET env, default: bundles).',
|
|
51
|
+
);
|
|
52
|
+
argParser.addOption(
|
|
53
|
+
'pocketbase-url',
|
|
54
|
+
help: 'PocketBase URL (or POCKETBASE_URL env).',
|
|
55
|
+
);
|
|
56
|
+
argParser.addOption(
|
|
57
|
+
'pocketbase-admin-email',
|
|
58
|
+
help: 'PocketBase admin email (or POCKETBASE_ADMIN_EMAIL env).',
|
|
59
|
+
);
|
|
60
|
+
argParser.addOption(
|
|
61
|
+
'pocketbase-admin-password',
|
|
62
|
+
help: 'PocketBase admin password (or POCKETBASE_ADMIN_PASSWORD env).',
|
|
63
|
+
);
|
|
29
64
|
argParser.addFlag(
|
|
30
65
|
'dry-run',
|
|
31
66
|
abbr: 'd',
|
|
@@ -33,12 +68,15 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
33
68
|
);
|
|
34
69
|
}
|
|
35
70
|
|
|
71
|
+
final FlutterPatcherConfig? config;
|
|
72
|
+
final Backend? backendOverride;
|
|
73
|
+
|
|
36
74
|
@override
|
|
37
75
|
String get name => 'migrate';
|
|
38
76
|
|
|
39
77
|
@override
|
|
40
78
|
String get description =>
|
|
41
|
-
'Run SQL migrations against the backend database (Supabase Postgres).';
|
|
79
|
+
'Run SQL migrations against the backend database (Supabase, Postgres, Cloudflare, AWS, PocketBase).';
|
|
42
80
|
|
|
43
81
|
String defaultMigrationsDir(String provider) {
|
|
44
82
|
final sub = switch (provider) {
|
|
@@ -79,19 +117,56 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
79
117
|
|
|
80
118
|
@override
|
|
81
119
|
Future<int> run() => runGuarded(() async {
|
|
82
|
-
final
|
|
120
|
+
final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
|
|
121
|
+
final provider =
|
|
122
|
+
argResults!['backend'] as String? ?? cfg?.provider ?? 'supabase';
|
|
83
123
|
|
|
84
124
|
if (provider == 'aws') {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
'
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
125
|
+
banner('migrate · aws');
|
|
126
|
+
step(
|
|
127
|
+
'AWS stores bundle metadata as JSON in S3 — no SQL migrations needed.',
|
|
128
|
+
);
|
|
129
|
+
step(
|
|
130
|
+
'The S3 bucket and object prefix are created automatically on first deploy.',
|
|
131
|
+
);
|
|
132
|
+
step('No action required. Run `flutter-ota deploy` to get started.');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (provider == 'pocketbase') {
|
|
137
|
+
final pbUrl =
|
|
138
|
+
argResults!['pocketbase-url'] as String? ??
|
|
139
|
+
Platform.environment['POCKETBASE_URL'] ??
|
|
140
|
+
cfg?.pocketbase.url;
|
|
141
|
+
final pbAdmin =
|
|
142
|
+
argResults!['pocketbase-admin-email'] as String? ??
|
|
143
|
+
Platform.environment['POCKETBASE_ADMIN_EMAIL'] ??
|
|
144
|
+
cfg?.pocketbase.adminEmail;
|
|
145
|
+
final pbPass =
|
|
146
|
+
argResults!['pocketbase-admin-password'] as String? ??
|
|
147
|
+
Platform.environment['POCKETBASE_ADMIN_PASSWORD'] ??
|
|
148
|
+
cfg?.pocketbase.adminPassword;
|
|
149
|
+
if (pbUrl == null || pbUrl.isEmpty) {
|
|
150
|
+
throw StateError(
|
|
151
|
+
'PocketBase requires a URL. Set POCKETBASE_URL env var, '
|
|
152
|
+
'or `flutter-ota init pocketbase`.',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
banner('migrate · pocketbase');
|
|
156
|
+
final installer = PocketBaseSchemaInstaller(
|
|
157
|
+
url: pbUrl,
|
|
158
|
+
adminEmail: pbAdmin ?? '',
|
|
159
|
+
adminPassword: pbPass ?? '',
|
|
160
|
+
);
|
|
161
|
+
final res = await installer.install();
|
|
162
|
+
final steps = Steps('migrate');
|
|
163
|
+
if (res.created.isNotEmpty) {
|
|
164
|
+
steps.success('Created: ${res.created.join(', ')}');
|
|
165
|
+
}
|
|
166
|
+
if (res.skipped.isNotEmpty) {
|
|
167
|
+
steps.success('Already present: ${res.skipped.join(', ')}');
|
|
168
|
+
}
|
|
169
|
+
steps.summary();
|
|
95
170
|
return;
|
|
96
171
|
}
|
|
97
172
|
|
|
@@ -110,20 +185,51 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
110
185
|
}
|
|
111
186
|
|
|
112
187
|
if (provider == 'cloudflare') {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
188
|
+
final cfCfg =
|
|
189
|
+
cfg ??
|
|
190
|
+
FlutterPatcherConfig(
|
|
191
|
+
provider: 'cloudflare',
|
|
192
|
+
supabase: const SupabaseConfigJson(),
|
|
193
|
+
cloudflare: const CloudflareConfigJson(),
|
|
194
|
+
);
|
|
195
|
+
final accountId =
|
|
196
|
+
argResults!['account-id'] as String? ??
|
|
197
|
+
Platform.environment['CLOUDFLARE_ACCOUNT_ID'] ??
|
|
198
|
+
cfCfg.cloudflare.accountId;
|
|
199
|
+
final apiToken =
|
|
200
|
+
argResults!['api-token'] as String? ??
|
|
201
|
+
Platform.environment['CLOUDFLARE_API_TOKEN'] ??
|
|
202
|
+
cfCfg.cloudflare.apiToken;
|
|
203
|
+
final dbId =
|
|
204
|
+
argResults!['d1-database-id'] as String? ??
|
|
205
|
+
Platform.environment['CLOUDFLARE_D1_DATABASE_ID'] ??
|
|
206
|
+
cfCfg.cloudflare.d1DatabaseId;
|
|
207
|
+
final r2Bucket =
|
|
208
|
+
argResults!['r2-bucket'] as String? ??
|
|
209
|
+
Platform.environment['R2_BUCKET'] ??
|
|
210
|
+
cfCfg.cloudflare.r2Bucket ??
|
|
211
|
+
'bundles';
|
|
212
|
+
|
|
213
|
+
if (accountId == null || apiToken == null) {
|
|
214
|
+
throw StateError(
|
|
215
|
+
'Cloudflare requires accountId and apiToken. Set them via '
|
|
216
|
+
'--account-id/--api-token, CLOUDFLARE_* env vars, or '
|
|
217
|
+
'`flutter-ota init cloudflare`.',
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
await _runCloudflareMigration(
|
|
221
|
+
accountId: accountId,
|
|
222
|
+
apiToken: apiToken,
|
|
223
|
+
databaseId: dbId,
|
|
224
|
+
r2Bucket: r2Bucket,
|
|
225
|
+
files: files,
|
|
226
|
+
);
|
|
121
227
|
return;
|
|
122
228
|
}
|
|
123
229
|
|
|
124
230
|
if (provider == 'supabase') {
|
|
125
|
-
final
|
|
126
|
-
|
|
231
|
+
final sbCfg =
|
|
232
|
+
cfg ??
|
|
127
233
|
FlutterPatcherConfig(
|
|
128
234
|
provider: 'supabase',
|
|
129
235
|
supabase: const SupabaseConfigJson(),
|
|
@@ -131,11 +237,11 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
131
237
|
final mgmtKey =
|
|
132
238
|
argResults!['management-key'] as String? ??
|
|
133
239
|
Platform.environment['SUPABASE_MANAGEMENT_KEY'] ??
|
|
134
|
-
|
|
240
|
+
sbCfg.supabase.managementKey;
|
|
135
241
|
final pgUrl =
|
|
136
242
|
argResults!['database-url'] as String? ??
|
|
137
243
|
Platform.environment['DATABASE_URL'] ??
|
|
138
|
-
|
|
244
|
+
sbCfg.supabase.databaseUrl;
|
|
139
245
|
if (mgmtKey != null && mgmtKey.isNotEmpty) {
|
|
140
246
|
await _runViaManagementApi(mgmtKey, files);
|
|
141
247
|
return;
|
|
@@ -194,9 +300,7 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
194
300
|
);
|
|
195
301
|
sw.stop();
|
|
196
302
|
if (res.statusCode >= 400) {
|
|
197
|
-
steps.fail(
|
|
198
|
-
'${p.basename(file.path)} (${res.statusCode}): ${res.body}',
|
|
199
|
-
);
|
|
303
|
+
steps.fail('${p.basename(file.path)} (${res.statusCode}): ${res.body}');
|
|
200
304
|
} else {
|
|
201
305
|
final ms = sw.elapsedMilliseconds;
|
|
202
306
|
final time = ms >= 1000
|
|
@@ -246,9 +350,7 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
246
350
|
}
|
|
247
351
|
}
|
|
248
352
|
final ms = sw.elapsedMilliseconds;
|
|
249
|
-
final time = ms >= 1000
|
|
250
|
-
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
251
|
-
: '${ms}ms';
|
|
353
|
+
final time = ms >= 1000 ? '${(ms / 1000).toStringAsFixed(1)}s' : '${ms}ms';
|
|
252
354
|
steps.success('Ensured storage bucket "${storage.bucketName}" in $time');
|
|
253
355
|
}
|
|
254
356
|
|
|
@@ -306,4 +408,153 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
306
408
|
}
|
|
307
409
|
steps.summary();
|
|
308
410
|
}
|
|
411
|
+
|
|
412
|
+
/// Auto-provision Cloudflare backend: create D1 database, run SQL, create R2.
|
|
413
|
+
///
|
|
414
|
+
/// Called from `init` to match Supabase's fully-automated flow.
|
|
415
|
+
static Future<void> runCloudflareMigration({
|
|
416
|
+
required String accountId,
|
|
417
|
+
required String apiToken,
|
|
418
|
+
String? databaseId,
|
|
419
|
+
String? r2Bucket,
|
|
420
|
+
}) async {
|
|
421
|
+
final sub = 'cloudflare';
|
|
422
|
+
final dir = p.join(
|
|
423
|
+
p.dirname(Platform.script.path),
|
|
424
|
+
'..',
|
|
425
|
+
'migrations',
|
|
426
|
+
sub,
|
|
427
|
+
);
|
|
428
|
+
final migrationsDir = Directory(dir);
|
|
429
|
+
if (!migrationsDir.existsSync()) {
|
|
430
|
+
throw StateError('Migrations directory not found: $dir');
|
|
431
|
+
}
|
|
432
|
+
final files =
|
|
433
|
+
migrationsDir
|
|
434
|
+
.listSync()
|
|
435
|
+
.whereType<File>()
|
|
436
|
+
.where((f) => f.path.endsWith('.sql'))
|
|
437
|
+
.toList()
|
|
438
|
+
..sort((a, b) => a.path.compareTo(b.path));
|
|
439
|
+
if (files.isEmpty) {
|
|
440
|
+
throw StateError('No *.sql files in $dir');
|
|
441
|
+
}
|
|
442
|
+
await _runCloudflareMigration(
|
|
443
|
+
accountId: accountId,
|
|
444
|
+
apiToken: apiToken,
|
|
445
|
+
databaseId: databaseId,
|
|
446
|
+
r2Bucket: r2Bucket ?? 'bundles',
|
|
447
|
+
files: files,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
static Future<void> _runCloudflareMigration({
|
|
452
|
+
required String accountId,
|
|
453
|
+
required String apiToken,
|
|
454
|
+
required String? databaseId,
|
|
455
|
+
required String r2Bucket,
|
|
456
|
+
required List<File> files,
|
|
457
|
+
}) async {
|
|
458
|
+
final headers = {
|
|
459
|
+
'Authorization': 'Bearer $apiToken',
|
|
460
|
+
'Content-Type': 'application/json',
|
|
461
|
+
};
|
|
462
|
+
final baseUrl = 'https://api.cloudflare.com/client/v4';
|
|
463
|
+
|
|
464
|
+
banner('migrate · cloudflare');
|
|
465
|
+
final steps = Steps('migrate');
|
|
466
|
+
|
|
467
|
+
// --- D1 database ---
|
|
468
|
+
var dbId = databaseId;
|
|
469
|
+
if (dbId == null || dbId.isEmpty) {
|
|
470
|
+
// Create a new D1 database
|
|
471
|
+
final sw = Stopwatch()..start();
|
|
472
|
+
final res = await http.post(
|
|
473
|
+
Uri.parse('$baseUrl/accounts/$accountId/d1/database'),
|
|
474
|
+
headers: headers,
|
|
475
|
+
body: jsonEncode({'name': 'flutter-ota-kit'}),
|
|
476
|
+
);
|
|
477
|
+
sw.stop();
|
|
478
|
+
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
|
479
|
+
if (res.statusCode >= 400 || body['success'] != true) {
|
|
480
|
+
final errors = body['errors'] as List? ?? [];
|
|
481
|
+
final msg = errors.isNotEmpty ? errors.first['message'] : res.body;
|
|
482
|
+
steps.fail('Could not create D1 database: $msg');
|
|
483
|
+
steps.summary();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
dbId = body['result']?['uuid'] as String?;
|
|
487
|
+
if (dbId == null || dbId.isEmpty) {
|
|
488
|
+
steps.fail('D1 database created but no UUID returned');
|
|
489
|
+
steps.summary();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
final ms = sw.elapsedMilliseconds;
|
|
493
|
+
final time = ms >= 1000
|
|
494
|
+
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
495
|
+
: '${ms}ms';
|
|
496
|
+
steps.success('Created D1 database ($dbId) in $time');
|
|
497
|
+
} else {
|
|
498
|
+
steps.success('Using existing D1 database ($dbId)');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// --- Run SQL migrations against D1 ---
|
|
502
|
+
for (final file in files) {
|
|
503
|
+
final sw = Stopwatch()..start();
|
|
504
|
+
final sql = file.readAsStringSync();
|
|
505
|
+
final res = await http.post(
|
|
506
|
+
Uri.parse('$baseUrl/accounts/$accountId/d1/database/$dbId/query'),
|
|
507
|
+
headers: headers,
|
|
508
|
+
body: jsonEncode({'sql': sql}),
|
|
509
|
+
);
|
|
510
|
+
sw.stop();
|
|
511
|
+
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
|
512
|
+
if (res.statusCode >= 400 || body['success'] != true) {
|
|
513
|
+
final errors = body['errors'] as List? ?? [];
|
|
514
|
+
final msg = errors.isNotEmpty ? errors.first['message'] : res.body;
|
|
515
|
+
steps.fail('${p.basename(file.path)} ($res.statusCode): $msg');
|
|
516
|
+
} else {
|
|
517
|
+
final ms = sw.elapsedMilliseconds;
|
|
518
|
+
final time = ms >= 1000
|
|
519
|
+
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
520
|
+
: '${ms}ms';
|
|
521
|
+
steps.success('Applied ${p.basename(file.path)} in $time');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// --- R2 bucket ---
|
|
526
|
+
final sw = Stopwatch()..start();
|
|
527
|
+
final r2Res = await http.put(
|
|
528
|
+
Uri.parse('$baseUrl/accounts/$accountId/s3/buckets/$r2Bucket'),
|
|
529
|
+
headers: headers,
|
|
530
|
+
);
|
|
531
|
+
sw.stop();
|
|
532
|
+
final r2Body = jsonDecode(r2Res.body) as Map<String, dynamic>;
|
|
533
|
+
if (r2Res.statusCode >= 400 || r2Body['success'] != true) {
|
|
534
|
+
final errors = r2Body['errors'] as List? ?? [];
|
|
535
|
+
final msg = errors.isNotEmpty ? errors.first['message'] : r2Res.body;
|
|
536
|
+
// Bucket already exists is not an error
|
|
537
|
+
if (!msg.toLowerCase().contains('already exists') &&
|
|
538
|
+
r2Res.statusCode != 409) {
|
|
539
|
+
steps.fail('Could not create R2 bucket "$r2Bucket": $msg');
|
|
540
|
+
} else {
|
|
541
|
+
final ms = sw.elapsedMilliseconds;
|
|
542
|
+
final time = ms >= 1000
|
|
543
|
+
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
544
|
+
: '${ms}ms';
|
|
545
|
+
steps.success('Ensured R2 bucket "$r2Bucket" in $time');
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
final ms = sw.elapsedMilliseconds;
|
|
549
|
+
final time = ms >= 1000
|
|
550
|
+
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
551
|
+
: '${ms}ms';
|
|
552
|
+
steps.success('Created R2 bucket "$r2Bucket" in $time');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
steps.summary();
|
|
556
|
+
if (steps.hasErrors) {
|
|
557
|
+
throw StateError('One or more migrations failed.');
|
|
558
|
+
}
|
|
559
|
+
}
|
|
309
560
|
}
|