Fix AppDb.use returns before db is closed

This commit is contained in:
Ming Ming 2022-03-24 17:03:13 +08:00
parent 5554c32781
commit 0c23b8e7e4
16 changed files with 541 additions and 477 deletions

View file

@ -34,13 +34,19 @@ class AppDb {
/// This function guarantees that: /// This function guarantees that:
/// 1) Database is always closed after [fn] exits, even with an error /// 1) Database is always closed after [fn] exits, even with an error
/// 2) Only at most 1 database instance being opened at any time /// 2) Only at most 1 database instance being opened at any time
Future<T> use<T>(FutureOr<T> Function(Database db) fn) async { Future<T> use<T>(Transaction Function(Database db) transactionBuilder,
FutureOr<T> Function(Transaction transaction) fn) async {
// make sure only one client is opening the db // make sure only one client is opening the db
return await platform.Lock.synchronized(k.appDbLockId, () async { return await platform.Lock.synchronized(k.appDbLockId, () async {
final db = await _open(); final db = await _open();
Transaction? transaction;
try { try {
return await fn(db); transaction = transactionBuilder(db);
return await fn(transaction);
} finally { } finally {
if (transaction != null) {
await transaction.completed;
}
db.close(); db.close();
} }
}); });

View file

@ -398,34 +398,36 @@ class AlbumAppDbDataSource implements AlbumDataSource {
@override @override
get(Account account, File albumFile) { get(Account account, File albumFile) {
_log.info("[get] ${albumFile.path}"); _log.info("[get] ${albumFile.path}");
return appDb.use((db) async { return appDb.use(
final transaction = db.transaction(AppDb.albumStoreName, idbModeReadOnly); (db) => db.transaction(AppDb.albumStoreName, idbModeReadOnly),
final store = transaction.objectStore(AppDb.albumStoreName); (transaction) async {
final index = store.index(AppDbAlbumEntry.indexName); final store = transaction.objectStore(AppDb.albumStoreName);
final path = AppDbAlbumEntry.toPathFromFile(account, albumFile); final index = store.index(AppDbAlbumEntry.indexName);
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]); final path = AppDbAlbumEntry.toPathFromFile(account, albumFile);
final List results = await index.getAll(range); final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
if (results.isNotEmpty == true) { final List results = await index.getAll(range);
final entries = results.map((e) => if (results.isNotEmpty == true) {
AppDbAlbumEntry.fromJson(e.cast<String, dynamic>(), account)); final entries = results.map((e) =>
if (entries.length > 1) { AppDbAlbumEntry.fromJson(e.cast<String, dynamic>(), account));
final items = entries.map((e) { if (entries.length > 1) {
_log.info("[get] ${e.path}[${e.index}]"); final items = entries.map((e) {
return AlbumStaticProvider.of(e.album).items; _log.info("[get] ${e.path}[${e.index}]");
}).reduce((value, element) => value + element); return AlbumStaticProvider.of(e.album).items;
return entries.first.album.copyWith( }).reduce((value, element) => value + element);
lastUpdated: OrNull(null), return entries.first.album.copyWith(
provider: AlbumStaticProvider.of(entries.first.album).copyWith( lastUpdated: OrNull(null),
items: items, provider: AlbumStaticProvider.of(entries.first.album).copyWith(
), items: items,
); ),
);
} else {
return entries.first.album;
}
} else { } else {
return entries.first.album; throw CacheNotFoundException("No entry: $path");
} }
} else { },
throw CacheNotFoundException("No entry: $path"); );
}
});
} }
@override @override
@ -437,12 +439,13 @@ class AlbumAppDbDataSource implements AlbumDataSource {
@override @override
update(Account account, Album album) { update(Account account, Album album) {
_log.info("[update] ${album.albumFile!.path}"); _log.info("[update] ${album.albumFile!.path}");
return appDb.use((db) async { return appDb.use(
final transaction = (db) => db.transaction(AppDb.albumStoreName, idbModeReadWrite),
db.transaction(AppDb.albumStoreName, idbModeReadWrite); (transaction) async {
final store = transaction.objectStore(AppDb.albumStoreName); final store = transaction.objectStore(AppDb.albumStoreName);
await _cacheAlbum(store, account, album); await _cacheAlbum(store, account, album);
}); },
);
} }
@override @override
@ -492,43 +495,45 @@ class AlbumCachedDataSource implements AlbumDataSource {
@override @override
cleanUp(Account account, String rootDir, List<File> albumFiles) async { cleanUp(Account account, String rootDir, List<File> albumFiles) async {
appDb.use((db) async { appDb.use(
final transaction = (db) => db.transaction(AppDb.albumStoreName, idbModeReadWrite),
db.transaction(AppDb.albumStoreName, idbModeReadWrite); (transaction) async {
final store = transaction.objectStore(AppDb.albumStoreName); final store = transaction.objectStore(AppDb.albumStoreName);
final index = store.index(AppDbAlbumEntry.indexName); final index = store.index(AppDbAlbumEntry.indexName);
final rootPath = AppDbAlbumEntry.toPath(account, rootDir); final rootPath = AppDbAlbumEntry.toPath(account, rootDir);
final range = KeyRange.bound( final range = KeyRange.bound(
["$rootPath/", 0], ["$rootPath/\uffff", int_util.int32Max]); ["$rootPath/", 0], ["$rootPath/\uffff", int_util.int32Max]);
final danglingKeys = await index final danglingKeys = await index
// get all albums for this account // get all albums for this account
.openKeyCursor(range: range, autoAdvance: true) .openKeyCursor(range: range, autoAdvance: true)
.map((cursor) => Tuple2((cursor.key as List)[0], cursor.primaryKey)) .map((cursor) => Tuple2((cursor.key as List)[0], cursor.primaryKey))
// and pick the dangling ones // and pick the dangling ones
.where((pair) => !albumFiles.any( .where((pair) => !albumFiles.any((f) =>
(f) => pair.item1 == AppDbAlbumEntry.toPathFromFile(account, f))) pair.item1 == AppDbAlbumEntry.toPathFromFile(account, f)))
// map to primary keys // map to primary keys
.map((pair) => pair.item2) .map((pair) => pair.item2)
.toList(); .toList();
for (final k in danglingKeys) { for (final k in danglingKeys) {
_log.fine("[cleanUp] Removing albumStore entry: $k"); _log.fine("[cleanUp] Removing albumStore entry: $k");
try { try {
await store.delete(k); await store.delete(k);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.shout( _log.shout(
"[cleanUp] Failed removing albumStore entry", e, stackTrace); "[cleanUp] Failed removing albumStore entry", e, stackTrace);
}
} }
} },
}); );
} }
Future<void> _cacheResult(Account account, Album result) { Future<void> _cacheResult(Account account, Album result) {
return appDb.use((db) async { return appDb.use(
final transaction = (db) => db.transaction(AppDb.albumStoreName, idbModeReadWrite),
db.transaction(AppDb.albumStoreName, idbModeReadWrite); (transaction) async {
final store = transaction.objectStore(AppDb.albumStoreName); final store = transaction.objectStore(AppDb.albumStoreName);
await _cacheAlbum(store, account, result); await _cacheAlbum(store, account, result);
}); },
);
} }
final AppDb appDb; final AppDb appDb;

View file

@ -269,31 +269,34 @@ class FileAppDbDataSource implements FileDataSource {
@override @override
list(Account account, File dir) { list(Account account, File dir) {
_log.info("[list] ${dir.path}"); _log.info("[list] ${dir.path}");
return appDb.use((db) async { return appDb.use(
final transaction = db.transaction( (db) => db.transaction(
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadOnly); [AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadOnly),
final fileStore = transaction.objectStore(AppDb.file2StoreName); (transaction) async {
final dirStore = transaction.objectStore(AppDb.dirStoreName); final fileStore = transaction.objectStore(AppDb.file2StoreName);
final dirItem = await dirStore final dirStore = transaction.objectStore(AppDb.dirStoreName);
.getObject(AppDbDirEntry.toPrimaryKeyForDir(account, dir)) as Map?; final dirItem = await dirStore
if (dirItem == null) { .getObject(AppDbDirEntry.toPrimaryKeyForDir(account, dir)) as Map?;
throw CacheNotFoundException("No entry: ${dir.path}"); if (dirItem == null) {
} throw CacheNotFoundException("No entry: ${dir.path}");
final dirEntry = AppDbDirEntry.fromJson(dirItem.cast<String, dynamic>());
final entries = await Future.wait(dirEntry.children.map((c) async {
final fileItem = await fileStore
.getObject(AppDbFile2Entry.toPrimaryKey(account, c)) as Map?;
if (fileItem == null) {
_log.warning(
"[list] Missing file ($c) in db for dir: ${logFilename(dir.path)}");
throw CacheNotFoundException("No entry for dir child: $c");
} }
return AppDbFile2Entry.fromJson(fileItem.cast<String, dynamic>()); final dirEntry =
})); AppDbDirEntry.fromJson(dirItem.cast<String, dynamic>());
// we need to add dir to match the remote query final entries = await Future.wait(dirEntry.children.map((c) async {
return [dirEntry.dir] + final fileItem = await fileStore
entries.map((e) => e.file).where((f) => _validateFile(f)).toList(); .getObject(AppDbFile2Entry.toPrimaryKey(account, c)) as Map?;
}); if (fileItem == null) {
_log.warning(
"[list] Missing file ($c) in db for dir: ${logFilename(dir.path)}");
throw CacheNotFoundException("No entry for dir child: $c");
}
return AppDbFile2Entry.fromJson(fileItem.cast<String, dynamic>());
}));
// we need to add dir to match the remote query
return [dirEntry.dir] +
entries.map((e) => e.file).where((f) => _validateFile(f)).toList();
},
);
} }
@override @override
@ -307,19 +310,21 @@ class FileAppDbDataSource implements FileDataSource {
Future<List<File>> listByDate( Future<List<File>> listByDate(
Account account, int fromEpochMs, int toEpochMs) async { Account account, int fromEpochMs, int toEpochMs) async {
_log.info("[listByDate] [$fromEpochMs, $toEpochMs]"); _log.info("[listByDate] [$fromEpochMs, $toEpochMs]");
final items = await appDb.use((db) async { final items = await appDb.use(
final transaction = db.transaction(AppDb.file2StoreName, idbModeReadOnly); (db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
final fileStore = transaction.objectStore(AppDb.file2StoreName); (transaction) async {
final dateTimeEpochMsIndex = final fileStore = transaction.objectStore(AppDb.file2StoreName);
fileStore.index(AppDbFile2Entry.dateTimeEpochMsIndexName); final dateTimeEpochMsIndex =
final range = KeyRange.bound( fileStore.index(AppDbFile2Entry.dateTimeEpochMsIndexName);
AppDbFile2Entry.toDateTimeEpochMsIndexKey(account, fromEpochMs), final range = KeyRange.bound(
AppDbFile2Entry.toDateTimeEpochMsIndexKey(account, toEpochMs), AppDbFile2Entry.toDateTimeEpochMsIndexKey(account, fromEpochMs),
false, AppDbFile2Entry.toDateTimeEpochMsIndexKey(account, toEpochMs),
true, false,
); true,
return await dateTimeEpochMsIndex.getAll(range); );
}); return await dateTimeEpochMsIndex.getAll(range);
},
);
return items return items
.cast<Map>() .cast<Map>()
.map((i) => AppDbFile2Entry.fromJson(i.cast<String, dynamic>())) .map((i) => AppDbFile2Entry.fromJson(i.cast<String, dynamic>()))
@ -357,21 +362,21 @@ class FileAppDbDataSource implements FileDataSource {
bool? favorite, bool? favorite,
}) { }) {
_log.info("[updateProperty] ${f.path}"); _log.info("[updateProperty] ${f.path}");
return appDb.use((db) async { return appDb.use(
final transaction = (db) => db.transaction(AppDb.file2StoreName, idbModeReadWrite),
db.transaction(AppDb.file2StoreName, idbModeReadWrite); (transaction) async {
// update file store
// update file store final newFile = f.copyWith(
final newFile = f.copyWith( metadata: metadata,
metadata: metadata, isArchived: isArchived,
isArchived: isArchived, overrideDateTime: overrideDateTime,
overrideDateTime: overrideDateTime, isFavorite: favorite,
isFavorite: favorite, );
); final fileStore = transaction.objectStore(AppDb.file2StoreName);
final fileStore = transaction.objectStore(AppDb.file2StoreName); await fileStore.put(AppDbFile2Entry.fromFile(account, newFile).toJson(),
await fileStore.put(AppDbFile2Entry.fromFile(account, newFile).toJson(), AppDbFile2Entry.toPrimaryKeyForFile(account, newFile));
AppDbFile2Entry.toPrimaryKeyForFile(account, newFile)); },
}); );
} }
@override @override
@ -577,20 +582,22 @@ class FileForwardCacheManager {
} }
Future<void> _cacheDir(Account account, File dir) async { Future<void> _cacheDir(Account account, File dir) async {
final dirItems = await appDb.use((db) async { final dirItems = await appDb.use(
final transaction = db.transaction(AppDb.dirStoreName, idbModeReadOnly); (db) => db.transaction(AppDb.dirStoreName, idbModeReadOnly),
final store = transaction.objectStore(AppDb.dirStoreName); (transaction) async {
final dirItem = await store final store = transaction.objectStore(AppDb.dirStoreName);
.getObject(AppDbDirEntry.toPrimaryKeyForDir(account, dir)) as Map?; final dirItem = await store
if (dirItem == null) { .getObject(AppDbDirEntry.toPrimaryKeyForDir(account, dir)) as Map?;
return null; if (dirItem == null) {
} return null;
final range = KeyRange.bound( }
AppDbDirEntry.toPrimaryLowerKeyForSubDirs(account, dir), final range = KeyRange.bound(
AppDbDirEntry.toPrimaryUpperKeyForSubDirs(account, dir), AppDbDirEntry.toPrimaryLowerKeyForSubDirs(account, dir),
); AppDbDirEntry.toPrimaryUpperKeyForSubDirs(account, dir),
return [dirItem] + (await store.getAll(range)).cast<Map>(); );
}); return [dirItem] + (await store.getAll(range)).cast<Map>();
},
);
if (dirItems == null) { if (dirItems == null) {
// no cache // no cache
return; return;
@ -606,12 +613,14 @@ class FileForwardCacheManager {
// cache files // cache files
final fileIds = dirs.map((e) => e.children).fold<List<int>>( final fileIds = dirs.map((e) => e.children).fold<List<int>>(
[], (previousValue, element) => previousValue + element); [], (previousValue, element) => previousValue + element);
final fileItems = await appDb.use((db) async { final fileItems = await appDb.use(
final transaction = db.transaction(AppDb.file2StoreName, idbModeReadOnly); (db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
final store = transaction.objectStore(AppDb.file2StoreName); (transaction) async {
return await Future.wait(fileIds.map( final store = transaction.objectStore(AppDb.file2StoreName);
(id) => store.getObject(AppDbFile2Entry.toPrimaryKey(account, id)))); return await Future.wait(fileIds.map((id) =>
}); store.getObject(AppDbFile2Entry.toPrimaryKey(account, id))));
},
);
final files = fileItems final files = fileItems
.cast<Map?>() .cast<Map?>()
.whereType<Map>() .whereType<Map>()

View file

@ -124,27 +124,30 @@ class FileCacheUpdater {
} }
Future<void> _cacheRemote(Account account, File dir, List<File> remote) { Future<void> _cacheRemote(Account account, File dir, List<File> remote) {
return appDb.use((db) async { return appDb.use(
final transaction = db.transaction( (db) => db.transaction(
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite); [AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite),
final dirStore = transaction.objectStore(AppDb.dirStoreName); (transaction) async {
final fileStore = transaction.objectStore(AppDb.file2StoreName); final dirStore = transaction.objectStore(AppDb.dirStoreName);
final fileStore = transaction.objectStore(AppDb.file2StoreName);
// add files to db // add files to db
await Future.wait(remote.map((f) => fileStore.put( await Future.wait(remote.map((f) => fileStore.put(
AppDbFile2Entry.fromFile(account, f).toJson(), AppDbFile2Entry.fromFile(account, f).toJson(),
AppDbFile2Entry.toPrimaryKeyForFile(account, f)))); AppDbFile2Entry.toPrimaryKeyForFile(account, f))));
// results from remote also contain the dir itself // results from remote also contain the dir itself
final resultGroup = final resultGroup =
remote.groupListsBy((f) => f.compareServerIdentity(dir)); remote.groupListsBy((f) => f.compareServerIdentity(dir));
final remoteDir = resultGroup[true]!.first; final remoteDir = resultGroup[true]!.first;
final remoteChildren = resultGroup[false] ?? []; final remoteChildren = resultGroup[false] ?? [];
// add dir to db // add dir to db
await dirStore.put( await dirStore.put(
AppDbDirEntry.fromFiles(account, remoteDir, remoteChildren).toJson(), AppDbDirEntry.fromFiles(account, remoteDir, remoteChildren)
AppDbDirEntry.toPrimaryKeyForDir(account, remoteDir)); .toJson(),
}); AppDbDirEntry.toPrimaryKeyForDir(account, remoteDir));
},
);
} }
/// Remove extra entries from local cache based on remote contents /// Remove extra entries from local cache based on remote contents
@ -159,27 +162,29 @@ class FileCacheUpdater {
_log.info( _log.info(
"[_cleanUpCache] Removed: ${removed.map((f) => f.path).toReadableString()}"); "[_cleanUpCache] Removed: ${removed.map((f) => f.path).toReadableString()}");
await appDb.use((db) async { await appDb.use(
final transaction = db.transaction( (db) => db.transaction(
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite); [AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite),
final dirStore = transaction.objectStore(AppDb.dirStoreName); (transaction) async {
final fileStore = transaction.objectStore(AppDb.file2StoreName); final dirStore = transaction.objectStore(AppDb.dirStoreName);
for (final f in removed) { final fileStore = transaction.objectStore(AppDb.file2StoreName);
try { for (final f in removed) {
if (f.isCollection == true) { try {
await _removeDirFromAppDb(account, f, if (f.isCollection == true) {
dirStore: dirStore, fileStore: fileStore); await _removeDirFromAppDb(account, f,
} else { dirStore: dirStore, fileStore: fileStore);
await _removeFileFromAppDb(account, f, fileStore: fileStore); } else {
await _removeFileFromAppDb(account, f, fileStore: fileStore);
}
} catch (e, stackTrace) {
_log.shout(
"[_cleanUpCache] Failed while removing file: ${logFilename(f.path)}",
e,
stackTrace);
} }
} catch (e, stackTrace) {
_log.shout(
"[_cleanUpCache] Failed while removing file: ${logFilename(f.path)}",
e,
stackTrace);
} }
} },
}); );
} }
final AppDb appDb; final AppDb appDb;
@ -198,23 +203,28 @@ class FileCacheRemover {
/// If [f] is a file, the file will be removed from file2Store, but no changes /// If [f] is a file, the file will be removed from file2Store, but no changes
/// to dirStore. /// to dirStore.
Future<void> call(Account account, File f) async { Future<void> call(Account account, File f) async {
await appDb.use((db) async { if (f.isCollection != false) {
if (f.isCollection != false) { // removing dir is basically a superset of removing file, so we'll treat
// removing dir is basically a superset of removing file, so we'll treat // unspecified file as dir
// unspecified file as dir await appDb.use(
final transaction = db.transaction( (db) => db.transaction(
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite); [AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite),
final dirStore = transaction.objectStore(AppDb.dirStoreName); (transaction) async {
final fileStore = transaction.objectStore(AppDb.file2StoreName); final dirStore = transaction.objectStore(AppDb.dirStoreName);
await _removeDirFromAppDb(account, f, final fileStore = transaction.objectStore(AppDb.file2StoreName);
dirStore: dirStore, fileStore: fileStore); await _removeDirFromAppDb(account, f,
} else { dirStore: dirStore, fileStore: fileStore);
final transaction = },
db.transaction(AppDb.file2StoreName, idbModeReadWrite); );
final fileStore = transaction.objectStore(AppDb.file2StoreName); } else {
await _removeFileFromAppDb(account, f, fileStore: fileStore); await appDb.use(
} (db) => db.transaction(AppDb.file2StoreName, idbModeReadWrite),
}); (transaction) async {
final fileStore = transaction.objectStore(AppDb.file2StoreName);
await _removeFileFromAppDb(account, f, fileStore: fileStore);
},
);
}
} }
final AppDb appDb; final AppDb appDb;

View file

@ -37,35 +37,36 @@ class CacheFavorite {
if (newFavorites.isEmpty && removedFavorites.isEmpty) { if (newFavorites.isEmpty && removedFavorites.isEmpty) {
return; return;
} }
await _c.appDb.use((db) async { await _c.appDb.use(
final transaction = (db) => db.transaction(AppDb.file2StoreName, idbModeReadWrite),
db.transaction(AppDb.file2StoreName, idbModeReadWrite); (transaction) async {
final fileStore = transaction.objectStore(AppDb.file2StoreName); final fileStore = transaction.objectStore(AppDb.file2StoreName);
await Future.wait(newFavorites.map((f) async { await Future.wait(newFavorites.map((f) async {
_log.info("[call] New favorite: ${f.path}"); _log.info("[call] New favorite: ${f.path}");
try { try {
await fileStore.put(AppDbFile2Entry.fromFile(account, f).toJson(), await fileStore.put(AppDbFile2Entry.fromFile(account, f).toJson(),
AppDbFile2Entry.toPrimaryKeyForFile(account, f)); AppDbFile2Entry.toPrimaryKeyForFile(account, f));
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.shout( _log.shout(
"[call] Failed while writing new favorite to AppDb: ${logFilename(f.path)}", "[call] Failed while writing new favorite to AppDb: ${logFilename(f.path)}",
e, e,
stackTrace); stackTrace);
} }
})); }));
await Future.wait(removedFavorites.map((f) async { await Future.wait(removedFavorites.map((f) async {
_log.info("[call] Remove favorite: ${f.path}"); _log.info("[call] Remove favorite: ${f.path}");
try { try {
await fileStore.put(AppDbFile2Entry.fromFile(account, f).toJson(), await fileStore.put(AppDbFile2Entry.fromFile(account, f).toJson(),
AppDbFile2Entry.toPrimaryKeyForFile(account, f)); AppDbFile2Entry.toPrimaryKeyForFile(account, f));
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.shout( _log.shout(
"[call] Failed while writing removed favorite to AppDb: ${logFilename(f.path)}", "[call] Failed while writing removed favorite to AppDb: ${logFilename(f.path)}",
e, e,
stackTrace); stackTrace);
} }
})); }));
}); },
);
KiwiContainer() KiwiContainer()
.resolve<EventBus>() .resolve<EventBus>()

View file

@ -12,14 +12,15 @@ class CompatV37 {
static Future<void> setAppDbMigrationFlag(AppDb appDb) async { static Future<void> setAppDbMigrationFlag(AppDb appDb) async {
_log.info("[setAppDbMigrationFlag] Set db flag"); _log.info("[setAppDbMigrationFlag] Set db flag");
try { try {
await appDb.use((db) async { await appDb.use(
final transaction = (db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
db.transaction(AppDb.metaStoreName, idbModeReadWrite); (transaction) async {
final metaStore = transaction.objectStore(AppDb.metaStoreName); final metaStore = transaction.objectStore(AppDb.metaStoreName);
await metaStore await metaStore
.put(const AppDbMetaEntryCompatV37(false).toEntry().toJson()); .put(const AppDbMetaEntryCompatV37(false).toEntry().toJson());
await transaction.completed; await transaction.completed;
}); },
);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.shout( _log.shout(
"[setAppDbMigrationFlag] Failed while setting db flag, drop db instead", "[setAppDbMigrationFlag] Failed while setting db flag, drop db instead",
@ -30,11 +31,13 @@ class CompatV37 {
} }
static Future<bool> isAppDbNeedMigration(AppDb appDb) async { static Future<bool> isAppDbNeedMigration(AppDb appDb) async {
final dbItem = await appDb.use((db) async { final dbItem = await appDb.use(
final transaction = db.transaction(AppDb.metaStoreName, idbModeReadOnly); (db) => db.transaction(AppDb.metaStoreName, idbModeReadOnly),
final metaStore = transaction.objectStore(AppDb.metaStoreName); (transaction) async {
return await metaStore.getObject(AppDbMetaEntryCompatV37.key) as Map?; final metaStore = transaction.objectStore(AppDb.metaStoreName);
}); return await metaStore.getObject(AppDbMetaEntryCompatV37.key) as Map?;
},
);
if (dbItem == null) { if (dbItem == null) {
return false; return false;
} }
@ -51,51 +54,53 @@ class CompatV37 {
static Future<void> migrateAppDb(AppDb appDb) async { static Future<void> migrateAppDb(AppDb appDb) async {
_log.info("[migrateAppDb] Migrate AppDb"); _log.info("[migrateAppDb] Migrate AppDb");
try { try {
await appDb.use((db) async { await appDb.use(
final transaction = db.transaction( (db) => db.transaction(
[AppDb.file2StoreName, AppDb.dirStoreName, AppDb.metaStoreName], [AppDb.file2StoreName, AppDb.dirStoreName, AppDb.metaStoreName],
idbModeReadWrite); idbModeReadWrite),
final noMediaFiles = <_NoMediaFile>[]; (transaction) async {
try { final noMediaFiles = <_NoMediaFile>[];
final fileStore = transaction.objectStore(AppDb.file2StoreName); try {
final dirStore = transaction.objectStore(AppDb.dirStoreName); final fileStore = transaction.objectStore(AppDb.file2StoreName);
// scan the db to see which dirs contain a no media marker final dirStore = transaction.objectStore(AppDb.dirStoreName);
await for (final c in fileStore.openCursor()) { // scan the db to see which dirs contain a no media marker
final item = c.value as Map; await for (final c in fileStore.openCursor()) {
final strippedPath = item["strippedPath"] as String; final item = c.value as Map;
if (file_util.isNoMediaMarkerPath(strippedPath)) { final strippedPath = item["strippedPath"] as String;
noMediaFiles.add(_NoMediaFile( if (file_util.isNoMediaMarkerPath(strippedPath)) {
item["server"], noMediaFiles.add(_NoMediaFile(
item["userId"], item["server"],
path_lib item["userId"],
.dirname(item["strippedPath"]) path_lib
.run((p) => p == "." ? "" : p), .dirname(item["strippedPath"])
item["file"]["fileId"], .run((p) => p == "." ? "" : p),
)); item["file"]["fileId"],
));
}
c.next();
} }
c.next(); // sort to make sure parent dirs are always in front of sub dirs
} noMediaFiles
// sort to make sure parent dirs are always in front of sub dirs .sort((a, b) => a.strippedDirPath.compareTo(b.strippedDirPath));
noMediaFiles _log.info(
.sort((a, b) => a.strippedDirPath.compareTo(b.strippedDirPath)); "[migrateAppDb] nomedia dirs: ${noMediaFiles.toReadableString()}");
_log.info(
"[migrateAppDb] nomedia dirs: ${noMediaFiles.toReadableString()}");
if (noMediaFiles.isNotEmpty) { if (noMediaFiles.isNotEmpty) {
await _migrateAppDbFileStore(appDb, noMediaFiles, await _migrateAppDbFileStore(appDb, noMediaFiles,
fileStore: fileStore); fileStore: fileStore);
await _migrateAppDbDirStore(appDb, noMediaFiles, await _migrateAppDbDirStore(appDb, noMediaFiles,
dirStore: dirStore); dirStore: dirStore);
} }
final metaStore = transaction.objectStore(AppDb.metaStoreName); final metaStore = transaction.objectStore(AppDb.metaStoreName);
await metaStore await metaStore
.put(const AppDbMetaEntryCompatV37(true).toEntry().toJson()); .put(const AppDbMetaEntryCompatV37(true).toEntry().toJson());
} catch (_) { } catch (_) {
transaction.abort(); transaction.abort();
rethrow; rethrow;
} }
}); },
);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.shout("[migrateAppDb] Failed while migrating, drop db instead", e, _log.shout("[migrateAppDb] Failed while migrating, drop db instead", e,
stackTrace); stackTrace);

View file

@ -7,11 +7,13 @@ import 'package:nc_photos/object_extension.dart';
class DbCompatV5 { class DbCompatV5 {
static Future<bool> isNeedMigration(AppDb appDb) async { static Future<bool> isNeedMigration(AppDb appDb) async {
final dbItem = await appDb.use((db) async { final dbItem = await appDb.use(
final transaction = db.transaction(AppDb.metaStoreName, idbModeReadOnly); (db) => db.transaction(AppDb.metaStoreName, idbModeReadOnly),
final metaStore = transaction.objectStore(AppDb.metaStoreName); (transaction) async {
return await metaStore.getObject(AppDbMetaEntryDbCompatV5.key) as Map?; final metaStore = transaction.objectStore(AppDb.metaStoreName);
}); return await metaStore.getObject(AppDbMetaEntryDbCompatV5.key) as Map?;
},
);
if (dbItem == null) { if (dbItem == null) {
return false; return false;
} }
@ -28,36 +30,38 @@ class DbCompatV5 {
static Future<void> migrate(AppDb appDb) async { static Future<void> migrate(AppDb appDb) async {
_log.info("[migrate] Migrate AppDb"); _log.info("[migrate] Migrate AppDb");
try { try {
await appDb.use((db) async { await appDb.use(
final transaction = db.transaction( (db) => db.transaction(
[AppDb.file2StoreName, AppDb.metaStoreName], idbModeReadWrite); [AppDb.file2StoreName, AppDb.metaStoreName], idbModeReadWrite),
try { (transaction) async {
final fileStore = transaction.objectStore(AppDb.file2StoreName); try {
await for (final c in fileStore.openCursor()) { final fileStore = transaction.objectStore(AppDb.file2StoreName);
final item = c.value as Map; await for (final c in fileStore.openCursor()) {
// migrate file entry: add bestDateTime final item = c.value as Map;
final fileEntry = item.cast<String, dynamic>().run((json) { // migrate file entry: add bestDateTime
final f = File.fromJson(json["file"].cast<String, dynamic>()); final fileEntry = item.cast<String, dynamic>().run((json) {
return AppDbFile2Entry( final f = File.fromJson(json["file"].cast<String, dynamic>());
json["server"], return AppDbFile2Entry(
(json["userId"] as String).toCi(), json["server"],
json["strippedPath"], (json["userId"] as String).toCi(),
f.bestDateTime.millisecondsSinceEpoch, json["strippedPath"],
File.fromJson(json["file"].cast<String, dynamic>()), f.bestDateTime.millisecondsSinceEpoch,
); File.fromJson(json["file"].cast<String, dynamic>()),
}); );
await c.update(fileEntry.toJson()); });
await c.update(fileEntry.toJson());
c.next(); c.next();
}
final metaStore = transaction.objectStore(AppDb.metaStoreName);
await metaStore
.put(const AppDbMetaEntryDbCompatV5(true).toEntry().toJson());
} catch (_) {
transaction.abort();
rethrow;
} }
final metaStore = transaction.objectStore(AppDb.metaStoreName); },
await metaStore );
.put(const AppDbMetaEntryDbCompatV5(true).toEntry().toJson());
} catch (_) {
transaction.abort();
rethrow;
}
});
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.shout( _log.shout(
"[migrate] Failed while migrating, drop db instead", e, stackTrace); "[migrate] Failed while migrating, drop db instead", e, stackTrace);

View file

@ -19,12 +19,14 @@ class FindFile {
List<int> fileIds, { List<int> fileIds, {
void Function(int fileId)? onFileNotFound, void Function(int fileId)? onFileNotFound,
}) async { }) async {
final dbItems = await _c.appDb.use((db) async { final dbItems = await _c.appDb.use(
final transaction = db.transaction(AppDb.file2StoreName, idbModeReadOnly); (db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
final fileStore = transaction.objectStore(AppDb.file2StoreName); (transaction) async {
return await Future.wait(fileIds.map((id) => final fileStore = transaction.objectStore(AppDb.file2StoreName);
fileStore.getObject(AppDbFile2Entry.toPrimaryKey(account, id)))); return await Future.wait(fileIds.map((id) =>
}); fileStore.getObject(AppDbFile2Entry.toPrimaryKey(account, id))));
},
);
final files = <File>[]; final files = <File>[];
for (final pair in zip([fileIds, dbItems])) { for (final pair in zip([fileIds, dbItems])) {
final dbItem = pair[1] as Map?; final dbItem = pair[1] as Map?;

View file

@ -18,24 +18,26 @@ class ListFavoriteOffline {
final rootDirs = account.roots final rootDirs = account.roots
.map((r) => File(path: file_util.unstripPath(account, r))) .map((r) => File(path: file_util.unstripPath(account, r)))
.toList(); .toList();
return _c.appDb.use((db) async { return _c.appDb.use(
final transaction = db.transaction(AppDb.file2StoreName, idbModeReadOnly); (db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
final fileStore = transaction.objectStore(AppDb.file2StoreName); (transaction) async {
final fileIsFavoriteIndex = final fileStore = transaction.objectStore(AppDb.file2StoreName);
fileStore.index(AppDbFile2Entry.fileIsFavoriteIndexName); final fileIsFavoriteIndex =
return await fileIsFavoriteIndex fileStore.index(AppDbFile2Entry.fileIsFavoriteIndexName);
.openCursor( return await fileIsFavoriteIndex
key: AppDbFile2Entry.toFileIsFavoriteIndexKey(account, true), .openCursor(
autoAdvance: true, key: AppDbFile2Entry.toFileIsFavoriteIndexKey(account, true),
) autoAdvance: true,
.map((c) => AppDbFile2Entry.fromJson( )
(c.value as Map).cast<String, dynamic>())) .map((c) => AppDbFile2Entry.fromJson(
.map((e) => e.file) (c.value as Map).cast<String, dynamic>()))
.where((f) => .map((e) => e.file)
file_util.isSupportedFormat(f) && .where((f) =>
rootDirs.any((r) => file_util.isOrUnderDir(f, r))) file_util.isSupportedFormat(f) &&
.toList(); rootDirs.any((r) => file_util.isOrUnderDir(f, r)))
}); .toList();
},
);
} }
final DiContainer _c; final DiContainer _c;

View file

@ -11,20 +11,22 @@ class PopulatePerson {
/// Return a list of files of the faces /// Return a list of files of the faces
Future<List<File>> call(Account account, List<Face> faces) async { Future<List<File>> call(Account account, List<Face> faces) async {
return await appDb.use((db) async { return await appDb.use(
final transaction = db.transaction(AppDb.file2StoreName, idbModeReadOnly); (db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
final store = transaction.objectStore(AppDb.file2StoreName); (transaction) async {
final products = <File>[]; final store = transaction.objectStore(AppDb.file2StoreName);
for (final f in faces) { final products = <File>[];
try { for (final f in faces) {
products.add(await _populateOne(account, f, fileStore: store)); try {
} catch (e, stackTrace) { products.add(await _populateOne(account, f, fileStore: store));
_log.severe("[call] Failed populating file of face: ${f.fileId}", e, } catch (e, stackTrace) {
stackTrace); _log.severe("[call] Failed populating file of face: ${f.fileId}", e,
stackTrace);
}
} }
} return products;
return products; },
}); );
} }
Future<File> _populateOne( Future<File> _populateOne(

View file

@ -20,17 +20,19 @@ class ResyncAlbum {
final items = AlbumStaticProvider.of(album).items; final items = AlbumStaticProvider.of(album).items;
final fileIds = final fileIds =
items.whereType<AlbumFileItem>().map((i) => i.file.fileId!).toList(); items.whereType<AlbumFileItem>().map((i) => i.file.fileId!).toList();
final dbItems = Map.fromEntries(await appDb.use((db) async { final dbItems = Map.fromEntries(await appDb.use(
final transaction = db.transaction(AppDb.file2StoreName, idbModeReadOnly); (db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
final store = transaction.objectStore(AppDb.file2StoreName); (transaction) async {
return await Future.wait(fileIds.map( final store = transaction.objectStore(AppDb.file2StoreName);
(id) async => MapEntry( return await Future.wait(fileIds.map(
id, (id) async => MapEntry(
await store.getObject(AppDbFile2Entry.toPrimaryKey(account, id)) id,
as Map?, await store.getObject(AppDbFile2Entry.toPrimaryKey(account, id))
), as Map?,
)); ),
})); ));
},
));
return items.map((i) { return items.map((i) {
if (i is AlbumFileItem) { if (i is AlbumFileItem) {
try { try {

View file

@ -12,22 +12,25 @@ class ScanDirOffline {
/// List all files under a dir recursively from the local DB /// List all files under a dir recursively from the local DB
Future<List<File>> call(Account account, File root) async { Future<List<File>> call(Account account, File root) async {
return await _c.appDb.use((db) async { return await _c.appDb.use(
final transaction = db.transaction(AppDb.file2StoreName, idbModeReadOnly); (db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
final store = transaction.objectStore(AppDb.file2StoreName); (transaction) async {
final index = store.index(AppDbFile2Entry.strippedPathIndexName); final store = transaction.objectStore(AppDb.file2StoreName);
final range = KeyRange.bound( final index = store.index(AppDbFile2Entry.strippedPathIndexName);
AppDbFile2Entry.toStrippedPathIndexLowerKeyForDir(account, root), final range = KeyRange.bound(
AppDbFile2Entry.toStrippedPathIndexUpperKeyForDir(account, root), AppDbFile2Entry.toStrippedPathIndexLowerKeyForDir(account, root),
); AppDbFile2Entry.toStrippedPathIndexUpperKeyForDir(account, root),
return await index );
.openCursor(range: range, autoAdvance: true) return await index
.map((c) => c.value) .openCursor(range: range, autoAdvance: true)
.cast<Map>() .map((c) => c.value)
.map((e) => AppDbFile2Entry.fromJson(e.cast<String, dynamic>()).file) .cast<Map>()
.where((f) => file_util.isSupportedFormat(f)) .map(
.toList(); (e) => AppDbFile2Entry.fromJson(e.cast<String, dynamic>()).file)
}); .where((f) => file_util.isSupportedFormat(f))
.toList();
},
);
} }
final DiContainer _c; final DiContainer _c;

View file

@ -100,7 +100,8 @@ class MockAppDb implements AppDb {
} }
@override @override
Future<T> use<T>(FutureOr<T> Function(Database db) fn) async { Future<T> use<T>(Transaction Function(Database db) transactionBuilder,
FutureOr<T> Function(Transaction transaction) fn) async {
final db = await _dbFactory.open( final db = await _dbFactory.open(
"test.db", "test.db",
version: 1, version: 1,
@ -110,9 +111,14 @@ class MockAppDb implements AppDb {
}, },
); );
Transaction? transaction;
try { try {
return await fn(db); transaction = transactionBuilder(db);
return await fn(transaction);
} finally { } finally {
if (transaction != null) {
await transaction.completed;
}
db.close(); db.close();
} }
} }

View file

@ -341,36 +341,43 @@ Sharee buildSharee({
Future<void> fillAppDb( Future<void> fillAppDb(
AppDb appDb, Account account, Iterable<File> files) async { AppDb appDb, Account account, Iterable<File> files) async {
await appDb.use((db) async { await appDb.use(
final transaction = db.transaction(AppDb.file2StoreName, idbModeReadWrite); (db) => db.transaction(AppDb.file2StoreName, idbModeReadWrite),
final file2Store = transaction.objectStore(AppDb.file2StoreName); (transaction) async {
for (final f in files) { final file2Store = transaction.objectStore(AppDb.file2StoreName);
await file2Store.put(AppDbFile2Entry.fromFile(account, f).toJson(), for (final f in files) {
AppDbFile2Entry.toPrimaryKeyForFile(account, f)); await file2Store.put(AppDbFile2Entry.fromFile(account, f).toJson(),
} AppDbFile2Entry.toPrimaryKeyForFile(account, f));
}); }
},
);
} }
Future<void> fillAppDbDir( Future<void> fillAppDbDir(
AppDb appDb, Account account, File dir, List<File> children) async { AppDb appDb, Account account, File dir, List<File> children) async {
await appDb.use((db) async { await appDb.use(
final transaction = db.transaction(AppDb.dirStoreName, idbModeReadWrite); (db) => db.transaction(AppDb.dirStoreName, idbModeReadWrite),
final dirStore = transaction.objectStore(AppDb.dirStoreName); (transaction) async {
await dirStore.put(AppDbDirEntry.fromFiles(account, dir, children).toJson(), final dirStore = transaction.objectStore(AppDb.dirStoreName);
AppDbDirEntry.toPrimaryKeyForDir(account, dir)); await dirStore.put(
}); AppDbDirEntry.fromFiles(account, dir, children).toJson(),
AppDbDirEntry.toPrimaryKeyForDir(account, dir));
},
);
} }
Future<List<T>> listAppDb<T>( Future<List<T>> listAppDb<T>(
AppDb appDb, String storeName, T Function(JsonObj) transform) { AppDb appDb, String storeName, T Function(JsonObj) transform) {
return appDb.use((db) async { return appDb.use(
final transaction = db.transaction(storeName, idbModeReadOnly); (db) => db.transaction(storeName, idbModeReadOnly),
final store = transaction.objectStore(storeName); (transaction) async {
return await store final store = transaction.objectStore(storeName);
.openCursor(autoAdvance: true) return await store
.map((c) => c.value) .openCursor(autoAdvance: true)
.cast<Map>() .map((c) => c.value)
.map((e) => transform(e.cast<String, dynamic>())) .cast<Map>()
.toList(); .map((e) => transform(e.cast<String, dynamic>()))
}); .toList();
},
);
} }

View file

@ -29,12 +29,14 @@ void main() {
/// Expect: true /// Expect: true
Future<void> _isAppDbNeedMigrationEntryFalse() async { Future<void> _isAppDbNeedMigrationEntryFalse() async {
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await appDb.use(
final transaction = db.transaction(AppDb.metaStoreName, idbModeReadWrite); (db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
final metaStore = transaction.objectStore(AppDb.metaStoreName); (transaction) async {
const entry = AppDbMetaEntryCompatV37(false); final metaStore = transaction.objectStore(AppDb.metaStoreName);
await metaStore.put(entry.toEntry().toJson()); const entry = AppDbMetaEntryCompatV37(false);
}); await metaStore.put(entry.toEntry().toJson());
},
);
expect(await CompatV37.isAppDbNeedMigration(appDb), true); expect(await CompatV37.isAppDbNeedMigration(appDb), true);
} }
@ -44,12 +46,14 @@ Future<void> _isAppDbNeedMigrationEntryFalse() async {
/// Expect: false /// Expect: false
Future<void> _isAppDbNeedMigrationEntryTrue() async { Future<void> _isAppDbNeedMigrationEntryTrue() async {
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await appDb.use(
final transaction = db.transaction(AppDb.metaStoreName, idbModeReadWrite); (db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
final metaStore = transaction.objectStore(AppDb.metaStoreName); (transaction) async {
const entry = AppDbMetaEntryCompatV37(true); final metaStore = transaction.objectStore(AppDb.metaStoreName);
await metaStore.put(entry.toEntry().toJson()); const entry = AppDbMetaEntryCompatV37(true);
}); await metaStore.put(entry.toEntry().toJson());
},
);
expect(await CompatV37.isAppDbNeedMigration(appDb), false); expect(await CompatV37.isAppDbNeedMigration(appDb), false);
} }
@ -59,12 +63,14 @@ Future<void> _isAppDbNeedMigrationEntryTrue() async {
/// Expect: false /// Expect: false
Future<void> _isAppDbNeedMigrationWithoutEntry() async { Future<void> _isAppDbNeedMigrationWithoutEntry() async {
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await appDb.use(
final transaction = db.transaction(AppDb.metaStoreName, idbModeReadWrite); (db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
final metaStore = transaction.objectStore(AppDb.metaStoreName); (transaction) async {
const entry = AppDbMetaEntryCompatV37(true); final metaStore = transaction.objectStore(AppDb.metaStoreName);
await metaStore.put(entry.toEntry().toJson()); const entry = AppDbMetaEntryCompatV37(true);
}); await metaStore.put(entry.toEntry().toJson());
},
);
expect(await CompatV37.isAppDbNeedMigration(appDb), false); expect(await CompatV37.isAppDbNeedMigration(appDb), false);
} }
@ -81,11 +87,9 @@ Future<void> _migrateAppDbWithoutNomedia() async {
..addJpeg("admin/dir1/test2.jpg")) ..addJpeg("admin/dir1/test2.jpg"))
.build(); .build();
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await util.fillAppDb(appDb, account, files);
await util.fillAppDb(appDb, account, files); await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 3));
await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 3)); await util.fillAppDbDir(appDb, account, files[2], [files[3]]);
await util.fillAppDbDir(appDb, account, files[2], [files[3]]);
});
await CompatV37.migrateAppDb(appDb); await CompatV37.migrateAppDb(appDb);
final fileObjs = await util.listAppDb( final fileObjs = await util.listAppDb(
@ -113,11 +117,9 @@ Future<void> _migrateAppDb() async {
..addJpeg("admin/dir1/test2.jpg")) ..addJpeg("admin/dir1/test2.jpg"))
.build(); .build();
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await util.fillAppDb(appDb, account, files);
await util.fillAppDb(appDb, account, files); await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 3));
await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 3)); await util.fillAppDbDir(appDb, account, files[2], files.slice(3, 5));
await util.fillAppDbDir(appDb, account, files[2], files.slice(3, 5));
});
await CompatV37.migrateAppDb(appDb); await CompatV37.migrateAppDb(appDb);
final fileObjs = await util.listAppDb( final fileObjs = await util.listAppDb(
@ -147,12 +149,10 @@ Future<void> _migrateAppDbNestedDir() async {
..addJpeg("admin/dir1/dir1-1/test3.jpg")) ..addJpeg("admin/dir1/dir1-1/test3.jpg"))
.build(); .build();
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await util.fillAppDb(appDb, account, files);
await util.fillAppDb(appDb, account, files); await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 3));
await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 3)); await util.fillAppDbDir(appDb, account, files[2], files.slice(3, 6));
await util.fillAppDbDir(appDb, account, files[2], files.slice(3, 6)); await util.fillAppDbDir(appDb, account, files[5], [files[6]]);
await util.fillAppDbDir(appDb, account, files[5], [files[6]]);
});
await CompatV37.migrateAppDb(appDb); await CompatV37.migrateAppDb(appDb);
final fileObjs = await util.listAppDb( final fileObjs = await util.listAppDb(
@ -183,12 +183,10 @@ Future<void> _migrateAppDbNestedMarker() async {
..addJpeg("admin/dir1/dir1-1/test3.jpg")) ..addJpeg("admin/dir1/dir1-1/test3.jpg"))
.build(); .build();
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await util.fillAppDb(appDb, account, files);
await util.fillAppDb(appDb, account, files); await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 3));
await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 3)); await util.fillAppDbDir(appDb, account, files[2], files.slice(3, 6));
await util.fillAppDbDir(appDb, account, files[2], files.slice(3, 6)); await util.fillAppDbDir(appDb, account, files[5], files.slice(6, 8));
await util.fillAppDbDir(appDb, account, files[5], files.slice(6, 8));
});
await CompatV37.migrateAppDb(appDb); await CompatV37.migrateAppDb(appDb);
final fileObjs = await util.listAppDb( final fileObjs = await util.listAppDb(
@ -216,11 +214,9 @@ Future<void> _migrateAppDbRoot() async {
..addJpeg("admin/dir1/test2.jpg")) ..addJpeg("admin/dir1/test2.jpg"))
.build(); .build();
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await util.fillAppDb(appDb, account, files);
await util.fillAppDb(appDb, account, files); await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 4));
await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 4)); await util.fillAppDbDir(appDb, account, files[3], [files[4]]);
await util.fillAppDbDir(appDb, account, files[3], [files[4]]);
});
await CompatV37.migrateAppDb(appDb); await CompatV37.migrateAppDb(appDb);
final objs = await util.listAppDb( final objs = await util.listAppDb(

View file

@ -13,39 +13,42 @@ void main() {
group("isNeedMigration", () { group("isNeedMigration", () {
test("w/ meta entry == false", () async { test("w/ meta entry == false", () async {
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await appDb.use(
final transaction = (db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
db.transaction(AppDb.metaStoreName, idbModeReadWrite); (transaction) async {
final metaStore = transaction.objectStore(AppDb.metaStoreName); final metaStore = transaction.objectStore(AppDb.metaStoreName);
const entry = AppDbMetaEntryDbCompatV5(false); const entry = AppDbMetaEntryDbCompatV5(false);
await metaStore.put(entry.toEntry().toJson()); await metaStore.put(entry.toEntry().toJson());
}); },
);
expect(await DbCompatV5.isNeedMigration(appDb), true); expect(await DbCompatV5.isNeedMigration(appDb), true);
}); });
test("w/ meta entry == true", () async { test("w/ meta entry == true", () async {
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await appDb.use(
final transaction = (db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
db.transaction(AppDb.metaStoreName, idbModeReadWrite); (transaction) async {
final metaStore = transaction.objectStore(AppDb.metaStoreName); final metaStore = transaction.objectStore(AppDb.metaStoreName);
const entry = AppDbMetaEntryDbCompatV5(true); const entry = AppDbMetaEntryDbCompatV5(true);
await metaStore.put(entry.toEntry().toJson()); await metaStore.put(entry.toEntry().toJson());
}); },
);
expect(await DbCompatV5.isNeedMigration(appDb), false); expect(await DbCompatV5.isNeedMigration(appDb), false);
}); });
test("w/o meta entry", () async { test("w/o meta entry", () async {
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await appDb.use(
final transaction = (db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
db.transaction(AppDb.metaStoreName, idbModeReadWrite); (transaction) async {
final metaStore = transaction.objectStore(AppDb.metaStoreName); final metaStore = transaction.objectStore(AppDb.metaStoreName);
const entry = AppDbMetaEntryDbCompatV5(true); const entry = AppDbMetaEntryDbCompatV5(true);
await metaStore.put(entry.toEntry().toJson()); await metaStore.put(entry.toEntry().toJson());
}); },
);
expect(await DbCompatV5.isNeedMigration(appDb), false); expect(await DbCompatV5.isNeedMigration(appDb), false);
}); });
@ -60,17 +63,18 @@ void main() {
)) ))
.build(); .build();
final appDb = MockAppDb(); final appDb = MockAppDb();
await appDb.use((db) async { await appDb.use(
final transaction = (db) => db.transaction(AppDb.file2StoreName, idbModeReadWrite),
db.transaction(AppDb.file2StoreName, idbModeReadWrite); (transaction) async {
final fileStore = transaction.objectStore(AppDb.file2StoreName); final fileStore = transaction.objectStore(AppDb.file2StoreName);
await fileStore.put({ await fileStore.put({
"server": account.url, "server": account.url,
"userId": account.username.toCaseInsensitiveString(), "userId": account.username.toCaseInsensitiveString(),
"strippedPath": files[0].strippedPathWithEmpty, "strippedPath": files[0].strippedPathWithEmpty,
"file": files[0].toJson(), "file": files[0].toJson(),
}, "${account.url}/${account.username.toCaseInsensitiveString()}/${files[0].fileId}"); }, "${account.url}/${account.username.toCaseInsensitiveString()}/${files[0].fileId}");
}); },
);
await DbCompatV5.migrate(appDb); await DbCompatV5.migrate(appDb);
final objs = final objs =