mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Switch local DB from IndexedDB to SQLite
This commit is contained in:
parent
2085b9197e
commit
2c4ec3447b
78 changed files with 7468 additions and 2785 deletions
|
@ -31,15 +31,6 @@
|
|||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<!-- Displays an Android View that continues showing the launch screen
|
||||
Drawable until Flutter paints its first frame, then this splash
|
||||
screen fades out. A splash screen is useful to avoid any visual
|
||||
gap between the end of Android's launch screen and the painting of
|
||||
Flutter's first frame. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
|
10
app/build.yaml
Normal file
10
app/build.yaml
Normal file
|
@ -0,0 +1,10 @@
|
|||
targets:
|
||||
$default:
|
||||
builders:
|
||||
drift_dev:
|
||||
options:
|
||||
apply_converters_on_variables: true
|
||||
generate_values_in_copy_with: true
|
||||
new_sql_code_generation: true
|
||||
scoped_dart_components: true
|
||||
generate_connect_constructor: true
|
|
@ -1,499 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:idb_shim/idb.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/ci_string.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/upgrader.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
import 'package:nc_photos/num_extension.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/type.dart';
|
||||
|
||||
class AppDb {
|
||||
static const dbName = "app.db";
|
||||
static const dbVersion = 6;
|
||||
static const albumStoreName = "albums";
|
||||
static const file2StoreName = "files2";
|
||||
static const dirStoreName = "dirs";
|
||||
static const metaStoreName = "meta";
|
||||
|
||||
/// Run [fn] with an opened database instance
|
||||
///
|
||||
/// This function guarantees that:
|
||||
/// 1) Database is always closed after [fn] exits, even with an error
|
||||
/// 2) Only at most 1 database instance being opened at any time
|
||||
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
|
||||
return await platform.Lock.synchronized(k.appDbLockId, () async {
|
||||
final db = await _open();
|
||||
Transaction? transaction;
|
||||
try {
|
||||
transaction = transactionBuilder(db);
|
||||
return await fn(transaction);
|
||||
} finally {
|
||||
if (transaction != null) {
|
||||
await transaction.completed;
|
||||
}
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> delete() async {
|
||||
_log.warning("[delete] Deleting database");
|
||||
return await platform.Lock.synchronized(k.appDbLockId, () async {
|
||||
final dbFactory = platform.getDbFactory();
|
||||
await dbFactory.deleteDatabase(dbName);
|
||||
});
|
||||
}
|
||||
|
||||
/// Open the database
|
||||
Future<Database> _open() {
|
||||
if (platform_k.isWeb) {
|
||||
return _openNative();
|
||||
} else {
|
||||
return _openSqflite();
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the sqflite database
|
||||
///
|
||||
/// We can't simply call deleteObjectStore on upgrade failure here, as the
|
||||
/// package does not remove the corresponding indexes, so when we recreate
|
||||
/// the indexes later, it'll fail. What we do here, is to delete the whole
|
||||
/// database instead
|
||||
Future<Database> _openSqflite() async {
|
||||
final dbFactory = platform.getDbFactory();
|
||||
try {
|
||||
int? fromVersion, toVersion;
|
||||
final db = await dbFactory.open(
|
||||
dbName,
|
||||
version: dbVersion,
|
||||
onUpgradeNeeded: (event) {
|
||||
_upgrade(event);
|
||||
fromVersion = event.oldVersion;
|
||||
toVersion = event.newVersion;
|
||||
},
|
||||
);
|
||||
if (fromVersion != null && toVersion != null) {
|
||||
await _onPostUpgrade(db, fromVersion!, toVersion!);
|
||||
}
|
||||
return db;
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_openSqflite] Failed while upgrading database", e, stackTrace);
|
||||
_log.warning("[_openSqflite] Recreating db");
|
||||
await dbFactory.deleteDatabase(dbName);
|
||||
return dbFactory.open(dbName,
|
||||
version: dbVersion, onUpgradeNeeded: _upgrade);
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the native IndexedDB database
|
||||
///
|
||||
/// Errors thrown in onUpgradeNeeded are not propagated properly to us on web,
|
||||
/// so the sqflite approach will not work
|
||||
Future<Database> _openNative() async {
|
||||
final dbFactory = platform.getDbFactory();
|
||||
int? fromVersion, toVersion;
|
||||
final db = await dbFactory.open(
|
||||
dbName,
|
||||
version: dbVersion,
|
||||
onUpgradeNeeded: (event) {
|
||||
try {
|
||||
_upgrade(event);
|
||||
fromVersion = event.oldVersion;
|
||||
toVersion = event.newVersion;
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_openNative] Failed while upgrading database", e, stackTrace);
|
||||
// drop the db and rebuild a new one instead
|
||||
try {
|
||||
event.database.deleteObjectStore(albumStoreName);
|
||||
} catch (_) {}
|
||||
try {
|
||||
event.database.deleteObjectStore(file2StoreName);
|
||||
} catch (_) {}
|
||||
try {
|
||||
event.database.deleteObjectStore(dirStoreName);
|
||||
} catch (_) {}
|
||||
try {
|
||||
event.database.deleteObjectStore(metaStoreName);
|
||||
} catch (_) {}
|
||||
try {
|
||||
event.database.deleteObjectStore(_fileDbStoreName);
|
||||
} catch (_) {}
|
||||
try {
|
||||
event.database.deleteObjectStore(_fileStoreName);
|
||||
} catch (_) {}
|
||||
_log.warning("[_openNative] Recreating db");
|
||||
_upgrade(_DummyVersionChangeEvent(
|
||||
0,
|
||||
event.newVersion,
|
||||
event.transaction,
|
||||
event.target,
|
||||
event.currentTarget,
|
||||
event.database));
|
||||
}
|
||||
},
|
||||
);
|
||||
if (fromVersion != null && toVersion != null) {
|
||||
await _onPostUpgrade(db, fromVersion!, toVersion!);
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
void _upgrade(VersionChangeEvent event) {
|
||||
_log.info("[_upgrade] Upgrade database: ${event.oldVersion} -> $dbVersion");
|
||||
|
||||
final db = event.database;
|
||||
// ignore: unused_local_variable
|
||||
ObjectStore? albumStore, file2Store, dirStore, metaStore;
|
||||
if (event.oldVersion < 2) {
|
||||
// version 2 store things in a new way, just drop all
|
||||
try {
|
||||
db.deleteObjectStore(albumStoreName);
|
||||
} catch (_) {}
|
||||
albumStore = db.createObjectStore(albumStoreName);
|
||||
albumStore.createIndex(
|
||||
AppDbAlbumEntry.indexName, AppDbAlbumEntry.keyPath);
|
||||
}
|
||||
if (event.oldVersion < 3) {
|
||||
// new object store in v3
|
||||
// no longer relevant in v4
|
||||
|
||||
// recreate file store from scratch
|
||||
// no longer relevant in v4
|
||||
}
|
||||
if (event.oldVersion < 4) {
|
||||
try {
|
||||
db.deleteObjectStore(_fileDbStoreName);
|
||||
} catch (_) {}
|
||||
try {
|
||||
db.deleteObjectStore(_fileStoreName);
|
||||
} catch (_) {}
|
||||
|
||||
file2Store = db.createObjectStore(file2StoreName);
|
||||
file2Store.createIndex(AppDbFile2Entry.strippedPathIndexName,
|
||||
AppDbFile2Entry.strippedPathKeyPath);
|
||||
|
||||
dirStore = db.createObjectStore(dirStoreName);
|
||||
}
|
||||
file2Store ??= event.transaction.objectStore(file2StoreName);
|
||||
if (event.oldVersion < 5) {
|
||||
file2Store.createIndex(AppDbFile2Entry.dateTimeEpochMsIndexName,
|
||||
AppDbFile2Entry.dateTimeEpochMsKeyPath);
|
||||
|
||||
metaStore =
|
||||
db.createObjectStore(metaStoreName, keyPath: AppDbMetaEntry.keyPath);
|
||||
}
|
||||
if (event.oldVersion < 6) {
|
||||
file2Store.createIndex(AppDbFile2Entry.fileIsFavoriteIndexName,
|
||||
AppDbFile2Entry.fileIsFavoriteKeyPath);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onPostUpgrade(
|
||||
Database db, int fromVersion, int toVersion) async {
|
||||
if (fromVersion.inRange(1, 4) && toVersion >= 5) {
|
||||
final transaction = db.transaction(AppDb.metaStoreName, idbModeReadWrite);
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
await metaStore
|
||||
.put(const AppDbMetaEntryDbCompatV5(false).toEntry().toJson());
|
||||
await transaction.completed;
|
||||
}
|
||||
}
|
||||
|
||||
static const _fileDbStoreName = "filesDb";
|
||||
static const _fileStoreName = "files";
|
||||
|
||||
static final _log = Logger("app_db.AppDb");
|
||||
}
|
||||
|
||||
class AppDbAlbumEntry {
|
||||
static const indexName = "albumStore_path_index";
|
||||
static const keyPath = ["path", "index"];
|
||||
static const maxDataSize = 160;
|
||||
|
||||
AppDbAlbumEntry(this.path, this.index, this.album);
|
||||
|
||||
JsonObj toJson() {
|
||||
return {
|
||||
"path": path,
|
||||
"index": index,
|
||||
"album": album.toAppDbJson(),
|
||||
};
|
||||
}
|
||||
|
||||
factory AppDbAlbumEntry.fromJson(JsonObj json, Account account) {
|
||||
return AppDbAlbumEntry(
|
||||
json["path"],
|
||||
json["index"],
|
||||
Album.fromJson(
|
||||
json["album"].cast<String, dynamic>(),
|
||||
upgraderFactory: DefaultAlbumUpgraderFactory(
|
||||
account: account,
|
||||
logFilePath: json["path"],
|
||||
),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
static String toPath(Account account, String filePath) =>
|
||||
"${account.url}/$filePath";
|
||||
static String toPathFromFile(Account account, File albumFile) =>
|
||||
toPath(account, albumFile.path);
|
||||
static String toPrimaryKey(Account account, File albumFile, int index) =>
|
||||
"${toPathFromFile(account, albumFile)}[$index]";
|
||||
|
||||
final String path;
|
||||
final int index;
|
||||
// properties other than Album.items is undefined when index > 0
|
||||
final Album album;
|
||||
}
|
||||
|
||||
class AppDbFile2Entry with EquatableMixin {
|
||||
static const strippedPathIndexName = "server_userId_strippedPath";
|
||||
static const strippedPathKeyPath = ["server", "userId", "strippedPath"];
|
||||
|
||||
static const dateTimeEpochMsIndexName = "server_userId_dateTimeEpochMs";
|
||||
static const dateTimeEpochMsKeyPath = ["server", "userId", "dateTimeEpochMs"];
|
||||
|
||||
static const fileIsFavoriteIndexName = "server_userId_fileIsFavorite";
|
||||
static const fileIsFavoriteKeyPath = ["server", "userId", "file.isFavorite"];
|
||||
|
||||
AppDbFile2Entry(this.server, this.userId, this.strippedPath,
|
||||
this.dateTimeEpochMs, this.file);
|
||||
|
||||
factory AppDbFile2Entry.fromFile(Account account, File file) =>
|
||||
AppDbFile2Entry(account.url, account.username, file.strippedPathWithEmpty,
|
||||
file.bestDateTime.millisecondsSinceEpoch, file);
|
||||
|
||||
factory AppDbFile2Entry.fromJson(JsonObj json) => AppDbFile2Entry(
|
||||
json["server"],
|
||||
(json["userId"] as String).toCi(),
|
||||
json["strippedPath"],
|
||||
json["dateTimeEpochMs"],
|
||||
File.fromJson(json["file"].cast<String, dynamic>()),
|
||||
);
|
||||
|
||||
JsonObj toJson() => {
|
||||
"server": server,
|
||||
"userId": userId.toCaseInsensitiveString(),
|
||||
"strippedPath": strippedPath,
|
||||
"dateTimeEpochMs": dateTimeEpochMs,
|
||||
"file": file.toJson(),
|
||||
};
|
||||
|
||||
static String toPrimaryKey(Account account, int fileId) =>
|
||||
"${account.url}/${account.username.toCaseInsensitiveString()}/$fileId";
|
||||
|
||||
static String toPrimaryKeyForFile(Account account, File file) =>
|
||||
toPrimaryKey(account, file.fileId!);
|
||||
|
||||
static List<Object> toStrippedPathIndexKey(
|
||||
Account account, String strippedPath) =>
|
||||
[
|
||||
account.url,
|
||||
account.username.toCaseInsensitiveString(),
|
||||
strippedPath == "." ? "" : strippedPath
|
||||
];
|
||||
|
||||
static List<Object> toStrippedPathIndexKeyForFile(
|
||||
Account account, File file) =>
|
||||
toStrippedPathIndexKey(account, file.strippedPathWithEmpty);
|
||||
|
||||
/// Return the lower bound key used to query files under [dir] and its sub
|
||||
/// dirs
|
||||
static List<Object> toStrippedPathIndexLowerKeyForDir(
|
||||
Account account, File dir) =>
|
||||
[
|
||||
account.url,
|
||||
account.username.toCaseInsensitiveString(),
|
||||
dir.strippedPath.run((p) => p == "." ? "" : "$p/")
|
||||
];
|
||||
|
||||
/// Return the upper bound key used to query files under [dir] and its sub
|
||||
/// dirs
|
||||
static List<Object> toStrippedPathIndexUpperKeyForDir(
|
||||
Account account, File dir) {
|
||||
return toStrippedPathIndexLowerKeyForDir(account, dir).run((k) {
|
||||
k[2] = (k[2] as String) + "\uffff";
|
||||
return k;
|
||||
});
|
||||
}
|
||||
|
||||
static List<Object> toDateTimeEpochMsIndexKey(Account account, int epochMs) =>
|
||||
[
|
||||
account.url,
|
||||
account.username.toCaseInsensitiveString(),
|
||||
epochMs,
|
||||
];
|
||||
|
||||
static List<Object> toFileIsFavoriteIndexKey(
|
||||
Account account, bool isFavorite) =>
|
||||
[
|
||||
account.url,
|
||||
account.username.toCaseInsensitiveString(),
|
||||
isFavorite ? 1 : 0,
|
||||
];
|
||||
|
||||
@override
|
||||
get props => [
|
||||
server,
|
||||
userId,
|
||||
strippedPath,
|
||||
dateTimeEpochMs,
|
||||
file,
|
||||
];
|
||||
|
||||
/// Server URL where this file belongs to
|
||||
final String server;
|
||||
final CiString userId;
|
||||
final String strippedPath;
|
||||
final int dateTimeEpochMs;
|
||||
final File file;
|
||||
}
|
||||
|
||||
class AppDbDirEntry with EquatableMixin {
|
||||
AppDbDirEntry._(
|
||||
this.server, this.userId, this.strippedPath, this.dir, this.children);
|
||||
|
||||
factory AppDbDirEntry.fromFiles(
|
||||
Account account, File dir, List<File> children) =>
|
||||
AppDbDirEntry._(
|
||||
account.url,
|
||||
account.username,
|
||||
dir.strippedPathWithEmpty,
|
||||
dir,
|
||||
children.map((f) => f.fileId!).toList(),
|
||||
);
|
||||
|
||||
factory AppDbDirEntry.fromJson(JsonObj json) => AppDbDirEntry._(
|
||||
json["server"],
|
||||
(json["userId"] as String).toCi(),
|
||||
json["strippedPath"],
|
||||
File.fromJson((json["dir"] as Map).cast<String, dynamic>()),
|
||||
json["children"].cast<int>(),
|
||||
);
|
||||
|
||||
JsonObj toJson() => {
|
||||
"server": server,
|
||||
"userId": userId.toCaseInsensitiveString(),
|
||||
"strippedPath": strippedPath,
|
||||
"dir": dir.toJson(),
|
||||
"children": children,
|
||||
};
|
||||
|
||||
static String toPrimaryKeyForDir(Account account, File dir) =>
|
||||
"${account.url}/${account.username.toCaseInsensitiveString()}/${dir.strippedPathWithEmpty}";
|
||||
|
||||
/// Return the lower bound key used to query dirs under [root] and its sub
|
||||
/// dirs
|
||||
static String toPrimaryLowerKeyForSubDirs(Account account, File root) {
|
||||
final strippedPath = root.strippedPath.run((p) => p == "." ? "" : "$p/");
|
||||
return "${account.url}/${account.username.toCaseInsensitiveString()}/$strippedPath";
|
||||
}
|
||||
|
||||
/// Return the upper bound key used to query dirs under [root] and its sub
|
||||
/// dirs
|
||||
static String toPrimaryUpperKeyForSubDirs(Account account, File root) =>
|
||||
toPrimaryLowerKeyForSubDirs(account, root) + "\uffff";
|
||||
|
||||
@override
|
||||
get props => [
|
||||
server,
|
||||
userId,
|
||||
strippedPath,
|
||||
dir,
|
||||
children,
|
||||
];
|
||||
|
||||
/// Server URL where this file belongs to
|
||||
final String server;
|
||||
final CiString userId;
|
||||
final String strippedPath;
|
||||
final File dir;
|
||||
final List<int> children;
|
||||
}
|
||||
|
||||
class AppDbMetaEntry with EquatableMixin {
|
||||
static const keyPath = "key";
|
||||
|
||||
const AppDbMetaEntry(this.key, this.obj);
|
||||
|
||||
factory AppDbMetaEntry.fromJson(JsonObj json) => AppDbMetaEntry(
|
||||
json["key"],
|
||||
json["obj"].cast<String, dynamic>(),
|
||||
);
|
||||
|
||||
JsonObj toJson() => {
|
||||
"key": key,
|
||||
"obj": obj,
|
||||
};
|
||||
|
||||
@override
|
||||
get props => [
|
||||
key,
|
||||
obj,
|
||||
];
|
||||
|
||||
final String key;
|
||||
final JsonObj obj;
|
||||
}
|
||||
|
||||
class AppDbMetaEntryDbCompatV5 {
|
||||
static const key = "dbCompatV5";
|
||||
|
||||
const AppDbMetaEntryDbCompatV5(this.isMigrated);
|
||||
|
||||
factory AppDbMetaEntryDbCompatV5.fromJson(JsonObj json) =>
|
||||
AppDbMetaEntryDbCompatV5(json["isMigrated"]);
|
||||
|
||||
AppDbMetaEntry toEntry() => AppDbMetaEntry(key, {
|
||||
"isMigrated": isMigrated,
|
||||
});
|
||||
|
||||
final bool isMigrated;
|
||||
}
|
||||
|
||||
class AppDbMetaEntryCompatV37 {
|
||||
static const key = "compatV37";
|
||||
|
||||
const AppDbMetaEntryCompatV37(this.isMigrated);
|
||||
|
||||
factory AppDbMetaEntryCompatV37.fromJson(JsonObj json) =>
|
||||
AppDbMetaEntryCompatV37(json["isMigrated"]);
|
||||
|
||||
AppDbMetaEntry toEntry() => AppDbMetaEntry(key, {
|
||||
"isMigrated": isMigrated,
|
||||
});
|
||||
|
||||
final bool isMigrated;
|
||||
}
|
||||
|
||||
class _DummyVersionChangeEvent implements VersionChangeEvent {
|
||||
const _DummyVersionChangeEvent(this.oldVersion, this.newVersion,
|
||||
this.transaction, this.target, this.currentTarget, this.database);
|
||||
|
||||
@override
|
||||
final int oldVersion;
|
||||
@override
|
||||
final int newVersion;
|
||||
@override
|
||||
final Transaction transaction;
|
||||
@override
|
||||
final Object target;
|
||||
@override
|
||||
final Object currentTarget;
|
||||
@override
|
||||
final Database database;
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/data_source.dart';
|
||||
import 'package:nc_photos/entity/face.dart';
|
||||
import 'package:nc_photos/entity/face/data_source.dart';
|
||||
import 'package:nc_photos/entity/favorite.dart';
|
||||
|
@ -21,6 +22,8 @@ import 'package:nc_photos/entity/share.dart';
|
|||
import 'package:nc_photos/entity/share/data_source.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/entity/sharee/data_source.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite_table_isolate.dart' as sql_isolate;
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/entity/tag/data_source.dart';
|
||||
import 'package:nc_photos/entity/tagged_file.dart';
|
||||
|
@ -34,13 +37,19 @@ import 'package:nc_photos/pref.dart';
|
|||
import 'package:nc_photos/pref_util.dart' as pref_util;
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
Future<void> initAppLaunch() async {
|
||||
enum InitIsolateType {
|
||||
main,
|
||||
service,
|
||||
}
|
||||
|
||||
Future<void> init(InitIsolateType isolateType) async {
|
||||
if (_hasInitedInThisIsolate) {
|
||||
_log.warning("[initAppLaunch] Already initialized in this isolate");
|
||||
return;
|
||||
}
|
||||
|
||||
initLog();
|
||||
_initDrift();
|
||||
_initKiwi();
|
||||
await _initPref();
|
||||
await _initAccountPrefs();
|
||||
|
@ -49,7 +58,7 @@ Future<void> initAppLaunch() async {
|
|||
if (features.isSupportSelfSignedCert) {
|
||||
_initSelfSignedCertManager();
|
||||
}
|
||||
_initDiContainer();
|
||||
await _initDiContainer(isolateType);
|
||||
_initVisibilityDetector();
|
||||
|
||||
_hasInitedInThisIsolate = true;
|
||||
|
@ -99,6 +108,10 @@ void initLog() {
|
|||
});
|
||||
}
|
||||
|
||||
void _initDrift() {
|
||||
driftRuntimeOptions.debugPrint = _debugPrintSql;
|
||||
}
|
||||
|
||||
Future<void> _initPref() async {
|
||||
final provider = PrefSharedPreferencesProvider();
|
||||
await provider.init();
|
||||
|
@ -146,31 +159,56 @@ void _initSelfSignedCertManager() {
|
|||
SelfSignedCertManager().init();
|
||||
}
|
||||
|
||||
void _initDiContainer() {
|
||||
LocalFileRepo? localFileRepo;
|
||||
Future<void> _initDiContainer(InitIsolateType isolateType) async {
|
||||
final c = DiContainer.late();
|
||||
c.pref = Pref();
|
||||
c.sqliteDb = await _createDb(isolateType);
|
||||
|
||||
c.albumRepo = AlbumRepo(AlbumCachedDataSource(c));
|
||||
c.albumRepoLocal = AlbumRepo(AlbumSqliteDbDataSource(c));
|
||||
c.faceRepo = const FaceRepo(FaceRemoteDataSource());
|
||||
c.fileRepo = FileRepo(FileCachedDataSource(c));
|
||||
c.fileRepoRemote = const FileRepo(FileWebdavDataSource());
|
||||
c.fileRepoLocal = FileRepo(FileSqliteDbDataSource(c));
|
||||
c.personRepo = const PersonRepo(PersonRemoteDataSource());
|
||||
c.shareRepo = ShareRepo(ShareRemoteDataSource());
|
||||
c.shareeRepo = ShareeRepo(ShareeRemoteDataSource());
|
||||
c.favoriteRepo = const FavoriteRepo(FavoriteRemoteDataSource());
|
||||
c.tagRepo = const TagRepo(TagRemoteDataSource());
|
||||
c.taggedFileRepo = const TaggedFileRepo(TaggedFileRemoteDataSource());
|
||||
|
||||
if (platform_k.isAndroid) {
|
||||
// local file currently only supported on Android
|
||||
localFileRepo = const LocalFileRepo(LocalFileMediaStoreDataSource());
|
||||
c.localFileRepo = const LocalFileRepo(LocalFileMediaStoreDataSource());
|
||||
}
|
||||
KiwiContainer().registerInstance<DiContainer>(DiContainer(
|
||||
albumRepo: AlbumRepo(AlbumCachedDataSource(AppDb())),
|
||||
faceRepo: const FaceRepo(FaceRemoteDataSource()),
|
||||
fileRepo: FileRepo(FileCachedDataSource(AppDb())),
|
||||
personRepo: const PersonRepo(PersonRemoteDataSource()),
|
||||
shareRepo: ShareRepo(ShareRemoteDataSource()),
|
||||
shareeRepo: ShareeRepo(ShareeRemoteDataSource()),
|
||||
favoriteRepo: const FavoriteRepo(FavoriteRemoteDataSource()),
|
||||
tagRepo: const TagRepo(TagRemoteDataSource()),
|
||||
taggedFileRepo: const TaggedFileRepo(TaggedFileRemoteDataSource()),
|
||||
localFileRepo: localFileRepo,
|
||||
appDb: AppDb(),
|
||||
pref: Pref(),
|
||||
));
|
||||
|
||||
KiwiContainer().registerInstance<DiContainer>(c);
|
||||
}
|
||||
|
||||
void _initVisibilityDetector() {
|
||||
VisibilityDetectorController.instance.updateInterval = Duration.zero;
|
||||
}
|
||||
|
||||
Future<sql.SqliteDb> _createDb(InitIsolateType isolateType) async {
|
||||
switch (isolateType) {
|
||||
case InitIsolateType.main:
|
||||
// use driftIsolate to prevent DB blocking the UI thread
|
||||
if (platform_k.isWeb) {
|
||||
// no isolate support on web
|
||||
return sql.SqliteDb();
|
||||
} else {
|
||||
return sql_isolate.createDb();
|
||||
}
|
||||
|
||||
case InitIsolateType.service:
|
||||
// service already runs in an isolate
|
||||
return sql.SqliteDb();
|
||||
}
|
||||
}
|
||||
|
||||
void _debugPrintSql(String log) {
|
||||
_log.finer(log);
|
||||
}
|
||||
|
||||
final _log = Logger("app_init");
|
||||
var _hasInitedInThisIsolate = false;
|
||||
|
|
|
@ -5,8 +5,6 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.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;
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
|
@ -153,8 +151,8 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
|
|||
_log.info("[of] New bloc instance for account: $account");
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final offlineC = c.copyWith(
|
||||
fileRepo: OrNull(FileRepo(FileAppDbDataSource(c.appDb))),
|
||||
albumRepo: OrNull(AlbumRepo(AlbumAppDbDataSource(c.appDb))),
|
||||
fileRepo: OrNull(c.fileRepoLocal),
|
||||
albumRepo: OrNull(c.albumRepoLocal),
|
||||
);
|
||||
final bloc = ListAlbumBloc(c, offlineC);
|
||||
KiwiContainer().registerInstance<ListAlbumBloc>(bloc, name: name);
|
||||
|
|
|
@ -3,8 +3,8 @@ import 'package:kiwi/kiwi.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
|
||||
import 'package:nc_photos/di_container.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;
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/throttler.dart';
|
||||
|
@ -175,9 +175,9 @@ class LsTrashbinBloc extends Bloc<LsTrashbinBlocEvent, LsTrashbinBlocState> {
|
|||
}
|
||||
|
||||
Future<List<File>> _query(LsTrashbinBlocQuery ev) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
// caching contents in trashbin doesn't sounds useful
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
final files = await LsTrashbin(fileRepo)(ev.account);
|
||||
final files = await LsTrashbin(c.fileRepoRemote)(ev.account);
|
||||
return files.where((f) => file_util.isSupportedFormat(f)).toList();
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import 'package:collection/collection.dart';
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
|
@ -108,6 +107,11 @@ class ScanAccountDirBlocInconsistent extends ScanAccountDirBlocState {
|
|||
class ScanAccountDirBloc
|
||||
extends Bloc<ScanAccountDirBlocEvent, ScanAccountDirBlocState> {
|
||||
ScanAccountDirBloc._(this.account) : super(const ScanAccountDirBlocInit()) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(require(c));
|
||||
assert(ScanDirOffline.require(c));
|
||||
_c = c;
|
||||
|
||||
_fileRemovedEventListener.begin();
|
||||
_filePropertyUpdatedEventListener.begin();
|
||||
_fileTrashbinRestoredEventListener.begin();
|
||||
|
@ -132,6 +136,8 @@ class ScanAccountDirBloc
|
|||
}));
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.fileRepo);
|
||||
|
||||
static ScanAccountDirBloc of(Account account) {
|
||||
final name =
|
||||
bloc_util.getInstNameForRootAwareAccount("ScanAccountDirBloc", account);
|
||||
|
@ -317,12 +323,11 @@ class ScanAccountDirBloc
|
|||
}
|
||||
|
||||
Future<List<File>> _queryOffline(ScanAccountDirBlocQueryBase ev) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final files = <File>[];
|
||||
for (final r in account.roots) {
|
||||
try {
|
||||
final dir = File(path: file_util.unstripPath(account, r));
|
||||
files.addAll(await ScanDirOffline(c)(account, dir,
|
||||
files.addAll(await ScanDirOffline(_c)(account, dir,
|
||||
isOnlySupportedFormat: false));
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
|
@ -341,11 +346,12 @@ class ScanAccountDirBloc
|
|||
final cacheMap = FileForwardCacheManager.prepareFileMap(cache);
|
||||
{
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final fileRepo = FileRepo(FileCachedDataSource(AppDb(),
|
||||
forwardCacheManager: FileForwardCacheManager(AppDb(), cacheMap)));
|
||||
final fileRepoNoCache = FileRepo(FileCachedDataSource(AppDb()));
|
||||
final fileRepo = FileRepo(FileCachedDataSource(
|
||||
_c,
|
||||
forwardCacheManager: FileForwardCacheManager(_c, cacheMap),
|
||||
));
|
||||
await for (final event in _queryWithFileRepo(fileRepo, ev,
|
||||
fileRepoForShareDir: fileRepoNoCache)) {
|
||||
fileRepoForShareDir: _c.fileRepo)) {
|
||||
if (event is ExceptionEvent) {
|
||||
_log.shout("[_queryOnline] Exception while request (1st pass)",
|
||||
event.error, event.stackTrace);
|
||||
|
@ -378,7 +384,8 @@ class ScanAccountDirBloc
|
|||
emit(ScanAccountDirBlocLoading(files));
|
||||
}
|
||||
|
||||
files = await _queryOnlinePass2(ev, cacheMap, files);
|
||||
// files = await _queryOnlinePass2(ev, cacheMap, files);
|
||||
files = await _queryOnlinePass2(ev, {}, files);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
|
@ -394,9 +401,11 @@ class ScanAccountDirBloc
|
|||
// files
|
||||
final pass2CacheMap = CombinedMapView(
|
||||
[FileForwardCacheManager.prepareFileMap(pass1Files), cacheMap]);
|
||||
final fileRepo = FileRepo(FileCachedDataSource(AppDb(),
|
||||
shouldCheckCache: true,
|
||||
forwardCacheManager: FileForwardCacheManager(AppDb(), pass2CacheMap)));
|
||||
final fileRepo = FileRepo(FileCachedDataSource(
|
||||
_c,
|
||||
shouldCheckCache: true,
|
||||
forwardCacheManager: FileForwardCacheManager(_c, pass2CacheMap),
|
||||
));
|
||||
final remoteTouchEtag =
|
||||
await touchTokenManager.getRemoteRootEtag(fileRepo, account);
|
||||
if (remoteTouchEtag == null) {
|
||||
|
@ -412,7 +421,7 @@ class ScanAccountDirBloc
|
|||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final fileRepoNoCache =
|
||||
FileRepo(FileCachedDataSource(AppDb(), shouldCheckCache: true));
|
||||
FileRepo(FileCachedDataSource(_c, shouldCheckCache: true));
|
||||
final newFiles = <File>[];
|
||||
await for (final event in _queryWithFileRepo(fileRepo, ev,
|
||||
fileRepoForShareDir: fileRepoNoCache)) {
|
||||
|
@ -489,6 +498,8 @@ class ScanAccountDirBloc
|
|||
return false;
|
||||
}
|
||||
|
||||
late final DiContainer _c;
|
||||
|
||||
final Account account;
|
||||
|
||||
late final _fileRemovedEventListener =
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/face.dart';
|
||||
import 'package:nc_photos/entity/favorite.dart';
|
||||
|
@ -7,6 +6,7 @@ import 'package:nc_photos/entity/local_file.dart';
|
|||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table.dart' as sql;
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/entity/tagged_file.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
|
@ -14,8 +14,11 @@ import 'package:nc_photos/pref.dart';
|
|||
|
||||
enum DiType {
|
||||
albumRepo,
|
||||
albumRepoLocal,
|
||||
faceRepo,
|
||||
fileRepo,
|
||||
fileRepoRemote,
|
||||
fileRepoLocal,
|
||||
personRepo,
|
||||
shareRepo,
|
||||
shareeRepo,
|
||||
|
@ -23,15 +26,18 @@ enum DiType {
|
|||
tagRepo,
|
||||
taggedFileRepo,
|
||||
localFileRepo,
|
||||
appDb,
|
||||
pref,
|
||||
sqliteDb,
|
||||
}
|
||||
|
||||
class DiContainer {
|
||||
const DiContainer({
|
||||
DiContainer({
|
||||
AlbumRepo? albumRepo,
|
||||
AlbumRepo? albumRepoLocal,
|
||||
FaceRepo? faceRepo,
|
||||
FileRepo? fileRepo,
|
||||
FileRepo? fileRepoRemote,
|
||||
FileRepo? fileRepoLocal,
|
||||
PersonRepo? personRepo,
|
||||
ShareRepo? shareRepo,
|
||||
ShareeRepo? shareeRepo,
|
||||
|
@ -39,11 +45,14 @@ class DiContainer {
|
|||
TagRepo? tagRepo,
|
||||
TaggedFileRepo? taggedFileRepo,
|
||||
LocalFileRepo? localFileRepo,
|
||||
AppDb? appDb,
|
||||
Pref? pref,
|
||||
sql.SqliteDb? sqliteDb,
|
||||
}) : _albumRepo = albumRepo,
|
||||
_albumRepoLocal = albumRepoLocal,
|
||||
_faceRepo = faceRepo,
|
||||
_fileRepo = fileRepo,
|
||||
_fileRepoRemote = fileRepoRemote,
|
||||
_fileRepoLocal = fileRepoLocal,
|
||||
_personRepo = personRepo,
|
||||
_shareRepo = shareRepo,
|
||||
_shareeRepo = shareeRepo,
|
||||
|
@ -51,17 +60,25 @@ class DiContainer {
|
|||
_tagRepo = tagRepo,
|
||||
_taggedFileRepo = taggedFileRepo,
|
||||
_localFileRepo = localFileRepo,
|
||||
_appDb = appDb,
|
||||
_pref = pref;
|
||||
_pref = pref,
|
||||
_sqliteDb = sqliteDb;
|
||||
|
||||
DiContainer.late();
|
||||
|
||||
static bool has(DiContainer contianer, DiType type) {
|
||||
switch (type) {
|
||||
case DiType.albumRepo:
|
||||
return contianer._albumRepo != null;
|
||||
case DiType.albumRepoLocal:
|
||||
return contianer._albumRepoLocal != null;
|
||||
case DiType.faceRepo:
|
||||
return contianer._faceRepo != null;
|
||||
case DiType.fileRepo:
|
||||
return contianer._fileRepo != null;
|
||||
case DiType.fileRepoRemote:
|
||||
return contianer._fileRepoRemote != null;
|
||||
case DiType.fileRepoLocal:
|
||||
return contianer._fileRepoLocal != null;
|
||||
case DiType.personRepo:
|
||||
return contianer._personRepo != null;
|
||||
case DiType.shareRepo:
|
||||
|
@ -76,17 +93,20 @@ class DiContainer {
|
|||
return contianer._taggedFileRepo != null;
|
||||
case DiType.localFileRepo:
|
||||
return contianer._localFileRepo != null;
|
||||
case DiType.appDb:
|
||||
return contianer._appDb != null;
|
||||
case DiType.pref:
|
||||
return contianer._pref != null;
|
||||
case DiType.sqliteDb:
|
||||
return contianer._sqliteDb != null;
|
||||
}
|
||||
}
|
||||
|
||||
DiContainer copyWith({
|
||||
OrNull<AlbumRepo>? albumRepo,
|
||||
OrNull<AlbumRepo>? albumRepoLocal,
|
||||
OrNull<FaceRepo>? faceRepo,
|
||||
OrNull<FileRepo>? fileRepo,
|
||||
OrNull<FileRepo>? fileRepoRemote,
|
||||
OrNull<FileRepo>? fileRepoLocal,
|
||||
OrNull<PersonRepo>? personRepo,
|
||||
OrNull<ShareRepo>? shareRepo,
|
||||
OrNull<ShareeRepo>? shareeRepo,
|
||||
|
@ -94,13 +114,18 @@ class DiContainer {
|
|||
OrNull<TagRepo>? tagRepo,
|
||||
OrNull<TaggedFileRepo>? taggedFileRepo,
|
||||
OrNull<LocalFileRepo>? localFileRepo,
|
||||
OrNull<AppDb>? appDb,
|
||||
OrNull<Pref>? pref,
|
||||
OrNull<sql.SqliteDb>? sqliteDb,
|
||||
}) {
|
||||
return DiContainer(
|
||||
albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj,
|
||||
albumRepoLocal:
|
||||
albumRepoLocal == null ? _albumRepoLocal : albumRepoLocal.obj,
|
||||
faceRepo: faceRepo == null ? _faceRepo : faceRepo.obj,
|
||||
fileRepo: fileRepo == null ? _fileRepo : fileRepo.obj,
|
||||
fileRepoRemote:
|
||||
fileRepoRemote == null ? _fileRepoRemote : fileRepoRemote.obj,
|
||||
fileRepoLocal: fileRepoLocal == null ? _fileRepoLocal : fileRepoLocal.obj,
|
||||
personRepo: personRepo == null ? _personRepo : personRepo.obj,
|
||||
shareRepo: shareRepo == null ? _shareRepo : shareRepo.obj,
|
||||
shareeRepo: shareeRepo == null ? _shareeRepo : shareeRepo.obj,
|
||||
|
@ -109,14 +134,17 @@ class DiContainer {
|
|||
taggedFileRepo:
|
||||
taggedFileRepo == null ? _taggedFileRepo : taggedFileRepo.obj,
|
||||
localFileRepo: localFileRepo == null ? _localFileRepo : localFileRepo.obj,
|
||||
appDb: appDb == null ? _appDb : appDb.obj,
|
||||
pref: pref == null ? _pref : pref.obj,
|
||||
sqliteDb: sqliteDb == null ? _sqliteDb : sqliteDb.obj,
|
||||
);
|
||||
}
|
||||
|
||||
AlbumRepo get albumRepo => _albumRepo!;
|
||||
AlbumRepo get albumRepoLocal => _albumRepoLocal!;
|
||||
FaceRepo get faceRepo => _faceRepo!;
|
||||
FileRepo get fileRepo => _fileRepo!;
|
||||
FileRepo get fileRepoRemote => _fileRepoRemote!;
|
||||
FileRepo get fileRepoLocal => _fileRepoLocal!;
|
||||
PersonRepo get personRepo => _personRepo!;
|
||||
ShareRepo get shareRepo => _shareRepo!;
|
||||
ShareeRepo get shareeRepo => _shareeRepo!;
|
||||
|
@ -125,20 +153,101 @@ class DiContainer {
|
|||
TaggedFileRepo get taggedFileRepo => _taggedFileRepo!;
|
||||
LocalFileRepo get localFileRepo => _localFileRepo!;
|
||||
|
||||
AppDb get appDb => _appDb!;
|
||||
sql.SqliteDb get sqliteDb => _sqliteDb!;
|
||||
Pref get pref => _pref!;
|
||||
|
||||
final AlbumRepo? _albumRepo;
|
||||
final FaceRepo? _faceRepo;
|
||||
final FileRepo? _fileRepo;
|
||||
final PersonRepo? _personRepo;
|
||||
final ShareRepo? _shareRepo;
|
||||
final ShareeRepo? _shareeRepo;
|
||||
final FavoriteRepo? _favoriteRepo;
|
||||
final TagRepo? _tagRepo;
|
||||
final TaggedFileRepo? _taggedFileRepo;
|
||||
final LocalFileRepo? _localFileRepo;
|
||||
set albumRepo(AlbumRepo v) {
|
||||
assert(_albumRepo == null);
|
||||
_albumRepo = v;
|
||||
}
|
||||
|
||||
final AppDb? _appDb;
|
||||
final Pref? _pref;
|
||||
set albumRepoLocal(AlbumRepo v) {
|
||||
assert(_albumRepoLocal == null);
|
||||
_albumRepoLocal = v;
|
||||
}
|
||||
|
||||
set faceRepo(FaceRepo v) {
|
||||
assert(_faceRepo == null);
|
||||
_faceRepo = v;
|
||||
}
|
||||
|
||||
set fileRepo(FileRepo v) {
|
||||
assert(_fileRepo == null);
|
||||
_fileRepo = v;
|
||||
}
|
||||
|
||||
set fileRepoRemote(FileRepo v) {
|
||||
assert(_fileRepoRemote == null);
|
||||
_fileRepoRemote = v;
|
||||
}
|
||||
|
||||
set fileRepoLocal(FileRepo v) {
|
||||
assert(_fileRepoLocal == null);
|
||||
_fileRepoLocal = v;
|
||||
}
|
||||
|
||||
set personRepo(PersonRepo v) {
|
||||
assert(_personRepo == null);
|
||||
_personRepo = v;
|
||||
}
|
||||
|
||||
set shareRepo(ShareRepo v) {
|
||||
assert(_shareRepo == null);
|
||||
_shareRepo = v;
|
||||
}
|
||||
|
||||
set shareeRepo(ShareeRepo v) {
|
||||
assert(_shareeRepo == null);
|
||||
_shareeRepo = v;
|
||||
}
|
||||
|
||||
set favoriteRepo(FavoriteRepo v) {
|
||||
assert(_favoriteRepo == null);
|
||||
_favoriteRepo = v;
|
||||
}
|
||||
|
||||
set tagRepo(TagRepo v) {
|
||||
assert(_tagRepo == null);
|
||||
_tagRepo = v;
|
||||
}
|
||||
|
||||
set taggedFileRepo(TaggedFileRepo v) {
|
||||
assert(_taggedFileRepo == null);
|
||||
_taggedFileRepo = v;
|
||||
}
|
||||
|
||||
set localFileRepo(LocalFileRepo v) {
|
||||
assert(_localFileRepo == null);
|
||||
_localFileRepo = v;
|
||||
}
|
||||
|
||||
set sqliteDb(sql.SqliteDb v) {
|
||||
assert(_sqliteDb == null);
|
||||
_sqliteDb = v;
|
||||
}
|
||||
|
||||
set pref(Pref v) {
|
||||
assert(_pref == null);
|
||||
_pref = v;
|
||||
}
|
||||
|
||||
AlbumRepo? _albumRepo;
|
||||
// Explicitly request a AlbumRepo backed by local source
|
||||
AlbumRepo? _albumRepoLocal;
|
||||
FaceRepo? _faceRepo;
|
||||
FileRepo? _fileRepo;
|
||||
// Explicitly request a FileRepo backed by remote source
|
||||
FileRepo? _fileRepoRemote;
|
||||
// Explicitly request a FileRepo backed by local source
|
||||
FileRepo? _fileRepoLocal;
|
||||
PersonRepo? _personRepo;
|
||||
ShareRepo? _shareRepo;
|
||||
ShareeRepo? _shareeRepo;
|
||||
FavoriteRepo? _favoriteRepo;
|
||||
TagRepo? _tagRepo;
|
||||
TaggedFileRepo? _taggedFileRepo;
|
||||
LocalFileRepo? _localFileRepo;
|
||||
|
||||
sql.SqliteDb? _sqliteDb;
|
||||
Pref? _pref;
|
||||
}
|
||||
|
|
|
@ -1,33 +1,16 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:idb_sqflite/idb_sqflite.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/ci_string.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||
import 'package:nc_photos/entity/album/upgrader.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/int_util.dart' as int_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/type.dart';
|
||||
import 'package:nc_photos/use_case/get_file_binary.dart';
|
||||
import 'package:nc_photos/use_case/ls_single_file.dart';
|
||||
import 'package:nc_photos/use_case/put_file_binary.dart';
|
||||
import 'package:quiver/iterables.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
/// Immutable object that represents an album
|
||||
class Album with EquatableMixin {
|
||||
|
@ -229,6 +212,8 @@ class Album with EquatableMixin {
|
|||
|
||||
/// versioning of this class, use to upgrade old persisted album
|
||||
static const version = 8;
|
||||
|
||||
static final _log = Logger("entity.album.Album");
|
||||
}
|
||||
|
||||
class AlbumShare with EquatableMixin {
|
||||
|
@ -299,6 +284,10 @@ class AlbumRepo {
|
|||
Future<Album> get(Account account, File albumFile) =>
|
||||
dataSrc.get(account, albumFile);
|
||||
|
||||
/// See [AlbumDataSource.getAll]
|
||||
Stream<dynamic> getAll(Account account, List<File> albumFiles) =>
|
||||
dataSrc.getAll(account, albumFiles);
|
||||
|
||||
/// See [AlbumDataSource.create]
|
||||
Future<Album> create(Account account, Album album) =>
|
||||
dataSrc.create(account, album);
|
||||
|
@ -307,11 +296,6 @@ class AlbumRepo {
|
|||
Future<void> update(Account account, Album album) =>
|
||||
dataSrc.update(account, album);
|
||||
|
||||
/// See [AlbumDataSource.cleanUp]
|
||||
Future<void> cleanUp(
|
||||
Account account, String rootDir, List<File> albumFiles) =>
|
||||
dataSrc.cleanUp(account, rootDir, albumFiles);
|
||||
|
||||
final AlbumDataSource dataSrc;
|
||||
}
|
||||
|
||||
|
@ -319,292 +303,12 @@ abstract class AlbumDataSource {
|
|||
/// Return the album defined by [albumFile]
|
||||
Future<Album> get(Account account, File albumFile);
|
||||
|
||||
/// Emit albums defined by [albumFiles] or ExceptionEvent
|
||||
Stream<dynamic> getAll(Account account, List<File> albumFiles);
|
||||
|
||||
// Create a new album
|
||||
Future<Album> create(Account account, Album album);
|
||||
|
||||
/// Update an album
|
||||
Future<void> update(Account account, Album album);
|
||||
|
||||
/// Clean up cached albums
|
||||
///
|
||||
/// Remove dangling albums in cache not listed in [albumFiles] and located
|
||||
/// inside [rootDir]. Do nothing if this data source does not cache previous
|
||||
/// results
|
||||
Future<void> cleanUp(Account account, String rootDir, List<File> albumFiles);
|
||||
}
|
||||
|
||||
class AlbumRemoteDataSource implements AlbumDataSource {
|
||||
@override
|
||||
get(Account account, File albumFile) async {
|
||||
_log.info("[get] ${albumFile.path}");
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
final data = await GetFileBinary(fileRepo)(account, albumFile);
|
||||
try {
|
||||
return Album.fromJson(
|
||||
jsonDecode(utf8.decode(data)),
|
||||
upgraderFactory: DefaultAlbumUpgraderFactory(
|
||||
account: account,
|
||||
albumFile: albumFile,
|
||||
logFilePath: albumFile.path,
|
||||
),
|
||||
)!
|
||||
.copyWith(
|
||||
lastUpdated: OrNull(null),
|
||||
albumFile: OrNull(albumFile),
|
||||
);
|
||||
} catch (e, stacktrace) {
|
||||
dynamic d = data;
|
||||
try {
|
||||
d = utf8.decode(data);
|
||||
} catch (_) {}
|
||||
_log.severe("[get] Invalid json data: $d", e, stacktrace);
|
||||
throw const FormatException("Invalid album format");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
create(Account account, Album album) async {
|
||||
_log.info("[create]");
|
||||
final fileName = _makeAlbumFileName();
|
||||
final filePath =
|
||||
"${remote_storage_util.getRemoteAlbumsDir(account)}/$fileName";
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
await PutFileBinary(fileRepo)(account, filePath,
|
||||
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())),
|
||||
shouldCreateMissingDir: true);
|
||||
// query album file
|
||||
final newFile = await LsSingleFile(KiwiContainer().resolve<DiContainer>())(
|
||||
account, filePath);
|
||||
return album.copyWith(albumFile: OrNull(newFile));
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
await PutFileBinary(fileRepo)(account, album.albumFile!.path,
|
||||
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())));
|
||||
}
|
||||
|
||||
@override
|
||||
cleanUp(Account account, String rootDir, List<File> albumFiles) async {}
|
||||
|
||||
String _makeAlbumFileName() {
|
||||
// just make up something
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final random = Random().nextInt(0xFFFFFF);
|
||||
return "${timestamp.toRadixString(16)}-${random.toRadixString(16).padLeft(6, '0')}.nc_album.json";
|
||||
}
|
||||
|
||||
static final _log = Logger("entity.album.AlbumRemoteDataSource");
|
||||
}
|
||||
|
||||
class AlbumAppDbDataSource implements AlbumDataSource {
|
||||
const AlbumAppDbDataSource(this.appDb);
|
||||
|
||||
@override
|
||||
get(Account account, File albumFile) {
|
||||
_log.info("[get] ${albumFile.path}");
|
||||
return appDb.use(
|
||||
(db) => db.transaction(AppDb.albumStoreName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final store = transaction.objectStore(AppDb.albumStoreName);
|
||||
final index = store.index(AppDbAlbumEntry.indexName);
|
||||
final path = AppDbAlbumEntry.toPathFromFile(account, albumFile);
|
||||
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
|
||||
final List results = await index.getAll(range);
|
||||
if (results.isNotEmpty == true) {
|
||||
final entries = results.map((e) =>
|
||||
AppDbAlbumEntry.fromJson(e.cast<String, dynamic>(), account));
|
||||
if (entries.length > 1) {
|
||||
final items = entries.map((e) {
|
||||
_log.info("[get] ${e.path}[${e.index}]");
|
||||
return AlbumStaticProvider.of(e.album).items;
|
||||
}).reduce((value, element) => value + element);
|
||||
return entries.first.album.copyWith(
|
||||
lastUpdated: OrNull(null),
|
||||
provider: AlbumStaticProvider.of(entries.first.album).copyWith(
|
||||
items: items,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return entries.first.album;
|
||||
}
|
||||
} else {
|
||||
throw CacheNotFoundException("No entry: $path");
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
create(Account account, Album album) async {
|
||||
_log.info("[create]");
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) {
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
return appDb.use(
|
||||
(db) => db.transaction(AppDb.albumStoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final store = transaction.objectStore(AppDb.albumStoreName);
|
||||
await _cacheAlbum(store, account, album);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
cleanUp(Account account, String rootDir, List<File> albumFiles) async {}
|
||||
|
||||
final AppDb appDb;
|
||||
|
||||
static final _log = Logger("entity.album.AlbumAppDbDataSource");
|
||||
}
|
||||
|
||||
class AlbumCachedDataSource implements AlbumDataSource {
|
||||
AlbumCachedDataSource(this.appDb) : _appDbSrc = AlbumAppDbDataSource(appDb);
|
||||
|
||||
@override
|
||||
get(Account account, File albumFile) async {
|
||||
try {
|
||||
final cache = await _appDbSrc.get(account, albumFile);
|
||||
if (cache.albumFile!.etag?.isNotEmpty == true &&
|
||||
cache.albumFile!.etag == albumFile.etag) {
|
||||
// cache is good
|
||||
_log.fine(
|
||||
"[get] etag matched for ${AppDbAlbumEntry.toPathFromFile(account, albumFile)}");
|
||||
return cache;
|
||||
}
|
||||
_log.info(
|
||||
"[get] Remote content updated for ${AppDbAlbumEntry.toPathFromFile(account, albumFile)}");
|
||||
} on CacheNotFoundException catch (_) {
|
||||
// normal when there's no cache
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout("[get] Cache failure", e, stacktrace);
|
||||
}
|
||||
|
||||
// no cache
|
||||
final remote = await _remoteSrc.get(account, albumFile);
|
||||
await _cacheResult(account, remote);
|
||||
return remote;
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
await _remoteSrc.update(account, album);
|
||||
await _appDbSrc.update(account, album);
|
||||
}
|
||||
|
||||
@override
|
||||
create(Account account, Album album) => _remoteSrc.create(account, album);
|
||||
|
||||
@override
|
||||
cleanUp(Account account, String rootDir, List<File> albumFiles) async {
|
||||
appDb.use(
|
||||
(db) => db.transaction(AppDb.albumStoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final store = transaction.objectStore(AppDb.albumStoreName);
|
||||
final index = store.index(AppDbAlbumEntry.indexName);
|
||||
final rootPath = AppDbAlbumEntry.toPath(account, rootDir);
|
||||
final range = KeyRange.bound(
|
||||
["$rootPath/", 0], ["$rootPath/\uffff", int_util.int32Max]);
|
||||
final danglingKeys = await index
|
||||
// get all albums for this account
|
||||
.openKeyCursor(range: range, autoAdvance: true)
|
||||
.map((cursor) => Tuple2((cursor.key as List)[0], cursor.primaryKey))
|
||||
// and pick the dangling ones
|
||||
.where((pair) => !albumFiles.any((f) =>
|
||||
pair.item1 == AppDbAlbumEntry.toPathFromFile(account, f)))
|
||||
// map to primary keys
|
||||
.map((pair) => pair.item2)
|
||||
.toList();
|
||||
for (final k in danglingKeys) {
|
||||
_log.fine("[cleanUp] Removing albumStore entry: $k");
|
||||
try {
|
||||
await store.delete(k);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[cleanUp] Failed removing albumStore entry", e, stackTrace);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _cacheResult(Account account, Album result) {
|
||||
return appDb.use(
|
||||
(db) => db.transaction(AppDb.albumStoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final store = transaction.objectStore(AppDb.albumStoreName);
|
||||
await _cacheAlbum(store, account, result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
final _remoteSrc = AlbumRemoteDataSource();
|
||||
final AlbumAppDbDataSource _appDbSrc;
|
||||
|
||||
static final _log = Logger("entity.album.AlbumCachedDataSource");
|
||||
}
|
||||
|
||||
Future<void> _cacheAlbum(
|
||||
ObjectStore store, Account account, Album album) async {
|
||||
final index = store.index(AppDbAlbumEntry.indexName);
|
||||
final path = AppDbAlbumEntry.toPathFromFile(account, album.albumFile!);
|
||||
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
|
||||
// count number of entries for this album
|
||||
final count = await index.count(range);
|
||||
|
||||
// cut large album into smaller pieces, needed to workaround Android DB
|
||||
// limitation
|
||||
final entries = <AppDbAlbumEntry>[];
|
||||
if (album.provider is AlbumStaticProvider) {
|
||||
var albumItemLists = partition(
|
||||
AlbumStaticProvider.of(album).items, AppDbAlbumEntry.maxDataSize)
|
||||
.toList();
|
||||
if (albumItemLists.isEmpty) {
|
||||
albumItemLists = [<AlbumItem>[]];
|
||||
}
|
||||
entries.addAll(albumItemLists.withIndex().map((pair) => AppDbAlbumEntry(
|
||||
path,
|
||||
pair.item1,
|
||||
album.copyWith(
|
||||
lastUpdated: OrNull(null),
|
||||
provider: AlbumStaticProvider.of(album).copyWith(
|
||||
items: pair.item2,
|
||||
),
|
||||
))));
|
||||
} else {
|
||||
entries.add(AppDbAlbumEntry(path, 0, album));
|
||||
}
|
||||
|
||||
for (final e in entries) {
|
||||
_log.info("[_cacheAlbum] Caching ${e.path}[${e.index}]");
|
||||
await store.put(e.toJson(),
|
||||
AppDbAlbumEntry.toPrimaryKey(account, e.album.albumFile!, e.index));
|
||||
}
|
||||
|
||||
if (count > entries.length) {
|
||||
// index is 0-based
|
||||
final rmRange =
|
||||
KeyRange.bound([path, entries.length], [path, int_util.int32Max]);
|
||||
final rmKeys = await index
|
||||
.openKeyCursor(range: rmRange, autoAdvance: true)
|
||||
.map((cursor) => cursor.primaryKey)
|
||||
.toList();
|
||||
for (final k in rmKeys) {
|
||||
_log.fine("[_cacheAlbum] Removing albumStore entry: $k");
|
||||
try {
|
||||
await store.delete(k);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_cacheAlbum] Failed removing albumStore entry", e, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final _log = Logger("entity.album");
|
||||
|
|
307
app/lib/entity/album/data_source.dart
Normal file
307
app/lib/entity/album/data_source.dart
Normal file
|
@ -0,0 +1,307 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:drift/drift.dart' as sql;
|
||||
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/album.dart';
|
||||
import 'package:nc_photos/entity/album/upgrader.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite_table_converter.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/exception_event.dart';
|
||||
import 'package:nc_photos/future_util.dart' as future_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/use_case/get_file_binary.dart';
|
||||
import 'package:nc_photos/use_case/ls_single_file.dart';
|
||||
import 'package:nc_photos/use_case/put_file_binary.dart';
|
||||
|
||||
class AlbumRemoteDataSource implements AlbumDataSource {
|
||||
@override
|
||||
get(Account account, File albumFile) async {
|
||||
_log.info("[get] ${albumFile.path}");
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
final data = await GetFileBinary(fileRepo)(account, albumFile);
|
||||
try {
|
||||
return Album.fromJson(
|
||||
jsonDecode(utf8.decode(data)),
|
||||
upgraderFactory: DefaultAlbumUpgraderFactory(
|
||||
account: account,
|
||||
albumFile: albumFile,
|
||||
logFilePath: albumFile.path,
|
||||
),
|
||||
)!
|
||||
.copyWith(
|
||||
lastUpdated: OrNull(null),
|
||||
albumFile: OrNull(albumFile),
|
||||
);
|
||||
} catch (e, stacktrace) {
|
||||
dynamic d = data;
|
||||
try {
|
||||
d = utf8.decode(data);
|
||||
} catch (_) {}
|
||||
_log.severe("[get] Invalid json data: $d", e, stacktrace);
|
||||
throw const FormatException("Invalid album format");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
getAll(Account account, List<File> albumFiles) async* {
|
||||
_log.info(
|
||||
"[getAll] ${albumFiles.map((f) => f.filename).toReadableString()}");
|
||||
final results = await future_util.waitOr(
|
||||
albumFiles.map((f) => get(account, f)),
|
||||
(error, stackTrace) => ExceptionEvent(error, stackTrace),
|
||||
);
|
||||
for (final r in results) {
|
||||
yield r;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
create(Account account, Album album) async {
|
||||
_log.info("[create]");
|
||||
final fileName = _makeAlbumFileName();
|
||||
final filePath =
|
||||
"${remote_storage_util.getRemoteAlbumsDir(account)}/$fileName";
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
await PutFileBinary(fileRepo)(account, filePath,
|
||||
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())),
|
||||
shouldCreateMissingDir: true);
|
||||
// query album file
|
||||
final newFile = await LsSingleFile(KiwiContainer().resolve<DiContainer>())(
|
||||
account, filePath);
|
||||
return album.copyWith(albumFile: OrNull(newFile));
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
await PutFileBinary(fileRepo)(account, album.albumFile!.path,
|
||||
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())));
|
||||
}
|
||||
|
||||
String _makeAlbumFileName() {
|
||||
// just make up something
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final random = Random().nextInt(0xFFFFFF);
|
||||
return "${timestamp.toRadixString(16)}-${random.toRadixString(16).padLeft(6, '0')}.nc_album.json";
|
||||
}
|
||||
|
||||
static final _log = Logger("entity.album.AlbumRemoteDataSource");
|
||||
}
|
||||
|
||||
class AlbumSqliteDbDataSource implements AlbumDataSource {
|
||||
AlbumSqliteDbDataSource(this._c);
|
||||
|
||||
@override
|
||||
get(Account account, File albumFile) async {
|
||||
final results = await getAll(account, [albumFile]).toList();
|
||||
if (results.first is! Album) {
|
||||
throw results.first;
|
||||
} else {
|
||||
return results.first;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
getAll(Account account, List<File> albumFiles) async* {
|
||||
_log.info(
|
||||
"[getAll] ${albumFiles.map((f) => f.filename).toReadableString()}");
|
||||
late final List<sql.CompleteFile> dbFiles;
|
||||
late final List<sql.AlbumWithShare> albumWithShares;
|
||||
await _c.sqliteDb.use((db) async {
|
||||
dbFiles = await db.completeFilesByFileIds(
|
||||
albumFiles.map((f) => f.fileId!),
|
||||
appAccount: account,
|
||||
);
|
||||
final query = db.select(db.albums).join([
|
||||
sql.leftOuterJoin(
|
||||
db.albumShares, db.albumShares.album.equalsExp(db.albums.rowId)),
|
||||
])
|
||||
..where(db.albums.file.isIn(dbFiles.map((f) => f.file.rowId)));
|
||||
albumWithShares = await query
|
||||
.map((r) => sql.AlbumWithShare(
|
||||
r.readTable(db.albums), r.readTableOrNull(db.albumShares)))
|
||||
.get();
|
||||
});
|
||||
|
||||
// group entries together
|
||||
final fileRowIdMap = <int, sql.CompleteFile>{};
|
||||
for (var f in dbFiles) {
|
||||
fileRowIdMap[f.file.rowId] = f;
|
||||
}
|
||||
final fileIdMap = <int, Map>{};
|
||||
for (final s in albumWithShares) {
|
||||
final f = fileRowIdMap[s.album.file];
|
||||
if (f == null) {
|
||||
_log.severe("[getAll] File missing for album (rowId: ${s.album.rowId}");
|
||||
} else {
|
||||
if (!fileIdMap.containsKey(f.file.fileId)) {
|
||||
fileIdMap[f.file.fileId] = {
|
||||
"file": f,
|
||||
"album": s.album,
|
||||
};
|
||||
}
|
||||
if (s.share != null) {
|
||||
(fileIdMap[f.file.fileId]!["shares"] ??= <sql.AlbumShare>[])
|
||||
.add(s.share!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort as the input list
|
||||
for (final item in albumFiles.map((f) => fileIdMap[f.fileId])) {
|
||||
if (item == null) {
|
||||
// cache not found
|
||||
yield CacheNotFoundException();
|
||||
} else {
|
||||
try {
|
||||
final f = SqliteFileConverter.fromSql(
|
||||
account.homeDir.toString(), item["file"]);
|
||||
yield SqliteAlbumConverter.fromSql(
|
||||
item["album"], f, item["shares"] ?? []);
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe(
|
||||
"[getAll] Failed while converting DB entry", e, stackTrace);
|
||||
yield ExceptionEvent(e, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
create(Account account, Album album) async {
|
||||
_log.info("[create]");
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
await _c.sqliteDb.use((db) async {
|
||||
final rowIds =
|
||||
await db.accountFileRowIdsOf(album.albumFile!, appAccount: account);
|
||||
final insert = SqliteAlbumConverter.toSql(album, rowIds.fileRowId);
|
||||
var rowId = await _updateCache(db, rowIds.fileRowId, insert.album);
|
||||
if (rowId == null) {
|
||||
// new album, need insert
|
||||
_log.info("[update] Insert new album");
|
||||
final insertedAlbum =
|
||||
await db.into(db.albums).insertReturning(insert.album);
|
||||
rowId = insertedAlbum.rowId;
|
||||
} else {
|
||||
await (db.delete(db.albumShares)..where((t) => t.album.equals(rowId)))
|
||||
.go();
|
||||
}
|
||||
if (insert.albumShares.isNotEmpty) {
|
||||
await db.batch((batch) {
|
||||
batch.insertAll(
|
||||
db.albumShares,
|
||||
insert.albumShares.map((s) => s.copyWith(album: sql.Value(rowId!))),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<int?> _updateCache(
|
||||
sql.SqliteDb db, int dbFileRowId, sql.AlbumsCompanion dbAlbum) async {
|
||||
final rowIdQuery = db.selectOnly(db.albums)
|
||||
..addColumns([db.albums.rowId])
|
||||
..where(db.albums.file.equals(dbFileRowId))
|
||||
..limit(1);
|
||||
final rowId =
|
||||
await rowIdQuery.map((r) => r.read(db.albums.rowId)!).getSingleOrNull();
|
||||
if (rowId == null) {
|
||||
// new album
|
||||
return null;
|
||||
}
|
||||
|
||||
await (db.update(db.albums)..where((t) => t.rowId.equals(rowId)))
|
||||
.write(dbAlbum);
|
||||
return rowId;
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
static final _log = Logger("entity.album.AlbumSqliteDbDataSource");
|
||||
}
|
||||
|
||||
class AlbumCachedDataSource implements AlbumDataSource {
|
||||
AlbumCachedDataSource(DiContainer c)
|
||||
: _sqliteDbSrc = AlbumSqliteDbDataSource(c);
|
||||
|
||||
@override
|
||||
get(Account account, File albumFile) async {
|
||||
final result = await getAll(account, [albumFile]).first;
|
||||
return result as Album;
|
||||
}
|
||||
|
||||
@override
|
||||
getAll(Account account, List<File> albumFiles) async* {
|
||||
var i = 0;
|
||||
await for (final cache in _sqliteDbSrc.getAll(account, albumFiles)) {
|
||||
final albumFile = albumFiles[i++];
|
||||
if (_validateCache(cache, albumFile)) {
|
||||
yield cache;
|
||||
} else {
|
||||
// no cache
|
||||
final remote = await _remoteSrc.get(account, albumFile);
|
||||
await _cacheResult(account, remote);
|
||||
yield remote;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
await _remoteSrc.update(account, album);
|
||||
await _sqliteDbSrc.update(account, album);
|
||||
}
|
||||
|
||||
@override
|
||||
create(Account account, Album album) => _remoteSrc.create(account, album);
|
||||
|
||||
Future<void> _cacheResult(Account account, Album result) {
|
||||
return _sqliteDbSrc.update(account, result);
|
||||
}
|
||||
|
||||
bool _validateCache(dynamic cache, File albumFile) {
|
||||
if (cache is Album) {
|
||||
if (cache.albumFile!.etag?.isNotEmpty == true &&
|
||||
cache.albumFile!.etag == albumFile.etag) {
|
||||
// cache is good
|
||||
_log.fine("[_validateCache] etag matched for ${albumFile.path}");
|
||||
return true;
|
||||
} else {
|
||||
_log.info(
|
||||
"[_validateCache] Remote content updated for ${albumFile.path}");
|
||||
return false;
|
||||
}
|
||||
} else if (cache is CacheNotFoundException) {
|
||||
// normal when there's no cache
|
||||
return false;
|
||||
} else if (cache is ExceptionEvent) {
|
||||
_log.shout(
|
||||
"[_validateCache] Cache failure", cache.error, cache.stackTrace);
|
||||
return false;
|
||||
} else {
|
||||
_log.shout("[_validateCache] Unknown type: ${cache.runtimeType}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final _remoteSrc = AlbumRemoteDataSource();
|
||||
final AlbumSqliteDbDataSource _sqliteDbSrc;
|
||||
|
||||
static final _log = Logger("entity.album.AlbumCachedDataSource");
|
||||
}
|
|
@ -379,7 +379,7 @@ class File with EquatableMixin {
|
|||
String? path,
|
||||
int? contentLength,
|
||||
String? contentType,
|
||||
String? etag,
|
||||
OrNull<String>? etag,
|
||||
DateTime? lastModified,
|
||||
bool? isCollection,
|
||||
int? usedBytes,
|
||||
|
@ -398,7 +398,7 @@ class File with EquatableMixin {
|
|||
path: path ?? this.path,
|
||||
contentLength: contentLength ?? this.contentLength,
|
||||
contentType: contentType ?? this.contentType,
|
||||
etag: etag ?? this.etag,
|
||||
etag: etag == null ? this.etag : etag.obj,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
isCollection: isCollection ?? this.isCollection,
|
||||
usedBytes: usedBytes ?? this.usedBytes,
|
||||
|
@ -447,7 +447,6 @@ class File with EquatableMixin {
|
|||
final bool? isCollection;
|
||||
final int? usedBytes;
|
||||
final bool? hasPreview;
|
||||
// maybe null when loaded from old cache
|
||||
final int? fileId;
|
||||
final bool? isFavorite;
|
||||
final CiString? ownerId;
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:drift/drift.dart' as sql;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/file_cache_manager.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
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/entity/webdav_response_parser.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
|
@ -18,7 +20,6 @@ import 'package:nc_photos/or_null.dart';
|
|||
import 'package:nc_photos/touch_token_manager.dart';
|
||||
import 'package:nc_photos/use_case/compat/v32.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
|
@ -265,55 +266,38 @@ class FileWebdavDataSource implements FileDataSource {
|
|||
static final _log = Logger("entity.file.data_source.FileWebdavDataSource");
|
||||
}
|
||||
|
||||
class FileAppDbDataSource implements FileDataSource {
|
||||
const FileAppDbDataSource(this.appDb);
|
||||
class FileSqliteDbDataSource implements FileDataSource {
|
||||
FileSqliteDbDataSource(this._c);
|
||||
|
||||
@override
|
||||
list(Account account, File dir) async {
|
||||
_log.info("[list] ${dir.path}");
|
||||
final dbItems = await appDb.use(
|
||||
(db) => db.transaction(
|
||||
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
final dirItem = await dirStore
|
||||
.getObject(AppDbDirEntry.toPrimaryKeyForDir(account, dir)) as Map?;
|
||||
if (dirItem == null) {
|
||||
throw CacheNotFoundException("No entry: ${dir.path}");
|
||||
}
|
||||
final dirEntry =
|
||||
AppDbDirEntry.fromJson(dirItem.cast<String, dynamic>());
|
||||
return Tuple2(
|
||||
dirEntry.dir,
|
||||
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");
|
||||
} else {
|
||||
return fileItem;
|
||||
}
|
||||
}),
|
||||
eagerError: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
// we need to add dir to match the remote query
|
||||
return [
|
||||
dbItems.item1,
|
||||
...(await dbItems.item2.computeAll(_covertAppDbFile2Entry))
|
||||
.where((f) => _validateFile(f))
|
||||
];
|
||||
final dbFiles = await _c.sqliteDb.use((db) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
final sql.File dbDir;
|
||||
try {
|
||||
dbDir = await db.fileOf(dir, sqlAccount: dbAccount);
|
||||
} catch (_) {
|
||||
throw CacheNotFoundException("No entry: ${dir.path}");
|
||||
}
|
||||
return await db.completeFilesByDirRowId(dbDir.rowId,
|
||||
sqlAccount: dbAccount);
|
||||
});
|
||||
final results = (await dbFiles.convertToAppFile(account))
|
||||
.where((f) => _validateFile(f))
|
||||
.toList();
|
||||
_log.fine("[list] Queried ${results.length} files");
|
||||
if (results.isEmpty) {
|
||||
// each dir will at least contain its own entry, so an empty list here
|
||||
// means that the dir has not been queried before
|
||||
throw CacheNotFoundException("No entry: ${dir.path}");
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@override
|
||||
listSingle(Account account, File f) {
|
||||
_log.info("[listSingle] ${f.path}");
|
||||
_log.severe("[listSingle] ${f.path}");
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
@ -322,39 +306,41 @@ class FileAppDbDataSource implements FileDataSource {
|
|||
Future<List<File>> listByDate(
|
||||
Account account, int fromEpochMs, int toEpochMs) async {
|
||||
_log.info("[listByDate] [$fromEpochMs, $toEpochMs]");
|
||||
final items = await appDb.use(
|
||||
(db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
final dateTimeEpochMsIndex =
|
||||
fileStore.index(AppDbFile2Entry.dateTimeEpochMsIndexName);
|
||||
final range = KeyRange.bound(
|
||||
AppDbFile2Entry.toDateTimeEpochMsIndexKey(account, fromEpochMs),
|
||||
AppDbFile2Entry.toDateTimeEpochMsIndexKey(account, toEpochMs),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return await dateTimeEpochMsIndex.getAll(range);
|
||||
},
|
||||
);
|
||||
return items
|
||||
.cast<Map>()
|
||||
.map((i) => AppDbFile2Entry.fromJson(i.cast<String, dynamic>()))
|
||||
.map((e) => e.file)
|
||||
.where((f) => _validateFile(f))
|
||||
.toList();
|
||||
final dbFiles = await _c.sqliteDb.use((db) async {
|
||||
final dateTime = sql.coalesce([
|
||||
db.accountFiles.overrideDateTime,
|
||||
db.images.dateTimeOriginal,
|
||||
db.files.lastModified,
|
||||
]).secondsSinceEpoch;
|
||||
final queryBuilder = db.queryFiles()
|
||||
..setQueryMode(sql.FilesQueryMode.completeFile)
|
||||
..setAppAccount(account);
|
||||
final query = queryBuilder.build();
|
||||
query
|
||||
..where(dateTime.isBetweenValues(
|
||||
fromEpochMs ~/ 1000, (toEpochMs ~/ 1000) - 1))
|
||||
..orderBy([sql.OrderingTerm.desc(dateTime)]);
|
||||
return await query
|
||||
.map((r) => sql.CompleteFile(
|
||||
r.readTable(db.files),
|
||||
r.readTable(db.accountFiles),
|
||||
r.readTableOrNull(db.images),
|
||||
r.readTableOrNull(db.trashes),
|
||||
))
|
||||
.get();
|
||||
});
|
||||
return await dbFiles.convertToAppFile(account);
|
||||
}
|
||||
|
||||
/// Remove a file/dir from database
|
||||
@override
|
||||
remove(Account account, File f) {
|
||||
_log.info("[remove] ${f.path}");
|
||||
return FileCacheRemover(appDb)(account, f);
|
||||
return FileSqliteCacheRemover(_c)(account, f);
|
||||
}
|
||||
|
||||
@override
|
||||
getBinary(Account account, File f) {
|
||||
_log.info("[getBinary] ${f.path}");
|
||||
_log.severe("[getBinary] ${f.path}");
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
@ -365,30 +351,51 @@ class FileAppDbDataSource implements FileDataSource {
|
|||
}
|
||||
|
||||
@override
|
||||
updateProperty(
|
||||
Account account,
|
||||
File f, {
|
||||
OrNull<Metadata>? metadata,
|
||||
OrNull<bool>? isArchived,
|
||||
OrNull<DateTime>? overrideDateTime,
|
||||
bool? favorite,
|
||||
}) {
|
||||
updateProperty(Account account, File f,
|
||||
{OrNull<Metadata>? metadata,
|
||||
OrNull<bool>? isArchived,
|
||||
OrNull<DateTime>? overrideDateTime,
|
||||
bool? favorite}) async {
|
||||
_log.info("[updateProperty] ${f.path}");
|
||||
return appDb.use(
|
||||
(db) => db.transaction(AppDb.file2StoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
// update file store
|
||||
final newFile = f.copyWith(
|
||||
metadata: metadata,
|
||||
isArchived: isArchived,
|
||||
overrideDateTime: overrideDateTime,
|
||||
isFavorite: favorite,
|
||||
await _c.sqliteDb.use((db) async {
|
||||
final rowIds = await db.accountFileRowIdsOf(f, appAccount: account);
|
||||
if (isArchived != null || overrideDateTime != null || favorite != null) {
|
||||
final update = sql.AccountFilesCompanion(
|
||||
isArchived: isArchived == null
|
||||
? const sql.Value.absent()
|
||||
: sql.Value(isArchived.obj),
|
||||
overrideDateTime: overrideDateTime == null
|
||||
? const sql.Value.absent()
|
||||
: sql.Value(overrideDateTime.obj),
|
||||
isFavorite:
|
||||
favorite == null ? const sql.Value.absent() : sql.Value(favorite),
|
||||
);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
await fileStore.put(AppDbFile2Entry.fromFile(account, newFile).toJson(),
|
||||
AppDbFile2Entry.toPrimaryKeyForFile(account, newFile));
|
||||
},
|
||||
);
|
||||
await (db.update(db.accountFiles)
|
||||
..where((t) => t.rowId.equals(rowIds.accountFileRowId)))
|
||||
.write(update);
|
||||
}
|
||||
if (metadata != null) {
|
||||
if (metadata.obj == null) {
|
||||
await (db.delete(db.images)
|
||||
..where((t) => t.accountFile.equals(rowIds.accountFileRowId)))
|
||||
.go();
|
||||
} else {
|
||||
await db
|
||||
.into(db.images)
|
||||
.insertOnConflictUpdate(sql.ImagesCompanion.insert(
|
||||
accountFile: sql.Value(rowIds.accountFileRowId),
|
||||
lastUpdated: metadata.obj!.lastUpdated,
|
||||
fileEtag: sql.Value(metadata.obj!.fileEtag),
|
||||
width: sql.Value(metadata.obj!.imageWidth),
|
||||
height: sql.Value(metadata.obj!.imageHeight),
|
||||
exifRaw: sql.Value(
|
||||
metadata.obj!.exif?.toJson().run((j) => jsonEncode(j))),
|
||||
dateTimeOriginal:
|
||||
sql.Value(metadata.obj!.exif?.dateTimeOriginal),
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -416,23 +423,23 @@ class FileAppDbDataSource implements FileDataSource {
|
|||
// do nothing
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
final DiContainer _c;
|
||||
|
||||
static final _log = Logger("entity.file.data_source.FileAppDbDataSource");
|
||||
static final _log = Logger("entity.file.data_source.FileSqliteDbDataSource");
|
||||
}
|
||||
|
||||
class FileCachedDataSource implements FileDataSource {
|
||||
FileCachedDataSource(
|
||||
this.appDb, {
|
||||
this._c, {
|
||||
this.shouldCheckCache = false,
|
||||
this.forwardCacheManager,
|
||||
}) : _appDbSrc = FileAppDbDataSource(appDb);
|
||||
}) : _sqliteDbSrc = FileSqliteDbDataSource(_c);
|
||||
|
||||
@override
|
||||
list(Account account, File dir) async {
|
||||
final cacheLoader = FileCacheLoader(
|
||||
appDb: appDb,
|
||||
appDbSrc: _appDbSrc,
|
||||
_c,
|
||||
cacheSrc: _sqliteDbSrc,
|
||||
remoteSrc: _remoteSrc,
|
||||
shouldCheckCache: shouldCheckCache,
|
||||
forwardCacheManager: forwardCacheManager,
|
||||
|
@ -445,7 +452,7 @@ class FileCachedDataSource implements FileDataSource {
|
|||
// no cache or outdated
|
||||
try {
|
||||
final remote = await _remoteSrc.list(account, dir);
|
||||
await FileCacheUpdater(appDb)(account, dir, remote: remote, cache: cache);
|
||||
await FileSqliteCacheUpdater(_c)(account, dir, remote: remote);
|
||||
if (shouldCheckCache) {
|
||||
// update our local touch token to match the remote one
|
||||
const tokenManager = TouchTokenManager();
|
||||
|
@ -461,11 +468,15 @@ class FileCachedDataSource implements FileDataSource {
|
|||
} on ApiException catch (e) {
|
||||
if (e.response.statusCode == 404) {
|
||||
_log.info("[list] File removed: $dir");
|
||||
_appDbSrc.remove(account, dir);
|
||||
if (cache != null) {
|
||||
await _sqliteDbSrc.remove(account, dir);
|
||||
}
|
||||
return [];
|
||||
} else if (e.response.statusCode == 403) {
|
||||
_log.info("[list] E2E encrypted dir: $dir");
|
||||
_appDbSrc.remove(account, dir);
|
||||
if (cache != null) {
|
||||
await _sqliteDbSrc.remove(account, dir);
|
||||
}
|
||||
return [];
|
||||
} else {
|
||||
rethrow;
|
||||
|
@ -480,7 +491,7 @@ class FileCachedDataSource implements FileDataSource {
|
|||
|
||||
@override
|
||||
remove(Account account, File f) async {
|
||||
await _appDbSrc.remove(account, f);
|
||||
await _sqliteDbSrc.remove(account, f);
|
||||
await _remoteSrc.remove(account, f);
|
||||
}
|
||||
|
||||
|
@ -503,23 +514,22 @@ class FileCachedDataSource implements FileDataSource {
|
|||
OrNull<DateTime>? overrideDateTime,
|
||||
bool? favorite,
|
||||
}) async {
|
||||
await _remoteSrc
|
||||
.updateProperty(
|
||||
account,
|
||||
f,
|
||||
metadata: metadata,
|
||||
isArchived: isArchived,
|
||||
overrideDateTime: overrideDateTime,
|
||||
favorite: favorite,
|
||||
)
|
||||
.then((_) => _appDbSrc.updateProperty(
|
||||
account,
|
||||
f,
|
||||
metadata: metadata,
|
||||
isArchived: isArchived,
|
||||
overrideDateTime: overrideDateTime,
|
||||
favorite: favorite,
|
||||
));
|
||||
await _remoteSrc.updateProperty(
|
||||
account,
|
||||
f,
|
||||
metadata: metadata,
|
||||
isArchived: isArchived,
|
||||
overrideDateTime: overrideDateTime,
|
||||
favorite: favorite,
|
||||
);
|
||||
await _sqliteDbSrc.updateProperty(
|
||||
account,
|
||||
f,
|
||||
metadata: metadata,
|
||||
isArchived: isArchived,
|
||||
overrideDateTime: overrideDateTime,
|
||||
favorite: favorite,
|
||||
);
|
||||
|
||||
// generate a new random token
|
||||
final token = const Uuid().v4().replaceAll("-", "");
|
||||
|
@ -559,12 +569,12 @@ class FileCachedDataSource implements FileDataSource {
|
|||
await _remoteSrc.createDir(account, path);
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
final DiContainer _c;
|
||||
final bool shouldCheckCache;
|
||||
final FileForwardCacheManager? forwardCacheManager;
|
||||
|
||||
final _remoteSrc = const FileWebdavDataSource();
|
||||
final FileAppDbDataSource _appDbSrc;
|
||||
final FileSqliteDbDataSource _sqliteDbSrc;
|
||||
|
||||
static final _log = Logger("entity.file.data_source.FileCachedDataSource");
|
||||
}
|
||||
|
@ -576,7 +586,11 @@ class FileCachedDataSource implements FileDataSource {
|
|||
/// passed to us in one transaction. For this reason, this should only be used
|
||||
/// when it's necessary to query everything
|
||||
class FileForwardCacheManager {
|
||||
FileForwardCacheManager(this.appDb, this.knownFiles);
|
||||
FileForwardCacheManager(this._c, Map<int, File> knownFiles) {
|
||||
_fileCache.addAll(knownFiles);
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) => true;
|
||||
|
||||
/// Transform a list of files to a map suitable to be passed as the
|
||||
/// [knownFiles] argument
|
||||
|
@ -587,117 +601,155 @@ class FileForwardCacheManager {
|
|||
|
||||
Future<List<File>> list(Account account, File dir) async {
|
||||
// check cache
|
||||
final dirKey = AppDbDirEntry.toPrimaryKeyForDir(account, dir);
|
||||
final cachedDir = _dirCache[dirKey];
|
||||
if (cachedDir != null) {
|
||||
var childFileIds = _dirCache[dir.strippedPathWithEmpty];
|
||||
if (childFileIds == null) {
|
||||
_log.info(
|
||||
"[list] No cache and querying everything under ${logFilename(dir.path)}");
|
||||
await _cacheDir(account, dir);
|
||||
childFileIds = _dirCache[dir.strippedPathWithEmpty];
|
||||
if (childFileIds == null) {
|
||||
throw CacheNotFoundException("No entry: ${dir.path}");
|
||||
}
|
||||
} else {
|
||||
_log.fine("[list] Returning data from cache: ${logFilename(dir.path)}");
|
||||
return _withDirEntry(cachedDir);
|
||||
}
|
||||
// no cache, query everything under [dir]
|
||||
_log.info(
|
||||
"[list] No cache and querying everything under ${logFilename(dir.path)}");
|
||||
await _cacheDir(account, dir);
|
||||
final cachedDir2 = _dirCache[dirKey];
|
||||
if (cachedDir2 == null) {
|
||||
throw CacheNotFoundException("No entry: ${dir.path}");
|
||||
}
|
||||
return _withDirEntry(cachedDir2);
|
||||
return _listByFileIds(dir, childFileIds);
|
||||
}
|
||||
|
||||
Future<void> _cacheDir(Account account, File dir) async {
|
||||
final dirItems = await appDb.use(
|
||||
(db) => db.transaction(AppDb.dirStoreName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final store = transaction.objectStore(AppDb.dirStoreName);
|
||||
final dirItem = await store
|
||||
.getObject(AppDbDirEntry.toPrimaryKeyForDir(account, dir)) as Map?;
|
||||
if (dirItem == null) {
|
||||
return null;
|
||||
}
|
||||
final range = KeyRange.bound(
|
||||
AppDbDirEntry.toPrimaryLowerKeyForSubDirs(account, dir),
|
||||
AppDbDirEntry.toPrimaryUpperKeyForSubDirs(account, dir),
|
||||
);
|
||||
return [dirItem] + (await store.getAll(range)).cast<Map>();
|
||||
},
|
||||
);
|
||||
if (dirItems == null) {
|
||||
await _c.sqliteDb.use((db) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
final dirCache = <String, List<int>>{};
|
||||
await _fillDirCacheForDir(
|
||||
db, dirCache, dbAccount, null, dir.fileId, dir.strippedPathWithEmpty);
|
||||
_log.info(
|
||||
"[_cacheDir] Cached ${dirCache.length} dirs under ${logFilename(dir.path)}");
|
||||
await _fillFileCache(
|
||||
db, account, dbAccount, dirCache.values.flatten().toSet());
|
||||
_dirCache.addAll(dirCache);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _fillDirCacheForDir(
|
||||
sql.SqliteDb db,
|
||||
Map<String, List<int>> dirCache,
|
||||
sql.Account dbAccount,
|
||||
int? dirRowId,
|
||||
int? dirFileId,
|
||||
String dirRelativePath) async {
|
||||
// get rowId
|
||||
final myDirRowId = dirRowId ??
|
||||
await _queryFileIdOfDir(db, dbAccount, dirFileId, dirRelativePath);
|
||||
if (myDirRowId == null) {
|
||||
// no cache
|
||||
return;
|
||||
}
|
||||
final dirs = dirItems
|
||||
.map((i) => AppDbDirEntry.fromJson(i.cast<String, dynamic>()))
|
||||
.toList();
|
||||
_dirCache.addEntries(dirs.map(
|
||||
(e) => MapEntry(AppDbDirEntry.toPrimaryKeyForDir(account, e.dir), e)));
|
||||
_log.info(
|
||||
"[_cacheDir] Cached ${dirs.length} dirs under ${logFilename(dir.path)}");
|
||||
|
||||
// cache files
|
||||
final fileIds = dirs.map((e) => e.children).fold<List<int>>(
|
||||
[], (previousValue, element) => previousValue + element);
|
||||
|
||||
final needQuery = <int>[];
|
||||
final files = <File>[];
|
||||
// check files already known to us
|
||||
if (knownFiles.isNotEmpty) {
|
||||
for (final id in fileIds) {
|
||||
final f = knownFiles[id];
|
||||
if (f != null) {
|
||||
files.add(f);
|
||||
} else {
|
||||
needQuery.add(id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
needQuery.addAll(fileIds);
|
||||
final children = await _queryChildOfDir(db, dbAccount, myDirRowId);
|
||||
if (children.isEmpty) {
|
||||
// no cache
|
||||
return;
|
||||
}
|
||||
_log.info(
|
||||
"[_cacheDir] ${files.length} files known, ${needQuery.length} files need querying");
|
||||
final childFileIds = children.map((c) => c.fileId).toList();
|
||||
dirCache[dirRelativePath] = childFileIds;
|
||||
|
||||
// query other files
|
||||
// recursively fill child dirs
|
||||
for (final c in children
|
||||
.where((c) => c.rowId != myDirRowId && c.isCollection == true)) {
|
||||
await _fillDirCacheForDir(
|
||||
db, dirCache, dbAccount, c.rowId, c.fileId, c.relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fillFileCache(sql.SqliteDb db, Account account,
|
||||
sql.Account dbAccount, Iterable<int> fileIds) async {
|
||||
final needQuery = fileIds.where((id) => !_fileCache.containsKey(id));
|
||||
if (needQuery.isNotEmpty) {
|
||||
final dbItems = await appDb.use(
|
||||
(db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final store = transaction.objectStore(AppDb.file2StoreName);
|
||||
return await Future.wait(needQuery.map((id) =>
|
||||
store.getObject(AppDbFile2Entry.toPrimaryKey(account, id))));
|
||||
},
|
||||
);
|
||||
files.addAll(
|
||||
await dbItems.whereType<Map>().computeAll(_covertAppDbFile2Entry));
|
||||
_log.info("[_fillFileCache] ${needQuery.length} files need querying");
|
||||
final dbFiles =
|
||||
await db.completeFilesByFileIds(needQuery, sqlAccount: dbAccount);
|
||||
for (final f in await dbFiles.convertToAppFile(account)) {
|
||||
_fileCache[f.fileId!] = f;
|
||||
}
|
||||
_log.info("[_fillFileCache] Cached ${dbFiles.length} files");
|
||||
} else {
|
||||
_log.info("[_fillFileCache] 0 files need querying");
|
||||
}
|
||||
_fileCache.addEntries(files.map((f) => MapEntry(f.fileId!, f)));
|
||||
_log.info(
|
||||
"[_cacheDir] Cached ${files.length} files under ${logFilename(dir.path)}");
|
||||
}
|
||||
|
||||
List<File> _withDirEntry(AppDbDirEntry dirEntry) {
|
||||
return [dirEntry.dir] +
|
||||
dirEntry.children.map((id) {
|
||||
try {
|
||||
return _fileCache[id]!;
|
||||
} catch (_) {
|
||||
_log.warning(
|
||||
"[list] Missing file ($id) in db for dir: ${logFilename(dirEntry.dir.path)}");
|
||||
throw CacheNotFoundException("No entry for dir child: $id");
|
||||
}
|
||||
}).toList();
|
||||
List<File> _listByFileIds(File dir, List<int> childFileIds) {
|
||||
return childFileIds.map((id) {
|
||||
try {
|
||||
return _fileCache[id]!;
|
||||
} catch (_) {
|
||||
_log.warning(
|
||||
"[_listByFileIds] Missing file ($id) in db for dir: ${logFilename(dir.path)}");
|
||||
throw CacheNotFoundException("No entry for dir child: $id");
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
final Map<int, File> knownFiles;
|
||||
final _dirCache = <String, AppDbDirEntry>{};
|
||||
Future<int?> _queryFileIdOfDir(sql.SqliteDb db, sql.Account dbAccount,
|
||||
int? dirFileId, String dirRelativePath) async {
|
||||
final dirQuery = db.queryFiles().run((q) {
|
||||
q
|
||||
..setQueryMode(sql.FilesQueryMode.expression,
|
||||
expressions: [db.files.rowId])
|
||||
..setSqlAccount(dbAccount);
|
||||
if (dirFileId != null) {
|
||||
q.byFileId(dirFileId);
|
||||
} else {
|
||||
q.byRelativePath(dirRelativePath);
|
||||
}
|
||||
return q.build()..limit(1);
|
||||
});
|
||||
return await dirQuery.map((r) => r.read(db.files.rowId)).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<_ForwardCacheQueryChildResult>> _queryChildOfDir(
|
||||
sql.SqliteDb db, sql.Account dbAccount, int dirRowId) async {
|
||||
final childQuery = db.selectOnly(db.files).join([
|
||||
sql.innerJoin(db.dirFiles, db.dirFiles.child.equalsExp(db.files.rowId),
|
||||
useColumns: false),
|
||||
sql.innerJoin(
|
||||
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId),
|
||||
useColumns: false),
|
||||
])
|
||||
..addColumns([
|
||||
db.files.rowId,
|
||||
db.files.fileId,
|
||||
db.files.isCollection,
|
||||
db.accountFiles.relativePath,
|
||||
])
|
||||
..where(db.dirFiles.dir.equals(dirRowId))
|
||||
..where(db.accountFiles.account.equals(dbAccount.rowId));
|
||||
return await childQuery
|
||||
.map((r) => _ForwardCacheQueryChildResult(
|
||||
r.read(db.files.rowId)!,
|
||||
r.read(db.files.fileId)!,
|
||||
r.read(db.accountFiles.relativePath)!,
|
||||
r.read(db.files.isCollection),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
final _dirCache = <String, List<int>>{};
|
||||
final _fileCache = <int, File>{};
|
||||
|
||||
static final _log = Logger("entity.file.data_source.FileForwardCacheManager");
|
||||
}
|
||||
|
||||
class _ForwardCacheQueryChildResult {
|
||||
const _ForwardCacheQueryChildResult(
|
||||
this.rowId, this.fileId, this.relativePath, this.isCollection);
|
||||
|
||||
final int rowId;
|
||||
final int fileId;
|
||||
final String relativePath;
|
||||
final bool? isCollection;
|
||||
}
|
||||
|
||||
bool _validateFile(File f) {
|
||||
// See: https://gitlab.com/nkming2/nc-photos/-/issues/9
|
||||
return f.lastModified != null;
|
||||
}
|
||||
|
||||
File _covertAppDbFile2Entry(Map json) =>
|
||||
AppDbFile2Entry.fromJson(json.cast<String, dynamic>()).file;
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:drift/drift.dart' as sql;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.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/exception.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/list_util.dart' as list_util;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/touch_token_manager.dart';
|
||||
|
||||
class FileCacheLoader {
|
||||
FileCacheLoader({
|
||||
required this.appDb,
|
||||
required this.appDbSrc,
|
||||
FileCacheLoader(
|
||||
this._c, {
|
||||
required this.cacheSrc,
|
||||
required this.remoteSrc,
|
||||
this.shouldCheckCache = false,
|
||||
this.forwardCacheManager,
|
||||
});
|
||||
}) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.fileRepo);
|
||||
|
||||
/// Return the cached results of listing a directory [dir]
|
||||
///
|
||||
|
@ -30,7 +33,7 @@ class FileCacheLoader {
|
|||
if (forwardCacheManager != null) {
|
||||
cache = await forwardCacheManager!.list(account, dir);
|
||||
} else {
|
||||
cache = await appDbSrc.list(account, dir);
|
||||
cache = await cacheSrc.list(account, dir);
|
||||
}
|
||||
// compare the cached root
|
||||
final cacheEtag =
|
||||
|
@ -40,8 +43,6 @@ class FileCacheLoader {
|
|||
// if no etag supplied, we need to query it form remote
|
||||
remoteEtag ??= (await remoteSrc.list(account, dir, depth: 0)).first.etag;
|
||||
if (cacheEtag == remoteEtag) {
|
||||
_log.fine(
|
||||
"[list] etag matched for ${AppDbDirEntry.toPrimaryKeyForDir(account, dir)}");
|
||||
if (shouldCheckCache) {
|
||||
await _checkTouchToken(account, dir, cache);
|
||||
} else {
|
||||
|
@ -65,11 +66,10 @@ class FileCacheLoader {
|
|||
Account account, File f, List<File> cache) async {
|
||||
final touchPath =
|
||||
"${remote_storage_util.getRemoteTouchDir(account)}/${f.strippedPath}";
|
||||
final fileRepo = FileRepo(FileCachedDataSource(appDb));
|
||||
const tokenManager = TouchTokenManager();
|
||||
String? remoteToken;
|
||||
try {
|
||||
remoteToken = await tokenManager.getRemoteToken(fileRepo, account, f);
|
||||
remoteToken = await tokenManager.getRemoteToken(_c.fileRepo, account, f);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_checkTouchToken] Failed getting remote token at '$touchPath'",
|
||||
|
@ -96,9 +96,9 @@ class FileCacheLoader {
|
|||
}
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
final DiContainer _c;
|
||||
final FileWebdavDataSource remoteSrc;
|
||||
final FileAppDbDataSource appDbSrc;
|
||||
final FileDataSource cacheSrc;
|
||||
final bool shouldCheckCache;
|
||||
final FileForwardCacheManager? forwardCacheManager;
|
||||
|
||||
|
@ -108,215 +108,264 @@ class FileCacheLoader {
|
|||
static final _log = Logger("entity.file.file_cache_manager.FileCacheLoader");
|
||||
}
|
||||
|
||||
class FileCacheUpdater {
|
||||
const FileCacheUpdater(this.appDb);
|
||||
class FileSqliteCacheUpdater {
|
||||
FileSqliteCacheUpdater(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
Future<void> call(
|
||||
Account account,
|
||||
File dir, {
|
||||
required List<File> remote,
|
||||
List<File>? cache,
|
||||
}) async {
|
||||
await _cacheRemote(account, dir, remote);
|
||||
if (cache != null) {
|
||||
await _cleanUpCache(account, remote, cache);
|
||||
final s = Stopwatch()..start();
|
||||
try {
|
||||
await _cacheRemote(account, dir, remote);
|
||||
} finally {
|
||||
_log.info("[call] Elapsed time: ${s.elapsedMilliseconds}ms");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cacheRemote(
|
||||
Account account, File dir, List<File> remote) async {
|
||||
final s = Stopwatch()..start();
|
||||
try {
|
||||
await appDb.use(
|
||||
(db) => db.transaction(
|
||||
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
final sqlFiles = await remote.convertToFileCompanion(null);
|
||||
await _c.sqliteDb.use((db) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
final inserts = await _updateCache(db, dbAccount, sqlFiles, remote, dir);
|
||||
if (inserts.isNotEmpty) {
|
||||
await _insertCache(db, dbAccount, inserts, dir);
|
||||
}
|
||||
if (_dirRowId == null) {
|
||||
_log.severe("[_cacheRemote] Dir not inserted");
|
||||
throw StateError("Row ID for dir is null");
|
||||
}
|
||||
|
||||
// add files to db
|
||||
await Future.wait(remote.map((f) => fileStore.put(
|
||||
AppDbFile2Entry.fromFile(account, f).toJson(),
|
||||
AppDbFile2Entry.toPrimaryKeyForFile(account, f))));
|
||||
|
||||
// results from remote also contain the dir itself
|
||||
final resultGroup =
|
||||
remote.groupListsBy((f) => f.compareServerIdentity(dir));
|
||||
final remoteDir = resultGroup[true]!.first;
|
||||
final remoteChildren = resultGroup[false] ?? [];
|
||||
// add dir to db
|
||||
await dirStore.put(
|
||||
AppDbDirEntry.fromFiles(account, remoteDir, remoteChildren)
|
||||
.toJson(),
|
||||
AppDbDirEntry.toPrimaryKeyForDir(account, remoteDir));
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
_log.info("[_cacheRemote] Elapsed time: ${s.elapsedMilliseconds}ms");
|
||||
}
|
||||
final dirChildRowIdQuery = db.selectOnly(db.dirFiles)
|
||||
..addColumns([db.dirFiles.child])
|
||||
..where(db.dirFiles.dir.equals(_dirRowId))
|
||||
..orderBy([sql.OrderingTerm.asc(db.dirFiles.rowId)]);
|
||||
final dirChildRowIds =
|
||||
await dirChildRowIdQuery.map((r) => r.read(db.dirFiles.child)!).get();
|
||||
final diff = list_util.diff(dirChildRowIds, _childRowIds.sorted());
|
||||
if (diff.item1.isNotEmpty) {
|
||||
await db.batch((batch) {
|
||||
// insert new children
|
||||
batch.insertAll(db.dirFiles,
|
||||
diff.item1.map((k) => sql.DirFile(dir: _dirRowId!, child: k)));
|
||||
});
|
||||
}
|
||||
if (diff.item2.isNotEmpty) {
|
||||
// delete obsolete children
|
||||
await _removeSqliteFiles(db, dbAccount, diff.item2);
|
||||
await _cleanUpRemovedFile(db, dbAccount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Remove extra entries from local cache based on remote contents
|
||||
Future<void> _cleanUpCache(
|
||||
Account account, List<File> remote, List<File> cache) async {
|
||||
final removed = cache
|
||||
.where((c) => !remote.any((r) => r.compareServerIdentity(c)))
|
||||
.toList();
|
||||
if (removed.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_log.info(
|
||||
"[_cleanUpCache] Removed: ${removed.map((f) => f.path).toReadableString()}");
|
||||
/// Update Db files in [sqlFiles]
|
||||
///
|
||||
/// Return a list of DB files that are not yet inserted to the DB (thus not
|
||||
/// possible to update)
|
||||
Future<List<sql.CompleteFileCompanion>> _updateCache(
|
||||
sql.SqliteDb db,
|
||||
sql.Account dbAccount,
|
||||
Iterable<sql.CompleteFileCompanion> sqlFiles,
|
||||
Iterable<File> remoteFiles,
|
||||
File dir,
|
||||
) async {
|
||||
// query list of rowIds for files in [remoteFiles]
|
||||
final rowIds = await db.accountFileRowIdsByFileIds(
|
||||
remoteFiles.map((f) => f.fileId!),
|
||||
sqlAccount: dbAccount,
|
||||
);
|
||||
final rowIdsMap = Map.fromEntries(rowIds.map((e) => MapEntry(e.fileId, e)));
|
||||
|
||||
await appDb.use(
|
||||
(db) => db.transaction(
|
||||
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
for (final f in removed) {
|
||||
try {
|
||||
if (f.isCollection == true) {
|
||||
await _removeDirFromAppDb(account, f,
|
||||
dirStore: dirStore, fileStore: fileStore);
|
||||
} else {
|
||||
await _removeFileFromAppDb(account, f, fileStore: fileStore);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_cleanUpCache] Failed while removing file: ${logFilename(f.path)}",
|
||||
e,
|
||||
stackTrace);
|
||||
final inserts = <sql.CompleteFileCompanion>[];
|
||||
// for updates, we use batch to speed up the process
|
||||
await db.batch((batch) {
|
||||
for (final f in sqlFiles) {
|
||||
final thisRowIds = rowIdsMap[f.file.fileId.value];
|
||||
if (thisRowIds != null) {
|
||||
// updates
|
||||
batch.update(
|
||||
db.files,
|
||||
f.file,
|
||||
where: (sql.$FilesTable t) => t.rowId.equals(thisRowIds.fileRowId),
|
||||
);
|
||||
batch.update(
|
||||
db.accountFiles,
|
||||
f.accountFile,
|
||||
where: (sql.$AccountFilesTable t) =>
|
||||
t.rowId.equals(thisRowIds.accountFileRowId),
|
||||
);
|
||||
if (f.image != null) {
|
||||
batch.update(
|
||||
db.images,
|
||||
f.image!,
|
||||
where: (sql.$ImagesTable t) =>
|
||||
t.accountFile.equals(thisRowIds.accountFileRowId),
|
||||
);
|
||||
}
|
||||
if (f.trash != null) {
|
||||
batch.update(
|
||||
db.trashes,
|
||||
f.trash!,
|
||||
where: (sql.$TrashesTable t) =>
|
||||
t.file.equals(thisRowIds.fileRowId),
|
||||
);
|
||||
}
|
||||
_onRowCached(thisRowIds.fileRowId, f, dir);
|
||||
} else {
|
||||
// inserts, do it later
|
||||
inserts.add(f);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
_log.info(
|
||||
"[_updateCache] Updated ${sqlFiles.length - inserts.length} files");
|
||||
return inserts;
|
||||
}
|
||||
|
||||
Future<void> _insertCache(sql.SqliteDb db, sql.Account dbAccount,
|
||||
List<sql.CompleteFileCompanion> sqlFiles, File dir) async {
|
||||
_log.info("[_insertCache] Insert ${sqlFiles.length} files");
|
||||
// check if the files exist in the db in other accounts
|
||||
final query = db.queryFiles().run((q) {
|
||||
q
|
||||
..setQueryMode(
|
||||
sql.FilesQueryMode.expression,
|
||||
expressions: [db.files.rowId, db.files.fileId],
|
||||
)
|
||||
..setAccountless()
|
||||
..byServerRowId(dbAccount.server)
|
||||
..byFileIds(sqlFiles.map((f) => f.file.fileId.value));
|
||||
return q.build();
|
||||
});
|
||||
final fileRowIdMap = Map.fromEntries(await query
|
||||
.map((r) => MapEntry(r.read(db.files.fileId)!, r.read(db.files.rowId)!))
|
||||
.get());
|
||||
|
||||
await Future.wait(
|
||||
sqlFiles.map((f) async {
|
||||
var rowId = fileRowIdMap[f.file.fileId.value];
|
||||
if (rowId != null) {
|
||||
// shared file that exists in other accounts
|
||||
} else {
|
||||
final dbFile = await db.into(db.files).insertReturning(
|
||||
f.file.copyWith(server: sql.Value(dbAccount.server)),
|
||||
);
|
||||
rowId = dbFile.rowId;
|
||||
}
|
||||
final dbAccountFile = await db
|
||||
.into(db.accountFiles)
|
||||
.insertReturning(f.accountFile.copyWith(
|
||||
account: sql.Value(dbAccount.rowId),
|
||||
file: sql.Value(rowId),
|
||||
));
|
||||
if (f.image != null) {
|
||||
await db.into(db.images).insert(
|
||||
f.image!.copyWith(accountFile: sql.Value(dbAccountFile.rowId)));
|
||||
}
|
||||
if (f.trash != null) {
|
||||
await db
|
||||
.into(db.trashes)
|
||||
.insert(f.trash!.copyWith(file: sql.Value(rowId)));
|
||||
}
|
||||
_onRowCached(rowId, f, dir);
|
||||
}),
|
||||
eagerError: true,
|
||||
);
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
void _onRowCached(int rowId, sql.CompleteFileCompanion dbFile, File dir) {
|
||||
if (_compareIdentity(dbFile, dir)) {
|
||||
_dirRowId = rowId;
|
||||
}
|
||||
_childRowIds.add(rowId);
|
||||
}
|
||||
|
||||
static final _log = Logger("entity.file.file_cache_manager.FileCacheUpdater");
|
||||
bool _compareIdentity(sql.CompleteFileCompanion dbFile, File appFile) {
|
||||
if (appFile.fileId != null) {
|
||||
return appFile.fileId == dbFile.file.fileId.value;
|
||||
} else {
|
||||
return appFile.strippedPathWithEmpty ==
|
||||
dbFile.accountFile.relativePath.value;
|
||||
}
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
int? _dirRowId;
|
||||
final _childRowIds = <int>[];
|
||||
|
||||
static final _log =
|
||||
Logger("entity.file.file_cache_manager.FileSqliteCacheUpdater");
|
||||
}
|
||||
|
||||
class FileCacheRemover {
|
||||
const FileCacheRemover(this.appDb);
|
||||
class FileSqliteCacheRemover {
|
||||
FileSqliteCacheRemover(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
/// Remove a file/dir from cache
|
||||
///
|
||||
/// If [f] is a dir, the dir and its sub-dirs will be removed from dirStore.
|
||||
/// The files inside any of these dirs will be removed from file2Store.
|
||||
///
|
||||
/// If [f] is a file, the file will be removed from file2Store, but no changes
|
||||
/// to dirStore.
|
||||
Future<void> call(Account account, File f) async {
|
||||
if (f.isCollection != false) {
|
||||
// removing dir is basically a superset of removing file, so we'll treat
|
||||
// unspecified file as dir
|
||||
await appDb.use(
|
||||
(db) => db.transaction(
|
||||
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
await _removeDirFromAppDb(account, f,
|
||||
dirStore: dirStore, fileStore: fileStore);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
Future<void> _removeFileFromAppDb(
|
||||
Account account,
|
||||
File file, {
|
||||
required ObjectStore fileStore,
|
||||
}) async {
|
||||
try {
|
||||
if (file.fileId == null) {
|
||||
final index = fileStore.index(AppDbFile2Entry.strippedPathIndexName);
|
||||
final key = await index
|
||||
.getKey(AppDbFile2Entry.toStrippedPathIndexKeyForFile(account, file));
|
||||
if (key != null) {
|
||||
_log.fine("[_removeFileFromAppDb] Removing fileStore entry: $key");
|
||||
await fileStore.delete(key);
|
||||
}
|
||||
} else {
|
||||
await AppDbFile2Entry.toPrimaryKeyForFile(account, file).run((key) {
|
||||
_log.fine("[_removeFileFromAppDb] Removing fileStore entry: $key");
|
||||
return fileStore.delete(key);
|
||||
});
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_removeFileFromAppDb] Failed removing fileStore entry: ${logFilename(file.path)}",
|
||||
e,
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a dir and all files inside from the database
|
||||
Future<void> _removeDirFromAppDb(
|
||||
Account account,
|
||||
File dir, {
|
||||
required ObjectStore dirStore,
|
||||
required ObjectStore fileStore,
|
||||
}) async {
|
||||
// delete the dir itself
|
||||
try {
|
||||
await AppDbDirEntry.toPrimaryKeyForDir(account, dir).run((key) {
|
||||
_log.fine("[_removeDirFromAppDb] Removing dirStore entry: $key");
|
||||
return dirStore.delete(key);
|
||||
await _c.sqliteDb.use((db) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
final rowIds = await db.accountFileRowIdsOf(f, sqlAccount: dbAccount);
|
||||
await _removeSqliteFiles(db, dbAccount, [rowIds.fileRowId]);
|
||||
await _cleanUpRemovedFile(db, dbAccount);
|
||||
});
|
||||
} catch (e, stackTrace) {
|
||||
if (dir.isCollection != null) {
|
||||
_log.shout("[_removeDirFromAppDb] Failed removing dirStore entry", e,
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
// then its children
|
||||
final childrenRange = KeyRange.bound(
|
||||
AppDbDirEntry.toPrimaryLowerKeyForSubDirs(account, dir),
|
||||
AppDbDirEntry.toPrimaryUpperKeyForSubDirs(account, dir),
|
||||
);
|
||||
for (final key in await dirStore.getAllKeys(childrenRange)) {
|
||||
_log.fine("[_removeDirFromAppDb] Removing dirStore entry: $key");
|
||||
try {
|
||||
await dirStore.delete(key);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_removeDirFromAppDb] Failed removing dirStore entry", e,
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
// delete files from fileStore
|
||||
// first the dir
|
||||
await _removeFileFromAppDb(account, dir, fileStore: fileStore);
|
||||
// then files under this dir and sub-dirs
|
||||
final range = KeyRange.bound(
|
||||
AppDbFile2Entry.toStrippedPathIndexLowerKeyForDir(account, dir),
|
||||
AppDbFile2Entry.toStrippedPathIndexUpperKeyForDir(account, dir),
|
||||
);
|
||||
final strippedPathIndex =
|
||||
fileStore.index(AppDbFile2Entry.strippedPathIndexName);
|
||||
for (final key in await strippedPathIndex.getAllKeys(range)) {
|
||||
_log.fine("[_removeDirFromAppDb] Removing fileStore entry: $key");
|
||||
try {
|
||||
await fileStore.delete(key);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_removeDirFromAppDb] Failed removing fileStore entry", e,
|
||||
stackTrace);
|
||||
}
|
||||
final DiContainer _c;
|
||||
}
|
||||
|
||||
/// Remove a files from the cache db
|
||||
///
|
||||
/// If a file is a dir, its children will also be recursively removed
|
||||
Future<void> _removeSqliteFiles(
|
||||
sql.SqliteDb db, sql.Account dbAccount, List<int> fileRowIds) async {
|
||||
// query list of children, in case some of the files are dirs
|
||||
final childQuery = db.selectOnly(db.dirFiles)
|
||||
..addColumns([db.dirFiles.child])
|
||||
..where(db.dirFiles.dir.isIn(fileRowIds));
|
||||
final childRowIds =
|
||||
await childQuery.map((r) => r.read(db.dirFiles.child)!).get();
|
||||
childRowIds.removeWhere((id) => fileRowIds.contains(id));
|
||||
|
||||
// remove the files in AccountFiles table. We are not removing in Files table
|
||||
// because a file could be associated with multiple accounts
|
||||
await (db.delete(db.accountFiles)
|
||||
..where(
|
||||
(t) => t.account.equals(dbAccount.rowId) & t.file.isIn(fileRowIds)))
|
||||
.go();
|
||||
|
||||
if (childRowIds.isNotEmpty) {
|
||||
// remove children recursively
|
||||
return _removeSqliteFiles(db, dbAccount, childRowIds);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final _log = Logger("entity.file.file_cache_manager");
|
||||
Future<void> _cleanUpRemovedFile(sql.SqliteDb db, sql.Account dbAccount) async {
|
||||
// delete dangling files: entries in Files w/o a corresponding entry in
|
||||
// AccountFiles
|
||||
final danglingFileQuery = db.selectOnly(db.files).join([
|
||||
sql.leftOuterJoin(
|
||||
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId),
|
||||
useColumns: false),
|
||||
])
|
||||
..addColumns([db.files.rowId])
|
||||
..where(db.accountFiles.relativePath.isNull());
|
||||
final danglingFileRowIds =
|
||||
await danglingFileQuery.map((r) => r.read(db.files.rowId)!).get();
|
||||
if (danglingFileRowIds.isNotEmpty) {
|
||||
__log.info(
|
||||
"[_cleanUpRemovedFile] Delete ${danglingFileRowIds.length} files");
|
||||
await (db.delete(db.files)..where((t) => t.rowId.isIn(danglingFileRowIds)))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
|
||||
final __log = Logger("entity.file.file_cache_manager");
|
||||
|
|
209
app/lib/entity/sqlite_table.dart
Normal file
209
app/lib/entity/sqlite_table.dart
Normal file
|
@ -0,0 +1,209 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
|
||||
part 'sqlite_table.g.dart';
|
||||
|
||||
class Servers extends Table {
|
||||
IntColumn get rowId => integer().autoIncrement()();
|
||||
TextColumn get address => text().unique()();
|
||||
}
|
||||
|
||||
class Accounts extends Table {
|
||||
IntColumn get rowId => integer().autoIncrement()();
|
||||
IntColumn get server =>
|
||||
integer().references(Servers, #rowId, onDelete: KeyAction.cascade)();
|
||||
TextColumn get userId => text()();
|
||||
|
||||
@override
|
||||
get uniqueKeys => [
|
||||
{server, userId},
|
||||
];
|
||||
}
|
||||
|
||||
/// A file located on a server
|
||||
class Files extends Table {
|
||||
IntColumn get rowId => integer().autoIncrement()();
|
||||
IntColumn get server =>
|
||||
integer().references(Servers, #rowId, onDelete: KeyAction.cascade)();
|
||||
IntColumn get fileId => integer()();
|
||||
IntColumn get contentLength => integer().nullable()();
|
||||
TextColumn get contentType => text().nullable()();
|
||||
TextColumn get etag => text().nullable()();
|
||||
DateTimeColumn get lastModified =>
|
||||
dateTime().map(const _DateTimeConverter()).nullable()();
|
||||
BoolColumn get isCollection => boolean().nullable()();
|
||||
IntColumn get usedBytes => integer().nullable()();
|
||||
BoolColumn get hasPreview => boolean().nullable()();
|
||||
TextColumn get ownerId => text().nullable()();
|
||||
|
||||
@override
|
||||
get uniqueKeys => [
|
||||
{server, fileId},
|
||||
];
|
||||
}
|
||||
|
||||
/// Account specific properties associated with a file
|
||||
///
|
||||
/// A file on a Nextcloud server can have more than 1 path when it's shared
|
||||
class AccountFiles extends Table {
|
||||
IntColumn get rowId => integer().autoIncrement()();
|
||||
IntColumn get account =>
|
||||
integer().references(Accounts, #rowId, onDelete: KeyAction.cascade)();
|
||||
IntColumn get file =>
|
||||
integer().references(Files, #rowId, onDelete: KeyAction.cascade)();
|
||||
TextColumn get relativePath => text()();
|
||||
BoolColumn get isFavorite => boolean().nullable()();
|
||||
BoolColumn get isArchived => boolean().nullable()();
|
||||
DateTimeColumn get overrideDateTime =>
|
||||
dateTime().map(const _DateTimeConverter()).nullable()();
|
||||
|
||||
@override
|
||||
get uniqueKeys => [
|
||||
{account, file},
|
||||
];
|
||||
}
|
||||
|
||||
/// An image file
|
||||
class Images extends Table {
|
||||
// image data technically is identical between accounts, but the way it's
|
||||
// stored in the server is account specific so we follow the server here
|
||||
IntColumn get accountFile =>
|
||||
integer().references(AccountFiles, #rowId, onDelete: KeyAction.cascade)();
|
||||
DateTimeColumn get lastUpdated =>
|
||||
dateTime().map(const _DateTimeConverter())();
|
||||
TextColumn get fileEtag => text().nullable()();
|
||||
IntColumn get width => integer().nullable()();
|
||||
IntColumn get height => integer().nullable()();
|
||||
TextColumn get exifRaw => text().nullable()();
|
||||
|
||||
// exif columns
|
||||
DateTimeColumn get dateTimeOriginal =>
|
||||
dateTime().map(const _DateTimeConverter()).nullable()();
|
||||
|
||||
@override
|
||||
get primaryKey => {accountFile};
|
||||
}
|
||||
|
||||
/// A file inside trashbin
|
||||
@DataClassName("Trash")
|
||||
class Trashes extends Table {
|
||||
IntColumn get file =>
|
||||
integer().references(Files, #rowId, onDelete: KeyAction.cascade)();
|
||||
TextColumn get filename => text()();
|
||||
TextColumn get originalLocation => text()();
|
||||
DateTimeColumn get deletionTime =>
|
||||
dateTime().map(const _DateTimeConverter())();
|
||||
|
||||
@override
|
||||
get primaryKey => {file};
|
||||
}
|
||||
|
||||
/// A file located under another dir (dir is also a file)
|
||||
class DirFiles extends Table {
|
||||
IntColumn get dir =>
|
||||
integer().references(Files, #rowId, onDelete: KeyAction.cascade)();
|
||||
IntColumn get child =>
|
||||
integer().references(Files, #rowId, onDelete: KeyAction.cascade)();
|
||||
|
||||
@override
|
||||
get primaryKey => {dir, child};
|
||||
}
|
||||
|
||||
class Albums extends Table {
|
||||
IntColumn get rowId => integer().autoIncrement()();
|
||||
IntColumn get file => integer()
|
||||
.references(Files, #rowId, onDelete: KeyAction.cascade)
|
||||
.unique()();
|
||||
IntColumn get version => integer()();
|
||||
DateTimeColumn get lastUpdated =>
|
||||
dateTime().map(const _DateTimeConverter())();
|
||||
TextColumn get name => text()();
|
||||
|
||||
// provider
|
||||
TextColumn get providerType => text()();
|
||||
TextColumn get providerContent => text()();
|
||||
|
||||
// cover provider
|
||||
TextColumn get coverProviderType => text()();
|
||||
TextColumn get coverProviderContent => text()();
|
||||
|
||||
// sort provider
|
||||
TextColumn get sortProviderType => text()();
|
||||
TextColumn get sortProviderContent => text()();
|
||||
}
|
||||
|
||||
class AlbumShares extends Table {
|
||||
IntColumn get album =>
|
||||
integer().references(Albums, #rowId, onDelete: KeyAction.cascade)();
|
||||
TextColumn get userId => text()();
|
||||
TextColumn get displayName => text().nullable()();
|
||||
DateTimeColumn get sharedAt => dateTime().map(const _DateTimeConverter())();
|
||||
|
||||
@override
|
||||
get primaryKey => {album, userId};
|
||||
}
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
Servers,
|
||||
Accounts,
|
||||
Files,
|
||||
Images,
|
||||
Trashes,
|
||||
AccountFiles,
|
||||
DirFiles,
|
||||
Albums,
|
||||
AlbumShares,
|
||||
],
|
||||
)
|
||||
class SqliteDb extends _$SqliteDb {
|
||||
SqliteDb({
|
||||
QueryExecutor? executor,
|
||||
}) : super(executor ?? platform.openSqliteConnection());
|
||||
|
||||
SqliteDb.connect(DatabaseConnection connection) : super.connect(connection);
|
||||
|
||||
@override
|
||||
get schemaVersion => 1;
|
||||
|
||||
@override
|
||||
get migration => MigrationStrategy(
|
||||
onCreate: (m) async {
|
||||
await m.createAll();
|
||||
|
||||
await m.createIndex(Index("files_server_index",
|
||||
"CREATE INDEX files_server_index ON files(server);"));
|
||||
await m.createIndex(Index("files_file_id_index",
|
||||
"CREATE INDEX files_file_id_index ON files(file_id);"));
|
||||
await m.createIndex(Index("files_content_type_index",
|
||||
"CREATE INDEX files_content_type_index ON files(content_type);"));
|
||||
|
||||
await m.createIndex(Index("account_files_file_index",
|
||||
"CREATE INDEX account_files_file_index ON account_files(file);"));
|
||||
await m.createIndex(Index("account_files_relative_path_index",
|
||||
"CREATE INDEX account_files_relative_path_index ON account_files(relative_path);"));
|
||||
|
||||
await m.createIndex(Index("dir_files_dir_index",
|
||||
"CREATE INDEX dir_files_dir_index ON dir_files(dir);"));
|
||||
await m.createIndex(Index("dir_files_child_index",
|
||||
"CREATE INDEX dir_files_child_index ON dir_files(child);"));
|
||||
|
||||
await m.createIndex(Index("album_shares_album_index",
|
||||
"CREATE INDEX album_shares_album_index ON album_shares(album);"));
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
await customStatement("PRAGMA foreign_keys = ON");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _DateTimeConverter extends TypeConverter<DateTime, DateTime> {
|
||||
const _DateTimeConverter();
|
||||
|
||||
@override
|
||||
DateTime? mapToDart(DateTime? fromDb) => fromDb?.toUtc();
|
||||
|
||||
@override
|
||||
DateTime? mapToSql(DateTime? value) => value?.toUtc();
|
||||
}
|
3013
app/lib/entity/sqlite_table.g.dart
Normal file
3013
app/lib/entity/sqlite_table.g.dart
Normal file
File diff suppressed because it is too large
Load diff
141
app/lib/entity/sqlite_table_converter.dart
Normal file
141
app/lib/entity/sqlite_table_converter.dart
Normal file
|
@ -0,0 +1,141 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:nc_photos/ci_string.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||
import 'package:nc_photos/entity/exif.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/object_extension.dart';
|
||||
|
||||
class SqliteAlbumConverter {
|
||||
static Album fromSql(
|
||||
sql.Album album, File albumFile, List<sql.AlbumShare> shares) {
|
||||
return Album(
|
||||
lastUpdated: album.lastUpdated,
|
||||
name: album.name,
|
||||
provider: AlbumProvider.fromJson({
|
||||
"type": album.providerType,
|
||||
"content": jsonDecode(album.providerContent),
|
||||
}),
|
||||
coverProvider: AlbumCoverProvider.fromJson({
|
||||
"type": album.coverProviderType,
|
||||
"content": jsonDecode(album.coverProviderContent),
|
||||
}),
|
||||
sortProvider: AlbumSortProvider.fromJson({
|
||||
"type": album.sortProviderType,
|
||||
"content": jsonDecode(album.sortProviderContent),
|
||||
}),
|
||||
shares: shares.isEmpty
|
||||
? null
|
||||
: shares
|
||||
.map((e) => AlbumShare(
|
||||
userId: e.userId.toCi(),
|
||||
displayName: e.displayName,
|
||||
sharedAt: e.sharedAt.toUtc(),
|
||||
))
|
||||
.toList(),
|
||||
albumFile: albumFile,
|
||||
savedVersion: album.version,
|
||||
);
|
||||
}
|
||||
|
||||
static sql.CompleteAlbumCompanion toSql(Album album, int albumFileRowId) {
|
||||
final providerJson = album.provider.toJson();
|
||||
final coverProviderJson = album.coverProvider.toJson();
|
||||
final sortProviderJson = album.sortProvider.toJson();
|
||||
final dbAlbum = sql.AlbumsCompanion.insert(
|
||||
file: albumFileRowId,
|
||||
version: Album.version,
|
||||
lastUpdated: album.lastUpdated,
|
||||
name: album.name,
|
||||
providerType: providerJson["type"],
|
||||
providerContent: jsonEncode(providerJson["content"]),
|
||||
coverProviderType: coverProviderJson["type"],
|
||||
coverProviderContent: jsonEncode(coverProviderJson["content"]),
|
||||
sortProviderType: sortProviderJson["type"],
|
||||
sortProviderContent: jsonEncode(sortProviderJson["content"]),
|
||||
);
|
||||
final dbAlbumShares = album.shares
|
||||
?.map((s) => sql.AlbumSharesCompanion(
|
||||
userId: Value(s.userId.toCaseInsensitiveString()),
|
||||
displayName: Value(s.displayName),
|
||||
sharedAt: Value(s.sharedAt),
|
||||
))
|
||||
.toList();
|
||||
return sql.CompleteAlbumCompanion(dbAlbum, dbAlbumShares ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
class SqliteFileConverter {
|
||||
static File fromSql(String homeDir, sql.CompleteFile f) {
|
||||
final metadata = f.image?.run((obj) => Metadata(
|
||||
lastUpdated: obj.lastUpdated,
|
||||
fileEtag: obj.fileEtag,
|
||||
imageWidth: obj.width,
|
||||
imageHeight: obj.height,
|
||||
exif: obj.exifRaw?.run((e) => Exif.fromJson(jsonDecode(e))),
|
||||
));
|
||||
return File(
|
||||
path: "remote.php/dav/files/$homeDir/${f.accountFile.relativePath}",
|
||||
contentLength: f.file.contentLength,
|
||||
contentType: f.file.contentType,
|
||||
etag: f.file.etag,
|
||||
lastModified: f.file.lastModified,
|
||||
isCollection: f.file.isCollection,
|
||||
usedBytes: f.file.usedBytes,
|
||||
hasPreview: f.file.hasPreview,
|
||||
fileId: f.file.fileId,
|
||||
isFavorite: f.accountFile.isFavorite,
|
||||
ownerId: f.file.ownerId?.toCi(),
|
||||
trashbinFilename: f.trash?.filename,
|
||||
trashbinOriginalLocation: f.trash?.originalLocation,
|
||||
trashbinDeletionTime: f.trash?.deletionTime,
|
||||
metadata: metadata,
|
||||
isArchived: f.accountFile.isArchived,
|
||||
overrideDateTime: f.accountFile.overrideDateTime,
|
||||
);
|
||||
}
|
||||
|
||||
static sql.CompleteFileCompanion toSql(sql.Account? account, File file) {
|
||||
final dbFile = sql.FilesCompanion(
|
||||
server: account == null ? const Value.absent() : Value(account.server),
|
||||
fileId: Value(file.fileId!),
|
||||
contentLength: Value(file.contentLength),
|
||||
contentType: Value(file.contentType),
|
||||
etag: Value(file.etag),
|
||||
lastModified: Value(file.lastModified),
|
||||
isCollection: Value(file.isCollection),
|
||||
usedBytes: Value(file.usedBytes),
|
||||
hasPreview: Value(file.hasPreview),
|
||||
ownerId: Value(file.ownerId!.toCaseInsensitiveString()),
|
||||
);
|
||||
final dbAccountFile = sql.AccountFilesCompanion(
|
||||
account: account == null ? const Value.absent() : Value(account.rowId),
|
||||
relativePath: Value(file.strippedPathWithEmpty),
|
||||
isFavorite: Value(file.isFavorite),
|
||||
isArchived: Value(file.isArchived),
|
||||
overrideDateTime: Value(file.overrideDateTime),
|
||||
);
|
||||
final dbImage = file.metadata?.run((m) => sql.ImagesCompanion.insert(
|
||||
lastUpdated: m.lastUpdated,
|
||||
fileEtag: Value(m.fileEtag),
|
||||
width: Value(m.imageWidth),
|
||||
height: Value(m.imageHeight),
|
||||
exifRaw: Value(m.exif?.toJson().run((j) => jsonEncode(j))),
|
||||
dateTimeOriginal: Value(m.exif?.dateTimeOriginal),
|
||||
));
|
||||
final dbTrash = file.trashbinDeletionTime == null
|
||||
? null
|
||||
: sql.TrashesCompanion.insert(
|
||||
filename: file.trashbinFilename!,
|
||||
originalLocation: file.trashbinOriginalLocation!,
|
||||
deletionTime: file.trashbinDeletionTime!,
|
||||
);
|
||||
return sql.CompleteFileCompanion(dbFile, dbAccountFile, dbImage, dbTrash);
|
||||
}
|
||||
}
|
538
app/lib/entity/sqlite_table_extension.dart
Normal file
538
app/lib/entity/sqlite_table_extension.dart
Normal file
|
@ -0,0 +1,538 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:nc_photos/account.dart' as app;
|
||||
import 'package:nc_photos/entity/file.dart' as app;
|
||||
import 'package:nc_photos/entity/sqlite_table.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_converter.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_isolate.dart';
|
||||
import 'package:nc_photos/future_extension.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
|
||||
class CompleteFile {
|
||||
const CompleteFile(this.file, this.accountFile, this.image, this.trash);
|
||||
|
||||
final File file;
|
||||
final AccountFile accountFile;
|
||||
final Image? image;
|
||||
final Trash? trash;
|
||||
}
|
||||
|
||||
class CompleteFileCompanion {
|
||||
const CompleteFileCompanion(
|
||||
this.file, this.accountFile, this.image, this.trash);
|
||||
|
||||
final FilesCompanion file;
|
||||
final AccountFilesCompanion accountFile;
|
||||
final ImagesCompanion? image;
|
||||
final TrashesCompanion? trash;
|
||||
}
|
||||
|
||||
extension CompleteFileListExtension on List<CompleteFile> {
|
||||
Future<List<app.File>> convertToAppFile(app.Account account) {
|
||||
return map((f) => {
|
||||
"homeDir": account.homeDir.toString(),
|
||||
"completeFile": f,
|
||||
}).computeAll(_covertSqliteDbFile);
|
||||
}
|
||||
}
|
||||
|
||||
extension FileListExtension on List<app.File> {
|
||||
Future<List<CompleteFileCompanion>> convertToFileCompanion(Account? account) {
|
||||
return map((f) => {
|
||||
"account": account,
|
||||
"file": f,
|
||||
}).computeAll(_convertAppFile);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumWithShare {
|
||||
const AlbumWithShare(this.album, this.share);
|
||||
|
||||
final Album album;
|
||||
final AlbumShare? share;
|
||||
}
|
||||
|
||||
class CompleteAlbumCompanion {
|
||||
const CompleteAlbumCompanion(this.album, this.albumShares);
|
||||
|
||||
final AlbumsCompanion album;
|
||||
final List<AlbumSharesCompanion> albumShares;
|
||||
}
|
||||
|
||||
class AccountFileRowIds {
|
||||
const AccountFileRowIds(
|
||||
this.accountFileRowId, this.accountRowId, this.fileRowId);
|
||||
|
||||
final int accountFileRowId;
|
||||
final int accountRowId;
|
||||
final int fileRowId;
|
||||
}
|
||||
|
||||
class AccountFileRowIdsWithFileId {
|
||||
const AccountFileRowIdsWithFileId(
|
||||
this.accountFileRowId, this.accountRowId, this.fileRowId, this.fileId);
|
||||
|
||||
final int accountFileRowId;
|
||||
final int accountRowId;
|
||||
final int fileRowId;
|
||||
final int fileId;
|
||||
}
|
||||
|
||||
extension SqliteDbExtension on SqliteDb {
|
||||
/// Start a transaction and run [block]
|
||||
///
|
||||
/// The [db] argument passed to [block] is identical to this
|
||||
///
|
||||
/// Do NOT call this when using [isolate], call [useInIsolate] instead
|
||||
Future<T> use<T>(Future<T> Function(SqliteDb db) block) async {
|
||||
return await platform.Lock.synchronized(k.appDbLockId, () async {
|
||||
return await transaction(() async {
|
||||
return await block(this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Start an isolate and run [callback] there, with access to the
|
||||
/// SQLite database
|
||||
Future<U> isolate<T, U>(T args, ComputeWithDbCallback<T, U> callback) async {
|
||||
// we need to acquire the lock here as method channel is not supported in
|
||||
// background isolates
|
||||
return await platform.Lock.synchronized(k.appDbLockId, () async {
|
||||
// in unit tests we use an in-memory db, which mean there's no way to
|
||||
// access it in other isolates
|
||||
if (platform_k.isUnitTest) {
|
||||
return await callback(this, args);
|
||||
} else {
|
||||
return await computeWithDb(callback, args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Start a transaction and run [block], this version is suitable to be called
|
||||
/// in [isolate]
|
||||
///
|
||||
/// See: [use]
|
||||
Future<T> useInIsolate<T>(Future<T> Function(SqliteDb db) block) async {
|
||||
return await transaction(() async {
|
||||
return await block(this);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> insertAccountOf(app.Account account) async {
|
||||
Server dbServer;
|
||||
try {
|
||||
dbServer = await into(servers).insertReturning(
|
||||
ServersCompanion.insert(
|
||||
address: account.url,
|
||||
),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
);
|
||||
} on StateError catch (_) {
|
||||
// already exists
|
||||
final query = select(servers)
|
||||
..where((t) => t.address.equals(account.url));
|
||||
dbServer = await query.getSingle();
|
||||
}
|
||||
await into(accounts).insert(
|
||||
AccountsCompanion.insert(
|
||||
server: dbServer.rowId,
|
||||
userId: account.username.toCaseInsensitiveString(),
|
||||
),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Account> accountOf(app.Account account) {
|
||||
final query = select(accounts).join([
|
||||
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
|
||||
useColumns: false)
|
||||
])
|
||||
..where(servers.address.equals(account.url))
|
||||
..where(
|
||||
accounts.userId.equals(account.username.toCaseInsensitiveString()))
|
||||
..limit(1);
|
||||
return query.map((r) => r.readTable(accounts)).getSingle();
|
||||
}
|
||||
|
||||
FilesQueryBuilder queryFiles() => FilesQueryBuilder(this);
|
||||
|
||||
/// Query File by app File
|
||||
///
|
||||
/// Only one of [sqlAccount] and [appAccount] must be passed
|
||||
Future<File> fileOf(
|
||||
app.File file, {
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
}) {
|
||||
assert((sqlAccount != null) != (appAccount != null));
|
||||
final query = queryFiles().run((q) {
|
||||
q.setQueryMode(FilesQueryMode.file);
|
||||
if (sqlAccount != null) {
|
||||
q.setSqlAccount(sqlAccount);
|
||||
} else {
|
||||
q.setAppAccount(appAccount!);
|
||||
}
|
||||
if (file.fileId != null) {
|
||||
q.byFileId(file.fileId!);
|
||||
} else {
|
||||
q.byRelativePath(file.strippedPathWithEmpty);
|
||||
}
|
||||
return q.build()..limit(1);
|
||||
});
|
||||
return query.map((r) => r.readTable(files)).getSingle();
|
||||
}
|
||||
|
||||
/// Query AccountFiles, Accounts and Files row ID by app File
|
||||
///
|
||||
/// Only one of [sqlAccount] and [appAccount] must be passed
|
||||
Future<AccountFileRowIds?> accountFileRowIdsOfOrNull(
|
||||
app.File file, {
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
}) {
|
||||
assert((sqlAccount != null) != (appAccount != null));
|
||||
final query = queryFiles().run((q) {
|
||||
q.setQueryMode(FilesQueryMode.expression, expressions: [
|
||||
accountFiles.rowId,
|
||||
accountFiles.account,
|
||||
accountFiles.file,
|
||||
]);
|
||||
if (sqlAccount != null) {
|
||||
q.setSqlAccount(sqlAccount);
|
||||
} else {
|
||||
q.setAppAccount(appAccount!);
|
||||
}
|
||||
if (file.fileId != null) {
|
||||
q.byFileId(file.fileId!);
|
||||
} else {
|
||||
q.byRelativePath(file.strippedPathWithEmpty);
|
||||
}
|
||||
return q.build()..limit(1);
|
||||
});
|
||||
return query
|
||||
.map((r) => AccountFileRowIds(
|
||||
r.read(accountFiles.rowId)!,
|
||||
r.read(accountFiles.account)!,
|
||||
r.read(accountFiles.file)!,
|
||||
))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
/// See [accountFileRowIdsOfOrNull]
|
||||
Future<AccountFileRowIds> accountFileRowIdsOf(
|
||||
app.File file, {
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
}) =>
|
||||
accountFileRowIdsOfOrNull(file,
|
||||
sqlAccount: sqlAccount, appAccount: appAccount)
|
||||
.notNull();
|
||||
|
||||
/// Query AccountFiles, Accounts and Files row ID by fileIds
|
||||
///
|
||||
/// Returned files are NOT guaranteed to be sorted as [fileIds]
|
||||
Future<List<AccountFileRowIdsWithFileId>> accountFileRowIdsByFileIds(
|
||||
Iterable<int> fileIds, {
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
}) {
|
||||
assert((sqlAccount != null) != (appAccount != null));
|
||||
final query = queryFiles().run((q) {
|
||||
q.setQueryMode(FilesQueryMode.expression, expressions: [
|
||||
accountFiles.rowId,
|
||||
accountFiles.account,
|
||||
accountFiles.file,
|
||||
files.fileId,
|
||||
]);
|
||||
if (sqlAccount != null) {
|
||||
q.setSqlAccount(sqlAccount);
|
||||
} else {
|
||||
q.setAppAccount(appAccount!);
|
||||
}
|
||||
q.byFileIds(fileIds);
|
||||
return q.build();
|
||||
});
|
||||
return query
|
||||
.map((r) => AccountFileRowIdsWithFileId(
|
||||
r.read(accountFiles.rowId)!,
|
||||
r.read(accountFiles.account)!,
|
||||
r.read(accountFiles.file)!,
|
||||
r.read(files.fileId)!,
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
/// Query CompleteFile by fileId
|
||||
///
|
||||
/// Returned files are NOT guaranteed to be sorted as [fileIds]
|
||||
Future<List<CompleteFile>> completeFilesByFileIds(
|
||||
Iterable<int> fileIds, {
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
}) {
|
||||
assert((sqlAccount != null) != (appAccount != null));
|
||||
final query = queryFiles().run((q) {
|
||||
q.setQueryMode(FilesQueryMode.completeFile);
|
||||
if (sqlAccount != null) {
|
||||
q.setSqlAccount(sqlAccount);
|
||||
} else {
|
||||
q.setAppAccount(appAccount!);
|
||||
}
|
||||
q.byFileIds(fileIds);
|
||||
return q.build();
|
||||
});
|
||||
return query
|
||||
.map((r) => CompleteFile(
|
||||
r.readTable(files),
|
||||
r.readTable(accountFiles),
|
||||
r.readTableOrNull(images),
|
||||
r.readTableOrNull(trashes),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<CompleteFile>> completeFilesByDirRowId(
|
||||
int dirRowId, {
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
}) {
|
||||
assert((sqlAccount != null) != (appAccount != null));
|
||||
final query = queryFiles().run((q) {
|
||||
q.setQueryMode(FilesQueryMode.completeFile);
|
||||
if (sqlAccount != null) {
|
||||
q.setSqlAccount(sqlAccount);
|
||||
} else {
|
||||
q.setAppAccount(appAccount!);
|
||||
}
|
||||
q.byDirRowId(dirRowId);
|
||||
return q.build();
|
||||
});
|
||||
return query
|
||||
.map((r) => CompleteFile(
|
||||
r.readTable(files),
|
||||
r.readTable(accountFiles),
|
||||
r.readTableOrNull(images),
|
||||
r.readTableOrNull(trashes),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
/// Query CompleteFile by favorite
|
||||
Future<List<CompleteFile>> completeFilesByFavorite({
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
}) {
|
||||
assert((sqlAccount != null) != (appAccount != null));
|
||||
final query = queryFiles().run((q) {
|
||||
q.setQueryMode(FilesQueryMode.completeFile);
|
||||
if (sqlAccount != null) {
|
||||
q.setSqlAccount(sqlAccount);
|
||||
} else {
|
||||
q.setAppAccount(appAccount!);
|
||||
}
|
||||
q.byFavorite(true);
|
||||
return q.build();
|
||||
});
|
||||
return query
|
||||
.map((r) => CompleteFile(
|
||||
r.readTable(files),
|
||||
r.readTable(accountFiles),
|
||||
r.readTableOrNull(images),
|
||||
r.readTableOrNull(trashes),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
}
|
||||
|
||||
enum FilesQueryMode {
|
||||
file,
|
||||
completeFile,
|
||||
expression,
|
||||
}
|
||||
|
||||
/// Build a Files table query
|
||||
///
|
||||
/// If you call more than one by* methods, the condition will be added up
|
||||
/// instead of replaced. No validations will be made to make sure the resulting
|
||||
/// conditions make sense
|
||||
class FilesQueryBuilder {
|
||||
FilesQueryBuilder(this.db);
|
||||
|
||||
/// Set the query mode
|
||||
///
|
||||
/// If [mode] == FilesQueryMode.expression, [expressions] must be defined and
|
||||
/// not empty
|
||||
void setQueryMode(
|
||||
FilesQueryMode mode, {
|
||||
Iterable<Expression>? expressions,
|
||||
}) {
|
||||
assert(
|
||||
(mode == FilesQueryMode.expression) != (expressions?.isEmpty != false));
|
||||
_queryMode = mode;
|
||||
_selectExpressions = expressions;
|
||||
}
|
||||
|
||||
void setSqlAccount(Account account) {
|
||||
assert(_appAccount == null);
|
||||
_sqlAccount = account;
|
||||
}
|
||||
|
||||
void setAppAccount(app.Account account) {
|
||||
assert(_sqlAccount == null);
|
||||
_appAccount = account;
|
||||
}
|
||||
|
||||
void setAccountless() {
|
||||
assert(_sqlAccount == null && _appAccount == null);
|
||||
_isAccountless = true;
|
||||
}
|
||||
|
||||
void byRowId(int rowId) {
|
||||
_byRowId = rowId;
|
||||
}
|
||||
|
||||
void byFileId(int fileId) {
|
||||
_byFileId = fileId;
|
||||
}
|
||||
|
||||
void byFileIds(Iterable<int> fileIds) {
|
||||
_byFileIds = fileIds;
|
||||
}
|
||||
|
||||
void byRelativePath(String path) {
|
||||
_byRelativePath = path;
|
||||
}
|
||||
|
||||
void byRelativePathPattern(String pattern) {
|
||||
_byRelativePathPattern = pattern;
|
||||
}
|
||||
|
||||
void byMimePattern(String pattern) {
|
||||
(_byMimePatterns ??= []).add(pattern);
|
||||
}
|
||||
|
||||
void byFavorite(bool favorite) {
|
||||
_byFavorite = favorite;
|
||||
}
|
||||
|
||||
void byDirRowId(int dirRowId) {
|
||||
_byDirRowId = dirRowId;
|
||||
}
|
||||
|
||||
void byServerRowId(int serverRowId) {
|
||||
_byServerRowId = serverRowId;
|
||||
}
|
||||
|
||||
JoinedSelectStatement build() {
|
||||
if (_sqlAccount == null && _appAccount == null && !_isAccountless) {
|
||||
throw StateError("Invalid query: missing account");
|
||||
}
|
||||
final dynamic select = _queryMode == FilesQueryMode.expression
|
||||
? db.selectOnly(db.files)
|
||||
: db.select(db.files);
|
||||
final query = select.join([
|
||||
innerJoin(db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId),
|
||||
useColumns: _queryMode == FilesQueryMode.completeFile),
|
||||
if (_appAccount != null) ...[
|
||||
innerJoin(
|
||||
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account),
|
||||
useColumns: false),
|
||||
innerJoin(db.servers, db.servers.rowId.equalsExp(db.accounts.server),
|
||||
useColumns: false),
|
||||
],
|
||||
if (_byDirRowId != null)
|
||||
innerJoin(db.dirFiles, db.dirFiles.child.equalsExp(db.files.rowId),
|
||||
useColumns: false),
|
||||
if (_queryMode == FilesQueryMode.completeFile) ...[
|
||||
leftOuterJoin(
|
||||
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
|
||||
leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
|
||||
],
|
||||
]) as JoinedSelectStatement;
|
||||
if (_queryMode == FilesQueryMode.expression) {
|
||||
query.addColumns(_selectExpressions!);
|
||||
}
|
||||
|
||||
if (_sqlAccount != null) {
|
||||
query.where(db.accountFiles.account.equals(_sqlAccount!.rowId));
|
||||
} else if (_appAccount != null) {
|
||||
query
|
||||
..where(db.servers.address.equals(_appAccount!.url))
|
||||
..where(db.accounts.userId
|
||||
.equals(_appAccount!.username.toCaseInsensitiveString()));
|
||||
}
|
||||
|
||||
if (_byRowId != null) {
|
||||
query.where(db.files.rowId.equals(_byRowId));
|
||||
}
|
||||
if (_byFileId != null) {
|
||||
query.where(db.files.fileId.equals(_byFileId));
|
||||
}
|
||||
if (_byFileIds != null) {
|
||||
query.where(db.files.fileId.isIn(_byFileIds!));
|
||||
}
|
||||
if (_byRelativePath != null) {
|
||||
query.where(db.accountFiles.relativePath.equals(_byRelativePath));
|
||||
}
|
||||
if (_byRelativePathPattern != null) {
|
||||
query.where(db.accountFiles.relativePath.like(_byRelativePathPattern!));
|
||||
}
|
||||
if (_byMimePatterns?.isNotEmpty == true) {
|
||||
final expression = _byMimePatterns!.sublist(1).fold<Expression<bool?>>(
|
||||
db.files.contentType.like(_byMimePatterns![0]),
|
||||
(previousValue, element) =>
|
||||
previousValue | db.files.contentType.like(element));
|
||||
query.where(expression);
|
||||
}
|
||||
if (_byFavorite != null) {
|
||||
if (_byFavorite!) {
|
||||
query.where(db.accountFiles.isFavorite.equals(true));
|
||||
} else {
|
||||
// null are treated as false
|
||||
query.where(db.accountFiles.isFavorite.equals(true).not());
|
||||
}
|
||||
}
|
||||
if (_byDirRowId != null) {
|
||||
query.where(db.dirFiles.dir.equals(_byDirRowId));
|
||||
}
|
||||
if (_byServerRowId != null) {
|
||||
query.where(db.files.server.equals(_byServerRowId));
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
final SqliteDb db;
|
||||
|
||||
FilesQueryMode _queryMode = FilesQueryMode.file;
|
||||
Iterable<Expression>? _selectExpressions;
|
||||
|
||||
Account? _sqlAccount;
|
||||
app.Account? _appAccount;
|
||||
bool _isAccountless = false;
|
||||
|
||||
int? _byRowId;
|
||||
int? _byFileId;
|
||||
Iterable<int>? _byFileIds;
|
||||
String? _byRelativePath;
|
||||
String? _byRelativePathPattern;
|
||||
List<String>? _byMimePatterns;
|
||||
bool? _byFavorite;
|
||||
int? _byDirRowId;
|
||||
int? _byServerRowId;
|
||||
}
|
||||
|
||||
app.File _covertSqliteDbFile(Map map) {
|
||||
final homeDir = map["homeDir"] as String;
|
||||
final file = map["completeFile"] as CompleteFile;
|
||||
return SqliteFileConverter.fromSql(homeDir, file);
|
||||
}
|
||||
|
||||
CompleteFileCompanion _convertAppFile(Map map) {
|
||||
final account = map["account"] as Account?;
|
||||
final file = map["file"] as app.File;
|
||||
return SqliteFileConverter.toSql(account, file);
|
||||
}
|
87
app/lib/entity/sqlite_table_isolate.dart
Normal file
87
app/lib/entity/sqlite_table_isolate.dart
Normal file
|
@ -0,0 +1,87 @@
|
|||
import 'dart:isolate';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/isolate.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table.dart';
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
|
||||
typedef ComputeWithDbCallback<T, U> = Future<U> Function(
|
||||
SqliteDb db, T message);
|
||||
|
||||
/// Create a drift db running in an isolate
|
||||
///
|
||||
/// This is only expected to be used in the main isolate
|
||||
Future<SqliteDb> createDb() async {
|
||||
// see: https://drift.simonbinder.eu/docs/advanced-features/isolates/
|
||||
final driftIsolate = await _createDriftIsolate();
|
||||
final connection = await driftIsolate.connect();
|
||||
return SqliteDb.connect(connection);
|
||||
}
|
||||
|
||||
Future<U> computeWithDb<T, U>(
|
||||
ComputeWithDbCallback<T, U> callback, T args) async {
|
||||
return await compute(
|
||||
_computeWithDbImpl<T, U>,
|
||||
_ComputeWithDbMessage(
|
||||
await platform.getSqliteConnectionArgs(), callback, args),
|
||||
);
|
||||
}
|
||||
|
||||
class _IsolateStartRequest {
|
||||
const _IsolateStartRequest(this.sendDriftIsolate, this.platformArgs);
|
||||
|
||||
final SendPort sendDriftIsolate;
|
||||
final Map<String, dynamic> platformArgs;
|
||||
}
|
||||
|
||||
class _ComputeWithDbMessage<T, U> {
|
||||
const _ComputeWithDbMessage(
|
||||
this.sqliteConnectionArgs, this.callback, this.args);
|
||||
|
||||
final Map<String, dynamic> sqliteConnectionArgs;
|
||||
final ComputeWithDbCallback<T, U> callback;
|
||||
final T args;
|
||||
}
|
||||
|
||||
Future<DriftIsolate> _createDriftIsolate() async {
|
||||
final args = await platform.getSqliteConnectionArgs();
|
||||
final receivePort = ReceivePort();
|
||||
await Isolate.spawn(
|
||||
_startBackground,
|
||||
_IsolateStartRequest(receivePort.sendPort, args),
|
||||
);
|
||||
// _startBackground will send the DriftIsolate to this ReceivePort
|
||||
return await receivePort.first as DriftIsolate;
|
||||
}
|
||||
|
||||
void _startBackground(_IsolateStartRequest request) {
|
||||
// this is the entry point from the background isolate! Let's create
|
||||
// the database from the path we received
|
||||
final executor = platform.openSqliteConnectionWithArgs(request.platformArgs);
|
||||
// we're using DriftIsolate.inCurrent here as this method already runs on a
|
||||
// background isolate. If we used DriftIsolate.spawn, a third isolate would be
|
||||
// started which is not what we want!
|
||||
final driftIsolate = DriftIsolate.inCurrent(
|
||||
() => DatabaseConnection.fromExecutor(executor),
|
||||
// this breaks background service!
|
||||
serialize: false,
|
||||
);
|
||||
// inform the starting isolate about this, so that it can call .connect()
|
||||
request.sendDriftIsolate.send(driftIsolate);
|
||||
}
|
||||
|
||||
Future<U> _computeWithDbImpl<T, U>(_ComputeWithDbMessage<T, U> message) async {
|
||||
// we don't use driftIsolate because opening a DB normally is found to perform
|
||||
// better
|
||||
final sqliteDb = SqliteDb(
|
||||
executor:
|
||||
platform.openSqliteConnectionWithArgs(message.sqliteConnectionArgs),
|
||||
);
|
||||
try {
|
||||
return await message.callback(sqliteDb, message.args);
|
||||
} finally {
|
||||
await sqliteDb.close();
|
||||
}
|
||||
}
|
33
app/lib/future_util.dart
Normal file
33
app/lib/future_util.dart
Normal file
|
@ -0,0 +1,33 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
|
||||
Future<List<T>> waitOr<T>(
|
||||
Iterable<Future<T>> futures,
|
||||
T Function(Object error, StackTrace? stackTrace) onError,
|
||||
) async {
|
||||
final completer = Completer<List<T>>();
|
||||
final results = List<T?>.filled(futures.length, null);
|
||||
var remaining = results.length;
|
||||
if (remaining == 0) {
|
||||
return Future.value(const []);
|
||||
}
|
||||
|
||||
void onResult() {
|
||||
if (--remaining <= 0) {
|
||||
// finished
|
||||
completer.complete(results.cast<T>());
|
||||
}
|
||||
}
|
||||
|
||||
for (final p in futures.withIndex()) {
|
||||
p.item2.then((value) {
|
||||
results[p.item1] = value;
|
||||
onResult();
|
||||
}).onError((error, stackTrace) {
|
||||
results[p.item1] = onError(error!, stackTrace);
|
||||
onResult();
|
||||
});
|
||||
}
|
||||
return completer.future;
|
||||
}
|
|
@ -67,8 +67,13 @@ extension IterableExtension<T> on Iterable<T> {
|
|||
}
|
||||
|
||||
Future<List<U>> computeAll<U>(ComputeCallback<T, U> callback) async {
|
||||
return await compute(
|
||||
_computeAllImpl<T, U>, _ComputeAllMessage(callback, asList()));
|
||||
final list = asList();
|
||||
if (list.isEmpty) {
|
||||
return [];
|
||||
} else {
|
||||
return await compute(
|
||||
_computeAllImpl<T, U>, _ComputeAllMessage(callback, list));
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a list containing elements in this iterable
|
||||
|
|
|
@ -9,7 +9,7 @@ import 'package:tuple/tuple.dart';
|
|||
/// The first returned list contains items exist in [b] but not [a], the second
|
||||
/// returned list contains items exist in [a] but not [b]
|
||||
Tuple2<List<T>, List<T>> diffWith<T>(
|
||||
List<T> a, List<T> b, int Function(T a, T b) comparator) {
|
||||
Iterable<T> a, Iterable<T> b, int Function(T a, T b) comparator) {
|
||||
final aIt = a.iterator, bIt = b.iterator;
|
||||
final aMissing = <T>[], bMissing = <T>[];
|
||||
while (true) {
|
||||
|
@ -31,7 +31,8 @@ Tuple2<List<T>, List<T>> diffWith<T>(
|
|||
}
|
||||
}
|
||||
|
||||
Tuple2<List<T>, List<T>> diff<T extends Comparable>(List<T> a, List<T> b) =>
|
||||
Tuple2<List<T>, List<T>> diff<T extends Comparable>(
|
||||
Iterable<T> a, Iterable<T> b) =>
|
||||
diffWith(a, b, Comparable.compare);
|
||||
|
||||
Tuple2<List<T>, List<T>> _diffUntilEqual<T>(
|
||||
|
|
|
@ -9,7 +9,7 @@ import 'package:nc_photos/widget/my_app.dart';
|
|||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await app_init.initAppLaunch();
|
||||
await app_init.init(app_init.InitIsolateType.main);
|
||||
|
||||
if (platform_k.isMobile) {
|
||||
// reset orientation override just in case, see #59
|
||||
|
|
|
@ -4,9 +4,8 @@ import 'package:event_bus/event_bus.dart';
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/di_container.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;
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
|
@ -14,7 +13,9 @@ import 'package:nc_photos/use_case/update_missing_metadata.dart';
|
|||
|
||||
/// Task to update metadata for missing files
|
||||
class MetadataTask {
|
||||
MetadataTask(this.account, this.pref);
|
||||
MetadataTask(this._c, this.account, this.pref) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.fileRepo);
|
||||
|
||||
@override
|
||||
toString() {
|
||||
|
@ -28,12 +29,11 @@ class MetadataTask {
|
|||
final shareFolder =
|
||||
File(path: file_util.unstripPath(account, pref.getShareFolderOr()));
|
||||
bool hasScanShareFolder = false;
|
||||
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
|
||||
for (final r in account.roots) {
|
||||
final dir = File(path: file_util.unstripPath(account, r));
|
||||
hasScanShareFolder |= file_util.isOrUnderDir(shareFolder, dir);
|
||||
final op = UpdateMissingMetadata(
|
||||
fileRepo, const _UpdateMissingMetadataConfigProvider());
|
||||
_c.fileRepo, const _UpdateMissingMetadataConfigProvider());
|
||||
await for (final _ in op(account, dir)) {
|
||||
if (!Pref().isEnableExifOr()) {
|
||||
_log.info("[call] EXIF disabled, task ending immaturely");
|
||||
|
@ -44,7 +44,7 @@ class MetadataTask {
|
|||
}
|
||||
if (!hasScanShareFolder) {
|
||||
final op = UpdateMissingMetadata(
|
||||
fileRepo, const _UpdateMissingMetadataConfigProvider());
|
||||
_c.fileRepo, const _UpdateMissingMetadataConfigProvider());
|
||||
await for (final _ in op(
|
||||
account,
|
||||
shareFolder,
|
||||
|
@ -65,6 +65,8 @@ class MetadataTask {
|
|||
}
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
final Account account;
|
||||
final AccountPref pref;
|
||||
|
||||
|
|
|
@ -1,4 +1,31 @@
|
|||
import 'package:idb_sqflite/idb_sqflite.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'dart:io' as dart;
|
||||
|
||||
IdbFactory getDbFactory() => getIdbFactorySqflite(databaseFactory);
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
Future<Map<String, dynamic>> getSqliteConnectionArgs() async {
|
||||
// put the database file, called db.sqlite here, into the documents folder
|
||||
// for your app.
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
return {
|
||||
"path": path_lib.join(dbFolder.path, "db.sqlite"),
|
||||
};
|
||||
}
|
||||
|
||||
QueryExecutor openSqliteConnectionWithArgs(Map<String, dynamic> args) {
|
||||
final file = dart.File(args["path"]);
|
||||
return NativeDatabase(
|
||||
file,
|
||||
// logStatements: true,
|
||||
);
|
||||
}
|
||||
|
||||
QueryExecutor openSqliteConnection() {
|
||||
// the LazyDatabase util lets us find the right location for the file async.
|
||||
return LazyDatabase(() async {
|
||||
final args = await getSqliteConnectionArgs();
|
||||
return openSqliteConnectionWithArgs(args);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,3 +8,4 @@ final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
|||
final isAndroid = !kIsWeb && Platform.isAndroid;
|
||||
final isDesktop =
|
||||
!kIsWeb && (Platform.isLinux || Platform.isMacOS || Platform.isWindows);
|
||||
final isUnitTest = !kIsWeb && Platform.environment.containsKey("FLUTTER_TEST");
|
||||
|
|
|
@ -3,13 +3,13 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations_en.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_init.dart' as app_init;
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/di_container.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;
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/event/native_event.dart';
|
||||
|
@ -69,7 +69,7 @@ class _Service {
|
|||
final service = FlutterBackgroundService();
|
||||
service.setForegroundMode(true);
|
||||
|
||||
await app_init.initAppLaunch();
|
||||
await app_init.init(app_init.InitIsolateType.service);
|
||||
await _L10n().init();
|
||||
|
||||
_log.info("[call] Service started");
|
||||
|
@ -87,6 +87,7 @@ class _Service {
|
|||
}
|
||||
onCancelSubscription.cancel();
|
||||
onDataSubscription.cancel();
|
||||
await KiwiContainer().resolve<DiContainer>().sqliteDb.close();
|
||||
service.stopBackgroundService();
|
||||
_log.info("[call] Service stopped");
|
||||
}
|
||||
|
@ -235,12 +236,12 @@ class _MetadataTask {
|
|||
final shareFolder = File(
|
||||
path: file_util.unstripPath(account, accountPref.getShareFolderOr()));
|
||||
bool hasScanShareFolder = false;
|
||||
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
for (final r in account.roots) {
|
||||
final dir = File(path: file_util.unstripPath(account, r));
|
||||
hasScanShareFolder |= file_util.isOrUnderDir(shareFolder, dir);
|
||||
final updater = UpdateMissingMetadata(
|
||||
fileRepo, const _UpdateMissingMetadataConfigProvider());
|
||||
c.fileRepo, const _UpdateMissingMetadataConfigProvider());
|
||||
void onServiceStop() {
|
||||
_log.info("[_updateMetadata] Stopping task: user canceled");
|
||||
updater.stop();
|
||||
|
@ -263,7 +264,7 @@ class _MetadataTask {
|
|||
}
|
||||
if (!hasScanShareFolder) {
|
||||
final shareUpdater = UpdateMissingMetadata(
|
||||
fileRepo, const _UpdateMissingMetadataConfigProvider());
|
||||
c.fileRepo, const _UpdateMissingMetadataConfigProvider());
|
||||
void onServiceStop() {
|
||||
_log.info("[_updateMetadata] Stopping task: user canceled");
|
||||
shareUpdater.stop();
|
||||
|
|
|
@ -6,12 +6,10 @@ import 'package:flutter/services.dart';
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/local_file.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/entity/share/data_source.dart';
|
||||
|
@ -179,9 +177,9 @@ class ShareHandler {
|
|||
clearSelection?.call();
|
||||
isSelectionCleared = true;
|
||||
|
||||
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
|
||||
final path = await _createDir(fileRepo, account, result.albumName);
|
||||
await _copyFilesToDir(fileRepo, account, files, path);
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final path = await _createDir(c.fileRepo, account, result.albumName);
|
||||
await _copyFilesToDir(c.fileRepo, account, files, path);
|
||||
controller?.close();
|
||||
return _shareFileAsLink(
|
||||
account,
|
||||
|
|
|
@ -17,12 +17,12 @@ import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
|
|||
class AddToAlbum {
|
||||
AddToAlbum(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(ListShare.require(_c));
|
||||
assert(ListShare.require(_c)),
|
||||
assert(PreProcessAlbum.require(_c));
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
DiContainer.has(c, DiType.albumRepo) &&
|
||||
DiContainer.has(c, DiType.shareRepo) &&
|
||||
DiContainer.has(c, DiType.appDb);
|
||||
DiContainer.has(c, DiType.shareRepo);
|
||||
|
||||
/// Add a list of AlbumItems to [album]
|
||||
Future<Album> call(
|
||||
|
@ -30,7 +30,7 @@ class AddToAlbum {
|
|||
_log.info("[call] Add ${items.length} items to album '${album.name}'");
|
||||
assert(album.provider is AlbumStaticProvider);
|
||||
// resync is needed to work out album cover and latest item
|
||||
final oldItems = await PreProcessAlbum(_c.appDb)(account, album);
|
||||
final oldItems = await PreProcessAlbum(_c)(account, album);
|
||||
final itemSet = oldItems
|
||||
.map((e) => OverrideComparator<AlbumItem>(
|
||||
e, _isItemFileEqual, _getItemHashCode))
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import 'package:drift/drift.dart' as sql;
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/debug_util.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;
|
||||
|
@ -17,7 +17,7 @@ class CacheFavorite {
|
|||
: assert(require(_c)),
|
||||
assert(ListFavoriteOffline.require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.appDb);
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
/// Cache favorites
|
||||
Future<void> call(
|
||||
|
@ -34,39 +34,45 @@ class CacheFavorite {
|
|||
final newFavorites = result.item1;
|
||||
final removedFavorites =
|
||||
result.item2.map((f) => f.copyWith(isFavorite: false)).toList();
|
||||
if (newFavorites.isEmpty && removedFavorites.isEmpty) {
|
||||
final newFileIds = newFavorites.map((f) => f.fileId!).toList();
|
||||
final removedFileIds = removedFavorites.map((f) => f.fileId!).toList();
|
||||
if (newFileIds.isEmpty && removedFileIds.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await _c.appDb.use(
|
||||
(db) => db.transaction(AppDb.file2StoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
await Future.wait(newFavorites.map((f) async {
|
||||
_log.info("[call] New favorite: ${f.path}");
|
||||
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 {
|
||||
await fileStore.put(AppDbFile2Entry.fromFile(account, f).toJson(),
|
||||
AppDbFile2Entry.toPrimaryKeyForFile(account, f));
|
||||
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] Failed while writing new favorite to AppDb: ${logFilename(f.path)}",
|
||||
e,
|
||||
stackTrace);
|
||||
_log.shout("[call] File not found in DB: $id", e, stackTrace);
|
||||
}
|
||||
}));
|
||||
await Future.wait(removedFavorites.map((f) async {
|
||||
_log.info("[call] Remove favorite: ${f.path}");
|
||||
}
|
||||
for (final id in removedFileIds) {
|
||||
try {
|
||||
await fileStore.put(AppDbFile2Entry.fromFile(account, f).toJson(),
|
||||
AppDbFile2Entry.toPrimaryKeyForFile(account, f));
|
||||
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] Failed while writing removed favorite to AppDb: ${logFilename(f.path)}",
|
||||
e,
|
||||
stackTrace);
|
||||
_log.shout("[call] File not found in DB: $id", e, stackTrace);
|
||||
}
|
||||
}));
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
KiwiContainer()
|
||||
.resolve<EventBus>()
|
||||
|
|
|
@ -1,211 +0,0 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
|
||||
/// Compatibility helper for v37
|
||||
class CompatV37 {
|
||||
static Future<void> setAppDbMigrationFlag(AppDb appDb) async {
|
||||
_log.info("[setAppDbMigrationFlag] Set db flag");
|
||||
try {
|
||||
await appDb.use(
|
||||
(db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
await metaStore
|
||||
.put(const AppDbMetaEntryCompatV37(false).toEntry().toJson());
|
||||
await transaction.completed;
|
||||
},
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[setAppDbMigrationFlag] Failed while setting db flag, drop db instead",
|
||||
e,
|
||||
stackTrace);
|
||||
await appDb.delete();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> isAppDbNeedMigration(AppDb appDb) async {
|
||||
final dbItem = await appDb.use(
|
||||
(db) => db.transaction(AppDb.metaStoreName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
return await metaStore.getObject(AppDbMetaEntryCompatV37.key) as Map?;
|
||||
},
|
||||
);
|
||||
if (dbItem == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
final dbEntry = AppDbMetaEntry.fromJson(dbItem.cast<String, dynamic>());
|
||||
final compatV37 = AppDbMetaEntryCompatV37.fromJson(dbEntry.obj);
|
||||
return !compatV37.isMigrated;
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[isAppDbNeedMigration] Failed", e, stackTrace);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> migrateAppDb(AppDb appDb) async {
|
||||
_log.info("[migrateAppDb] Migrate AppDb");
|
||||
try {
|
||||
await appDb.use(
|
||||
(db) => db.transaction(
|
||||
[AppDb.file2StoreName, AppDb.dirStoreName, AppDb.metaStoreName],
|
||||
idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final noMediaFiles = <_NoMediaFile>[];
|
||||
try {
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
// scan the db to see which dirs contain a no media marker
|
||||
await for (final c in fileStore.openCursor()) {
|
||||
final item = c.value as Map;
|
||||
final strippedPath = item["strippedPath"] as String;
|
||||
if (file_util.isNoMediaMarkerPath(strippedPath)) {
|
||||
noMediaFiles.add(_NoMediaFile(
|
||||
item["server"],
|
||||
item["userId"],
|
||||
path_lib
|
||||
.dirname(item["strippedPath"])
|
||||
.run((p) => p == "." ? "" : p),
|
||||
item["file"]["fileId"],
|
||||
));
|
||||
}
|
||||
c.next();
|
||||
}
|
||||
// sort to make sure parent dirs are always in front of sub dirs
|
||||
noMediaFiles
|
||||
.sort((a, b) => a.strippedDirPath.compareTo(b.strippedDirPath));
|
||||
_log.info(
|
||||
"[migrateAppDb] nomedia dirs: ${noMediaFiles.toReadableString()}");
|
||||
|
||||
if (noMediaFiles.isNotEmpty) {
|
||||
await _migrateAppDbFileStore(appDb, noMediaFiles,
|
||||
fileStore: fileStore);
|
||||
await _migrateAppDbDirStore(appDb, noMediaFiles,
|
||||
dirStore: dirStore);
|
||||
}
|
||||
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
await metaStore
|
||||
.put(const AppDbMetaEntryCompatV37(true).toEntry().toJson());
|
||||
} catch (_) {
|
||||
transaction.abort();
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[migrateAppDb] Failed while migrating, drop db instead", e,
|
||||
stackTrace);
|
||||
await appDb.delete();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove files under no media dirs
|
||||
static Future<void> _migrateAppDbFileStore(
|
||||
AppDb appDb,
|
||||
List<_NoMediaFile> noMediaFiles, {
|
||||
required ObjectStore fileStore,
|
||||
}) async {
|
||||
await for (final c in fileStore.openCursor()) {
|
||||
final item = c.value as Map;
|
||||
final under = noMediaFiles.firstWhereOrNull((e) {
|
||||
if (e.server != item["server"] || e.userId != item["userId"]) {
|
||||
return false;
|
||||
}
|
||||
final prefix = e.strippedDirPath.isEmpty ? "" : "${e.strippedDirPath}/";
|
||||
final itemDir = path_lib
|
||||
.dirname(item["strippedPath"])
|
||||
.run((p) => p == "." ? "" : p);
|
||||
// check isNotEmpty to prevent user root being removed when the
|
||||
// marker is placed in root
|
||||
return item["strippedPath"].isNotEmpty &&
|
||||
item["strippedPath"].startsWith(prefix) &&
|
||||
// keep no media marker in top-most dir
|
||||
!(itemDir == e.strippedDirPath &&
|
||||
file_util.isNoMediaMarkerPath(item["strippedPath"]));
|
||||
});
|
||||
if (under != null) {
|
||||
_log.fine("[_migrateAppDbFileStore] Remove db entry: ${c.primaryKey}");
|
||||
await c.delete();
|
||||
}
|
||||
c.next();
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove dirs under no media dirs
|
||||
static Future<void> _migrateAppDbDirStore(
|
||||
AppDb appDb,
|
||||
List<_NoMediaFile> noMediaFiles, {
|
||||
required ObjectStore dirStore,
|
||||
}) async {
|
||||
await for (final c in dirStore.openCursor()) {
|
||||
final item = c.value as Map;
|
||||
final under = noMediaFiles.firstWhereOrNull((e) {
|
||||
if (e.server != item["server"] || e.userId != item["userId"]) {
|
||||
return false;
|
||||
}
|
||||
final prefix = e.strippedDirPath.isEmpty ? "" : "${e.strippedDirPath}/";
|
||||
return item["strippedPath"].startsWith(prefix) ||
|
||||
e.strippedDirPath == item["strippedPath"];
|
||||
});
|
||||
if (under != null) {
|
||||
if (under.strippedDirPath == item["strippedPath"]) {
|
||||
// this dir contains the no media marker
|
||||
// remove all children, keep only the marker
|
||||
final newChildren = (item["children"] as List)
|
||||
.where((childId) => childId == under.fileId)
|
||||
.toList();
|
||||
if (newChildren.isEmpty) {
|
||||
// ???
|
||||
_log.severe(
|
||||
"[_migrateAppDbDirStore] Marker not found in dir: ${item["strippedPath"]}");
|
||||
// drop this dir
|
||||
await c.delete();
|
||||
}
|
||||
_log.fine(
|
||||
"[_migrateAppDbDirStore] Migrate db entry: ${c.primaryKey}");
|
||||
await c.update(Map.of(item).apply((obj) {
|
||||
obj["children"] = newChildren;
|
||||
}));
|
||||
} else {
|
||||
// this dir is a sub dir
|
||||
// drop this dir
|
||||
_log.fine("[_migrateAppDbDirStore] Remove db entry: ${c.primaryKey}");
|
||||
await c.delete();
|
||||
}
|
||||
}
|
||||
c.next();
|
||||
}
|
||||
}
|
||||
|
||||
static final _log = Logger("use_case.compat.v37.CompatV37");
|
||||
}
|
||||
|
||||
class _NoMediaFile {
|
||||
const _NoMediaFile(
|
||||
this.server, this.userId, this.strippedDirPath, this.fileId);
|
||||
|
||||
@override
|
||||
toString() => "$runtimeType {"
|
||||
"server: $server, "
|
||||
"userId: $userId, "
|
||||
"strippedDirPath: $strippedDirPath, "
|
||||
"fileId: $fileId, "
|
||||
"}";
|
||||
|
||||
final String server;
|
||||
// no need to use CiString as all strings are stored with the same casing in
|
||||
// db
|
||||
final String userId;
|
||||
final String strippedDirPath;
|
||||
final int fileId;
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/ci_string.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
|
||||
class DbCompatV5 {
|
||||
static Future<bool> isNeedMigration(AppDb appDb) async {
|
||||
final dbItem = await appDb.use(
|
||||
(db) => db.transaction(AppDb.metaStoreName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
return await metaStore.getObject(AppDbMetaEntryDbCompatV5.key) as Map?;
|
||||
},
|
||||
);
|
||||
if (dbItem == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
final dbEntry = AppDbMetaEntry.fromJson(dbItem.cast<String, dynamic>());
|
||||
final compatV35 = AppDbMetaEntryDbCompatV5.fromJson(dbEntry.obj);
|
||||
return !compatV35.isMigrated;
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[isNeedMigration] Failed", e, stackTrace);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> migrate(AppDb appDb) async {
|
||||
_log.info("[migrate] Migrate AppDb");
|
||||
try {
|
||||
await appDb.use(
|
||||
(db) => db.transaction(
|
||||
[AppDb.file2StoreName, AppDb.metaStoreName], idbModeReadWrite),
|
||||
(transaction) async {
|
||||
try {
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
await for (final c in fileStore.openCursor()) {
|
||||
final item = c.value as Map;
|
||||
// migrate file entry: add bestDateTime
|
||||
final fileEntry = item.cast<String, dynamic>().run((json) {
|
||||
final f = File.fromJson(json["file"].cast<String, dynamic>());
|
||||
return AppDbFile2Entry(
|
||||
json["server"],
|
||||
(json["userId"] as String).toCi(),
|
||||
json["strippedPath"],
|
||||
f.bestDateTime.millisecondsSinceEpoch,
|
||||
File.fromJson(json["file"].cast<String, dynamic>()),
|
||||
);
|
||||
});
|
||||
await c.update(fileEntry.toJson());
|
||||
|
||||
c.next();
|
||||
}
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
await metaStore
|
||||
.put(const AppDbMetaEntryDbCompatV5(true).toEntry().toJson());
|
||||
} catch (_) {
|
||||
transaction.abort();
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[migrate] Failed while migrating, drop db instead", e, stackTrace);
|
||||
await appDb.delete();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static final _log = Logger("use_case.db_compat.v5.DbCompatV5");
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
|
||||
class FindFile {
|
||||
FindFile(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.appDb);
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
/// Find list of files in the DB by [fileIds]
|
||||
///
|
||||
|
@ -19,37 +19,34 @@ class FindFile {
|
|||
List<int> fileIds, {
|
||||
void Function(int fileId)? onFileNotFound,
|
||||
}) async {
|
||||
final dbItems = await _c.appDb.use(
|
||||
(db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
return await Future.wait(fileIds.map((id) =>
|
||||
fileStore.getObject(AppDbFile2Entry.toPrimaryKey(account, id))));
|
||||
},
|
||||
);
|
||||
final fileMap = await compute(_covertFileMap, dbItems);
|
||||
final files = <File>[];
|
||||
for (final id in fileIds) {
|
||||
final f = fileMap[id];
|
||||
if (f == null) {
|
||||
if (onFileNotFound == null) {
|
||||
throw StateError("File ID not found: $id");
|
||||
} else {
|
||||
onFileNotFound(id);
|
||||
}
|
||||
} else {
|
||||
files.add(f);
|
||||
}
|
||||
_log.info("[call] fileIds: ${fileIds.toReadableString()}");
|
||||
final dbFiles = await _c.sqliteDb.use((db) async {
|
||||
return await db.completeFilesByFileIds(fileIds, appAccount: account);
|
||||
});
|
||||
final files = await dbFiles.convertToAppFile(account);
|
||||
final fileMap = <int, File>{};
|
||||
for (final f in files) {
|
||||
fileMap[f.fileId!] = f;
|
||||
}
|
||||
return files;
|
||||
|
||||
return () sync* {
|
||||
for (final id in fileIds) {
|
||||
final f = fileMap[id];
|
||||
if (f == null) {
|
||||
if (onFileNotFound == null) {
|
||||
throw StateError("File ID not found: $id");
|
||||
} else {
|
||||
onFileNotFound(id);
|
||||
}
|
||||
} else {
|
||||
yield fileMap[id]!;
|
||||
}
|
||||
}
|
||||
}()
|
||||
.toList();
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
}
|
||||
|
||||
Map<int, File> _covertFileMap(List<Object?> dbItems) {
|
||||
return Map.fromEntries(dbItems
|
||||
.whereType<Map>()
|
||||
.map((j) => AppDbFile2Entry.fromJson(j.cast<String, dynamic>()).file)
|
||||
.map((f) => MapEntry(f.fileId!, f)));
|
||||
static final _log = Logger("use_case.find_file.FindFile");
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
|
@ -50,30 +50,22 @@ class ListAlbum {
|
|||
yield ExceptionEvent(e, stackTrace);
|
||||
return;
|
||||
}
|
||||
final albumFiles =
|
||||
final List<File?> albumFiles =
|
||||
ls.where((element) => element.isCollection != true).toList();
|
||||
// migrate files
|
||||
for (var i = 0; i < albumFiles.length; ++i) {
|
||||
var f = albumFiles[i];
|
||||
var f = albumFiles[i]!;
|
||||
try {
|
||||
if (CompatV25.isAlbumFileNeedMigration(f)) {
|
||||
f = await CompatV25.migrateAlbumFile(_c, account, f);
|
||||
albumFiles[i] = await CompatV25.migrateAlbumFile(_c, account, f);
|
||||
}
|
||||
albumFiles[i] = f;
|
||||
yield await _c.albumRepo.get(account, f);
|
||||
} catch (e, stackTrace) {
|
||||
yield ExceptionEvent(e, stackTrace);
|
||||
albumFiles[i] = null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
_c.albumRepo.cleanUp(
|
||||
account, remote_storage_util.getRemoteAlbumsDir(account), albumFiles);
|
||||
} catch (e, stacktrace) {
|
||||
// not important, log and ignore
|
||||
_log.shout("[_call] Failed while cleanUp", e, stacktrace);
|
||||
}
|
||||
yield* _c.albumRepo.getAll(account, albumFiles.whereNotNull().toList());
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
static final _log = Logger("use_case.list_album.ListAlbum");
|
||||
}
|
||||
|
|
|
@ -1,43 +1,19 @@
|
|||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.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/find_file.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
|
||||
class ListFavoriteOffline {
|
||||
ListFavoriteOffline(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(FindFile.require(_c));
|
||||
ListFavoriteOffline(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.appDb);
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
/// List all favorites for [account] from the local DB
|
||||
Future<List<File>> call(Account account) {
|
||||
final rootDirs = account.roots
|
||||
.map((r) => File(path: file_util.unstripPath(account, r)))
|
||||
.toList();
|
||||
return _c.appDb.use(
|
||||
(db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
final fileIsFavoriteIndex =
|
||||
fileStore.index(AppDbFile2Entry.fileIsFavoriteIndexName);
|
||||
return await fileIsFavoriteIndex
|
||||
.openCursor(
|
||||
key: AppDbFile2Entry.toFileIsFavoriteIndexKey(account, true),
|
||||
autoAdvance: true,
|
||||
)
|
||||
.map((c) => AppDbFile2Entry.fromJson(
|
||||
(c.value as Map).cast<String, dynamic>()))
|
||||
.map((e) => e.file)
|
||||
.where((f) =>
|
||||
file_util.isSupportedFormat(f) &&
|
||||
rootDirs.any((r) => file_util.isOrUnderDir(f, r)))
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
Future<List<File>> call(Account account) async {
|
||||
final dbFiles = await _c.sqliteDb.use((db) async {
|
||||
return await db.completeFilesByFavorite(appAccount: account);
|
||||
});
|
||||
return await dbFiles.convertToAppFile(account);
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
|
@ -15,7 +14,9 @@ import 'package:nc_photos/use_case/list_tagged_file.dart';
|
|||
import 'package:nc_photos/use_case/scan_dir.dart';
|
||||
|
||||
class PopulateAlbum {
|
||||
const PopulateAlbum(this.appDb);
|
||||
PopulateAlbum(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.fileRepo);
|
||||
|
||||
Future<List<AlbumItem>> call(Account account, Album album) async {
|
||||
if (album.provider is AlbumStaticProvider) {
|
||||
|
@ -40,7 +41,7 @@ class PopulateAlbum {
|
|||
final provider = album.provider as AlbumDirProvider;
|
||||
final products = <AlbumItem>[];
|
||||
for (final d in provider.dirs) {
|
||||
final stream = ScanDir(FileRepo(FileCachedDataSource(appDb)))(account, d);
|
||||
final stream = ScanDir(_c.fileRepo)(account, d);
|
||||
await for (final result in stream) {
|
||||
if (result is ExceptionEvent) {
|
||||
_log.shout(
|
||||
|
@ -81,7 +82,7 @@ class PopulateAlbum {
|
|||
final date = DateTime(provider.year, provider.month, provider.day);
|
||||
final from = date.subtract(const Duration(days: 2));
|
||||
final to = date.add(const Duration(days: 3));
|
||||
final files = await FileAppDbDataSource(appDb).listByDate(
|
||||
final files = await FileSqliteDbDataSource(_c).listByDate(
|
||||
account, from.millisecondsSinceEpoch, to.millisecondsSinceEpoch);
|
||||
return files
|
||||
.where((f) => file_util.isSupportedFormat(f))
|
||||
|
@ -93,7 +94,7 @@ class PopulateAlbum {
|
|||
.toList();
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
final DiContainer _c;
|
||||
|
||||
static final _log = Logger("use_case.populate_album.PopulateAlbum");
|
||||
}
|
||||
|
|
|
@ -1,51 +1,38 @@
|
|||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/face.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
|
||||
class PopulatePerson {
|
||||
const PopulatePerson(this.appDb);
|
||||
PopulatePerson(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
/// Return a list of files of the faces
|
||||
Future<List<File>> call(Account account, List<Face> faces) async {
|
||||
return await appDb.use(
|
||||
(db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final store = transaction.objectStore(AppDb.file2StoreName);
|
||||
final products = <File>[];
|
||||
for (final f in faces) {
|
||||
try {
|
||||
products.add(await _populateOne(account, f, fileStore: store));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[call] Failed populating file of face: ${f.fileId}", e,
|
||||
stackTrace);
|
||||
final fileIds = faces.map((f) => f.fileId).toList();
|
||||
final dbFiles = await _c.sqliteDb.use((db) async {
|
||||
return await db.completeFilesByFileIds(fileIds, appAccount: account);
|
||||
});
|
||||
final files = await dbFiles.convertToAppFile(account);
|
||||
final fileMap = Map.fromEntries(files.map((f) => MapEntry(f.fileId, f)));
|
||||
return faces
|
||||
.map((f) {
|
||||
final file = fileMap[f.fileId];
|
||||
if (file == null) {
|
||||
_log.warning(
|
||||
"[call] File doesn't exist in DB, removed?: ${f.fileId}");
|
||||
}
|
||||
}
|
||||
return products;
|
||||
},
|
||||
);
|
||||
return file;
|
||||
})
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<File> _populateOne(
|
||||
Account account,
|
||||
Face face, {
|
||||
required ObjectStore fileStore,
|
||||
}) async {
|
||||
final dbItem = await fileStore
|
||||
.getObject(AppDbFile2Entry.toPrimaryKey(account, face.fileId)) as Map?;
|
||||
if (dbItem == null) {
|
||||
_log.warning(
|
||||
"[_populateOne] File doesn't exist in DB, removed?: '${face.fileId}'");
|
||||
throw CacheNotFoundException();
|
||||
}
|
||||
final dbEntry = AppDbFile2Entry.fromJson(dbItem.cast<String, dynamic>());
|
||||
return dbEntry.file;
|
||||
}
|
||||
final DiContainer _c;
|
||||
|
||||
final AppDb appDb;
|
||||
|
||||
static final _log = Logger("use_case.populate_album.PopulatePerson");
|
||||
static final _log = Logger("use_case.populate_person.PopulatePerson");
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
|
@ -12,19 +12,24 @@ import 'package:nc_photos/use_case/resync_album.dart';
|
|||
/// - with AlbumStaticProvider: [ResyncAlbum]
|
||||
/// - with AlbumDynamicProvider/AlbumSmartProvider: [PopulateAlbum]
|
||||
class PreProcessAlbum {
|
||||
const PreProcessAlbum(this.appDb);
|
||||
PreProcessAlbum(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(PopulateAlbum.require(_c)),
|
||||
assert(ResyncAlbum.require(_c));
|
||||
|
||||
static bool require(DiContainer c) => true;
|
||||
|
||||
Future<List<AlbumItem>> call(Account account, Album album) {
|
||||
if (album.provider is AlbumStaticProvider) {
|
||||
return ResyncAlbum(appDb)(account, album);
|
||||
return ResyncAlbum(_c)(account, album);
|
||||
} else if (album.provider is AlbumDynamicProvider ||
|
||||
album.provider is AlbumSmartProvider) {
|
||||
return PopulateAlbum(appDb)(account, album);
|
||||
return PopulateAlbum(_c)(account, album);
|
||||
} else {
|
||||
throw ArgumentError(
|
||||
"Unknown album provider: ${album.provider.runtimeType}");
|
||||
}
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
final DiContainer _c;
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@ import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
|
|||
class RemoveFromAlbum {
|
||||
RemoveFromAlbum(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(UnshareFileFromAlbum.require(_c));
|
||||
assert(UnshareFileFromAlbum.require(_c)),
|
||||
assert(PreProcessAlbum.require(_c));
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
DiContainer.has(c, DiType.albumRepo) && DiContainer.has(c, DiType.appDb);
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.albumRepo);
|
||||
|
||||
/// Remove a list of AlbumItems from [album]
|
||||
///
|
||||
|
@ -91,7 +91,7 @@ class RemoveFromAlbum {
|
|||
_log.info(
|
||||
"[_fixAlbumPostRemove] Resync as interesting item is being removed");
|
||||
// need to update the album properties
|
||||
final newItemsSynced = await PreProcessAlbum(_c.appDb)(account, newAlbum);
|
||||
final newItemsSynced = await PreProcessAlbum(_c)(account, newAlbum);
|
||||
newAlbum = await UpdateAlbumWithActualItems(null)(
|
||||
account,
|
||||
newAlbum,
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/use_case/find_file.dart';
|
||||
|
||||
/// Resync files inside an album with the file db
|
||||
class ResyncAlbum {
|
||||
const ResyncAlbum(this.appDb);
|
||||
ResyncAlbum(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(FindFile.require(_c));
|
||||
|
||||
static bool require(DiContainer c) => true;
|
||||
|
||||
Future<List<AlbumItem>> call(Account account, Album album) async {
|
||||
_log.info("[call] Resync album: ${album.name}");
|
||||
|
@ -20,22 +22,27 @@ class ResyncAlbum {
|
|||
"Resync only make sense for static albums: ${album.name}");
|
||||
}
|
||||
final items = AlbumStaticProvider.of(album).items;
|
||||
final dbItems = await appDb.use(
|
||||
(db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final store = transaction.objectStore(AppDb.file2StoreName);
|
||||
return await Future.wait(items.whereType<AlbumFileItem>().map((i) =>
|
||||
store.getObject(
|
||||
AppDbFile2Entry.toPrimaryKey(account, i.file.fileId!))));
|
||||
},
|
||||
|
||||
final files = await FindFile(_c)(
|
||||
account,
|
||||
items.whereType<AlbumFileItem>().map((i) => i.file.fileId!).toList(),
|
||||
onFileNotFound: (_) {},
|
||||
);
|
||||
final fileMap = await compute(_covertFileMap, dbItems);
|
||||
final fileIt = files.iterator;
|
||||
var nextFile = fileIt.moveNext() ? fileIt.current : null;
|
||||
return items.map((i) {
|
||||
if (i is AlbumFileItem) {
|
||||
try {
|
||||
return i.copyWith(
|
||||
file: fileMap[i.file.fileId]!,
|
||||
);
|
||||
if (i.file.fileId! == nextFile?.fileId) {
|
||||
final newItem = i.copyWith(
|
||||
file: nextFile,
|
||||
);
|
||||
nextFile = fileIt.moveNext() ? fileIt.current : null;
|
||||
return newItem;
|
||||
} else {
|
||||
_log.warning("[call] File not found: ${logFilename(i.file.path)}");
|
||||
return i;
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[call] Failed syncing file in album: ${logFilename(i.file.path)}",
|
||||
|
@ -49,14 +56,7 @@ class ResyncAlbum {
|
|||
}).toList();
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
final DiContainer _c;
|
||||
|
||||
static final _log = Logger("use_case.resync_album.ResyncAlbum");
|
||||
}
|
||||
|
||||
Map<int, File> _covertFileMap(List<Object?> dbItems) {
|
||||
return Map.fromEntries(dbItems
|
||||
.whereType<Map>()
|
||||
.map((j) => AppDbFile2Entry.fromJson(j.cast<String, dynamic>()).file)
|
||||
.map((f) => MapEntry(f.fileId!, f)));
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.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/iterable_extension.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_converter.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
|
||||
class ScanDirOffline {
|
||||
ScanDirOffline(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.appDb);
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
/// List all files under a dir recursively from the local DB
|
||||
Future<Iterable<File>> call(
|
||||
Future<List<File>> call(
|
||||
Account account,
|
||||
File root, {
|
||||
bool isOnlySupportedFormat = true,
|
||||
}) async {
|
||||
final dbItems = await _c.appDb.use(
|
||||
(db) => db.transaction(AppDb.file2StoreName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final store = transaction.objectStore(AppDb.file2StoreName);
|
||||
final index = store.index(AppDbFile2Entry.strippedPathIndexName);
|
||||
final range = KeyRange.bound(
|
||||
AppDbFile2Entry.toStrippedPathIndexLowerKeyForDir(account, root),
|
||||
AppDbFile2Entry.toStrippedPathIndexUpperKeyForDir(account, root),
|
||||
);
|
||||
return await index
|
||||
.openCursor(range: range, autoAdvance: false)
|
||||
.map((c) {
|
||||
final v = c.value as Map;
|
||||
c.next();
|
||||
return v;
|
||||
}).toList();
|
||||
},
|
||||
);
|
||||
final results = await dbItems.computeAll(_covertAppDbFile2Entry);
|
||||
if (isOnlySupportedFormat) {
|
||||
return results.where((f) => file_util.isSupportedFormat(f));
|
||||
} else {
|
||||
return results;
|
||||
}
|
||||
return await _c.sqliteDb.isolate({
|
||||
"account": account,
|
||||
"root": root,
|
||||
"isOnlySupportedFormat": isOnlySupportedFormat,
|
||||
}, (db, Map args) async {
|
||||
final Account account = args["account"];
|
||||
final File root = args["root"];
|
||||
final bool isOnlySupportedFormat = args["isOnlySupportedFormat"];
|
||||
final dbFiles = await db.useInIsolate((db) async {
|
||||
final query = db.queryFiles().run((q) {
|
||||
q
|
||||
..setQueryMode(sql.FilesQueryMode.completeFile)
|
||||
..setAppAccount(account);
|
||||
root.strippedPathWithEmpty.run((p) {
|
||||
if (p.isNotEmpty) {
|
||||
q.byRelativePathPattern("$p/%");
|
||||
}
|
||||
});
|
||||
if (isOnlySupportedFormat) {
|
||||
q
|
||||
..byMimePattern("image/%")
|
||||
..byMimePattern("video/%");
|
||||
}
|
||||
return q.build();
|
||||
});
|
||||
return await query
|
||||
.map((r) => sql.CompleteFile(
|
||||
r.readTable(db.files),
|
||||
r.readTable(db.accountFiles),
|
||||
r.readTableOrNull(db.images),
|
||||
r.readTableOrNull(db.trashes),
|
||||
))
|
||||
.get();
|
||||
});
|
||||
return dbFiles
|
||||
.map(
|
||||
(f) => SqliteFileConverter.fromSql(account.homeDir.toString(), f))
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
}
|
||||
|
||||
File _covertAppDbFile2Entry(Map json) =>
|
||||
AppDbFile2Entry.fromJson(json.cast<String, dynamic>()).file;
|
||||
|
|
|
@ -1,4 +1,30 @@
|
|||
import 'package:idb_shim/idb_browser.dart';
|
||||
import 'package:idb_shim/idb_shim.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/wasm.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
|
||||
IdbFactory getDbFactory() => idbFactoryBrowser;
|
||||
Future<Map<String, dynamic>> getSqliteConnectionArgs() async => {};
|
||||
|
||||
QueryExecutor openSqliteConnectionWithArgs(Map<String, dynamic> args) =>
|
||||
openSqliteConnection();
|
||||
|
||||
QueryExecutor openSqliteConnection() {
|
||||
return LazyDatabase(() async {
|
||||
// Load wasm bundle
|
||||
final response = await http.get(Uri.parse("sqlite3.wasm"));
|
||||
// Create a virtual file system backed by IndexedDb with everything in
|
||||
// `/drift/my_app/` being persisted.
|
||||
final fs = await IndexedDbFileSystem.open(dbName: "nc-photos");
|
||||
final sqlite3 = await WasmSqlite3.load(
|
||||
response.bodyBytes,
|
||||
SqliteEnvironment(fileSystem: fs),
|
||||
);
|
||||
|
||||
// Then, open a database inside that persisted folder.
|
||||
return WasmDatabase(
|
||||
sqlite3: sqlite3,
|
||||
path: "/drift/nc-photos/app.db",
|
||||
// logStatements: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'package:kiwi/kiwi.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/download_handler.dart';
|
||||
|
@ -82,6 +81,15 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
SelectableItemStreamListMixin<AlbumBrowser>,
|
||||
DraggableItemListMixin<AlbumBrowser>,
|
||||
AlbumBrowserMixin<AlbumBrowser> {
|
||||
_AlbumBrowserState() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(require(c));
|
||||
assert(PreProcessAlbum.require(c));
|
||||
_c = c;
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.albumRepo);
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
@ -156,11 +164,10 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
if (newAlbum.copyWith(lastUpdated: OrNull(_album!.lastUpdated)) !=
|
||||
_album) {
|
||||
_log.info("[doneEditMode] Album modified: $newAlbum");
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
setState(() {
|
||||
_album = newAlbum;
|
||||
});
|
||||
UpdateAlbum(albumRepo)(
|
||||
UpdateAlbum(_c.albumRepo)(
|
||||
widget.account,
|
||||
newAlbum,
|
||||
).catchError((e, stackTrace) {
|
||||
|
@ -184,15 +191,14 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
}
|
||||
|
||||
Future<void> _initAlbum() async {
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
var album = await albumRepo.get(widget.account, widget.album.albumFile!);
|
||||
var album = await _c.albumRepo.get(widget.account, widget.album.albumFile!);
|
||||
if (widget.album.shares?.isNotEmpty == true) {
|
||||
try {
|
||||
final file = await LsSingleFile(KiwiContainer().resolve<DiContainer>())(
|
||||
widget.account, album.albumFile!.path);
|
||||
final file =
|
||||
await LsSingleFile(_c)(widget.account, album.albumFile!.path);
|
||||
if (file.etag != album.albumFile!.etag) {
|
||||
_log.info("[_initAlbum] Album modified in remote, forcing download");
|
||||
album = await albumRepo.get(widget.account, File(path: file.path));
|
||||
album = await _c.albumRepo.get(widget.account, File(path: file.path));
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("[_initAlbum] Failed while syncing remote album file", e,
|
||||
|
@ -815,7 +821,7 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
|
||||
Future<void> _setAlbum(Album album) async {
|
||||
assert(album.provider is AlbumStaticProvider);
|
||||
final items = await PreProcessAlbum(AppDb())(widget.account, album);
|
||||
final items = await PreProcessAlbum(_c)(widget.account, album);
|
||||
if (album.albumFile!.isOwned(widget.account.username)) {
|
||||
album = await _updateAlbumPostResync(album, items);
|
||||
}
|
||||
|
@ -835,8 +841,7 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
|
||||
Future<Album> _updateAlbumPostResync(
|
||||
Album album, List<AlbumItem> items) async {
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
return await UpdateAlbumWithActualItems(albumRepo)(
|
||||
return await UpdateAlbumWithActualItems(_c.albumRepo)(
|
||||
widget.account, album, items);
|
||||
}
|
||||
|
||||
|
@ -866,6 +871,8 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
static List<AlbumItem> _getAlbumItemsOf(Album a) =>
|
||||
AlbumStaticProvider.of(a).items;
|
||||
|
||||
late final DiContainer _c;
|
||||
|
||||
Album? _album;
|
||||
var _sortedItems = <AlbumItem>[];
|
||||
var _backingFiles = <File>[];
|
||||
|
|
|
@ -3,12 +3,12 @@ import 'package:kiwi/kiwi.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/data_source.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/notified_action.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
|
@ -197,10 +197,11 @@ mixin AlbumBrowserMixin<T extends StatefulWidget>
|
|||
|
||||
Future<void> _onUnsetCoverPressed(Account account, Album album) async {
|
||||
_log.info("[_onUnsetCoverPressed] Unset album cover for '${album.name}'");
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
try {
|
||||
await NotifiedAction(
|
||||
() async {
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(c));
|
||||
await UpdateAlbum(albumRepo)(
|
||||
account,
|
||||
album.copyWith(
|
||||
|
|
|
@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc/list_importable_album.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
|
@ -54,6 +53,15 @@ class AlbumImporter extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _AlbumImporterState extends State<AlbumImporter> {
|
||||
_AlbumImporterState() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(require(c));
|
||||
assert(PreProcessAlbum.require(c));
|
||||
_c = c;
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.albumRepo);
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
@ -236,12 +244,11 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
|||
);
|
||||
_log.info("[_createAllAlbums] Creating dir album: $album");
|
||||
|
||||
final items = await PreProcessAlbum(AppDb())(widget.account, album);
|
||||
final items = await PreProcessAlbum(_c)(widget.account, album);
|
||||
album = await UpdateAlbumWithActualItems(null)(
|
||||
widget.account, album, items);
|
||||
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
await CreateAlbum(albumRepo)(widget.account, album);
|
||||
await CreateAlbum(_c.albumRepo)(widget.account, album);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_createAllAlbums] Failed creating dir album", e, stacktrace);
|
||||
|
@ -260,6 +267,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
|||
.toList();
|
||||
}
|
||||
|
||||
late final DiContainer _c;
|
||||
late ListImportableAlbumBloc _bloc;
|
||||
|
||||
var _backingFiles = <File>[];
|
||||
|
|
|
@ -2,15 +2,15 @@ import 'dart:ui';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc/scan_account_dir.dart';
|
||||
import 'package:nc_photos/compute_queue.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/language_util.dart' as language_util;
|
||||
|
@ -237,11 +237,11 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final failures = <File>[];
|
||||
for (final f in selectedFiles) {
|
||||
try {
|
||||
await UpdateProperty(fileRepo)
|
||||
await UpdateProperty(c.fileRepo)
|
||||
.updateIsArchived(widget.account, f, false);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'package:kiwi/kiwi.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
|
@ -76,6 +75,15 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
|
|||
with
|
||||
SelectableItemStreamListMixin<DynamicAlbumBrowser>,
|
||||
AlbumBrowserMixin<DynamicAlbumBrowser> {
|
||||
_DynamicAlbumBrowserState() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(require(c));
|
||||
assert(PreProcessAlbum.require(c));
|
||||
_c = c;
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.albumRepo);
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
@ -139,11 +147,10 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
|
|||
if (newAlbum.copyWith(lastUpdated: OrNull(_album!.lastUpdated)) !=
|
||||
_album) {
|
||||
_log.info("[doneEditMode] Album modified: $newAlbum");
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
setState(() {
|
||||
_album = newAlbum;
|
||||
});
|
||||
UpdateAlbum(albumRepo)(
|
||||
UpdateAlbum(_c.albumRepo)(
|
||||
widget.account,
|
||||
newAlbum,
|
||||
).catchError((e, stackTrace) {
|
||||
|
@ -171,7 +178,7 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
|
|||
final List<AlbumItem> items;
|
||||
final Album album;
|
||||
try {
|
||||
items = await PreProcessAlbum(AppDb())(widget.account, widget.album);
|
||||
items = await PreProcessAlbum(_c)(widget.account, widget.album);
|
||||
album = await _updateAlbumPostPopulate(widget.album, items);
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[_initAlbum] Failed while PreProcessAlbum", e, stackTrace);
|
||||
|
@ -350,9 +357,8 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
|
|||
}
|
||||
_log.info(
|
||||
"[_onConvertBasicPressed] Converting album '${_album!.name}' to static");
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
try {
|
||||
await UpdateAlbum(albumRepo)(
|
||||
await UpdateAlbum(_c.albumRepo)(
|
||||
widget.account,
|
||||
_album!.copyWith(
|
||||
provider: AlbumStaticProvider(
|
||||
|
@ -632,11 +638,12 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
|
|||
|
||||
Future<Album> _updateAlbumPostPopulate(
|
||||
Album album, List<AlbumItem> items) async {
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
return await UpdateAlbumWithActualItems(albumRepo)(
|
||||
return await UpdateAlbumWithActualItems(_c.albumRepo)(
|
||||
widget.account, album, items);
|
||||
}
|
||||
|
||||
late final DiContainer _c;
|
||||
|
||||
Album? _album;
|
||||
var _sortedItems = <AlbumItem>[];
|
||||
var _backingFiles = <File>[];
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/data_source.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
|
|
|
@ -737,8 +737,9 @@ class _Web {
|
|||
}
|
||||
|
||||
void startMetadataTask(int missingMetadataCount) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
MetadataTaskManager().addTask(MetadataTask(
|
||||
state.widget.account, AccountPref.of(state.widget.account)));
|
||||
c, state.widget.account, AccountPref.of(state.widget.account)));
|
||||
_metadataTaskProcessTotalCount = missingMetadataCount;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
|
@ -37,6 +36,14 @@ class NewAlbumDialog extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
||||
_NewAlbumDialogState() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(require(c));
|
||||
_c = c;
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.albumRepo);
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
@ -139,8 +146,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
|||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
);
|
||||
_log.info("[_onConfirmStaticAlbum] Creating static album: $album");
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
final newAlbum = await CreateAlbum(albumRepo)(widget.account, album);
|
||||
final newAlbum = await CreateAlbum(_c.albumRepo)(widget.account, album);
|
||||
Navigator.of(context).pop(newAlbum);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout("[_onConfirmStaticAlbum] Failed", e, stacktrace);
|
||||
|
@ -173,8 +179,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
|||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
);
|
||||
_log.info("[_onConfirmDirAlbum] Creating dir album: $album");
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
final newAlbum = await CreateAlbum(albumRepo)(widget.account, album);
|
||||
final newAlbum = await CreateAlbum(_c.albumRepo)(widget.account, album);
|
||||
Navigator.of(context).pop(newAlbum);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout("[_onConfirmDirAlbum] Failed", e, stacktrace);
|
||||
|
@ -220,6 +225,8 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
|||
}
|
||||
}
|
||||
|
||||
late final DiContainer _c;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
var _provider = _Provider.static;
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc/list_face.dart';
|
||||
import 'package:nc_photos/cache_manager_util.dart';
|
||||
|
@ -76,6 +75,14 @@ class PersonBrowser extends StatefulWidget {
|
|||
|
||||
class _PersonBrowserState extends State<PersonBrowser>
|
||||
with SelectableItemStreamListMixin<PersonBrowser> {
|
||||
_PersonBrowserState() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(require(c));
|
||||
_c = c;
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
@ -414,7 +421,7 @@ class _PersonBrowserState extends State<PersonBrowser>
|
|||
}
|
||||
|
||||
void _transformItems(List<Face> items) async {
|
||||
final files = await PopulatePerson(AppDb())(widget.account, items);
|
||||
final files = await PopulatePerson(_c)(widget.account, items);
|
||||
_backingFiles = files
|
||||
.sorted(compareFileDateTimeDescending)
|
||||
.where((element) =>
|
||||
|
@ -442,6 +449,8 @@ class _PersonBrowserState extends State<PersonBrowser>
|
|||
_bloc.add(ListFaceBlocQuery(widget.account, widget.person));
|
||||
}
|
||||
|
||||
late final DiContainer _c;
|
||||
|
||||
final ListFaceBloc _bloc = ListFaceBloc();
|
||||
List<File>? _backingFiles;
|
||||
|
||||
|
|
|
@ -12,9 +12,9 @@ import 'package:nc_photos/language_util.dart' as language_util;
|
|||
import 'package:nc_photos/mobile/android/android_info.dart';
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
import 'package:nc_photos/platform/features.dart' as features;
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/platform/notification.dart';
|
||||
import 'package:nc_photos/platform/features.dart' as features;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/service.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
|
|
|
@ -5,7 +5,6 @@ import 'package:kiwi/kiwi.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/async_util.dart' as async_util;
|
||||
import 'package:nc_photos/bloc/list_sharee.dart';
|
||||
|
@ -13,8 +12,6 @@ import 'package:nc_photos/bloc/search_suggestion.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/share.dart';
|
||||
import 'package:nc_photos/entity/share/data_source.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
|
@ -41,6 +38,16 @@ class ShareAlbumDialog extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
|
||||
_ShareAlbumDialogState() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(require(c));
|
||||
_c = c;
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
DiContainer.has(c, DiType.albumRepo) &&
|
||||
DiContainer.has(c, DiType.shareRepo);
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
@ -227,12 +234,10 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
|
|||
}
|
||||
|
||||
Future<bool> _createShare(Sharee sharee) async {
|
||||
final shareRepo = ShareRepo(ShareRemoteDataSource());
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
var hasFailure = false;
|
||||
try {
|
||||
_album = await _editMutex.protect(() async {
|
||||
return await ShareAlbumWithUser(shareRepo, albumRepo)(
|
||||
return await ShareAlbumWithUser(_c.shareRepo, _c.albumRepo)(
|
||||
widget.account,
|
||||
_album,
|
||||
sharee,
|
||||
|
@ -320,6 +325,8 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
|
|||
}
|
||||
}
|
||||
|
||||
late final DiContainer _c;
|
||||
|
||||
late final _shareeBloc = ListShareeBloc.of(widget.account);
|
||||
final _suggestionBloc = SearchSuggestionBloc<Sharee>(
|
||||
itemToKeywords: (item) => [item.shareWith, item.label.toCi()],
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:nc_photos/bloc/list_sharing.dart';
|
|||
import 'package:nc_photos/cache_manager_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/data_source.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/ci_string.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/help_utils.dart' as help_utils;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
|
@ -253,6 +256,10 @@ class _SignInState extends State<SignIn> {
|
|||
return;
|
||||
}
|
||||
// we've got a good account
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
await c.sqliteDb.use((db) async {
|
||||
await db.insertAccountOf(account!);
|
||||
});
|
||||
// only signing in with app password would trigger distinct
|
||||
final accounts = (Pref().getAccounts3Or([])..add(account)).distinct();
|
||||
try {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/download_handler.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
|
@ -60,6 +61,12 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
|
|||
with
|
||||
SelectableItemStreamListMixin<SmartAlbumBrowser>,
|
||||
AlbumBrowserMixin<SmartAlbumBrowser> {
|
||||
_SmartAlbumBrowserState() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(PreProcessAlbum.require(c));
|
||||
_c = c;
|
||||
}
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
@ -89,7 +96,7 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
|
|||
Future<void> _initAlbum() async {
|
||||
assert(widget.album.provider is AlbumSmartProvider);
|
||||
_log.info("[_initAlbum] ${widget.album}");
|
||||
final items = await PreProcessAlbum(AppDb())(widget.account, widget.album);
|
||||
final items = await PreProcessAlbum(_c)(widget.account, widget.album);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_album = widget.album;
|
||||
|
@ -314,6 +321,8 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
|
|||
.toList();
|
||||
}
|
||||
|
||||
late final DiContainer _c;
|
||||
|
||||
Album? _album;
|
||||
var _sortedItems = <AlbumItem>[];
|
||||
var _backingFiles = <File>[];
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/changelog.dart' as changelog;
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/mobile/android/activity.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/use_case/compat/v29.dart';
|
||||
import 'package:nc_photos/use_case/compat/v37.dart';
|
||||
import 'package:nc_photos/use_case/db_compat/v5.dart';
|
||||
import 'package:nc_photos/widget/home.dart';
|
||||
import 'package:nc_photos/widget/processing_dialog.dart';
|
||||
import 'package:nc_photos/widget/setup.dart';
|
||||
|
@ -47,7 +42,6 @@ class _SplashState extends State<Splash> {
|
|||
if (_shouldUpgrade()) {
|
||||
await _handleUpgrade();
|
||||
}
|
||||
await _migrateDb();
|
||||
_initTimedExit();
|
||||
}
|
||||
|
||||
|
@ -162,10 +156,6 @@ class _SplashState extends State<Splash> {
|
|||
showUpdateDialog();
|
||||
await _upgrade29(lastVersion);
|
||||
}
|
||||
if (lastVersion < 370) {
|
||||
showUpdateDialog();
|
||||
await _upgrade37(lastVersion);
|
||||
}
|
||||
if (isShowDialog) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
@ -181,53 +171,6 @@ class _SplashState extends State<Splash> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _upgrade37(int lastVersion) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
return CompatV37.setAppDbMigrationFlag(c.appDb);
|
||||
}
|
||||
|
||||
Future<void> _migrateDb() async {
|
||||
bool isShowDialog = false;
|
||||
void showUpdateDialog() {
|
||||
if (!isShowDialog) {
|
||||
isShowDialog = true;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => ProcessingDialog(
|
||||
text: L10n.global().migrateDatabaseProcessingNotification,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
if (await DbCompatV5.isNeedMigration(c.appDb)) {
|
||||
showUpdateDialog();
|
||||
try {
|
||||
await DbCompatV5.migrate(c.appDb);
|
||||
} catch (_) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().migrateDatabaseFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
}
|
||||
if (await CompatV37.isAppDbNeedMigration(c.appDb)) {
|
||||
showUpdateDialog();
|
||||
try {
|
||||
await CompatV37.migrateAppDb(c.appDb);
|
||||
} catch (_) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().migrateDatabaseFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
}
|
||||
if (isShowDialog) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
String _gatherChangelog(int from) {
|
||||
if (from < 100) {
|
||||
from *= 10;
|
||||
|
|
|
@ -7,7 +7,6 @@ import 'package:intl/intl.dart';
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
|
@ -17,7 +16,6 @@ import 'package:nc_photos/entity/album/cover_provider.dart';
|
|||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/notified_action.dart';
|
||||
import 'package:nc_photos/platform/features.dart' as features;
|
||||
|
@ -59,6 +57,16 @@ class ViewerDetailPane extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||
_ViewerDetailPaneState() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(require(c));
|
||||
_c = c;
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
DiContainer.has(c, DiType.fileRepo) &&
|
||||
DiContainer.has(c, DiType.albumRepo);
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
@ -380,8 +388,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
try {
|
||||
await NotifiedAction(
|
||||
() async {
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
|
||||
await UpdateAlbum(albumRepo)(
|
||||
await UpdateAlbum(_c.albumRepo)(
|
||||
widget.account,
|
||||
widget.album!.copyWith(
|
||||
coverProvider: AlbumManualCoverProvider(
|
||||
|
@ -427,8 +434,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
try {
|
||||
await NotifiedAction(
|
||||
() async {
|
||||
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
|
||||
await UpdateProperty(fileRepo)
|
||||
await UpdateProperty(_c.fileRepo)
|
||||
.updateIsArchived(widget.account, widget.file, false);
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
|
@ -464,9 +470,8 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
if (value == null || value is! DateTime) {
|
||||
return;
|
||||
}
|
||||
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
|
||||
try {
|
||||
await UpdateProperty(fileRepo)
|
||||
await UpdateProperty(_c.fileRepo)
|
||||
.updateOverrideDateTime(widget.account, widget.file, value);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
@ -520,6 +525,8 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
return false;
|
||||
}
|
||||
|
||||
late final DiContainer _c;
|
||||
|
||||
late DateTime _dateTime;
|
||||
// EXIF data
|
||||
String? _model;
|
||||
|
|
198
app/pubspec.lock
198
app/pubspec.lock
|
@ -15,6 +15,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
analyzer_plugin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer_plugin
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.10.0"
|
||||
android_intent_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -108,6 +115,62 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.9"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.11"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.2.3"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.3.3"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -143,6 +206,20 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -150,6 +227,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -227,6 +311,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.2"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -299,6 +390,20 @@ packages:
|
|||
url: "https://gitlab.com/nc-photos/flutter-draggable-scrollbar"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
drift:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.1"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -336,6 +441,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -467,6 +579,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
hashcodes:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -502,20 +621,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
idb_shim:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: idb_shim
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
idb_sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: idb_sqflite
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
image_size_getter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -546,6 +651,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.4"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
kiwi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -833,6 +945,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
quiver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -840,6 +959,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -882,13 +1008,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
sembast:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sembast
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -978,6 +1097,13 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1000,7 +1126,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.9.0"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
url: "https://pub.dartlang.org"
|
||||
|
@ -1013,6 +1139,27 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1+1"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.2"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.8"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.22.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1076,6 +1223,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.16"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
tuple:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -45,6 +45,7 @@ dependencies:
|
|||
git:
|
||||
url: https://gitlab.com/nc-photos/flutter-draggable-scrollbar
|
||||
ref: v0.1.0-nc-photos-5
|
||||
drift: ^1.7.1
|
||||
equatable: ^2.0.0
|
||||
event_bus: ^2.0.0
|
||||
exifdart:
|
||||
|
@ -65,9 +66,6 @@ dependencies:
|
|||
# android/ios only
|
||||
google_maps_flutter: ^2.1.0
|
||||
http: ^0.13.1
|
||||
idb_shim: ^2.0.0
|
||||
# android/ios only
|
||||
idb_sqflite: ^1.0.0
|
||||
image_size_getter:
|
||||
git:
|
||||
url: https://gitlab.com/nc-photos/dart_image_size_getter
|
||||
|
@ -88,8 +86,7 @@ dependencies:
|
|||
quiver: ^3.1.0
|
||||
screen_brightness: ^0.2.1
|
||||
shared_preferences: ^2.0.8
|
||||
# android/ios only
|
||||
sqflite: ^2.0.0
|
||||
sqlite3_flutter_libs: ^0.5.8
|
||||
synchronized: ^3.0.0
|
||||
tuple: ^2.0.0
|
||||
url_launcher: ^6.0.3
|
||||
|
@ -113,6 +110,8 @@ dependency_overrides:
|
|||
dev_dependencies:
|
||||
test: any
|
||||
bloc_test: any
|
||||
build_runner: ^2.1.11
|
||||
drift_dev: ^1.7.0
|
||||
flutter_lints: ^2.0.1
|
||||
# flutter_test:
|
||||
# sdk: flutter
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:bloc_test/bloc_test.dart';
|
|||
import 'package:nc_photos/bloc/list_album_share_outlier.dart';
|
||||
import 'package:nc_photos/ci_string.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../mock_type.dart';
|
||||
|
@ -56,8 +56,9 @@ void _initialState() {
|
|||
final c = DiContainer(
|
||||
shareRepo: MockShareRepo(),
|
||||
shareeRepo: MockShareeRepo(),
|
||||
appDb: MockAppDb(),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
final bloc = ListAlbumShareOutlierBloc(c);
|
||||
expect(bloc.state.account, null);
|
||||
expect(bloc.state.items, const []);
|
||||
|
@ -84,11 +85,14 @@ void _testQueryUnsharedAlbumExtraShare(String description) {
|
|||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -112,18 +116,22 @@ void _testQueryUnsharedAlbumExtraJsonShare(String description) {
|
|||
final account = util.buildAccount();
|
||||
final album = util.AlbumBuilder().build();
|
||||
final albumFile = album.albumFile!;
|
||||
final c = DiContainer(
|
||||
shareRepo: MockShareMemoryRepo([
|
||||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
]),
|
||||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
]),
|
||||
appDb: MockAppDb(),
|
||||
);
|
||||
late final DiContainer c;
|
||||
|
||||
blocTest<ListAlbumShareOutlierBloc, ListAlbumShareOutlierBlocState>(
|
||||
description,
|
||||
setUp: () async {
|
||||
c = DiContainer(
|
||||
shareRepo: MockShareMemoryRepo([
|
||||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
]),
|
||||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
]),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -164,11 +172,14 @@ void _testQuerySharedAlbumMissingShare(String description) {
|
|||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -215,11 +226,14 @@ void _testQuerySharedAlbumMissingManagedShareOtherAdded(String description) {
|
|||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -272,12 +286,16 @@ void _testQuerySharedAlbumMissingManagedShareOtherReshared(String description) {
|
|||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDb(obj, user1Account, user1Files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -321,11 +339,14 @@ void _testQuerySharedAlbumMissingUnmanagedShareOtherAdded(String description) {
|
|||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -343,16 +364,20 @@ void _testQuerySharedAlbumMissingJsonShare(String description) {
|
|||
final account = util.buildAccount();
|
||||
final album = (util.AlbumBuilder()..addShare("user1")).build();
|
||||
final albumFile = album.albumFile!;
|
||||
final c = DiContainer(
|
||||
shareRepo: MockShareMemoryRepo(),
|
||||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
]),
|
||||
appDb: MockAppDb(),
|
||||
);
|
||||
late final DiContainer c;
|
||||
|
||||
blocTest<ListAlbumShareOutlierBloc, ListAlbumShareOutlierBlocState>(
|
||||
description,
|
||||
setUp: () async {
|
||||
c = DiContainer(
|
||||
shareRepo: MockShareMemoryRepo(),
|
||||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
]),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -396,11 +421,14 @@ void _testQuerySharedAlbumExtraShare(String description) {
|
|||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -451,12 +479,16 @@ void _testQuerySharedAlbumExtraShareOtherAdded(String description) {
|
|||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDb(obj, user1Account, user1Files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -513,12 +545,16 @@ void _testQuerySharedAlbumExtraUnmanagedShare(String description) {
|
|||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDb(obj, user1Account, user1Files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -537,20 +573,24 @@ void _testQuerySharedAlbumExtraJsonShare(String description) {
|
|||
final account = util.buildAccount();
|
||||
final album = (util.AlbumBuilder()..addShare("user1")).build();
|
||||
final albumFile = album.albumFile!;
|
||||
final c = DiContainer(
|
||||
shareRepo: MockShareMemoryRepo([
|
||||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
util.buildShare(id: "1", file: albumFile, shareWith: "user2"),
|
||||
]),
|
||||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: MockAppDb(),
|
||||
);
|
||||
late final DiContainer c;
|
||||
|
||||
blocTest<ListAlbumShareOutlierBloc, ListAlbumShareOutlierBlocState>(
|
||||
description,
|
||||
setUp: () async {
|
||||
c = DiContainer(
|
||||
shareRepo: MockShareMemoryRepo([
|
||||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
util.buildShare(id: "1", file: albumFile, shareWith: "user2"),
|
||||
]),
|
||||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -592,11 +632,14 @@ void _testQuerySharedAlbumNotOwnedMissingShareToOwner(String description) {
|
|||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -643,11 +686,14 @@ void _testQuerySharedAlbumNotOwnedMissingManagedShare(String description) {
|
|||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -692,11 +738,14 @@ void _testQuerySharedAlbumNotOwnedMissingUnmanagedShare(String description) {
|
|||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -718,20 +767,24 @@ void _testQuerySharedAlbumNotOwnedMissingJsonShare(String description) {
|
|||
..addShare("user2"))
|
||||
.build();
|
||||
final albumFile = album.albumFile!;
|
||||
final c = DiContainer(
|
||||
shareRepo: MockShareMemoryRepo([
|
||||
util.buildShare(
|
||||
id: "0", file: albumFile, uidOwner: "user1", shareWith: "admin"),
|
||||
]),
|
||||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: MockAppDb(),
|
||||
);
|
||||
late final DiContainer c;
|
||||
|
||||
blocTest<ListAlbumShareOutlierBloc, ListAlbumShareOutlierBlocState>(
|
||||
description,
|
||||
setUp: () async {
|
||||
c = DiContainer(
|
||||
shareRepo: MockShareMemoryRepo([
|
||||
util.buildShare(
|
||||
id: "0", file: albumFile, uidOwner: "user1", shareWith: "admin"),
|
||||
]),
|
||||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -772,11 +825,14 @@ void _testQuerySharedAlbumNotOwnedExtraManagedShare(String description) {
|
|||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -822,11 +878,14 @@ void _testQuerySharedAlbumNotOwnedExtraUnmanagedShare(String description) {
|
|||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
@ -851,22 +910,26 @@ void _testQuerySharedAlbumNotOwnedExtraJsonShare(String description) {
|
|||
final album =
|
||||
(util.AlbumBuilder(ownerId: "user1")..addShare("admin")).build();
|
||||
final albumFile = album.albumFile!;
|
||||
final c = DiContainer(
|
||||
shareRepo: MockShareMemoryRepo([
|
||||
util.buildShare(
|
||||
id: "0", file: albumFile, uidOwner: "user1", shareWith: "admin"),
|
||||
util.buildShare(
|
||||
id: "1", file: albumFile, uidOwner: "user1", shareWith: "user2"),
|
||||
]),
|
||||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
appDb: MockAppDb(),
|
||||
);
|
||||
late final DiContainer c;
|
||||
|
||||
blocTest<ListAlbumShareOutlierBloc, ListAlbumShareOutlierBlocState>(
|
||||
description,
|
||||
setUp: () async {
|
||||
c = DiContainer(
|
||||
shareRepo: MockShareMemoryRepo([
|
||||
util.buildShare(
|
||||
id: "0", file: albumFile, uidOwner: "user1", shareWith: "admin"),
|
||||
util.buildShare(
|
||||
id: "1", file: albumFile, uidOwner: "user1", shareWith: "user2"),
|
||||
]),
|
||||
shareeRepo: MockShareeMemoryRepo([
|
||||
util.buildSharee(shareWith: "user1".toCi()),
|
||||
util.buildSharee(shareWith: "user2".toCi()),
|
||||
]),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
},
|
||||
tearDown: () => c.sqliteDb.close(),
|
||||
build: () => ListAlbumShareOutlierBloc(c),
|
||||
act: (bloc) => bloc.add(ListAlbumShareOutlierBlocQuery(account, album)),
|
||||
wait: const Duration(milliseconds: 500),
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/bloc/ls_dir.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../mock_type.dart';
|
||||
|
@ -134,32 +132,30 @@ void main() {
|
|||
});
|
||||
}
|
||||
|
||||
class _MockFileRepo extends MockFileRepo {
|
||||
@override
|
||||
list(Account account, File root) async {
|
||||
return [
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/test1.jpg",
|
||||
),
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/d1",
|
||||
isCollection: true,
|
||||
),
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/d1/test2.jpg",
|
||||
),
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/d1/d2-1",
|
||||
isCollection: true,
|
||||
),
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/d1/d2-2",
|
||||
isCollection: true,
|
||||
),
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/d1/d2-1/d3",
|
||||
isCollection: true,
|
||||
),
|
||||
].where((element) => path_lib.dirname(element.path) == root.path).toList();
|
||||
}
|
||||
class _MockFileRepo extends MockFileMemoryRepo {
|
||||
_MockFileRepo()
|
||||
: super([
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/test1.jpg",
|
||||
),
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/d1",
|
||||
isCollection: true,
|
||||
),
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/d1/test2.jpg",
|
||||
),
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/d1/d2-1",
|
||||
isCollection: true,
|
||||
),
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/d1/d2-2",
|
||||
isCollection: true,
|
||||
),
|
||||
File(
|
||||
path: "remote.php/dav/files/admin/d1/d2-1/d3",
|
||||
isCollection: true,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
|
335
app/test/entity/album/data_source_test.dart
Normal file
335
app/test/entity/album/data_source_test.dart
Normal file
|
@ -0,0 +1,335 @@
|
|||
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/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/data_source.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../test_util.dart' as util;
|
||||
|
||||
void main() {
|
||||
group("AlbumSqliteDbDataSource", () {
|
||||
group("get", () {
|
||||
test("normal", _dbGet);
|
||||
test("n/a", _dbGetNa);
|
||||
});
|
||||
group("getAll", () {
|
||||
test("normal", _dbGetAll);
|
||||
test("n/a", _dbGetAllNa);
|
||||
});
|
||||
group("update", () {
|
||||
test("existing album", _dbUpdateExisting);
|
||||
test("new album", _dbUpdateNew);
|
||||
test("shares", _dbUpdateShares);
|
||||
test("delete shares", _dbUpdateDeleteShares);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Get an album from DB
|
||||
///
|
||||
/// Expect: album
|
||||
Future<void> _dbGet() async {
|
||||
final account = util.buildAccount();
|
||||
final albums = [
|
||||
(util.AlbumBuilder.ofId(albumId: 0)).build(),
|
||||
(util.AlbumBuilder.ofId(albumId: 1)).build(),
|
||||
];
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(
|
||||
c.sqliteDb, account, albums.map((a) => a.albumFile!));
|
||||
await util.insertAlbums(c.sqliteDb, account, albums);
|
||||
});
|
||||
|
||||
final src = AlbumSqliteDbDataSource(c);
|
||||
expect(await src.get(account, albums[0].albumFile!), albums[0]);
|
||||
}
|
||||
|
||||
/// Get an album that doesn't exist in DB
|
||||
///
|
||||
/// Expect: CacheNotFoundException
|
||||
Future<void> _dbGetNa() async {
|
||||
final account = util.buildAccount();
|
||||
final albums = [
|
||||
(util.AlbumBuilder.ofId(albumId: 0)).build(),
|
||||
];
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
});
|
||||
|
||||
final src = AlbumSqliteDbDataSource(c);
|
||||
expect(
|
||||
() async => await src.get(account, albums[0].albumFile!),
|
||||
throwsA(const TypeMatcher<CacheNotFoundException>()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get multiple albums from DB
|
||||
///
|
||||
/// Expect: albums
|
||||
Future<void> _dbGetAll() async {
|
||||
final account = util.buildAccount();
|
||||
final albums = [
|
||||
(util.AlbumBuilder.ofId(albumId: 0)).build(),
|
||||
(util.AlbumBuilder.ofId(albumId: 1)).build(),
|
||||
(util.AlbumBuilder.ofId(albumId: 2)).build(),
|
||||
];
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(
|
||||
c.sqliteDb, account, albums.map((a) => a.albumFile!));
|
||||
await util.insertAlbums(c.sqliteDb, account, albums);
|
||||
});
|
||||
|
||||
final src = AlbumSqliteDbDataSource(c);
|
||||
expect(
|
||||
await src
|
||||
.getAll(account, [albums[0].albumFile!, albums[2].albumFile!]).toList(),
|
||||
[albums[0], albums[2]],
|
||||
);
|
||||
}
|
||||
|
||||
/// Get multiple albums that doesn't exists in DB
|
||||
///
|
||||
/// Expect: ExceptionEvent with CacheNotFoundException
|
||||
Future<void> _dbGetAllNa() async {
|
||||
final account = util.buildAccount();
|
||||
final albums = [
|
||||
(util.AlbumBuilder.ofId(albumId: 0)).build(),
|
||||
(util.AlbumBuilder.ofId(albumId: 1)).build(),
|
||||
(util.AlbumBuilder.ofId(albumId: 2)).build(),
|
||||
];
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, [albums[0].albumFile!]);
|
||||
await util.insertAlbums(c.sqliteDb, account, [albums[0]]);
|
||||
});
|
||||
|
||||
final src = AlbumSqliteDbDataSource(c);
|
||||
final results = await src
|
||||
.getAll(account, [albums[0].albumFile!, albums[2].albumFile!]).toList();
|
||||
expect(results.length, 2);
|
||||
expect(results[0], albums[0]);
|
||||
expect(
|
||||
() => throw results[1],
|
||||
throwsA(const TypeMatcher<CacheNotFoundException>()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Update an existing album in DB
|
||||
///
|
||||
/// Expect: album updated
|
||||
Future<void> _dbUpdateExisting() async {
|
||||
final account = util.buildAccount();
|
||||
final albums = [
|
||||
(util.AlbumBuilder.ofId(albumId: 0)).build(),
|
||||
(util.AlbumBuilder.ofId(albumId: 1)).build(),
|
||||
];
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(
|
||||
c.sqliteDb, account, albums.map((a) => a.albumFile!));
|
||||
await util.insertAlbums(c.sqliteDb, account, albums);
|
||||
});
|
||||
|
||||
final updateAlbum = albums[1].copyWith(
|
||||
name: "edit",
|
||||
lastUpdated: OrNull(DateTime.utc(2021, 2, 3, 4, 5, 6)),
|
||||
provider: AlbumStaticProvider(
|
||||
items: [
|
||||
AlbumLabelItem(
|
||||
addedBy: "admin".toCi(),
|
||||
addedAt: DateTime.utc(2021, 2, 3, 4, 5, 6),
|
||||
text: "test",
|
||||
),
|
||||
AlbumFileItem(
|
||||
addedBy: "admin".toCi(),
|
||||
addedAt: DateTime.utc(2021, 2, 3, 4, 5, 6),
|
||||
file: files[1],
|
||||
),
|
||||
],
|
||||
),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: files[1]),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: true),
|
||||
);
|
||||
final src = AlbumSqliteDbDataSource(c);
|
||||
await src.update(account, updateAlbum);
|
||||
expect(
|
||||
await util.listSqliteDbAlbums(c.sqliteDb),
|
||||
{albums[0], updateAlbum},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update an album that doesn't exist in DB
|
||||
///
|
||||
/// Expect: album inserted
|
||||
Future<void> _dbUpdateNew() async {
|
||||
final account = util.buildAccount();
|
||||
final albums = [
|
||||
(util.AlbumBuilder.ofId(albumId: 0)).build(),
|
||||
];
|
||||
final newAlbum = (util.AlbumBuilder.ofId(albumId: 1)).build();
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account,
|
||||
[...albums.map((a) => a.albumFile!), newAlbum.albumFile!]);
|
||||
await util.insertAlbums(c.sqliteDb, account, albums);
|
||||
});
|
||||
|
||||
final src = AlbumSqliteDbDataSource(c);
|
||||
await src.update(account, newAlbum);
|
||||
expect(
|
||||
await util.listSqliteDbAlbums(c.sqliteDb),
|
||||
{...albums, newAlbum},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update shares of an album
|
||||
///
|
||||
/// Expect: album shares updated
|
||||
Future<void> _dbUpdateShares() async {
|
||||
final account = util.buildAccount();
|
||||
final albums = [
|
||||
(util.AlbumBuilder.ofId(albumId: 0)).build(),
|
||||
(util.AlbumBuilder.ofId(albumId: 1)..addShare("user1")).build(),
|
||||
];
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(
|
||||
c.sqliteDb, account, albums.map((a) => a.albumFile!));
|
||||
await util.insertAlbums(c.sqliteDb, account, albums);
|
||||
});
|
||||
|
||||
final updateAlbum = albums[1].copyWith(
|
||||
name: "edit",
|
||||
lastUpdated: OrNull(DateTime.utc(2021, 2, 3, 4, 5, 6)),
|
||||
provider: AlbumStaticProvider(
|
||||
items: [
|
||||
AlbumLabelItem(
|
||||
addedBy: "admin".toCi(),
|
||||
addedAt: DateTime.utc(2021, 2, 3, 4, 5, 6),
|
||||
text: "test",
|
||||
),
|
||||
AlbumFileItem(
|
||||
addedBy: "admin".toCi(),
|
||||
addedAt: DateTime.utc(2021, 2, 3, 4, 5, 6),
|
||||
file: files[1],
|
||||
),
|
||||
],
|
||||
),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: files[1]),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: true),
|
||||
shares: OrNull([
|
||||
AlbumShare(
|
||||
userId: "user1".toCi(),
|
||||
sharedAt: DateTime.utc(2021, 2, 3, 4, 5, 6),
|
||||
),
|
||||
AlbumShare(
|
||||
userId: "user2".toCi(),
|
||||
sharedAt: DateTime.utc(2021, 2, 3, 4, 5, 7),
|
||||
),
|
||||
]),
|
||||
);
|
||||
final src = AlbumSqliteDbDataSource(c);
|
||||
await src.update(account, updateAlbum);
|
||||
expect(
|
||||
await util.listSqliteDbAlbums(c.sqliteDb),
|
||||
{albums[0], updateAlbum},
|
||||
);
|
||||
}
|
||||
|
||||
/// Delete shares of an album
|
||||
///
|
||||
/// Expect: album shares deleted
|
||||
Future<void> _dbUpdateDeleteShares() async {
|
||||
final account = util.buildAccount();
|
||||
final albums = [
|
||||
(util.AlbumBuilder.ofId(albumId: 0)).build(),
|
||||
(util.AlbumBuilder.ofId(albumId: 1)..addShare("user1")).build(),
|
||||
];
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(
|
||||
c.sqliteDb, account, albums.map((a) => a.albumFile!));
|
||||
await util.insertAlbums(c.sqliteDb, account, albums);
|
||||
});
|
||||
|
||||
final updateAlbum = albums[1].copyWith(
|
||||
name: "edit",
|
||||
lastUpdated: OrNull(DateTime.utc(2021, 2, 3, 4, 5, 6)),
|
||||
provider: AlbumStaticProvider(
|
||||
items: [
|
||||
AlbumLabelItem(
|
||||
addedBy: "admin".toCi(),
|
||||
addedAt: DateTime.utc(2021, 2, 3, 4, 5, 6),
|
||||
text: "test",
|
||||
),
|
||||
AlbumFileItem(
|
||||
addedBy: "admin".toCi(),
|
||||
addedAt: DateTime.utc(2021, 2, 3, 4, 5, 6),
|
||||
file: files[1],
|
||||
),
|
||||
],
|
||||
),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: files[1]),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: true),
|
||||
shares: OrNull(null),
|
||||
);
|
||||
final src = AlbumSqliteDbDataSource(c);
|
||||
await src.update(account, updateAlbum);
|
||||
expect(
|
||||
await util.listSqliteDbAlbums(c.sqliteDb),
|
||||
{albums[0], updateAlbum},
|
||||
);
|
||||
}
|
|
@ -1,16 +1,15 @@
|
|||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/list_extension.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../mock_type.dart';
|
||||
import '../../test_util.dart' as util;
|
||||
|
||||
void main() {
|
||||
group("FileAppDbDataSource", () {
|
||||
group("FileSqliteDbDataSource", () {
|
||||
test("list", _list);
|
||||
test("listSingle", _listSingle);
|
||||
group("remove", () {
|
||||
|
@ -19,7 +18,12 @@ void main() {
|
|||
test("dir w/ file", _removeDir);
|
||||
test("dir w/ sub dir", _removeDirWithSubDir);
|
||||
});
|
||||
test("updateProperty", _updateProperty);
|
||||
group("updateProperty", () {
|
||||
test("file properties", _updateFileProperty);
|
||||
test("update metadata", _updateMetadata);
|
||||
test("add metadata", _updateAddMetadata);
|
||||
test("delete metadata", _updateDeleteMetadata);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -38,12 +42,19 @@ Future<void> _list() async {
|
|||
..addDir("admin/test")
|
||||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final appDb = await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDbDir(obj, account, files[0], files.slice(1, 3));
|
||||
await util.fillAppDbDir(obj, account, files[2], [files[3]]);
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(
|
||||
c.sqliteDb, account, files[0], files.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
});
|
||||
final src = FileAppDbDataSource(appDb);
|
||||
|
||||
final src = FileSqliteDbDataSource(c);
|
||||
expect(await src.list(account, files[0]), files.slice(0, 3));
|
||||
expect(await src.list(account, files[2]), files.slice(2, 4));
|
||||
}
|
||||
|
@ -54,69 +65,83 @@ Future<void> _list() async {
|
|||
Future<void> _listSingle() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()..addDir("admin")).build();
|
||||
final appDb = await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDbDir(obj, account, files[0], []);
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(c.sqliteDb, account, files[0], const []);
|
||||
});
|
||||
final src = FileAppDbDataSource(appDb);
|
||||
|
||||
final src = FileSqliteDbDataSource(c);
|
||||
expect(() async => await src.listSingle(account, files[0]),
|
||||
throwsUnimplementedError);
|
||||
}
|
||||
|
||||
/// Remove a file
|
||||
///
|
||||
/// Expect: entry removed from file2Store
|
||||
/// Expect: entry removed from Files table
|
||||
Future<void> _removeFile() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg"))
|
||||
.build();
|
||||
final appDb = await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDbDir(obj, account, files[0], [files[1]]);
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
|
||||
});
|
||||
final src = FileAppDbDataSource(appDb);
|
||||
|
||||
final src = FileSqliteDbDataSource(c);
|
||||
await src.remove(account, files[1]);
|
||||
expect(
|
||||
(await util.listAppDb(
|
||||
appDb, AppDb.file2StoreName, (e) => AppDbFile2Entry.fromJson(e)))
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
[files[0]],
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{files[0]},
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove an empty dir
|
||||
///
|
||||
/// Expect: dir entry removed from dirStore;
|
||||
/// no changes to parent dir entry
|
||||
/// Expect: entry removed from DirFiles table
|
||||
Future<void> _removeEmptyDir() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addDir("admin/test"))
|
||||
.build();
|
||||
final appDb = await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDbDir(obj, account, files[0], [files[1]]);
|
||||
await util.fillAppDbDir(obj, account, files[1], []);
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[1], const []);
|
||||
});
|
||||
final src = FileAppDbDataSource(appDb);
|
||||
|
||||
final src = FileSqliteDbDataSource(c);
|
||||
await src.remove(account, files[1]);
|
||||
// parent dir is not updated, parent dir is only updated when syncing with
|
||||
// remote
|
||||
expect(
|
||||
await util.listAppDb(
|
||||
appDb, AppDb.dirStoreName, (e) => AppDbDirEntry.fromJson(e)),
|
||||
[
|
||||
AppDbDirEntry.fromFiles(account, files[0], [files[1]]),
|
||||
],
|
||||
await util.listSqliteDbDirs(c.sqliteDb),
|
||||
{
|
||||
files[0]: {files[0]},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove a dir with file
|
||||
///
|
||||
/// Expect: file entries under the dir removed from file2Store
|
||||
/// Expect: file entries under the dir removed from Files table
|
||||
Future<void> _removeDir() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
|
@ -124,25 +149,28 @@ Future<void> _removeDir() async {
|
|||
..addDir("admin/test")
|
||||
..addJpeg("admin/test/test1.jpg"))
|
||||
.build();
|
||||
final appDb = await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDbDir(obj, account, files[0], [files[1]]);
|
||||
await util.fillAppDbDir(obj, account, files[1], [files[2]]);
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[1], [files[2]]);
|
||||
});
|
||||
final src = FileAppDbDataSource(appDb);
|
||||
|
||||
final src = FileSqliteDbDataSource(c);
|
||||
await src.remove(account, files[1]);
|
||||
expect(
|
||||
(await util.listAppDb(
|
||||
appDb, AppDb.file2StoreName, (e) => AppDbFile2Entry.fromJson(e)))
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
[files[0]],
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{files[0]},
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove a dir with file
|
||||
///
|
||||
/// Expect: file entries under the dir removed from file2Store
|
||||
/// Expect: file entries under the dir removed from Files table
|
||||
Future<void> _removeDirWithSubDir() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
|
@ -151,75 +179,192 @@ Future<void> _removeDirWithSubDir() async {
|
|||
..addDir("admin/test/test2")
|
||||
..addJpeg("admin/test/test2/test3.jpg"))
|
||||
.build();
|
||||
final appDb = await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDbDir(obj, account, files[0], [files[1]]);
|
||||
await util.fillAppDbDir(obj, account, files[1], [files[2]]);
|
||||
await util.fillAppDbDir(obj, account, files[2], [files[3]]);
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[1], [files[2]]);
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
});
|
||||
final src = FileAppDbDataSource(appDb);
|
||||
|
||||
final src = FileSqliteDbDataSource(c);
|
||||
await src.remove(account, files[1]);
|
||||
expect(
|
||||
await util.listAppDb(
|
||||
appDb, AppDb.dirStoreName, (e) => AppDbDirEntry.fromJson(e)),
|
||||
[
|
||||
AppDbDirEntry.fromFiles(account, files[0], [files[1]]),
|
||||
],
|
||||
await util.listSqliteDbDirs(c.sqliteDb),
|
||||
{
|
||||
files[0]: {files[0]}
|
||||
},
|
||||
);
|
||||
expect(
|
||||
(await util.listAppDb(
|
||||
appDb, AppDb.file2StoreName, (e) => AppDbFile2Entry.fromJson(e)))
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
[files[0]],
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{files[0]},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update the properties of a file
|
||||
///
|
||||
/// Expect: file's property updated in file2Store;
|
||||
/// file's property updated in dirStore
|
||||
Future<void> _updateProperty() async {
|
||||
/// Expect: file's property updated in Files table
|
||||
Future<void> _updateFileProperty() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg"))
|
||||
.build();
|
||||
final appDb = await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDbDir(obj, account, files[0], [files[1]]);
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
|
||||
});
|
||||
final src = FileAppDbDataSource(appDb);
|
||||
|
||||
final src = FileSqliteDbDataSource(c);
|
||||
await src.updateProperty(
|
||||
account,
|
||||
files[1],
|
||||
isArchived: OrNull(true),
|
||||
overrideDateTime: OrNull(DateTime.utc(2020, 1, 2, 3, 4, 5)),
|
||||
);
|
||||
final expectFile = files[1].copyWith(
|
||||
isArchived: OrNull(true),
|
||||
overrideDateTime: OrNull(DateTime.utc(2020, 1, 2, 3, 4, 5)),
|
||||
);
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{files[0], expectFile},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update metadata of a file
|
||||
///
|
||||
/// Expect: Metadata updated in Images table
|
||||
Future<void> _updateMetadata() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg"))
|
||||
.build();
|
||||
files[1] = files[1].copyWith(
|
||||
metadata: OrNull(Metadata(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
|
||||
imageWidth: 123,
|
||||
)),
|
||||
);
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
|
||||
});
|
||||
|
||||
final src = FileSqliteDbDataSource(c);
|
||||
await src.updateProperty(
|
||||
account,
|
||||
files[1],
|
||||
metadata: OrNull(Metadata(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678),
|
||||
imageWidth: 123,
|
||||
lastUpdated: DateTime.utc(2021, 1, 2, 3, 4, 5),
|
||||
imageWidth: 321,
|
||||
imageHeight: 123,
|
||||
)),
|
||||
isArchived: OrNull(true),
|
||||
overrideDateTime: OrNull(DateTime.utc(2020, 1, 2, 3, 4, 5, 678)),
|
||||
);
|
||||
final expectFile = files[1].copyWith(
|
||||
metadata: OrNull(Metadata(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678),
|
||||
imageWidth: 123,
|
||||
lastUpdated: DateTime.utc(2021, 1, 2, 3, 4, 5),
|
||||
imageWidth: 321,
|
||||
imageHeight: 123,
|
||||
)),
|
||||
isArchived: OrNull(true),
|
||||
overrideDateTime: OrNull(DateTime.utc(2020, 1, 2, 3, 4, 5, 678)),
|
||||
);
|
||||
expect(
|
||||
await util.listAppDb(
|
||||
appDb, AppDb.dirStoreName, (e) => AppDbDirEntry.fromJson(e)),
|
||||
[
|
||||
AppDbDirEntry.fromFiles(account, files[0], [expectFile]),
|
||||
],
|
||||
);
|
||||
expect(
|
||||
(await util.listAppDb(
|
||||
appDb, AppDb.file2StoreName, (e) => AppDbFile2Entry.fromJson(e)))
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
[files[0], expectFile],
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{files[0], expectFile},
|
||||
);
|
||||
}
|
||||
|
||||
/// Add metadata to a file
|
||||
///
|
||||
/// Expect: Metadata added to Images table
|
||||
Future<void> _updateAddMetadata() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
|
||||
});
|
||||
|
||||
final src = FileSqliteDbDataSource(c);
|
||||
await src.updateProperty(
|
||||
account,
|
||||
files[1],
|
||||
metadata: OrNull(Metadata(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
|
||||
imageWidth: 123,
|
||||
)),
|
||||
);
|
||||
final expectFile = files[1].copyWith(
|
||||
metadata: OrNull(Metadata(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
|
||||
imageWidth: 123,
|
||||
)),
|
||||
);
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{files[0], expectFile},
|
||||
);
|
||||
}
|
||||
|
||||
/// Delete metadata of a file
|
||||
///
|
||||
/// Expect: Metadata deleted from Images table
|
||||
Future<void> _updateDeleteMetadata() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg"))
|
||||
.build();
|
||||
files[1] = files[1].copyWith(
|
||||
metadata: OrNull(Metadata(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
|
||||
imageWidth: 123,
|
||||
)),
|
||||
);
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
|
||||
});
|
||||
|
||||
final src = FileSqliteDbDataSource(c);
|
||||
await src.updateProperty(
|
||||
account,
|
||||
files[1],
|
||||
metadata: OrNull(null),
|
||||
);
|
||||
final expectFile = files[1].copyWith(
|
||||
metadata: OrNull(null),
|
||||
);
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{files[0], expectFile},
|
||||
);
|
||||
}
|
||||
|
|
502
app/test/entity/file/file_cache_manager_test.dart
Normal file
502
app/test/entity/file/file_cache_manager_test.dart
Normal file
|
@ -0,0 +1,502 @@
|
|||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/file/file_cache_manager.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/list_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../mock_type.dart';
|
||||
import '../../test_util.dart' as util;
|
||||
|
||||
void main() {
|
||||
group("FileCacheLoader", () {
|
||||
group("default", () {
|
||||
test("no cache", _loaderNoCache);
|
||||
test("outdated", _loaderOutdatedCache);
|
||||
test("query remote etag", _loaderQueryRemoteSameEtag);
|
||||
test("query remote etag (updated)", _loaderQueryRemoteDiffEtag);
|
||||
});
|
||||
});
|
||||
group("FileSqliteCacheUpdater", () {
|
||||
test("identical", _updaterIdentical);
|
||||
test("new file", _updaterNewFile);
|
||||
test("delete file", _updaterDeleteFile);
|
||||
test("delete dir", _updaterDeleteDir);
|
||||
test("update file", _updaterUpdateFile);
|
||||
test("new shared file", _updaterNewSharedFile);
|
||||
test("new shared dir", _updaterNewSharedDir);
|
||||
test("delete shared file", _updaterDeleteSharedFile);
|
||||
test("delete shared dir", _updaterDeleteSharedDir);
|
||||
});
|
||||
}
|
||||
|
||||
/// Load dir: no cache
|
||||
///
|
||||
/// Expect: null
|
||||
Future<void> _loaderNoCache() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin", etag: "1")
|
||||
..addJpeg("admin/test1.jpg", etag: "2")
|
||||
..addDir("admin/test", etag: "3")
|
||||
..addJpeg("admin/test/test2.jpg", etag: "4"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
fileRepo: MockFileMemoryRepo(files),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
});
|
||||
|
||||
final cacheSrc = FileSqliteDbDataSource(c);
|
||||
final remoteSrc = MockFileWebdavDataSource(MockFileMemoryDataSource(files));
|
||||
final loader = FileCacheLoader(c, cacheSrc: cacheSrc, remoteSrc: remoteSrc);
|
||||
expect(await loader(account, files[0]), null);
|
||||
}
|
||||
|
||||
/// Load dir: outdated cache
|
||||
///
|
||||
/// Expect: return cache;
|
||||
/// isGood == false
|
||||
Future<void> _loaderOutdatedCache() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin", etag: "1")
|
||||
..addJpeg("admin/test1.jpg", etag: "2")
|
||||
..addDir("admin/test", etag: "3")
|
||||
..addJpeg("admin/test/test2.jpg", etag: "4"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
fileRepo: MockFileMemoryRepo(files),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
final dbFiles = [
|
||||
files[0].copyWith(etag: OrNull("a")),
|
||||
...files.slice(1),
|
||||
];
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, dbFiles);
|
||||
await util.insertDirRelation(
|
||||
c.sqliteDb, account, dbFiles[0], dbFiles.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, dbFiles[2], [dbFiles[3]]);
|
||||
});
|
||||
|
||||
final cacheSrc = FileSqliteDbDataSource(c);
|
||||
final remoteSrc = MockFileWebdavDataSource(MockFileMemoryDataSource(files));
|
||||
final loader = FileCacheLoader(c, cacheSrc: cacheSrc, remoteSrc: remoteSrc);
|
||||
expect(
|
||||
(await loader(account, files[0]))?.toSet(),
|
||||
dbFiles.slice(0, 3).toSet(),
|
||||
);
|
||||
expect(loader.isGood, false);
|
||||
}
|
||||
|
||||
/// Load dir: no etag, up-to-date cache
|
||||
///
|
||||
/// Expect: return cache;
|
||||
/// isGood == true
|
||||
Future<void> _loaderQueryRemoteSameEtag() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin", etag: "1")
|
||||
..addJpeg("admin/test1.jpg", etag: "2")
|
||||
..addDir("admin/test", etag: "3")
|
||||
..addJpeg("admin/test/test2.jpg", etag: "4"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
fileRepo: MockFileMemoryRepo(files),
|
||||
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 util.insertDirRelation(
|
||||
c.sqliteDb, account, files[0], files.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
});
|
||||
|
||||
final cacheSrc = FileSqliteDbDataSource(c);
|
||||
final remoteSrc = MockFileWebdavDataSource(MockFileMemoryDataSource(files));
|
||||
final loader = FileCacheLoader(c, cacheSrc: cacheSrc, remoteSrc: remoteSrc);
|
||||
expect(
|
||||
(await loader(account, files[0].copyWith(etag: OrNull(null))))?.toSet(),
|
||||
files.slice(0, 3).toSet(),
|
||||
);
|
||||
expect(loader.isGood, true);
|
||||
}
|
||||
|
||||
/// Load dir: no etag, outdated cache
|
||||
///
|
||||
/// Expect: return cache;
|
||||
/// isGood == false
|
||||
Future<void> _loaderQueryRemoteDiffEtag() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin", etag: "1")
|
||||
..addJpeg("admin/test1.jpg", etag: "2")
|
||||
..addDir("admin/test", etag: "3")
|
||||
..addJpeg("admin/test/test2.jpg", etag: "4"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
fileRepo: MockFileMemoryRepo(files),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
final dbFiles = [
|
||||
files[0].copyWith(etag: OrNull("a")),
|
||||
...files.slice(1),
|
||||
];
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, dbFiles);
|
||||
await util.insertDirRelation(
|
||||
c.sqliteDb, account, dbFiles[0], dbFiles.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, dbFiles[2], [dbFiles[3]]);
|
||||
});
|
||||
|
||||
final cacheSrc = FileSqliteDbDataSource(c);
|
||||
final remoteSrc = MockFileWebdavDataSource(MockFileMemoryDataSource(files));
|
||||
final loader = FileCacheLoader(c, cacheSrc: cacheSrc, remoteSrc: remoteSrc);
|
||||
expect(
|
||||
(await loader(account, files[0].copyWith(etag: OrNull(null))))?.toSet(),
|
||||
dbFiles.slice(0, 3).toSet(),
|
||||
);
|
||||
expect(loader.isGood, false);
|
||||
}
|
||||
|
||||
/// Update dir in cache: same set of files
|
||||
///
|
||||
/// Expect: nothing happens
|
||||
Future<void> _updaterIdentical() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/test")
|
||||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(
|
||||
c.sqliteDb, account, files[0], files.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
});
|
||||
|
||||
final updater = FileSqliteCacheUpdater(c);
|
||||
await updater(account, files[0], remote: files.slice(0, 3));
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
files.toSet(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Update dir in cache: new file
|
||||
///
|
||||
/// Expect: new file added to Files table
|
||||
Future<void> _updaterNewFile() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/test")
|
||||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final newFile = (util.FilesBuilder(initialFileId: files.length)
|
||||
..addJpeg("admin/test2.jpg"))
|
||||
.build()
|
||||
.first;
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(
|
||||
c.sqliteDb, account, files[0], files.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
});
|
||||
|
||||
final updater = FileSqliteCacheUpdater(c);
|
||||
await updater(account, files[0], remote: [...files.slice(0, 3), newFile]);
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{...files, newFile},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update dir in cache: file missing
|
||||
///
|
||||
/// Expect: missing file deleted from Files table
|
||||
Future<void> _updaterDeleteFile() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/test")
|
||||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(
|
||||
c.sqliteDb, account, files[0], files.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
});
|
||||
|
||||
final updater = FileSqliteCacheUpdater(c);
|
||||
await updater(account, files[0], remote: [files[0], files[2]]);
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{files[0], ...files.slice(2)},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update dir in cache: dir missing
|
||||
///
|
||||
/// Expect: missing dir deleted from Files table;
|
||||
/// missing dir deleted from DirFiles table
|
||||
/// files under dir deleted from Files table;
|
||||
/// dirs under dir deleted from DirFiles table;
|
||||
Future<void> _updaterDeleteDir() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/test")
|
||||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(
|
||||
c.sqliteDb, account, files[0], files.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
});
|
||||
|
||||
final updater = FileSqliteCacheUpdater(c);
|
||||
await updater(account, files[0], remote: files.slice(0, 2));
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
files.slice(0, 2).toSet(),
|
||||
);
|
||||
expect(
|
||||
await util.listSqliteDbDirs(c.sqliteDb),
|
||||
{
|
||||
files[0]: files.slice(0, 2).toSet(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update dir in cache: file updated
|
||||
///
|
||||
/// Expect: file updated in Files table
|
||||
Future<void> _updaterUpdateFile() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg", contentLength: 321)
|
||||
..addDir("admin/test")
|
||||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final newFile = files[1].copyWith(contentLength: 654);
|
||||
final c = DiContainer(
|
||||
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 util.insertDirRelation(
|
||||
c.sqliteDb, account, files[0], files.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
});
|
||||
|
||||
final updater = FileSqliteCacheUpdater(c);
|
||||
await updater(account, files[0],
|
||||
remote: [files[0], newFile, ...files.slice(2)]);
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{files[0], newFile, ...files.slice(2)},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update dir in cache: new shared file
|
||||
///
|
||||
/// Expect: file added to AccountFiles table
|
||||
Future<void> _updaterNewSharedFile() async {
|
||||
final account = util.buildAccount();
|
||||
final user1Account = util.buildAccount(username: "user1");
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/test")
|
||||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final user1Files = (util.FilesBuilder(initialFileId: files.length)
|
||||
..addDir("user1", ownerId: "user1"))
|
||||
.build();
|
||||
user1Files
|
||||
.add(files[1].copyWith(path: "remote.php/dav/files/user1/test1.jpg"));
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
await util.insertDirRelation(
|
||||
c.sqliteDb, account, files[0], files.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
});
|
||||
|
||||
final updater = FileSqliteCacheUpdater(c);
|
||||
await updater(user1Account, user1Files[0], remote: user1Files);
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{...files, ...user1Files},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update dir in cache: new shared dir
|
||||
///
|
||||
/// Expect: file added to AccountFiles table
|
||||
Future<void> _updaterNewSharedDir() async {
|
||||
final account = util.buildAccount();
|
||||
final user1Account = util.buildAccount(username: "user1");
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg", ownerId: "user1")
|
||||
..addDir("admin/test")
|
||||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final user1Files = <File>[];
|
||||
user1Files.add(files[2].copyWith(path: "remote.php/dav/files/user1/share"));
|
||||
user1Files.add(
|
||||
files[3].copyWith(path: "remote.php/dav/files/user1/share/test2.jpg"));
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
await util.insertDirRelation(
|
||||
c.sqliteDb, account, files[0], files.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
});
|
||||
|
||||
final updater = FileSqliteCacheUpdater(c);
|
||||
await updater(user1Account, user1Files[0], remote: user1Files);
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{...files, ...user1Files},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update dir in cache: shared file missing
|
||||
///
|
||||
/// Expect: file removed from AccountFiles table;
|
||||
/// file remained in Files table
|
||||
Future<void> _updaterDeleteSharedFile() async {
|
||||
final account = util.buildAccount();
|
||||
final user1Account = util.buildAccount(username: "user1");
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/test")
|
||||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final user1Files =
|
||||
(util.FilesBuilder(initialFileId: files.length)..addDir("user1")).build();
|
||||
user1Files
|
||||
.add(files[1].copyWith(path: "remote.php/dav/files/user1/test1.jpg"));
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
await util.insertDirRelation(
|
||||
c.sqliteDb, account, files[0], files.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
|
||||
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
|
||||
await util.insertDirRelation(
|
||||
c.sqliteDb, user1Account, user1Files[0], [user1Files[1]]);
|
||||
});
|
||||
|
||||
final updater = FileSqliteCacheUpdater(c);
|
||||
await updater(user1Account, user1Files[0], remote: [user1Files[0]]);
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{...files, user1Files[0]},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update dir in cache: shared dir missing
|
||||
///
|
||||
/// Expect: file removed from AccountFiles table;
|
||||
/// file remained in Files table
|
||||
Future<void> _updaterDeleteSharedDir() async {
|
||||
final account = util.buildAccount();
|
||||
final user1Account = util.buildAccount(username: "user1");
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/test")
|
||||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final user1Files =
|
||||
(util.FilesBuilder(initialFileId: files.length)..addDir("user1")).build();
|
||||
user1Files.add(files[2].copyWith(path: "remote.php/dav/files/user1/share"));
|
||||
user1Files.add(
|
||||
files[3].copyWith(path: "remote.php/dav/files/user1/share/test2.jpg"));
|
||||
final c = DiContainer(
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
await util.insertDirRelation(
|
||||
c.sqliteDb, account, files[0], files.slice(1, 3));
|
||||
await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]);
|
||||
|
||||
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
|
||||
await util.insertDirRelation(
|
||||
c.sqliteDb, user1Account, user1Files[0], [user1Files[1]]);
|
||||
});
|
||||
|
||||
final updater = FileSqliteCacheUpdater(c);
|
||||
await updater(user1Account, user1Files[0], remote: [user1Files[0]]);
|
||||
expect(
|
||||
await util.listSqliteDbFiles(c.sqliteDb),
|
||||
{...files, user1Files[0]},
|
||||
);
|
||||
}
|
|
@ -752,7 +752,7 @@ void main() {
|
|||
});
|
||||
|
||||
test("etag", () {
|
||||
final file = src.copyWith(etag: "000");
|
||||
final file = src.copyWith(etag: OrNull("000"));
|
||||
expect(
|
||||
file,
|
||||
File(
|
||||
|
|
|
@ -3,26 +3,22 @@ import 'dart:math' as math;
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:idb_shim/idb.dart';
|
||||
import 'package:idb_shim/idb_client_memory.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.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/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/exception_event.dart';
|
||||
import 'package:nc_photos/future_util.dart' as future_util;
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
|
||||
/// Mock of [AlbumRepo] where all methods will throw UnimplementedError
|
||||
class MockAlbumRepo implements AlbumRepo {
|
||||
@override
|
||||
Future<void> cleanUp(Account account, String rootDir, List<File> albumFiles) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> create(Account account, Album album) {
|
||||
throw UnimplementedError();
|
||||
|
@ -36,6 +32,11 @@ class MockAlbumRepo implements AlbumRepo {
|
|||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<dynamic> getAll(Account account, List<File> albumFiles) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(Account account, Album album) {
|
||||
throw UnimplementedError();
|
||||
|
@ -54,6 +55,17 @@ class MockAlbumMemoryRepo extends MockAlbumRepo {
|
|||
element.albumFile?.compareServerIdentity(albumFile) == true);
|
||||
}
|
||||
|
||||
@override
|
||||
getAll(Account account, List<File> albumFiles) async* {
|
||||
final results = await future_util.waitOr(
|
||||
albumFiles.map((f) => get(account, f)),
|
||||
(error, stackTrace) => ExceptionEvent(error, stackTrace),
|
||||
);
|
||||
for (final r in results) {
|
||||
yield r;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
final i = albums.indexWhere((element) =>
|
||||
|
@ -67,120 +79,6 @@ class MockAlbumMemoryRepo extends MockAlbumRepo {
|
|||
final List<Album> albums;
|
||||
}
|
||||
|
||||
/// Each MockAppDb instance contains a unique memory database
|
||||
class MockAppDb implements AppDb {
|
||||
static Future<MockAppDb> create({
|
||||
bool hasAlbumStore = true,
|
||||
bool hasFileDb2Store = true,
|
||||
bool hasDirStore = true,
|
||||
bool hasMetaStore = true,
|
||||
// compat
|
||||
bool hasFileStore = false,
|
||||
bool hasFileDbStore = false,
|
||||
}) async {
|
||||
final inst = MockAppDb();
|
||||
final db = await inst._dbFactory.open(
|
||||
"test.db",
|
||||
version: 1,
|
||||
onUpgradeNeeded: (event) async {
|
||||
final db = event.database;
|
||||
_createDb(
|
||||
db,
|
||||
hasAlbumStore: hasAlbumStore,
|
||||
hasFileDb2Store: hasFileDb2Store,
|
||||
hasDirStore: hasDirStore,
|
||||
hasMetaStore: hasMetaStore,
|
||||
hasFileStore: hasFileStore,
|
||||
hasFileDbStore: hasFileDbStore,
|
||||
);
|
||||
},
|
||||
);
|
||||
db.close();
|
||||
return inst;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T> use<T>(Transaction Function(Database db) transactionBuilder,
|
||||
FutureOr<T> Function(Transaction transaction) fn) async {
|
||||
final db = await _dbFactory.open(
|
||||
"test.db",
|
||||
version: 1,
|
||||
onUpgradeNeeded: (event) async {
|
||||
final db = event.database;
|
||||
_createDb(db);
|
||||
},
|
||||
);
|
||||
|
||||
Transaction? transaction;
|
||||
try {
|
||||
transaction = transactionBuilder(db);
|
||||
return await fn(transaction);
|
||||
} finally {
|
||||
if (transaction != null) {
|
||||
await transaction.completed;
|
||||
}
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete() async {
|
||||
await _dbFactory.deleteDatabase("test.db");
|
||||
}
|
||||
|
||||
static void _createDb(
|
||||
Database db, {
|
||||
bool hasAlbumStore = true,
|
||||
bool hasFileDb2Store = true,
|
||||
bool hasDirStore = true,
|
||||
bool hasMetaStore = true,
|
||||
// compat
|
||||
bool hasFileStore = false,
|
||||
bool hasFileDbStore = false,
|
||||
}) {
|
||||
if (hasAlbumStore) {
|
||||
final albumStore = db.createObjectStore(AppDb.albumStoreName);
|
||||
albumStore.createIndex(
|
||||
AppDbAlbumEntry.indexName, AppDbAlbumEntry.keyPath);
|
||||
}
|
||||
if (hasFileDb2Store) {
|
||||
final file2Store = db.createObjectStore(AppDb.file2StoreName);
|
||||
file2Store.createIndex(AppDbFile2Entry.strippedPathIndexName,
|
||||
AppDbFile2Entry.strippedPathKeyPath);
|
||||
file2Store.createIndex(AppDbFile2Entry.dateTimeEpochMsIndexName,
|
||||
AppDbFile2Entry.dateTimeEpochMsKeyPath);
|
||||
}
|
||||
if (hasDirStore) {
|
||||
db.createObjectStore(AppDb.dirStoreName);
|
||||
}
|
||||
if (hasMetaStore) {
|
||||
db.createObjectStore(AppDb.metaStoreName,
|
||||
keyPath: AppDbMetaEntry.keyPath);
|
||||
}
|
||||
|
||||
// compat
|
||||
if (hasFileStore) {
|
||||
final fileStore = db.createObjectStore(_fileStoreName);
|
||||
fileStore.createIndex(_fileIndexName, _fileKeyPath);
|
||||
}
|
||||
if (hasFileDbStore) {
|
||||
final fileDbStore = db.createObjectStore(_fileDbStoreName);
|
||||
fileDbStore.createIndex(_fileDbIndexName, _fileDbKeyPath, unique: false);
|
||||
}
|
||||
}
|
||||
|
||||
late final _dbFactory = newIdbFactoryMemory();
|
||||
|
||||
// compat only
|
||||
static const _fileDbStoreName = "filesDb";
|
||||
static const _fileDbIndexName = "fileDbStore_namespacedFileId";
|
||||
static const _fileDbKeyPath = "namespacedFileId";
|
||||
|
||||
static const _fileStoreName = "files";
|
||||
static const _fileIndexName = "fileStore_path_index";
|
||||
static const _fileKeyPath = ["path", "index"];
|
||||
}
|
||||
|
||||
/// EventBus that ignore all events
|
||||
class MockEventBus implements EventBus {
|
||||
@override
|
||||
|
@ -200,10 +98,10 @@ class MockEventBus implements EventBus {
|
|||
final _streamController = StreamController.broadcast();
|
||||
}
|
||||
|
||||
/// Mock of [FileRepo] where all methods will throw UnimplementedError
|
||||
class MockFileRepo implements FileRepo {
|
||||
/// Mock of [FileDataSource] where all methods will throw UnimplementedError
|
||||
class MockFileDataSource implements FileDataSource {
|
||||
@override
|
||||
Future<void> copy(Object account, File f, String destination,
|
||||
Future<void> copy(Account account, File f, String destination,
|
||||
{bool? shouldOverwrite}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
@ -214,20 +112,17 @@ class MockFileRepo implements FileRepo {
|
|||
}
|
||||
|
||||
@override
|
||||
FileDataSource get dataSrc => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<Uint8List> getBinary(Account account, File file) {
|
||||
Future<Uint8List> getBinary(Account account, File f) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<File>> list(Account account, File root) async {
|
||||
Future<List<File>> list(Account account, File dir) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<File> listSingle(Account account, File root) async {
|
||||
Future<File> listSingle(Account account, File f) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
@ -243,14 +138,14 @@ class MockFileRepo implements FileRepo {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(Account account, File file) {
|
||||
Future<void> remove(Account account, File f) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateProperty(
|
||||
Account account,
|
||||
File file, {
|
||||
File f, {
|
||||
OrNull<Metadata>? metadata,
|
||||
OrNull<bool>? isArchived,
|
||||
OrNull<DateTime>? overrideDateTime,
|
||||
|
@ -260,9 +155,9 @@ class MockFileRepo implements FileRepo {
|
|||
}
|
||||
}
|
||||
|
||||
/// [FileRepo] mock that support some ops with an internal List
|
||||
class MockFileMemoryRepo extends MockFileRepo {
|
||||
MockFileMemoryRepo([
|
||||
/// [FileDataSource] mock that support some ops with an internal List
|
||||
class MockFileMemoryDataSource extends MockFileDataSource {
|
||||
MockFileMemoryDataSource([
|
||||
List<File> initialData = const [],
|
||||
]) : files = initialData.map((f) => f.copyWith()).toList() {
|
||||
_id = files
|
||||
|
@ -274,7 +169,12 @@ class MockFileMemoryRepo extends MockFileRepo {
|
|||
|
||||
@override
|
||||
list(Account account, File root) async {
|
||||
return files.where((f) => file_util.isOrUnderDir(f, root)).toList();
|
||||
return files.where((f) => path_lib.dirname(f.path) == root.path).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
listSingle(Account account, File file) async {
|
||||
return files.where((f) => f.strippedPath == file.strippedPath).first;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -289,9 +189,78 @@ class MockFileMemoryRepo extends MockFileRepo {
|
|||
}
|
||||
|
||||
final List<File> files;
|
||||
// ignore: unused_field
|
||||
var _id = 0;
|
||||
}
|
||||
|
||||
class MockFileWebdavDataSource implements FileWebdavDataSource {
|
||||
const MockFileWebdavDataSource(this.src);
|
||||
|
||||
@override
|
||||
copy(Account account, File f, String destination, {bool? shouldOverwrite}) =>
|
||||
src.copy(account, f, destination, shouldOverwrite: shouldOverwrite);
|
||||
|
||||
@override
|
||||
createDir(Account account, String path) => src.createDir(account, path);
|
||||
|
||||
@override
|
||||
getBinary(Account account, File f) => src.getBinary(account, f);
|
||||
|
||||
@override
|
||||
list(Account account, File dir, {int? depth}) async {
|
||||
if (depth == 0) {
|
||||
return [await src.listSingle(account, dir)];
|
||||
} else {
|
||||
return src.list(account, dir);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
listSingle(Account account, File f) => src.listSingle(account, f);
|
||||
|
||||
@override
|
||||
move(Account account, File f, String destination, {bool? shouldOverwrite}) =>
|
||||
src.move(account, f, destination, shouldOverwrite: shouldOverwrite);
|
||||
|
||||
@override
|
||||
putBinary(Account account, String path, Uint8List content) =>
|
||||
src.putBinary(account, path, content);
|
||||
|
||||
@override
|
||||
remove(Account account, File f) => src.remove(account, f);
|
||||
|
||||
@override
|
||||
updateProperty(
|
||||
Account account,
|
||||
File f, {
|
||||
OrNull<Metadata>? metadata,
|
||||
OrNull<bool>? isArchived,
|
||||
OrNull<DateTime>? overrideDateTime,
|
||||
bool? favorite,
|
||||
}) =>
|
||||
src.updateProperty(
|
||||
account,
|
||||
f,
|
||||
metadata: metadata,
|
||||
isArchived: isArchived,
|
||||
overrideDateTime: overrideDateTime,
|
||||
favorite: favorite,
|
||||
);
|
||||
|
||||
final MockFileMemoryDataSource src;
|
||||
}
|
||||
|
||||
/// [FileRepo] mock that support some ops with an internal List
|
||||
class MockFileMemoryRepo extends FileRepo {
|
||||
MockFileMemoryRepo([
|
||||
List<File> initialData = const [],
|
||||
]) : super(MockFileMemoryDataSource(initialData));
|
||||
|
||||
List<File> get files {
|
||||
return (dataSrc as MockFileMemoryDataSource).files;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock of [ShareRepo] where all methods will throw UnimplementedError
|
||||
class MockShareRepo implements ShareRepo {
|
||||
@override
|
||||
|
@ -426,6 +395,4 @@ extension MockDiContainerExtension on DiContainer {
|
|||
MockShareMemoryRepo get shareMemoryRepo => shareRepo as MockShareMemoryRepo;
|
||||
MockShareeMemoryRepo get shareeMemoryRepo =>
|
||||
shareeRepo as MockShareeMemoryRepo;
|
||||
|
||||
MockAppDb get appMemeoryDb => appDb as MockAppDb;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' as sql;
|
||||
import 'package:drift/native.dart' as sql;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:idb_shim/idb.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/ci_string.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
|
@ -13,8 +13,12 @@ import 'package:nc_photos/entity/album/sort_provider.dart';
|
|||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite_table_converter.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/type.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class FilesBuilder {
|
||||
FilesBuilder({
|
||||
|
@ -29,21 +33,25 @@ class FilesBuilder {
|
|||
String relativePath, {
|
||||
int? contentLength,
|
||||
String? contentType,
|
||||
String? etag,
|
||||
DateTime? lastModified,
|
||||
bool isCollection = false,
|
||||
bool hasPreview = true,
|
||||
String ownerId = "admin",
|
||||
Metadata? metadata,
|
||||
}) {
|
||||
files.add(File(
|
||||
path: "remote.php/dav/files/$relativePath",
|
||||
contentLength: contentLength,
|
||||
contentType: contentType,
|
||||
etag: etag,
|
||||
lastModified:
|
||||
lastModified ?? DateTime.utc(2020, 1, 2, 3, 4, 5 + files.length),
|
||||
isCollection: isCollection,
|
||||
hasPreview: hasPreview,
|
||||
fileId: fileId++,
|
||||
ownerId: ownerId.toCi(),
|
||||
metadata: metadata,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -51,6 +59,7 @@ class FilesBuilder {
|
|||
String relativePath,
|
||||
String contentType, {
|
||||
int contentLength = 1024,
|
||||
String? etag,
|
||||
DateTime? lastModified,
|
||||
bool hasPreview = true,
|
||||
String ownerId = "admin",
|
||||
|
@ -59,6 +68,7 @@ class FilesBuilder {
|
|||
relativePath,
|
||||
contentLength: contentLength,
|
||||
contentType: contentType,
|
||||
etag: etag,
|
||||
lastModified: lastModified,
|
||||
hasPreview: hasPreview,
|
||||
ownerId: ownerId,
|
||||
|
@ -67,33 +77,62 @@ class FilesBuilder {
|
|||
void addJpeg(
|
||||
String relativePath, {
|
||||
int contentLength = 1024,
|
||||
String? etag,
|
||||
DateTime? lastModified,
|
||||
bool hasPreview = true,
|
||||
String ownerId = "admin",
|
||||
OrNull<Metadata>? metadata,
|
||||
}) =>
|
||||
add(
|
||||
relativePath,
|
||||
contentLength: contentLength,
|
||||
contentType: "image/jpeg",
|
||||
etag: etag,
|
||||
lastModified: lastModified,
|
||||
hasPreview: hasPreview,
|
||||
ownerId: ownerId,
|
||||
metadata: metadata?.obj ??
|
||||
Metadata(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
|
||||
imageWidth: 640,
|
||||
imageHeight: 480,
|
||||
),
|
||||
);
|
||||
|
||||
void addDir(
|
||||
String relativePath, {
|
||||
int contentLength = 1024,
|
||||
String? etag,
|
||||
DateTime? lastModified,
|
||||
String ownerId = "admin",
|
||||
}) =>
|
||||
add(
|
||||
relativePath,
|
||||
etag: etag,
|
||||
lastModified: lastModified,
|
||||
isCollection: true,
|
||||
hasPreview: false,
|
||||
ownerId: ownerId,
|
||||
);
|
||||
|
||||
void addAlbumJson(
|
||||
String homeDir,
|
||||
String filename, {
|
||||
int contentLength = 1024,
|
||||
String? etag,
|
||||
DateTime? lastModified,
|
||||
String ownerId = "admin",
|
||||
}) =>
|
||||
add(
|
||||
"$homeDir/.com.nkming.nc_photos/albums/$filename.nc_album.json",
|
||||
contentLength: contentLength,
|
||||
contentType: "application/json",
|
||||
etag: etag,
|
||||
lastModified: lastModified,
|
||||
hasPreview: false,
|
||||
ownerId: ownerId,
|
||||
);
|
||||
|
||||
final files = <File>[];
|
||||
int fileId;
|
||||
}
|
||||
|
@ -339,45 +378,189 @@ Sharee buildSharee({
|
|||
shareWith: shareWith,
|
||||
);
|
||||
|
||||
Future<void> fillAppDb(
|
||||
AppDb appDb, Account account, Iterable<File> files) async {
|
||||
await appDb.use(
|
||||
(db) => db.transaction(AppDb.file2StoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final file2Store = transaction.objectStore(AppDb.file2StoreName);
|
||||
for (final f in files) {
|
||||
await file2Store.put(AppDbFile2Entry.fromFile(account, f).toJson(),
|
||||
AppDbFile2Entry.toPrimaryKeyForFile(account, f));
|
||||
}
|
||||
},
|
||||
sql.SqliteDb buildTestDb() {
|
||||
sql.driftRuntimeOptions.debugPrint = _debugPrintSql;
|
||||
return sql.SqliteDb(
|
||||
executor: sql.NativeDatabase.memory(
|
||||
logStatements: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> fillAppDbDir(
|
||||
AppDb appDb, Account account, File dir, List<File> children) async {
|
||||
await appDb.use(
|
||||
(db) => db.transaction(AppDb.dirStoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
await dirStore.put(
|
||||
AppDbDirEntry.fromFiles(account, dir, children).toJson(),
|
||||
AppDbDirEntry.toPrimaryKeyForDir(account, dir));
|
||||
},
|
||||
);
|
||||
Future<void> insertFiles(
|
||||
sql.SqliteDb db, Account account, Iterable<File> files) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
for (final f in files) {
|
||||
final sharedQuery = db.selectOnly(db.files).join([
|
||||
sql.innerJoin(
|
||||
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId),
|
||||
useColumns: false),
|
||||
])
|
||||
..addColumns([db.files.rowId])
|
||||
..where(db.accountFiles.account.equals(dbAccount.rowId).not())
|
||||
..where(db.files.fileId.equals(f.fileId!));
|
||||
var rowId = (await sharedQuery.map((r) => r.read(db.files.rowId)).get())
|
||||
.firstOrNull;
|
||||
final insert = SqliteFileConverter.toSql(dbAccount, f);
|
||||
if (rowId == null) {
|
||||
final dbFile = await db.into(db.files).insertReturning(insert.file);
|
||||
rowId = dbFile.rowId;
|
||||
}
|
||||
final dbAccountFile = await db
|
||||
.into(db.accountFiles)
|
||||
.insertReturning(insert.accountFile.copyWith(file: sql.Value(rowId)));
|
||||
if (insert.image != null) {
|
||||
await db.into(db.images).insert(
|
||||
insert.image!.copyWith(accountFile: sql.Value(dbAccountFile.rowId)));
|
||||
}
|
||||
if (insert.trash != null) {
|
||||
await db
|
||||
.into(db.trashes)
|
||||
.insert(insert.trash!.copyWith(file: sql.Value(rowId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<T>> listAppDb<T>(
|
||||
AppDb appDb, String storeName, T Function(JsonObj) transform) {
|
||||
return appDb.use(
|
||||
(db) => db.transaction(storeName, idbModeReadOnly),
|
||||
(transaction) async {
|
||||
final store = transaction.objectStore(storeName);
|
||||
return await store
|
||||
.openCursor(autoAdvance: true)
|
||||
.map((c) => c.value)
|
||||
.cast<Map>()
|
||||
.map((e) => transform(e.cast<String, dynamic>()))
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
Future<void> insertDirRelation(
|
||||
sql.SqliteDb db, Account account, File dir, Iterable<File> children) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
final dirRowIds = (await db
|
||||
.accountFileRowIdsByFileIds([dir.fileId!], sqlAccount: dbAccount))
|
||||
.first;
|
||||
final childRowIds = await db.accountFileRowIdsByFileIds(
|
||||
[dir, ...children].map((f) => f.fileId!),
|
||||
sqlAccount: dbAccount);
|
||||
await db.batch((batch) {
|
||||
batch.insertAll(
|
||||
db.dirFiles,
|
||||
childRowIds.map((c) => sql.DirFilesCompanion.insert(
|
||||
dir: dirRowIds.fileRowId,
|
||||
child: c.fileRowId,
|
||||
)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> insertAlbums(
|
||||
sql.SqliteDb db, Account account, Iterable<Album> albums) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
for (final a in albums) {
|
||||
final rowIds =
|
||||
await db.accountFileRowIdsOf(a.albumFile!, sqlAccount: dbAccount);
|
||||
final insert = SqliteAlbumConverter.toSql(a, rowIds.fileRowId);
|
||||
final dbAlbum = await db.into(db.albums).insertReturning(insert.album);
|
||||
for (final s in insert.albumShares) {
|
||||
await db
|
||||
.into(db.albumShares)
|
||||
.insert(s.copyWith(album: sql.Value(dbAlbum.rowId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Set<File>> listSqliteDbFiles(sql.SqliteDb db) async {
|
||||
final query = db.select(db.files).join([
|
||||
sql.innerJoin(
|
||||
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),
|
||||
sql.innerJoin(
|
||||
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account)),
|
||||
sql.leftOuterJoin(
|
||||
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
|
||||
sql.leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
|
||||
]);
|
||||
return (await query
|
||||
.map((r) => SqliteFileConverter.fromSql(
|
||||
r.readTable(db.accounts).userId,
|
||||
sql.CompleteFile(
|
||||
r.readTable(db.files),
|
||||
r.readTable(db.accountFiles),
|
||||
r.readTableOrNull(db.images),
|
||||
r.readTableOrNull(db.trashes),
|
||||
),
|
||||
))
|
||||
.get())
|
||||
.toSet();
|
||||
}
|
||||
|
||||
Future<Map<File, Set<File>>> listSqliteDbDirs(sql.SqliteDb db) async {
|
||||
final query = db.select(db.files).join([
|
||||
sql.innerJoin(
|
||||
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),
|
||||
sql.innerJoin(
|
||||
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account)),
|
||||
sql.leftOuterJoin(
|
||||
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
|
||||
sql.leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
|
||||
]);
|
||||
final fileMap = Map.fromEntries(await query.map((r) {
|
||||
final f = sql.CompleteFile(
|
||||
r.readTable(db.files),
|
||||
r.readTable(db.accountFiles),
|
||||
r.readTableOrNull(db.images),
|
||||
r.readTableOrNull(db.trashes),
|
||||
);
|
||||
return MapEntry(
|
||||
f.file.rowId,
|
||||
SqliteFileConverter.fromSql(r.readTable(db.accounts).userId, f),
|
||||
);
|
||||
}).get());
|
||||
|
||||
final dirQuery = db.select(db.dirFiles);
|
||||
final dirs = await dirQuery.map((r) => Tuple2(r.dir, r.child)).get();
|
||||
final result = <File, Set<File>>{};
|
||||
for (final d in dirs) {
|
||||
(result[fileMap[d.item1]!] ??= <File>{}).add(fileMap[d.item2]!);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Set<Album>> listSqliteDbAlbums(sql.SqliteDb db) async {
|
||||
final albumQuery = db.select(db.albums).join([
|
||||
sql.innerJoin(db.files, db.files.rowId.equalsExp(db.albums.file)),
|
||||
sql.innerJoin(
|
||||
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),
|
||||
sql.innerJoin(
|
||||
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account)),
|
||||
]);
|
||||
final albums = await albumQuery.map((r) {
|
||||
final albumFile = SqliteFileConverter.fromSql(
|
||||
r.readTable(db.accounts).userId,
|
||||
sql.CompleteFile(
|
||||
r.readTable(db.files),
|
||||
r.readTable(db.accountFiles),
|
||||
null,
|
||||
null,
|
||||
),
|
||||
);
|
||||
return Tuple2(
|
||||
r.read(db.albums.rowId),
|
||||
SqliteAlbumConverter.fromSql(r.readTable(db.albums), albumFile, []),
|
||||
);
|
||||
}).get();
|
||||
|
||||
final results = <Album>{};
|
||||
for (final a in albums) {
|
||||
final shareQuery = db.select(db.albumShares)
|
||||
..where((t) => t.album.equals(a.item1));
|
||||
final dbShares = await shareQuery.get();
|
||||
results.add(a.item2.copyWith(
|
||||
lastUpdated: OrNull(null),
|
||||
shares: dbShares.isEmpty
|
||||
? null
|
||||
: OrNull(dbShares
|
||||
.map((s) => AlbumShare(
|
||||
userId: s.userId.toCi(),
|
||||
displayName: s.displayName,
|
||||
sharedAt: s.sharedAt))
|
||||
.toList()),
|
||||
));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
bool shouldPrintSql = false;
|
||||
|
||||
void _debugPrintSql(String log) {
|
||||
if (shouldPrintSql) {
|
||||
debugPrint(log, wrapWidth: 1024);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import 'package:nc_photos/entity/album/item.dart';
|
|||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/use_case/add_to_album.dart';
|
||||
|
@ -44,13 +44,17 @@ Future<void> _addFile() async {
|
|||
final album = util.AlbumBuilder().build();
|
||||
final albumFile = album.albumFile!;
|
||||
final c = DiContainer(
|
||||
fileRepo: MockFileMemoryRepo(),
|
||||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
shareRepo: MockShareRepo(),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, [file]);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider()),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, [file]);
|
||||
});
|
||||
|
||||
await AddToAlbum(c)(
|
||||
account,
|
||||
|
@ -80,7 +84,7 @@ Future<void> _addFile() async {
|
|||
addedBy: "admin".toCi(),
|
||||
addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5),
|
||||
file: file,
|
||||
),
|
||||
).minimize(),
|
||||
],
|
||||
latestItemTime: DateTime.utc(2020, 1, 2, 3, 4, 5),
|
||||
),
|
||||
|
@ -106,13 +110,17 @@ Future<void> _addExistingFile() async {
|
|||
final newFile = files[0].copyWith();
|
||||
final albumFile = album.albumFile!;
|
||||
final c = DiContainer(
|
||||
fileRepo: MockFileMemoryRepo(),
|
||||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
shareRepo: MockShareRepo(),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider()),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
await AddToAlbum(c)(
|
||||
account,
|
||||
|
@ -188,14 +196,19 @@ Future<void> _addExistingSharedFile() async {
|
|||
final album = (util.AlbumBuilder()..addFileItem(files[0])).build();
|
||||
final albumFile = album.albumFile!;
|
||||
final c = DiContainer(
|
||||
fileRepo: MockFileMemoryRepo(),
|
||||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
shareRepo: MockShareRepo(),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDb(obj, user1Account, user1Files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider()),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
|
||||
});
|
||||
|
||||
await AddToAlbum(c)(
|
||||
account,
|
||||
|
@ -247,17 +260,21 @@ Future<void> _addFileToSharedAlbumOwned() async {
|
|||
final album = (util.AlbumBuilder()..addShare("user1")).build();
|
||||
final albumFile = album.albumFile!;
|
||||
final c = DiContainer(
|
||||
fileRepo: MockFileMemoryRepo(),
|
||||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
shareRepo: MockShareMemoryRepo([
|
||||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, [file]);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider({
|
||||
"isLabEnableSharedAlbum": true,
|
||||
})),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, [file]);
|
||||
});
|
||||
|
||||
await AddToAlbum(c)(
|
||||
account,
|
||||
|
@ -290,17 +307,21 @@ Future<void> _addFileOwnedByUserToSharedAlbumOwned() async {
|
|||
final album = (util.AlbumBuilder()..addShare("user1")).build();
|
||||
final albumFile = album.albumFile!;
|
||||
final c = DiContainer(
|
||||
fileRepo: MockFileMemoryRepo(),
|
||||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
shareRepo: MockShareMemoryRepo([
|
||||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, [file]);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider({
|
||||
"isLabEnableSharedAlbum": true,
|
||||
})),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, [file]);
|
||||
});
|
||||
|
||||
await AddToAlbum(c)(
|
||||
account,
|
||||
|
@ -335,6 +356,7 @@ Future<void> _addFileToMultiuserSharedAlbumNotOwned() async {
|
|||
.build();
|
||||
final albumFile = album.albumFile!;
|
||||
final c = DiContainer(
|
||||
fileRepo: MockFileMemoryRepo(),
|
||||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
shareRepo: MockShareMemoryRepo([
|
||||
util.buildShare(
|
||||
|
@ -342,13 +364,16 @@ Future<void> _addFileToMultiuserSharedAlbumNotOwned() async {
|
|||
util.buildShare(
|
||||
id: "1", file: albumFile, uidOwner: "user1", shareWith: "user2"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, [file]);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider({
|
||||
"isLabEnableSharedAlbum": true,
|
||||
})),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, [file]);
|
||||
});
|
||||
|
||||
await AddToAlbum(c)(
|
||||
account,
|
||||
|
|
|
@ -1,230 +0,0 @@
|
|||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/list_extension.dart';
|
||||
import 'package:nc_photos/use_case/compat/v37.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../mock_type.dart';
|
||||
import '../../test_util.dart' as util;
|
||||
|
||||
void main() {
|
||||
group("CompatV37", () {
|
||||
group("isAppDbNeedMigration", () {
|
||||
test("w/ meta entry == false", _isAppDbNeedMigrationEntryFalse);
|
||||
test("w/ meta entry == true", _isAppDbNeedMigrationEntryTrue);
|
||||
test("w/o meta entry", _isAppDbNeedMigrationWithoutEntry);
|
||||
});
|
||||
group("migrateAppDb", () {
|
||||
test("w/o nomedia", _migrateAppDbWithoutNomedia);
|
||||
test("w/ nomedia", _migrateAppDb);
|
||||
test("w/ nomedia nested dir", _migrateAppDbNestedDir);
|
||||
test("w/ nomedia nested no media marker", _migrateAppDbNestedMarker);
|
||||
test("w/ nomedia root", _migrateAppDbRoot);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Check if migration is necessary with isMigrated flag = false
|
||||
///
|
||||
/// Expect: true
|
||||
Future<void> _isAppDbNeedMigrationEntryFalse() async {
|
||||
final appDb = MockAppDb();
|
||||
await appDb.use(
|
||||
(db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
const entry = AppDbMetaEntryCompatV37(false);
|
||||
await metaStore.put(entry.toEntry().toJson());
|
||||
},
|
||||
);
|
||||
|
||||
expect(await CompatV37.isAppDbNeedMigration(appDb), true);
|
||||
}
|
||||
|
||||
/// Check if migration is necessary with isMigrated flag = true
|
||||
///
|
||||
/// Expect: false
|
||||
Future<void> _isAppDbNeedMigrationEntryTrue() async {
|
||||
final appDb = MockAppDb();
|
||||
await appDb.use(
|
||||
(db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
const entry = AppDbMetaEntryCompatV37(true);
|
||||
await metaStore.put(entry.toEntry().toJson());
|
||||
},
|
||||
);
|
||||
|
||||
expect(await CompatV37.isAppDbNeedMigration(appDb), false);
|
||||
}
|
||||
|
||||
/// Check if migration is necessary with isMigrated flag missing
|
||||
///
|
||||
/// Expect: false
|
||||
Future<void> _isAppDbNeedMigrationWithoutEntry() async {
|
||||
final appDb = MockAppDb();
|
||||
await appDb.use(
|
||||
(db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
const entry = AppDbMetaEntryCompatV37(true);
|
||||
await metaStore.put(entry.toEntry().toJson());
|
||||
},
|
||||
);
|
||||
|
||||
expect(await CompatV37.isAppDbNeedMigration(appDb), false);
|
||||
}
|
||||
|
||||
/// Migrate db without nomedia file
|
||||
///
|
||||
/// Expect: all files remain
|
||||
Future<void> _migrateAppDbWithoutNomedia() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/dir1")
|
||||
..addJpeg("admin/dir1/test2.jpg"))
|
||||
.build();
|
||||
final appDb = MockAppDb();
|
||||
await util.fillAppDb(appDb, account, files);
|
||||
await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 3));
|
||||
await util.fillAppDbDir(appDb, account, files[2], [files[3]]);
|
||||
await CompatV37.migrateAppDb(appDb);
|
||||
|
||||
final fileObjs = await util.listAppDb(
|
||||
appDb, AppDb.file2StoreName, (e) => AppDbFile2Entry.fromJson(e).file);
|
||||
expect(fileObjs, files);
|
||||
final dirEntries = await util.listAppDb(
|
||||
appDb, AppDb.dirStoreName, (e) => AppDbDirEntry.fromJson(e));
|
||||
expect(dirEntries, [
|
||||
AppDbDirEntry.fromFiles(account, files[0], files.slice(1, 3)),
|
||||
AppDbDirEntry.fromFiles(account, files[2], [files[3]]),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Migrate db with nomedia file
|
||||
///
|
||||
/// nomedia: admin/dir1/.nomedia
|
||||
/// Expect: files (except .nomedia) under admin/dir1 removed
|
||||
Future<void> _migrateAppDb() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/dir1")
|
||||
..addGenericFile("admin/dir1/.nomedia", "text/plain")
|
||||
..addJpeg("admin/dir1/test2.jpg"))
|
||||
.build();
|
||||
final appDb = MockAppDb();
|
||||
await util.fillAppDb(appDb, account, files);
|
||||
await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 3));
|
||||
await util.fillAppDbDir(appDb, account, files[2], files.slice(3, 5));
|
||||
await CompatV37.migrateAppDb(appDb);
|
||||
|
||||
final fileObjs = await util.listAppDb(
|
||||
appDb, AppDb.file2StoreName, (e) => AppDbFile2Entry.fromJson(e).file);
|
||||
expect(fileObjs, files.slice(0, 4));
|
||||
final dirEntries = await util.listAppDb(
|
||||
appDb, AppDb.dirStoreName, (e) => AppDbDirEntry.fromJson(e));
|
||||
expect(dirEntries, [
|
||||
AppDbDirEntry.fromFiles(account, files[0], files.slice(1, 3)),
|
||||
AppDbDirEntry.fromFiles(account, files[2], [files[3]]),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Migrate db with nomedia file
|
||||
///
|
||||
/// nomedia: admin/dir1/.nomedia
|
||||
/// Expect: files (except .nomedia) under admin/dir1 removed
|
||||
Future<void> _migrateAppDbNestedDir() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/dir1")
|
||||
..addGenericFile("admin/dir1/.nomedia", "text/plain")
|
||||
..addJpeg("admin/dir1/test2.jpg")
|
||||
..addDir("admin/dir1/dir1-1")
|
||||
..addJpeg("admin/dir1/dir1-1/test3.jpg"))
|
||||
.build();
|
||||
final appDb = MockAppDb();
|
||||
await util.fillAppDb(appDb, account, files);
|
||||
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[5], [files[6]]);
|
||||
await CompatV37.migrateAppDb(appDb);
|
||||
|
||||
final fileObjs = await util.listAppDb(
|
||||
appDb, AppDb.file2StoreName, (e) => AppDbFile2Entry.fromJson(e).file);
|
||||
expect(fileObjs, files.slice(0, 4));
|
||||
final dirEntries = await util.listAppDb(
|
||||
appDb, AppDb.dirStoreName, (e) => AppDbDirEntry.fromJson(e));
|
||||
expect(dirEntries, [
|
||||
AppDbDirEntry.fromFiles(account, files[0], files.slice(1, 3)),
|
||||
AppDbDirEntry.fromFiles(account, files[2], [files[3]]),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Migrate db with nomedia file
|
||||
///
|
||||
/// nomedia: admin/dir1/.nomedia, admin/dir1/dir1-1/.nomedia
|
||||
/// Expect: files (except admin/dir1/.nomedia) under admin/dir1 removed
|
||||
Future<void> _migrateAppDbNestedMarker() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/dir1")
|
||||
..addGenericFile("admin/dir1/.nomedia", "text/plain")
|
||||
..addJpeg("admin/dir1/test2.jpg")
|
||||
..addDir("admin/dir1/dir1-1")
|
||||
..addGenericFile("admin/dir1/dir1-1/.nomedia", "text/plain")
|
||||
..addJpeg("admin/dir1/dir1-1/test3.jpg"))
|
||||
.build();
|
||||
final appDb = MockAppDb();
|
||||
await util.fillAppDb(appDb, account, files);
|
||||
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[5], files.slice(6, 8));
|
||||
await CompatV37.migrateAppDb(appDb);
|
||||
|
||||
final fileObjs = await util.listAppDb(
|
||||
appDb, AppDb.file2StoreName, (e) => AppDbFile2Entry.fromJson(e).file);
|
||||
expect(fileObjs, files.slice(0, 4));
|
||||
final dirEntries = await util.listAppDb(
|
||||
appDb, AppDb.dirStoreName, (e) => AppDbDirEntry.fromJson(e));
|
||||
expect(dirEntries, [
|
||||
AppDbDirEntry.fromFiles(account, files[0], files.slice(1, 3)),
|
||||
AppDbDirEntry.fromFiles(account, files[2], [files[3]]),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Migrate db with nomedia file
|
||||
///
|
||||
/// nomedia: admin/.nomedia
|
||||
/// Expect: files (except .nomedia) under admin removed
|
||||
Future<void> _migrateAppDbRoot() async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addDir("admin")
|
||||
..addGenericFile("admin/.nomedia", "text/plain")
|
||||
..addJpeg("admin/test1.jpg")
|
||||
..addDir("admin/dir1")
|
||||
..addJpeg("admin/dir1/test2.jpg"))
|
||||
.build();
|
||||
final appDb = MockAppDb();
|
||||
await util.fillAppDb(appDb, account, files);
|
||||
await util.fillAppDbDir(appDb, account, files[0], files.slice(1, 4));
|
||||
await util.fillAppDbDir(appDb, account, files[3], [files[4]]);
|
||||
await CompatV37.migrateAppDb(appDb);
|
||||
|
||||
final objs = await util.listAppDb(
|
||||
appDb, AppDb.file2StoreName, (e) => AppDbFile2Entry.fromJson(e).file);
|
||||
expect(objs, files.slice(0, 2));
|
||||
final dirEntries = await util.listAppDb(
|
||||
appDb, AppDb.dirStoreName, (e) => AppDbDirEntry.fromJson(e));
|
||||
expect(dirEntries, [
|
||||
AppDbDirEntry.fromFiles(account, files[0], [files[1]]),
|
||||
]);
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/use_case/db_compat/v5.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../mock_type.dart';
|
||||
import '../../test_util.dart' as util;
|
||||
|
||||
void main() {
|
||||
group("DbCompatV5", () {
|
||||
group("isNeedMigration", () {
|
||||
test("w/ meta entry == false", () async {
|
||||
final appDb = MockAppDb();
|
||||
await appDb.use(
|
||||
(db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
const entry = AppDbMetaEntryDbCompatV5(false);
|
||||
await metaStore.put(entry.toEntry().toJson());
|
||||
},
|
||||
);
|
||||
|
||||
expect(await DbCompatV5.isNeedMigration(appDb), true);
|
||||
});
|
||||
|
||||
test("w/ meta entry == true", () async {
|
||||
final appDb = MockAppDb();
|
||||
await appDb.use(
|
||||
(db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
const entry = AppDbMetaEntryDbCompatV5(true);
|
||||
await metaStore.put(entry.toEntry().toJson());
|
||||
},
|
||||
);
|
||||
|
||||
expect(await DbCompatV5.isNeedMigration(appDb), false);
|
||||
});
|
||||
|
||||
test("w/o meta entry", () async {
|
||||
final appDb = MockAppDb();
|
||||
await appDb.use(
|
||||
(db) => db.transaction(AppDb.metaStoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final metaStore = transaction.objectStore(AppDb.metaStoreName);
|
||||
const entry = AppDbMetaEntryDbCompatV5(true);
|
||||
await metaStore.put(entry.toEntry().toJson());
|
||||
},
|
||||
);
|
||||
|
||||
expect(await DbCompatV5.isNeedMigration(appDb), false);
|
||||
});
|
||||
});
|
||||
|
||||
test("migrate", () async {
|
||||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()
|
||||
..addJpeg(
|
||||
"admin/test1.jpg",
|
||||
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 5),
|
||||
))
|
||||
.build();
|
||||
final appDb = MockAppDb();
|
||||
await appDb.use(
|
||||
(db) => db.transaction(AppDb.file2StoreName, idbModeReadWrite),
|
||||
(transaction) async {
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
await fileStore.put({
|
||||
"server": account.url,
|
||||
"userId": account.username.toCaseInsensitiveString(),
|
||||
"strippedPath": files[0].strippedPathWithEmpty,
|
||||
"file": files[0].toJson(),
|
||||
}, "${account.url}/${account.username.toCaseInsensitiveString()}/${files[0].fileId}");
|
||||
},
|
||||
);
|
||||
await DbCompatV5.migrate(appDb);
|
||||
|
||||
final objs =
|
||||
await util.listAppDb(appDb, AppDb.file2StoreName, (item) => item);
|
||||
expect(objs, [
|
||||
{
|
||||
"server": account.url,
|
||||
"userId": account.username.toCaseInsensitiveString(),
|
||||
"strippedPath": files[0].strippedPathWithEmpty,
|
||||
"dateTimeEpochMs": 1577934245000,
|
||||
"file": files[0].toJson(),
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/use_case/find_file.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../mock_type.dart';
|
||||
import '../test_util.dart' as util;
|
||||
|
||||
void main() {
|
||||
|
@ -23,10 +22,13 @@ Future<void> _findFile() async {
|
|||
..addJpeg("admin/test2.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
expect(await FindFile(c)(account, [1]), [files[1]]);
|
||||
}
|
||||
|
@ -38,10 +40,13 @@ Future<void> _findMissingFile() async {
|
|||
final account = util.buildAccount();
|
||||
final files = (util.FilesBuilder()..addJpeg("admin/test1.jpg")).build();
|
||||
final c = DiContainer(
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
expect(() => FindFile(c)(account, [1]), throwsStateError);
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ Future<void> _root() async {
|
|||
expect(
|
||||
await Ls(fileRepo)(
|
||||
account, File(path: file_util.unstripPath(account, "."))),
|
||||
files.slice(1, 4),
|
||||
files.slice(1, 3),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/use_case/remove_album.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
@ -35,9 +35,10 @@ Future<void> _removeAlbum() async {
|
|||
albumRepo: MockAlbumMemoryRepo([album1, album2]),
|
||||
fileRepo: MockFileMemoryRepo([albumFile1, albumFile2]),
|
||||
shareRepo: MockShareRepo(),
|
||||
appDb: MockAppDb(),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider()),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
|
||||
await RemoveAlbum(c)(
|
||||
account, c.albumMemoryRepo.findAlbumByPath(albumFile1.path));
|
||||
|
@ -65,11 +66,12 @@ Future<void> _removeSharedAlbum() async {
|
|||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
|
||||
]),
|
||||
appDb: MockAppDb(),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider({
|
||||
"isLabEnableSharedAlbum": true,
|
||||
})),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
|
||||
await RemoveAlbum(c)(
|
||||
account, c.albumMemoryRepo.findAlbumByPath(albumFile.path));
|
||||
|
@ -106,11 +108,12 @@ Future<void> _removeSharedAlbumFileInOtherAlbum() async {
|
|||
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
|
||||
util.buildShare(id: "2", file: albumFiles[1], shareWith: "user1"),
|
||||
]),
|
||||
appDb: MockAppDb(),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider({
|
||||
"isLabEnableSharedAlbum": true,
|
||||
})),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
|
||||
await RemoveAlbum(c)(
|
||||
account, c.albumMemoryRepo.findAlbumByPath(albumFiles[0].path));
|
||||
|
@ -146,14 +149,19 @@ Future<void> _removeSharedAlbumResyncedFile() async {
|
|||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDb(obj, user1Account, user1Files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider({
|
||||
"isLabEnableSharedAlbum": true,
|
||||
})),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
|
||||
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
|
||||
});
|
||||
|
||||
await RemoveAlbum(c)(
|
||||
account, c.albumMemoryRepo.findAlbumByPath(albumFile.path));
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:nc_photos/entity/album.dart';
|
|||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/use_case/remove_from_album.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
@ -52,10 +52,13 @@ Future<void> _removeLastFile() async {
|
|||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
fileRepo: MockFileMemoryRepo([albumFile, file1]),
|
||||
shareRepo: MockShareRepo(),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
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 RemoveFromAlbum(c)(
|
||||
account, c.albumMemoryRepo.findAlbumByPath(albumFile.path), [fileItem1]);
|
||||
|
@ -100,10 +103,13 @@ Future<void> _remove1OfNFiles() async {
|
|||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
fileRepo: MockFileMemoryRepo([albumFile, ...files]),
|
||||
shareRepo: MockShareRepo(),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
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 RemoveFromAlbum(c)(account,
|
||||
c.albumMemoryRepo.findAlbumByPath(albumFile.path), [fileItems[0]]);
|
||||
|
@ -119,7 +125,7 @@ Future<void> _remove1OfNFiles() async {
|
|||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
|
||||
name: "test",
|
||||
provider: AlbumStaticProvider(
|
||||
items: [fileItems[1], fileItems[2]],
|
||||
items: [fileItems[1].minimize(), fileItems[2].minimize()],
|
||||
latestItemTime: files[2].lastModified,
|
||||
),
|
||||
coverProvider: AlbumAutoCoverProvider(coverFile: files[2]),
|
||||
|
@ -154,10 +160,13 @@ Future<void> _removeLatestOfNFiles() async {
|
|||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
fileRepo: MockFileMemoryRepo([albumFile, ...files]),
|
||||
shareRepo: MockShareRepo(),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
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 RemoveFromAlbum(c)(account,
|
||||
c.albumMemoryRepo.findAlbumByPath(albumFile.path), [fileItems[0]]);
|
||||
|
@ -173,7 +182,7 @@ Future<void> _removeLatestOfNFiles() async {
|
|||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
|
||||
name: "test",
|
||||
provider: AlbumStaticProvider(
|
||||
items: [fileItems[1], fileItems[2]],
|
||||
items: [fileItems[1].minimize(), fileItems[2].minimize()],
|
||||
latestItemTime: files[1].lastModified,
|
||||
),
|
||||
coverProvider: AlbumAutoCoverProvider(coverFile: files[1]),
|
||||
|
@ -205,10 +214,13 @@ Future<void> _removeManualCoverFile() async {
|
|||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
fileRepo: MockFileMemoryRepo([albumFile, ...files]),
|
||||
shareRepo: MockShareRepo(),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
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 RemoveFromAlbum(c)(account,
|
||||
c.albumMemoryRepo.findAlbumByPath(albumFile.path), [fileItems[0]]);
|
||||
|
@ -224,7 +236,7 @@ Future<void> _removeManualCoverFile() async {
|
|||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
|
||||
name: "test",
|
||||
provider: AlbumStaticProvider(
|
||||
items: [fileItems[1], fileItems[2]],
|
||||
items: [fileItems[1].minimize(), fileItems[2].minimize()],
|
||||
latestItemTime: files[2].lastModified,
|
||||
),
|
||||
coverProvider: AlbumAutoCoverProvider(coverFile: files[2]),
|
||||
|
@ -256,10 +268,13 @@ Future<void> _removeFromSharedAlbumOwned() async {
|
|||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
util.buildShare(id: "1", file: file1, shareWith: "user1"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
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 RemoveFromAlbum(c)(
|
||||
account, c.albumMemoryRepo.findAlbumByPath(albumFile.path), [fileItem1]);
|
||||
|
@ -299,10 +314,14 @@ Future<void> _removeFromSharedAlbumOwnedWithOtherShare() async {
|
|||
util.buildShare(
|
||||
id: "3", uidOwner: "user1", file: file1, shareWith: "user2"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, user1Account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, user1Account, files);
|
||||
});
|
||||
|
||||
await RemoveFromAlbum(c)(
|
||||
account, c.albumMemoryRepo.findAlbumByPath(albumFile.path), [fileItem1]);
|
||||
|
@ -343,10 +362,13 @@ Future<void> _removeFromSharedAlbumOwnedLeaveExtraShare() async {
|
|||
util.buildShare(id: "1", file: file1, shareWith: "user1"),
|
||||
util.buildShare(id: "2", file: file1, shareWith: "user2"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
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 RemoveFromAlbum(c)(
|
||||
account, c.albumMemoryRepo.findAlbumByPath(albumFile.path), [fileItem1]);
|
||||
|
@ -389,10 +411,13 @@ Future<void> _removeFromSharedAlbumOwnedFileInOtherAlbum() async {
|
|||
util.buildShare(id: "2", file: files[0], shareWith: "user2"),
|
||||
util.buildShare(id: "3", file: album2File, shareWith: "user1"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
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 RemoveFromAlbum(c)(account,
|
||||
c.albumMemoryRepo.findAlbumByPath(album1File.path), [album1fileItems[0]]);
|
||||
|
@ -432,10 +457,13 @@ Future<void> _removeFromSharedAlbumNotOwned() async {
|
|||
util.buildShare(id: "2", file: file1, shareWith: "user1"),
|
||||
util.buildShare(id: "3", file: file1, shareWith: "user2"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
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 RemoveFromAlbum(c)(
|
||||
account, c.albumMemoryRepo.findAlbumByPath(albumFile.path), [fileItem1]);
|
||||
|
@ -479,10 +507,13 @@ Future<void> _removeFromSharedAlbumNotOwnedWithOwnerShare() async {
|
|||
util.buildShare(
|
||||
id: "3", uidOwner: "user1", file: file1, shareWith: "user2"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
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 RemoveFromAlbum(c)(
|
||||
account, c.albumMemoryRepo.findAlbumByPath(albumFile.path), [fileItem1]);
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:nc_photos/entity/album.dart';
|
|||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/use_case/remove.dart';
|
||||
|
@ -45,11 +45,14 @@ Future<void> _removeFile() async {
|
|||
albumRepo: MockAlbumMemoryRepo(),
|
||||
fileRepo: MockFileMemoryRepo(files),
|
||||
shareRepo: MockShareMemoryRepo(),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider()),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
await Remove(c)(account, [files[0]]);
|
||||
expect(c.fileMemoryRepo.files, [files[1]]);
|
||||
|
@ -68,11 +71,14 @@ Future<void> _removeFileNoCleanUp() async {
|
|||
albumRepo: MockAlbumMemoryRepo(),
|
||||
fileRepo: MockFileMemoryRepo(files),
|
||||
shareRepo: MockShareMemoryRepo(),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider()),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
await Remove(c)(account, [files[0]], shouldCleanUp: false);
|
||||
expect(c.fileMemoryRepo.files, [files[1]]);
|
||||
|
@ -91,11 +97,14 @@ Future<void> _removeAlbumFile() async {
|
|||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
fileRepo: MockFileMemoryRepo([albumFile, ...files]),
|
||||
shareRepo: MockShareMemoryRepo(),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider()),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
await Remove(c)(account, [files[0]]);
|
||||
expect(
|
||||
|
@ -132,11 +141,14 @@ Future<void> _removeAlbumFileNoCleanUp() async {
|
|||
albumRepo: MockAlbumMemoryRepo([album]),
|
||||
fileRepo: MockFileMemoryRepo([albumFile, ...files]),
|
||||
shareRepo: MockShareMemoryRepo(),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider()),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
await Remove(c)(account, [files[0]], shouldCleanUp: false);
|
||||
expect(
|
||||
|
@ -182,11 +194,14 @@ Future<void> _removeSharedAlbumFile() async {
|
|||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider()),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
await Remove(c)(account, [files[0]]);
|
||||
expect(
|
||||
|
@ -246,12 +261,17 @@ Future<void> _removeSharedAlbumSharedFile() async {
|
|||
id: "2", file: user1Files[0], uidOwner: "user1", shareWith: "admin"),
|
||||
util.buildShare(id: "3", file: files[0], shareWith: "user2"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDb(obj, user1Account, user1Files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider()),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
|
||||
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
|
||||
});
|
||||
|
||||
await Remove(c)(account, [files[0]]);
|
||||
expect(
|
||||
|
@ -309,11 +329,14 @@ Future<void> _removeSharedAlbumResyncedFile() async {
|
|||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
|
||||
]),
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
pref: Pref.scoped(PrefMemoryProvider()),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
await Remove(c)(account, [files[0]]);
|
||||
expect(
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
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/object_extension.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/use_case/scan_dir_offline.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../mock_type.dart';
|
||||
import '../test_util.dart' as util;
|
||||
|
||||
void main() {
|
||||
|
@ -33,10 +32,13 @@ Future<void> _root() async {
|
|||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
// convert to set because ScanDirOffline does not guarantee order
|
||||
expect(
|
||||
|
@ -59,10 +61,13 @@ Future<void> _subDir() async {
|
|||
..addJpeg("admin/test/test2.jpg"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
expect(
|
||||
(await ScanDirOffline(c)(
|
||||
|
@ -84,10 +89,13 @@ Future<void> _unsupportedFile() async {
|
|||
..addGenericFile("admin/test2.pdf", "application/pdf"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
}),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await util.insertFiles(c.sqliteDb, account, files);
|
||||
});
|
||||
|
||||
// convert to set because ScanDirOffline does not guarantee order
|
||||
expect(
|
||||
|
@ -118,11 +126,15 @@ Future<void> _multiAccountRoot() async {
|
|||
..addJpeg("user1/test/test2.jpg", ownerId: "user1"))
|
||||
.build();
|
||||
final c = DiContainer(
|
||||
appDb: await MockAppDb().applyFuture((obj) async {
|
||||
await util.fillAppDb(obj, account, files);
|
||||
await util.fillAppDb(obj, user1Account, user1Files);
|
||||
}),
|
||||
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 c.sqliteDb.insertAccountOf(user1Account);
|
||||
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
|
||||
});
|
||||
|
||||
expect(
|
||||
(await ScanDirOffline(c)(
|
||||
|
|
|
@ -36,8 +36,9 @@ Future<void> _unshareWithoutFile() async {
|
|||
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
|
||||
util.buildShare(id: "1", file: albumFile, shareWith: "user2"),
|
||||
]),
|
||||
appDb: MockAppDb(),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
|
||||
await UnshareAlbumWithUser(c)(account,
|
||||
c.albumMemoryRepo.findAlbumByPath(albumFile.path), "user1".toCi());
|
||||
|
@ -74,8 +75,9 @@ Future<void> _unshareWithFile() async {
|
|||
util.buildShare(id: "2", file: file1, shareWith: "user1"),
|
||||
util.buildShare(id: "3", file: file1, shareWith: "user2"),
|
||||
]),
|
||||
appDb: MockAppDb(),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
|
||||
await UnshareAlbumWithUser(c)(account,
|
||||
c.albumMemoryRepo.findAlbumByPath(albumFile.path), "user1".toCi());
|
||||
|
@ -123,8 +125,9 @@ Future<void> _unshareWithFileNotOwned() async {
|
|||
util.buildShare(
|
||||
id: "5", uidOwner: "user2", file: files[1], shareWith: "user1"),
|
||||
]),
|
||||
appDb: MockAppDb(),
|
||||
sqliteDb: util.buildTestDb(),
|
||||
);
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
|
||||
await UnshareAlbumWithUser(c)(account,
|
||||
c.albumMemoryRepo.findAlbumByPath(albumFile.path), "user1".toCi());
|
||||
|
|
BIN
app/web/sqlite3.wasm
Normal file
BIN
app/web/sqlite3.wasm
Normal file
Binary file not shown.
Loading…
Reference in a new issue