nc-photos/app/lib/entity/album/repo2.dart

150 lines
4 KiB
Dart

import 'dart:async';
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/exception.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/type.dart';
part 'repo2.g.dart';
abstract class AlbumRepo2 {
/// Query all [Album]s defined by [albumFiles]
Stream<List<Album>> getAlbums(
Account account,
List<File> albumFiles, {
ErrorWithValueHandler<File>? onError,
});
/// Create a new [album]
Future<Album> create(Account account, Album album);
/// Update an [album]
Future<void> update(Account account, Album album);
}
class BasicAlbumRepo2 implements AlbumRepo2 {
const BasicAlbumRepo2(this.dataSrc);
@override
Stream<List<Album>> getAlbums(
Account account,
List<File> albumFiles, {
ErrorWithValueHandler<File>? onError,
}) async* {
yield await dataSrc.getAlbums(account, albumFiles, onError: onError);
}
@override
Future<Album> create(Account account, Album album) =>
dataSrc.create(account, album);
@override
Future<void> update(Account account, Album album) =>
dataSrc.update(account, album);
final AlbumDataSource2 dataSrc;
}
@npLog
class CachedAlbumRepo2 implements AlbumRepo2 {
const CachedAlbumRepo2(this.remoteDataSrc, this.cacheDataSrc);
@override
Stream<List<Album>> getAlbums(
Account account,
List<File> albumFiles, {
ErrorWithValueHandler<File>? onError,
}) async* {
// get cache
final cached = <Album>[];
final failed = <File>[];
try {
cached.addAll(await cacheDataSrc.getAlbums(
account,
albumFiles,
onError: (f, e, stackTrace) {
failed.add(f);
if (e is CacheNotFoundException) {
// not in cache, normal
} else {
_log.shout("[getAlbums] Cache failure", e, stackTrace);
}
},
));
yield cached;
} catch (e, stackTrace) {
_log.shout("[getAlbums] Failed while getAlbums", e, stackTrace);
}
final cachedGroup = cached.groupListsBy((c) {
try {
return _validateCache(
c, albumFiles.firstWhere(c.albumFile!.compareServerIdentity));
} catch (_) {
return false;
}
});
// query remote
final outdated = [
...failed,
...cachedGroup[false]?.map((e) => e.albumFile!) ?? const <File>[],
];
final remote =
await remoteDataSrc.getAlbums(account, outdated, onError: onError);
yield (cachedGroup[true] ?? []) + remote;
// update cache
for (final a in remote) {
unawaited(cacheDataSrc.update(account, a));
}
}
@override
Future<Album> create(Account account, Album album) =>
remoteDataSrc.create(account, album);
@override
Future<void> update(Account account, Album album) async {
await remoteDataSrc.update(account, album);
try {
await cacheDataSrc.update(account, album);
} catch (e, stackTrace) {
_log.warning("[update] Failed to update cache", e, stackTrace);
}
}
/// Return true if the cached album is considered up to date
bool _validateCache(Album cache, File albumFile) {
if (cache.albumFile!.etag?.isNotEmpty == true &&
cache.albumFile!.etag == albumFile.etag) {
// cache is good
_log.fine("[_validateCache] etag matched for ${albumFile.path}");
return true;
} else {
_log.info(
"[_validateCache] Remote content updated for ${albumFile.path}");
return false;
}
}
final AlbumDataSource2 remoteDataSrc;
final AlbumDataSource2 cacheDataSrc;
}
abstract class AlbumDataSource2 {
/// Query all [Album]s defined by [albumFiles]
Future<List<Album>> getAlbums(
Account account,
List<File> albumFiles, {
ErrorWithValueHandler<File>? onError,
});
Future<Album> create(Account account, Album album);
Future<void> update(Account account, Album album);
}