mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
Treat metadata as property
This commit is contained in:
parent
ed9d5de275
commit
a846a51332
8 changed files with 153 additions and 57 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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]
|
||||
///
|
||||
|
|
|
@ -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("-", "");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -2,5 +2,7 @@
|
|||
class OrNull<T> {
|
||||
OrNull(this.obj);
|
||||
|
||||
static bool isNull(OrNull x) => x != null && x.obj == null;
|
||||
|
||||
final T obj;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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));
|
||||
}
|
Loading…
Reference in a new issue