mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Add new FilesController to improve file handling
This commit is contained in:
parent
d5da991a26
commit
5b20ec4fff
13 changed files with 831 additions and 1 deletions
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
309
app/lib/controller/files_controller.dart
Normal file
309
app/lib/controller/files_controller.dart
Normal 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;
|
||||
}
|
32
app/lib/controller/files_controller.g.dart
Normal file
32
app/lib/controller/files_controller.g.dart
Normal 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}]}";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
225
app/lib/entity/file/data_source2.dart
Normal file
225
app/lib/entity/file/data_source2.dart
Normal 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;
|
||||
}
|
21
app/lib/entity/file/data_source2.g.dart
Normal file
21
app/lib/entity/file/data_source2.g.dart
Normal 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");
|
||||
}
|
144
app/lib/entity/file/repo.dart
Normal file
144
app/lib/entity/file/repo.dart
Normal 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);
|
||||
}
|
21
app/lib/entity/file/repo.g.dart
Normal file
21
app/lib/entity/file/repo.g.dart
Normal 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");
|
||||
}
|
|
@ -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]
|
||||
|
|
12
app/lib/use_case/file/list_file.dart
Normal file
12
app/lib/use_case/file/list_file.dart
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue