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: 2025-02-11 11:11:34 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              :       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              : }
        

Generated by: LCOV version 2.3-1