Treat metadata as property

This commit is contained in:
Ming Ming 2021-05-29 01:15:09 +08:00
parent ed9d5de275
commit a846a51332
8 changed files with 153 additions and 57 deletions

View file

@ -89,15 +89,15 @@ class ListAlbumBlocInconsistent extends ListAlbumBlocState {
class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> { class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
ListAlbumBloc() : super(ListAlbumBlocInit()) { ListAlbumBloc() : super(ListAlbumBlocInit()) {
_fileMetadataUpdatedListener = _filePropertyUpdatedListener =
AppEventListener<FileMetadataUpdatedEvent>(_onFileMetadataUpdatedEvent); AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdatedEvent);
_albumUpdatedListener = _albumUpdatedListener =
AppEventListener<AlbumUpdatedEvent>(_onAlbumUpdatedEvent); AppEventListener<AlbumUpdatedEvent>(_onAlbumUpdatedEvent);
_fileRemovedListener = _fileRemovedListener =
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent); AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
_albumCreatedListener = _albumCreatedListener =
AppEventListener<AlbumCreatedEvent>(_onAlbumCreatedEvent); AppEventListener<AlbumCreatedEvent>(_onAlbumCreatedEvent);
_fileMetadataUpdatedListener.begin(); _filePropertyUpdatedListener.begin();
_albumUpdatedListener.begin(); _albumUpdatedListener.begin();
_fileRemovedListener.begin(); _fileRemovedListener.begin();
_albumCreatedListener.begin(); _albumCreatedListener.begin();
@ -115,7 +115,7 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
@override @override
close() { close() {
_fileMetadataUpdatedListener.end(); _filePropertyUpdatedListener.end();
_albumUpdatedListener.end(); _albumUpdatedListener.end();
_fileRemovedListener.end(); _fileRemovedListener.end();
_albumCreatedListener.end(); _albumCreatedListener.end();
@ -160,7 +160,11 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
yield ListAlbumBlocInconsistent(state.account, state.albums); 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) { if (state is ListAlbumBlocInit) {
// no data in this bloc, ignore // no data in this bloc, ignore
return; return;
@ -220,7 +224,7 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
} }
} }
AppEventListener<FileMetadataUpdatedEvent> _fileMetadataUpdatedListener; AppEventListener<FilePropertyUpdatedEvent> _filePropertyUpdatedListener;
AppEventListener<AlbumUpdatedEvent> _albumUpdatedListener; AppEventListener<AlbumUpdatedEvent> _albumUpdatedListener;
AppEventListener<FileRemovedEvent> _fileRemovedListener; AppEventListener<FileRemovedEvent> _fileRemovedListener;
AppEventListener<AlbumCreatedEvent> _albumCreatedListener; AppEventListener<AlbumCreatedEvent> _albumCreatedListener;

View file

@ -118,10 +118,10 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
ScanDirBloc() : super(ScanDirBlocInit()) { ScanDirBloc() : super(ScanDirBlocInit()) {
_fileRemovedEventListener = _fileRemovedEventListener =
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent); AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
_fileMetadataUpdatedEventListener = _filePropertyUpdatedEventListener =
AppEventListener<FileMetadataUpdatedEvent>(_onFileMetadataUpdatedEvent); AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdatedEvent);
_fileRemovedEventListener.begin(); _fileRemovedEventListener.begin();
_fileMetadataUpdatedEventListener.begin(); _filePropertyUpdatedEventListener.begin();
} }
static ScanDirBloc of(Account account) { static ScanDirBloc of(Account account) {
@ -165,8 +165,8 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
@override @override
close() { close() {
_fileRemovedEventListener.end(); _fileRemovedEventListener.end();
_fileMetadataUpdatedEventListener.end(); _filePropertyUpdatedEventListener.end();
_metadataUpdatedSubscription?.cancel(); _propertyUpdatedSubscription?.cancel();
return super.close(); return super.close();
} }
@ -215,22 +215,28 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
add(_ScanDirBlocExternalEvent()); add(_ScanDirBlocExternalEvent());
} }
void _onFileMetadataUpdatedEvent(FileMetadataUpdatedEvent ev) { void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) {
if (!ev.hasAnyProperties([
FilePropertyUpdatedEvent.propMetadata,
])) {
// not interested
return;
}
if (state is ScanDirBlocInit) { if (state is ScanDirBlocInit) {
// no data in this bloc, ignore // no data in this bloc, ignore
return; return;
} }
_successiveMetadataUpdatedCount += 1; _successivePropertyUpdatedCount += 1;
_metadataUpdatedSubscription?.cancel(); _propertyUpdatedSubscription?.cancel();
// only trigger the event on the 10th update or 10s after the last update // only trigger the event on the 10th update or 10s after the last update
if (_successiveMetadataUpdatedCount % 10 == 0) { if (_successivePropertyUpdatedCount % 10 == 0) {
add(_ScanDirBlocExternalEvent()); add(_ScanDirBlocExternalEvent());
} else { } else {
_metadataUpdatedSubscription = _propertyUpdatedSubscription =
Future.delayed(const Duration(seconds: 10)).asStream().listen((_) { Future.delayed(const Duration(seconds: 10)).asStream().listen((_) {
add(_ScanDirBlocExternalEvent()); add(_ScanDirBlocExternalEvent());
_successiveMetadataUpdatedCount = 0; _successivePropertyUpdatedCount = 0;
}); });
} }
} }
@ -267,10 +273,10 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
} }
AppEventListener<FileRemovedEvent> _fileRemovedEventListener; AppEventListener<FileRemovedEvent> _fileRemovedEventListener;
AppEventListener<FileMetadataUpdatedEvent> _fileMetadataUpdatedEventListener; AppEventListener<FilePropertyUpdatedEvent> _filePropertyUpdatedEventListener;
int _successiveMetadataUpdatedCount = 0; int _successivePropertyUpdatedCount = 0;
StreamSubscription<void> _metadataUpdatedSubscription; StreamSubscription<void> _propertyUpdatedSubscription;
bool _shouldCheckCache = true; bool _shouldCheckCache = true;

