hurshi / dio-http-cache

http cache lib for Flutter dio like RxCache
Apache License 2.0
274 stars 223 forks source link

cache into file instead of database #61

Closed pishguy closed 3 years ago

pishguy commented 3 years ago

how can i set configuration to file-cache instead of database?

hurshi commented 3 years ago

Hi, thanks for your issue, The disk cache supports customization now: https://github.com/hurshi/dio-http-cache/issues/33#issuecomment-698268181

pishguy commented 3 years ago

@hurshi is any simple implementation about this feature to know how can i set that?

pishguy commented 3 years ago

@hurshi i read the library source code and i can't find any disk store implementation and it seems supported database internally

hurshi commented 3 years ago

Yes, by default only database storage is implemented, but now there is support for custom disk caching, so you can implement it yourself, whether it's file storage, Hive or Moor.

5arif commented 3 years ago

Yes, by default only database storage is implemented, but now there is support for custom disk caching, so you can implement it yourself, whether it's file storage, Hive or Moor.

hi @hurshi i try using hive but it seems not working, it keep call to network after first request, where did i miss, please help

class HiveStore implements ICacheStore {
  final boxName = 'hive_cache';

  @override
  Future<bool> clearAll() async {
    var box = await Hive.openBox(boxName);
    box.clear();

    return true;
  }

  @override
  Future<bool> clearExpired() async {
    var now = DateTime.now().millisecondsSinceEpoch;
    var box = await Hive.openBox<CacheObj>(boxName);
    var expired = box.values.where((e) =>
        e.maxStaleDate > 0 && e.maxStaleDate < now ||
        e.maxStaleDate <= 0 && e.maxAgeDate < now);

    box.deleteAll(expired);
    return true;
  }

  @override
  Future<bool> delete(String key, {String subKey}) async {
    var box = await Hive.openBox<CacheObj>(boxName);
    var delTarget = box.values.where((e) => e.key == key);

    if (subKey != null) {
      delTarget = box.values.where((e) => e.key == key && e.subKey == subKey);
    }

    box.deleteAll(delTarget);
    return true;
  }

  @override
  Future<CacheObj> getCacheObj(String key, {String subKey}) async {
    var box = await Hive.openBox<CacheObj>(boxName);
    if (box.values.length == 0) {
      return null;
    }

    var cache = subKey != null
        ? box.values.firstWhere((e) => e.key == key && e.subKey == subKey)
        : box.values.firstWhere((e) => e.key == key);

    return cache;
  }

  @override
  Future<bool> setCacheObj(CacheObj obj) async {
    var box = await Hive.openBox<CacheObj>(boxName);
    box.put(obj.key, obj);
    return true;
  }
}
class HiveCacheAdapter extends TypeAdapter<CacheObj> {
  HiveCacheAdapter({int typeId = TYPE_ID}) : _typeId = typeId;
  static const TYPE_ID = 101;
  final int _typeId;

  @override
  CacheObj read(BinaryReader reader) {
    return CacheObj.fromJson(reader.readMap());
  }

  @override
  int get typeId => _typeId;

  @override
  void write(BinaryWriter writer, CacheObj obj) {
    writer.writeMap(obj.toJson());
  }
}

and at my config

static CacheConfig get cacheConfig {
    var config = CacheConfig(
      defaultRequestMethod: 'GET',
      baseUrl: Endpoint.baseUrl,
      defaultMaxAge: const Duration(days: 3),
      skipDiskCache: true,
      diskStore: HiveStore(),
    );

    return config;
  }
pishguy commented 3 years ago

@5arif hi, could you solve your issue?

5arif commented 3 years ago

@MahdiPishguy not yet, any clue ?

pishguy commented 3 years ago

@MahdiPishguy not yet, any clue ?

not yet. i'm trying to check your implementation

isaacfi commented 3 years ago

Hi,

Based on the class DiskCacheStore I did some implementation, where I could save the content in files into the app filesystem and use SQLite to apply the cache rules for deletion, creation and querying. This may solve the Issue 56

@hurshi By some weird reason if I implement the ICacheStore interface I have a runtime error that said that my class is not a subclass of DiskCacheStore and I had to implement this class directly as workaround to this problem.

import 'dart:io';

import 'package:dio_http_cache/dio_http_cache.dart';
import 'package:dio_http_cache/src/store/store_disk.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:path/path.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'package:uuid/uuid.dart';

/// Cache file store
class CacheFileStore implements DiskCacheStore {
  final String cacheFolderName;
  final String databaseName;
  final String _tableCacheObject = "cache_file_dio";
  final String _columnKey = "key";
  final String _columnSubKey = "subKey";
  final String _columnMaxAgeDate = "max_age_date";
  final String _columnMaxStaleDate = "max_stale_date";
  final String _columnFileName = "file_name";
  final String _columnStatusCode = "statusCode";
  final String _columnHeaders = "headers";

  Database _db;
  static const int _curDBVersion = 3;

