LCOV - code coverage report
Current view: top level - src - storage.dart Coverage Total Hit
Test: filtered_lcov.info Lines: 84.4 % 90 76
Test Date: 2026-03-22 12:33:26 Functions: - 0 0

            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              : }
        

Generated by: LCOV version 2.4-0