View file

@ -366,8 +366,16 @@ class FileRepo {
this.dataSrc.putBinary(account, path, content); this.dataSrc.putBinary(account, path, content);
/// See [FileDataSource.updateMetadata] /// See [FileDataSource.updateMetadata]
Future<void> updateMetadata(Account account, File file, Metadata metadata) => Future<void> updateProperty(
this.dataSrc.updateMetadata(account, file, metadata); Account account,
File file, {
OrNull<Metadata> metadata,
}) =>
this.dataSrc.updateProperty(
account,
file,
metadata: metadata,
);
/// See [FileDataSource.copy] /// See [FileDataSource.copy]
Future<void> copy( Future<void> copy(
@ -417,11 +425,12 @@ abstract class FileDataSource {
/// Upload content to [path] /// Upload content to [path]
Future<void> putBinary(Account account, String path, Uint8List content); Future<void> putBinary(Account account, String path, Uint8List content);
/// Update metadata for a file /// Update one or more properties of a file
/// Future<void> updateProperty(
/// This will completely replace the metadata of the file [f]. Partial update Account account,
/// is not supported File f, {
Future<void> updateMetadata(Account account, File f, Metadata metadata); OrNull<Metadata> metadata,
});
/// Copy [f] to [destination] /// Copy [f] to [destination]
/// ///

View file

@ -105,17 +105,22 @@ class FileWebdavDataSource implements FileDataSource {
} }
@override @override
updateMetadata(Account account, File f, Metadata metadata) async { updateProperty(
_log.info("[updateMetadata] ${f.path}"); Account account,
if (metadata != null && metadata.fileEtag != f.etag) { File f, {
OrNull<Metadata> metadata,
}) async {
_log.info("[updateProperty] ${f.path}");
if (metadata?.obj != null && metadata.obj.fileEtag != f.etag) {
_log.warning( _log.warning(
"[updateMetadata] etag mismatch (metadata: ${metadata.fileEtag}, file: ${f.etag})"); "[updateProperty] Metadata etag mismatch (metadata: ${metadata.obj.fileEtag}, file: ${f.etag})");
} }
final setProps = { final setProps = {
if (metadata != null) "app:metadata": jsonEncode(metadata.toJson()), if (metadata?.obj != null)
"app:metadata": jsonEncode(metadata.obj.toJson()),
}; };
final removeProps = [ final removeProps = [
if (metadata == null) "app:metadata", if (OrNull.isNull(metadata)) "app:metadata",
]; ];
final response = await Api(account).files().proppatch( final response = await Api(account).files().proppatch(
path: f.path, path: f.path,
@ -126,7 +131,7 @@ class FileWebdavDataSource implements FileDataSource {
remove: removeProps.isNotEmpty ? removeProps : null, remove: removeProps.isNotEmpty ? removeProps : null,
); );
if (!response.isGood) { if (!response.isGood) {
_log.severe("[updateMetadata] Failed requesting server: $response"); _log.severe("[updateProperty] Failed requesting server: $response");
throw ApiException( throw ApiException(
response: response, response: response,
message: "Failed communicating with server: ${response.statusCode}"); message: "Failed communicating with server: ${response.statusCode}");
@ -236,8 +241,12 @@ class FileAppDbDataSource implements FileDataSource {
} }
@override @override
updateMetadata(Account account, File f, Metadata metadata) { updateProperty(
_log.info("[updateMetadata] ${f.path}"); Account account,
File f, {
OrNull<Metadata> metadata,
}) {
_log.info("[updateProperty] ${f.path}");
return AppDb.use((db) async { return AppDb.use((db) async {
final transaction = db.transaction(AppDb.fileStoreName, idbModeReadWrite); final transaction = db.transaction(AppDb.fileStoreName, idbModeReadWrite);
final store = transaction.objectStore(AppDb.fileStoreName); final store = transaction.objectStore(AppDb.fileStoreName);
@ -245,7 +254,9 @@ class FileAppDbDataSource implements FileDataSource {
final parentList = await _doList(store, account, parentDir); final parentList = await _doList(store, account, parentDir);
final jsonList = parentList.map((e) { final jsonList = parentList.map((e) {
if (e.path == f.path) { if (e.path == f.path) {
return e.copyWith(metadata: OrNull(metadata)); return e.copyWith(
metadata: metadata,
);
} else { } else {
return e; return e;
} }
@ -369,10 +380,22 @@ class FileCachedDataSource implements FileDataSource {
} }
@override @override
updateMetadata(Account account, File f, Metadata metadata) async { updateProperty(
Account account,
File f, {
OrNull<Metadata> metadata,
}) async {
await _remoteSrc await _remoteSrc
.updateMetadata(account, f, metadata) .updateProperty(
.then((_) => _appDbSrc.updateMetadata(account, f, metadata)); account,
f,
metadata: metadata,
)
.then((_) => _appDbSrc.updateProperty(
account,
f,
metadata: metadata,
));
// generate a new random token // generate a new random token
final token = Uuid().v4().replaceAll("-", ""); final token = Uuid().v4().replaceAll("-", "");

View file

@ -48,11 +48,15 @@ class AlbumUpdatedEvent {
final Album album; final Album album;
} }
class FileMetadataUpdatedEvent { class FilePropertyUpdatedEvent {
FileMetadataUpdatedEvent(this.account, this.file); FilePropertyUpdatedEvent(this.account, this.file, this.properties);
final Account account; final Account account;
final File file; final File file;
final int properties;
// Bit masks for properties field
static const propMetadata = 0x01;
} }
class FileRemovedEvent { class FileRemovedEvent {
@ -63,3 +67,8 @@ class FileRemovedEvent {
} }
class ThemeChangedEvent {} class ThemeChangedEvent {}
extension FilePropertyUpdatedEventExtension on FilePropertyUpdatedEvent {
bool hasAnyProperties(List<int> properties) =>
properties.any((p) => this.properties & p != 0);
}

View file

@ -2,5 +2,7 @@
class OrNull<T> { class OrNull<T> {
OrNull(this.obj); OrNull(this.obj);
static bool isNull(OrNull x) => x != null && x.obj == null;
final T obj; final T obj;
} }

View file

@ -7,8 +7,9 @@ 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/mobile/platform.dart' import 'package:nc_photos/mobile/platform.dart'
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; 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/scan_missing_metadata.dart';
import 'package:nc_photos/use_case/update_metadata.dart'; import 'package:nc_photos/use_case/update_property.dart';
class UpdateMissingMetadata { class UpdateMissingMetadata {
UpdateMissingMetadata(this.fileRepo); UpdateMissingMetadata(this.fileRepo);
@ -51,8 +52,13 @@ class UpdateMissingMetadata {
exif: exif, exif: exif,
); );
await UpdateMetadata(FileRepo(FileCachedDataSource()), final updateOp = UpdateProperty(FileRepo(FileCachedDataSource()),
AlbumRepo(AlbumCachedDataSource()))(account, file, metadataObj); AlbumRepo(AlbumCachedDataSource()));
await updateOp(
account,
file,
metadata: OrNull(metadataObj),
);
yield file; yield file;
} catch (e, stacktrace) { } catch (e, stacktrace) {
_log.shout( _log.shout(

View file

@ -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/list_album.dart';
import 'package:nc_photos/use_case/update_album.dart'; import 'package:nc_photos/use_case/update_album.dart';
class UpdateMetadata { class UpdateProperty {
UpdateMetadata(this.fileRepo, this.albumRepo); UpdateProperty(this.fileRepo, this.albumRepo);
Future<void> call(Account account, File file, Metadata metadata) async { Future<void> call(
if (metadata != null && metadata.fileEtag != file.etag) { Account account,
_log.warning( File file, {
"[call] Metadata fileEtag mismatch with actual file's (metadata: ${metadata.fileEtag}, file: ${file.etag})"); 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() KiwiContainer()
.resolve<EventBus>() .resolve<EventBus>()
.fire(FileMetadataUpdatedEvent(account, file)); .fire(FilePropertyUpdatedEvent(account, file, properties));
} }
Future<void> _cleanUpAlbums( 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); final albums = await ListAlbum(fileRepo, albumRepo)(account);
for (final a in albums) { for (final a in albums) {
try { try {
@ -34,7 +61,9 @@ class UpdateMetadata {
final newItems = a.items.map((e) { final newItems = a.items.map((e) {
if (e is AlbumFileItem && e.file.path == file.path) { if (e is AlbumFileItem && e.file.path == file.path) {
return AlbumFileItem( return AlbumFileItem(
file: e.file.copyWith(metadata: OrNull(metadata)), file: e.file.copyWith(
metadata: metadata,
),
); );
} else { } else {
return e; return e;
@ -53,5 +82,13 @@ class UpdateMetadata {
final FileRepo fileRepo; final FileRepo fileRepo;
final AlbumRepo albumRepo; 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));
} }