  Future<Database> get _database async {
    if (null == _db) {
      var path = await getDatabasesPath();
      await Directory(path).create(recursive: true);
      path = join(path, "$databaseName.db");
      _db = await openDatabase(path,
          version: _curDBVersion,
          onConfigure: (db) => _tryFixDbNoVersionBug(db, path),
          onCreate: _onCreate,
          onUpgrade: _onUpgrade);
      await _clearExpired(_db);
    }
    return _db;
  }

  _tryFixDbNoVersionBug(Database db, String dbPath) async {
    if ((await db.getVersion()) == 0) {
      var isTableUserLogExist = await db
          .rawQuery(
              "select DISTINCT tbl_name from sqlite_master where tbl_name = '$_tableCacheObject'")
          .then((v) => (null != v && v.length > 0));
      if (isTableUserLogExist) {
        await db.setVersion(1);
      }
    }
  }

  _getCreateTableSql() => '''
      CREATE TABLE IF NOT EXISTS $_tableCacheObject ( 
        $_columnKey text, 
        $_columnSubKey text, 
        $_columnMaxAgeDate integer,
        $_columnMaxStaleDate integer,
        $_columnFileName text,
        $_columnStatusCode integer,
        $_columnHeaders BLOB,
        PRIMARY KEY ($_columnKey, $_columnSubKey)
        ) 
      ''';

  _onCreate(Database db, int version) async {
    await db.execute(_getCreateTableSql());
  }

  List<List<String>> _dbUpgradeList() => [
        // 0 -> 1
        null,
        // 1 -> 2
        [
          "ALTER TABLE $_tableCacheObject ADD COLUMN $_columnStatusCode integer;"
        ],
        // 2 -> 3 : Change $_columnContent from text to BLOB
        ["DROP TABLE IF EXISTS $_tableCacheObject;", _getCreateTableSql()],
      ];

  _onUpgrade(Database db, int oldVersion, int newVersion) async {
    var mergeLength = _dbUpgradeList().length;
    if (oldVersion < 0 || oldVersion >= mergeLength) return;
    await db.transaction((txn) async {
      var tempVersion = oldVersion;
      while (tempVersion < newVersion) {
        if (tempVersion < mergeLength) {
          var sqlList = _dbUpgradeList()[tempVersion];
          if (null != sqlList && sqlList.length > 0) {
            sqlList.forEach((sql) async {
              sql = sql.trim();
              if (null != sql && sql.length > 0) {
                await txn.execute(sql);
              }
            });
          }
        }
        tempVersion++;
      }
    });
  }

  CacheFileStore(
      {this.databaseName = 'CacheFileDioDB',
      this.cacheFolderName = 'cache_file_dio'}) {
    if (this.databaseName == null || this.databaseName.trim().isEmpty) {
      throw Exception('databaseName cannot be null or empty');
    }
    if (this.cacheFolderName == null || this.cacheFolderName.trim().isEmpty) {
      throw Exception('cacheFolderName cannot be null or empty');
    }
  }

  @override
  Future<CacheObj> getCacheObj(String key, {String subKey}) async {
    var db = await _database;
    if (null == db) return null;
    final cacheDirPath = p.join(
        (await getApplicationDocumentsDirectory()).path, cacheFolderName);
    final cacheDir = Directory(cacheDirPath);
    if (!cacheDir.existsSync()) {
      cacheDir.createSync(recursive: true);
    }
    var where = "$_columnKey=\"$key\"";
    if (null != subKey) where += " and $_columnSubKey=\"$subKey\"";
    var resultList = await db.query(_tableCacheObject, where: where);
    if (null == resultList || resultList.length <= 0) return null;
    final cacheFileObj = _CacheFileObj.fromJson(resultList[0]);
    final cacheFilePath = p.join(cacheDirPath, cacheFileObj.fileName);
    final file = File(cacheFilePath);
    var cacheObj = CacheObj.fromJson(cacheFileObj.toJson());
    cacheObj.content = file.readAsBytesSync();
    return cacheObj;
  }

  @override
  Future<bool> setCacheObj(CacheObj obj) async {
    var db = await _database;
    if (null == db) return false;
    final cacheDirPath = p.join(
        (await getApplicationDocumentsDirectory()).path, cacheFolderName);
    final cacheDir = Directory(cacheDirPath);
    if (!cacheDir.existsSync()) {
      cacheDir.createSync(recursive: true);
    }
    var fileName = 'cache_' + Uuid().v4().replaceAll('-', '');
    final cacheFilePath = p.join(cacheDirPath, fileName);
    final fileToSave = File(cacheFilePath);
    var content = obj.content;
    var headers = obj.headers;
    fileToSave.writeAsBytesSync(content);
    await db.insert(
        _tableCacheObject,
        {
          _columnKey: obj.key,
          _columnSubKey: obj.subKey ?? "",
          _columnMaxAgeDate: obj.maxAgeDate ?? 0,
          _columnMaxStaleDate: obj.maxStaleDate ?? 0,
          _columnFileName: fileName,
          _columnStatusCode: obj.statusCode,
          _columnHeaders: headers
        },
        conflictAlgorithm: ConflictAlgorithm.replace);
    return true;
  }

