@_nazmiforreal/flutter-ota 0.1.6 → 0.1.8
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/README.md +13 -13
- package/bin/{flutter-patcher-linux-x64 → flutter-ota-linux-x64} +0 -0
- package/bin/{flutter-patcher.js → flutter-ota.js} +4 -4
- package/dart-src/packages/cli-tools/bin/{flutter_patcher.dart → flutter_ota_kit.dart} +2 -2
- package/dart-src/packages/cli-tools/lib/{flutter_patcher_cli.dart → flutter_ota_kit_cli.dart} +5 -3
- package/dart-src/packages/cli-tools/lib/src/backend.dart +6 -25
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +10 -4
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +5 -3
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +279 -10
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +7 -7
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +5 -5
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +3 -3
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +294 -45
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +59 -24
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +5 -5
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +117 -0
- package/dart-src/packages/cli-tools/lib/src/config.dart +29 -78
- package/dart-src/packages/cli-tools/lib/src/operations.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/pack.dart +12 -6
- package/dart-src/packages/cli-tools/lib/src/runner.dart +21 -3
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
- package/dart-src/packages/cli-tools/lib/src/verbose.dart +3 -0
- package/dart-src/packages/cli-tools/migrations/aws/README.md +7 -0
- package/dart-src/packages/cli-tools/migrations/postgres/bundles.sql +45 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_target_app_version_list.sql +21 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_app_version.sql +126 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
- package/dart-src/packages/cli-tools/migrations/postgres/hash_user_id.sql +30 -0
- package/dart-src/packages/cli-tools/migrations/postgres/is_device_eligible.sql +237 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +11 -0
- package/dart-src/packages/cli-tools/pubspec.yaml +8 -10
- package/dart-src/packages/cli-tools/test/backends_test.dart +4 -4
- package/dart-src/packages/cli-tools/test/build_pack_test.dart +1 -1
- package/dart-src/packages/cli-tools/test/cli_test.dart +3 -3
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +7 -7
- package/dart-src/packages/core/lib/{flutter_patcher_core.dart → flutter_ota_kit_core.dart} +1 -1
- package/dart-src/packages/core/pubspec.yaml +2 -2
- package/dart-src/packages/core/test/rollout_test.dart +1 -1
- package/dart-src/packages/core/test/semver_test.dart +1 -1
- package/dart-src/packages/core/test/types_test.dart +2 -2
- package/dart-src/packages/core/test/uuid_test.dart +1 -1
- package/dart-src/packages/core/tool/dbg.dart +1 -1
- package/dart-src/plugins/aws/lib/{flutter_patcher_aws.dart → flutter_ota_kit_aws.dart} +0 -2
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_database.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +25 -3
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +1 -1
- package/dart-src/plugins/aws/pubspec.yaml +4 -4
- package/dart-src/plugins/aws/test/aws_database_test.dart +3 -3
- package/dart-src/plugins/aws/test/aws_storage_test.dart +2 -2
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +2 -2
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +2 -2
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +1 -1
- package/dart-src/plugins/cloudflare/pubspec.yaml +4 -4
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +2 -2
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +2 -2
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +2 -2
- package/dart-src/plugins/plugin-core/lib/{flutter_patcher_plugin_core.dart → flutter_ota_kit_plugin_core.dart} +1 -1
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/types.dart +2 -2
- package/dart-src/plugins/plugin-core/pubspec.yaml +3 -3
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +3 -3
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +5 -5
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +3 -3
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +1 -1
- package/dart-src/plugins/postgres/lib/{flutter_patcher_postgres.dart → flutter_ota_kit_postgres.dart} +1 -1
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +1 -1
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +2 -2
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +2 -2
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +9 -9
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +1 -1
- package/dart-src/plugins/postgres/pubspec.yaml +4 -4
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +2 -2
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +2 -2
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +2 -2
- package/dart-src/plugins/supabase/lib/edge.dart +1 -1
- package/dart-src/plugins/supabase/lib/{flutter_patcher_supabase.dart → flutter_ota_kit_supabase.dart} +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +5 -1
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +2 -2
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +2 -5
- package/dart-src/plugins/supabase/pubspec.yaml +4 -4
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +5 -5
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +3 -3
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +3 -3
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +2 -2
- package/migrations/aws/README.md +7 -0
- package/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
- package/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
- package/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
- package/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
- package/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
- package/migrations/cloudflare/bundles.sql +42 -0
- package/migrations/postgres/bundles.sql +45 -0
- package/migrations/postgres/get_target_app_version_list.sql +21 -0
- package/migrations/postgres/get_update_info_by_app_version.sql +126 -0
- package/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
- package/migrations/postgres/hash_user_id.sql +30 -0
- package/migrations/postgres/is_device_eligible.sql +237 -0
- package/package.json +4 -4
- package/scripts/postinstall.js +15 -15
- package/scripts/vendor.sh +1 -1
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -496
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -743
- package/dart-src/packages/cli-tools/pubspec.lock +0 -638
- package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +0 -315
- package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +0 -72
- package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +0 -190
- package/dart-src/plugins/standalone/.gitkeep +0 -0
- package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +0 -12
- package/dart-src/plugins/standalone/lib/src/standalone_config.dart +0 -107
- package/dart-src/plugins/standalone/lib/src/standalone_database.dart +0 -233
- package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +0 -175
- package/dart-src/plugins/standalone/pubspec.yaml +0 -18
- package/dart-src/plugins/standalone/test/standalone_test.dart +0 -264
- /package/dart-src/plugins/cloudflare/lib/{flutter_patcher_cloudflare.dart → flutter_ota_kit_cloudflare.dart} +0 -0
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import 'dart:convert' show jsonDecode, jsonEncode;
|
|
2
|
-
|
|
3
|
-
import 'package:flutter_patcher_core/flutter_patcher_core.dart'
|
|
4
|
-
show
|
|
5
|
-
AppVersionGetBundlesArgs,
|
|
6
|
-
Bundle,
|
|
7
|
-
FingerprintGetBundlesArgs,
|
|
8
|
-
GetBundlesArgs,
|
|
9
|
-
UpdateInfo,
|
|
10
|
-
UpdateStatus;
|
|
11
|
-
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
12
|
-
show
|
|
13
|
-
AbstractDatabasePlugin,
|
|
14
|
-
BundleChange,
|
|
15
|
-
BundleChangeOperation,
|
|
16
|
-
DatabaseBundleQueryOptions,
|
|
17
|
-
Paginated,
|
|
18
|
-
PaginationInfo,
|
|
19
|
-
createDatabasePlugin;
|
|
20
|
-
import 'package:http/http.dart' as http;
|
|
21
|
-
import 'package:http/http.dart';
|
|
22
|
-
|
|
23
|
-
import 'standalone_config.dart'
|
|
24
|
-
show
|
|
25
|
-
StandaloneRepositoryConfig,
|
|
26
|
-
StandaloneRepositoryRoutes,
|
|
27
|
-
buildPath,
|
|
28
|
-
joinUrl,
|
|
29
|
-
mergeHeaders;
|
|
30
|
-
|
|
31
|
-
/// Double-curried standalone repository factory.
|
|
32
|
-
///
|
|
33
|
-
/// Faithful port of hot-updater `plugins/standalone/src/standaloneRepository.ts`.
|
|
34
|
-
DatabasePluginLike standaloneRepository(
|
|
35
|
-
StandaloneRepositoryConfig config, [
|
|
36
|
-
Object? hooks,
|
|
37
|
-
]) =>
|
|
38
|
-
createDatabasePlugin<StandaloneRepositoryConfig>(
|
|
39
|
-
name: 'standalone-repository',
|
|
40
|
-
factory: (c) => _StandaloneRepository(c),
|
|
41
|
-
)(config);
|
|
42
|
-
|
|
43
|
-
/// Alias so the curried factory can be used uniformly with other backends.
|
|
44
|
-
typedef DatabasePluginLike = dynamic;
|
|
45
|
-
|
|
46
|
-
class _StandaloneRepository implements AbstractDatabasePlugin {
|
|
47
|
-
_StandaloneRepository(this.config)
|
|
48
|
-
: routes = config.routes ?? const StandaloneRepositoryRoutes();
|
|
49
|
-
|
|
50
|
-
final StandaloneRepositoryConfig config;
|
|
51
|
-
final StandaloneRepositoryRoutes routes;
|
|
52
|
-
|
|
53
|
-
Client _client() => config.clientFactory?.call() ?? http.Client();
|
|
54
|
-
|
|
55
|
-
Map<String, String> _headers([Map<String, String>? extra]) =>
|
|
56
|
-
mergeHeaders(config.commonHeaders, extra);
|
|
57
|
-
|
|
58
|
-
Future<dynamic> _decode(Response res) {
|
|
59
|
-
if (res.statusCode >= 400) {
|
|
60
|
-
throw StateError(
|
|
61
|
-
'standalone repository request failed (${res.statusCode}): '
|
|
62
|
-
'${res.body}',
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
return Future.value(jsonDecode(res.body));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
@override
|
|
69
|
-
bool get supportsCursorPagination => true;
|
|
70
|
-
|
|
71
|
-
@override
|
|
72
|
-
Future<List<String>> getChannels() async {
|
|
73
|
-
final client = _client();
|
|
74
|
-
final res = await client.get(
|
|
75
|
-
Uri.parse(joinUrl(config.baseUrl, routes.channels)),
|
|
76
|
-
headers: _headers({'Cache-Control': 'no-cache'}),
|
|
77
|
-
);
|
|
78
|
-
final decoded = await _decode(res) as Map<String, dynamic>;
|
|
79
|
-
final data = decoded['data'] as Map<String, dynamic>;
|
|
80
|
-
final channels = (data['channels'] as List?)?.cast<String>() ?? const [];
|
|
81
|
-
return channels;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
@override
|
|
85
|
-
Future<Bundle?> getBundleById(String bundleId) async {
|
|
86
|
-
final client = _client();
|
|
87
|
-
final res = await client.get(
|
|
88
|
-
Uri.parse(joinUrl(config.baseUrl, buildPath(routes.retrieve, id: bundleId))),
|
|
89
|
-
headers: _headers({'Accept': 'application/json'}),
|
|
90
|
-
);
|
|
91
|
-
if (res.statusCode == 404) return null;
|
|
92
|
-
final decoded = await _decode(res);
|
|
93
|
-
return Bundle.fromJson(decoded as Map<String, dynamic>);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
@override
|
|
97
|
-
Future<Paginated<List<Bundle>>> getBundles(
|
|
98
|
-
DatabaseBundleQueryOptions options,
|
|
99
|
-
) async {
|
|
100
|
-
final client = _client();
|
|
101
|
-
final query = <String, String>{};
|
|
102
|
-
final where = options.where;
|
|
103
|
-
if (where != null) {
|
|
104
|
-
if (where.channel != null) query['channel'] = where.channel!;
|
|
105
|
-
if (where.platform != null) query['platform'] = where.platform!.value;
|
|
106
|
-
if (where.enabled != null) query['enabled'] = '${where.enabled}';
|
|
107
|
-
final id = where.id;
|
|
108
|
-
if (id != null) {
|
|
109
|
-
if (id.eq != null) query['idEq'] = id.eq!;
|
|
110
|
-
if (id.gt != null) query['idGt'] = id.gt!;
|
|
111
|
-
if (id.gte != null) query['idGte'] = id.gte!;
|
|
112
|
-
if (id.lt != null) query['idLt'] = id.lt!;
|
|
113
|
-
if (id.lte != null) query['idLte'] = id.lte!;
|
|
114
|
-
if (id.ins != null) query['idIn'] = id.ins!.join(',');
|
|
115
|
-
}
|
|
116
|
-
if (where.targetAppVersion != null) {
|
|
117
|
-
query['targetAppVersion'] = where.targetAppVersion!;
|
|
118
|
-
}
|
|
119
|
-
if (where.targetAppVersionIn != null) {
|
|
120
|
-
query['targetAppVersionIn'] = where.targetAppVersionIn!.join(',');
|
|
121
|
-
}
|
|
122
|
-
if (where.targetAppVersionNotNull != null) {
|
|
123
|
-
query['targetAppVersionNotNull'] = '${where.targetAppVersionNotNull}';
|
|
124
|
-
}
|
|
125
|
-
if (where.fingerprintHash != null) {
|
|
126
|
-
query['fingerprintHash'] = where.fingerprintHash!;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
query['limit'] = '${options.limit}';
|
|
130
|
-
if (options.page != null) query['page'] = '${options.page}';
|
|
131
|
-
if (options.offset != null) query['offset'] = '${options.offset}';
|
|
132
|
-
if (options.cursor?.after != null) query['after'] = options.cursor!.after!;
|
|
133
|
-
if (options.cursor?.before != null) {
|
|
134
|
-
query['before'] = options.cursor!.before!;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
final uri = Uri.parse(joinUrl(config.baseUrl, routes.list))
|
|
138
|
-
.replace(queryParameters: query);
|
|
139
|
-
final res = await client.get(
|
|
140
|
-
uri,
|
|
141
|
-
headers: _headers({'Cache-Control': 'no-cache'}),
|
|
142
|
-
);
|
|
143
|
-
final decoded = await _decode(res) as Map<String, dynamic>;
|
|
144
|
-
final data = (decoded['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
|
145
|
-
final bundles = data.map(Bundle.fromJson).toList();
|
|
146
|
-
final pagination =
|
|
147
|
-
(decoded['pagination'] as Map?)?.cast<String, dynamic>() ?? {};
|
|
148
|
-
return Paginated(
|
|
149
|
-
data: bundles,
|
|
150
|
-
pagination: PaginationInfo(
|
|
151
|
-
total: pagination['total'] as int? ?? bundles.length,
|
|
152
|
-
hasNextPage: pagination['hasNextPage'] as bool? ?? false,
|
|
153
|
-
hasPreviousPage: pagination['hasPreviousPage'] as bool? ?? false,
|
|
154
|
-
currentPage: pagination['currentPage'] as int? ?? 1,
|
|
155
|
-
totalPages: pagination['totalPages'] as int? ?? 1,
|
|
156
|
-
nextCursor: pagination['nextCursor'] as String?,
|
|
157
|
-
previousCursor: pagination['previousCursor'] as String?,
|
|
158
|
-
),
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
@override
|
|
163
|
-
Future<void> commitBundle({
|
|
164
|
-
required List<BundleChange> changedSets,
|
|
165
|
-
}) async {
|
|
166
|
-
final client = _client();
|
|
167
|
-
for (final change in changedSets) {
|
|
168
|
-
final bundle = change.data;
|
|
169
|
-
switch (change.operation) {
|
|
170
|
-
case BundleChangeOperation.insert:
|
|
171
|
-
final res = await client.post(
|
|
172
|
-
Uri.parse(joinUrl(config.baseUrl, routes.create)),
|
|
173
|
-
headers: _headers({'Content-Type': 'application/json'}),
|
|
174
|
-
body: jsonEncode([bundle.toJson()]),
|
|
175
|
-
);
|
|
176
|
-
await _decode(res);
|
|
177
|
-
case BundleChangeOperation.update:
|
|
178
|
-
final res = await client.patch(
|
|
179
|
-
Uri.parse(joinUrl(config.baseUrl, buildPath(routes.update, id: bundle.id))),
|
|
180
|
-
headers: _headers({'Content-Type': 'application/json'}),
|
|
181
|
-
body: jsonEncode(bundle.toJson()),
|
|
182
|
-
);
|
|
183
|
-
await _decode(res);
|
|
184
|
-
case BundleChangeOperation.delete:
|
|
185
|
-
final res = await client.delete(
|
|
186
|
-
Uri.parse(joinUrl(config.baseUrl, buildPath(routes.delete, id: bundle.id))),
|
|
187
|
-
);
|
|
188
|
-
await _decode(res);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
@override
|
|
194
|
-
Future<UpdateInfo?> getUpdateInfo(GetBundlesArgs args) async {
|
|
195
|
-
final client = _client();
|
|
196
|
-
final path = args is AppVersionGetBundlesArgs
|
|
197
|
-
? _appVersionPath(args)
|
|
198
|
-
: _fingerprintPath(args);
|
|
199
|
-
final res = await client.get(
|
|
200
|
-
Uri.parse(joinUrl(config.baseUrl, path)),
|
|
201
|
-
headers: _headers({'Cache-Control': 'no-cache'}),
|
|
202
|
-
);
|
|
203
|
-
if (res.statusCode == 404) return null;
|
|
204
|
-
final decoded = await _decode(res) as Map<String, dynamic>;
|
|
205
|
-
if (decoded['status'] == 'UP_TO_DATE') return null;
|
|
206
|
-
return UpdateInfo(
|
|
207
|
-
id: decoded['id'] as String,
|
|
208
|
-
shouldForceUpdate: (decoded['shouldForceUpdate'] ?? false) as bool,
|
|
209
|
-
message: decoded['message'] as String?,
|
|
210
|
-
status: UpdateStatus.fromValue(
|
|
211
|
-
decoded['status'] as String? ?? 'update-available',
|
|
212
|
-
),
|
|
213
|
-
storageUri: decoded['fileUrl'] as String?,
|
|
214
|
-
fileHash: decoded['fileHash'] as String?,
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
String _fingerprintPath(GetBundlesArgs args) {
|
|
219
|
-
final fp = args is FingerprintGetBundlesArgs ? args.fingerprintHash : 'unknown';
|
|
220
|
-
final cohort = args.cohort != null ? '/${args.cohort}' : '';
|
|
221
|
-
return '/api/fingerprint/${args.platform.value}/$fp/'
|
|
222
|
-
'${args.channel}/${args.minBundleId}/${args.bundleId}$cohort';
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
String _appVersionPath(AppVersionGetBundlesArgs args) {
|
|
226
|
-
final cohort = args.cohort != null ? '/${args.cohort}' : '';
|
|
227
|
-
return '/api/app-version/${args.platform.value}/${args.appVersion}/'
|
|
228
|
-
'${args.channel}/${args.minBundleId}/${args.bundleId}$cohort';
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
@override
|
|
232
|
-
Future<void> onUnmount() async {}
|
|
233
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import 'dart:convert' show jsonDecode, jsonEncode;
|
|
2
|
-
import 'dart:io' show File;
|
|
3
|
-
|
|
4
|
-
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
5
|
-
show
|
|
6
|
-
NodeStorageProfile,
|
|
7
|
-
RuntimeStorageProfile,
|
|
8
|
-
StorageObject,
|
|
9
|
-
createUniversalStoragePlugin;
|
|
10
|
-
import 'package:http/http.dart' as http;
|
|
11
|
-
import 'package:http/http.dart';
|
|
12
|
-
|
|
13
|
-
import 'standalone_config.dart'
|
|
14
|
-
show
|
|
15
|
-
StandaloneStorageConfig,
|
|
16
|
-
StandaloneStorageRoutes,
|
|
17
|
-
joinUrl,
|
|
18
|
-
mergeHeaders;
|
|
19
|
-
|
|
20
|
-
/// Single-curried standalone storage factory.
|
|
21
|
-
///
|
|
22
|
-
/// Faithful port of hot-updater `plugins/standalone/src/standaloneStorage.ts`.
|
|
23
|
-
StoragePluginLike standaloneStorage(StandaloneStorageConfig config) =>
|
|
24
|
-
createUniversalStoragePlugin<StandaloneStorageConfig>(
|
|
25
|
-
name: 'standalone-storage',
|
|
26
|
-
supportedProtocol: 'http',
|
|
27
|
-
factory: (c) {
|
|
28
|
-
final storage = _StandaloneStorage(c);
|
|
29
|
-
return (node: storage, runtime: storage);
|
|
30
|
-
},
|
|
31
|
-
)(config);
|
|
32
|
-
|
|
33
|
-
/// Alias so the curried factory can be used uniformly with other backends.
|
|
34
|
-
typedef StoragePluginLike = dynamic;
|
|
35
|
-
|
|
36
|
-
class _StandaloneStorage implements NodeStorageProfile, RuntimeStorageProfile {
|
|
37
|
-
_StandaloneStorage(this.config)
|
|
38
|
-
: routes = config.routes ?? const StandaloneStorageRoutes();
|
|
39
|
-
|
|
40
|
-
final StandaloneStorageConfig config;
|
|
41
|
-
final StandaloneStorageRoutes routes;
|
|
42
|
-
|
|
43
|
-
Client _client() => config.clientFactory?.call() ?? http.Client();
|
|
44
|
-
|
|
45
|
-
Map<String, String> _headers([Map<String, String>? extra]) =>
|
|
46
|
-
mergeHeaders(config.commonHeaders, extra);
|
|
47
|
-
|
|
48
|
-
// --- Node profile ---------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
@override
|
|
51
|
-
Future<Map<String, String>> upload(String key, String filePath) async {
|
|
52
|
-
final client = _client();
|
|
53
|
-
final bytes = await File(filePath).readAsBytes();
|
|
54
|
-
final uri = Uri.parse(joinUrl(config.baseUrl, routes.upload))
|
|
55
|
-
.replace(queryParameters: {'key': key});
|
|
56
|
-
final res = await client.post(
|
|
57
|
-
uri,
|
|
58
|
-
headers: _headers({'Content-Type': 'application/octet-stream'}),
|
|
59
|
-
body: bytes,
|
|
60
|
-
);
|
|
61
|
-
if (res.statusCode >= 400) {
|
|
62
|
-
throw StateError(
|
|
63
|
-
'standalone storage upload failed (${res.statusCode}): ${res.body}',
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
final decoded = jsonDecode(res.body) as Map<String, dynamic>;
|
|
67
|
-
final storageUri = decoded['storageUri'] as String?;
|
|
68
|
-
if (storageUri == null) {
|
|
69
|
-
throw StateError('standalone storage upload returned no storageUri');
|
|
70
|
-
}
|
|
71
|
-
return {'storageUri': storageUri};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
@override
|
|
75
|
-
Future<bool> exists(String storageUri) async {
|
|
76
|
-
final fileUrl = (await getDownloadUrl(storageUri))['fileUrl'];
|
|
77
|
-
if (fileUrl == null) return false;
|
|
78
|
-
final client = _client();
|
|
79
|
-
final res = await client.head(Uri.parse(fileUrl));
|
|
80
|
-
return res.statusCode >= 200 && res.statusCode < 300;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@override
|
|
84
|
-
Future<void> delete(String storageUri) async {
|
|
85
|
-
final client = _client();
|
|
86
|
-
final res = await client.delete(
|
|
87
|
-
Uri.parse(joinUrl(config.baseUrl, routes.delete)),
|
|
88
|
-
headers: _headers({'Content-Type': 'application/json'}),
|
|
89
|
-
body: jsonEncode({'storageUri': storageUri}),
|
|
90
|
-
);
|
|
91
|
-
if (res.statusCode >= 400) {
|
|
92
|
-
throw StateError(
|
|
93
|
-
'standalone storage delete failed (${res.statusCode}): ${res.body}',
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@override
|
|
99
|
-
Future<void> downloadFile(String storageUri, String filePath) async {
|
|
100
|
-
final fileUrl = (await getDownloadUrl(storageUri))['fileUrl'];
|
|
101
|
-
if (fileUrl == null) {
|
|
102
|
-
throw StateError('standalone storage could not resolve download url');
|
|
103
|
-
}
|
|
104
|
-
final client = _client();
|
|
105
|
-
final res = await client.get(Uri.parse(fileUrl));
|
|
106
|
-
if (res.statusCode >= 400) {
|
|
107
|
-
throw StateError(
|
|
108
|
-
'standalone storage download failed (${res.statusCode})',
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
await File(filePath).writeAsBytes(res.bodyBytes);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
@override
|
|
115
|
-
Future<List<StorageObject>> listObjects([String? prefix]) async {
|
|
116
|
-
final client = _client();
|
|
117
|
-
final uri = Uri.parse(joinUrl(config.baseUrl, routes.list))
|
|
118
|
-
.replace(queryParameters: prefix != null ? {'prefix': prefix} : null);
|
|
119
|
-
final res = await client.get(uri);
|
|
120
|
-
if (res.statusCode >= 400) return const [];
|
|
121
|
-
final decoded = jsonDecode(res.body) as Map<String, dynamic>;
|
|
122
|
-
final data = (decoded['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
|
123
|
-
return data
|
|
124
|
-
.map((o) => StorageObject(
|
|
125
|
-
key: o['key'] as String,
|
|
126
|
-
storageUri: o['storageUri'] as String,
|
|
127
|
-
size: o['size'] as int? ?? 0,
|
|
128
|
-
))
|
|
129
|
-
.toList();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
@override
|
|
133
|
-
Future<void> deleteObjects(List<String> keys) async {
|
|
134
|
-
for (final key in keys) {
|
|
135
|
-
await delete(key);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// --- Runtime profile ------------------------------------------------------
|
|
140
|
-
|
|
141
|
-
@override
|
|
142
|
-
Future<Map<String, String>> getDownloadUrl(String storageUri) async {
|
|
143
|
-
final client = _client();
|
|
144
|
-
final res = await client.post(
|
|
145
|
-
Uri.parse(joinUrl(config.baseUrl, routes.getDownloadUrl)),
|
|
146
|
-
headers: _headers({'Content-Type': 'application/json'}),
|
|
147
|
-
body: jsonEncode({'storageUri': storageUri}),
|
|
148
|
-
);
|
|
149
|
-
if (res.statusCode >= 400) {
|
|
150
|
-
throw StateError(
|
|
151
|
-
'standalone storage getDownloadUrl failed (${res.statusCode}): '
|
|
152
|
-
'${res.body}',
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
final decoded = jsonDecode(res.body) as Map<String, dynamic>;
|
|
156
|
-
final fileUrl = decoded['fileUrl'] as String?;
|
|
157
|
-
if (fileUrl == null) {
|
|
158
|
-
throw StateError('standalone storage getDownloadUrl returned no fileUrl');
|
|
159
|
-
}
|
|
160
|
-
return {'fileUrl': fileUrl};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
@override
|
|
164
|
-
Future<String?> readText(String storageUri) async {
|
|
165
|
-
final client = _client();
|
|
166
|
-
final res = await client.post(
|
|
167
|
-
Uri.parse(joinUrl(config.baseUrl, routes.readText)),
|
|
168
|
-
headers: _headers({'Content-Type': 'application/json'}),
|
|
169
|
-
body: jsonEncode({'storageUri': storageUri}),
|
|
170
|
-
);
|
|
171
|
-
if (res.statusCode >= 400) return null;
|
|
172
|
-
final decoded = jsonDecode(res.body) as Map<String, dynamic>;
|
|
173
|
-
return decoded['data'] as String?;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
name: flutter_patcher_standalone
|
|
2
|
-
description: Standalone self-hosted backend for flutter_patcher (REST repository + storage).
|
|
3
|
-
version: 0.1.0
|
|
4
|
-
publish_to: none
|
|
5
|
-
|
|
6
|
-
environment:
|
|
7
|
-
sdk: ">=3.3.0 <4.0.0"
|
|
8
|
-
|
|
9
|
-
dependencies:
|
|
10
|
-
flutter_patcher_core:
|
|
11
|
-
path: ../../packages/core
|
|
12
|
-
flutter_patcher_plugin_core:
|
|
13
|
-
path: ../../plugins/plugin-core
|
|
14
|
-
http: ^1.2.0
|
|
15
|
-
|
|
16
|
-
dev_dependencies:
|
|
17
|
-
test: ^1.25.0
|
|
18
|
-
lints: ^4.0.0
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import 'dart:convert' show jsonDecode, jsonEncode;
|
|
2
|
-
import 'dart:io' show Directory, File;
|
|
3
|
-
|
|
4
|
-
import 'package:flutter_patcher_core/flutter_patcher_core.dart'
|
|
5
|
-
show Bundle, FingerprintGetBundlesArgs, Platform;
|
|
6
|
-
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
7
|
-
show DatabaseBundleQueryOptions;
|
|
8
|
-
import 'package:flutter_patcher_standalone/flutter_patcher_standalone.dart'
|
|
9
|
-
show
|
|
10
|
-
StandaloneRepositoryConfig,
|
|
11
|
-
StandaloneStorageConfig,
|
|
12
|
-
standaloneRepository,
|
|
13
|
-
standaloneStorage;
|
|
14
|
-
import 'package:http/http.dart' as http;
|
|
15
|
-
import 'package:http/testing.dart' show MockClient;
|
|
16
|
-
import 'package:test/test.dart';
|
|
17
|
-
|
|
18
|
-
/// A tiny in-memory router for the standalone REST contract.
|
|
19
|
-
Future<http.Response> _router(http.Request request) async {
|
|
20
|
-
final path = request.url.path;
|
|
21
|
-
final method = request.method;
|
|
22
|
-
|
|
23
|
-
if (path == '/api/bundles/channels' && method == 'GET') {
|
|
24
|
-
return http.Response(
|
|
25
|
-
jsonEncode({'data': {'channels': ['production', 'staging']}}),
|
|
26
|
-
200,
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
if (path == '/api/bundles' && method == 'GET') {
|
|
30
|
-
return http.Response(
|
|
31
|
-
jsonEncode({
|
|
32
|
-
'data': [
|
|
33
|
-
{
|
|
34
|
-
'id': 'b1',
|
|
35
|
-
'platform': 'android',
|
|
36
|
-
'channel': 'production',
|
|
37
|
-
'enabled': true,
|
|
38
|
-
'shouldForceUpdate': false,
|
|
39
|
-
'fileHash': 'h1',
|
|
40
|
-
'storageUri': 's3://b/b1/patch.zip',
|
|
41
|
-
'createdAt': '2024-01-01T00:00:00.000Z',
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
'pagination': {
|
|
45
|
-
'total': 1,
|
|
46
|
-
'hasNextPage': false,
|
|
47
|
-
'hasPreviousPage': false,
|
|
48
|
-
'currentPage': 1,
|
|
49
|
-
'totalPages': 1,
|
|
50
|
-
'nextCursor': null,
|
|
51
|
-
'previousCursor': null,
|
|
52
|
-
},
|
|
53
|
-
}),
|
|
54
|
-
200,
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
if (path == '/api/bundles' && method == 'POST') {
|
|
58
|
-
final body = jsonDecode(request.body) as List;
|
|
59
|
-
expect(body, hasLength(1));
|
|
60
|
-
expect((body.first as Map)['id'], 'b1');
|
|
61
|
-
return http.Response(jsonEncode({'success': true}), 201);
|
|
62
|
-
}
|
|
63
|
-
if (path.startsWith('/api/bundles/') && method == 'PATCH') {
|
|
64
|
-
expect(request.url.path, '/api/bundles/b1');
|
|
65
|
-
return http.Response(jsonEncode({'success': true}), 200);
|
|
66
|
-
}
|
|
67
|
-
if (path.startsWith('/api/bundles/') && method == 'DELETE') {
|
|
68
|
-
expect(request.url.path, '/api/bundles/b1');
|
|
69
|
-
return http.Response(jsonEncode({'success': true}), 200);
|
|
70
|
-
}
|
|
71
|
-
if (path.startsWith('/api/bundles/') && method == 'GET') {
|
|
72
|
-
return http.Response(
|
|
73
|
-
jsonEncode({
|
|
74
|
-
'id': 'b1',
|
|
75
|
-
'platform': 'android',
|
|
76
|
-
'channel': 'production',
|
|
77
|
-
'enabled': true,
|
|
78
|
-
'shouldForceUpdate': false,
|
|
79
|
-
'fileHash': 'h1',
|
|
80
|
-
'storageUri': 's3://b/b1/patch.zip',
|
|
81
|
-
'createdAt': '2024-01-01T00:00:00.000Z',
|
|
82
|
-
}),
|
|
83
|
-
200,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
if (path == '/api/fingerprint/android/fp/production/00000000-0000-0000-0000-000000000000/b1' &&
|
|
87
|
-
method == 'GET') {
|
|
88
|
-
return http.Response(
|
|
89
|
-
jsonEncode({
|
|
90
|
-
'id': 'b2',
|
|
91
|
-
'shouldForceUpdate': false,
|
|
92
|
-
'status': 'UPDATE',
|
|
93
|
-
'fileUrl': 'https://cdn.example.com/b2/patch.zip',
|
|
94
|
-
'fileHash': 'h2',
|
|
95
|
-
}),
|
|
96
|
-
200,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (path == '/api/upload' && method == 'POST') {
|
|
100
|
-
expect(request.url.queryParameters['key'], 'b1/patch.zip');
|
|
101
|
-
return http.Response(
|
|
102
|
-
jsonEncode({'storageUri': 's3://b/b1/patch.zip'}),
|
|
103
|
-
201,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
if (path == '/api/getDownloadUrl' && method == 'POST') {
|
|
107
|
-
final body = jsonDecode(request.body) as Map;
|
|
108
|
-
expect(body['storageUri'], 's3://b/b1/patch.zip');
|
|
109
|
-
return http.Response(
|
|
110
|
-
jsonEncode({'fileUrl': 'https://cdn.example.com/b1/patch.zip'}),
|
|
111
|
-
200,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
if (path == '/api/delete' && method == 'DELETE') {
|
|
115
|
-
final body = jsonDecode(request.body) as Map;
|
|
116
|
-
expect(body['storageUri'], 's3://b/b1/patch.zip');
|
|
117
|
-
return http.Response(jsonEncode({'success': true}), 200);
|
|
118
|
-
}
|
|
119
|
-
if (path == '/api/readText' && method == 'POST') {
|
|
120
|
-
final body = jsonDecode(request.body) as Map;
|
|
121
|
-
expect(body['storageUri'], 's3://manifest');
|
|
122
|
-
return http.Response(jsonEncode({'data': 'manifest-content'}), 200);
|
|
123
|
-
}
|
|
124
|
-
if (path == '/api/list' && method == 'GET') {
|
|
125
|
-
return http.Response(
|
|
126
|
-
jsonEncode({
|
|
127
|
-
'data': [
|
|
128
|
-
{'key': 'b1/patch.zip', 'storageUri': 's3://b/b1/patch.zip', 'size': 3},
|
|
129
|
-
],
|
|
130
|
-
}),
|
|
131
|
-
200,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
if (path == '/api/_file' && method == 'GET') {
|
|
135
|
-
return http.Response('zip', 200);
|
|
136
|
-
}
|
|
137
|
-
if (request.url.host == 'cdn.example.com' && method == 'GET') {
|
|
138
|
-
return http.Response('zip-bytes', 200);
|
|
139
|
-
}
|
|
140
|
-
return http.Response(jsonEncode({'error': 'not found'}), 404);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
void main() {
|
|
144
|
-
late MockClient client;
|
|
145
|
-
late StandaloneRepositoryConfig repoConfig;
|
|
146
|
-
late StandaloneStorageConfig storageConfig;
|
|
147
|
-
|
|
148
|
-
setUp(() {
|
|
149
|
-
client = MockClient(_router);
|
|
150
|
-
repoConfig = StandaloneRepositoryConfig(
|
|
151
|
-
baseUrl: 'http://localhost',
|
|
152
|
-
clientFactory: () => client,
|
|
153
|
-
);
|
|
154
|
-
storageConfig = StandaloneStorageConfig(
|
|
155
|
-
baseUrl: 'http://localhost',
|
|
156
|
-
clientFactory: () => client,
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
group('standaloneRepository', () {
|
|
161
|
-
test('getChannels', () async {
|
|
162
|
-
final db = standaloneRepository(repoConfig)();
|
|
163
|
-
final channels = await db.getChannels();
|
|
164
|
-
expect(channels, ['production', 'staging']);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('getBundleById', () async {
|
|
168
|
-
final db = standaloneRepository(repoConfig)();
|
|
169
|
-
final bundle = await db.getBundleById('b1');
|
|
170
|
-
expect(bundle?.id, 'b1');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test('getBundles', () async {
|
|
174
|
-
final db = standaloneRepository(repoConfig)();
|
|
175
|
-
final res = await db.getBundles(
|
|
176
|
-
const DatabaseBundleQueryOptions(where: null),
|
|
177
|
-
);
|
|
178
|
-
expect(res.data, hasLength(1));
|
|
179
|
-
expect(res.data.first.id, 'b1');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('commitBundle insert/update/delete', () async {
|
|
183
|
-
final db = standaloneRepository(repoConfig)();
|
|
184
|
-
await db.appendBundle(
|
|
185
|
-
Bundle(
|
|
186
|
-
id: 'b1',
|
|
187
|
-
platform: Platform.android,
|
|
188
|
-
channel: 'production',
|
|
189
|
-
enabled: true,
|
|
190
|
-
shouldForceUpdate: false,
|
|
191
|
-
fileHash: 'h1',
|
|
192
|
-
storageUri: 's3://b/b1/patch.zip',
|
|
193
|
-
),
|
|
194
|
-
);
|
|
195
|
-
await db.commitBundle();
|
|
196
|
-
// update + delete exercise PATCH/DELETE routes via a fresh change set.
|
|
197
|
-
await db.updateBundle(
|
|
198
|
-
'b1',
|
|
199
|
-
{'enabled': false, 'channel': 'production'},
|
|
200
|
-
);
|
|
201
|
-
await db.commitBundle();
|
|
202
|
-
final existing = await db.getBundleById('b1');
|
|
203
|
-
await db.deleteBundle(existing!);
|
|
204
|
-
await db.commitBundle();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('getUpdateInfo resolves update-available', () async {
|
|
208
|
-
final db = standaloneRepository(repoConfig)();
|
|
209
|
-
final info = await db.getUpdateInfo(
|
|
210
|
-
FingerprintGetBundlesArgs(
|
|
211
|
-
platform: Platform.android,
|
|
212
|
-
bundleId: 'b1',
|
|
213
|
-
fingerprintHash: 'fp',
|
|
214
|
-
),
|
|
215
|
-
);
|
|
216
|
-
expect(info, isNotNull);
|
|
217
|
-
expect(info!.id, 'b2');
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
group('standaloneStorage', () {
|
|
222
|
-
test('upload returns storageUri', () async {
|
|
223
|
-
final storage = standaloneStorage(storageConfig);
|
|
224
|
-
final tmp = File('${Directory.systemTemp.path}/fp_test.zip')
|
|
225
|
-
..writeAsStringSync('zip');
|
|
226
|
-
final result = await storage.profiles.node!.upload('b1/patch.zip', tmp.path);
|
|
227
|
-
expect(result['storageUri'], 's3://b/b1/patch.zip');
|
|
228
|
-
await tmp.delete();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test('getDownloadUrl returns fileUrl', () async {
|
|
232
|
-
final storage = standaloneStorage(storageConfig);
|
|
233
|
-
final result =
|
|
234
|
-
await storage.profiles.runtime!.getDownloadUrl('s3://b/b1/patch.zip');
|
|
235
|
-
expect(result['fileUrl'], 'https://cdn.example.com/b1/patch.zip');
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test('delete succeeds', () async {
|
|
239
|
-
final storage = standaloneStorage(storageConfig);
|
|
240
|
-
await storage.profiles.node!.delete('s3://b/b1/patch.zip');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test('readText returns content', () async {
|
|
244
|
-
final storage = standaloneStorage(storageConfig);
|
|
245
|
-
final text = await storage.profiles.runtime!.readText('s3://manifest');
|
|
246
|
-
expect(text, 'manifest-content');
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test('downloadFile fetches bytes', () async {
|
|
250
|
-
final storage = standaloneStorage(storageConfig);
|
|
251
|
-
final out = '${Directory.systemTemp.path}/fp_dl.zip';
|
|
252
|
-
await storage.profiles.node!.downloadFile('s3://b/b1/patch.zip', out);
|
|
253
|
-
expect(File(out).readAsStringSync(), 'zip-bytes');
|
|
254
|
-
await File(out).delete();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test('listObjects returns entries', () async {
|
|
258
|
-
final storage = standaloneStorage(storageConfig);
|
|
259
|
-
final objects = await storage.profiles.node!.listObjects();
|
|
260
|
-
expect(objects, hasLength(1));
|
|
261
|
-
expect(objects.first.key, 'b1/patch.zip');
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
}
|
|
File without changes
|