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 : '''
111 : DELETE FROM storage WHERE domain = '$domain';
112 3 : ''';
113 :
114 6 : await _database.execute(query);
115 :
116 9 : _log.finest('domain $domain cleared');
117 : }
118 :
119 3 : @override
120 : Future<void> set(
121 : String key,
122 : StorageValue value, {
123 : String domain = AbstractStorage.defaultDomain,
124 : bool overwrite = true,
125 : }) async {
126 3 : return setDomain(
127 3 : {
128 : key: value,
129 : },
130 : domain: domain,
131 : overwrite: overwrite,
132 : );
133 : }
134 :
135 3 : @override
136 : Future<void> setDomain(
137 : Map<String, StorageValue> pairs, {
138 : String domain = AbstractStorage.defaultDomain,
139 : bool overwrite = true,
140 : }) async {
141 3 : if (pairs.isEmpty) {
142 0 : _log.info('setAll called with empty pair map');
143 :
144 : return;
145 : }
146 :
147 : var isFirst = true;
148 9 : final values = pairs.entries.fold('', (previousValue, pair) {
149 : final prefix = isFirst ? '' : ', ';
150 3 : final result =
151 : '$previousValue$prefix('
152 : "'$domain', "
153 3 : "'${pair.key}', "
154 6 : "'${pair.value.value}', "
155 6 : "'${pair.value.iv}')";
156 : isFirst = false;
157 :
158 : return result;
159 : });
160 :
161 : final conflictClause = overwrite ? 'REPLACE' : 'IGNORE';
162 : final query =
163 : '''
164 : INSERT OR $conflictClause INTO storage (domain, key, value, iv) VALUES $values;
165 3 : ''';
166 :
167 6 : await _database.execute(query);
168 : }
169 :
170 3 : @override
171 : Future<void> delete(
172 : String key, {
173 : String domain = AbstractStorage.defaultDomain,
174 : }) async {
175 6 : return deleteDomain([key], domain: domain);
176 : }
177 :
178 3 : @override
179 : Future<void> deleteDomain(
180 : List<String> keys, {
181 : String domain = AbstractStorage.defaultDomain,
182 : }) async {
183 3 : if (keys.isEmpty) {
184 0 : _log.info('deleteDomain called with empty key list');
185 :
186 : return;
187 : }
188 :
189 : // SQLite has a limit of 999 variables per query
190 9 : keys.slices(_sqLiteSliceSize).forEach((keys) async {
191 : var isFirst = true;
192 6 : final andClause = keys.fold('', (previousValue, key) {
193 : final prefix = isFirst ? '' : ' OR ';
194 3 : final result = "$previousValue$prefix(key = '$key')";
195 : isFirst = false;
196 :
197 : return result;
198 : });
199 :
200 : final query =
201 : '''
202 : DELETE FROM storage WHERE domain = '$domain' AND ($andClause)
203 3 : ''';
204 :
205 6 : await _database.execute(query);
206 : });
207 : }
208 :
209 3 : @override
210 : Future<StorageValue?> get(
211 : String key, {
212 : StorageValue? defaultValue,
213 : String domain = AbstractStorage.defaultDomain,
214 : }) async {
215 6 : final list = await _database.rawQuery(
216 : '''
217 : SELECT value, iv FROM storage WHERE domain = '$domain' and key = '$key' LIMIT 1;
218 3 : ''',
219 : );
220 :
221 3 : return list.isNotEmpty
222 3 : ? StorageValue(
223 6 : list.first['value']! as String,
224 6 : list.first['iv']! as String,
225 : )
226 : : defaultValue;
227 : }
228 :
229 3 : @override
230 : Future<Map<String, StorageValue>> getDomain({
231 : String domain = AbstractStorage.defaultDomain,
232 : }) async {
233 6 : final list = await _database.rawQuery(
234 : '''
235 : SELECT key, value, iv FROM storage WHERE domain = '$domain';
236 3 : ''',
237 : );
238 :
239 3 : return {
240 3 : for (final pair in list)
241 9 : pair['key']! as String: StorageValue(
242 3 : pair['value']! as String,
243 3 : pair['iv']! as String,
244 : ),
245 : };
246 : }
247 :
248 3 : @override
249 : Future<List<String>> getDomainKeys({
250 : String domain = AbstractStorage.defaultDomain,
251 : }) async {
252 6 : final list = await _database.rawQuery(
253 : '''
254 : SELECT key FROM storage WHERE domain = '$domain';
255 3 : ''',
256 : );
257 :
258 3 : return [
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 3 : 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 : }
|