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 : final result =
150 : // ignore: lines_longer_than_80_chars
151 18 : "$previousValue$prefix('$domain', '${pair.key}', '${pair.value.value}', '${pair.value.iv}' )";
152 : isFirst = false;
153 :
154 : return result;
155 : });
156 :
157 : final conflictClause = overwrite ? 'REPLACE' : 'IGNORE';
158 : final query = '''
159 : INSERT OR $conflictClause INTO storage (domain, key, value, iv) VALUES $values;
160 3 : ''';
161 :
162 6 : await _database.execute(query);
163 : }
164 :
165 3 : @override
166 : Future<void> delete(
167 : String key, {
168 : String domain = AbstractStorage.defaultDomain,
169 : }) async {
170 6 : return deleteDomain([key], domain: domain);
171 : }
172 :
173 3 : @override
174 : Future<void> deleteDomain(
175 : List<String> keys, {
176 : String domain = AbstractStorage.defaultDomain,
177 : }) async {
178 3 : if (keys.isEmpty) {
179 0 : _log.info('deleteDomain called with empty key list');
180 :
181 : return;
182 : }
183 :
184 : // SQLite has a limit of 999 variables per query
185 9 : keys.slices(_sqLiteSliceSize).forEach((keys) async {
186 : var isFirst = true;
187 6 : final andClause = keys.fold('', (previousValue, key) {
188 : final prefix = isFirst ? '' : ' OR ';
189 3 : final result = "$previousValue$prefix(key = '$key')";
190 : isFirst = false;
191 :
192 : return result;
193 : });
194 :
195 : final query = '''
196 : DELETE FROM storage WHERE domain = '$domain' AND ($andClause)
197 3 : ''';
198 :
199 6 : await _database.execute(query);
200 : });
201 : }
202 :
203 3 : @override
204 : Future<StorageValue?> get(
205 : String key, {
206 : StorageValue? defaultValue,
207 : String domain = AbstractStorage.defaultDomain,
208 : }) async {
209 6 : final list = await _database.rawQuery(
210 : '''
211 : SELECT value, iv FROM storage WHERE domain = '$domain' and key = '$key' LIMIT 1;
212 3 : ''',
213 : );
214 :
215 3 : return list.isNotEmpty
216 3 : ? StorageValue(
217 6 : list.first['value']! as String,
218 6 : list.first['iv']! as String,
219 : )
220 : : defaultValue;
221 : }
222 :
223 3 : @override
224 : Future<Map<String, StorageValue>> getDomain({
225 : String domain = AbstractStorage.defaultDomain,
226 : }) async {
227 6 : final list = await _database.rawQuery(
228 : '''
229 : SELECT key, value, iv FROM storage WHERE domain = '$domain';
230 3 : ''',
231 : );
232 :
233 3 : return {
234 : // There is no way to write null in these fields
235 : // ignore: cast_nullable_to_non_nullable
236 3 : for (final pair in list)
237 9 : pair['key']! as String: StorageValue(
238 3 : pair['value']! as String,
239 3 : pair['iv']! as String,
240 : ),
241 : };
242 : }
243 :
244 3 : @override
245 : Future<List<String>> getDomainKeys({
246 : String domain = AbstractStorage.defaultDomain,
247 : }) async {
248 6 : final list = await _database.rawQuery(
249 : '''
250 : SELECT key FROM storage WHERE domain = '$domain';
251 3 : ''',
252 : );
253 :
254 3 : return [
255 : // There is no way to write null in these fields
256 : // ignore: cast_nullable_to_non_nullable
257 9 : for (final pair in list) pair['key']! as String,
258 : ];
259 : }
260 : }
261 :
262 : /// Storage value unit
263 : @immutable
264 : class StorageValue implements Comparable<StorageValue> {
265 : /// Create a new storage value unit
266 4 : const StorageValue(this.value, this.iv);
267 :
268 : /// Value
269 : final String value;
270 :
271 : /// Initialization vector
272 : final String iv;
273 :
274 0 : @override
275 : int compareTo(StorageValue other) {
276 0 : return (value.compareTo(other.value) == 0 && iv.compareTo(other.iv) == 0)
277 : ? 0
278 : : 1;
279 : }
280 :
281 2 : @override
282 : bool operator ==(Object other) {
283 14 : return other is StorageValue && other.value == value && other.iv == iv;
284 : }
285 :
286 0 : @override
287 : int get hashCode {
288 0 : return value.hashCode + iv.hashCode;
289 : }
290 : }
|