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> {
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;

View file

@ -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;

View file

@ -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]
///

View file

@ -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("-", "");

View file

@ -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);
}

View file

@ -2,5 +2,7 @@
class OrNull<T> {
OrNull(this.obj);
static bool isNull(OrNull x) => x != null && x.obj == null;
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/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(

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/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));
}