mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-13 18:58:53 +01:00
Optimize SyncFavorite to only update a column
This commit is contained in:
parent
450f43694a
commit
49b5901149
9 changed files with 224 additions and 75 deletions
|
@ -113,10 +113,11 @@ class ListFavoriteBloc
|
|||
emit(ListFavoriteBlocSuccess(ev.account, remote));
|
||||
|
||||
if (cache != null) {
|
||||
CacheFavorite(_c)(ev.account, remote, cache: cache)
|
||||
CacheFavorite(_c)(ev.account, remote.map((f) => f.fileId!))
|
||||
.onError((e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_onEventQuery] Failed while CacheFavorite", e, stackTrace);
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
|
|
|
@ -291,7 +291,7 @@ class ScanAccountDirBloc
|
|||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if ((ev.newFavorites + ev.removedFavorites).any(_isFileOfInterest)) {
|
||||
if (ev.account.compareServerIdentity(account)) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
|
|
|
@ -12,6 +12,13 @@ class Favorite with EquatableMixin {
|
|||
"fileId: '$fileId', "
|
||||
"}";
|
||||
|
||||
Favorite copyWith({
|
||||
int? fileId,
|
||||
}) =>
|
||||
Favorite(
|
||||
fileId: fileId ?? this.fileId,
|
||||
);
|
||||
|
||||
@override
|
||||
get props => [
|
||||
fileId,
|
||||
|
|
|
@ -110,12 +110,9 @@ class ShareRemovedEvent {
|
|||
}
|
||||
|
||||
class FavoriteResyncedEvent {
|
||||
const FavoriteResyncedEvent(
|
||||
this.account, this.newFavorites, this.removedFavorites);
|
||||
const FavoriteResyncedEvent(this.account);
|
||||
|
||||
final Account account;
|
||||
final List<File> newFavorites;
|
||||
final List<File> removedFavorites;
|
||||
}
|
||||
|
||||
class ThemeChangedEvent {}
|
||||
|
|
|
@ -5,81 +5,92 @@ import 'package:kiwi/kiwi.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/list_util.dart' as list_util;
|
||||
import 'package:nc_photos/use_case/list_favorite_offline.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
|
||||
class CacheFavorite {
|
||||
CacheFavorite(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(ListFavoriteOffline.require(_c));
|
||||
CacheFavorite(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
/// Cache favorites
|
||||
Future<void> call(
|
||||
Account account,
|
||||
List<File> remote, {
|
||||
List<File>? cache,
|
||||
}) async {
|
||||
cache ??= await ListFavoriteOffline(_c)(account);
|
||||
final remoteSorted =
|
||||
remote.sorted((a, b) => a.fileId!.compareTo(b.fileId!));
|
||||
final cacheSorted = cache.sorted((a, b) => a.fileId!.compareTo(b.fileId!));
|
||||
final result = list_util.diffWith<File>(
|
||||
cacheSorted, remoteSorted, (a, b) => a.fileId!.compareTo(b.fileId!));
|
||||
final newFavorites = result.item1;
|
||||
final removedFavorites =
|
||||
result.item2.map((f) => f.copyWith(isFavorite: false)).toList();
|
||||
final newFileIds = newFavorites.map((f) => f.fileId!).toList();
|
||||
final removedFileIds = removedFavorites.map((f) => f.fileId!).toList();
|
||||
if (newFileIds.isEmpty && removedFileIds.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await _c.sqliteDb.use((db) async {
|
||||
final rowIds = await db.accountFileRowIdsByFileIds(
|
||||
newFileIds + removedFileIds,
|
||||
appAccount: account,
|
||||
);
|
||||
final rowIdsMap =
|
||||
Map.fromEntries(rowIds.map((e) => MapEntry(e.fileId, e)));
|
||||
await db.batch((batch) {
|
||||
for (final id in newFileIds) {
|
||||
try {
|
||||
batch.update(
|
||||
db.accountFiles,
|
||||
const sql.AccountFilesCompanion(isFavorite: sql.Value(true)),
|
||||
where: (sql.$AccountFilesTable t) =>
|
||||
t.rowId.equals(rowIdsMap[id]!.accountFileRowId),
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[call] File not found in DB: $id", e, stackTrace);
|
||||
}
|
||||
}
|
||||
for (final id in removedFileIds) {
|
||||
try {
|
||||
batch.update(
|
||||
db.accountFiles,
|
||||
const sql.AccountFilesCompanion(isFavorite: sql.Value(null)),
|
||||
where: (sql.$AccountFilesTable t) =>
|
||||
t.rowId.equals(rowIdsMap[id]!.accountFileRowId),
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[call] File not found in DB: $id", e, stackTrace);
|
||||
}
|
||||
}
|
||||
});
|
||||
/// Cache favorites using results from remote
|
||||
///
|
||||
/// Return number of files updated
|
||||
Future<int> call(Account account, Iterable<int> remoteFileIds) async {
|
||||
_log.info("[call] Cache favorites");
|
||||
final remote = remoteFileIds.sorted(Comparable.compare);
|
||||
final updateCount = await _c.sqliteDb.use((db) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
final cache = await _getCacheFavorites(db, dbAccount);
|
||||
final cacheMap =
|
||||
Map.fromEntries(cache.map((e) => MapEntry(e.fileId, e.rowId)));
|
||||
final diff =
|
||||
list_util.diff(cacheMap.keys.sorted(Comparable.compare), remote);
|
||||
final newFileIds = diff.item1;
|
||||
_log.info("[call] New favorites: ${newFileIds.toReadableString()}");
|
||||
final removedFildIds = diff.item2;
|
||||
_log.info(
|
||||
"[call] Removed favorites: ${removedFildIds.toReadableString()}");
|
||||
|
||||
var updateCount = 0;
|
||||
if (newFileIds.isNotEmpty) {
|
||||
final rowIds = await db.accountFileRowIdsByFileIds(newFileIds,
|
||||
sqlAccount: dbAccount);
|
||||
final count = await (db.update(db.accountFiles)
|
||||
..where(
|
||||
(t) => t.rowId.isIn(rowIds.map((id) => id.accountFileRowId))))
|
||||
.write(
|
||||
const sql.AccountFilesCompanion(isFavorite: sql.Value(true)));
|
||||
_log.info("[call] Updated $count row (new)");
|
||||
updateCount += count;
|
||||
}
|
||||
if (removedFildIds.isNotEmpty) {
|
||||
final count = await (db.update(db.accountFiles)
|
||||
..where((t) =>
|
||||
t.account.equals(dbAccount.rowId) &
|
||||
t.file.isIn(removedFildIds.map((id) => cacheMap[id]))))
|
||||
.write(
|
||||
const sql.AccountFilesCompanion(isFavorite: sql.Value(false)));
|
||||
_log.info("[call] Updated $count row (remove)");
|
||||
updateCount += count;
|
||||
}
|
||||
return updateCount;
|
||||
});
|
||||
|
||||
KiwiContainer()
|
||||
.resolve<EventBus>()
|
||||
.fire(FavoriteResyncedEvent(account, newFavorites, removedFavorites));
|
||||
if (updateCount > 0) {
|
||||
KiwiContainer().resolve<EventBus>().fire(FavoriteResyncedEvent(account));
|
||||
}
|
||||
return updateCount;
|
||||
}
|
||||
|
||||
Future<List<_FileRowIdWithFileId>> _getCacheFavorites(
|
||||
sql.SqliteDb db, sql.Account dbAccount) async {
|
||||
final query = db.queryFiles().run((q) {
|
||||
q
|
||||
..setQueryMode(sql.FilesQueryMode.expression,
|
||||
expressions: [db.files.rowId, db.files.fileId])
|
||||
..setSqlAccount(dbAccount)
|
||||
..byFavorite(true);
|
||||
return q.build();
|
||||
});
|
||||
return await query
|
||||
.map((r) => _FileRowIdWithFileId(
|
||||
r.read(db.files.rowId)!, r.read(db.files.fileId)!))
|
||||
.get();
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
static final _log = Logger("use_case.cache_favorite.CacheFavorite");
|
||||
}
|
||||
|
||||
class _FileRowIdWithFileId {
|
||||
const _FileRowIdWithFileId(this.rowId, this.fileId);
|
||||
|
||||
final int rowId;
|
||||
final int fileId;
|
||||
}
|
||||
|
|
|
@ -1,22 +1,34 @@
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/use_case/cache_favorite.dart';
|
||||
import 'package:nc_photos/use_case/list_favorite.dart';
|
||||
|
||||
class SyncFavorite {
|
||||
SyncFavorite(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(CacheFavorite.require(_c)),
|
||||
assert(ListFavorite.require(_c));
|
||||
assert(CacheFavorite.require(_c));
|
||||
|
||||
static bool require(DiContainer c) => true;
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.favoriteRepo);
|
||||
|
||||
/// Sync favorites in AppDb with remote server
|
||||
Future<void> call(Account account) async {
|
||||
/// Sync favorites in cache db with remote server
|
||||
///
|
||||
/// Return number of files updated
|
||||
Future<int> call(Account account) async {
|
||||
_log.info("[call] Sync favorites with remote");
|
||||
final remote = await ListFavorite(_c)(account);
|
||||
await CacheFavorite(_c)(account, remote);
|
||||
final remote = await _getRemoteFavoriteFileIds(account);
|
||||
return await CacheFavorite(_c)(account, remote);
|
||||
}
|
||||
|
||||
Future<List<int>> _getRemoteFavoriteFileIds(Account account) async {
|
||||
final fileIds = <int>[];
|
||||
for (final r in account.roots) {
|
||||
final favorites = await _c.favoriteRepo
|
||||
.list(account, File(path: file_util.unstripPath(account, r)));
|
||||
fileIds.addAll(favorites.map((f) => f.fileId));
|
||||
}
|
||||
return fileIds;
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/ci_string.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/favorite.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
|
@ -98,6 +99,29 @@ class MockEventBus implements EventBus {
|
|||
final _streamController = StreamController.broadcast();
|
||||
}
|
||||
|
||||
class MockFavoriteRepo implements FavoriteRepo {
|
||||
@override
|
||||
FavoriteDataSource get dataSrc => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<List<Favorite>> list(Account account, File dir) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class MockFavoriteMemoryRepo extends MockFavoriteRepo {
|
||||
MockFavoriteMemoryRepo([
|
||||
List<Favorite> initialData = const [],
|
||||
]) : favorite = initialData.map((a) => a.copyWith()).toList();
|
||||
|
||||
@override
|
||||
list(Account account, File dir) async {
|
||||
return favorite.toList();
|
||||
}
|
||||
|
||||
final List<Favorite> favorite;
|
||||
}
|
||||
|
||||
/// Mock of [FileDataSource] where all methods will throw UnimplementedError
|
||||
class MockFileDataSource implements FileDataSource {
|
||||
@override
|
||||
|
|
|
@ -38,6 +38,7 @@ class FilesBuilder {
|
|||
DateTime? lastModified,
|
||||
bool isCollection = false,
|
||||
bool hasPreview = true,
|
||||
bool? isFavorite,
|
||||
String ownerId = "admin",
|
||||
String? ownerDisplayName,
|
||||
Metadata? metadata,
|
||||
|
@ -52,6 +53,7 @@ class FilesBuilder {
|
|||
isCollection: isCollection,
|
||||
hasPreview: hasPreview,
|
||||
fileId: fileId++,
|
||||
isFavorite: isFavorite,
|
||||
ownerId: ownerId.toCi(),
|
||||
ownerDisplayName: ownerDisplayName ?? ownerId.toString(),
|
||||
metadata: metadata,
|
||||
|
@ -65,6 +67,7 @@ class FilesBuilder {
|
|||
String? etag,
|
||||
DateTime? lastModified,
|
||||
bool hasPreview = true,
|
||||
bool? isFavorite,
|
||||
String ownerId = "admin",
|
||||
String? ownerDisplayName,
|
||||
}) =>
|
||||
|
@ -75,6 +78,7 @@ class FilesBuilder {
|
|||
etag: etag,
|
||||
lastModified: lastModified,
|
||||
hasPreview: hasPreview,
|
||||
isFavorite: isFavorite,
|
||||
ownerId: ownerId,
|
||||
ownerDisplayName: ownerDisplayName,
|
||||
);
|
||||
|
@ -85,6 +89,7 @@ class FilesBuilder {
|
|||
String? etag,
|
||||
DateTime? lastModified,
|
||||
bool hasPreview = true,
|
||||
bool? isFavorite,
|
||||
String ownerId = "admin",
|
||||
String? ownerDisplayName,
|
||||
OrNull<Metadata>? metadata,
|
||||
|
@ -96,6 +101,7 @@ class FilesBuilder {
|
|||
etag: etag,
|
||||
lastModified: lastModified,
|
||||
hasPreview: hasPreview,
|
||||
isFavorite: isFavorite,
|
||||
ownerId: ownerId,
|
||||
ownerDisplayName: ownerDisplayName,
|
||||
metadata: metadata?.obj ??
|
||||
|
@ -111,6 +117,7 @@ class FilesBuilder {
|
|||
int contentLength = 1024,
|
||||
String? etag,
|
||||
DateTime? lastModified,
|
||||
bool? isFavorite,
|
||||
String ownerId = "admin",
|
||||
String? ownerDisplayName,
|
||||
}) =>
|
||||
|
@ -120,6 +127,7 @@ class FilesBuilder {
|
|||
lastModified: lastModified,
|
||||
isCollection: true,
|
||||
hasPreview: false,
|
||||
isFavorite: isFavorite,
|
||||
ownerId: ownerId,
|
||||
ownerDisplayName: ownerDisplayName,
|
||||
);
|
||||
|
|
89
app/test/use_case/sync_favorite_test.dart
Normal file
89
app/test/use_case/sync_favorite_test.dart
Normal file
|
@ -0,0 +1,89 @@
|
|||
import 'package:drift/drift.dart' as sql;
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/favorite.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/use_case/sync_favorite.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../mock_type.dart';
|
||||
import '../test_util.dart' as util;
|
||||
|
||||
void main() {
|
||||
group("SyncFavorite", () {
|
||||
test("new", _new);
|
||||
test("remove", _remove);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _new() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder(initialFileId: 100)
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg", isFavorite: true)
|
||||
..addJpeg("admin/test2.jpg", isFavorite: true)
|
||||
..addJpeg("admin/test3.jpg")
|
||||
..addJpeg("admin/test4.jpg")
|
||||
..addJpeg("admin/test5.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
favoriteRepo: MockFavoriteMemoryRepo([
|
||||
const Favorite(fileId: 101),
|
||||
const Favorite(fileId: 102),
|
||||
const Favorite(fileId: 103),
|
||||
const Favorite(fileId: 104),
|
||||
]),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
await SyncFavorite(c)(account);
|
||||
expect(
|
||||
await listSqliteDbFavoriteFileIds(c.sqliteDb),
|
||||
{101, 102, 103, 104},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _remove() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder(initialFileId: 100)
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg", isFavorite: true)
|
||||
..addJpeg("admin/test2.jpg", isFavorite: true)
|
||||
..addJpeg("admin/test3.jpg", isFavorite: true)
|
||||
..addJpeg("admin/test4.jpg", isFavorite: true)
|
||||
..addJpeg("admin/test5.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
favoriteRepo: MockFavoriteMemoryRepo([
|
||||
const Favorite(fileId: 103),
|
||||
const Favorite(fileId: 104),
|
||||
]),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
await SyncFavorite(c)(account);
|
||||
expect(
|
||||
await listSqliteDbFavoriteFileIds(c.sqliteDb),
|
||||
{103, 104},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Set<int>> listSqliteDbFavoriteFileIds(sql.SqliteDb db) async {
|
||||
final query = db.selectOnly(db.files).join([
|
||||
sql.innerJoin(
|
||||
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),
|
||||
])
|
||||
..addColumns([db.files.fileId])
|
||||
..where(db.accountFiles.isFavorite.equals(true));
|
||||
return (await query.map((r) => r.read(db.files.fileId)!).get()).toSet();
|
||||
}
|
Loading…
Reference in a new issue