Add new FilesController to improve file handling

This commit is contained in:
Ming Ming 2024-01-13 02:12:32 +08:00
parent d5da991a26
commit 5b20ec4fff
13 changed files with 831 additions and 1 deletions

View file

@ -14,6 +14,8 @@ import 'package:nc_photos/entity/favorite.dart';
import 'package:nc_photos/entity/favorite/data_source.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file/data_source2.dart';
import 'package:nc_photos/entity/file/repo.dart';
import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/entity/local_file/data_source.dart';
import 'package:nc_photos/entity/nc_album/data_source.dart';
@ -147,6 +149,10 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
c.fileRepo = FileRepo(FileCachedDataSource(c));
c.fileRepoRemote = const FileRepo(FileWebdavDataSource());
c.fileRepoLocal = FileRepo(FileSqliteDbDataSource(c));
c.fileRepo2 =
CachedFileRepo(const FileRemoteDataSource(), FileNpDbDataSource(c.npDb));
c.fileRepo2Remote = const BasicFileRepo(FileRemoteDataSource());
c.fileRepo2Local = BasicFileRepo(FileNpDbDataSource(c.npDb));
c.shareRepo = ShareRepo(ShareRemoteDataSource());
c.shareeRepo = ShareeRepo(ShareeRemoteDataSource());
c.favoriteRepo = const FavoriteRepo(FavoriteRemoteDataSource());

View file

@ -2,6 +2,7 @@ import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/controller/account_pref_controller.dart';
import 'package:nc_photos/controller/collections_controller.dart';
import 'package:nc_photos/controller/files_controller.dart';
import 'package:nc_photos/controller/persons_controller.dart';
import 'package:nc_photos/controller/places_controller.dart';
import 'package:nc_photos/controller/server_controller.dart';
@ -29,6 +30,8 @@ class AccountController {
_sharingsController = null;
_placesController?.dispose();
_placesController = null;
_filesController?.dispose();
_filesController = null;
}
Account get account => _account!;
@ -76,6 +79,13 @@ class AccountController {
account: _account!,
);
FilesController get filesController =>
_filesController ??= FilesController(
KiwiContainer().resolve<DiContainer>(),
account: _account!,
accountPrefController: accountPrefController,
);
Account? _account;
CollectionsController? _collectionsController;
ServerController? _serverController;
@ -85,4 +95,5 @@ class AccountController {
SessionController? _sessionController;
SharingsController? _sharingsController;
PlacesController? _placesController;
FilesController? _filesController;
}

View file

