From a846a51332edc37f6ec7e076066d556d85d3ed5d Mon Sep 17 00:00:00 2001 From: Ming Ming <nkming2@gmail.com> Date: Sat, 29 May 2021 01:15:09 +0800 Subject: [PATCH] Treat metadata as property --- lib/bloc/list_album.dart | 16 +++-- lib/bloc/scan_dir.dart | 34 ++++++----- lib/entity/file.dart | 23 ++++--- lib/entity/file/data_source.dart | 49 +++++++++++---- lib/event/event.dart | 13 +++- lib/or_null.dart | 2 + lib/use_case/update_missing_metadata.dart | 12 +++- ...ate_metadata.dart => update_property.dart} | 61 +++++++++++++++---- 8 files changed, 153 insertions(+), 57 deletions(-) rename lib/use_case/{update_metadata.dart => update_property.dart} (51%) diff --git a/lib/bloc/list_album.dart b/lib/bloc/list_album.dart index fbd143af..218eb622 100644 --- a/lib/bloc/list_album.dart +++ b/lib/bloc/list_album.dart @@ -89,15 +89,15 @@ class ListAlbumBlocInconsistent extends ListAlbumBlocState { class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> { ListAlbumBloc() : super(ListAlbumBlocInit()) { - _fileMetadataUpdatedListener = - AppEventListener<FileMetadataUpdatedEvent>(_onFileMetadataUpdatedEvent); + _filePropertyUpdatedListener = + AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdatedEvent); _albumUpdatedListener = AppEventListener<AlbumUpdatedEvent>(_onAlbumUpdatedEvent); _fileRemovedListener = AppEventListener<FileRemovedEvent>(_onFileRemovedEvent); _albumCreatedListener = AppEventListener<AlbumCreatedEvent>(_onAlbumCreatedEvent); - _fileMetadataUpdatedListener.begin(); + _filePropertyUpdatedListener.begin(); _albumUpdatedListener.begin(); _fileRemovedListener.begin(); _albumCreatedListener.begin(); @@ -115,7 +115,7 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> { @override close() { - _fileMetadataUpdatedListener.end(); + _filePropertyUpdatedListener.end(); _albumUpdatedListener.end(); _fileRemovedListener.end(); _albumCreatedListener.end(); @@ -160,7 +160,11 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> { yield ListAlbumBlocInconsistent(state.account, state.albums); } - void _onFileMetadataUpdatedEvent(FileMetadataUpdatedEvent ev) { + void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) { + if (!ev.hasAnyProperties([FilePropertyUpdatedEvent.propMetadata])) { + // not interested + return; + } if (state is ListAlbumBlocInit) { // no data in this bloc, ignore return; @@ -220,7 +224,7 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> { } } - AppEventListener<FileMetadataUpdatedEvent> _fileMetadataUpdatedListener; + AppEventListener<FilePropertyUpdatedEvent> _filePropertyUpdatedListener; AppEventListener<AlbumUpdatedEvent> _albumUpdatedListener; AppEventListener<FileRemovedEvent> _fileRemovedListener; AppEventListener<AlbumCreatedEvent> _albumCreatedListener; diff --git a/lib/bloc/scan_dir.dart b/lib/bloc/scan_dir.dart index 4b4b4b83..42768743 100644 --- a/lib/bloc/scan_dir.dart +++ b/lib/bloc/scan_dir.dart @@ -118,10 +118,10 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> { ScanDirBloc() : super(ScanDirBlocInit()) { _fileRemovedEventListener = AppEventListener<FileRemovedEvent>(_onFileRemovedEvent); - _fileMetadataUpdatedEventListener = - AppEventListener<FileMetadataUpdatedEvent>(_onFileMetadataUpdatedEvent); + _filePropertyUpdatedEventListener = + AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdatedEvent); _fileRemovedEventListener.begin(); - _fileMetadataUpdatedEventListener.begin(); + _filePropertyUpdatedEventListener.begin(); } static ScanDirBloc of(Account account) { @@ -165,8 +165,8 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> { @override close() { _fileRemovedEventListener.end(); - _fileMetadataUpdatedEventListener.end(); - _metadataUpdatedSubscription?.cancel(); + _filePropertyUpdatedEventListener.end(); + _propertyUpdatedSubscription?.cancel(); return super.close(); } @@ -215,22 +215,28 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> { add(_ScanDirBlocExternalEvent()); } - void _onFileMetadataUpdatedEvent(FileMetadataUpdatedEvent ev) { + void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) { + if (!ev.hasAnyProperties([ + FilePropertyUpdatedEvent.propMetadata, + ])) { + // not interested + return; + } if (state is ScanDirBlocInit) { // no data in this bloc, ignore return; } - _successiveMetadataUpdatedCount += 1; - _metadataUpdatedSubscription?.cancel(); + _successivePropertyUpdatedCount += 1; + _propertyUpdatedSubscription?.cancel(); // only trigger the event on the 10th update or 10s after the last update - if (_successiveMetadataUpdatedCount % 10 == 0) { + if (_successivePropertyUpdatedCount % 10 == 0) { add(_ScanDirBlocExternalEvent()); } else { - _metadataUpdatedSubscription = + _propertyUpdatedSubscription = Future.delayed(const Duration(seconds: 10)).asStream().listen((_) { add(_ScanDirBlocExternalEvent()); - _successiveMetadataUpdatedCount = 0; + _successivePropertyUpdatedCount = 0; }); } } @@ -267,10 +273,10 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> { } AppEventListener<FileRemovedEvent> _fileRemovedEventListener; - AppEventListener<FileMetadataUpdatedEvent> _fileMetadataUpdatedEventListener; + AppEventListener<FilePropertyUpdatedEvent> _filePropertyUpdatedEventListener; - int _successiveMetadataUpdatedCount = 0; - StreamSubscription<void> _metadataUpdatedSubscription; + int _successivePropertyUpdatedCount = 0; + StreamSubscription<void> _propertyUpdatedSubscription; bool _shouldCheckCache = true; diff --git a/lib/entity/file.dart b/lib/entity/file.dart index bbf1d298..af31ee97 100644 --- a/lib/entity/file.dart +++ b/lib/entity/file.dart @@ -366,8 +366,16 @@ class FileRepo { this.dataSrc.putBinary(account, path, content); /// See [FileDataSource.updateMetadata] - Future<void> updateMetadata(Account account, File file, Metadata metadata) => - this.dataSrc.updateMetadata(account, file, metadata); + Future<void> updateProperty( + Account account, + File file, { + OrNull<Metadata> metadata, + }) => + this.dataSrc.updateProperty( + account, + file, + metadata: metadata, + ); /// See [FileDataSource.copy] Future<void> copy( @@ -417,11 +425,12 @@ abstract class FileDataSource { /// Upload content to [path] Future<void> putBinary(Account account, String path, Uint8List content); - /// Update metadata for a file - /// - /// This will completely replace the metadata of the file [f]. Partial update - /// is not supported - Future<void> updateMetadata(Account account, File f, Metadata metadata); + /// Update one or more properties of a file + Future<void> updateProperty( + Account account, + File f, { + OrNull<Metadata> metadata, + }); /// Copy [f] to [destination] /// diff --git a/lib/entity/file/data_source.dart b/lib/entity/file/data_source.dart index b9de38c4..7976a196 100644 --- a/lib/entity/file/data_source.dart +++ b/lib/entity/file/data_source.dart @@ -105,17 +105,22 @@ class FileWebdavDataSource implements FileDataSource { } @override - updateMetadata(Account account, File f, Metadata metadata) async { - _log.info("[updateMetadata] ${f.path}"); - if (metadata != null && metadata.fileEtag != f.etag) { + updateProperty( + Account account, + File f, { + OrNull<Metadata> metadata, + }) async { + _log.info("[updateProperty] ${f.path}"); + if (metadata?.obj != null && metadata.obj.fileEtag != f.etag) { _log.warning( - "[updateMetadata] etag mismatch (metadata: ${metadata.fileEtag}, file: ${f.etag})"); + "[updateProperty] Metadata etag mismatch (metadata: ${metadata.obj.fileEtag}, file: ${f.etag})"); } final setProps = { - if (metadata != null) "app:metadata": jsonEncode(metadata.toJson()), + if (metadata?.obj != null) + "app:metadata": jsonEncode(metadata.obj.toJson()), }; final removeProps = [ - if (metadata == null) "app:metadata", + if (OrNull.isNull(metadata)) "app:metadata", ]; final response = await Api(account).files().proppatch( path: f.path, @@ -126,7 +131,7 @@ class FileWebdavDataSource implements FileDataSource { remove: removeProps.isNotEmpty ? removeProps : null, ); if (!response.isGood) { - _log.severe("[updateMetadata] Failed requesting server: $response"); + _log.severe("[updateProperty] Failed requesting server: $response"); throw ApiException( response: response, message: "Failed communicating with server: ${response.statusCode}"); @@ -236,8 +241,12 @@ class FileAppDbDataSource implements FileDataSource { } @override - updateMetadata(Account account, File f, Metadata metadata) { - _log.info("[updateMetadata] ${f.path}"); + updateProperty( + Account account, + File f, { + OrNull<Metadata> metadata, + }) { + _log.info("[updateProperty] ${f.path}"); return AppDb.use((db) async { final transaction = db.transaction(AppDb.fileStoreName, idbModeReadWrite); final store = transaction.objectStore(AppDb.fileStoreName); @@ -245,7 +254,9 @@ class FileAppDbDataSource implements FileDataSource { final parentList = await _doList(store, account, parentDir); final jsonList = parentList.map((e) { if (e.path == f.path) { - return e.copyWith(metadata: OrNull(metadata)); + return e.copyWith( + metadata: metadata, + ); } else { return e; } @@ -369,10 +380,22 @@ class FileCachedDataSource implements FileDataSource { } @override - updateMetadata(Account account, File f, Metadata metadata) async { + updateProperty( + Account account, + File f, { + OrNull<Metadata> metadata, + }) async { await _remoteSrc - .updateMetadata(account, f, metadata) - .then((_) => _appDbSrc.updateMetadata(account, f, metadata)); + .updateProperty( + account, + f, + metadata: metadata, + ) + .then((_) => _appDbSrc.updateProperty( + account, + f, + metadata: metadata, + )); // generate a new random token final token = Uuid().v4().replaceAll("-", ""); diff --git a/lib/event/event.dart b/lib/event/event.dart index b0ec26b7..cf8ffb18 100644 --- a/lib/event/event.dart +++ b/lib/event/event.dart @@ -48,11 +48,15 @@ class AlbumUpdatedEvent { final Album album; } -class FileMetadataUpdatedEvent { - FileMetadataUpdatedEvent(this.account, this.file); +class FilePropertyUpdatedEvent { + FilePropertyUpdatedEvent(this.account, this.file, this.properties); final Account account; final File file; + final int properties; + + // Bit masks for properties field + static const propMetadata = 0x01; } class FileRemovedEvent { @@ -63,3 +67,8 @@ class FileRemovedEvent { } class ThemeChangedEvent {} + +extension FilePropertyUpdatedEventExtension on FilePropertyUpdatedEvent { + bool hasAnyProperties(List<int> properties) => + properties.any((p) => this.properties & p != 0); +} diff --git a/lib/or_null.dart b/lib/or_null.dart index e3b816dc..faef3c7d 100644 --- a/lib/or_null.dart +++ b/lib/or_null.dart @@ -2,5 +2,7 @@ class OrNull<T> { OrNull(this.obj); + static bool isNull(OrNull x) => x != null && x.obj == null; + final T obj; } diff --git a/lib/use_case/update_missing_metadata.dart b/lib/use_case/update_missing_metadata.dart index e6eaaae1..6715e308 100644 --- a/lib/use_case/update_missing_metadata.dart +++ b/lib/use_case/update_missing_metadata.dart @@ -7,8 +7,9 @@ import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/mobile/platform.dart' if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; +import 'package:nc_photos/or_null.dart'; import 'package:nc_photos/use_case/scan_missing_metadata.dart'; -import 'package:nc_photos/use_case/update_metadata.dart'; +import 'package:nc_photos/use_case/update_property.dart'; class UpdateMissingMetadata { UpdateMissingMetadata(this.fileRepo); @@ -51,8 +52,13 @@ class UpdateMissingMetadata { exif: exif, ); - await UpdateMetadata(FileRepo(FileCachedDataSource()), - AlbumRepo(AlbumCachedDataSource()))(account, file, metadataObj); + final updateOp = UpdateProperty(FileRepo(FileCachedDataSource()), + AlbumRepo(AlbumCachedDataSource())); + await updateOp( + account, + file, + metadata: OrNull(metadataObj), + ); yield file; } catch (e, stacktrace) { _log.shout( diff --git a/lib/use_case/update_metadata.dart b/lib/use_case/update_property.dart similarity index 51% rename from lib/use_case/update_metadata.dart rename to lib/use_case/update_property.dart index ba0ceb7a..7e1b2b61 100644 --- a/lib/use_case/update_metadata.dart +++ b/lib/use_case/update_property.dart @@ -9,23 +9,50 @@ import 'package:nc_photos/or_null.dart'; import 'package:nc_photos/use_case/list_album.dart'; import 'package:nc_photos/use_case/update_album.dart'; -class UpdateMetadata { - UpdateMetadata(this.fileRepo, this.albumRepo); +class UpdateProperty { + UpdateProperty(this.fileRepo, this.albumRepo); - Future<void> call(Account account, File file, Metadata metadata) async { - if (metadata != null && metadata.fileEtag != file.etag) { - _log.warning( - "[call] Metadata fileEtag mismatch with actual file's (metadata: ${metadata.fileEtag}, file: ${file.etag})"); + Future<void> call( + Account account, + File file, { + OrNull<Metadata> metadata, + }) async { + if (metadata == null) { + // ? + _log.warning("[call] Nothing to update"); + return; } - await fileRepo.updateMetadata(account, file, metadata); - await _cleanUpAlbums(account, file, metadata); + + if (metadata?.obj != null && metadata.obj.fileEtag != file.etag) { + _log.warning( + "[call] Metadata fileEtag mismatch with actual file's (metadata: ${metadata.obj.fileEtag}, file: ${file.etag})"); + } + await fileRepo.updateProperty( + account, + file, + metadata: metadata, + ); + await _cleanUpAlbums( + account, + file, + metadata: metadata, + ); + + int properties = 0; + if (metadata != null) { + properties |= FilePropertyUpdatedEvent.propMetadata; + } + assert(properties != 0); KiwiContainer() .resolve<EventBus>() - .fire(FileMetadataUpdatedEvent(account, file)); + .fire(FilePropertyUpdatedEvent(account, file, properties)); } Future<void> _cleanUpAlbums( - Account account, File file, Metadata metadata) async { + Account account, + File file, { + OrNull<Metadata> metadata, + }) async { final albums = await ListAlbum(fileRepo, albumRepo)(account); for (final a in albums) { try { @@ -34,7 +61,9 @@ class UpdateMetadata { final newItems = a.items.map((e) { if (e is AlbumFileItem && e.file.path == file.path) { return AlbumFileItem( - file: e.file.copyWith(metadata: OrNull(metadata)), + file: e.file.copyWith( + metadata: metadata, + ), ); } else { return e; @@ -53,5 +82,13 @@ class UpdateMetadata { final FileRepo fileRepo; final AlbumRepo albumRepo; - static final _log = Logger("use_case.update_metadata.UpdateMetadata"); + static final _log = Logger("use_case.update_property.UpdateProperty"); +} + +extension UpdatePropertyExtension on UpdateProperty { + /// Convenience function to only update metadata + /// + /// See [UpdateProperty.call] + Future<void> updateMetadata(Account account, File file, Metadata metadata) => + call(account, file, metadata: OrNull(metadata)); }