mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-25 00:14:42 +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/favorite/data_source.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file/data_source.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.dart';
|
||||||
import 'package:nc_photos/entity/local_file/data_source.dart';
|
import 'package:nc_photos/entity/local_file/data_source.dart';
|
||||||
import 'package:nc_photos/entity/nc_album/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.fileRepo = FileRepo(FileCachedDataSource(c));
|
||||||
c.fileRepoRemote = const FileRepo(FileWebdavDataSource());
|
c.fileRepoRemote = const FileRepo(FileWebdavDataSource());
|
||||||
c.fileRepoLocal = FileRepo(FileSqliteDbDataSource(c));
|
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.shareRepo = ShareRepo(ShareRemoteDataSource());
|
||||||
c.shareeRepo = ShareeRepo(ShareeRemoteDataSource());
|
c.shareeRepo = ShareeRepo(ShareeRemoteDataSource());
|
||||||
c.favoriteRepo = const FavoriteRepo(FavoriteRemoteDataSource());
|
c.favoriteRepo = const FavoriteRepo(FavoriteRemoteDataSource());
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:kiwi/kiwi.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/controller/account_pref_controller.dart';
|
import 'package:nc_photos/controller/account_pref_controller.dart';
|
||||||
import 'package:nc_photos/controller/collections_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/persons_controller.dart';
|
||||||
import 'package:nc_photos/controller/places_controller.dart';
|
import 'package:nc_photos/controller/places_controller.dart';
|
||||||
import 'package:nc_photos/controller/server_controller.dart';
|
import 'package:nc_photos/controller/server_controller.dart';
|
||||||
|
@ -29,6 +30,8 @@ class AccountController {
|
||||||
_sharingsController = null;
|
_sharingsController = null;
|
||||||
_placesController?.dispose();
|
_placesController?.dispose();
|
||||||
_placesController = null;
|
_placesController = null;
|
||||||
|
_filesController?.dispose();
|
||||||
|
_filesController = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Account get account => _account!;
|
Account get account => _account!;
|
||||||
|
@ -76,6 +79,13 @@ class AccountController {
|
||||||
account: _account!,
|
account: _account!,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
FilesController get filesController =>
|
||||||
|
_filesController ??= FilesController(
|
||||||
|
KiwiContainer().resolve<DiContainer>(),
|
||||||
|
account: _account!,
|
||||||
|
accountPrefController: accountPrefController,
|
||||||
|
);
|
||||||
|
|
||||||
Account? _account;
|
Account? _account;
|
||||||
CollectionsController? _collectionsController;
|
CollectionsController? _collectionsController;
|
||||||
ServerController? _serverController;
|
ServerController? _serverController;
|
||||||
|
@ -85,4 +95,5 @@ class AccountController {
|
||||||
SessionController? _sessionController;
|
SessionController? _sessionController;
|
||||||
SharingsController? _sharingsController;
|
SharingsController? _sharingsController;
|
||||||
PlacesController? _placesController;
|
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/face_recognition_person/repo.dart';
|
||||||
import 'package:nc_photos/entity/favorite.dart';
|
import 'package:nc_photos/entity/favorite.dart';
|
||||||
import 'package:nc_photos/entity/file.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/local_file.dart';
|
||||||
import 'package:nc_photos/entity/nc_album/repo.dart';
|
import 'package:nc_photos/entity/nc_album/repo.dart';
|
||||||
import 'package:nc_photos/entity/pref.dart';
|
import 'package:nc_photos/entity/pref.dart';
|
||||||
|
@ -26,6 +27,9 @@ enum DiType {
|
||||||
fileRepo,
|
fileRepo,
|
||||||
fileRepoRemote,
|
fileRepoRemote,
|
||||||
fileRepoLocal,
|
fileRepoLocal,
|
||||||
|
fileRepo2,
|
||||||
|
fileRepo2Remote,
|
||||||
|
fileRepo2Local,
|
||||||
shareRepo,
|
shareRepo,
|
||||||
shareeRepo,
|
shareeRepo,
|
||||||
favoriteRepo,
|
favoriteRepo,
|
||||||
|
@ -60,6 +64,9 @@ class DiContainer {
|
||||||
FileRepo? fileRepo,
|
FileRepo? fileRepo,
|
||||||
FileRepo? fileRepoRemote,
|
FileRepo? fileRepoRemote,
|
||||||
FileRepo? fileRepoLocal,
|
FileRepo? fileRepoLocal,
|
||||||
|
FileRepo2? fileRepo2,
|
||||||
|
FileRepo2? fileRepo2Remote,
|
||||||
|
FileRepo2? fileRepo2Local,
|
||||||
ShareRepo? shareRepo,
|
ShareRepo? shareRepo,
|
||||||
ShareeRepo? shareeRepo,
|
ShareeRepo? shareeRepo,
|
||||||
FavoriteRepo? favoriteRepo,
|
FavoriteRepo? favoriteRepo,
|
||||||
|
@ -90,6 +97,9 @@ class DiContainer {
|
||||||
_fileRepo = fileRepo,
|
_fileRepo = fileRepo,
|
||||||
_fileRepoRemote = fileRepoRemote,
|
_fileRepoRemote = fileRepoRemote,
|
||||||
_fileRepoLocal = fileRepoLocal,
|
_fileRepoLocal = fileRepoLocal,
|
||||||
|
_fileRepo2 = fileRepo2,
|
||||||
|
_fileRepo2Remote = fileRepo2Remote,
|
||||||
|
_fileRepo2Local = fileRepo2Local,
|
||||||
_shareRepo = shareRepo,
|
_shareRepo = shareRepo,
|
||||||
_shareeRepo = shareeRepo,
|
_shareeRepo = shareeRepo,
|
||||||
_favoriteRepo = favoriteRepo,
|
_favoriteRepo = favoriteRepo,
|
||||||
|
@ -134,6 +144,12 @@ class DiContainer {
|
||||||
return contianer._fileRepoRemote != null;
|
return contianer._fileRepoRemote != null;
|
||||||
case DiType.fileRepoLocal:
|
case DiType.fileRepoLocal:
|
||||||
return contianer._fileRepoLocal != null;
|
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:
|
case DiType.shareRepo:
|
||||||
return contianer._shareRepo != null;
|
return contianer._shareRepo != null;
|
||||||
case DiType.shareeRepo:
|
case DiType.shareeRepo:
|
||||||
|
@ -183,6 +199,7 @@ class DiContainer {
|
||||||
OrNull<AlbumRepo>? albumRepo,
|
OrNull<AlbumRepo>? albumRepo,
|
||||||
OrNull<AlbumRepo2>? albumRepo2,
|
OrNull<AlbumRepo2>? albumRepo2,
|
||||||
OrNull<FileRepo>? fileRepo,
|
OrNull<FileRepo>? fileRepo,
|
||||||
|
OrNull<FileRepo2>? fileRepo2,
|
||||||
OrNull<ShareRepo>? shareRepo,
|
OrNull<ShareRepo>? shareRepo,
|
||||||
OrNull<ShareeRepo>? shareeRepo,
|
OrNull<ShareeRepo>? shareeRepo,
|
||||||
OrNull<FavoriteRepo>? favoriteRepo,
|
OrNull<FavoriteRepo>? favoriteRepo,
|
||||||
|
@ -201,6 +218,7 @@ class DiContainer {
|
||||||
albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj,
|
albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj,
|
||||||
albumRepo2: albumRepo2 == null ? _albumRepo2 : albumRepo2.obj,
|
albumRepo2: albumRepo2 == null ? _albumRepo2 : albumRepo2.obj,
|
||||||
fileRepo: fileRepo == null ? _fileRepo : fileRepo.obj,
|
fileRepo: fileRepo == null ? _fileRepo : fileRepo.obj,
|
||||||
|
fileRepo2: fileRepo2 == null ? _fileRepo2 : fileRepo2.obj,
|
||||||
shareRepo: shareRepo == null ? _shareRepo : shareRepo.obj,
|
shareRepo: shareRepo == null ? _shareRepo : shareRepo.obj,
|
||||||
shareeRepo: shareeRepo == null ? _shareeRepo : shareeRepo.obj,
|
shareeRepo: shareeRepo == null ? _shareeRepo : shareeRepo.obj,
|
||||||
favoriteRepo: favoriteRepo == null ? _favoriteRepo : favoriteRepo.obj,
|
favoriteRepo: favoriteRepo == null ? _favoriteRepo : favoriteRepo.obj,
|
||||||
|
@ -231,6 +249,9 @@ class DiContainer {
|
||||||
FileRepo get fileRepo => _fileRepo!;
|
FileRepo get fileRepo => _fileRepo!;
|
||||||
FileRepo get fileRepoRemote => _fileRepoRemote!;
|
FileRepo get fileRepoRemote => _fileRepoRemote!;
|
||||||
FileRepo get fileRepoLocal => _fileRepoLocal!;
|
FileRepo get fileRepoLocal => _fileRepoLocal!;
|
||||||
|
FileRepo2 get fileRepo2 => _fileRepo2!;
|
||||||
|
FileRepo2 get fileRepo2Remote => _fileRepo2Remote!;
|
||||||
|
FileRepo2 get fileRepo2Local => _fileRepo2Local!;
|
||||||
ShareRepo get shareRepo => _shareRepo!;
|
ShareRepo get shareRepo => _shareRepo!;
|
||||||
ShareeRepo get shareeRepo => _shareeRepo!;
|
ShareeRepo get shareeRepo => _shareeRepo!;
|
||||||
FavoriteRepo get favoriteRepo => _favoriteRepo!;
|
FavoriteRepo get favoriteRepo => _favoriteRepo!;
|
||||||
|
@ -302,6 +323,21 @@ class DiContainer {
|
||||||
_fileRepoLocal = v;
|
_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) {
|
set shareRepo(ShareRepo v) {
|
||||||
assert(_shareRepo == null);
|
assert(_shareRepo == null);
|
||||||
_shareRepo = v;
|
_shareRepo = v;
|
||||||
|
@ -419,6 +455,9 @@ class DiContainer {
|
||||||
FileRepo? _fileRepoRemote;
|
FileRepo? _fileRepoRemote;
|
||||||
// Explicitly request a FileRepo backed by local source
|
// Explicitly request a FileRepo backed by local source
|
||||||
FileRepo? _fileRepoLocal;
|
FileRepo? _fileRepoLocal;
|
||||||
|
FileRepo2? _fileRepo2;
|
||||||
|
FileRepo2? _fileRepo2Remote;
|
||||||
|
FileRepo2? _fileRepo2Local;
|
||||||
ShareRepo? _shareRepo;
|
ShareRepo? _shareRepo;
|
||||||
ShareeRepo? _shareeRepo;
|
ShareeRepo? _shareeRepo;
|
||||||
FavoriteRepo? _favoriteRepo;
|
FavoriteRepo? _favoriteRepo;
|
||||||
|
|
|
@ -420,7 +420,7 @@ class FileSqliteDbDataSource implements FileDataSource {
|
||||||
@override
|
@override
|
||||||
remove(Account account, FileDescriptor f) {
|
remove(Account account, FileDescriptor f) {
|
||||||
_log.info("[remove] ${f.fdPath}");
|
_log.info("[remove] ${f.fdPath}");
|
||||||
return FileSqliteCacheRemover(_c)(account, f);
|
return _c.fileRepo2.remove(account, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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) =>
|
bool isUnderDir(FileDescriptor file, FileDescriptor dir) =>
|
||||||
file.fdPath.startsWith("${dir.fdPath}/");
|
file.fdPath.startsWith("${dir.fdPath}/");
|
||||||
|
|
||||||
|
bool isUnderDirPath(String filePath, String dirPath) =>
|
||||||
|
filePath.startsWith("$dirPath/");
|
||||||
|
|
||||||
/// Return if [file] is [dir] or located under [dir]
|
/// Return if [file] is [dir] or located under [dir]
|
||||||
///
|
///
|
||||||
/// See [isUnderDir]
|
/// See [isUnderDir]
|
||||||
bool isOrUnderDir(FileDescriptor file, FileDescriptor dir) =>
|
bool isOrUnderDir(FileDescriptor file, FileDescriptor dir) =>
|
||||||
file.fdPath == dir.fdPath || isUnderDir(file, 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
|
/// Convert a stripped path to a full path
|
||||||
///
|
///
|
||||||
/// See [File.strippedPath]
|
/// 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)!;
|
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>(
|
Future<List<T>> waitOr<T>(
|
||||||
Iterable<Future<T>> futures,
|
Iterable<Future<T>> futures,
|
||||||
T Function(Object error, StackTrace? stackTrace) onError,
|
T Function(Object error, StackTrace? stackTrace) onError,
|
||||||
|
|
Loading…
Add table
Reference in a new issue