@ -0,0 +1,309 @@
import 'dart:async';
import 'dart:collection';
import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/controller/account_pref_controller.dart';
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/rx_extension.dart';
import 'package:nc_photos/use_case/file/list_file.dart';
import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/use_case/update_property.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/lazy.dart';
import 'package:np_common/object_util.dart';
import 'package:np_common/or_null.dart';
import 'package:rxdart/rxdart.dart';
import 'package:to_string/to_string.dart';
part 'files_controller.g.dart';
abstract class FilesStreamEvent {
/// All files as a ordered list
List<FileDescriptor> get data;
/// All files as a map with the fileId as key
Map<int, FileDescriptor> get dataMap;
bool get hasNext;
}
@npLog
class FilesController {
FilesController(
this._c, {
required this.account,
required this.accountPrefController,
});
void dispose() {
_dataStreamController.close();
}
/// Return a stream of files associated with [account]
///
/// The returned stream will emit new list of files whenever there are
/// changes to the files (e.g., new file, removed file, etc)
///
/// There's no guarantee that the returned list is always sorted in some ways,
/// callers must sort it by themselves if the ordering is important
ValueStream<FilesStreamEvent> get stream {
if (!_isDataStreamInited) {
_isDataStreamInited = true;
unawaited(_load());
}
return _dataStreamController.stream;
}
Future<void> reload() async {
var results = <FileDescriptor>[];
final completer = Completer();
ListFile(_c)(
account,
file_util.unstripPath(account, accountPrefController.shareFolder.value),
).listen(
(ev) {
results = ev;
},
onError: _dataStreamController.addError,
onDone: () => completer.complete(),
);
await completer.future;
_dataStreamController
.add(_convertListResultsToEvent(results, hasNext: false));
}
/// Update files property and return number of files updated
Future<void> updateProperty(
List<FileDescriptor> files, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? isFavorite,
OrNull<ImageLocation>? location,
Exception? Function(List<int> fileIds) errorBuilder =
UpdatePropertyFailureError.new,
}) async {
final backups = <int, FileDescriptor>{};
// file ids that need to be queried again to get the correct
// FileDescriptor.fdDateTime
final outdated = <int>[];
await _mutex.protect(() async {
final next = Map.of(_dataStreamController.value.files);
for (final f in files) {
final original = next[f.fdId];
if (original == null) {
_log.warning("[updateProperty] File not found: $f");
continue;
}
backups[f.fdId] = original;
if (original is File) {
next[f.fdId] = original.copyWith(
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
isFavorite: isFavorite,
location: location,
);
} else {
next[f.fdId] = original.copyWith(
fdIsArchived: isArchived == null ? null : (isArchived.obj ?? false),
// in case of unsetting, we can't work out the new value here
fdDateTime: overrideDateTime?.obj,
fdIsFavorite: isFavorite,
);
if (OrNull.isSetNull(overrideDateTime)) {
outdated.add(f.fdId);
}
}
}
_dataStreamController
.addWithValue((value) => value.copyWith(files: next));
});
final failures = <int>[];
for (final f in files) {
try {
await UpdateProperty(_c)(
account,
f,
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
favorite: isFavorite,
location: location,
);
} catch (e, stackTrace) {
_log.severe("Failed while UpdateProperty: ${logFilename(f.fdPath)}", e,
stackTrace);
failures.add(f.fdId);
outdated.remove(f.fdId);
}
}
if (failures.isNotEmpty) {
// restore
final next = Map.of(_dataStreamController.value.files);
for (final f in failures) {
if (backups.containsKey(f)) {
next[f] = backups[f]!;
}
}
_dataStreamController
.addWithValue((value) => value.copyWith(files: next));
errorBuilder(failures)?.let(_dataStreamController.addError);
}
// TODO query outdated
}
Future<void> remove(
List<FileDescriptor> files, {
Exception? Function(List<int> fileIds) errorBuilder =
RemoveFailureError.new,
}) async {
final backups = <int, FileDescriptor>{};
await _mutex.protect(() async {
final next = Map.of(_dataStreamController.value.files);
for (final f in files) {
final original = next.remove(f.fdId);
if (original == null) {
_log.warning("[updateProperty] File not found: $f");
continue;
}
backups[f.fdId] = original;
}
_dataStreamController
.addWithValue((value) => value.copyWith(files: next));
});
final failures = <int>[];
try {
await Remove(_c)(
account,
files,
onError: (index, value, error, stackTrace) {
_log.severe("Failed while Remove: ${logFilename(value.fdPath)}",
error, stackTrace);
failures.add(value.fdId);
},
);
} catch (e, stackTrace) {
_log.severe("Failed while Remove", e, stackTrace);
failures.addAll(files.map((e) => e.fdId));
}
if (failures.isNotEmpty) {
// restore
final next = LinkedHashMap.of(_dataStreamController.value.files);
for (final f in failures) {
if (backups.containsKey(f)) {
next[f] = backups[f]!;
}
}
_dataStreamController
.addWithValue((value) => value.copyWith(files: next));
errorBuilder(failures)?.let(_dataStreamController.addError);
}
}
Future<void> _load() async {
var lastData = _FilesStreamEvent(
files: const {},
hasNext: false,
);
final completer = Completer();
ListFile(_c)(
account,
file_util.unstripPath(account, accountPrefController.shareFolder.value),
).listen(
(ev) {
lastData = _convertListResultsToEvent(ev, hasNext: true);
_dataStreamController.add(lastData);
},
onError: _dataStreamController.addError,
onDone: () => completer.complete(),
);
await completer.future;
_dataStreamController.add(lastData.copyWith(hasNext: false));
}
_FilesStreamEvent _convertListResultsToEvent(
List<FileDescriptor> results, {
required bool hasNext,
}) {
return _FilesStreamEvent(
files: {
for (final f in results) f.fdId: f,
},
hasNext: hasNext,
);
}
final DiContainer _c;
final Account account;
final AccountPrefController accountPrefController;
var _isDataStreamInited = false;
final _dataStreamController = BehaviorSubject.seeded(
_FilesStreamEvent(
files: const {},
hasNext: true,
),
);
final _mutex = Mutex();
}
@toString
class UpdatePropertyFailureError implements Exception {
const UpdatePropertyFailureError(this.fileIds);
@override
String toString() => _$toString();
final List<int> fileIds;
}
@toString
class RemoveFailureError implements Exception {
const RemoveFailureError(this.fileIds);
@override
String toString() => _$toString();
final List<int> fileIds;
}
class _FilesStreamEvent implements FilesStreamEvent {
_FilesStreamEvent({
required this.files,
Lazy<List<FileDescriptor>>? dataLazy,
required this.hasNext,
}) {
this.dataLazy = dataLazy ?? (Lazy(() => files.values.toList()));
}
_FilesStreamEvent copyWith({
Map<int, FileDescriptor>? files,
bool? hasNext,
}) {
return _FilesStreamEvent(
files: files ?? this.files,
dataLazy: (files == null) ? dataLazy : null,
hasNext: hasNext ?? this.hasNext,
);
}
@override
List<FileDescriptor> get data => dataLazy();
@override
Map<int, FileDescriptor> get dataMap => files;
final Map<int, FileDescriptor> files;
late final Lazy<List<FileDescriptor>> dataLazy;
/// If true, the results are intermediate values and may not represent the
/// latest state
@override
final bool hasNext;
}

View file

@ -0,0 +1,32 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'files_controller.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$FilesControllerNpLog on FilesController {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("controller.files_controller.FilesController");
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$UpdatePropertyFailureErrorToString on UpdatePropertyFailureError {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "UpdatePropertyFailureError {fileIds: [length: ${fileIds.length}]}";
}
}
extension _$RemoveFailureErrorToString on RemoveFailureError {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "RemoveFailureError {fileIds: [length: ${fileIds.length}]}";
}
}

View file

@ -3,6 +3,7 @@ import 'package:nc_photos/entity/album/repo2.dart';
import 'package:nc_photos/entity/face_recognition_person/repo.dart';
import 'package:nc_photos/entity/favorite.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/repo.dart';
import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/entity/nc_album/repo.dart';
import 'package:nc_photos/entity/pref.dart';
@ -26,6 +27,9 @@ enum DiType {
fileRepo,
fileRepoRemote,
fileRepoLocal,
fileRepo2,
fileRepo2Remote,
fileRepo2Local,
shareRepo,
shareeRepo,
favoriteRepo,
@ -60,6 +64,9 @@ class DiContainer {
FileRepo? fileRepo,
FileRepo? fileRepoRemote,
FileRepo? fileRepoLocal,
FileRepo2? fileRepo2,
FileRepo2? fileRepo2Remote,
FileRepo2? fileRepo2Local,
ShareRepo? shareRepo,
ShareeRepo? shareeRepo,
FavoriteRepo? favoriteRepo,
@ -90,6 +97,9 @@ class DiContainer {
_fileRepo = fileRepo,
_fileRepoRemote = fileRepoRemote,
_fileRepoLocal = fileRepoLocal,
_fileRepo2 = fileRepo2,
_fileRepo2Remote = fileRepo2Remote,
_fileRepo2Local = fileRepo2Local,
_shareRepo = shareRepo,
_shareeRepo = shareeRepo,
_favoriteRepo = favoriteRepo,
@ -134,6 +144,12 @@ class DiContainer {
return contianer._fileRepoRemote != null;
case DiType.fileRepoLocal:
return contianer._fileRepoLocal != null;
case DiType.fileRepo2:
return contianer._fileRepo2 != null;
case DiType.fileRepo2Remote:
return contianer._fileRepo2Remote != null;
case DiType.fileRepo2Local:
return contianer._fileRepo2Local != null;
case DiType.shareRepo:
return contianer._shareRepo != null;
case DiType.shareeRepo:
@ -183,6 +199,7 @@ class DiContainer {
OrNull<AlbumRepo>? albumRepo,
OrNull<AlbumRepo2>? albumRepo2,
OrNull<FileRepo>? fileRepo,
OrNull<FileRepo2>? fileRepo2,
OrNull<ShareRepo>? shareRepo,
OrNull<ShareeRepo>? shareeRepo,
OrNull<FavoriteRepo>? favoriteRepo,
@ -201,6 +218,7 @@ class DiContainer {
albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj,
albumRepo2: albumRepo2 == null ? _albumRepo2 : albumRepo2.obj,
fileRepo: fileRepo == null ? _fileRepo : fileRepo.obj,
fileRepo2: fileRepo2 == null ? _fileRepo2 : fileRepo2.obj,
shareRepo: shareRepo == null ? _shareRepo : shareRepo.obj,
shareeRepo: shareeRepo == null ? _shareeRepo : shareeRepo.obj,
favoriteRepo: favoriteRepo == null ? _favoriteRepo : favoriteRepo.obj,
@ -231,6 +249,9 @@ class DiContainer {
FileRepo get fileRepo => _fileRepo!;
FileRepo get fileRepoRemote => _fileRepoRemote!;
FileRepo get fileRepoLocal => _fileRepoLocal!;
FileRepo2 get fileRepo2 => _fileRepo2!;
FileRepo2 get fileRepo2Remote => _fileRepo2Remote!;
FileRepo2 get fileRepo2Local => _fileRepo2Local!;
ShareRepo get shareRepo => _shareRepo!;
ShareeRepo get shareeRepo => _shareeRepo!;
FavoriteRepo get favoriteRepo => _favoriteRepo!;
@ -302,6 +323,21 @@ class DiContainer {
_fileRepoLocal = v;
}
set fileRepo2(FileRepo2 v) {
assert(_fileRepo2 == null);
_fileRepo2 = v;
}
set fileRepo2Remote(FileRepo2 v) {
assert(_fileRepo2Remote == null);
_fileRepo2Remote = v;
}
set fileRepo2Local(FileRepo2 v) {
assert(_fileRepo2Local == null);
_fileRepo2Local = v;
}
set shareRepo(ShareRepo v) {
assert(_shareRepo == null);
_shareRepo = v;
@ -419,6 +455,9 @@ class DiContainer {
FileRepo? _fileRepoRemote;
// Explicitly request a FileRepo backed by local source
FileRepo? _fileRepoLocal;
FileRepo2? _fileRepo2;
FileRepo2? _fileRepo2Remote;
FileRepo2? _fileRepo2Local;
ShareRepo? _shareRepo;
ShareeRepo? _shareeRepo;
FavoriteRepo? _favoriteRepo;

View file

@ -420,7 +420,7 @@ class FileSqliteDbDataSource implements FileDataSource {
@override
remove(Account account, FileDescriptor f) {
_log.info("[remove] ${f.fdPath}");
return FileSqliteCacheRemover(_c)(account, f);
return _c.fileRepo2.remove(account, f);
}
@override

View file

@ -0,0 +1,225 @@
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/repo.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/np_api_util.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:np_async/np_async.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/object_util.dart';
import 'package:np_common/or_null.dart';
import 'package:np_db/np_db.dart';
part 'data_source2.g.dart';
@npLog
class FileRemoteDataSource implements FileDataSource2 {
const FileRemoteDataSource();
@override
Stream<List<FileDescriptor>> getFileDescriptors(
Account account, String shareDirPath) {
throw UnsupportedError("getFileDescriptors not supported");
}
@override
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? location,
}) async {
_log.info("[updateProperty] ${f.fdPath}");
if (f is File &&
metadata?.obj != null &&
metadata!.obj!.fileEtag != f.etag) {
_log.warning(
"[updateProperty] Metadata etag mismatch (metadata: ${metadata.obj!.fileEtag}, file: ${f.etag})");
}
final setProps = {
if (metadata?.obj != null)
"app:metadata": jsonEncode(metadata!.obj!.toJson()),
if (isArchived?.obj != null) "app:is-archived": isArchived!.obj,
if (overrideDateTime?.obj != null)
"app:override-date-time":
overrideDateTime!.obj!.toUtc().toIso8601String(),
if (favorite != null) "oc:favorite": favorite ? 1 : 0,
if (location?.obj != null)
"app:location": jsonEncode(location!.obj!.toJson()),
};
final removeProps = [
if (OrNull.isSetNull(metadata)) "app:metadata",
if (OrNull.isSetNull(isArchived)) "app:is-archived",
if (OrNull.isSetNull(overrideDateTime)) "app:override-date-time",
if (OrNull.isSetNull(location)) "app:location",
];
final response = await ApiUtil.fromAccount(account).files().proppatch(
path: f.fdPath,
namespaces: {
"com.nkming.nc_photos": "app",
"http://owncloud.org/ns": "oc",
},
set: setProps.isNotEmpty ? setProps : null,
remove: removeProps.isNotEmpty ? removeProps : null,
);
if (!response.isGood) {
_log.severe("[updateProperty] Failed requesting server: $response");
throw ApiException(
response: response,
message: "Server responed with an error: HTTP ${response.statusCode}",
);
}
}
@override
Future<void> remove(Account account, FileDescriptor f) async {
_log.info("[remove] ${f.fdPath}");
final response =
await ApiUtil.fromAccount(account).files().delete(path: f.fdPath);
if (!response.isGood) {
_log.severe("[remove] Failed requesting server: $response");
throw ApiException(
response: response,
message: "Server responed with an error: HTTP ${response.statusCode}",
);
}
}
}
@npLog
class FileNpDbDataSource implements FileDataSource2 {
const FileNpDbDataSource(this.db);
@override
Stream<List<FileDescriptor>> getFileDescriptors(
Account account, String shareDirPath) async* {
_log.info("[getFileDescriptors] $account");
yield await _getPartialFileDescriptors(account);
yield await _getCompleteFileDescriptors(account, shareDirPath);
}
@override
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? location,
}) async {
_log.info("[updateProperty] ${f.fdPath}");
if (overrideDateTime != null || metadata != null) {
f = DbFileConverter.fromDb(
account.userId.toCaseInsensitiveString(),
await db.getFilesByFileIds(
account: account.toDb(),
fileIds: [f.fdId],
).first,
);
}
await db.updateFileByFileId(
account: account.toDb(),
fileId: f.fdId,
isFavorite: favorite?.let(OrNull.new),
isArchived: isArchived,
overrideDateTime: overrideDateTime,
bestDateTime: overrideDateTime == null && metadata == null
? null
: file_util.getBestDateTime(
overrideDateTime: overrideDateTime == null
? (f as File).overrideDateTime
: overrideDateTime.obj,
dateTimeOriginal: metadata == null
? (f as File).metadata?.exif?.dateTimeOriginal
: metadata.obj?.exif?.dateTimeOriginal,
lastModified: (f as File).lastModified,
),
imageData: metadata?.let((e) => OrNull(e.obj?.toDb())),
location: location?.let((e) => OrNull(e.obj?.toDb())),
);
}
@override
Future<void> remove(Account account, FileDescriptor f) async {
_log.info("[remove] ${f.fdPath}");
await db.deleteFile(
account: account.toDb(),
file: f.toDbKey(),
);
}
Future<List<FileDescriptor>> _getPartialFileDescriptors(
Account account) async {
_log.info("[_getPartialFileDescriptors] $account");
final results = await db.getFileDescriptors(
account: account.toDb(),
// need this because this arg expect empty string for root instead of "."
includeRelativeRoots: account.roots
.map((e) => File(path: file_util.unstripPath(account, e))
.strippedPathWithEmpty)
.toList(),
excludeRelativeRoots: [remote_storage_util.remoteStorageDirRelativePath],
mimes: file_util.supportedFormatMimes,
limit: _partialCount,
);
return results
.map((e) =>
DbFileDescriptorConverter.fromDb(account.userId.toString(), e))
.toList();
}
Future<List<FileDescriptor>> _getCompleteFileDescriptors(
Account account, String shareDirPath) async {
_log.info("[_getCompleteFileDescriptors] $account");
final dbResults = await db.getFileDescriptors(
account: account.toDb(),
includeRelativeRoots: account.roots
.map((e) => File(path: file_util.unstripPath(account, e))
.strippedPathWithEmpty)
.toList(),
excludeRelativeRoots: [remote_storage_util.remoteStorageDirRelativePath],
mimes: file_util.supportedFormatMimes,
);
final results = dbResults
.map((e) => DbFileDescriptorConverter.fromDb(
account.userId.toCaseInsensitiveString(), e))
.toList();
final isShareDirIncluded = account.roots.any((e) => file_util
.isOrUnderDirPath(shareDirPath, file_util.unstripPath(account, e)));
if (!isShareDirIncluded) {
_log.info(
"[_getCompleteFileDescriptors] Explicitly getting share folder");
try {
final shareDirResults = await db.getFilesByDirKey(
account: account.toDb(),
dir: File(path: shareDirPath).toDbKey(),
);
results.addAll(shareDirResults.map((e) => DbFileConverter.fromDb(
account.userId.toCaseInsensitiveString(), e)));
} on DbNotFoundException catch (_) {
// normal when there's no cache
} catch (e, stackTrace) {
_log.shout(
"[_getCompleteFileDescriptors] Failed while getFilesByDirKey: ${logFilename(shareDirPath)}",
e,
stackTrace);
}
}
return results;
}
final NpDb db;
static const _partialCount = 100;
}

View file

@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'data_source2.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$FileRemoteDataSourceNpLog on FileRemoteDataSource {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("entity.file.data_source2.FileRemoteDataSource");
}
extension _$FileNpDbDataSourceNpLog on FileNpDbDataSource {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("entity.file.data_source2.FileNpDbDataSource");
}

View file

@ -0,0 +1,144 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/or_null.dart';
part 'repo.g.dart';
abstract class FileRepo2 {
/// Query all files belonging to [account]
///
/// Normally the stream should complete with only a single event, but some
/// implementation might want to return multiple set of values, say one set of
/// cached value and later another set of updated value from a remote source.
/// In any case, each event is guaranteed to be one complete set of data
Stream<List<FileDescriptor>> getFileDescriptors(
Account account, String shareDirPath);
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? location,
});
Future<void> remove(Account account, FileDescriptor f);
}
/// A repo that simply relay the call to the backed [FileDataSource]
@npLog
class BasicFileRepo implements FileRepo2 {
const BasicFileRepo(this.dataSrc);
@override
Stream<List<FileDescriptor>> getFileDescriptors(
Account account, String shareDirPath) =>
dataSrc.getFileDescriptors(account, shareDirPath);
@override
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? location,
}) =>
dataSrc.updateProperty(
account,
f,
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
favorite: favorite,
location: location,
);
@override
Future<void> remove(Account account, FileDescriptor f) =>
dataSrc.remove(account, f);
final FileDataSource2 dataSrc;
}
/// A repo that manage a remote data source and a cache data source
@npLog
class CachedFileRepo implements FileRepo2 {
const CachedFileRepo(this.remoteDataSrc, this.cacheDataSrc);
@override
Stream<List<FileDescriptor>> getFileDescriptors(
Account account, String shareDirPath) =>
cacheDataSrc.getFileDescriptors(account, shareDirPath);
@override
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? location,
}) async {
await remoteDataSrc.updateProperty(
account,
f,
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
favorite: favorite,
location: location,
);
try {
await cacheDataSrc.updateProperty(
account,
f,
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
favorite: favorite,
location: location,
);
} catch (e, stackTrace) {
_log.warning("[updateProperty] Failed to update cache", e, stackTrace);
}
}
@override
Future<void> remove(Account account, FileDescriptor f) async {
await remoteDataSrc.remove(account, f);
try {
await cacheDataSrc.remove(account, f);
} catch (e, stackTrace) {
_log.warning("[remove] Failed to update cache", e, stackTrace);
}
}
final FileDataSource2 remoteDataSrc;
final FileDataSource2 cacheDataSrc;
}
abstract class FileDataSource2 {
/// Query all files belonging to [account]
Stream<List<FileDescriptor>> getFileDescriptors(
Account account, String shareDirPath);
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? location,
});
Future<void> remove(Account account, FileDescriptor f);
}

View file

@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'repo.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$BasicFileRepoNpLog on BasicFileRepo {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("entity.file.repo.BasicFileRepo");
}
extension _$CachedFileRepoNpLog on CachedFileRepo {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("entity.file.repo.CachedFileRepo");
}

View file

@ -49,12 +49,18 @@ bool isNcAlbumFile(Account account, FileDescriptor file) =>
bool isUnderDir(FileDescriptor file, FileDescriptor dir) =>
file.fdPath.startsWith("${dir.fdPath}/");
bool isUnderDirPath(String filePath, String dirPath) =>
filePath.startsWith("$dirPath/");
/// Return if [file] is [dir] or located under [dir]
///
/// See [isUnderDir]
bool isOrUnderDir(FileDescriptor file, FileDescriptor dir) =>
file.fdPath == dir.fdPath || isUnderDir(file, dir);
bool isOrUnderDirPath(String filePath, String dirPath) =>
filePath == dirPath || isUnderDirPath(filePath, dirPath);
/// Convert a stripped path to a full path
///
/// See [File.strippedPath]

View file

@ -0,0 +1,12 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
class ListFile {
const ListFile(this._c);
Stream<List<FileDescriptor>> call(Account account, String shareDirPath) =>
_c.fileRepo2.getFileDescriptors(account, shareDirPath);
final DiContainer _c;
}

View file

@ -6,6 +6,10 @@ extension FutureNotNullExtension<T> on Future<T?> {
Future<T> notNull() async => (await this)!;
}
extension FutureCollectionExtension<T> on Future<Iterable<T>> {
Future<T> get first async => (await this).first;
}
Future<List<T>> waitOr<T>(
Iterable<Future<T>> futures,
T Function(Object error, StackTrace? stackTrace) onError,