Line data Source code
1 : import 'dart:convert';
2 :
3 : import 'package:at_client/at_client.dart';
4 : import 'package:at_client/src/client/secondary.dart';
5 : import 'package:at_commons/at_builders.dart';
6 : import 'package:at_commons/at_commons.dart';
7 : import 'package:at_lookup/at_lookup.dart';
8 : import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart';
9 : import 'package:at_utils/at_utils.dart';
10 :
11 : /// Contains methods to execute verb on local secondary storage using [executeVerb]
12 : /// Set [AtClientPreference.isLocalStoreRequired] to true and other preferences that your app needs.
13 : /// Delete and Update commands will be synced to the server
14 : class LocalSecondary implements Secondary {
15 : final AtClient _atClient;
16 :
17 : final _logger = AtSignLogger('LocalSecondary');
18 :
19 : /// Local keystore used to store data for the current atSign.
20 : SecondaryKeyStore? keyStore;
21 :
22 1 : LocalSecondary(this._atClient) {
23 2 : keyStore = SecondaryPersistenceStoreFactory.getInstance()
24 3 : .getSecondaryPersistenceStore(_atClient.getCurrentAtSign())!
25 1 : .getSecondaryKeyStore();
26 : }
27 :
28 : /// Executes a verb builder on the local secondary. For update and delete operation, if [sync] is
29 : /// set to true then data is synced from local to remote.
30 : /// if [sync] is set to false, no sync operation is done.
31 : @override
32 1 : Future<String?> executeVerb(VerbBuilder builder, {sync}) async {
33 : String? verbResult;
34 :
35 : try {
36 2 : if (builder is UpdateVerbBuilder || builder is DeleteVerbBuilder) {
37 : //1. if local and server are out of sync, first sync before updating current key-value
38 :
39 : //2 . update/delete to local store
40 1 : if (builder is UpdateVerbBuilder) {
41 2 : verbResult = await _update(builder);
42 1 : } else if (builder is DeleteVerbBuilder) {
43 2 : verbResult = await _delete(builder);
44 : }
45 : // 3. sync latest update/delete if strategy is immediate
46 : if (sync != null && sync) {
47 0 : _logger.finer('calling sync immediate from local secondary');
48 0 : AtClientManager.getInstance().syncService.sync();
49 : }
50 1 : } else if (builder is LLookupVerbBuilder) {
51 2 : verbResult = await _llookup(builder);
52 1 : } else if (builder is ScanVerbBuilder) {
53 2 : verbResult = await _scan(builder);
54 : }
55 1 : } on Exception catch (e) {
56 1 : if (e is AtLookUpException) {
57 0 : return Future.error(AtClientException(e.errorCode, e.errorMessage));
58 : } else {
59 : rethrow;
60 : }
61 : }
62 : return verbResult;
63 : }
64 :
65 1 : Future<String> _update(UpdateVerbBuilder builder) async {
66 : try {
67 : dynamic updateResult;
68 1 : var updateKey = AtClientUtil.buildKey(builder);
69 1 : switch (builder.operation) {
70 1 : case UPDATE_META:
71 0 : var metadata = Metadata();
72 : metadata
73 0 : ..ttl = builder.ttl
74 0 : ..ttb = builder.ttb
75 0 : ..ttr = builder.ttr
76 0 : ..ccd = builder.ccd
77 0 : ..isBinary = builder.isBinary
78 0 : ..isEncrypted = builder.isEncrypted;
79 0 : var atMetadata = AtMetadataAdapter(metadata);
80 0 : updateResult = await keyStore!.putMeta(updateKey, atMetadata);
81 : break;
82 : default:
83 1 : var atData = AtData();
84 2 : atData.data = builder.value;
85 1 : if (builder.dataSignature != null) {
86 0 : var metadata = Metadata();
87 : metadata
88 0 : ..ttl = builder.ttl
89 0 : ..ttb = builder.ttb
90 0 : ..ttr = builder.ttr
91 0 : ..ccd = builder.ccd
92 0 : ..isBinary = builder.isBinary
93 0 : ..isEncrypted = builder.isEncrypted
94 0 : ..dataSignature = builder.dataSignature;
95 0 : var atMetadata = AtMetadataAdapter(metadata);
96 : updateResult =
97 0 : await keyStore!.putAll(updateKey, atData, atMetadata);
98 : break;
99 : }
100 : // #TODO replace below call with putAll.
101 3 : updateResult = await keyStore!.put(updateKey, atData,
102 1 : time_to_live: builder.ttl,
103 1 : time_to_born: builder.ttb,
104 1 : time_to_refresh: builder.ttr,
105 1 : isCascade: builder.ccd,
106 1 : isBinary: builder.isBinary,
107 1 : isEncrypted: builder.isEncrypted);
108 : break;
109 : }
110 1 : return 'data:$updateResult';
111 0 : } on DataStoreException catch (e) {
112 0 : _logger.severe('exception in local update:${e.toString()}');
113 : rethrow;
114 : }
115 : }
116 :
117 1 : Future<String> _llookup(LLookupVerbBuilder builder) async {
118 : try {
119 : var llookupKey = '';
120 1 : if (builder.isCached) {
121 0 : llookupKey += 'cached:';
122 : }
123 1 : if (builder.sharedWith != null) {
124 0 : llookupKey += '${AtUtils.formatAtSign(builder.sharedWith!)}:';
125 : }
126 1 : if (builder.atKey != null) {
127 2 : llookupKey += builder.atKey!;
128 : }
129 1 : if (builder.sharedBy != null) {
130 3 : llookupKey += AtUtils.formatAtSign(builder.sharedBy)!;
131 : }
132 3 : var llookupMeta = await keyStore!.getMeta(llookupKey);
133 1 : var isActive = _isActiveKey(llookupMeta);
134 : String? result;
135 : if (isActive) {
136 3 : var llookupResult = await keyStore!.get(llookupKey);
137 2 : result = _prepareResponseData(builder.operation, llookupResult);
138 : }
139 1 : return 'data:$result';
140 1 : } on DataStoreException catch (e) {
141 0 : _logger.severe('exception in llookup:${e.toString()}');
142 : rethrow;
143 : }
144 : }
145 :
146 1 : Future<String> _delete(DeleteVerbBuilder builder) async {
147 : try {
148 : var deleteKey = '';
149 1 : if (builder.isCached) {
150 0 : deleteKey += 'cached:';
151 : }
152 1 : if (builder.isPublic) {
153 0 : deleteKey += 'public:';
154 : }
155 1 : if (builder.sharedWith != null && builder.sharedWith!.isNotEmpty) {
156 0 : deleteKey += '${AtUtils.formatAtSign(builder.sharedWith!)}:';
157 : }
158 3 : if (builder.sharedBy != null && builder.sharedBy!.isNotEmpty) {
159 1 : deleteKey +=
160 4 : '${builder.atKey}${AtUtils.formatAtSign(builder.sharedBy!)}';
161 : } else {
162 0 : deleteKey += builder.atKey!;
163 : }
164 3 : var deleteResult = await keyStore!.remove(deleteKey);
165 1 : return 'data:$deleteResult';
166 0 : } on DataStoreException catch (e) {
167 0 : _logger.severe('exception in delete:${e.toString()}');
168 : rethrow;
169 : }
170 : }
171 :
172 1 : Future<String?> _scan(ScanVerbBuilder builder) async {
173 : try {
174 : // Call to remote secondary sever and performs an outbound scan to retrieve values from sharedBy secondary
175 : // shared with current atSign
176 1 : if (builder.sharedBy != null) {
177 0 : var command = builder.buildCommand();
178 0 : return await RemoteSecondary(
179 0 : _atClient.getCurrentAtSign()!, _atClient.getPreferences()!,
180 0 : privateKey: _atClient.getPreferences()!.privateKey)
181 0 : .executeCommand(command, auth: true);
182 : }
183 : List<String?> keys;
184 3 : keys = keyStore!.getKeys(regex: builder.regex) as List<String?>;
185 : // Gets keys shared to sharedWith atSign.
186 1 : if (builder.sharedWith != null) {
187 0 : keys.retainWhere(
188 0 : (element) => element!.startsWith(builder.sharedWith!) == true);
189 : }
190 2 : keys.removeWhere((key) =>
191 2 : key.toString().startsWith('privatekey:') ||
192 2 : key.toString().startsWith('private:') ||
193 2 : key.toString().startsWith('public:_'));
194 1 : var keyString = keys.toString();
195 : // Apply regex on keyString to remove unnecessary characters and spaces
196 2 : keyString = keyString.replaceFirst(RegExp(r'^\['), '');
197 2 : keyString = keyString.replaceFirst(RegExp(r'\]$'), '');
198 1 : keyString = keyString.replaceAll(', ', ',');
199 2 : var keysArray = keyString.isNotEmpty ? (keyString.split(',')) : [];
200 1 : return json.encode(keysArray);
201 0 : } on DataStoreException catch (e) {
202 0 : _logger.severe('exception in scan:${e.toString()}');
203 : rethrow;
204 : }
205 : }
206 :
207 1 : bool _isActiveKey(AtMetaData? atMetaData) {
208 : if (atMetaData == null) return true;
209 1 : var ttb = atMetaData.availableAt;
210 1 : var ttl = atMetaData.expiresAt;
211 : if (ttb == null && ttl == null) return true;
212 0 : var now = DateTime.now().toUtc().millisecondsSinceEpoch;
213 : if (ttb != null) {
214 0 : var ttbMs = ttb.toUtc().millisecondsSinceEpoch;
215 0 : if (ttbMs > now) return false;
216 : }
217 : if (ttl != null) {
218 0 : var ttlMs = ttl.toUtc().millisecondsSinceEpoch;
219 0 : if (ttlMs < now) return false;
220 : }
221 : //If TTB or TTL populated but not met, return true.
222 : return true;
223 : }
224 :
225 1 : String? _prepareResponseData(String? operation, AtData? atData) {
226 : String? result;
227 : if (atData == null) {
228 : return result;
229 : }
230 : switch (operation) {
231 1 : case 'meta':
232 0 : result = json.encode(atData.metaData!.toJson());
233 : break;
234 1 : case 'all':
235 0 : result = json.encode(atData.toJson());
236 : break;
237 : default:
238 1 : result = atData.data;
239 : break;
240 : }
241 : return result;
242 : }
243 :
244 1 : Future<String?> getPrivateKey() async {
245 3 : var privateKeyData = await keyStore!.get(AT_PKAM_PRIVATE_KEY);
246 1 : return privateKeyData?.data;
247 : }
248 :
249 1 : Future<String?> getEncryptionPrivateKey() async {
250 3 : var privateKeyData = await keyStore!.get(AT_ENCRYPTION_PRIVATE_KEY);
251 1 : return privateKeyData?.data;
252 : }
253 :
254 1 : Future<String?> getPublicKey() async {
255 3 : var publicKeyData = await keyStore!.get(AT_PKAM_PUBLIC_KEY);
256 1 : return publicKeyData?.data;
257 : }
258 :
259 1 : Future<String?> getEncryptionPublicKey(String atSign) async {
260 1 : atSign = AtUtils.formatAtSign(atSign)!;
261 : var privateKeyData =
262 4 : await keyStore!.get('$AT_ENCRYPTION_PUBLIC_KEY$atSign');
263 1 : return privateKeyData?.data;
264 : }
265 :
266 1 : Future<String?> getEncryptionSelfKey() async {
267 3 : var selfKeyData = await keyStore!.get(AT_ENCRYPTION_SELF_KEY);
268 1 : return selfKeyData?.data;
269 : }
270 :
271 : ///Returns `true` on successfully storing the values into local secondary.
272 1 : Future<bool> putValue(String key, String value) async {
273 : dynamic isStored;
274 2 : var atData = AtData()..data = value;
275 3 : isStored = await keyStore!.put(key, atData);
276 : return isStored != null ? true : false;
277 : }
278 : }
|