Line data Source code
1 : import 'dart:async';
2 :
3 : import 'package:collection/collection.dart';
4 : import 'package:flutter/widgets.dart';
5 : import 'package:logging/logging.dart';
6 : import 'package:path/path.dart';
7 : import 'package:sqflite/sqflite.dart';
8 : import 'package:the_storage/src/abstract_storage.dart';
9 :
10 : const _sqLiteSliceSize = 512;
11 :
12 : /// Storage (db backend)
13 : class Storage implements AbstractStorage<StorageValue> {
14 : /// Create a new storage
15 3 : Storage();
16 :
17 : late final Database _database;
18 : final _log = Logger('TheStorage: Storage');
19 : late final String _dbName;
20 :
21 : /// Init storage
22 3 : Future<void> init([String dbName = AbstractStorage.storageFileName]) async {
23 3 : WidgetsFlutterBinding.ensureInitialized();
24 :
25 3 : _dbName = dbName;
26 :
27 6 : _database = await openDatabase(
28 3 : join(
29 3 : await getDatabasesPath(),
30 : dbName,
31 : ),
32 3 : onCreate: _onCreate,
33 3 : onUpgrade: _onUpgrade,
34 3 : onDowngrade: _onDowngrade,
35 : version: 1,
36 : );
37 :
38 6 : _log.finest('initialized');
39 : }
40 :
41 : /// Dispose storage
42 2 : Future<void> dispose() async {
43 4 : await _database.close();
44 : }
45 :
46 3 : @override
47 : Future<void> reset() async {
48 3 : WidgetsFlutterBinding.ensureInitialized();
49 :
50 6 : await _database.close();
51 3 : await deleteDatabase(
52 3 : join(
53 3 : await getDatabasesPath(),
54 3 : _dbName,
55 : ),
56 : );
57 :
58 6 : _log.finest('reset');
59 : }
60 :
61 3 : Future<void> _onCreate(Database db, int _) async {
62 3 : await db.execute(
63 : '''
64 : CREATE TABLE storage (
65 : domain TEXT NOT NULL,
66 : key TEXT NOT NULL,
67 : value TEXT NOT NULL,
68 : iv TEXT NOT NULL,
69 : PRIMARY KEY (domain, key)
70 : );
71 : ''',
72 : );
73 3 : await db.execute(
74 : '''
75 : CREATE INDEX storage_domain_index ON storage(domain);
76 : ''',
77 : );
78 :
79 6 : _log.finest('database created');
80 : }
81 :
82 0 : FutureOr<void> _onUpgrade(Database _, int oldVersion, int newVersion) {
83 0 : _log
84 0 : ..finest('database upgraded from $oldVersion to $newVersion')
85 0 : ..warning('no upgrade migrations found');
86 : }
87 :
88 0 : FutureOr<void> _onDowngrade(Database _, int oldVersion, int newVersion) {
89 0 : _log
90 0 : ..finest('database downgraded from $oldVersion to $newVersion')
91 0 : ..warning('no downgrade migrations found');
92 : }
93 :
94 3 : @override
95 : Future<void> clearAll() async {
96 : const query = '''
97 : DELETE FROM storage;
98 : ''';
99 :
100 6 : await _database.execute(query);
101 :
102 6 : _log.finest('storage cleared');
103 : }
104 :
105 3 : @override
106 : Future<void> clearDomain([
107 : String? domain = AbstractStorage.defaultDomain,
108 : ]) async {
109 : final query = '''
110 : DELETE FROM storage WHERE domain = '$domain';
111 3 : ''';
112 :
113 6 : await _database.execute(query);
114 :
115 9 : _log.finest('domain $domain cleared');
116 : }
117 :
118 3 : @override
119 : Future<void> set(
120 : String key,
121 : StorageValue value, {
122 : String domain = AbstractStorage.defaultDomain,
123 : bool overwrite = true,
124 : }) async {
125 3 : return setDomain(
126 3 : {
127 : key: value,
128 : },
129 : domain: domain,
130 : overwrite: overwrite,
131 : );
132 : }
133 :
134 3 : @override
135 : Future<void> setDomain(
136 : Map<String, StorageValue> pairs, {
137 : String domain = AbstractStorage.defaultDomain,
138 : bool overwrite = true,
139 : }) async {
140 3 : if (pairs.isEmpty) {
141 0 : _log.info('setAll called with empty pair map');
142 :
143 : return;
144 : }
145 :
146 : var isFirst = true;
147 9 : final values = pairs.entries.fold('', (previousValue, pair) {
148 : final prefix = isFirst ? '' : ', ';
149 3 : final result = '$previousValue$prefix('
150 : "'$domain', "
151 3 : "'${pair.key}', "
152 6 : "'${pair.value.value}', "
153 6 : "'${pair.value.iv}')";
154 : isFirst = false;
155 :
156 : return result;
157 : });
158 :
159 : final conflictClause = overwrite ? 'REPLACE' : 'IGNORE';
160 : final query = '''
161 : INSERT OR $conflictClause INTO storage (domain, key, value, iv) VALUES $values;
162 3 : ''';
163 :
164 6 : await _database.execute(query);
165 : }
166 :
167 3 : @override
168 : Future<void> delete(
169 : String key, {
170 : String domain = AbstractStorage.defaultDomain,
171 : }) async {
172 6 : return deleteDomain([key], domain: domain);
173 : }
174 :
175 3 : @override
176 : Future<void> deleteDomain(
177 : List<String> keys, {
178 : String domain = AbstractStorage.defaultDomain,
179 : }) async {
180 3 : if (keys.isEmpty) {
181 0 : _log.info('deleteDomain called with empty key list');
182 :
183 : return;
184 : }
185 :
186 : // SQLite has a limit of 999 variables per query
187 9 : keys.slices(_sqLiteSliceSize).forEach((keys) async {
188 : var isFirst = true;
189 6 : final andClause = keys.fold('', (previousValue, key) {
190 : final prefix = isFirst ? '' : ' OR ';
191 3 : final result = "$previousValue$prefix(key = '$key')";
192 : isFirst = false;
193 :
194 : return result;
195 : });
196 :
197 : final query = '''
198 : DELETE FROM storage WHERE domain = '$domain' AND ($andClause)
199 3 : ''';
200 :
201 6 : await _database.execute(query);
202 : });
203 : }
204 :
205 3 : @override
206 : Future<StorageValue?> get(
207 : String key, {
208 : StorageValue? defaultValue,
209 : String domain = AbstractStorage.defaultDomain,
210 : }) async {
211 6 : final list = await _database.rawQuery(
212 : '''
213 : SELECT value, iv FROM storage WHERE domain = '$domain' and key = '$key' LIMIT 1;
214 3 : ''',
215 : );
216 :
217 3 : return list.isNotEmpty
218 3 : ? StorageValue(
219 6 : list.first['value']! as String,
220 6 : list.first['iv']! as String,
221 : )
222 : : defaultValue;
223 : }
224 :
225 3 : @override
226 : Future<Map<String, StorageValue>> getDomain({
227 : String domain = AbstractStorage.defaultDomain,
228 : }) async {
229 6 : final list = await _database.rawQuery(
230 : '''
231 : SELECT key, value, iv FROM storage WHERE domain = '$domain';
232 3 : ''',
233 : );
234 :
235 3 : return {
236 : // There is no way to write null in these fields
237 : // ignore: cast_nullable_to_non_nullable
238 3 : for (final pair in list)
239 9 : pair['key']! as String: StorageValue(
240 3 : pair['value']! as String,
241 3 : pair['iv']! as String,
242 : ),
243 : };
244 : }
245 :
246 3 : @override
247 : Future<List<String>> getDomainKeys({
248 : String domain = AbstractStorage.defaultDomain,
249 : }) async {
250 6 : final list = await _database.rawQuery(
251 : '''
252 : SELECT key FROM storage WHERE domain = '$domain';
253 3 : ''',
254 : );
255 :
256 3 : return [
257 : // There is no way to write null in these fields
258 : // ignore: cast_nullable_to_non_nullable
259 9 : for (final pair in list) pair['key']! as String,
260 : ];
261 : }
262 : }
263 :
264 : /// Storage value unit
265 : @immutable
266 : class StorageValue implements Comparable<StorageValue> {
267 : /// Create a new storage value unit
268 5 : const StorageValue(this.value, this.iv);
269 :
270 : /// Value
271 : final String value;
272 :
273 : /// Initialization vector
274 : final String iv;
275 :
276 0 : @override
277 : int compareTo(StorageValue other) {
278 0 : return (value.compareTo(other.value) == 0 && iv.compareTo(other.iv) == 0)
279 : ? 0
280 : : 1;
281 : }
282 :
283 2 : @override
284 : bool operator ==(Object other) {
285 14 : return other is StorageValue && other.value == value && other.iv == iv;
286 : }
287 :
288 0 : @override
289 : int get hashCode {
290 0 : return value.hashCode + iv.hashCode;
291 : }
292 : }
|