  @override
  Future<bool> delete(String key, {String subKey}) async {
    var db = await _database;
    if (null == db) return false;
    final cacheDirPath = p.join(
        (await getApplicationDocumentsDirectory()).path, cacheFolderName);
    final cacheDir = Directory(cacheDirPath);
    if (!cacheDir.existsSync()) {
      cacheDir.createSync(recursive: true);
    }
    var where = "$_columnKey=\"$key\"";
    if (null != subKey) where += " and $_columnSubKey=\"$subKey\"";
    var resultList = await db.query(_tableCacheObject, where: where);
    if (null == resultList || resultList.length <= 0) return false;
    resultList.forEach((ri) {
      final item = _CacheFileObj.fromJson(ri);
      final cacheFilePath = p.join(cacheDirPath, item.fileName);
      final fileItem = File(cacheFilePath);
      if (fileItem.existsSync()) {
        fileItem.deleteSync();
      }
    });
    return 0 != await db.delete(_tableCacheObject, where: where);
  }

  @override
  Future<bool> clearExpired() async {
    var db = await _database;
    return _clearExpired(db);
  }

  Future<bool> _clearExpired(Database db) async {
    if (null == db) return false;
    final cacheDirPath = p.join(
        (await getApplicationDocumentsDirectory()).path, cacheFolderName);
    final cacheDir = Directory(cacheDirPath);
    if (!cacheDir.existsSync()) {
      cacheDir.createSync(recursive: true);
    }
    var now = DateTime.now().millisecondsSinceEpoch;
    var where1 = "$_columnMaxStaleDate > 0 and $_columnMaxStaleDate < $now";
    var where2 = "$_columnMaxStaleDate <= 0 and $_columnMaxAgeDate < $now";
    var resultList =
        await db.query(_tableCacheObject, where: "( $where1 ) or ( $where2 )");
    if (null == resultList || resultList.length <= 0) return false;
    resultList.forEach((ri) {
      final item = _CacheFileObj.fromJson(ri);
      final cacheFilePath = p.join(cacheDirPath, item.fileName);
      final fileItem = File(cacheFilePath);
      if (fileItem.existsSync()) {
        fileItem.deleteSync();
      }
    });
    return 0 !=
        await db.delete(_tableCacheObject, where: "( $where1 ) or ( $where2 )");
  }

  @override
  Future<bool> clearAll() async {
    var db = await _database;
    if (null == db) return false;
    final cacheDirPath = p.join(
        (await getApplicationDocumentsDirectory()).path, cacheFolderName);
    final cacheDir = Directory(cacheDirPath);
    if (cacheDir.existsSync()) {
      cacheDir.deleteSync(recursive: true);
    }
    return 0 != await db.delete(_tableCacheObject);
  }
}

@JsonSerializable()
class _CacheFileObj {
  String key;
  String subKey;
  @JsonKey(name: "max_age_date")
  int maxAgeDate;
  @JsonKey(name: "max_stale_date")
  int maxStaleDate;
  @JsonKey(name: "file_name")
  String fileName;
  int statusCode;
  List<int> headers;

  _CacheFileObj._(
      this.key, this.subKey, this.fileName, this.statusCode, this.headers);

  factory _CacheFileObj(String key, String fileName,
      {String subKey = "",
      Duration maxAge,
      Duration maxStale,
      int statusCode = 200,
      List<int> headers}) {
    return _CacheFileObj._(key, subKey, fileName, statusCode, headers)
      ..maxAge = maxAge
      ..maxStale = maxStale;
  }

  set maxAge(Duration duration) {
    if (null != duration) this.maxAgeDate = _convertDuration(duration);
  }

  set maxStale(Duration duration) {
    if (null != duration) this.maxStaleDate = _convertDuration(duration);
  }

  _convertDuration(Duration duration) =>
      DateTime.now().add(duration).millisecondsSinceEpoch;

  factory _CacheFileObj.fromJson(Map<String, dynamic> json) =>
      _$CacheFileObjFromJson(json);

  toJson() => _$CacheFileObjToJson(this);

  static _CacheFileObj _$CacheFileObjFromJson(Map json) {
    return _CacheFileObj(
      json['key'] as String,
      json['file_name'] as String,
      subKey: json['subKey'] as String,
      statusCode: json['statusCode'] as int,
      headers: (json['headers'] as List)?.map((e) => e as int)?.toList(),
    )
      ..maxAgeDate = json['max_age_date'] as int
      ..maxStaleDate = json['max_stale_date'] as int;
  }

  Map<String, dynamic> _$CacheFileObjToJson(_CacheFileObj instance) =>
      <String, dynamic>{
        'key': instance.key,
        'subKey': instance.subKey,
        'max_age_date': instance.maxAgeDate,
        'max_stale_date': instance.maxStaleDate,
        'file_name': instance.fileName,
        'statusCode': instance.statusCode,
        'headers': instance.headers,
      };
}

Finally, my configuration is:

final dio = Dio();
dio.interceptors.add(DioCacheManager(
      CacheConfig(
        baseUrl: urlApi,
        defaultRequestMethod: 'POST',
        diskStore: CacheFileStore(
            databaseName: 'MyCache',
            cacheFolderName: 'MyCache'),
      ),
    ).interceptor);