@_nazmiforreal/flutter-ota 0.1.0 → 0.1.1
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-patcher.js +37 -39
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +743 -0
- package/dart-src/packages/cli-tools/.gitkeep +0 -0
- package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
- package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
- package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
- package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
- package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
- package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
- package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
- package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
- package/dart-src/packages/cli-tools/pubspec.lock +638 -0
- package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
- package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
- package/dart-src/packages/core/lib/src/bundle.dart +191 -0
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
- package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
- package/dart-src/packages/core/lib/src/metadata.dart +31 -0
- package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
- package/dart-src/packages/core/lib/src/platform.dart +15 -0
- package/dart-src/packages/core/lib/src/rollout.dart +209 -0
- package/dart-src/packages/core/lib/src/semver.dart +70 -0
- package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
- package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
- package/dart-src/packages/core/lib/src/status.dart +24 -0
- package/dart-src/packages/core/lib/src/strategy.dart +10 -0
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
- package/dart-src/packages/core/lib/src/uuid.dart +45 -0
- package/dart-src/packages/core/pubspec.lock +381 -0
- package/dart-src/packages/core/pubspec.yaml +9 -0
- package/dart-src/packages/core/tool/dbg.dart +6 -0
- package/dart-src/plugins/aws/.gitkeep +0 -0
- package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
- package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
- package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
- package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
- package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
- package/dart-src/plugins/aws/pubspec.lock +435 -0
- package/dart-src/plugins/aws/pubspec.yaml +22 -0
- package/dart-src/plugins/cloudflare/.gitkeep +0 -0
- package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
- package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
- package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/plugins/cloudflare/pubspec.lock +411 -0
- package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
- package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
- package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
- package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
- package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
- package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
- package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
- package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
- package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
- package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
- package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
- package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
- package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
- package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
- package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
- package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
- package/dart-src/plugins/plugin-core/pubspec.lock +396 -0
- package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
- package/dart-src/plugins/postgres/.gitkeep +0 -0
- package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
- package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
- package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
- package/dart-src/plugins/postgres/pubspec.lock +427 -0
- package/dart-src/plugins/postgres/pubspec.yaml +18 -0
- package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
- package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
- package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
- package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
- package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
- package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
- package/dart-src/plugins/standalone/.gitkeep +0 -0
- package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
- package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
- package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
- package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
- package/dart-src/plugins/standalone/pubspec.lock +411 -0
- package/dart-src/plugins/standalone/pubspec.yaml +18 -0
- package/dart-src/plugins/supabase/lib/edge.dart +10 -0
- package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
- package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
- package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
- package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
- package/dart-src/plugins/supabase/pubspec.lock +515 -0
- package/dart-src/plugins/supabase/pubspec.yaml +19 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +32 -23
- /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import 'dart:convert' show utf8;
|
|
2
|
+
import 'dart:io' show File;
|
|
3
|
+
import 'dart:typed_data' show Uint8List;
|
|
4
|
+
|
|
5
|
+
import 'package:crypto/crypto.dart' show Hmac, sha256;
|
|
6
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
7
|
+
show StorageObject;
|
|
8
|
+
import 'package:http/http.dart' show Client, Request, Response;
|
|
9
|
+
|
|
10
|
+
import 'r2_config.dart' show R2S3StorageConfig;
|
|
11
|
+
|
|
12
|
+
/// A thin S3-compatible client for Cloudflare R2.
|
|
13
|
+
///
|
|
14
|
+
/// Mirrors the subset of `@aws-sdk/client-s3` used by hot-updater's
|
|
15
|
+
/// `r2S3Storage.ts`: delete / put / head / get objects, listing, and
|
|
16
|
+
/// presigned GET URLs. Requests are signed with AWS Signature V4.
|
|
17
|
+
abstract class R2S3ClientLike {
|
|
18
|
+
const R2S3ClientLike();
|
|
19
|
+
|
|
20
|
+
Future<void> deleteObject(String key);
|
|
21
|
+
|
|
22
|
+
Future<void> putObject(String key, List<int> body, String contentType);
|
|
23
|
+
|
|
24
|
+
Future<bool> headObject(String key);
|
|
25
|
+
|
|
26
|
+
Future<List<int>> getObjectAsBytes(String key);
|
|
27
|
+
|
|
28
|
+
Future<String> getObjectAsString(String key);
|
|
29
|
+
|
|
30
|
+
Future<String> getSignedUrl(String key, {int expiresIn = 3600});
|
|
31
|
+
|
|
32
|
+
Future<List<StorageObject>> listObjects([String? prefix]);
|
|
33
|
+
|
|
34
|
+
Future<void> deleteObjects(List<String> keys);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Real R2 S3 client backed by `package:http` + AWS SigV4 signing.
|
|
38
|
+
class R2S3Client implements R2S3ClientLike {
|
|
39
|
+
R2S3Client(this.config, {Client? http}) : _http = http ?? Client();
|
|
40
|
+
|
|
41
|
+
final R2S3StorageConfig config;
|
|
42
|
+
final Client _http;
|
|
43
|
+
|
|
44
|
+
static const String _service = 'r2';
|
|
45
|
+
|
|
46
|
+
Uri _objectUri(String key) {
|
|
47
|
+
final host = config.endpoint ??
|
|
48
|
+
'https://${config.accountId}.r2.cloudflarestorage.com';
|
|
49
|
+
return Uri.parse('$host/${config.bucketName}/$key');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
Future<void> deleteObject(String key) async {
|
|
54
|
+
final response = await _send('DELETE', _objectUri(key), body: null);
|
|
55
|
+
_assertStatus(response, [200, 204], 'deleteObject');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@override
|
|
59
|
+
Future<void> putObject(
|
|
60
|
+
String key,
|
|
61
|
+
List<int> body,
|
|
62
|
+
String contentType,
|
|
63
|
+
) async {
|
|
64
|
+
final uri = _objectUri(key);
|
|
65
|
+
final response = await _send(
|
|
66
|
+
'PUT',
|
|
67
|
+
uri,
|
|
68
|
+
body: body,
|
|
69
|
+
extraHeaders: {
|
|
70
|
+
'Content-Type': contentType,
|
|
71
|
+
'Cache-Control': 'max-age=31536000',
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
_assertStatus(response, [200], 'putObject');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
Future<bool> headObject(String key) async {
|
|
79
|
+
final uri = _objectUri(key);
|
|
80
|
+
final request = Request('HEAD', uri)..headers.addAll(_authHeaders('HEAD', uri, null));
|
|
81
|
+
final response = await _http.send(request);
|
|
82
|
+
if (response.statusCode == 404) return false;
|
|
83
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
84
|
+
throw Exception('R2 HeadObject failed (${response.statusCode})');
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
Future<List<int>> getObjectAsBytes(String key) async {
|
|
91
|
+
final response = await _send('GET', _objectUri(key), body: null);
|
|
92
|
+
_assertStatus(response, [200], 'getObjectAsBytes');
|
|
93
|
+
return response.bodyBytes;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@override
|
|
97
|
+
Future<String> getObjectAsString(String key) async {
|
|
98
|
+
final bytes = await getObjectAsBytes(key);
|
|
99
|
+
return utf8.decode(bytes);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@override
|
|
103
|
+
Future<String> getSignedUrl(String key, {int expiresIn = 3600}) async {
|
|
104
|
+
final uri = _objectUri(key);
|
|
105
|
+
final now = DateTime.now().toUtc();
|
|
106
|
+
final amzDate = _amzDate(now);
|
|
107
|
+
final dateStamp = amzDate.substring(0, 8);
|
|
108
|
+
final scope = '$dateStamp/${config.region}/$_service/aws4_request';
|
|
109
|
+
|
|
110
|
+
final params = {
|
|
111
|
+
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
|
112
|
+
'X-Amz-Credential': '${config.accessKeyId}/$scope',
|
|
113
|
+
'X-Amz-Date': amzDate,
|
|
114
|
+
'X-Amz-Expires': '$expiresIn',
|
|
115
|
+
'X-Amz-SignedHeaders': 'host',
|
|
116
|
+
};
|
|
117
|
+
final canonicalQuery = params.entries
|
|
118
|
+
.map((e) => '${_uriEncode(e.key)}=${_uriEncode(e.value)}')
|
|
119
|
+
.toList()
|
|
120
|
+
..sort();
|
|
121
|
+
final queryString = canonicalQuery.join('&');
|
|
122
|
+
|
|
123
|
+
const payloadHash = 'UNSIGNED-PAYLOAD';
|
|
124
|
+
final canonicalHeaders = 'host:${uri.host}\n';
|
|
125
|
+
final canonicalRequest =
|
|
126
|
+
'GET\n${uri.path}\n$queryString\n$canonicalHeaders\nhost\n$payloadHash';
|
|
127
|
+
final stringToSign = _stringToSign(
|
|
128
|
+
amzDate,
|
|
129
|
+
scope,
|
|
130
|
+
canonicalRequest,
|
|
131
|
+
);
|
|
132
|
+
final signingKey = _signingKey(dateStamp, config.region);
|
|
133
|
+
final signature = _toHex(_hmac(signingKey, stringToSign));
|
|
134
|
+
return '${uri.scheme}://${uri.host}${uri.path}?$queryString'
|
|
135
|
+
'&X-Amz-Signature=$signature';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@override
|
|
139
|
+
Future<List<StorageObject>> listObjects([String? prefix]) async {
|
|
140
|
+
final base = _objectUri('').replace(path: '/${config.bucketName}/');
|
|
141
|
+
final listUri = base.replace(
|
|
142
|
+
queryParameters: {
|
|
143
|
+
'list-type': '2',
|
|
144
|
+
if (prefix != null && prefix.isNotEmpty) 'prefix': prefix,
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
final response = await _send('GET', listUri, body: null);
|
|
148
|
+
_assertStatus(response, [200], 'listObjects');
|
|
149
|
+
return _parseListObjects(response.body);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@override
|
|
153
|
+
Future<void> deleteObjects(List<String> keys) async {
|
|
154
|
+
for (final key in keys) {
|
|
155
|
+
await deleteObject(key);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- signing helpers -------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
Map<String, String> _authHeaders(
|
|
162
|
+
String method,
|
|
163
|
+
Uri uri,
|
|
164
|
+
List<int>? body,
|
|
165
|
+
) {
|
|
166
|
+
final now = DateTime.now().toUtc();
|
|
167
|
+
final amzDate = _amzDate(now);
|
|
168
|
+
final dateStamp = amzDate.substring(0, 8);
|
|
169
|
+
final scope = '$dateStamp/${config.region}/$_service/aws4_request';
|
|
170
|
+
final payloadHash = body == null ? _hashHex(const []) : _hashHex(body);
|
|
171
|
+
|
|
172
|
+
final canonicalHeaders = 'host:${uri.host}\n'
|
|
173
|
+
'x-amz-content-sha256:$payloadHash\n'
|
|
174
|
+
'x-amz-date:$amzDate\n';
|
|
175
|
+
const signedHeaders = 'host;x-amz-content-sha256;x-amz-date';
|
|
176
|
+
|
|
177
|
+
final canonicalRequest = '$method\n${uri.path}\n'
|
|
178
|
+
'\n$canonicalHeaders$signedHeaders\n$payloadHash';
|
|
179
|
+
final stringToSign = _stringToSign(amzDate, scope, canonicalRequest);
|
|
180
|
+
final signingKey = _signingKey(dateStamp, config.region);
|
|
181
|
+
final signature = _toHex(_hmac(signingKey, stringToSign));
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
'x-amz-date': amzDate,
|
|
185
|
+
'x-amz-content-sha256': payloadHash,
|
|
186
|
+
'Authorization':
|
|
187
|
+
'AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/$scope, '
|
|
188
|
+
'SignedHeaders=$signedHeaders, Signature=$signature',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
Future<Response> _send(
|
|
193
|
+
String method,
|
|
194
|
+
Uri uri, {
|
|
195
|
+
required List<int>? body,
|
|
196
|
+
Map<String, String>? extraHeaders,
|
|
197
|
+
}) async {
|
|
198
|
+
final request = Request(method, uri);
|
|
199
|
+
request.headers.addAll(_authHeaders(method, uri, body));
|
|
200
|
+
if (extraHeaders != null) request.headers.addAll(extraHeaders);
|
|
201
|
+
if (body != null) request.bodyBytes = body;
|
|
202
|
+
final streamed = await _http.send(request);
|
|
203
|
+
return await Response.fromStream(streamed);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
List<StorageObject> _parseListObjects(String xml) {
|
|
207
|
+
final objects = <StorageObject>[];
|
|
208
|
+
final contents = _allMatches(xml, RegExp(r'<Contents>([\s\S]*?)</Contents>'));
|
|
209
|
+
for (final block in contents) {
|
|
210
|
+
final key = _tag(block, 'Key');
|
|
211
|
+
final size = int.tryParse(_tag(block, 'Size') ?? '');
|
|
212
|
+
final lastModified = _tag(block, 'LastModified');
|
|
213
|
+
if (key == null) continue;
|
|
214
|
+
objects.add(
|
|
215
|
+
StorageObject(
|
|
216
|
+
key: key,
|
|
217
|
+
storageUri: 'r2://${config.bucketName}/$key',
|
|
218
|
+
size: size ?? 0,
|
|
219
|
+
lastModifiedAt: lastModified == null
|
|
220
|
+
? null
|
|
221
|
+
: DateTime.tryParse(lastModified),
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return objects;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- low-level sigv4 primitives --------------------------------------------
|
|
229
|
+
|
|
230
|
+
List<int> _signingKey(String dateStamp, String region) {
|
|
231
|
+
var key = utf8.encode('AWS4${config.secretAccessKey}');
|
|
232
|
+
key = _hmac(key, dateStamp);
|
|
233
|
+
key = _hmac(key, region);
|
|
234
|
+
key = _hmac(key, _service);
|
|
235
|
+
key = _hmac(key, 'aws4_request');
|
|
236
|
+
return key;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
String _stringToSign(String amzDate, String scope, String canonicalRequest) =>
|
|
240
|
+
'AWS4-HMAC-SHA256\n$amzDate\n$scope\n${_hashHex(utf8.encode(canonicalRequest))}';
|
|
241
|
+
|
|
242
|
+
// --- static utilities ------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
static String _amzDate(DateTime utc) =>
|
|
245
|
+
'${utc.toIso8601String().replaceAll('-', '').replaceAll(':', '').split('.').first}Z';
|
|
246
|
+
|
|
247
|
+
static String _uriEncode(String value) => Uri.encodeQueryComponent(value);
|
|
248
|
+
|
|
249
|
+
static Uint8List _hmac(List<int> key, String data) =>
|
|
250
|
+
Hmac(sha256, key).convert(utf8.encode(data)).bytes as Uint8List;
|
|
251
|
+
|
|
252
|
+
static String _hashHex(List<int> data) => _toHex(sha256.convert(data).bytes);
|
|
253
|
+
|
|
254
|
+
static String _toHex(List<int> bytes) =>
|
|
255
|
+
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
256
|
+
|
|
257
|
+
static List<String> _allMatches(String input, RegExp regex) =>
|
|
258
|
+
regex.allMatches(input).map((m) => m.group(1)!).toList();
|
|
259
|
+
|
|
260
|
+
static String? _tag(String block, String name) {
|
|
261
|
+
final m = RegExp('<$name>([\\s\\S]*?)</$name>').firstMatch(block);
|
|
262
|
+
return m?.group(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
static void _assertStatus(
|
|
266
|
+
Response response,
|
|
267
|
+
List<int> ok,
|
|
268
|
+
String operation,
|
|
269
|
+
) {
|
|
270
|
+
if (!ok.contains(response.statusCode)) {
|
|
271
|
+
throw Exception(
|
|
272
|
+
'R2 $operation failed (${response.statusCode}): ${response.body}',
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// Read a local file's bytes (node profile helper).
|
|
279
|
+
Future<List<int>> readFileBytes(String filePath) =>
|
|
280
|
+
File(filePath).readAsBytes();
|
|
281
|
+
|
|
282
|
+
/// Write bytes to a local file, creating parent directories.
|
|
283
|
+
Future<void> writeFileBytes(String filePath, List<int> bytes) async {
|
|
284
|
+
final file = File(filePath);
|
|
285
|
+
await file.parent.create(recursive: true);
|
|
286
|
+
await file.writeAsBytes(bytes);
|
|
287
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
2
|
+
show StoragePlugin, createUniversalStoragePlugin;
|
|
3
|
+
|
|
4
|
+
import 'r2_config.dart' show R2S3StorageConfig;
|
|
5
|
+
import 'r2_storage_profile.dart'
|
|
6
|
+
show createS3RuntimeStorageProfile, createS3StorageProfile;
|
|
7
|
+
|
|
8
|
+
/// Cloudflare R2 storage plugin (faithful port of hot-updater's `r2Storage`).
|
|
9
|
+
///
|
|
10
|
+
/// Uses the S3-compatible R2 API (AWS Signature V4). The node profile handles
|
|
11
|
+
/// upload/delete/download/list; the runtime profile serves presigned download
|
|
12
|
+
/// URLs and reads small text objects. Note: hot-updater also supports a
|
|
13
|
+
/// deprecated Wrangler-CLI based profile; only the S3-compatible profile is
|
|
14
|
+
/// ported here.
|
|
15
|
+
final StoragePlugin Function(R2S3StorageConfig config) r2Storage =
|
|
16
|
+
createUniversalStoragePlugin<R2S3StorageConfig>(
|
|
17
|
+
name: 'r2Storage',
|
|
18
|
+
supportedProtocol: 'r2',
|
|
19
|
+
factory: (config) => (
|
|
20
|
+
node: createS3StorageProfile(config),
|
|
21
|
+
runtime: createS3RuntimeStorageProfile(config),
|
|
22
|
+
),
|
|
23
|
+
);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import 'package:path/path.dart' show basename;
|
|
2
|
+
|
|
3
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
4
|
+
show
|
|
5
|
+
NodeStorageProfile,
|
|
6
|
+
RuntimeStorageProfile,
|
|
7
|
+
StorageObject,
|
|
8
|
+
createStorageKeyBuilder,
|
|
9
|
+
getContentType,
|
|
10
|
+
parseStorageUri;
|
|
11
|
+
|
|
12
|
+
import 'r2_config.dart' show R2S3StorageConfig, resolveR2Client;
|
|
13
|
+
import 'r2_s3_client.dart' show R2S3ClientLike, readFileBytes, writeFileBytes;
|
|
14
|
+
|
|
15
|
+
void _ensureExpectedBucket(String bucket, String bucketName) {
|
|
16
|
+
if (bucket != bucketName) {
|
|
17
|
+
throw ArgumentError(
|
|
18
|
+
'Bucket name mismatch: expected "$bucketName", but found "$bucket".',
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Build the node (deploy/CLI) storage profile for R2 S3.
|
|
24
|
+
NodeStorageProfile createS3StorageProfile(R2S3StorageConfig config) {
|
|
25
|
+
final R2S3ClientLike client = resolveR2Client(config);
|
|
26
|
+
final getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
27
|
+
|
|
28
|
+
return _R2NodeProfile(client, config, getStorageKey);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Build the runtime (update-check) storage profile for R2 S3.
|
|
32
|
+
RuntimeStorageProfile createS3RuntimeStorageProfile(R2S3StorageConfig config) {
|
|
33
|
+
final R2S3ClientLike client = resolveR2Client(config);
|
|
34
|
+
return _R2RuntimeProfile(client, config);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class _R2NodeProfile implements NodeStorageProfile {
|
|
38
|
+
_R2NodeProfile(this._client, this._config, this._getStorageKey);
|
|
39
|
+
|
|
40
|
+
final R2S3ClientLike _client;
|
|
41
|
+
final R2S3StorageConfig _config;
|
|
42
|
+
final String Function(String, [String, String, String]) _getStorageKey;
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
Future<Map<String, String>> upload(String key, String filePath) async {
|
|
46
|
+
final bytes = await readFileBytes(filePath);
|
|
47
|
+
final contentType = getContentType(filePath);
|
|
48
|
+
final filename = basename(filePath);
|
|
49
|
+
final storageKey = _getStorageKey(key, filename);
|
|
50
|
+
|
|
51
|
+
await _client.putObject(storageKey, bytes, contentType);
|
|
52
|
+
return {'storageUri': 'r2://${_config.bucketName}/$storageKey'};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
Future<bool> exists(String storageUri) async {
|
|
57
|
+
final parsed = parseStorageUri(storageUri, 'r2');
|
|
58
|
+
_ensureExpectedBucket(parsed.bucket, _config.bucketName);
|
|
59
|
+
return _client.headObject(parsed.key);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
Future<void> delete(String storageUri) async {
|
|
64
|
+
final parsed = parseStorageUri(storageUri, 'r2');
|
|
65
|
+
_ensureExpectedBucket(parsed.bucket, _config.bucketName);
|
|
66
|
+
await _client.deleteObject(parsed.key);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
Future<void> downloadFile(String storageUri, String filePath) async {
|
|
71
|
+
final parsed = parseStorageUri(storageUri, 'r2');
|
|
72
|
+
_ensureExpectedBucket(parsed.bucket, _config.bucketName);
|
|
73
|
+
final bytes = await _client.getObjectAsBytes(parsed.key);
|
|
74
|
+
await writeFileBytes(filePath, bytes);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
Future<List<StorageObject>> listObjects([String? prefix]) =>
|
|
79
|
+
_client.listObjects(prefix);
|
|
80
|
+
|
|
81
|
+
@override
|
|
82
|
+
Future<void> deleteObjects(List<String> keys) =>
|
|
83
|
+
_client.deleteObjects(keys);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class _R2RuntimeProfile implements RuntimeStorageProfile {
|
|
87
|
+
_R2RuntimeProfile(this._client, this._config);
|
|
88
|
+
|
|
89
|
+
final R2S3ClientLike _client;
|
|
90
|
+
final R2S3StorageConfig _config;
|
|
91
|
+
|
|
92
|
+
@override
|
|
93
|
+
Future<Map<String, String>> getDownloadUrl(String storageUri) async {
|
|
94
|
+
final parsed = parseStorageUri(storageUri, 'r2');
|
|
95
|
+
_ensureExpectedBucket(parsed.bucket, _config.bucketName);
|
|
96
|
+
final fileUrl = await _client.getSignedUrl(parsed.key);
|
|
97
|
+
return {'fileUrl': fileUrl};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@override
|
|
101
|
+
Future<String?> readText(String storageUri) async {
|
|
102
|
+
final parsed = parseStorageUri(storageUri, 'r2');
|
|
103
|
+
_ensureExpectedBucket(parsed.bucket, _config.bucketName);
|
|
104
|
+
try {
|
|
105
|
+
return await _client.getObjectAsString(parsed.key);
|
|
106
|
+
} catch (_) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Migration number: 0001 2025-01-27T08:37:50.374Z
|
|
2
|
+
-- HotUpdater.bundles
|
|
3
|
+
|
|
4
|
+
CREATE TABLE bundles (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
platform TEXT NOT NULL CHECK (platform IN ('ios', 'android')),
|
|
7
|
+
target_app_version TEXT NOT NULL,
|
|
8
|
+
should_force_update INTEGER NOT NULL CHECK (should_force_update IN (0, 1)),
|
|
9
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
|
|
10
|
+
file_url TEXT NOT NULL,
|
|
11
|
+
file_hash TEXT NOT NULL,
|
|
12
|
+
git_commit_hash TEXT,
|
|
13
|
+
message TEXT
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- Migration number: 0003 2025-05-18T16:25:12.486Z
|
|
2
|
+
-- HotUpdater.bundles
|
|
3
|
+
|
|
4
|
+
ALTER TABLE bundles ADD COLUMN fingerprint_hash TEXT;
|
|
5
|
+
ALTER TABLE bundles ADD COLUMN storage_uri TEXT;
|
|
6
|
+
ALTER TABLE bundles ADD COLUMN metadata JSONB DEFAULT '{}';
|
|
7
|
+
|
|
8
|
+
UPDATE bundles
|
|
9
|
+
SET storage_uri = 'r2://%%BUCKET_NAME%%/' || id || '/bundle.zip'
|
|
10
|
+
WHERE storage_uri IS NULL;
|
|
11
|
+
|
|
12
|
+
CREATE TABLE bundles_temp (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
platform TEXT NOT NULL,
|
|
15
|
+
should_force_update INTEGER NOT NULL,
|
|
16
|
+
enabled INTEGER NOT NULL,
|
|
17
|
+
file_hash TEXT NOT NULL,
|
|
18
|
+
git_commit_hash TEXT,
|
|
19
|
+
message TEXT,
|
|
20
|
+
channel TEXT NOT NULL DEFAULT 'production',
|
|
21
|
+
storage_uri TEXT NOT NULL,
|
|
22
|
+
target_app_version TEXT,
|
|
23
|
+
fingerprint_hash TEXT,
|
|
24
|
+
metadata JSONB DEFAULT '{}',
|
|
25
|
+
CHECK ((target_app_version IS NOT NULL) OR (fingerprint_hash IS NOT NULL))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
INSERT INTO bundles_temp
|
|
29
|
+
SELECT id, platform, should_force_update, enabled, file_hash, git_commit_hash, message, channel, storage_uri, target_app_version, fingerprint_hash, metadata
|
|
30
|
+
FROM bundles;
|
|
31
|
+
|
|
32
|
+
DROP TABLE bundles;
|
|
33
|
+
|
|
34
|
+
ALTER TABLE bundles_temp RENAME TO bundles;
|
|
35
|
+
|
|
36
|
+
CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
|
|
37
|
+
CREATE INDEX bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
|
|
38
|
+
CREATE INDEX bundles_channel_idx ON bundles(channel);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- Migration number: 0004 2025-12-21T00:00:00.000Z
|
|
2
|
+
|
|
3
|
+
-- HotUpdater.bundles
|
|
4
|
+
|
|
5
|
+
ALTER TABLE bundles ADD COLUMN rollout_cohort_count INTEGER DEFAULT 1000
|
|
6
|
+
CHECK (rollout_cohort_count >= 0 AND rollout_cohort_count <= 1000);
|
|
7
|
+
|
|
8
|
+
CREATE INDEX IF NOT EXISTS bundles_rollout_idx ON bundles(rollout_cohort_count);
|
|
9
|
+
|
|
10
|
+
-- SQLite doesn't have array type, store as JSON text
|
|
11
|
+
ALTER TABLE bundles ADD COLUMN target_cohorts TEXT;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Migration number: 0005 2026-04-22T00:00:00.000Z
|
|
2
|
+
|
|
3
|
+
-- HotUpdater.bundle_artifact_columns
|
|
4
|
+
|
|
5
|
+
ALTER TABLE bundles ADD COLUMN manifest_storage_uri TEXT;
|
|
6
|
+
ALTER TABLE bundles ADD COLUMN manifest_file_hash TEXT;
|
|
7
|
+
ALTER TABLE bundles ADD COLUMN asset_base_storage_uri TEXT;
|
|
8
|
+
|
|
9
|
+
CREATE TABLE bundle_patches (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
bundle_id TEXT NOT NULL,
|
|
12
|
+
base_bundle_id TEXT NOT NULL,
|
|
13
|
+
base_file_hash TEXT NOT NULL,
|
|
14
|
+
patch_file_hash TEXT NOT NULL,
|
|
15
|
+
patch_storage_uri TEXT NOT NULL,
|
|
16
|
+
order_index INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
FOREIGN KEY (bundle_id) REFERENCES bundles(id) ON DELETE CASCADE,
|
|
18
|
+
FOREIGN KEY (base_bundle_id) REFERENCES bundles(id) ON DELETE CASCADE
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE INDEX IF NOT EXISTS bundle_patches_bundle_id_idx
|
|
22
|
+
ON bundle_patches(bundle_id);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS bundle_patches_base_bundle_id_idx
|
|
24
|
+
ON bundle_patches(base_bundle_id);
|