@_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.
Files changed (147) hide show
  1. package/README.md +13 -13
  2. package/bin/{flutter-patcher-linux-x64 → flutter-ota-linux-x64} +0 -0
  3. package/bin/{flutter-patcher.js → flutter-ota.js} +4 -4
  4. package/dart-src/packages/cli-tools/bin/{flutter_patcher.dart → flutter_ota_kit.dart} +2 -2
  5. package/dart-src/packages/cli-tools/lib/{flutter_patcher_cli.dart → flutter_ota_kit_cli.dart} +5 -3
  6. package/dart-src/packages/cli-tools/lib/src/backend.dart +6 -25
  7. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +10 -4
  8. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +5 -3
  9. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +279 -10
  10. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +7 -7
  11. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +5 -5
  12. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +4 -4
  13. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +4 -4
  14. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +3 -3
  15. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +1 -1
  16. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +294 -45
  17. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +1 -1
  18. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +59 -24
  19. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +5 -5
  20. package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +117 -0
  21. package/dart-src/packages/cli-tools/lib/src/config.dart +29 -78
  22. package/dart-src/packages/cli-tools/lib/src/operations.dart +4 -4
  23. package/dart-src/packages/cli-tools/lib/src/pack.dart +12 -6
  24. package/dart-src/packages/cli-tools/lib/src/runner.dart +21 -3
  25. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
  26. package/dart-src/packages/cli-tools/lib/src/verbose.dart +3 -0
  27. package/dart-src/packages/cli-tools/migrations/aws/README.md +7 -0
  28. package/dart-src/packages/cli-tools/migrations/postgres/bundles.sql +45 -0
  29. package/dart-src/packages/cli-tools/migrations/postgres/get_target_app_version_list.sql +21 -0
  30. package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_app_version.sql +126 -0
  31. package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
  32. package/dart-src/packages/cli-tools/migrations/postgres/hash_user_id.sql +30 -0
  33. package/dart-src/packages/cli-tools/migrations/postgres/is_device_eligible.sql +237 -0
  34. package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +11 -0
  35. package/dart-src/packages/cli-tools/pubspec.yaml +8 -10
  36. package/dart-src/packages/cli-tools/test/backends_test.dart +4 -4
  37. package/dart-src/packages/cli-tools/test/build_pack_test.dart +1 -1
  38. package/dart-src/packages/cli-tools/test/cli_test.dart +3 -3
  39. package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +2 -2
  40. package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +2 -2
  41. package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +7 -7
  42. package/dart-src/packages/core/lib/{flutter_patcher_core.dart → flutter_ota_kit_core.dart} +1 -1
  43. package/dart-src/packages/core/pubspec.yaml +2 -2
  44. package/dart-src/packages/core/test/rollout_test.dart +1 -1
  45. package/dart-src/packages/core/test/semver_test.dart +1 -1
  46. package/dart-src/packages/core/test/types_test.dart +2 -2
  47. package/dart-src/packages/core/test/uuid_test.dart +1 -1
  48. package/dart-src/packages/core/tool/dbg.dart +1 -1
  49. package/dart-src/plugins/aws/lib/{flutter_patcher_aws.dart → flutter_ota_kit_aws.dart} +0 -2
  50. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +1 -1
  51. package/dart-src/plugins/aws/lib/src/aws_database.dart +1 -1
  52. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +1 -1
  53. package/dart-src/plugins/aws/lib/src/aws_storage.dart +1 -1
  54. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +25 -3
  55. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +1 -1
  56. package/dart-src/plugins/aws/pubspec.yaml +4 -4
  57. package/dart-src/plugins/aws/test/aws_database_test.dart +3 -3
  58. package/dart-src/plugins/aws/test/aws_storage_test.dart +2 -2
  59. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +2 -2
  60. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +1 -1
  61. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +1 -1
  62. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +1 -1
  63. package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +1 -1
  64. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +2 -2
  65. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +1 -1
  66. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +1 -1
  67. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +1 -1
  68. package/dart-src/plugins/cloudflare/pubspec.yaml +4 -4
  69. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +2 -2
  70. package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +2 -2
  71. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +2 -2
  72. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +2 -2
  73. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +2 -2
  74. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +2 -2
  75. package/dart-src/plugins/plugin-core/lib/{flutter_patcher_plugin_core.dart → flutter_ota_kit_plugin_core.dart} +1 -1
  76. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +1 -1
  77. package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +1 -1
  78. package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +1 -1
  79. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +1 -1
  80. package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +1 -1
  81. package/dart-src/plugins/plugin-core/lib/src/types.dart +2 -2
  82. package/dart-src/plugins/plugin-core/pubspec.yaml +3 -3
  83. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +3 -3
  84. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +2 -2
  85. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +2 -2
  86. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +2 -2
  87. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +5 -5
  88. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +2 -2
  89. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +2 -2
  90. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +2 -2
  91. package/dart-src/plugins/plugin-core/test/helpers_test.dart +2 -2
  92. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +2 -2
  93. package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +3 -3
  94. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +1 -1
  95. package/dart-src/plugins/postgres/lib/{flutter_patcher_postgres.dart → flutter_ota_kit_postgres.dart} +1 -1
  96. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +1 -1
  97. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +2 -2
  98. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +2 -2
  99. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +9 -9
  100. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +1 -1
  101. package/dart-src/plugins/postgres/pubspec.yaml +4 -4
  102. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +2 -2
  103. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +2 -2
  104. package/dart-src/plugins/postgres/test/postgres_database_test.dart +2 -2
  105. package/dart-src/plugins/supabase/lib/edge.dart +1 -1
  106. package/dart-src/plugins/supabase/lib/{flutter_patcher_supabase.dart → flutter_ota_kit_supabase.dart} +1 -1
  107. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +1 -1
  108. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +5 -1
  109. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +2 -2
  110. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +1 -1
  111. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +1 -1
  112. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +2 -5
  113. package/dart-src/plugins/supabase/pubspec.yaml +4 -4
  114. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +5 -5
  115. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +3 -3
  116. package/dart-src/plugins/supabase/test/supabase_database_test.dart +3 -3
  117. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +2 -2
  118. package/migrations/aws/README.md +7 -0
  119. package/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
  120. package/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
  121. package/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
  122. package/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
  123. package/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
  124. package/migrations/cloudflare/bundles.sql +42 -0
  125. package/migrations/postgres/bundles.sql +45 -0
  126. package/migrations/postgres/get_target_app_version_list.sql +21 -0
  127. package/migrations/postgres/get_update_info_by_app_version.sql +126 -0
  128. package/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
  129. package/migrations/postgres/hash_user_id.sql +30 -0
  130. package/migrations/postgres/is_device_eligible.sql +237 -0
  131. package/package.json +4 -4
  132. package/scripts/postinstall.js +15 -15
  133. package/scripts/vendor.sh +1 -1
  134. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -496
  135. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -743
  136. package/dart-src/packages/cli-tools/pubspec.lock +0 -638
  137. package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +0 -315
  138. package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +0 -72
  139. package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +0 -190
  140. package/dart-src/plugins/standalone/.gitkeep +0 -0
  141. package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +0 -12
  142. package/dart-src/plugins/standalone/lib/src/standalone_config.dart +0 -107
  143. package/dart-src/plugins/standalone/lib/src/standalone_database.dart +0 -233
  144. package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +0 -175
  145. package/dart-src/plugins/standalone/pubspec.yaml +0 -18
  146. package/dart-src/plugins/standalone/test/standalone_test.dart +0 -264
  147. /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
- }