Refactor: extract all db code to dedicated package

This commit is contained in:
Ming Ming 2023-12-06 22:05:33 +08:00
parent 702fac4b38
commit 0e4411c725
133 changed files with 8062 additions and 4074 deletions

View file

@ -1,4 +1,3 @@
import 'package:drift/drift.dart';
import 'package:equatable/equatable.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart';
@ -30,19 +29,16 @@ 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/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/isolate_util.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';
import 'package:nc_photos/entity/tagged_file/data_source.dart';
import 'package:nc_photos/k.dart' as k;
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/mobile/self_signed_cert_manager.dart';
import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/touch_manager.dart';
import 'package:np_db/np_db.dart';
import 'package:np_gps_map/np_gps_map.dart';
import 'package:np_log/np_log.dart' as np_log;
import 'package:np_platform_util/np_platform_util.dart';
@ -64,10 +60,6 @@ Future<void> init(InitIsolateType isolateType) async {
initLog();
await _initDeviceInfo();
initDrift();
if (isolateType == InitIsolateType.main) {
await _initDriftWorkaround();
}
_initKiwi();
await _initPref();
await _initAccountPrefs();
@ -93,19 +85,6 @@ void initLog() {
);
}
void initDrift() {
driftRuntimeOptions.debugPrint = (log) => debugPrint(log, wrapWidth: 1024);
}
Future<void> _initDriftWorkaround() async {
if (getRawPlatform() == NpPlatform.android && AndroidInfo().sdkInt < 24) {
_log.info("[_initDriftWorkaround] Workaround Android 6- bug");
// see: https://github.com/flutter/flutter/issues/73318 and
// https://github.com/simolus3/drift/issues/895
await platform.applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
}
}
Future<void> _initPref() async {
final provider = PrefSharedPreferencesProvider();
await provider.init();
@ -156,15 +135,15 @@ void _initSelfSignedCertManager() {
Future<void> _initDiContainer(InitIsolateType isolateType) async {
final c = DiContainer.late();
c.pref = Pref();
c.sqliteDb = await _createDb(isolateType);
c.npDb = await _createDb(isolateType);
c.albumRepo = AlbumRepo(AlbumCachedDataSource(c));
c.albumRepoRemote = AlbumRepo(AlbumRemoteDataSource());
c.albumRepoLocal = AlbumRepo(AlbumSqliteDbDataSource(c));
c.albumRepo2 = CachedAlbumRepo2(
const AlbumRemoteDataSource2(), AlbumSqliteDbDataSource2(c.sqliteDb));
const AlbumRemoteDataSource2(), AlbumSqliteDbDataSource2(c.npDb));
c.albumRepo2Remote = const BasicAlbumRepo2(AlbumRemoteDataSource2());
c.albumRepo2Local = BasicAlbumRepo2(AlbumSqliteDbDataSource2(c.sqliteDb));
c.albumRepo2Local = BasicAlbumRepo2(AlbumSqliteDbDataSource2(c.npDb));
c.fileRepo = FileRepo(FileCachedDataSource(c));
c.fileRepoRemote = const FileRepo(FileWebdavDataSource());
c.fileRepoLocal = FileRepo(FileSqliteDbDataSource(c));
@ -173,25 +152,25 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
c.favoriteRepo = const FavoriteRepo(FavoriteRemoteDataSource());
c.tagRepo = const TagRepo(TagRemoteDataSource());
c.tagRepoRemote = const TagRepo(TagRemoteDataSource());
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.npDb));
c.taggedFileRepo = const TaggedFileRepo(TaggedFileRemoteDataSource());
c.searchRepo = SearchRepo(SearchSqliteDbDataSource(c));
c.ncAlbumRepo = CachedNcAlbumRepo(
const NcAlbumRemoteDataSource(), NcAlbumSqliteDbDataSource(c.sqliteDb));
const NcAlbumRemoteDataSource(), NcAlbumSqliteDbDataSource(c.npDb));
c.ncAlbumRepoRemote = const BasicNcAlbumRepo(NcAlbumRemoteDataSource());
c.ncAlbumRepoLocal = BasicNcAlbumRepo(NcAlbumSqliteDbDataSource(c.sqliteDb));
c.ncAlbumRepoLocal = BasicNcAlbumRepo(NcAlbumSqliteDbDataSource(c.npDb));
c.faceRecognitionPersonRepo = const BasicFaceRecognitionPersonRepo(
FaceRecognitionPersonRemoteDataSource());
c.faceRecognitionPersonRepoRemote = const BasicFaceRecognitionPersonRepo(
FaceRecognitionPersonRemoteDataSource());
c.faceRecognitionPersonRepoLocal = BasicFaceRecognitionPersonRepo(
FaceRecognitionPersonSqliteDbDataSource(c.sqliteDb));
FaceRecognitionPersonSqliteDbDataSource(c.npDb));
c.recognizeFaceRepo =
const BasicRecognizeFaceRepo(RecognizeFaceRemoteDataSource());
c.recognizeFaceRepoRemote =
const BasicRecognizeFaceRepo(RecognizeFaceRemoteDataSource());
c.recognizeFaceRepoLocal =
BasicRecognizeFaceRepo(RecognizeFaceSqliteDbDataSource(c.sqliteDb));
BasicRecognizeFaceRepo(RecognizeFaceSqliteDbDataSource(c.npDb));
c.touchManager = TouchManager(c);
@ -207,21 +186,14 @@ 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 (getRawPlatform() == NpPlatform.web) {
// no isolate support on web
return sql.SqliteDb();
} else {
return sql_isolate.createDb();
}
case InitIsolateType.flutterIsolate:
// service already runs in an isolate
return sql.SqliteDb();
Future<NpDb> _createDb(InitIsolateType isolateType) async {
final npDb = NpDb();
if (isolateType == InitIsolateType.main) {
await npDb.initMainIsolate(androidSdk: AndroidInfo().sdkInt);
} else {
await npDb.initBackgroundIsolate(androidSdk: AndroidInfo().sdkInt);
}
return npDb;
}
final _log = Logger("app_init");

View file

@ -8,7 +8,6 @@ import 'package:nc_photos/entity/file_descriptor.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';
import 'package:nc_photos/use_case/list_location_file.dart';
import 'package:nc_photos/use_case/list_location_group.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:to_string/to_string.dart';
@ -90,7 +89,6 @@ class ListLocationBloc
extends Bloc<ListLocationBlocEvent, ListLocationBlocState> {
ListLocationBloc(this._c)
: assert(require(_c)),
assert(ListLocationFile.require(_c)),
super(ListLocationBlocInit()) {
_fileRemovedEventListener.begin();

View file

@ -122,7 +122,6 @@ class ScanAccountDirBloc
ScanAccountDirBloc._(this.account) : super(const ScanAccountDirBlocInit()) {
final c = KiwiContainer().resolve<DiContainer>();
assert(require(c));
assert(ScanDirOffline.require(c));
_c = c;
_fileRemovedEventListener.begin();
@ -437,10 +436,13 @@ class ScanAccountDirBloc
}
/// Query a small amount of files to give an illusion of quick startup
Future<List<File>> _queryOfflineMini(ScanAccountDirBlocQueryBase ev) async {
Future<List<FileDescriptor>> _queryOfflineMini(
ScanAccountDirBlocQueryBase ev) async {
return await ScanDirOfflineMini(_c)(
account,
account.roots.map((r) => File(path: file_util.unstripPath(account, r))),
account.roots
.map((r) => File(path: file_util.unstripPath(account, r)))
.toList(),
scanMiniCount,
isOnlySupportedFormat: true,
);

View file

@ -58,7 +58,7 @@ abstract class SearchBlocState {
final Account? account;
final SearchCriteria criteria;
final List<File> items;
final List<FileDescriptor> items;
}
class SearchBlocInit extends SearchBlocState {
@ -67,20 +67,20 @@ class SearchBlocInit extends SearchBlocState {
class SearchBlocLoading extends SearchBlocState {
const SearchBlocLoading(
Account? account, SearchCriteria criteria, List<File> items)
Account? account, SearchCriteria criteria, List<FileDescriptor> items)
: super(account, criteria, items);
}
class SearchBlocSuccess extends SearchBlocState {
const SearchBlocSuccess(
Account? account, SearchCriteria criteria, List<File> items)
Account? account, SearchCriteria criteria, List<FileDescriptor> items)
: super(account, criteria, items);
}
@toString
class SearchBlocFailure extends SearchBlocState {
const SearchBlocFailure(Account? account, SearchCriteria criteria,
List<File> items, this.exception)
List<FileDescriptor> items, this.exception)
: super(account, criteria, items);
@override
@ -93,7 +93,7 @@ class SearchBlocFailure extends SearchBlocState {
/// may have been changed externally
class SearchBlocInconsistent extends SearchBlocState {
const SearchBlocInconsistent(
Account? account, SearchCriteria criteria, List<File> items)
Account? account, SearchCriteria criteria, List<FileDescriptor> items)
: super(account, criteria, items);
}
@ -196,7 +196,7 @@ class SearchBloc extends Bloc<SearchBlocEvent, SearchBlocState> {
}
}
Future<List<File>> _query(SearchBlocQuery ev) =>
Future<List<FileDescriptor>> _query(SearchBlocQuery ev) =>
Search(_c)(ev.account, ev.criteria);
bool _isFileOfInterest(FileDescriptor file) {

View file

@ -0,0 +1,434 @@
import 'dart:convert';
import 'package:nc_photos/account.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/face_recognition_person.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/nc_album.dart';
import 'package:nc_photos/entity/nc_album_item.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:nc_photos/entity/tag.dart';
import 'package:nc_photos/use_case/list_location_group.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_common/object_util.dart';
import 'package:np_common/or_null.dart';
import 'package:np_common/type.dart';
import 'package:np_db/np_db.dart';
import 'package:np_string/np_string.dart';
abstract class DbAccountConverter {
static DbAccount toDb(Account src) => DbAccount(
serverAddress: src.url,
userId: src.userId,
);
}
extension AccountExtension on Account {
DbAccount toDb() => DbAccountConverter.toDb(this);
}
extension AccountListExtension on List<Account> {
List<DbAccount> toDb() => map(DbAccountConverter.toDb).toList();
}
abstract class DbAlbumConverter {
static Album fromDb(File albumFile, DbAlbum src) {
return Album(
lastUpdated: src.lastUpdated,
name: src.name,
provider: AlbumProvider.fromJson({
"type": src.providerType,
"content": src.providerContent,
}),
coverProvider: AlbumCoverProvider.fromJson({
"type": src.coverProviderType,
"content": src.coverProviderContent,
}),
sortProvider: AlbumSortProvider.fromJson({
"type": src.sortProviderType,
"content": src.sortProviderContent,
}),
shares: src.shares.isEmpty
? null
: src.shares
.map((e) => AlbumShare(
userId: e.userId.toCi(),
displayName: e.displayName,
sharedAt: e.sharedAt.toUtc(),
))
.toList(),
// replace with the original etag when this album was cached
albumFile: albumFile.copyWith(etag: OrNull(src.fileEtag)),
savedVersion: src.version,
);
}
static DbAlbum toDb(Album src) {
final providerJson = src.provider.toJson();
final coverProviderJson = src.coverProvider.toJson();
final sortProviderJson = src.sortProvider.toJson();
return DbAlbum(
fileId: src.albumFile!.fileId!,
fileEtag: src.albumFile!.etag!,
version: Album.version,
lastUpdated: src.lastUpdated,
name: src.name,
providerType: providerJson["type"],
providerContent: providerJson["content"],
coverProviderType: coverProviderJson["type"],
coverProviderContent: coverProviderJson["content"],
sortProviderType: sortProviderJson["type"],
sortProviderContent: sortProviderJson["content"],
shares: src.shares
?.map((e) => DbAlbumShare(
userId: e.userId.toCaseInsensitiveString(),
displayName: e.displayName,
sharedAt: e.sharedAt,
))
.toList() ??
[],
);
}
}
abstract class DbFaceRecognitionPersonConverter {
static FaceRecognitionPerson fromDb(DbFaceRecognitionPerson src) {
return FaceRecognitionPerson(
name: src.name,
thumbFaceId: src.thumbFaceId,
count: src.count,
);
}
static DbFaceRecognitionPerson toDb(FaceRecognitionPerson src) {
return DbFaceRecognitionPerson(
name: src.name,
thumbFaceId: src.thumbFaceId,
count: src.count,
);
}
}
extension FaceRecognitionPersonExtension on FaceRecognitionPerson {
DbFaceRecognitionPerson toDb() => DbFaceRecognitionPersonConverter.toDb(this);
}
abstract class DbFileConverter {
static File fromDb(String userId, DbFile src) {
return File(
path: "remote.php/dav/files/$userId/${src.relativePath}",
contentLength: src.contentLength,
contentType: src.contentType,
etag: src.etag,
lastModified: src.lastModified,
isCollection: src.isCollection,
usedBytes: src.usedBytes,
hasPreview: src.hasPreview,
fileId: src.fileId,
isFavorite: src.isFavorite,
ownerId: src.ownerId,
ownerDisplayName: src.ownerDisplayName,
metadata: src.imageData?.let(DbMetadataConverter.fromDb),
isArchived: src.isArchived,
overrideDateTime: src.overrideDateTime,
trashbinFilename: src.trashData?.filename,
trashbinOriginalLocation: src.trashData?.originalLocation,
trashbinDeletionTime: src.trashData?.deletionTime,
location: src.location?.let(DbImageLocationConverter.fromDb),
);
}
static DbFile toDb(File src) {
return DbFile(
fileId: src.fileId!,
contentLength: src.contentLength,
contentType: src.contentType,
etag: src.etag,
lastModified: src.lastModified,
isCollection: src.isCollection,
usedBytes: src.usedBytes,
hasPreview: src.hasPreview,
ownerId: src.ownerId,
ownerDisplayName: src.ownerDisplayName,
relativePath: src.strippedPathWithEmpty,
isFavorite: src.isFavorite,
isArchived: src.isArchived,
overrideDateTime: src.overrideDateTime,
bestDateTime: src.bestDateTime,
imageData: src.metadata?.let((s) => DbImageData(
lastUpdated: s.lastUpdated,
fileEtag: s.fileEtag,
width: s.imageWidth,
height: s.imageHeight,
exif: s.exif?.toJson(),
exifDateTimeOriginal: s.exif?.dateTimeOriginal,
)),
location: src.location?.let((s) => DbLocation(
version: s.version,
name: s.name,
latitude: s.latitude,
longitude: s.longitude,
countryCode: s.countryCode,
admin1: s.admin1,
admin2: s.admin2,
)),
trashData: src.trashbinDeletionTime == null
? null
: DbTrashData(
filename: src.trashbinFilename!,
originalLocation: src.trashbinOriginalLocation!,
deletionTime: src.trashbinDeletionTime!,
),
);
}
}
abstract class DbFileDescriptorConverter {
static FileDescriptor fromDb(String userId, DbFileDescriptor src) {
return FileDescriptor(
fdPath: "remote.php/dav/files/$userId/${src.relativePath}",
fdId: src.fileId,
fdMime: src.contentType,
fdIsArchived: src.isArchived ?? false,
fdIsFavorite: src.isFavorite ?? false,
fdDateTime: src.bestDateTime,
);
}
}
extension FileDescriptorExtension on FileDescriptor {
DbFileKey toDbKey() => DbFileKey.byId(fdId);
}
abstract class DbMetadataConverter {
static Metadata fromDb(DbImageData src) {
return Metadata(
lastUpdated: src.lastUpdated,
fileEtag: src.fileEtag,
imageWidth: src.width,
imageHeight: src.height,
exif: src.exif?.let(Exif.new),
);
}
static DbImageData toDb(Metadata src) {
return DbImageData(
lastUpdated: src.lastUpdated,
fileEtag: src.fileEtag,
width: src.imageWidth,
height: src.imageHeight,
exif: src.exif?.toJson(),
exifDateTimeOriginal: src.exif?.dateTimeOriginal,
);
}
}
extension MetadataExtension on Metadata {
DbImageData toDb() => DbMetadataConverter.toDb(this);
}
abstract class DbImageLocationConverter {
static ImageLocation fromDb(DbLocation src) {
return ImageLocation(
version: src.version,
name: src.name,
latitude: src.latitude,
longitude: src.longitude,
countryCode: src.countryCode,
admin1: src.admin1,
admin2: src.admin2,
);
}
static DbLocation toDb(ImageLocation src) {
return DbLocation(
version: src.version,
name: src.name,
latitude: src.latitude,
longitude: src.longitude,
countryCode: src.countryCode,
admin1: src.admin1,
admin2: src.admin2,
);
}
}
extension ImageLocationExtension on ImageLocation {
DbLocation toDb() => DbImageLocationConverter.toDb(this);
}
abstract class DbLocationGroupConverter {
static LocationGroup fromDb(DbLocationGroup src) {
return LocationGroup(
src.place,
src.countryCode,
src.count,
src.latestFileId,
src.latestDateTime,
);
}
}
extension FileExtension on File {
DbFileKey toDbKey() {
if (fileId != null) {
return DbFileKey.byId(fileId!);
} else {
return DbFileKey.byPath(strippedPathWithEmpty);
}
}
DbFile toDb() => DbFileConverter.toDb(this);
}
abstract class DbNcAlbumConverter {
static NcAlbum fromDb(String userId, DbNcAlbum src) {
return NcAlbum(
path:
"${api.ApiPhotos.path}/$userId/${src.isOwned ? "albums" : "sharedalbums"}/${src.relativePath}",
lastPhoto: src.lastPhoto,
nbItems: src.nbItems,
location: src.location,
dateStart: src.dateStart,
dateEnd: src.dateEnd,
collaborators:
src.collaborators.map(NcAlbumCollaborator.fromJson).toList(),
);
}
static DbNcAlbum toDb(NcAlbum src) {
return DbNcAlbum(
relativePath: src.strippedPath,
lastPhoto: src.lastPhoto,
nbItems: src.nbItems,
location: src.location,
dateStart: src.dateStart,
dateEnd: src.dateEnd,
collaborators: src.collaborators.map((e) => e.toJson()).toList(),
isOwned: src.isOwned,
);
}
}
extension NcAlbumExtension on NcAlbum {
DbNcAlbum toDb() => DbNcAlbumConverter.toDb(this);
}
abstract class DbNcAlbumItemConverter {
static NcAlbumItem fromDb(String userId, String albumRelativePath,
bool isAlbumOwned, DbNcAlbumItem src) {
return NcAlbumItem(
path:
"${api.ApiPhotos.path}/$userId/${isAlbumOwned ? "albums" : "sharedalbums"}/$albumRelativePath/${src.relativePath}",
fileId: src.fileId,
contentLength: src.contentLength,
contentType: src.contentType,
etag: src.etag,
lastModified: src.lastModified,
hasPreview: src.hasPreview,
isFavorite: src.isFavorite,
fileMetadataWidth: src.fileMetadataWidth,
fileMetadataHeight: src.fileMetadataHeight,
);
}
static DbNcAlbumItem toDb(NcAlbumItem src) {
return DbNcAlbumItem(
relativePath: src.strippedPath,
fileId: src.fileId,
contentLength: src.contentLength,
contentType: src.contentType,
etag: src.etag,
lastModified: src.lastModified,
hasPreview: src.hasPreview,
isFavorite: src.isFavorite,
fileMetadataWidth: src.fileMetadataWidth,
fileMetadataHeight: src.fileMetadataHeight,
);
}
}
abstract class DbRecognizeFaceConverter {
static RecognizeFace fromDb(DbRecognizeFace src) {
return RecognizeFace(label: src.label);
}
static DbRecognizeFace toDb(RecognizeFace src) {
return DbRecognizeFace(
label: src.label,
);
}
}
extension RecognizeFaceExtension on RecognizeFace {
DbRecognizeFace toDb() => DbRecognizeFaceConverter.toDb(this);
}
abstract class DbRecognizeFaceItemConverter {
static RecognizeFaceItem fromDb(
String userId, String faceLabel, DbRecognizeFaceItem src) {
return RecognizeFaceItem(
path:
"${api.ApiRecognize.path}/$userId/faces/$faceLabel/${src.relativePath}",
fileId: src.fileId,
contentLength: src.contentLength,
contentType: src.contentType,
etag: src.etag,
lastModified: src.lastModified,
hasPreview: src.hasPreview,
realPath: src.realPath,
isFavorite: src.isFavorite,
fileMetadataWidth: src.fileMetadataWidth,
fileMetadataHeight: src.fileMetadataHeight,
faceDetections: src.faceDetections
?.let((obj) => (jsonDecode(obj) as List).cast<JsonObj>()),
);
}
static DbRecognizeFaceItem toDb(RecognizeFaceItem src) {
return DbRecognizeFaceItem(
relativePath: src.strippedPath,
fileId: src.fileId,
contentLength: src.contentLength,
contentType: src.contentType,
etag: src.etag,
lastModified: src.lastModified,
hasPreview: src.hasPreview,
realPath: src.realPath,
isFavorite: src.isFavorite,
fileMetadataWidth: src.fileMetadataWidth,
fileMetadataHeight: src.fileMetadataHeight,
faceDetections: src.faceDetections?.let(jsonEncode),
);
}
}
abstract class DbTagConverter {
static Tag fromDb(DbTag src) {
return Tag(
id: src.id,
displayName: src.displayName,
userVisible: src.userVisible,
userAssignable: src.userAssignable,
);
}
static DbTag toDb(Tag src) {
return DbTag(
id: src.id,
displayName: src.displayName,
userVisible: src.userVisible,
userAssignable: src.userAssignable,
);
}
}
extension TagExtension on Tag {
DbTag toDb() => DbTagConverter.toDb(this);
}

View file

@ -10,11 +10,11 @@ import 'package:nc_photos/entity/recognize_face/repo.dart';
import 'package:nc_photos/entity/search.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/tag.dart';
import 'package:nc_photos/entity/tagged_file.dart';
import 'package:nc_photos/touch_manager.dart';
import 'package:np_common/or_null.dart';
import 'package:np_db/np_db.dart';
enum DiType {
albumRepo,
@ -45,8 +45,8 @@ enum DiType {
recognizeFaceRepoRemote,
recognizeFaceRepoLocal,
pref,
sqliteDb,
touchManager,
npDb,
}
class DiContainer {
@ -79,8 +79,8 @@ class DiContainer {
RecognizeFaceRepo? recognizeFaceRepoRemote,
RecognizeFaceRepo? recognizeFaceRepoLocal,
Pref? pref,
sql.SqliteDb? sqliteDb,
TouchManager? touchManager,
NpDb? npDb,
}) : _albumRepo = albumRepo,
_albumRepoRemote = albumRepoRemote,
_albumRepoLocal = albumRepoLocal,
@ -109,8 +109,8 @@ class DiContainer {
_recognizeFaceRepoRemote = recognizeFaceRepoRemote,
_recognizeFaceRepoLocal = recognizeFaceRepoLocal,
_pref = pref,
_sqliteDb = sqliteDb,
_touchManager = touchManager;
_touchManager = touchManager,
_npDb = npDb;
DiContainer.late();
@ -172,10 +172,10 @@ class DiContainer {
return contianer._recognizeFaceRepoLocal != null;
case DiType.pref:
return contianer._pref != null;
case DiType.sqliteDb:
return contianer._sqliteDb != null;
case DiType.touchManager:
return contianer._touchManager != null;
case DiType.npDb:
return contianer._npDb != null;
}
}
@ -194,8 +194,8 @@ class DiContainer {
OrNull<FaceRecognitionPersonRepo>? faceRecognitionPersonRepo,
OrNull<RecognizeFaceRepo>? recognizeFaceRepo,
OrNull<Pref>? pref,
OrNull<sql.SqliteDb>? sqliteDb,
OrNull<TouchManager>? touchManager,
OrNull<NpDb>? npDb,
}) {
return DiContainer(
albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj,
@ -217,8 +217,8 @@ class DiContainer {
? _recognizeFaceRepo
: recognizeFaceRepo.obj,
pref: pref == null ? _pref : pref.obj,
sqliteDb: sqliteDb == null ? _sqliteDb : sqliteDb.obj,
touchManager: touchManager == null ? _touchManager : touchManager.obj,
npDb: npDb == null ? _npDb : npDb.obj,
);
}
@ -253,9 +253,9 @@ class DiContainer {
RecognizeFaceRepo get recognizeFaceRepoRemote => _recognizeFaceRepoRemote!;
RecognizeFaceRepo get recognizeFaceRepoLocal => _recognizeFaceRepoLocal!;
sql.SqliteDb get sqliteDb => _sqliteDb!;
Pref get pref => _pref!;
TouchManager get touchManager => _touchManager!;
NpDb get npDb => _npDb!;
set albumRepo(AlbumRepo v) {
assert(_albumRepo == null);
@ -392,11 +392,6 @@ class DiContainer {
_recognizeFaceRepoLocal = v;
}
set sqliteDb(sql.SqliteDb v) {
assert(_sqliteDb == null);
_sqliteDb = v;
}
set pref(Pref v) {
assert(_pref == null);
_pref = v;
@ -407,6 +402,11 @@ class DiContainer {
_touchManager = v;
}
set npDb(NpDb v) {
assert(_npDb == null);
_npDb = v;
}
AlbumRepo? _albumRepo;
AlbumRepo? _albumRepoRemote;
// Explicitly request a AlbumRepo backed by local source
@ -438,9 +438,9 @@ class DiContainer {
RecognizeFaceRepo? _recognizeFaceRepoRemote;
RecognizeFaceRepo? _recognizeFaceRepoLocal;
sql.SqliteDb? _sqliteDb;
Pref? _pref;
TouchManager? _touchManager;
NpDb? _npDb;
}
extension DiContainerExtension on DiContainer {

View file

@ -6,11 +6,11 @@ import 'package:nc_photos/entity/album/data_source2.dart';
import 'package:nc_photos/entity/album/repo2.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/exception_event.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_db/np_db.dart';
part 'data_source.g.dart';
@ -90,7 +90,7 @@ class AlbumSqliteDbDataSource implements AlbumDataSource {
_log.info(
"[getAll] ${albumFiles.map((f) => f.filename).toReadableString()}");
final failed = <String, Map>{};
final albums = await AlbumSqliteDbDataSource2(_c.sqliteDb).getAlbums(
final albums = await AlbumSqliteDbDataSource2(_c.npDb).getAlbums(
account,
albumFiles,
onError: (v, error, stackTrace) {
@ -119,13 +119,13 @@ class AlbumSqliteDbDataSource implements AlbumDataSource {
@override
create(Account account, Album album) async {
_log.info("[create]");
return AlbumSqliteDbDataSource2(_c.sqliteDb).create(account, album);
return AlbumSqliteDbDataSource2(_c.npDb).create(account, album);
}
@override
update(Account account, Album album) async {
_log.info("[update] ${album.albumFile!.path}");
return AlbumSqliteDbDataSource2(_c.sqliteDb).update(account, album);
return AlbumSqliteDbDataSource2(_c.npDb).update(account, album);
}
final DiContainer _c;
@ -134,7 +134,7 @@ class AlbumSqliteDbDataSource implements AlbumDataSource {
/// Backward compatibility only, use [CachedAlbumRepo2] instead
@npLog
class AlbumCachedDataSource implements AlbumDataSource {
AlbumCachedDataSource(DiContainer c) : sqliteDb = c.sqliteDb;
AlbumCachedDataSource(DiContainer c) : npDb = c.npDb;
@override
get(Account account, File albumFile) async {
@ -146,7 +146,7 @@ class AlbumCachedDataSource implements AlbumDataSource {
getAll(Account account, List<File> albumFiles) async* {
final repo = CachedAlbumRepo2(
const AlbumRemoteDataSource2(),
AlbumSqliteDbDataSource2(sqliteDb),
AlbumSqliteDbDataSource2(npDb),
);
final albums = await repo.getAlbums(account, albumFiles).last;
for (final a in albums) {
@ -158,7 +158,7 @@ class AlbumCachedDataSource implements AlbumDataSource {
update(Account account, Album album) {
return CachedAlbumRepo2(
const AlbumRemoteDataSource2(),
AlbumSqliteDbDataSource2(sqliteDb),
AlbumSqliteDbDataSource2(npDb),
).update(account, album);
}
@ -166,9 +166,9 @@ class AlbumCachedDataSource implements AlbumDataSource {
create(Account account, Album album) {
return CachedAlbumRepo2(
const AlbumRemoteDataSource2(),
AlbumSqliteDbDataSource2(sqliteDb),
AlbumSqliteDbDataSource2(npDb),
).create(account, album);
}
final sql.SqliteDb sqliteDb;
final NpDb npDb;
}

View file

@ -3,26 +3,26 @@ import 'dart:math';
import 'package:clock/clock.dart';
import 'package:collection/collection.dart';
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/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/repo2.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/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart' as sql;
import 'package:nc_photos/exception.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';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_common/or_null.dart';
import 'package:np_common/type.dart';
import 'package:np_db/np_db.dart';
part 'data_source2.g.dart';
@ -113,7 +113,7 @@ class AlbumRemoteDataSource2 implements AlbumDataSource2 {
@npLog
class AlbumSqliteDbDataSource2 implements AlbumDataSource2 {
const AlbumSqliteDbDataSource2(this.sqliteDb);
const AlbumSqliteDbDataSource2(this.npDb);
@override
Future<List<Album>> getAlbums(
@ -121,73 +121,38 @@ class AlbumSqliteDbDataSource2 implements AlbumDataSource2 {
List<File> albumFiles, {
ErrorWithValueHandler<File>? onError,
}) async {
late final List<sql.CompleteFile> dbFiles;
late final List<sql.AlbumWithShare> albumWithShares;
await 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(
"[getAlbums] File missing for album (rowId: ${s.album.rowId}");
} else {
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
final albums = await npDb.getAlbumsByAlbumFileIds(
account: account.toDb(),
fileIds: albumFiles.map((e) => e.fileId!).toList(),
);
final files = await npDb.getFilesByFileIds(
account: account.toDb(),
fileIds: albums.map((e) => e.fileId).toList(),
);
final albumMap = albums.map((e) => MapEntry(e.fileId, e)).toMap();
final fileMap = files.map((e) => MapEntry(e.fileId, e)).toMap();
return albumFiles
.map((f) {
final item = fileIdMap[f.fileId];
if (item == null) {
var dbAlbum = albumMap[f.fileId];
final dbFile = fileMap[f.fileId];
if (dbAlbum == null || dbFile == null) {
// cache not found
onError?.call(
f, const CacheNotFoundException(), StackTrace.current);
return null;
} else {
try {
final queriedFile = sql.SqliteFileConverter.fromSql(
account.userId.toString(), item["file"]);
var dbAlbum = item["album"] as sql.Album;
if (dbAlbum.version < 9) {
dbAlbum = AlbumUpgraderV8(logFilePath: queriedFile.path)
.doDb(dbAlbum)!;
}
return sql.SqliteAlbumConverter.fromSql(
dbAlbum, queriedFile, item["shares"] ?? []);
} catch (e, stackTrace) {
_log.severe("[getAlbums] Failed while converting DB entry", e,
stackTrace);
onError?.call(f, e, stackTrace);
return null;
}
try {
final file =
DbFileConverter.fromDb(account.userId.toString(), dbFile);
if (dbAlbum.version < 9) {
dbAlbum = AlbumUpgraderV8(logFilePath: file.path).doDb(dbAlbum)!;
}
return DbAlbumConverter.fromDb(file, dbAlbum);
} catch (e, stackTrace) {
_log.severe(
"[getAlbums] Failed while converting DB entry", e, stackTrace);
onError?.call(f, e, stackTrace);
return null;
}
})
.whereNotNull()
@ -203,50 +168,12 @@ class AlbumSqliteDbDataSource2 implements AlbumDataSource2 {
@override
Future<void> update(Account account, Album album) async {
_log.info("[update] ${album.albumFile!.path}");
await sqliteDb.use((db) async {
final rowIds =
await db.accountFileRowIdsOf(album.albumFile!, appAccount: account);
final insert = sql.SqliteAlbumConverter.toSql(
album, rowIds.fileRowId, album.albumFile!.etag!);
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!))),
);
});
}
});
await npDb.syncAlbum(
account: account.toDb(),
albumFile: DbFileConverter.toDb(album.albumFile!),
album: DbAlbumConverter.toDb(album),
);
}
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 sql.SqliteDb sqliteDb;
final NpDb npDb;
}

View file

@ -1,15 +1,13 @@
import 'dart:convert';
import 'package:clock/clock.dart';
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/exif.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/object_extension.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/type.dart';
import 'package:np_db/np_db.dart';
import 'package:np_string/np_string.dart';
import 'package:tuple/tuple.dart';
@ -17,7 +15,7 @@ part 'upgrader.g.dart';
abstract class AlbumUpgrader {
JsonObj? doJson(JsonObj json);
sql.Album? doDb(sql.Album dbObj);
DbAlbum? doDb(DbAlbum dbObj);
}
/// Upgrade v1 Album to v2
@ -37,7 +35,7 @@ class AlbumUpgraderV1 implements AlbumUpgrader {
}
@override
sql.Album? doDb(sql.Album dbObj) => null;
DbAlbum? doDb(DbAlbum dbObj) => null;
/// File path for logging only
final String? logFilePath;
@ -72,7 +70,7 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
}
@override
sql.Album? doDb(sql.Album dbObj) => null;
DbAlbum? doDb(DbAlbum dbObj) => null;
/// File path for logging only
final String? logFilePath;
@ -101,7 +99,7 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
}
@override
sql.Album? doDb(sql.Album dbObj) => null;
DbAlbum? doDb(DbAlbum dbObj) => null;
/// File path for logging only
final String? logFilePath;
@ -172,7 +170,7 @@ class AlbumUpgraderV4 implements AlbumUpgrader {
}
@override
sql.Album? doDb(sql.Album dbObj) => null;
DbAlbum? doDb(DbAlbum dbObj) => null;
/// File path for logging only
final String? logFilePath;
@ -216,7 +214,7 @@ class AlbumUpgraderV5 implements AlbumUpgrader {
}
@override
sql.Album? doDb(sql.Album dbObj) => null;
DbAlbum? doDb(DbAlbum dbObj) => null;
final Account account;
final File? albumFile;
@ -239,7 +237,7 @@ class AlbumUpgraderV6 implements AlbumUpgrader {
}
@override
sql.Album? doDb(sql.Album dbObj) => null;
DbAlbum? doDb(DbAlbum dbObj) => null;
/// File path for logging only
final String? logFilePath;
@ -259,7 +257,7 @@ class AlbumUpgraderV7 implements AlbumUpgrader {
}
@override
sql.Album? doDb(sql.Album dbObj) => null;
DbAlbum? doDb(DbAlbum dbObj) => null;
/// File path for logging only
final String? logFilePath;
@ -302,32 +300,30 @@ class AlbumUpgraderV8 implements AlbumUpgrader {
}
@override
sql.Album? doDb(sql.Album dbObj) {
DbAlbum? doDb(DbAlbum dbObj) {
_log.fine("[doDb] Upgrade v8 Album for file: $logFilePath");
if (dbObj.coverProviderType == "manual") {
final content = (jsonDecode(dbObj.coverProviderContent) as Map)
.cast<String, dynamic>();
final content = dbObj.coverProviderContent;
final converted = _fileJsonToFileDescriptorJson(
(content["coverFile"] as Map).cast<String, dynamic>());
if (converted["fdId"] != null) {
return dbObj.copyWith(
coverProviderContent: jsonEncode({"coverFile": converted}),
coverProviderContent: {"coverFile": converted},
);
} else {
return dbObj.copyWith(coverProviderContent: "{}");
return dbObj.copyWith(coverProviderContent: const {});
}
} else if (dbObj.coverProviderType == "auto") {
final content = (jsonDecode(dbObj.coverProviderContent) as Map)
.cast<String, dynamic>();
final content = dbObj.coverProviderContent;
if (content["coverFile"] != null) {
final converted = _fileJsonToFileDescriptorJson(
(content["coverFile"] as Map).cast<String, dynamic>());
if (converted["fdId"] != null) {
return dbObj.copyWith(
coverProviderContent: jsonEncode({"coverFile": converted}),
coverProviderContent: {"coverFile": converted},
);
} else {
return dbObj.copyWith(coverProviderContent: "{}");
return dbObj.copyWith(coverProviderContent: const {});
}
}
}

View file

@ -17,12 +17,9 @@ class CollectionLocationGroupAdapter
CollectionAdapterUnshareableTag
implements CollectionAdapter {
CollectionLocationGroupAdapter(this._c, this.account, this.collection)
: assert(require(_c)),
_provider =
: _provider =
collection.contentProvider as CollectionLocationGroupProvider;
static bool require(DiContainer c) => ListLocationFile.require(c);
@override
Stream<List<CollectionItem>> listItem() async* {
final files = <File>[];

View file

@ -9,7 +9,6 @@ import 'package:nc_photos/entity/collection_item/basic_item.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/pref.dart';
import 'package:nc_photos/use_case/list_location_file.dart';
class CollectionMemoryAdapter
with
@ -18,10 +17,7 @@ class CollectionMemoryAdapter
CollectionAdapterUnshareableTag
implements CollectionAdapter {
CollectionMemoryAdapter(this._c, this.account, this.collection)
: assert(require(_c)),
_provider = collection.contentProvider as CollectionMemoryProvider;
static bool require(DiContainer c) => ListLocationFile.require(c);
: _provider = collection.contentProvider as CollectionMemoryProvider;
@override
Stream<List<CollectionItem>> listItem() async* {

View file

@ -36,8 +36,7 @@ class CollectionNcAlbumAdapter
: assert(require(_c)),
_provider = collection.contentProvider as CollectionNcAlbumProvider;
static bool require(DiContainer c) =>
ListNcAlbumItem.require(c) && FindFileDescriptor.require(c);
static bool require(DiContainer c) => ListNcAlbumItem.require(c);
@override
Stream<List<CollectionItem>> listItem() {

View file

@ -2,15 +2,15 @@ import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/entity_converter.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/entity/face_recognition_face.dart';
import 'package:nc_photos/entity/face_recognition_person.dart';
import 'package:nc_photos/entity/face_recognition_person/repo.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/np_api_util.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_db/np_db.dart';
part 'data_source.g.dart';
@ -66,18 +66,16 @@ class FaceRecognitionPersonRemoteDataSource
@npLog
class FaceRecognitionPersonSqliteDbDataSource
implements FaceRecognitionPersonDataSource {
const FaceRecognitionPersonSqliteDbDataSource(this.sqliteDb);
const FaceRecognitionPersonSqliteDbDataSource(this.db);
@override
Future<List<FaceRecognitionPerson>> getPersons(Account account) async {
_log.info("[getPersons] $account");
final dbPersons = await sqliteDb.use((db) async {
return await db.allFaceRecognitionPersons(account: sql.ByAccount.app(account));
});
return dbPersons
final results = await db.getFaceRecognitionPersons(account: account.toDb());
return results
.map((p) {
try {
return SqliteFaceRecognitionPersonConverter.fromSql(p);
return DbFaceRecognitionPersonConverter.fromDb(p);
} catch (e, stackTrace) {
_log.severe(
"[getPersons] Failed while converting DB entry", e, stackTrace);
@ -97,5 +95,5 @@ class FaceRecognitionPersonSqliteDbDataSource
.getFaces(account, person);
}
final sql.SqliteDb sqliteDb;
final NpDb db;
}

View file

@ -1,26 +1,26 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:drift/drift.dart' as sql;
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/entity_converter.dart';
import 'package:nc_photos/db/entity_converter.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_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/files_query_builder.dart' as sql;
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/np_api_util.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/use_case/compat/v32.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_common/object_util.dart';
import 'package:np_common/or_null.dart';
import 'package:np_datetime/np_datetime.dart';
import 'package:np_db/np_db.dart';
import 'package:path/path.dart' as path_lib;
part 'data_source.g.dart';
@ -363,23 +363,22 @@ class FileWebdavDataSource implements FileDataSource {
@npLog
class FileSqliteDbDataSource implements FileDataSource {
FileSqliteDbDataSource(this._c);
const FileSqliteDbDataSource(this._c);
@override
list(Account account, File dir) async {
Future<List<File>> list(Account account, File dir) async {
_log.info("[list] ${dir.path}");
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))
final List<DbFile> dbFiles;
try {
dbFiles = await _c.npDb.getFilesByDirKey(
account: account.toDb(),
dir: dir.toDbKey(),
);
} on DbNotFoundException catch (_) {
throw CacheNotFoundException("No entry: ${dir.path}");
}
final results = dbFiles
.map((f) => DbFileConverter.fromDb(account.userId.toString(), f))
.where((f) => _validateFile(f))
.toList();
_log.fine("[list] Queried ${results.length} files");
@ -405,33 +404,17 @@ class FileSqliteDbDataSource implements FileDataSource {
Future<List<File>> listByDate(
Account account, int fromEpochMs, int toEpochMs) async {
_log.info("[listByDate] [$fromEpochMs, $toEpochMs]");
final dbFiles = await _c.sqliteDb.use((db) async {
final query = db.queryFiles().run((q) {
q.setQueryMode(sql.FilesQueryMode.completeFile);
q.setAppAccount(account);
for (final r in account.roots) {
if (r.isNotEmpty) {
q.byOrRelativePathPattern("$r/%");
}
}
return q.build();
});
final dateTime = db.accountFiles.bestDateTime.unixepoch;
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.imageLocations),
r.readTableOrNull(db.trashes),
))
.get();
});
return await dbFiles.convertToAppFile(account);
final results = await _c.npDb.getFilesByTimeRange(
account: account.toDb(),
dirRoots: account.roots,
range: TimeRange(
from: DateTime.fromMillisecondsSinceEpoch(fromEpochMs),
to: DateTime.fromMillisecondsSinceEpoch(toEpochMs),
),
);
return results
.map((e) => DbFileConverter.fromDb(account.userId.toString(), e))
.toList();
}
@override
@ -453,7 +436,7 @@ class FileSqliteDbDataSource implements FileDataSource {
}
@override
updateProperty(
Future<void> updateProperty(
Account account,
File f, {
OrNull<Metadata>? metadata,
@ -463,79 +446,26 @@ class FileSqliteDbDataSource implements FileDataSource {
OrNull<ImageLocation>? location,
}) async {
_log.info("[updateProperty] ${f.path}");
await _c.sqliteDb.use((db) async {
final rowIds = await db.accountFileRowIdsOf(f, appAccount: account);
if (isArchived != null ||
overrideDateTime != null ||
favorite != null ||
metadata != 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),
bestDateTime: overrideDateTime == null && metadata == null
? const sql.Value.absent()
: sql.Value(file_util.getBestDateTime(
overrideDateTime: overrideDateTime == null
? f.overrideDateTime
: overrideDateTime.obj,
dateTimeOriginal: metadata == null
? f.metadata?.exif?.dateTimeOriginal
: metadata.obj?.exif?.dateTimeOriginal,
lastModified: f.lastModified,
)),
);
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),
));
}
}
if (location != null) {
if (location.obj == null) {
await (db.delete(db.imageLocations)
..where((t) => t.accountFile.equals(rowIds.accountFileRowId)))
.go();
} else {
await db
.into(db.imageLocations)
.insertOnConflictUpdate(sql.ImageLocationsCompanion.insert(
accountFile: sql.Value(rowIds.accountFileRowId),
version: location.obj!.version,
name: sql.Value(location.obj!.name),
latitude: sql.Value(location.obj!.latitude),
longitude: sql.Value(location.obj!.longitude),
countryCode: sql.Value(location.obj!.countryCode),
admin1: sql.Value(location.obj!.admin1),
admin2: sql.Value(location.obj!.admin2),
));
}
}
});
await _c.npDb.updateFileByFileId(
account: account.toDb(),
fileId: f.fileId!,
isFavorite: favorite?.let(OrNull.new),
isArchived: isArchived,
overrideDateTime: overrideDateTime,
bestDateTime: overrideDateTime == null && metadata == null
? null
: file_util.getBestDateTime(
overrideDateTime: overrideDateTime == null
? f.overrideDateTime
: overrideDateTime.obj,
dateTimeOriginal: metadata == null
? f.metadata?.exif?.dateTimeOriginal
: metadata.obj?.exif?.dateTimeOriginal,
lastModified: f.lastModified,
),
imageData: metadata?.let((e) => OrNull(e.obj?.toDb())),
location: location?.let((e) => OrNull(e.obj?.toDb())),
);
}
@override
@ -554,15 +484,13 @@ class FileSqliteDbDataSource implements FileDataSource {
File f,
String destination, {
bool? shouldOverwrite,
}) async {
}) {
_log.info("[move] ${f.path} to $destination");
await _c.sqliteDb.use((db) async {
await db.moveFileByFileId(
sql.ByAccount.app(account),
f.fileId!,
File(path: destination).strippedPathWithEmpty,
);
});
return _c.npDb.updateFileByFileId(
account: account.toDb(),
fileId: f.fileId!,
relativePath: File(path: destination).strippedPathWithEmpty,
);
}
@override
@ -603,7 +531,7 @@ class FileCachedDataSource implements FileDataSource {
}) : _sqliteDbSrc = FileSqliteDbDataSource(_c);
@override
list(Account account, File dir) async {
Future<List<File>> list(Account account, File dir) async {
final cacheLoader = FileCacheLoader(
_c,
cacheSrc: _sqliteDbSrc,

View file

@ -1,20 +1,13 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' as sql;
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.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/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/files_query_builder.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
part 'file_cache_manager.g.dart';
@ -88,9 +81,7 @@ class FileCacheLoader {
@npLog
class FileSqliteCacheUpdater {
FileSqliteCacheUpdater(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
const FileSqliteCacheUpdater(this._c);
Future<void> call(
Account account,
@ -99,335 +90,50 @@ class FileSqliteCacheUpdater {
}) async {
final s = Stopwatch()..start();
try {
await _cacheRemote(account, dir, remote);
await _c.npDb.syncDirFiles(
account: account.toDb(),
dirFileId: dir.fileId!,
files: remote.map((e) => e.toDb()).toList(),
);
} finally {
_log.info("[call] Elapsed time: ${s.elapsedMilliseconds}ms");
}
}
Future<void> updateSingle(Account account, File remoteFile) async {
final sqlFile = SqliteFileConverter.toSql(null, remoteFile);
await _c.sqliteDb.use((db) async {
final dbAccount = await db.accountOf(account);
final inserts =
await _updateCache(db, dbAccount, [sqlFile], [remoteFile], null);
if (inserts.isNotEmpty) {
await _insertCache(db, dbAccount, inserts, null);
}
});
}
Future<void> _cacheRemote(
Account account, File dir, List<File> remote) async {
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");
}
final dirFileQuery = db.select(db.dirFiles)
..where((t) => t.dir.equals(_dirRowId!))
..orderBy([(t) => sql.OrderingTerm.asc(t.child)]);
final dirFiles = await dirFileQuery.get();
final diff = getDiff(dirFiles.map((e) => e.child),
_childRowIds.sorted(Comparable.compare));
if (diff.onlyInB.isNotEmpty) {
await db.batch((batch) {
// insert new children
batch.insertAll(db.dirFiles,
diff.onlyInB.map((k) => sql.DirFile(dir: _dirRowId!, child: k)));
});
}
if (diff.onlyInA.isNotEmpty) {
// remove entries from the DirFiles table first
await diff.onlyInA.withPartitionNoReturn((sublist) async {
final deleteQuery = db.delete(db.dirFiles)
..where((t) => t.child.isIn(sublist))
..where((t) =>
t.dir.equals(_dirRowId!) | t.dir.equalsExp(db.dirFiles.child));
await deleteQuery.go();
}, sql.maxByFileIdsSize);
// select files having another dir parent under this account (i.e.,
// moved files)
final moved = await diff.onlyInA.withPartition((sublist) async {
final query = db.selectOnly(db.dirFiles).join([
sql.innerJoin(db.accountFiles,
db.accountFiles.file.equalsExp(db.dirFiles.dir)),
]);
query
..addColumns([db.dirFiles.child])
..where(db.accountFiles.account.equals(dbAccount.rowId))
..where(db.dirFiles.child.isIn(sublist));
return query.map((r) => r.read(db.dirFiles.child)!).get();
}, sql.maxByFileIdsSize);
final removed = diff.onlyInA.where((e) => !moved.contains(e)).toList();
if (removed.isNotEmpty) {
// delete obsolete children
await _removeSqliteFiles(db, dbAccount, removed);
await db.cleanUpDanglingFiles();
}
}
});
}
/// 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(
sql.ByAccount.sql(dbAccount), remoteFiles.map((f) => f.fileId!));
final rowIdsMap = Map.fromEntries(rowIds.map((e) => MapEntry(e.fileId, e)));
final inserts = <sql.CompleteFileCompanion>[];
// for updates, we use batch to speed up the process
await db.batch((batch) {
for (final f in sqlFiles) {
final isSupportedImageFormat =
file_util.isSupportedImageMime(f.file.contentType.value ?? "");
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),
);
} else {
if (isSupportedImageFormat) {
batch.deleteWhere(
db.images,
(sql.$ImagesTable t) =>
t.accountFile.equals(thisRowIds.accountFileRowId),
);
}
}
if (f.imageLocation != null) {
batch.update(
db.imageLocations,
f.imageLocation!,
where: (sql.$ImageLocationsTable t) =>
t.accountFile.equals(thisRowIds.accountFileRowId),
);
} else {
if (isSupportedImageFormat) {
batch.deleteWhere(
db.imageLocations,
(sql.$ImageLocationsTable 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),
);
} else {
batch.deleteWhere(
db.trashes,
(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 entries =
await sqlFiles.map((f) => f.file.fileId.value).withPartition((sublist) {
final query = db.queryFiles().run((q) {
q
..setQueryMode(
sql.FilesQueryMode.expression,
expressions: [db.files.rowId, db.files.fileId],
)
..setAccountless()
..byServerRowId(dbAccount.server)
..byFileIds(sublist);
return q.build();
});
return query
.map((r) =>
MapEntry(r.read(db.files.fileId)!, r.read(db.files.rowId)!))
.get();
}, sql.maxByFileIdsSize);
final fileRowIdMap = Map.fromEntries(entries);
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.imageLocation != null) {
await db.into(db.imageLocations).insert(f.imageLocation!
.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);
}));
}
void _onRowCached(int rowId, sql.CompleteFileCompanion dbFile, File? dir) {
if (dir != null) {
if (_compareIdentity(dbFile, dir)) {
_dirRowId = rowId;
}
}
_childRowIds.add(rowId);
}
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;
}
await _c.npDb.syncFile(
account: account.toDb(),
file: remoteFile.toDb(),
);
}
final DiContainer _c;
int? _dirRowId;
final _childRowIds = <int>[];
}
class FileSqliteCacheRemover {
FileSqliteCacheRemover(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
const FileSqliteCacheRemover(this._c);
/// Remove a file/dir from cache
Future<void> call(Account account, FileDescriptor f) async {
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 db.cleanUpDanglingFiles();
});
await _c.npDb.deleteFile(
account: account.toDb(),
file: f.toDbKey(),
);
}
final DiContainer _c;
}
class FileSqliteCacheEmptier {
FileSqliteCacheEmptier(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
const FileSqliteCacheEmptier(this._c);
/// Empty a dir from cache
Future<void> call(Account account, File dir) async {
await _c.sqliteDb.use((db) async {
final dbAccount = await db.accountOf(account);
final rowIds = await db.accountFileRowIdsOf(dir, sqlAccount: dbAccount);
// remove children
final childIdsQuery = db.selectOnly(db.dirFiles)
..addColumns([db.dirFiles.child])
..where(db.dirFiles.dir.equals(rowIds.fileRowId));
final childIds =
await childIdsQuery.map((r) => r.read(db.dirFiles.child)!).get();
childIds.removeWhere((id) => id == rowIds.fileRowId);
if (childIds.isNotEmpty) {
await _removeSqliteFiles(db, dbAccount, childIds);
await db.cleanUpDanglingFiles();
}
// remove dir in DirFiles
await (db.delete(db.dirFiles)
..where((t) => t.dir.equals(rowIds.fileRowId)))
.go();
});
await _c.npDb.truncateDir(
account: account.toDb(),
dir: dir.toDbKey(),
);
}
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 childRowIds = await fileRowIds.withPartition((sublist) {
final childQuery = db.selectOnly(db.dirFiles)
..addColumns([db.dirFiles.child])
..where(db.dirFiles.dir.isIn(sublist));
return childQuery.map((r) => r.read(db.dirFiles.child)!).get();
}, sql.maxByFileIdsSize);
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 fileRowIds.withPartitionNoReturn((sublist) async {
await (db.delete(db.accountFiles)
..where(
(t) => t.account.equals(dbAccount.rowId) & t.file.isIn(sublist)))
.go();
}, sql.maxByFileIdsSize);
if (childRowIds.isNotEmpty) {
// remove children recursively
return _removeSqliteFiles(db, dbAccount, childRowIds);
} else {
return;
}
}

View file

@ -1,18 +1,16 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/entity_converter.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/entity/nc_album.dart';
import 'package:nc_photos/entity/nc_album/repo.dart';
import 'package:nc_photos/entity/nc_album_item.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/np_api_util.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_db/np_db.dart';
part 'data_source.g.dart';
@ -152,18 +150,16 @@ class NcAlbumRemoteDataSource implements NcAlbumDataSource {
@npLog
class NcAlbumSqliteDbDataSource implements NcAlbumCacheDataSource {
const NcAlbumSqliteDbDataSource(this.sqliteDb);
const NcAlbumSqliteDbDataSource(this.npDb);
@override
Future<List<NcAlbum>> getAlbums(Account account) async {
_log.info("[getAlbums] account: ${account.userId}");
final dbAlbums = await sqliteDb.use((db) async {
return await db.ncAlbumsByAccount(account: sql.ByAccount.app(account));
});
return dbAlbums
.map((a) {
final results = await npDb.getNcAlbums(account: account.toDb());
return results
.map((e) {
try {
return SqliteNcAlbumConverter.fromSql(account.userId.toString(), a);
return DbNcAlbumConverter.fromDb(account.userId.toString(), e);
} catch (e, stackTrace) {
_log.severe(
"[getAlbums] Failed while converting DB entry", e, stackTrace);
@ -177,40 +173,28 @@ class NcAlbumSqliteDbDataSource implements NcAlbumCacheDataSource {
@override
Future<void> create(Account account, NcAlbum album) async {
_log.info("[create] account: ${account.userId}, album: ${album.path}");
await sqliteDb.use((db) async {
await db.insertNcAlbum(
account: sql.ByAccount.app(account),
object: SqliteNcAlbumConverter.toSql(null, album),
);
});
await npDb.addNcAlbum(account: account.toDb(), album: album.toDb());
}
@override
Future<void> remove(Account account, NcAlbum album) async {
_log.info("[remove] account: ${account.userId}, album: ${album.path}");
await sqliteDb.use((db) async {
await db.deleteNcAlbumByRelativePath(
account: sql.ByAccount.app(account),
relativePath: album.strippedPath,
);
});
await npDb.deleteNcAlbum(account: account.toDb(), album: album.toDb());
}
@override
Future<List<NcAlbumItem>> getItems(Account account, NcAlbum album) async {
_log.info(
"[getItems] account: ${account.userId}, album: ${album.strippedPath}");
final dbItems = await sqliteDb.use((db) async {
return await db.ncAlbumItemsByParentRelativePath(
account: sql.ByAccount.app(account),
parentRelativePath: album.strippedPath,
);
});
return dbItems
.map((i) {
final results = await npDb.getNcAlbumItemsByParent(
account: account.toDb(),
parent: album.toDb(),
);
return results
.map((e) {
try {
return SqliteNcAlbumItemConverter.fromSql(account.userId.toString(),
album.strippedPath, album.isOwned, i);
return DbNcAlbumItemConverter.fromDb(account.userId.toString(),
album.strippedPath, album.isOwned, e);
} catch (e, stackTrace) {
_log.severe(
"[getItems] Failed while converting DB entry", e, stackTrace);
@ -223,83 +207,25 @@ class NcAlbumSqliteDbDataSource implements NcAlbumCacheDataSource {
@override
Future<void> updateAlbumsCache(Account account, List<NcAlbum> remote) async {
await sqliteDb.use((db) async {
final dbAccount = await db.accountOf(account);
final existings = (await db.partialNcAlbumsByAccount(
account: sql.ByAccount.sql(dbAccount),
columns: [db.ncAlbums.rowId, db.ncAlbums.relativePath],
))
.whereNotNull()
.toList();
await db.batch((batch) async {
for (final r in remote) {
final dbObj = SqliteNcAlbumConverter.toSql(dbAccount, r);
final found = existings.indexWhere((e) => e[1] == r.strippedPath);
if (found != -1) {
// existing record, update it
batch.update(
db.ncAlbums,
dbObj,
where: (sql.$NcAlbumsTable t) =>
t.rowId.equals(existings[found][0]),
);
} else {
// insert
batch.insert(db.ncAlbums, dbObj);
}
}
for (final e in existings
.where((e) => !remote.any((r) => r.strippedPath == e[1]))) {
batch.deleteWhere(
db.ncAlbums,
(sql.$NcAlbumsTable t) => t.rowId.equals(e[0]),
);
}
});
});
_log.info(
"[updateAlbumsCache] account: ${account.userId}, remote: ${remote.map((e) => e.strippedPath)}");
await npDb.syncNcAlbums(
account: account.toDb(),
albums: remote.map(DbNcAlbumConverter.toDb).toList(),
);
}
@override
Future<void> updateItemsCache(
Account account, NcAlbum album, List<NcAlbumItem> remote) async {
await sqliteDb.use((db) async {
final dbAlbum = await db.ncAlbumByRelativePath(
account: sql.ByAccount.app(account),
relativePath: album.strippedPath,
);
final existingItems = await db.ncAlbumItemsByParent(
parent: dbAlbum!,
);
final diff = getDiffWith<NcAlbumItem>(
existingItems
.map((e) => SqliteNcAlbumItemConverter.fromSql(
account.userId.raw, album.strippedPath, album.isOwned, e))
.sorted(NcAlbumItemExtension.identityComparator),
remote.sorted(NcAlbumItemExtension.identityComparator),
NcAlbumItemExtension.identityComparator,
);
if (diff.onlyInA.isNotEmpty || diff.onlyInB.isNotEmpty) {
await db.batch((batch) async {
for (final item in diff.onlyInB) {
// new
batch.insert(
db.ncAlbumItems,
SqliteNcAlbumItemConverter.toSql(dbAlbum, item),
);
}
final rmIds = diff.onlyInA.map((e) => e.fileId).toList();
if (rmIds.isNotEmpty) {
// removed
batch.deleteWhere(
db.ncAlbumItems,
(sql.$NcAlbumItemsTable t) =>
t.parent.equals(dbAlbum.rowId) & t.fileId.isIn(rmIds),
);
}
});
}
});
_log.info(
"[updateItemsCache] account: ${account.userId}, album: ${album.name}, remote: ${remote.map((e) => e.strippedPath)}");
await npDb.syncNcAlbumItems(
account: account.toDb(),
album: album.toDb(),
items: remote.map(DbNcAlbumItemConverter.toDb).toList(),
);
}
final sql.SqliteDb sqliteDb;
final NpDb npDb;
}

View file

@ -64,9 +64,6 @@ extension NcAlbumItemExtension on NcAlbumItem {
int get identityHashCode => fileId.hashCode;
static int identityComparator(NcAlbumItem a, NcAlbumItem b) =>
a.fileId.compareTo(b.fileId);
File toFile() {
Metadata? metadata;
if (fileMetadataWidth != null && fileMetadataHeight != null) {

View file

@ -2,18 +2,17 @@ import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/entity_converter.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face/repo.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/table.dart';
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/np_api_util.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_common/type.dart';
import 'package:np_db/np_db.dart';
part 'data_source.g.dart';
@ -110,20 +109,16 @@ class RecognizeFaceRemoteDataSource implements RecognizeFaceDataSource {
@npLog
class RecognizeFaceSqliteDbDataSource implements RecognizeFaceDataSource {
const RecognizeFaceSqliteDbDataSource(this.sqliteDb);
const RecognizeFaceSqliteDbDataSource(this.db);
@override
Future<List<RecognizeFace>> getFaces(Account account) async {
_log.info("[getFaces] $account");
final dbFaces = await sqliteDb.use((db) async {
return await db.allRecognizeFaces(
account: sql.ByAccount.app(account),
);
});
return dbFaces
final results = await db.getRecognizeFaces(account: account.toDb());
return results
.map((f) {
try {
return SqliteRecognizeFaceConverter.fromSql(f);
return DbRecognizeFaceConverter.fromDb(f);
} catch (e, stackTrace) {
_log.severe(
"[getFaces] Failed while converting DB entry", e, stackTrace);
@ -138,8 +133,23 @@ class RecognizeFaceSqliteDbDataSource implements RecognizeFaceDataSource {
Future<List<RecognizeFaceItem>> getItems(
Account account, RecognizeFace face) async {
_log.info("[getItems] $face");
final results = await getMultiFaceItems(account, [face]);
return results[face]!;
final results = await db.getRecognizeFaceItemsByFaceLabel(
account: account.toDb(),
label: face.label,
);
return results
.map((r) {
try {
return DbRecognizeFaceItemConverter.fromDb(
account.userId.toString(), face.label, r);
} catch (e, stackTrace) {
_log.severe(
"[getItems] Failed while converting DB entry", e, stackTrace);
return null;
}
})
.whereNotNull()
.toList();
}
@override
@ -147,42 +157,25 @@ class RecognizeFaceSqliteDbDataSource implements RecognizeFaceDataSource {
Account account,
List<RecognizeFace> faces, {
ErrorWithValueHandler<RecognizeFace>? onError,
List<RecognizeFaceItemSort>? orderBy,
int? limit,
}) async {
_log.info("[getMultiFaceItems] ${faces.toReadableString()}");
final dbItems = await sqliteDb.use((db) async {
final results = await Future.wait(faces.map((f) async {
try {
return MapEntry(
f,
await db.recognizeFaceItemsByParentLabel(
account: sql.ByAccount.app(account),
label: f.label,
orderBy: orderBy?.toOrderingItem(db).toList(),
limit: limit,
),
);
} catch (e, stackTrace) {
onError?.call(f, e, stackTrace);
return null;
}
}));
return results.whereNotNull().toMap();
});
return dbItems.entries
.map((entry) {
final face = entry.key;
final results = await db.getRecognizeFaceItemsByFaceLabels(
account: account.toDb(),
labels: faces.map((e) => e.label).toList(),
);
return results.entries
.map((e) {
try {
return MapEntry(
face,
entry.value
.map((i) => SqliteRecognizeFaceItemConverter.fromSql(
account.userId.raw, face.label, i))
faces.firstWhere((f) => f.label == e.key),
e.value
.map((f) => DbRecognizeFaceItemConverter.fromDb(
account.userId.toString(), e.key, f))
.toList(),
);
} catch (e, stackTrace) {
onError?.call(face, e, stackTrace);
_log.severe("[getMultiFaceItems] Failed while converting DB entry",
e, stackTrace);
return null;
}
})
@ -196,16 +189,30 @@ class RecognizeFaceSqliteDbDataSource implements RecognizeFaceDataSource {
List<RecognizeFace> faces, {
ErrorWithValueHandler<RecognizeFace>? onError,
}) async {
final results = await getMultiFaceItems(
account,
faces,
onError: onError,
orderBy: [RecognizeFaceItemSort.fileIdDesc],
limit: 1,
_log.info("[getMultiFaceLastItems] ${faces.toReadableString()}");
final results = await db.getLatestRecognizeFaceItemsByFaceLabels(
account: account.toDb(),
labels: faces.map((e) => e.label).toList(),
);
return (results..removeWhere((key, value) => value.isEmpty))
.map((key, value) => MapEntry(key, value.first));
return results.entries
.map((e) {
try {
return MapEntry(
faces.firstWhere((f) => f.label == e.key),
DbRecognizeFaceItemConverter.fromDb(
account.userId.toString(), e.key, e.value),
);
} catch (e, stackTrace) {
_log.severe(
"[getMultiFaceLastItems] Failed while converting DB entry",
e,
stackTrace);
return null;
}
})
.whereNotNull()
.toMap();
}
final sql.SqliteDb sqliteDb;
final NpDb db;
}

View file

@ -1,7 +1,7 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/sqlite/files_query_builder.dart' as sql;
import 'package:np_collection/np_collection.dart';
import 'package:to_string/to_string.dart';
@ -29,7 +29,7 @@ class SearchCriteria {
}
abstract class SearchFilter {
void apply(sql.FilesQueryBuilder query);
Map<Symbol, Object> toQueryArgument();
bool isSatisfy(File file);
}
@ -38,25 +38,17 @@ enum SearchFileType {
video,
}
extension on SearchFileType {
String toSqlPattern() {
switch (this) {
case SearchFileType.image:
return "image/%";
case SearchFileType.video:
return "video/%";
}
}
}
@toString
class SearchFileTypeFilter implements SearchFilter {
const SearchFileTypeFilter(this.type);
@override
apply(sql.FilesQueryBuilder query) {
query.byMimePattern(type.toSqlPattern());
Map<Symbol, Object> toQueryArgument() {
if (type == SearchFileType.image) {
return {#mimes: file_util.supportedImageFormatMimes};
} else {
return {#mimes: file_util.supportedVideoFormatMimes};
}
}
@override
@ -81,8 +73,8 @@ class SearchFavoriteFilter implements SearchFilter {
const SearchFavoriteFilter(this.value);
@override
apply(sql.FilesQueryBuilder query) {
query.byFavorite(value);
Map<Symbol, Object> toQueryArgument() {
return {#isFavorite: value};
}
@override
@ -97,7 +89,7 @@ class SearchFavoriteFilter implements SearchFilter {
class SearchRepo {
const SearchRepo(this.dataSrc);
Future<List<File>> list(Account account, SearchCriteria criteria) =>
Future<List<FileDescriptor>> list(Account account, SearchCriteria criteria) =>
dataSrc.list(account, criteria);
final SearchDataSource dataSrc;
@ -105,5 +97,5 @@ class SearchRepo {
abstract class SearchDataSource {
/// List all results from a given search criteria
Future<List<File>> list(Account account, SearchCriteria criteria);
Future<List<FileDescriptor>> list(Account account, SearchCriteria criteria);
}

View file

@ -1,31 +1,30 @@
import 'package:drift/drift.dart' as sql;
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/person/builder.dart';
import 'package:nc_photos/entity/search.dart';
import 'package:nc_photos/entity/search_util.dart' as search_util;
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/files_query_builder.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/use_case/list_tagged_file.dart';
import 'package:nc_photos/use_case/person/list_person_face.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_db/np_db.dart';
import 'package:np_string/np_string.dart';
part 'data_source.g.dart';
@npLog
class SearchSqliteDbDataSource implements SearchDataSource {
SearchSqliteDbDataSource(this._c);
const SearchSqliteDbDataSource(this._c);
@override
list(Account account, SearchCriteria criteria) async {
Future<List<FileDescriptor>> list(
Account account, SearchCriteria criteria) async {
_log.info("[list] $criteria");
final stopwatch = Stopwatch()..start();
try {
@ -50,81 +49,57 @@ class SearchSqliteDbDataSource implements SearchDataSource {
}
}
Future<List<File>> _listByPath(
Future<List<FileDescriptor>> _listByPath(
Account account, SearchCriteria criteria, Set<String> keywords) async {
try {
final dbFiles = await _c.sqliteDb.use((db) async {
final query = db.queryFiles().run((q) {
q.setQueryMode(sql.FilesQueryMode.completeFile);
q.setAppAccount(account);
for (final r in account.roots) {
if (r.isNotEmpty) {
q.byOrRelativePathPattern("$r/%");
}
}
for (final f in criteria.filters) {
f.apply(q);
}
return q.build();
});
// limit to supported formats only
query.where(db.files.contentType.like("image/%") |
db.files.contentType.like("video/%"));
for (final k in keywords) {
query.where(db.accountFiles.relativePath.like("%$k%"));
}
return await query
.map((r) => sql.CompleteFile(
r.readTable(db.files),
r.readTable(db.accountFiles),
r.readTableOrNull(db.images),
r.readTableOrNull(db.imageLocations),
r.readTableOrNull(db.trashes),
))
.get();
});
return await dbFiles.convertToAppFile(account);
final args = {
#account: account.toDb(),
#includeRelativePaths: account.roots,
#excludeRelativePaths: [
remote_storage_util.remoteStorageDirRelativePath
],
#relativePathKeywords: keywords,
#mimes: file_util.supportedFormatMimes,
};
for (final f in criteria.filters) {
args.addAll(f.toQueryArgument());
}
final List<DbFileDescriptor> dbFiles =
await Function.apply(_c.npDb.getFileDescriptors, null, args);
return dbFiles
.map((e) => DbFileDescriptorConverter.fromDb(
account.userId.toCaseInsensitiveString(), e))
.toList();
} catch (e, stackTrace) {
_log.severe("[_listByPath] Failed while _listByPath", e, stackTrace);
return [];
}
}
Future<List<File>> _listByLocation(
Future<List<FileDescriptor>> _listByLocation(
Account account, SearchCriteria criteria) async {
// location search requires exact match, for example, searching "united"
// will NOT return results from US, UK, UAE, etc. Searching by the alpha2
// code is supported
try {
final dbFiles = await _c.sqliteDb.use((db) async {
final query = db.queryFiles().run((q) {
q.setQueryMode(sql.FilesQueryMode.completeFile);
q.setAppAccount(account);
for (final r in account.roots) {
if (r.isNotEmpty) {
q.byOrRelativePathPattern("$r/%");
}
}
for (final f in criteria.filters) {
f.apply(q);
}
q.byLocation(criteria.input);
return q.build();
});
// limit to supported formats only
query.where(db.files.contentType.like("image/%") |
db.files.contentType.like("video/%"));
return await query
.map((r) => sql.CompleteFile(
r.readTable(db.files),
r.readTable(db.accountFiles),
r.readTableOrNull(db.images),
r.readTableOrNull(db.imageLocations),
r.readTableOrNull(db.trashes),
))
.get();
});
return await dbFiles.convertToAppFile(account);
final args = {
#account: account.toDb(),
#includeRelativePaths: account.roots,
#excludeRelativePaths: [
remote_storage_util.remoteStorageDirRelativePath
],
#location: criteria.input,
#mimes: file_util.supportedFormatMimes,
};
for (final f in criteria.filters) {
args.addAll(f.toQueryArgument());
}
final List<DbFileDescriptor> dbFiles =
await Function.apply(_c.npDb.getFileDescriptors, null, args);
return dbFiles
.map((e) => DbFileDescriptorConverter.fromDb(
account.userId.toCaseInsensitiveString(), e))
.toList();
} catch (e, stackTrace) {
_log.severe(
"[_listByLocation] Failed while _listByLocation", e, stackTrace);
@ -132,21 +107,19 @@ class SearchSqliteDbDataSource implements SearchDataSource {
}
}
Future<List<File>> _listByTag(
Future<List<FileDescriptor>> _listByTag(
Account account, SearchCriteria criteria) async {
// tag search requires exact match, for example, searching "super" will NOT
// return results from "super tag"
try {
final dbTag = await _c.sqliteDb.use((db) async {
return await db.tagByDisplayName(
appAccount: account,
displayName: criteria.input,
);
});
final dbTag = await _c.npDb.getTagByDisplayName(
account: account.toDb(),
displayName: criteria.input,
);
if (dbTag == null) {
return [];
}
final tag = SqliteTagConverter.fromSql(dbTag);
final tag = DbTagConverter.fromDb(dbTag);
_log.info("[_listByTag] Found tag: ${tag.displayName}");
final files = await ListTaggedFile(_c)(account, [tag]);
return files
@ -158,21 +131,20 @@ class SearchSqliteDbDataSource implements SearchDataSource {
}
}
Future<List<File>> _listByPerson(
Future<List<FileDescriptor>> _listByPerson(
Account account, SearchCriteria criteria) async {
// person search requires exact match of any parts, for example, searching
// "Ada" will return results from "Ada Crook" but NOT "Adabelle"
try {
final dbPersons = await _c.sqliteDb.use((db) async {
return await db.faceRecognitionPersonsByName(
appAccount: account,
name: criteria.input,
);
});
final dbPersons = await _c.npDb.searchFaceRecognitionPersonsByName(
account: account.toDb(),
name: criteria.input,
);
if (dbPersons.isEmpty) {
return [];
}
final persons = (await dbPersons.convertToAppFaceRecognitionPerson())
final persons = dbPersons
.map(DbFaceRecognitionPersonConverter.fromDb)
.map((p) => PersonBuilder.byFaceRecognitionPerson(account, p))
.toList();
_log.info(

View file

@ -1,137 +0,0 @@
part of '../database.dart';
extension SqliteDbNcAlbumExtension on SqliteDb {
Future<List<NcAlbum>> ncAlbumsByAccount({
required ByAccount account,
}) {
assert((account.sqlAccount != null) != (account.appAccount != null));
if (account.sqlAccount != null) {
final query = select(ncAlbums)
..where((t) => t.account.equals(account.sqlAccount!.rowId));
return query.get();
} else {
final query = select(ncAlbums).join([
innerJoin(accounts, accounts.rowId.equalsExp(ncAlbums.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(account.appAccount!.userId.toCaseInsensitiveString()));
return query.map((r) => r.readTable(ncAlbums)).get();
}
}
Future<List<List>> partialNcAlbumsByAccount({
required ByAccount account,
required List<Expression> columns,
}) {
final query = selectOnly(ncAlbums)..addColumns(columns);
if (account.sqlAccount != null) {
query.where(ncAlbums.account.equals(account.sqlAccount!.rowId));
} else {
query.join([
innerJoin(accounts, accounts.rowId.equalsExp(ncAlbums.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(account.appAccount!.userId.toCaseInsensitiveString()));
}
return query.map((r) => columns.map((c) => r.read(c)).toList()).get();
}
Future<NcAlbum?> ncAlbumByRelativePath({
required ByAccount account,
required String relativePath,
}) {
if (account.sqlAccount != null) {
final query = select(ncAlbums)
..where((t) => t.account.equals(account.sqlAccount!.rowId))
..where((t) => t.relativePath.equals(relativePath));
return query.getSingleOrNull();
} else {
final query = select(ncAlbums).join([
innerJoin(accounts, accounts.rowId.equalsExp(ncAlbums.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(account.appAccount!.userId.toCaseInsensitiveString()))
..where(ncAlbums.relativePath.equals(relativePath));
return query.map((r) => r.readTable(ncAlbums)).getSingleOrNull();
}
}
Future<void> insertNcAlbum({
required ByAccount account,
required NcAlbumsCompanion object,
}) async {
final Account dbAccount;
if (account.sqlAccount != null) {
dbAccount = account.sqlAccount!;
} else {
dbAccount = await accountOf(account.appAccount!);
}
await into(ncAlbums).insert(object.copyWith(
account: Value(dbAccount.rowId),
));
}
/// Delete [NaAlbum] by relativePath
///
/// Return the number of deleted rows
Future<int> deleteNcAlbumByRelativePath({
required ByAccount account,
required String relativePath,
}) async {
final Account dbAccount;
if (account.sqlAccount != null) {
dbAccount = account.sqlAccount!;
} else {
dbAccount = await accountOf(account.appAccount!);
}
return await (delete(ncAlbums)
..where((t) => t.account.equals(dbAccount.rowId))
..where((t) => t.relativePath.equals(relativePath)))
.go();
}
Future<List<NcAlbumItem>> ncAlbumItemsByParent({
required NcAlbum parent,
}) {
final query = select(ncAlbumItems)
..where((t) => t.parent.equals(parent.rowId));
return query.get();
}
Future<List<NcAlbumItem>> ncAlbumItemsByParentRelativePath({
required ByAccount account,
required String parentRelativePath,
}) {
final query = select(ncAlbumItems).join([
innerJoin(ncAlbums, ncAlbums.rowId.equalsExp(ncAlbumItems.parent),
useColumns: false),
]);
if (account.sqlAccount != null) {
query.where(ncAlbums.account.equals(account.sqlAccount!.rowId));
} else {
query.join([
innerJoin(accounts, accounts.rowId.equalsExp(ncAlbums.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(account.appAccount!.userId.toCaseInsensitiveString()));
}
query.where(ncAlbums.relativePath.equals(parentRelativePath));
return query.map((r) => r.readTable(ncAlbumItems)).get();
}
}

View file

@ -1,762 +0,0 @@
part of 'database.dart';
const maxByFileIdsSize = 30000;
class CompleteFile {
const CompleteFile(
this.file, this.accountFile, this.image, this.imageLocation, this.trash);
final File file;
final AccountFile accountFile;
final Image? image;
final ImageLocation? imageLocation;
final Trash? trash;
}
class CompleteFileCompanion {
const CompleteFileCompanion(
this.file, this.accountFile, this.image, this.imageLocation, this.trash);
final FilesCompanion file;
final AccountFilesCompanion accountFile;
final ImagesCompanion? image;
final ImageLocationsCompanion? imageLocation;
final TrashesCompanion? trash;
}
extension CompleteFileListExtension on List<CompleteFile> {
Future<List<app.File>> convertToAppFile(app.Account account) {
return map((f) => {
"userId": account.userId.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 FileDescriptor {
const FileDescriptor({
required this.relativePath,
required this.fileId,
required this.contentType,
required this.isArchived,
required this.isFavorite,
required this.bestDateTime,
});
final String relativePath;
final int fileId;
final String? contentType;
final bool? isArchived;
final bool? isFavorite;
final DateTime bestDateTime;
}
extension FileDescriptorListExtension on List<FileDescriptor> {
List<app.FileDescriptor> convertToAppFileDescriptor(app.Account account) {
return map((f) =>
SqliteFileDescriptorConverter.fromSql(account.userId.toString(), f))
.toList();
}
}
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;
}
class ByAccount {
const ByAccount.sql(Account account) : this._(sqlAccount: account);
const ByAccount.app(app.Account account) : this._(appAccount: account);
const ByAccount._({
this.sqlAccount,
this.appAccount,
}) : assert((sqlAccount != null) != (appAccount != null));
final Account? sqlAccount;
final app.Account? appAccount;
}
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 PlatformLock.synchronized(k.appDbLockId, () async {
return await transaction(() async {
return await block(this);
});
});
}
/// Run [block] after acquiring the database
///
/// The [db] argument passed to [block] is identical to this
///
/// This function does not start a transaction, see [use] instead
Future<T> useNoTransaction<T>(Future<T> Function(SqliteDb db) block) async {
return await PlatformLock.synchronized(k.appDbLockId, () 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 PlatformLock.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 (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.userId.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.userId.toCaseInsensitiveString()))
..limit(1);
return query.map((r) => r.readTable(accounts)).getSingle();
}
/// Delete Account by app Account
///
/// If the deleted Account is the last one associated with a Server, then the
/// Server will also be deleted
Future<void> deleteAccountOf(app.Account account) async {
final dbAccount = await accountOf(account);
_log.info("[deleteAccountOf] Remove account: ${dbAccount.rowId}");
await (delete(accounts)..where((t) => t.rowId.equals(dbAccount.rowId)))
.go();
final accountCountExp =
accounts.rowId.count(filter: accounts.server.equals(dbAccount.server));
final accountCountQuery = selectOnly(accounts)
..addColumns([accountCountExp]);
final accountCount =
await accountCountQuery.map((r) => r.read(accountCountExp)).getSingle();
_log.info("[deleteAccountOf] Remaining accounts in server: $accountCount");
if (accountCount == 0) {
_log.info("[deleteAccountOf] Remove server: ${dbAccount.server}");
await (delete(servers)..where((t) => t.rowId.equals(dbAccount.server)))
.go();
}
await cleanUpDanglingFiles();
}
/// Delete Files without a corresponding entry in AccountFiles
Future<void> cleanUpDanglingFiles() async {
final query = selectOnly(files).join([
leftOuterJoin(accountFiles, accountFiles.file.equalsExp(files.rowId),
useColumns: false),
])
..addColumns([files.rowId])
..where(accountFiles.relativePath.isNull());
final fileRowIds = await query.map((r) => r.read(files.rowId)!).get();
if (fileRowIds.isNotEmpty) {
_log.info("[cleanUpDanglingFiles] Delete ${fileRowIds.length} files");
await fileRowIds.withPartitionNoReturn((sublist) async {
await (delete(files)..where((t) => t.rowId.isIn(sublist))).go();
}, maxByFileIdsSize);
}
}
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.FileDescriptor 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!);
}
try {
q.byFileId(file.fdId);
} catch (_) {
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.FileDescriptor 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(
ByAccount account, Iterable<int> fileIds) {
return fileIds.withPartition((sublist) {
final query = queryFiles().run((q) {
q.setQueryMode(FilesQueryMode.expression, expressions: [
accountFiles.rowId,
accountFiles.account,
accountFiles.file,
files.fileId,
]);
if (account.sqlAccount != null) {
q.setSqlAccount(account.sqlAccount!);
} else {
q.setAppAccount(account.appAccount!);
}
q.byFileIds(sublist);
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();
}, maxByFileIdsSize);
}
/// 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));
return fileIds.withPartition((sublist) {
final query = queryFiles().run((q) {
q.setQueryMode(FilesQueryMode.completeFile);
if (sqlAccount != null) {
q.setSqlAccount(sqlAccount);
} else {
q.setAppAccount(appAccount!);
}
q.byFileIds(sublist);
return q.build();
});
return query
.map((r) => CompleteFile(
r.readTable(files),
r.readTable(accountFiles),
r.readTableOrNull(images),
r.readTableOrNull(imageLocations),
r.readTableOrNull(trashes),
))
.get();
}, maxByFileIdsSize);
}
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(imageLocations),
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(imageLocations),
r.readTableOrNull(trashes),
))
.get();
}
/// Query [FileDescriptor]s by fileId
///
/// Returned files are NOT guaranteed to be sorted as [fileIds]
Future<List<FileDescriptor>> fileDescriptorsByFileIds(
ByAccount account, Iterable<int> fileIds) {
return fileIds.withPartition((sublist) {
final query = queryFiles().run((q) {
q.setQueryMode(
FilesQueryMode.expression,
expressions: [
accountFiles.relativePath,
files.fileId,
files.contentType,
accountFiles.isArchived,
accountFiles.isFavorite,
accountFiles.bestDateTime,
],
);
if (account.sqlAccount != null) {
q.setSqlAccount(account.sqlAccount!);
} else {
q.setAppAccount(account.appAccount!);
}
q.byFileIds(sublist);
return q.build();
});
return query
.map((r) => FileDescriptor(
relativePath: r.read(accountFiles.relativePath)!,
fileId: r.read(files.fileId)!,
contentType: r.read(files.contentType),
isArchived: r.read(accountFiles.isArchived),
isFavorite: r.read(accountFiles.isFavorite),
bestDateTime: r.read(accountFiles.bestDateTime)!,
))
.get();
}, maxByFileIdsSize);
}
Future<void> moveFileByFileId(
ByAccount account, int fileId, String destinationRelativePath) async {
final rowId = (await accountFileRowIdsByFileIds(account, [fileId])).first;
final q = update(accountFiles)
..where((t) => t.rowId.equals(rowId.accountFileRowId));
await q.write(AccountFilesCompanion(
relativePath: Value(destinationRelativePath),
));
}
Future<List<Tag>> allTags({
Account? sqlAccount,
app.Account? appAccount,
}) {
assert((sqlAccount != null) != (appAccount != null));
if (sqlAccount != null) {
final query = select(tags)
..where((t) => t.server.equals(sqlAccount.server));
return query.get();
} else {
final query = select(tags).join([
innerJoin(servers, servers.rowId.equalsExp(tags.server),
useColumns: false),
])
..where(servers.address.equals(appAccount!.url));
return query.map((r) => r.readTable(tags)).get();
}
}
Future<Tag?> tagByDisplayName({
Account? sqlAccount,
app.Account? appAccount,
required String displayName,
}) {
assert((sqlAccount != null) != (appAccount != null));
if (sqlAccount != null) {
final query = select(tags)
..where((t) => t.server.equals(sqlAccount.server))
..where((t) => t.displayName.like(displayName))
..limit(1);
return query.getSingleOrNull();
} else {
final query = select(tags).join([
innerJoin(servers, servers.rowId.equalsExp(tags.server),
useColumns: false),
])
..where(servers.address.equals(appAccount!.url))
..where(tags.displayName.like(displayName))
..limit(1);
return query.map((r) => r.readTable(tags)).getSingleOrNull();
}
}
Future<List<FaceRecognitionPerson>> allFaceRecognitionPersons({
required ByAccount account,
}) {
assert((account.sqlAccount != null) != (account.appAccount != null));
if (account.sqlAccount != null) {
final query = select(faceRecognitionPersons)
..where((t) => t.account.equals(account.sqlAccount!.rowId));
return query.get();
} else {
final query = select(faceRecognitionPersons).join([
innerJoin(
accounts, accounts.rowId.equalsExp(faceRecognitionPersons.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(account.appAccount!.userId.toCaseInsensitiveString()));
return query.map((r) => r.readTable(faceRecognitionPersons)).get();
}
}
Future<List<FaceRecognitionPerson>> faceRecognitionPersonsByName({
Account? sqlAccount,
app.Account? appAccount,
required String name,
}) {
assert((sqlAccount != null) != (appAccount != null));
if (sqlAccount != null) {
final query = select(faceRecognitionPersons)
..where((t) => t.account.equals(sqlAccount.rowId))
..where((t) =>
t.name.like(name) |
t.name.like("% $name") |
t.name.like("$name %"));
return query.get();
} else {
final query = select(faceRecognitionPersons).join([
innerJoin(
accounts, accounts.rowId.equalsExp(faceRecognitionPersons.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(appAccount!.url))
..where(
accounts.userId.equals(appAccount.userId.toCaseInsensitiveString()))
..where(faceRecognitionPersons.name.like(name) |
faceRecognitionPersons.name.like("% $name") |
faceRecognitionPersons.name.like("$name %"));
return query.map((r) => r.readTable(faceRecognitionPersons)).get();
}
}
Future<List<RecognizeFace>> allRecognizeFaces({
required ByAccount account,
}) {
assert((account.sqlAccount != null) != (account.appAccount != null));
if (account.sqlAccount != null) {
final query = select(recognizeFaces)
..where((t) => t.account.equals(account.sqlAccount!.rowId));
return query.get();
} else {
final query = select(recognizeFaces).join([
innerJoin(accounts, accounts.rowId.equalsExp(recognizeFaces.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(account.appAccount!.userId.toCaseInsensitiveString()));
return query.map((r) => r.readTable(recognizeFaces)).get();
}
}
Future<RecognizeFace> recognizeFaceByLabel({
required ByAccount account,
required String label,
}) {
assert((account.sqlAccount != null) != (account.appAccount != null));
if (account.sqlAccount != null) {
final query = select(recognizeFaces)
..where((t) => t.account.equals(account.sqlAccount!.rowId))
..where((t) => t.label.equals(label));
return query.getSingle();
} else {
final query = select(recognizeFaces).join([
innerJoin(accounts, accounts.rowId.equalsExp(recognizeFaces.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(account.appAccount!.userId.toCaseInsensitiveString()))
..where(recognizeFaces.label.equals(label));
return query.map((r) => r.readTable(recognizeFaces)).getSingle();
}
}
Future<List<RecognizeFaceItem>> recognizeFaceItemsByParentLabel({
required ByAccount account,
required String label,
List<OrderingTerm>? orderBy,
int? limit,
int? offset,
}) {
assert((account.sqlAccount != null) != (account.appAccount != null));
final query = select(recognizeFaceItems).join([
innerJoin(recognizeFaces,
recognizeFaces.rowId.equalsExp(recognizeFaceItems.parent),
useColumns: false),
]);
if (account.sqlAccount != null) {
query
..where(recognizeFaces.account.equals(account.sqlAccount!.rowId))
..where(recognizeFaces.label.equals(label));
} else {
query
..join([
innerJoin(accounts, accounts.rowId.equalsExp(recognizeFaces.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(account.appAccount!.userId.toCaseInsensitiveString()))
..where(recognizeFaces.label.equals(label));
}
if (orderBy != null) {
query.orderBy(orderBy);
if (limit != null) {
query.limit(limit, offset: offset);
}
}
return query.map((r) => r.readTable(recognizeFaceItems)).get();
}
Future<int> countMissingMetadataByFileIds({
Account? sqlAccount,
app.Account? appAccount,
required List<int> fileIds,
}) async {
assert((sqlAccount != null) != (appAccount != null));
if (fileIds.isEmpty) {
return 0;
}
final counts = await fileIds.withPartition((sublist) async {
final count = countAll(
filter:
images.lastUpdated.isNull() | imageLocations.version.isNull());
final query = selectOnly(files).join([
innerJoin(accountFiles, accountFiles.file.equalsExp(files.rowId),
useColumns: false),
if (appAccount != null) ...[
innerJoin(accounts, accounts.rowId.equalsExp(accountFiles.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
],
leftOuterJoin(images, images.accountFile.equalsExp(accountFiles.rowId),
useColumns: false),
leftOuterJoin(imageLocations,
imageLocations.accountFile.equalsExp(accountFiles.rowId),
useColumns: false),
]);
query.addColumns([count]);
if (sqlAccount != null) {
query.where(accountFiles.account.equals(sqlAccount.rowId));
} else if (appAccount != null) {
query
..where(servers.address.equals(appAccount.url))
..where(accounts.userId
.equals(appAccount.userId.toCaseInsensitiveString()));
}
query
..where(files.fileId.isIn(sublist))
..where(whereFileIsSupportedImageMime());
return [await query.map((r) => r.read(count)!).getSingle()];
}, maxByFileIdsSize);
return counts.reduce((value, element) => value + element);
}
Future<void> truncate() async {
await delete(servers).go();
// technically deleting Servers table is enough to clear the followings, but
// just in case
await delete(accounts).go();
await delete(files).go();
await delete(images).go();
await delete(imageLocations).go();
await delete(trashes).go();
await delete(accountFiles).go();
await delete(dirFiles).go();
await delete(albums).go();
await delete(albumShares).go();
await delete(tags).go();
await delete(faceRecognitionPersons).go();
await delete(ncAlbums).go();
await delete(ncAlbumItems).go();
await delete(recognizeFaces).go();
await delete(recognizeFaceItems).go();
// reset the auto increment counter
await customStatement("UPDATE sqlite_sequence SET seq=0;");
}
Expression<bool> whereFileIsSupportedMime() {
return file_util.supportedFormatMimes
.map<Expression<bool>>((m) => files.contentType.equals(m))
.reduce((value, element) => value | element);
}
Expression<bool> whereFileIsSupportedImageMime() {
return file_util.supportedImageFormatMimes
.map<Expression<bool>>((m) => files.contentType.equals(m))
.reduce((value, element) => value | element);
}
}
app.File _covertSqliteDbFile(Map map) {
final userId = map["userId"] as String;
final file = map["completeFile"] as CompleteFile;
return SqliteFileConverter.fromSql(userId, file);
}
CompleteFileCompanion _convertAppFile(Map map) {
final account = map["account"] as Account?;
final file = map["file"] as app.File;
return SqliteFileConverter.toSql(account, file);
}

View file

@ -1,422 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.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/face_recognition_person.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/nc_album.dart';
import 'package:nc_photos/entity/nc_album_item.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/tag.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_async/np_async.dart';
import 'package:np_common/or_null.dart';
import 'package:np_common/type.dart';
import 'package:np_string/np_string.dart';
extension SqlTagListExtension on List<sql.Tag> {
Future<List<Tag>> convertToAppTag() {
return computeAll(SqliteTagConverter.fromSql);
}
}
extension AppTagListExtension on List<Tag> {
Future<List<sql.TagsCompanion>> convertToTagCompanion(
sql.Account? dbAccount) {
return map((t) => {
"account": dbAccount,
"tag": t,
}).computeAll(_convertAppTag);
}
}
extension SqlFaceRecognitionPersonListExtension
on List<sql.FaceRecognitionPerson> {
Future<List<FaceRecognitionPerson>> convertToAppFaceRecognitionPerson() {
return computeAll(SqliteFaceRecognitionPersonConverter.fromSql);
}
}
extension AppFaceRecognitionPersonListExtension on List<FaceRecognitionPerson> {
Future<List<sql.FaceRecognitionPersonsCompanion>>
convertToFaceRecognitionPersonCompanion(sql.Account? dbAccount) {
return map((p) => {
"account": dbAccount,
"person": p,
}).computeAll(_convertAppFaceRecognitionPerson);
}
}
extension SqlRecognizeFaceListExtension on List<sql.RecognizeFace> {
Future<List<RecognizeFace>> convertToAppRecognizeFace() {
return computeAll(SqliteRecognizeFaceConverter.fromSql);
}
}
extension AppRecognizeFaceListExtension on List<RecognizeFace> {
Future<List<sql.RecognizeFacesCompanion>> convertToRecognizeFaceCompanion(
sql.Account? dbAccount) {
return map((f) => {
"account": dbAccount,
"face": f,
}).computeAll(_convertAppRecognizeFace);
}
}
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(),
// replace with the original etag when this album was cached
albumFile: albumFile.copyWith(etag: OrNull(album.fileEtag)),
savedVersion: album.version,
);
}
static sql.CompleteAlbumCompanion toSql(
Album album, int albumFileRowId, String albumFileEtag) {
final providerJson = album.provider.toJson();
final coverProviderJson = album.coverProvider.toJson();
final sortProviderJson = album.sortProvider.toJson();
final dbAlbum = sql.AlbumsCompanion.insert(
file: albumFileRowId,
fileEtag: Value(albumFileEtag),
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 SqliteFileDescriptorConverter {
static FileDescriptor fromSql(String userId, sql.FileDescriptor f) {
return FileDescriptor(
fdPath: "remote.php/dav/files/$userId/${f.relativePath}",
fdId: f.fileId,
fdMime: f.contentType,
fdIsArchived: f.isArchived ?? false,
fdIsFavorite: f.isFavorite ?? false,
fdDateTime: f.bestDateTime,
);
}
}
class SqliteFileConverter {
static File fromSql(String userId, 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))),
));
final location = f.imageLocation?.run((obj) => ImageLocation(
version: obj.version,
name: obj.name,
latitude: obj.latitude,
longitude: obj.longitude,
countryCode: obj.countryCode,
admin1: obj.admin1,
admin2: obj.admin2,
));
return File(
path: "remote.php/dav/files/$userId/${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(),
ownerDisplayName: f.file.ownerDisplayName,
trashbinFilename: f.trash?.filename,
trashbinOriginalLocation: f.trash?.originalLocation,
trashbinDeletionTime: f.trash?.deletionTime,
metadata: metadata,
isArchived: f.accountFile.isArchived,
overrideDateTime: f.accountFile.overrideDateTime,
location: location,
);
}
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()),
ownerDisplayName: Value(file.ownerDisplayName),
);
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),
bestDateTime: Value(file.bestDateTime),
);
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 dbImageLocation =
file.location?.run((l) => sql.ImageLocationsCompanion.insert(
version: l.version,
name: Value(l.name),
latitude: Value(l.latitude),
longitude: Value(l.longitude),
countryCode: Value(l.countryCode),
admin1: Value(l.admin1),
admin2: Value(l.admin2),
));
final dbTrash = file.trashbinDeletionTime == null
? null
: sql.TrashesCompanion.insert(
filename: file.trashbinFilename!,
originalLocation: file.trashbinOriginalLocation!,
deletionTime: file.trashbinDeletionTime!,
);
return sql.CompleteFileCompanion(
dbFile, dbAccountFile, dbImage, dbImageLocation, dbTrash);
}
}
class SqliteTagConverter {
static Tag fromSql(sql.Tag tag) => Tag(
id: tag.tagId,
displayName: tag.displayName,
userVisible: tag.userVisible,
userAssignable: tag.userAssignable,
);
static sql.TagsCompanion toSql(sql.Account? dbAccount, Tag tag) =>
sql.TagsCompanion(
server:
dbAccount == null ? const Value.absent() : Value(dbAccount.server),
tagId: Value(tag.id),
displayName: Value(tag.displayName),
userVisible: Value(tag.userVisible),
userAssignable: Value(tag.userAssignable),
);
}
class SqliteFaceRecognitionPersonConverter {
static FaceRecognitionPerson fromSql(sql.FaceRecognitionPerson person) =>
FaceRecognitionPerson(
name: person.name,
thumbFaceId: person.thumbFaceId,
count: person.count,
);
static sql.FaceRecognitionPersonsCompanion toSql(
sql.Account? dbAccount, FaceRecognitionPerson person) =>
sql.FaceRecognitionPersonsCompanion(
account:
dbAccount == null ? const Value.absent() : Value(dbAccount.rowId),
name: Value(person.name),
thumbFaceId: Value(person.thumbFaceId),
count: Value(person.count),
);
}
class SqliteNcAlbumConverter {
static NcAlbum fromSql(String userId, sql.NcAlbum ncAlbum) {
final json = ncAlbum.collaborators
.run((obj) => (jsonDecode(obj) as List).cast<Map>());
return NcAlbum(
path:
"${api.ApiPhotos.path}/$userId/${ncAlbum.isOwned ? "albums" : "sharedalbums"}/${ncAlbum.relativePath}",
lastPhoto: ncAlbum.lastPhoto,
nbItems: ncAlbum.nbItems,
location: ncAlbum.location,
dateStart: ncAlbum.dateStart,
dateEnd: ncAlbum.dateEnd,
collaborators: json
.map((e) => NcAlbumCollaborator.fromJson(e.cast<String, dynamic>()))
.toList(),
);
}
static sql.NcAlbumsCompanion toSql(sql.Account? dbAccount, NcAlbum ncAlbum) =>
sql.NcAlbumsCompanion(
account:
dbAccount == null ? const Value.absent() : Value(dbAccount.rowId),
relativePath: Value(ncAlbum.strippedPath),
lastPhoto: Value(ncAlbum.lastPhoto),
nbItems: Value(ncAlbum.nbItems),
location: Value(ncAlbum.location),
dateStart: Value(ncAlbum.dateStart),
dateEnd: Value(ncAlbum.dateEnd),
collaborators: Value(
jsonEncode(ncAlbum.collaborators.map((c) => c.toJson()).toList())),
isOwned: Value(ncAlbum.isOwned),
);
}
class SqliteNcAlbumItemConverter {
static NcAlbumItem fromSql(String userId, String albumRelativePath,
bool isAlbumOwned, sql.NcAlbumItem item) =>
NcAlbumItem(
path:
"${api.ApiPhotos.path}/$userId/${isAlbumOwned ? "albums" : "sharedalbums"}/$albumRelativePath/${item.relativePath}",
fileId: item.fileId,
contentLength: item.contentLength,
contentType: item.contentType,
etag: item.etag,
lastModified: item.lastModified,
hasPreview: item.hasPreview,
isFavorite: item.isFavorite,
fileMetadataWidth: item.fileMetadataWidth,
fileMetadataHeight: item.fileMetadataHeight,
);
static sql.NcAlbumItemsCompanion toSql(
sql.NcAlbum parent,
NcAlbumItem item,
) =>
sql.NcAlbumItemsCompanion(
parent: Value(parent.rowId),
relativePath: Value(item.strippedPath),
fileId: Value(item.fileId),
contentLength: Value(item.contentLength),
contentType: Value(item.contentType),
etag: Value(item.etag),
lastModified: Value(item.lastModified),
hasPreview: Value(item.hasPreview),
isFavorite: Value(item.isFavorite),
fileMetadataWidth: Value(item.fileMetadataWidth),
fileMetadataHeight: Value(item.fileMetadataHeight),
);
}
class SqliteRecognizeFaceConverter {
static RecognizeFace fromSql(sql.RecognizeFace face) => RecognizeFace(
label: face.label,
);
static sql.RecognizeFacesCompanion toSql(
sql.Account? dbAccount, RecognizeFace face) =>
sql.RecognizeFacesCompanion(
account:
dbAccount == null ? const Value.absent() : Value(dbAccount.rowId),
label: Value(face.label),
);
}
class SqliteRecognizeFaceItemConverter {
static RecognizeFaceItem fromSql(
String userId, String faceLabel, sql.RecognizeFaceItem item) =>
RecognizeFaceItem(
path:
"${api.ApiRecognize.path}/$userId/faces/$faceLabel/${item.relativePath}",
fileId: item.fileId,
contentLength: item.contentLength,
contentType: item.contentType,
etag: item.etag,
lastModified: item.lastModified,
hasPreview: item.hasPreview,
realPath: item.realPath,
isFavorite: item.isFavorite,
fileMetadataWidth: item.fileMetadataWidth,
fileMetadataHeight: item.fileMetadataHeight,
faceDetections: item.faceDetections
?.run((obj) => (jsonDecode(obj) as List).cast<JsonObj>()),
);
static sql.RecognizeFaceItemsCompanion toSql(
sql.RecognizeFace parent,
RecognizeFaceItem item,
) =>
sql.RecognizeFaceItemsCompanion(
parent: Value(parent.rowId),
relativePath: Value(item.strippedPath),
fileId: Value(item.fileId),
contentLength: Value(item.contentLength),
contentType: Value(item.contentType),
etag: Value(item.etag),
lastModified: Value(item.lastModified),
hasPreview: Value(item.hasPreview),
realPath: Value(item.realPath),
isFavorite: Value(item.isFavorite),
fileMetadataWidth: Value(item.fileMetadataWidth),
fileMetadataHeight: Value(item.fileMetadataHeight),
faceDetections:
Value(item.faceDetections?.run((obj) => jsonEncode(obj))),
);
}
sql.TagsCompanion _convertAppTag(Map map) {
final account = map["account"] as sql.Account?;
final tag = map["tag"] as Tag;
return SqliteTagConverter.toSql(account, tag);
}
sql.FaceRecognitionPersonsCompanion _convertAppFaceRecognitionPerson(Map map) {
final account = map["account"] as sql.Account?;
final person = map["person"] as FaceRecognitionPerson;
return SqliteFaceRecognitionPersonConverter.toSql(account, person);
}
sql.RecognizeFacesCompanion _convertAppRecognizeFace(Map map) {
final account = map["account"] as sql.Account?;
final face = map["face"] as RecognizeFace;
return SqliteRecognizeFaceConverter.toSql(account, face);
}

View file

@ -1,14 +1,14 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/entity_converter.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/entity/tag.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/np_api_util.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_db/np_db.dart';
part 'data_source.g.dart';
@ -64,22 +64,20 @@ class TagRemoteDataSource implements TagDataSource {
@npLog
class TagSqliteDbDataSource implements TagDataSource {
const TagSqliteDbDataSource(this.sqliteDb);
const TagSqliteDbDataSource(this.db);
@override
list(Account account) async {
Future<List<Tag>> list(Account account) async {
_log.info("[list] $account");
final dbTags = await sqliteDb.use((db) async {
return await db.allTags(appAccount: account);
});
return dbTags.convertToAppTag();
final results = await db.getTags(account: account.toDb());
return results.map(DbTagConverter.fromDb).toList();
}
@override
listByFile(Account account, File file) async {
Future<List<Tag>> listByFile(Account account, File file) async {
_log.info("[listByFile] ${file.path}");
throw UnimplementedError();
}
final sql.SqliteDb sqliteDb;
final NpDb db;
}

View file

@ -1,14 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:kiwi/kiwi.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/pref_util.dart' as pref_util;
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/help_utils.dart' as help_utils;
import 'package:nc_photos/legacy/connect.dart';
import 'package:nc_photos/theme.dart';
@ -17,6 +16,7 @@ import 'package:nc_photos/widget/home.dart';
import 'package:nc_photos/widget/root_picker.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_db/np_db.dart';
import 'package:np_platform_util/np_platform_util.dart';
import 'package:np_string/np_string.dart';
@ -306,10 +306,7 @@ class _SignInState extends State<SignIn> {
}
Future<void> _persistAccount(Account account) async {
final c = KiwiContainer().resolve<DiContainer>();
await c.sqliteDb.use((db) async {
await db.insertAccountOf(account);
});
await context.read<NpDb>().addAccounts([account.toDb()]);
// only signing in with app password would trigger distinct
final accounts = (Pref().getAccounts3Or([])..add(account)).distinct();
try {

View file

@ -1,4 +1,3 @@
export 'db_util.dart';
export 'download.dart';
export 'file_saver.dart';
export 'notification.dart';

View file

@ -98,7 +98,7 @@ class _Service {
}
await onCancelSubscription.cancel();
await onDataSubscription.cancel();
await KiwiContainer().resolve<DiContainer>().sqliteDb.close();
await KiwiContainer().resolve<DiContainer>().npDb.dispose();
service.stopBackgroundService();
_log.info("[call] Service stopped");
}

View file

@ -1,113 +1,33 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' as sql;
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/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/files_query_builder.dart' as sql;
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
part 'cache_favorite.g.dart';
@npLog
class CacheFavorite {
CacheFavorite(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
const CacheFavorite(this._c);
/// Cache favorites using results from remote
///
/// Return number of files updated
Future<int> call(Account account, Iterable<int> remoteFileIds) async {
_log.info("[call] Cache favorites");
final remote = remoteFileIds.sorted(Comparable.compare);
final updateCount = await _c.sqliteDb.use((db) async {
final dbAccount = await db.accountOf(account);
final cache = await _getCacheFavorites(db, dbAccount);
final cacheMap =
Map.fromEntries(cache.map((e) => MapEntry(e.fileId, e.rowId)));
final diff = getDiff(cacheMap.keys.sorted(Comparable.compare), remote);
final newFileIds = diff.onlyInB;
_log.info("[call] New favorites: ${newFileIds.toReadableString()}");
final removedFildIds = diff.onlyInA;
_log.info(
"[call] Removed favorites: ${removedFildIds.toReadableString()}");
var updateCount = 0;
if (newFileIds.isNotEmpty) {
final rowIds = await db.accountFileRowIdsByFileIds(
sql.ByAccount.sql(dbAccount), newFileIds);
final counts =
await rowIds.map((id) => id.accountFileRowId).withPartition(
(sublist) async {
return [
await (db.update(db.accountFiles)
..where((t) => t.rowId.isIn(sublist)))
.write(const sql.AccountFilesCompanion(
isFavorite: sql.Value(true))),
];
},
sql.maxByFileIdsSize,
);
final count = counts.sum;
_log.info("[call] Updated $count row (new)");
updateCount += count;
}
if (removedFildIds.isNotEmpty) {
final counts =
await removedFildIds.map((id) => cacheMap[id]!).withPartition(
(sublist) async {
return [
await (db.update(db.accountFiles)
..where((t) =>
t.account.equals(dbAccount.rowId) &
t.file.isIn(sublist)))
.write(const sql.AccountFilesCompanion(
isFavorite: sql.Value(false)))
];
},
sql.maxByFileIdsSize,
);
final count = counts.sum;
_log.info("[call] Updated $count row (remove)");
updateCount += count;
}
return updateCount;
});
if (updateCount > 0) {
final result = await _c.npDb.syncFavoriteFiles(
account: account.toDb(),
favoriteFileIds: remoteFileIds.toList(),
);
final count = result.insert + result.delete + result.update;
if (count > 0) {
KiwiContainer().resolve<EventBus>().fire(FavoriteResyncedEvent(account));
}
return updateCount;
}
Future<List<_FileRowIdWithFileId>> _getCacheFavorites(
sql.SqliteDb db, sql.Account dbAccount) async {
final query = db.queryFiles().run((q) {
q
..setQueryMode(sql.FilesQueryMode.expression,
expressions: [db.files.rowId, db.files.fileId])
..setSqlAccount(dbAccount)
..byFavorite(true);
return q.build();
});
return await query
.map((r) => _FileRowIdWithFileId(
r.read(db.files.rowId)!, r.read(db.files.fileId)!))
.get();
return count;
}
final DiContainer _c;
}
class _FileRowIdWithFileId {
const _FileRowIdWithFileId(this.rowId, this.fileId);
final int rowId;
final int fileId;
}

View file

@ -1,21 +1,17 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_db/np_db.dart';
part 'v46.g.dart';
@npLog
class CompatV46 {
static Future<void> insertDbAccounts(Pref pref, sql.SqliteDb sqliteDb) async {
static Future<void> insertDbAccounts(Pref pref, NpDb db) async {
_log.info("[insertDbAccounts] Insert current accounts to Sqlite database");
await sqliteDb.use((db) async {
final accounts = pref.getAccounts3Or([]);
for (final a in accounts) {
_log.info("[insertDbAccounts] Insert account to Sqlite db: $a");
await db.insertAccountOf(a);
}
});
final accounts = pref.getAccounts3Or([]);
await db.addAccounts(accounts.toDb());
}
static final _log = _$CompatV46NpLog.log;

View file

@ -1,100 +1,10 @@
import 'package:drift/drift.dart' as sql;
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:tuple/tuple.dart';
import 'package:np_db/np_db.dart';
part 'v55.g.dart';
@npLog
class CompatV55 {
static Future<void> migrateDb(
sql.SqliteDb db, {
NpDb db, {
void Function(int current, int count)? onProgress,
}) {
return db.use((db) async {
final countExp = db.accountFiles.rowId.count();
final countQ = db.selectOnly(db.accountFiles)..addColumns([countExp]);
final count = await countQ.map((r) => r.read(countExp)!).getSingle();
onProgress?.call(0, count);
final dateTimeUpdates = <Tuple2<int, DateTime>>[];
final imageRemoves = <int>[];
for (var i = 0; i < count; i += 1000) {
final q = db.select(db.files).join([
sql.innerJoin(
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),
sql.innerJoin(db.images,
db.images.accountFile.equalsExp(db.accountFiles.rowId)),
]);
q
..orderBy([
sql.OrderingTerm(
expression: db.accountFiles.rowId,
mode: sql.OrderingMode.asc,
),
])
..limit(1000, offset: i);
final dbFiles = await q
.map((r) => sql.CompleteFile(
r.readTable(db.files),
r.readTable(db.accountFiles),
r.readTable(db.images),
null,
null,
))
.get();
for (final f in dbFiles) {
final bestDateTime = file_util.getBestDateTime(
overrideDateTime: f.accountFile.overrideDateTime,
dateTimeOriginal: f.image?.dateTimeOriginal,
lastModified: f.file.lastModified,
);
if (f.accountFile.bestDateTime != bestDateTime) {
// need update
dateTimeUpdates.add(Tuple2(f.accountFile.rowId, bestDateTime));
}
if (f.file.contentType == "image/heic" &&
f.image != null &&
f.image!.exifRaw == null) {
imageRemoves.add(f.accountFile.rowId);
}
}
onProgress?.call(i, count);
}
_log.info(
"[migrateDb] ${dateTimeUpdates.length} rows require updating, ${imageRemoves.length} rows require removing");
if (kDebugMode) {
_log.fine(
"[migrateDb] dateTimeUpdates: ${dateTimeUpdates.map((e) => e.item1).toReadableString()}");
_log.fine(
"[migrateDb] imageRemoves: ${imageRemoves.map((e) => e).toReadableString()}");
}
await db.batch((batch) {
for (final pair in dateTimeUpdates) {
batch.update(
db.accountFiles,
sql.AccountFilesCompanion(
bestDateTime: sql.Value(pair.item2),
),
where: (sql.$AccountFilesTable table) =>
table.rowId.equals(pair.item1),
);
}
for (final r in imageRemoves) {
batch.deleteWhere(
db.images,
(sql.$ImagesTable table) => table.accountFile.equals(r),
);
}
});
});
return db.migrateV55(onProgress);
}
static final _log = _$CompatV55NpLog.log;
}

View file

@ -1,15 +1,11 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' as sql;
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/face_recognition_person.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/use_case/face_recognition_person/list_face_recognition_person.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
part 'sync_face_recognition_person.g.dart';
@ -22,13 +18,10 @@ class SyncFaceRecognitionPerson {
/// Return if any people were updated
Future<bool> call(Account account) async {
_log.info("[call] Sync people with remote");
int personSorter(FaceRecognitionPerson a, FaceRecognitionPerson b) =>
a.name.compareTo(b.name);
late final List<FaceRecognitionPerson> remote;
final List<FaceRecognitionPerson> remote;
try {
remote = (await ListFaceRecognitionPerson(_c.withRemoteRepo())(account)
.last)
..sort(personSorter);
remote =
await ListFaceRecognitionPerson(_c.withRemoteRepo())(account).last;
} catch (e) {
if (e is ApiException && e.response.statusCode == 404) {
// face recognition app probably not installed, ignore
@ -37,56 +30,11 @@ class SyncFaceRecognitionPerson {
}
rethrow;
}
final cache = (await ListFaceRecognitionPerson(_c.withLocalRepo())(account)
.last)
..sort(personSorter);
final diff = getDiffWith(cache, remote, personSorter);
final inserts = diff.onlyInB;
_log.info("[call] New people: ${inserts.toReadableString()}");
final deletes = diff.onlyInA;
_log.info("[call] Removed people: ${deletes.toReadableString()}");
final updates = remote.where((r) {
final c = cache.firstWhereOrNull((c) => c.name == r.name);
return c != null && c != r;
}).toList();
_log.info("[call] Updated people: ${updates.toReadableString()}");
if (inserts.isNotEmpty || deletes.isNotEmpty || updates.isNotEmpty) {
await _c.sqliteDb.use((db) async {
final dbAccount = await db.accountOf(account);
await db.batch((batch) {
for (final d in deletes) {
batch.deleteWhere(
db.faceRecognitionPersons,
(sql.$FaceRecognitionPersonsTable p) =>
p.account.equals(dbAccount.rowId) & p.name.equals(d.name),
);
}
for (final u in updates) {
batch.update(
db.faceRecognitionPersons,
sql.FaceRecognitionPersonsCompanion(
name: sql.Value(u.name),
thumbFaceId: sql.Value(u.thumbFaceId),
count: sql.Value(u.count),
),
where: (sql.$FaceRecognitionPersonsTable p) =>
p.account.equals(dbAccount.rowId) & p.name.equals(u.name),
);
}
for (final i in inserts) {
batch.insert(
db.faceRecognitionPersons,
SqliteFaceRecognitionPersonConverter.toSql(dbAccount, i),
mode: sql.InsertMode.insertOrIgnore,
);
}
});
});
return true;
} else {
return false;
}
final result = await _c.npDb.syncFaceRecognitionPersons(
account: account.toDb(),
persons: remote.map(DbFaceRecognitionPersonConverter.toDb).toList(),
);
return result.insert > 0 || result.delete > 0 || result.update > 0;
}
final DiContainer _c;

View file

@ -1,8 +1,8 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
@ -10,9 +10,7 @@ part 'find_file.g.dart';
@npLog
class FindFile {
FindFile(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
const FindFile(this._c);
/// Find list of files in the DB by [fileIds]
///
@ -24,10 +22,13 @@ class FindFile {
void Function(int fileId)? onFileNotFound,
}) async {
_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 results = await _c.npDb.getFilesByFileIds(
account: account.toDb(),
fileIds: fileIds,
);
final files = results
.map((e) => DbFileConverter.fromDb(account.userId.toString(), e))
.toList();
final fileMap = <int, File>{};
for (final f in files) {
fileMap[f.fileId!] = f;

View file

@ -1,8 +1,8 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
@ -10,9 +10,7 @@ part 'find_file_descriptor.g.dart';
@npLog
class FindFileDescriptor {
FindFileDescriptor(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
const FindFileDescriptor(this._c);
/// Find list of files in the DB by [fileIds]
///
@ -24,11 +22,14 @@ class FindFileDescriptor {
void Function(int fileId)? onFileNotFound,
}) async {
_log.info("[call] fileIds: ${fileIds.toReadableString()}");
final dbFiles = await _c.sqliteDb.use((db) async {
return await db.fileDescriptorsByFileIds(
sql.ByAccount.app(account), fileIds);
});
final files = dbFiles.convertToAppFileDescriptor(account);
final dbResults = await _c.npDb.getFileDescriptors(
account: account.toDb(),
fileIds: fileIds,
);
final files = dbResults
.map((e) =>
DbFileDescriptorConverter.fromDb(account.userId.toString(), e))
.toList();
final fileMap = <int, FileDescriptor>{};
for (final f in files) {
fileMap[f.fdId] = f;

View file

@ -5,9 +5,7 @@ import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/use_case/find_file.dart';
class InflateFileDescriptor {
InflateFileDescriptor(this._c)
: assert(require(_c)),
assert(FindFile.require(_c));
InflateFileDescriptor(this._c) : assert(require(_c));
static bool require(DiContainer c) => true;

View file

@ -1,57 +1,23 @@
import 'package:drift/drift.dart' as sql;
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/files_query_builder.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:np_geocoder/np_geocoder.dart';
class ListLocationFile {
ListLocationFile(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
const ListLocationFile(this._c);
/// List all files located in [place], [countryCode]
Future<List<File>> call(
Account account, File dir, String? place, String countryCode) async {
final dbFiles = await _c.sqliteDb.use((db) async {
final query = db.queryFiles().run((q) {
q
..setQueryMode(sql.FilesQueryMode.completeFile)
..setAppAccount(account);
dir.strippedPathWithEmpty.run((p) {
if (p.isNotEmpty) {
q.byOrRelativePathPattern("$p/%");
}
});
return q.build();
});
if (place == null || alpha2CodeToName(countryCode) == place) {
// some places in the DB have the same name as the country, in such
// cases, we return all photos from the country
query.where(db.imageLocations.countryCode.equals(countryCode));
} else {
query
..where(db.imageLocations.name.equals(place) |
db.imageLocations.admin1.equals(place) |
db.imageLocations.admin2.equals(place))
..where(db.imageLocations.countryCode.equals(countryCode));
}
return await query
.map((r) => sql.CompleteFile(
r.readTable(db.files),
r.readTable(db.accountFiles),
r.readTableOrNull(db.images),
r.readTableOrNull(db.imageLocations),
r.readTableOrNull(db.trashes),
))
.get();
});
final dbFiles = await _c.npDb.getFilesByDirKeyAndLocation(
account: account.toDb(),
dirRelativePath: dir.strippedPathWithEmpty,
place: place,
countryCode: countryCode,
);
return dbFiles
.map((f) => SqliteFileConverter.fromSql(account.userId.toString(), f))
.map((f) => DbFileConverter.fromDb(account.userId.toString(), f))
.toList();
}

View file

@ -1,11 +1,10 @@
import 'package:drift/drift.dart' as sql;
import 'package:equatable/equatable.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_geocoder/np_geocoder.dart';
import 'package:to_string/to_string.dart';
part 'list_location_group.g.dart';
@ -58,172 +57,29 @@ class LocationGroupResult with EquatableMixin {
@npLog
class ListLocationGroup {
ListLocationGroup(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
const ListLocationGroup(this._c);
/// List location groups based on the name of the places
Future<LocationGroupResult> call(Account account) async {
final s = Stopwatch()..start();
try {
return await _c.sqliteDb.use((db) async {
final dbAccount = await db.accountOf(account);
final nameResult = <LocationGroup>[];
final admin1Result = <LocationGroup>[];
final admin2Result = <LocationGroup>[];
final countryCodeResult = <LocationGroup>[];
for (final r in account.roots) {
final latest = db.accountFiles.bestDateTime.max();
final count = db.imageLocations.rowId.count();
final nameQ = _buildQuery(
db, dbAccount, r, latest, count, db.imageLocations.name);
try {
_mergeResults(
nameResult,
await nameQ.map((r) {
return LocationGroup(
r.read(db.imageLocations.name)!,
r.read(db.imageLocations.countryCode)!,
r.read(count)!,
r.read(db.files.fileId)!,
r.read(latest)!.toUtc(),
);
}).get(),
);
} catch (e, stackTrace) {
_log.shout("[call] Failed while query name group", e, stackTrace);
}
final admin1Q = _buildQuery(
db, dbAccount, r, latest, count, db.imageLocations.admin1);
try {
_mergeResults(
admin1Result,
await admin1Q
.map((r) => LocationGroup(
r.read(db.imageLocations.admin1)!,
r.read(db.imageLocations.countryCode)!,
r.read(count)!,
r.read(db.files.fileId)!,
r.read(latest)!.toUtc(),
))
.get(),
);
} catch (e, stackTrace) {
_log.shout("[call] Failed while query admin1 group", e, stackTrace);
}
final admin2Q = _buildQuery(
db, dbAccount, r, latest, count, db.imageLocations.admin2);
try {
_mergeResults(
admin2Result,
await admin2Q
.map((r) => LocationGroup(
r.read(db.imageLocations.admin2)!,
r.read(db.imageLocations.countryCode)!,
r.read(count)!,
r.read(db.files.fileId)!,
r.read(latest)!.toUtc(),
))
.get(),
);
} catch (e, stackTrace) {
_log.shout("[call] Failed while query admin2 group", e, stackTrace);
}
final countryCodeQ = _buildQuery(
db, dbAccount, r, latest, count, db.imageLocations.countryCode);
try {
_mergeResults(
countryCodeResult,
await countryCodeQ.map((r) {
final cc = r.read(db.imageLocations.countryCode)!;
return LocationGroup(
alpha2CodeToName(cc) ?? cc,
cc,
r.read(count)!,
r.read(db.files.fileId)!,
r.read(latest)!.toUtc(),
);
}).get(),
);
} catch (e, stackTrace) {
_log.shout(
"[call] Failed while query countryCode group", e, stackTrace);
}
}
return LocationGroupResult(
nameResult, admin1Result, admin2Result, countryCodeResult);
});
final dbObj = await _c.npDb.groupLocations(
account: account.toDb(),
includeRelativeRoots: account.roots,
excludeRelativeRoots: [
remote_storage_util.remoteStorageDirRelativePath
],
);
return LocationGroupResult(
dbObj.name.map(DbLocationGroupConverter.fromDb).toList(),
dbObj.admin1.map(DbLocationGroupConverter.fromDb).toList(),
dbObj.admin2.map(DbLocationGroupConverter.fromDb).toList(),
dbObj.countryCode.map(DbLocationGroupConverter.fromDb).toList(),
);
} finally {
_log.info("[call] Elapsed time: ${s.elapsedMilliseconds}ms");
}
}
sql.JoinedSelectStatement _buildQuery(
sql.SqliteDb db,
sql.Account dbAccount,
String dir,
sql.Expression<DateTime> latest,
sql.Expression<int> count,
sql.GeneratedColumn<String> groupColumn,
) {
final query = db.selectOnly(db.imageLocations).join([
sql.innerJoin(db.accountFiles,
db.accountFiles.rowId.equalsExp(db.imageLocations.accountFile),
useColumns: false),
sql.innerJoin(db.files, db.files.rowId.equalsExp(db.accountFiles.file),
useColumns: false),
]);
if (identical(groupColumn, db.imageLocations.countryCode)) {
query
..addColumns([
db.imageLocations.countryCode,
count,
db.files.fileId,
latest,
])
..groupBy([db.imageLocations.countryCode],
having: db.accountFiles.bestDateTime.equalsExp(latest));
} else {
query
..addColumns([
groupColumn,
db.imageLocations.countryCode,
count,
db.files.fileId,
latest,
])
..groupBy([groupColumn, db.imageLocations.countryCode],
having: db.accountFiles.bestDateTime.equalsExp(latest));
}
query
..where(db.accountFiles.account.equals(dbAccount.rowId))
..where(groupColumn.isNotNull());
if (dir.isNotEmpty) {
query.where(db.accountFiles.relativePath.like("$dir/%"));
}
return query;
}
static void _mergeResults(
List<LocationGroup> into, List<LocationGroup> from) {
for (final g in from) {
final i = into.indexWhere(
(e) => e.place == g.place && e.countryCode == g.countryCode);
if (i >= 0) {
// duplicate entry, sum the count and pick the newer file
final newer =
into[i].latestDateTime.isAfter(g.latestDateTime) ? into[i] : g;
into[i] = LocationGroup(g.place, g.countryCode, into[i].count + g.count,
newer.latestFileId, newer.latestDateTime);
} else {
into.add(g);
}
}
}
final DiContainer _c;
}

View file

@ -13,9 +13,7 @@ part 'list_share.g.dart';
/// List all shares from a given file
@npLog
class ListShare {
ListShare(this._c)
: assert(require(_c)),
assert(FindFile.require(_c));
ListShare(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.shareRepo);

View file

@ -12,9 +12,7 @@ part 'list_tagged_file.g.dart';
@npLog
class ListTaggedFile {
ListTaggedFile(this._c)
: assert(require(_c)),
assert(FindFile.require(_c));
ListTaggedFile(this._c) : assert(require(_c));
static bool require(DiContainer c) =>
DiContainer.has(c, DiType.taggedFileRepo);

View file

@ -1,17 +1,13 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' as sql;
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/use_case/recognize_face/list_recognize_face.dart';
import 'package:nc_photos/use_case/recognize_face/list_recognize_face_item.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
part 'sync_recognize_face.g.dart';
@ -24,95 +20,28 @@ class SyncRecognizeFace {
/// Return if any people were updated
Future<bool> call(Account account) async {
_log.info("[call] Sync people with remote");
final faces = await _getFaceResults(account);
if (faces == null) {
return false;
}
var shouldUpdate = !faces.isEmpty;
final items =
await _getFaceItemResults(account, faces.results.values.toList());
shouldUpdate = shouldUpdate || items.values.any((e) => !e.isEmpty);
if (!shouldUpdate) {
return false;
}
await _c.sqliteDb.use((db) async {
final dbAccount = await db.accountOf(account);
await db.batch((batch) {
for (final d in faces.deletes) {
batch.deleteWhere(
db.recognizeFaces,
(sql.$RecognizeFacesTable t) =>
t.account.equals(dbAccount.rowId) & t.label.equals(d),
);
}
for (final u in faces.updates) {
batch.update(
db.recognizeFaces,
sql.RecognizeFacesCompanion(
label: sql.Value(faces.results[u]!.label),
),
where: (sql.$RecognizeFacesTable t) =>
t.account.equals(dbAccount.rowId) & t.label.equals(u),
);
}
for (final i in faces.inserts) {
batch.insert(
db.recognizeFaces,
SqliteRecognizeFaceConverter.toSql(dbAccount, faces.results[i]!),
mode: sql.InsertMode.insertOrIgnore,
);
}
});
// update each item
for (final f in faces.results.values) {
try {
await _syncDbForFaceItem(db, dbAccount, f, items[f]!);
} catch (e, stackTrace) {
_log.shout("[call] Failed to update db for face: $f", e, stackTrace);
}
}
});
return true;
}
Future<_FaceResult?> _getFaceResults(Account account) async {
int faceSorter(RecognizeFace a, RecognizeFace b) =>
a.label.compareTo(b.label);
late final List<RecognizeFace> remote;
final List<RecognizeFace> remote;
try {
remote = (await ListRecognizeFace(_c.withRemoteRepo())(account).last)
..sort(faceSorter);
remote = await ListRecognizeFace(_c.withRemoteRepo())(account).last;
} catch (e) {
if (e is ApiException && e.response.statusCode == 404) {
// recognize app probably not installed, ignore
_log.info("[_getFaceResults] Recognize app not installed");
return null;
_log.info("[call] Recognize app not installed");
return false;
}
rethrow;
}
final cache = (await ListRecognizeFace(_c.withLocalRepo())(account).last)
..sort(faceSorter);
final diff = getDiffWith(cache, remote, faceSorter);
final inserts = diff.onlyInB;
_log.info("[_getFaceResults] New face: ${inserts.toReadableString()}");
final deletes = diff.onlyInA;
_log.info("[_getFaceResults] Removed face: ${deletes.toReadableString()}");
final updates = remote.where((r) {
final c = cache.firstWhereOrNull((c) => c.label == r.label);
return c != null && c != r;
}).toList();
_log.info("[_getFaceResults] Updated face: ${updates.toReadableString()}");
return _FaceResult(
results: remote.map((e) => MapEntry(e.label, e)).toMap(),
inserts: inserts.map((e) => e.label).toList(),
updates: updates.map((e) => e.label).toList(),
deletes: deletes.map((e) => e.label).toList(),
final remoteItems = await _getFaceItems(account, remote);
return _c.npDb.syncRecognizeFacesAndItems(
account: account.toDb(),
data: remoteItems.map((key, value) => MapEntry(
key.toDb(),
value.map(DbRecognizeFaceItemConverter.toDb).toList(),
)),
);
}
Future<Map<RecognizeFace, _FaceItemResult>> _getFaceItemResults(
Future<Map<RecognizeFace, List<RecognizeFaceItem>>> _getFaceItems(
Account account, List<RecognizeFace> faces) async {
Object? firstError;
StackTrace? firstStackTrace;
@ -121,7 +50,7 @@ class SyncRecognizeFace {
faces,
onError: (f, e, stackTrace) {
_log.severe(
"[_getFaceItemResults] Failed while listing remote face: $f",
"[_getFaceItems] Failed while listing remote face: $f",
e,
stackTrace,
);
@ -135,148 +64,8 @@ class SyncRecognizeFace {
Error.throwWithStackTrace(
firstError!, firstStackTrace ?? StackTrace.current);
}
final cache = await ListMultipleRecognizeFaceItem(_c.withLocalRepo())(
account,
faces,
onError: (f, e, stackTrace) {
_log.severe("[_getFaceItemResults] Failed while listing cache face: $f",
e, stackTrace);
},
).last;
int itemSorter(RecognizeFaceItem a, RecognizeFaceItem b) =>
a.fileId.compareTo(b.fileId);
final results = <RecognizeFace, _FaceItemResult>{};
for (final f in faces) {
final thisCache = (cache[f] ?? [])..sort(itemSorter);
final thisRemote = (remote[f] ?? [])..sort(itemSorter);
final diff =
getDiffWith<RecognizeFaceItem>(thisCache, thisRemote, itemSorter);
final inserts = diff.onlyInB;
_log.info(
"[_getFaceItemResults] New item: ${inserts.toReadableString()}");
final deletes = diff.onlyInA;
_log.info(
"[_getFaceItemResults] Removed item: ${deletes.toReadableString()}");
final updates = thisRemote.where((r) {
final c = thisCache.firstWhereOrNull((c) => c.fileId == r.fileId);
return c != null && c != r;
}).toList();
_log.info(
"[_getFaceItemResults] Updated item: ${updates.toReadableString()}");
results[f] = _FaceItemResult(
results: thisRemote.map((e) => MapEntry(e.fileId, e)).toMap(),
inserts: inserts.map((e) => e.fileId).toList(),
updates: updates.map((e) => e.fileId).toList(),
deletes: deletes.map((e) => e.fileId).toList(),
);
}
return results;
}
// Future<_FaceItemResult?> _getFaceItemResults(
// Account account, RecognizeFace face) async {
// late final List<RecognizeFaceItem> remote;
// try {
// remote =
// await ListRecognizeFaceItem(_c.withRemoteRepo())(account, face).last;
// } catch (e) {
// if (e is ApiException && e.response.statusCode == 404) {
// // recognize app probably not installed, ignore
// _log.info("[_getFaceItemResults] Recognize app not installed");
// return null;
// }
// rethrow;
// }
// final cache =
// await ListRecognizeFaceItem(_c.withLocalRepo())(account, face).last;
// int itemSorter(RecognizeFaceItem a, RecognizeFaceItem b) =>
// a.fileId.compareTo(b.fileId);
// final diff = getDiffWith(cache, remote, itemSorter);
// final inserts = diff.onlyInB;
// _log.info("[_getFaceItemResults] New face: ${inserts.toReadableString()}");
// final deletes = diff.onlyInA;
// _log.info(
// "[_getFaceItemResults] Removed face: ${deletes.toReadableString()}");
// final updates = remote.where((r) {
// final c = cache.firstWhereOrNull((c) => c.fileId == r.fileId);
// return c != null && c != r;
// }).toList();
// _log.info(
// "[_getFaceItemResults] Updated face: ${updates.toReadableString()}");
// return _FaceItemResult(
// results: remote.map((e) => MapEntry(e.fileId, e)).toMap(),
// inserts: inserts.map((e) => e.fileId).toList(),
// updates: updates.map((e) => e.fileId).toList(),
// deletes: deletes.map((e) => e.fileId).toList(),
// );
// }
Future<void> _syncDbForFaceItem(sql.SqliteDb db, sql.Account dbAccount,
RecognizeFace face, _FaceItemResult item) async {
await db.transaction(() async {
final dbFace = await db.recognizeFaceByLabel(
account: sql.ByAccount.sql(dbAccount),
label: face.label,
);
await db.batch((batch) {
for (final d in item.deletes) {
batch.deleteWhere(
db.recognizeFaceItems,
(sql.$RecognizeFaceItemsTable t) =>
t.parent.equals(dbFace.rowId) & t.fileId.equals(d),
);
}
for (final u in item.updates) {
batch.update(
db.recognizeFaceItems,
SqliteRecognizeFaceItemConverter.toSql(dbFace, item.results[u]!),
where: (sql.$RecognizeFaceItemsTable t) =>
t.parent.equals(dbFace.rowId) & t.fileId.equals(u),
);
}
for (final i in item.inserts) {
batch.insert(
db.recognizeFaceItems,
SqliteRecognizeFaceItemConverter.toSql(dbFace, item.results[i]!),
mode: sql.InsertMode.insertOrIgnore,
);
}
});
});
return remote;
}
final DiContainer _c;
}
class _FaceResult {
const _FaceResult({
required this.results,
required this.inserts,
required this.updates,
required this.deletes,
});
bool get isEmpty => inserts.isEmpty && updates.isEmpty && deletes.isEmpty;
final Map<String, RecognizeFace> results;
final List<String> inserts;
final List<String> updates;
final List<String> deletes;
}
class _FaceItemResult {
const _FaceItemResult({
required this.results,
required this.inserts,
required this.updates,
required this.deletes,
});
bool get isEmpty => inserts.isEmpty && updates.isEmpty && deletes.isEmpty;
final Map<int, RecognizeFaceItem> results;
final List<int> inserts;
final List<int> updates;
final List<int> deletes;
}

View file

@ -14,9 +14,7 @@ part 'resync_album.g.dart';
/// Resync files inside an album with the file db
@npLog
class ResyncAlbum {
ResyncAlbum(this._c)
: assert(require(_c)),
assert(FindFile.require(_c));
ResyncAlbum(this._c) : assert(require(_c));
static bool require(DiContainer c) => true;

View file

@ -1,132 +1,53 @@
import 'package:drift/drift.dart' as sql;
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/files_query_builder.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
class ScanDirOffline {
ScanDirOffline(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
const ScanDirOffline(this._c);
Future<List<FileDescriptor>> call(
Account account,
File root, {
bool isOnlySupportedFormat = true,
}) async {
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 strippedPath = root.strippedPathWithEmpty;
final bool isOnlySupportedFormat = args["isOnlySupportedFormat"];
final dbFiles = await db.useInIsolate((db) async {
final query = db.queryFiles().run((q) {
q
..setQueryMode(
sql.FilesQueryMode.expression,
expressions: [
db.accountFiles.relativePath,
db.files.fileId,
db.files.contentType,
db.accountFiles.isArchived,
db.accountFiles.isFavorite,
db.accountFiles.bestDateTime,
],
)
..setAppAccount(account);
if (strippedPath.isNotEmpty) {
q.byOrRelativePathPattern("$strippedPath/%");
}
return q.build();
});
if (isOnlySupportedFormat) {
query.where(db.whereFileIsSupportedMime());
}
if (strippedPath.isEmpty) {
query.where(db.accountFiles.relativePath
.like("${remote_storage_util.remoteStorageDirRelativePath}/%")
.not());
}
return await query
.map((r) => <String, dynamic>{
"relativePath": r.read(db.accountFiles.relativePath)!,
"fileId": r.read(db.files.fileId)!,
"contentType": r.read(db.files.contentType)!,
"isArchived": r.read(db.accountFiles.isArchived),
"isFavorite": r.read(db.accountFiles.isFavorite),
"bestDateTime": r.read(db.accountFiles.bestDateTime)!.toUtc(),
})
.get();
});
return dbFiles
.map((f) => FileDescriptor(
fdPath:
"remote.php/dav/files/${account.userId.toString()}/${f["relativePath"]}",
fdId: f["fileId"],
fdMime: f["contentType"],
fdIsArchived: f["isArchived"] ?? false,
fdIsFavorite: f["isFavorite"] ?? false,
fdDateTime: f["bestDateTime"],
))
.toList();
});
final results = await _c.npDb.getFileDescriptors(
account: account.toDb(),
includeRelativeRoots: [root.strippedPathWithEmpty],
excludeRelativeRoots: [remote_storage_util.remoteStorageDirRelativePath],
mimes: isOnlySupportedFormat ? file_util.supportedFormatMimes : null,
);
return results
.map((e) =>
DbFileDescriptorConverter.fromDb(account.userId.toString(), e))
.toList();
}
final DiContainer _c;
}
class ScanDirOfflineMini {
ScanDirOfflineMini(this._c) : assert(require(_c));
const ScanDirOfflineMini(this._c);
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
Future<List<File>> call(
Future<List<FileDescriptor>> call(
Account account,
Iterable<File> roots,
List<File> roots,
int limit, {
bool isOnlySupportedFormat = true,
}) async {
final dbFiles = await _c.sqliteDb.use((db) async {
final query = db.queryFiles().run((q) {
q
..setQueryMode(sql.FilesQueryMode.completeFile)
..setAppAccount(account);
for (final r in roots) {
final path = r.strippedPathWithEmpty;
if (path.isEmpty) {
break;
}
q.byOrRelativePathPattern("$path/%");
}
return q.build();
});
if (isOnlySupportedFormat) {
query.where(db.whereFileIsSupportedMime());
}
query
..orderBy([sql.OrderingTerm.desc(db.accountFiles.bestDateTime)])
..limit(limit);
return await query
.map((r) => sql.CompleteFile(
r.readTable(db.files),
r.readTable(db.accountFiles),
r.readTableOrNull(db.images),
r.readTableOrNull(db.imageLocations),
r.readTableOrNull(db.trashes),
))
.get();
});
return dbFiles
.map((f) => SqliteFileConverter.fromSql(account.userId.toString(), f))
final results = await _c.npDb.getFileDescriptors(
account: account.toDb(),
includeRelativeRoots: roots.map((e) => e.strippedPathWithEmpty).toList(),
excludeRelativeRoots: [remote_storage_util.remoteStorageDirRelativePath],
mimes: isOnlySupportedFormat ? file_util.supportedFormatMimes : null,
limit: limit,
);
return results
.map((e) =>
DbFileDescriptorConverter.fromDb(account.userId.toString(), e))
.toList();
}

View file

@ -1,6 +1,6 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/search.dart';
@ -9,7 +9,8 @@ class Search {
static bool require(DiContainer c) => DiContainer.has(c, DiType.searchRepo);
Future<List<File>> call(Account account, SearchCriteria criteria) async {
Future<List<FileDescriptor>> call(
Account account, SearchCriteria criteria) async {
final files = await _c.searchRepo.list(account, criteria);
return files.where((f) => file_util.isSupportedFormat(f)).toList();
}

View file

@ -24,8 +24,7 @@ part 'startup_sync.g.dart';
class StartupSync {
StartupSync(this._c) : assert(require(_c));
static bool require(DiContainer c) =>
SyncFavorite.require(c) && SyncTag.require(c);
static bool require(DiContainer c) => SyncFavorite.require(c);
/// Sync in a background isolate
static Future<SyncResult> runInIsolate(

View file

@ -1,14 +1,12 @@
import 'package:flutter/rendering.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.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/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/files_query_builder.dart' as sql;
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/progress_util.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/use_case/ls_single_file.dart';
@ -23,7 +21,6 @@ class SyncDir {
static bool require(DiContainer c) =>
DiContainer.has(c, DiType.fileRepoRemote) &&
DiContainer.has(c, DiType.sqliteDb) &&
DiContainer.has(c, DiType.touchManager);
/// Sync local SQLite DB with remote content
@ -118,25 +115,10 @@ class SyncDir {
Future<Map<int, String>> _queryAllDirEtags(
Account account, String dirPath) async {
final dir = File(path: dirPath);
return await _c.sqliteDb.use((db) async {
final query = db.queryFiles().run((q) {
q
..setQueryMode(sql.FilesQueryMode.expression,
expressions: [db.files.fileId, db.files.etag])
..setAppAccount(account);
if (dir.strippedPathWithEmpty.isNotEmpty) {
q
..byOrRelativePath(dir.strippedPathWithEmpty)
..byOrRelativePathPattern("${dir.strippedPathWithEmpty}/%");
}
return q.build();
});
query.where(db.files.isCollection.equals(true));
return Map.fromEntries(await query
.map(
(r) => MapEntry(r.read(db.files.fileId)!, r.read(db.files.etag)!))
.get());
});
return _c.npDb.getDirFileIdToEtagByLikeRelativePath(
account: account.toDb(),
relativePath: dir.strippedPathWithEmpty,
);
}
final DiContainer _c;

View file

@ -11,9 +11,7 @@ part 'sync_favorite.g.dart';
@npLog
class SyncFavorite {
SyncFavorite(this._c)
: assert(require(_c)),
assert(CacheFavorite.require(_c));
SyncFavorite(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.favoriteRepo);

View file

@ -1,71 +1,23 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' as sql;
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/entity/tag.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
part 'sync_tag.g.dart';
@npLog
class SyncTag {
SyncTag(this._c) : assert(require(_c));
static bool require(DiContainer c) =>
DiContainer.has(c, DiType.tagRepoRemote) &&
DiContainer.has(c, DiType.tagRepoLocal);
const SyncTag(this._c);
/// Sync tags in cache db with remote server
Future<void> call(Account account) async {
_log.info("[call] Sync tags with remote");
int tagSorter(Tag a, Tag b) => a.id.compareTo(b.id);
final remote = (await _c.tagRepoRemote.list(account))..sort(tagSorter);
final cache = (await _c.tagRepoLocal.list(account))..sort(tagSorter);
final diff = getDiffWith<Tag>(cache, remote, tagSorter);
final inserts = diff.onlyInB;
_log.info("[call] New tags: ${inserts.toReadableString()}");
final deletes = diff.onlyInA;
_log.info("[call] Removed tags: ${deletes.toReadableString()}");
final updates = remote.where((r) {
final c = cache.firstWhereOrNull((c) => c.id == r.id);
return c != null && c != r;
}).toList();
_log.info("[call] Updated tags: ${updates.toReadableString()}");
if (inserts.isNotEmpty || deletes.isNotEmpty || updates.isNotEmpty) {
await _c.sqliteDb.use((db) async {
final dbAccount = await db.accountOf(account);
await db.batch((batch) {
for (final d in deletes) {
batch.deleteWhere(
db.tags,
(sql.$TagsTable t) =>
t.server.equals(dbAccount.server) & t.tagId.equals(d.id),
);
}
for (final u in updates) {
batch.update(
db.tags,
sql.TagsCompanion(
displayName: sql.Value(u.displayName),
userVisible: sql.Value(u.userVisible),
userAssignable: sql.Value(u.userAssignable),
),
where: (sql.$TagsTable t) =>
t.server.equals(dbAccount.server) & t.tagId.equals(u.id),
);
}
for (final i in inserts) {
batch.insert(db.tags, SqliteTagConverter.toSql(dbAccount, i),
mode: sql.InsertMode.insertOrIgnore);
}
});
});
}
final remote = await _c.tagRepoRemote.list(account);
await _c.npDb.syncTags(
account: account.toDb(),
tags: remote.map(DbTagConverter.toDb).toList(),
);
}
final DiContainer _c;

View file

@ -1,4 +1,3 @@
export 'db_util.dart';
export 'download.dart';
export 'file_saver.dart';
export 'notification.dart';

View file

@ -15,10 +15,10 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/controller/pref_controller.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/server_status.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/help_utils.dart' as help_util;
@ -32,6 +32,7 @@ import 'package:nc_photos/widget/settings.dart';
import 'package:nc_photos/widget/settings/account_settings.dart';
import 'package:nc_photos/widget/sign_in.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_db/np_db.dart';
import 'package:to_string/to_string.dart';
part 'account_picker_dialog.g.dart';
@ -51,6 +52,7 @@ class AccountPickerDialog extends StatelessWidget {
container: KiwiContainer().resolve(),
accountController: context.read(),
prefController: context.read(),
db: context.read(),
),
child: const _WrappedAccountPickerDialog(),
);

View file

@ -6,6 +6,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
required DiContainer container,
required this.accountController,
required this.prefController,
required this.db,
}) : _c = container,
super(_State.init(
accounts: container.pref.getAccounts3Or([]),
@ -100,9 +101,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
Future<void> _removeAccountFromDb(Account account) async {
try {
await _c.sqliteDb.use((db) async {
await db.deleteAccountOf(account);
});
await db.deleteAccount(account.toDb());
} catch (e, stackTrace) {
_log.shout("[_removeAccountFromDb] Failed while removing account from db",
e, stackTrace);
@ -112,6 +111,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
final DiContainer _c;
final AccountController accountController;
final PrefController prefController;
final NpDb db;
late final Account activeAccount = accountController.account;
final _prefLock = Mutex();

View file

@ -15,12 +15,13 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/progress.dart';
import 'package:nc_photos/bloc/scan_account_dir.dart';
import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/collection.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k;
@ -534,12 +535,12 @@ class _HomePhotosState extends State<HomePhotos>
.value)) {
try {
final c = KiwiContainer().resolve<DiContainer>();
final missingMetadataCount = await c.sqliteDb.use((db) async {
return await db.countMissingMetadataByFileIds(
appAccount: widget.account,
fileIds: _backingFiles.map((e) => e.fdId).toList(),
);
});
final missingMetadataCount =
await c.npDb.countFilesByFileIdsMissingMetadata(
account: widget.account.toDb(),
fileIds: _backingFiles.map((e) => e.fdId).toList(),
mimes: file_util.supportedImageFormatMimes,
);
_log.info(
"[_tryStartMetadataTask] Missing count: $missingMetadataCount");
if (missingMetadataCount > 0) {

View file

@ -10,7 +10,6 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/search.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/search.dart';
@ -509,7 +508,7 @@ class _HomeSearchState extends State<HomeSearch>
);
}
void _transformItems(List<File> files) {
void _transformItems(List<FileDescriptor> files) {
_buildItemQueue.addJob(
PhotoListItemBuilderArguments(
widget.account,

View file

@ -50,6 +50,7 @@ import 'package:nc_photos/widget/trashbin_browser.dart';
import 'package:nc_photos/widget/trashbin_viewer.dart';
import 'package:nc_photos/widget/viewer.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_db/np_db.dart';
import 'package:to_string/to_string.dart';
part 'my_app.g.dart';
@ -74,6 +75,9 @@ class MyApp extends StatelessWidget {
RepositoryProvider(
create: (_) => PrefController(_c),
),
RepositoryProvider<NpDb>(
create: (_) => _c.npDb,
),
],
child: BlocProvider(
create: (context) => _Bloc(

View file

@ -40,15 +40,13 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
Future<void> _onVacuumDb(_VacuumDb ev, Emitter<_State> emit) async {
_log.info(ev);
await _c.sqliteDb.useNoTransaction((db) async {
await db.customStatement("VACUUM;");
});
await _c.npDb.sqlVacuum();
emit(state.copyWith(message: StateMessage("Finished successfully")));
}
Future<void> _onExportDb(_ExportDb ev, Emitter<_State> emit) async {
_log.info(ev);
await platform.exportSqliteDb(_c.sqliteDb);
await _c.npDb.export(await getApplicationDocumentsDirectory());
emit(state.copyWith(message: StateMessage("Finished successfully")));
}

View file

@ -7,17 +7,15 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/sqlite/database.dart';
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
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/mobile/self_signed_cert_manager.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/widget/page_visibility_mixin.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_platform_util/np_platform_util.dart';
import 'package:path_provider/path_provider.dart';
import 'package:to_string/to_string.dart';
part 'developer/bloc.dart';

View file

@ -10,8 +10,10 @@ class _Error {
@npLog
class _Bloc extends Bloc<_Event, _State> with BlocLogger {
_Bloc(DiContainer c)
: _c = c,
_Bloc(
DiContainer c, {
required this.db,
}) : _c = c,
super(const _State()) {
on<_ClearCacheDatabase>(_onClearCacheDatabase);
}
@ -24,13 +26,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
Future<void> _onClearCacheDatabase(
_ClearCacheDatabase ev, Emitter<_State> emit) async {
try {
await _c.sqliteDb.use((db) async {
await db.truncate();
final accounts = _c.pref.getAccounts3Or([]);
for (final a in accounts) {
await db.insertAccountOf(a);
}
});
final accounts = _c.pref.getAccounts3Or([]);
await db.clearAndInitWithAccounts(accounts.toDb());
emit(state.copyWith(lastSuccessful: ev));
} catch (e, stackTrace) {
_log.shout("[_onClearCacheDatabase] Uncaught exception", e, stackTrace);
@ -39,5 +36,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
}
final DiContainer _c;
final NpDb db;
final _errorStream = StreamController<_Error>.broadcast();
}

View file

@ -7,13 +7,14 @@ import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/sqlite/database.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_db/np_db.dart';
import 'package:to_string/to_string.dart';
part 'expert/bloc.dart';
@ -26,7 +27,10 @@ class ExpertSettings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => _Bloc(KiwiContainer().resolve<DiContainer>()),
create: (_) => _Bloc(
KiwiContainer().resolve<DiContainer>(),
db: context.read(),
),
child: const _WrappedExpertSettings(),
);
}

View file

@ -2,14 +2,13 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:kiwi/kiwi.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/pref_util.dart' as pref_util;
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/legacy/sign_in.dart' as legacy;
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/widget/connect.dart';
@ -17,6 +16,7 @@ import 'package:nc_photos/widget/home.dart';
import 'package:nc_photos/widget/root_picker.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_db/np_db.dart';
import 'package:np_string/np_string.dart';
part 'sign_in.g.dart';
@ -174,10 +174,7 @@ class _SignInState extends State<SignIn> {
}
Future<void> _persistAccount(Account account) async {
final c = KiwiContainer().resolve<DiContainer>();
await c.sqliteDb.use((db) async {
await db.insertAccountOf(account);
});
await context.read<NpDb>().addAccounts([account.toDb()]);
// only signing in with app password would trigger distinct
final accounts = (Pref().getAccounts3Or([])..add(account)).distinct();
try {

View file

@ -6,9 +6,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/mobile/android/activity.dart';
import 'package:nc_photos/mobile/android/permission_util.dart';
@ -20,6 +20,7 @@ import 'package:nc_photos/widget/home.dart';
import 'package:nc_photos/widget/setup.dart';
import 'package:nc_photos/widget/sign_in.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_db/np_db.dart';
import 'package:np_platform_permission/np_platform_permission.dart';
import 'package:np_platform_util/np_platform_util.dart';
import 'package:to_string/to_string.dart';
@ -206,7 +207,7 @@ class _SplashState extends State<Splash> {
try {
_log.info("[_upgrade46] insertDbAccounts");
final c = KiwiContainer().resolve<DiContainer>();
await CompatV46.insertDbAccounts(Pref(), c.sqliteDb);
await CompatV46.insertDbAccounts(c.pref, context.read());
} catch (e, stackTrace) {
_log.shout("[_upgrade46] Failed while clearDefaultCache", e, stackTrace);
unawaited(Pref().setAccounts3(null));
@ -235,7 +236,7 @@ class _SplashState extends State<Splash> {
try {
_log.info("[_upgrade55] migrate DB");
await CompatV55.migrateDb(
c.sqliteDb,
c.npDb,
onProgress: (current, count) {
_upgradeCubit.setState(
L10n.global().migrateDatabaseProcessingNotification,
@ -246,13 +247,8 @@ class _SplashState extends State<Splash> {
);
} catch (e, stackTrace) {
_log.shout("[_upgrade55] Failed while migrateDb", e, stackTrace);
await c.sqliteDb.use((db) async {
await db.truncate();
final accounts = Pref().getAccounts3Or([]);
for (final a in accounts) {
await db.insertAccountOf(a);
}
});
final accounts = Pref().getAccounts3Or([]);
await context.read<NpDb>().clearAndInitWithAccounts(accounts.toDb());
}
_upgradeCubit.setIntermediate();
}

View file

@ -202,14 +202,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306
url: "https://pub.dev"
source: hosted
version: "1.3.1"
checked_yaml:
dependency: transitive
description:
@ -226,14 +218,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.1"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c"
url: "https://pub.dev"
source: hosted
version: "0.3.5"
clock:
dependency: "direct main"
description:
@ -398,21 +382,13 @@ packages:
source: git
version: "0.1.0"
drift:
dependency: "direct main"
dependency: "direct dev"
description:
name: drift
sha256: "21abd7b1c1a637a264f58f9f05c7b910d29c204aab1cbfcb4d9fada1e98a9303"
url: "https://pub.dev"
source: hosted
version: "2.8.0"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: ac1454f20b4b721bfbde7bd3b05123150bae2196ef992c405c07a63f5976a397
url: "https://pub.dev"
source: hosted
version: "2.8.0"
dynamic_color:
dependency: "direct main"
description:
@ -983,6 +959,27 @@ packages:
relative: true
source: path
version: "0.0.1"
np_datetime:
dependency: "direct main"
description:
path: "../np_datetime"
relative: true
source: path
version: "1.0.0"
np_db:
dependency: "direct main"
description:
path: "../np_db"
relative: true
source: path
version: "1.0.0"
np_db_sqlite:
dependency: "direct dev"
description:
path: "../np_db_sqlite"
relative: true
source: path
version: "1.0.0"
np_geocoder:
dependency: "direct main"
description:
@ -1296,14 +1293,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.1"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rxdart:
dependency: "direct main"
description:
@ -1518,7 +1507,7 @@ packages:
source: hosted
version: "2.4.5"
sqlite3:
dependency: "direct main"
dependency: transitive
description:
name: sqlite3
sha256: "2cef47b59d310e56f8275b13734ee80a9cf4a48a43172020cb55a620121fbf66"
@ -1526,21 +1515,13 @@ packages:
source: hosted
version: "1.11.1"
sqlite3_flutter_libs:
dependency: "direct main"
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "1e20a88d5c7ae8400e009f38ddbe8b001800a6dffa37832481a86a219bc904c7"
url: "https://pub.dev"
source: hosted
version: "0.5.15"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: b5b24c2804d39cbd619b424d8c9b1321cc5e813fd0e7b95a2707f596f82d5cd3
url: "https://pub.dev"
source: hosted
version: "0.29.0"
stack_trace:
dependency: transitive
description:
@ -1907,4 +1888,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=2.19.6 <3.0.0"
flutter: ">=3.4.0-17.0.pre"
flutter: ">=3.7.0"

View file

@ -52,7 +52,6 @@ dependencies:
git:
url: https://gitlab.com/nc-photos/flutter-draggable-scrollbar
ref: v0.1.0-nc-photos-6
drift: 2.8.0
dynamic_color: ^1.6.6
equatable: ^2.0.5
event_bus: ^2.0.0
@ -101,6 +100,10 @@ dependencies:
path: ../np_common
np_collection:
path: ../np_collection
np_datetime:
path: ../np_datetime
np_db:
path: ../np_db
np_geocoder:
path: ../np_geocoder
np_gps_map:
@ -139,8 +142,6 @@ dependencies:
shared_preferences_platform_interface: any
sliver_tools: ^0.2.10
smooth_corner: ^1.1.0
sqlite3: any
sqlite3_flutter_libs: ^0.5.15
to_string:
git:
url: https://gitlab.com/nkming2/dart-to-string
@ -176,13 +177,15 @@ dev_dependencies:
path: copy_with_build
ref: copy_with_build-1.7.0
dart_code_metrics: any
drift_dev: 2.8.0
drift: 2.8.0
flutter_test:
sdk: flutter
# integration_test:
# sdk: flutter
np_codegen_build:
path: ../codegen_build
np_db_sqlite:
path: ../np_db_sqlite
np_lints:
path: ../np_lints
to_string_build:

View file

@ -1,7 +1,8 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:nc_photos/bloc/list_album_share_outlier.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:np_string/np_string.dart';
import 'package:test/test.dart';
@ -56,7 +57,7 @@ void _initialState() {
final c = DiContainer(
shareRepo: MockShareRepo(),
shareeRepo: MockShareeRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
final bloc = ListAlbumShareOutlierBloc(c);
@ -85,10 +86,10 @@ void _testQueryUnsharedAlbumExtraShare(String description) {
shareeRepo: MockShareeMemoryRepo([
util.buildSharee(shareWith: "user1".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
},
@ -128,7 +129,7 @@ void _testQueryUnsharedAlbumExtraJsonShare(String description) {
shareeRepo: MockShareeMemoryRepo([
util.buildSharee(shareWith: "user1".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
},
tearDown: () => c.sqliteDb.close(),
@ -172,10 +173,10 @@ void _testQuerySharedAlbumMissingShare(String description) {
shareeRepo: MockShareeMemoryRepo([
util.buildSharee(shareWith: "user1".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
},
@ -226,10 +227,10 @@ void _testQuerySharedAlbumMissingManagedShareOtherAdded(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
},
@ -286,11 +287,11 @@ void _testQuerySharedAlbumMissingManagedShareOtherReshared(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccountOf(user1Account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
});
@ -339,10 +340,10 @@ void _testQuerySharedAlbumMissingUnmanagedShareOtherAdded(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
},
@ -374,7 +375,7 @@ void _testQuerySharedAlbumMissingJsonShare(String description) {
shareeRepo: MockShareeMemoryRepo([
util.buildSharee(shareWith: "user1".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
},
tearDown: () => c.sqliteDb.close(),
@ -421,10 +422,10 @@ void _testQuerySharedAlbumExtraShare(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
},
@ -479,11 +480,11 @@ void _testQuerySharedAlbumExtraShareOtherAdded(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccountOf(user1Account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
});
@ -545,11 +546,11 @@ void _testQuerySharedAlbumExtraUnmanagedShare(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccountOf(user1Account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
});
@ -587,7 +588,7 @@ void _testQuerySharedAlbumExtraJsonShare(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
},
tearDown: () => c.sqliteDb.close(),
@ -632,10 +633,10 @@ void _testQuerySharedAlbumNotOwnedMissingShareToOwner(String description) {
shareeRepo: MockShareeMemoryRepo([
util.buildSharee(shareWith: "user1".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
},
@ -686,10 +687,10 @@ void _testQuerySharedAlbumNotOwnedMissingManagedShare(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
},
@ -738,10 +739,10 @@ void _testQuerySharedAlbumNotOwnedMissingUnmanagedShare(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
},
@ -781,7 +782,7 @@ void _testQuerySharedAlbumNotOwnedMissingJsonShare(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
},
tearDown: () => c.sqliteDb.close(),
@ -825,10 +826,10 @@ void _testQuerySharedAlbumNotOwnedExtraManagedShare(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
},
@ -878,10 +879,10 @@ void _testQuerySharedAlbumNotOwnedExtraUnmanagedShare(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
},
@ -926,7 +927,7 @@ void _testQuerySharedAlbumNotOwnedExtraJsonShare(String description) {
util.buildSharee(shareWith: "user1".toCi()),
util.buildSharee(shareWith: "user2".toCi()),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
},
tearDown: () => c.sqliteDb.close(),

View file

@ -1,3 +1,4 @@
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
@ -6,9 +7,9 @@ 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/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/exception.dart';
import 'package:np_common/or_null.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:np_string/np_string.dart';
import 'package:test/test.dart';
@ -43,11 +44,11 @@ Future<void> _dbGet() async {
(util.AlbumBuilder.ofId(albumId: 1)).build(),
];
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(
c.sqliteDb, account, albums.map((a) => a.albumFile!));
await util.insertAlbums(c.sqliteDb, account, albums);
@ -66,11 +67,11 @@ Future<void> _dbGetNa() async {
(util.AlbumBuilder.ofId(albumId: 0)).build(),
];
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
});
final src = AlbumSqliteDbDataSource(c);
@ -91,11 +92,11 @@ Future<void> _dbGetAll() async {
(util.AlbumBuilder.ofId(albumId: 2)).build(),
];
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(
c.sqliteDb, account, albums.map((a) => a.albumFile!));
await util.insertAlbums(c.sqliteDb, account, albums);
@ -120,11 +121,11 @@ Future<void> _dbGetAllNa() async {
(util.AlbumBuilder.ofId(albumId: 2)).build(),
];
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, [albums[0].albumFile!]);
await util.insertAlbums(c.sqliteDb, account, [albums[0]]);
});
@ -154,11 +155,11 @@ Future<void> _dbUpdateExisting() async {
..addJpeg("admin/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(
c.sqliteDb, account, albums.map((a) => a.albumFile!));
await util.insertAlbums(c.sqliteDb, account, albums);
@ -202,11 +203,11 @@ Future<void> _dbUpdateNew() async {
];
final newAlbum = (util.AlbumBuilder.ofId(albumId: 1)).build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account,
[...albums.map((a) => a.albumFile!), newAlbum.albumFile!]);
await util.insertAlbums(c.sqliteDb, account, albums);
@ -234,11 +235,11 @@ Future<void> _dbUpdateShares() async {
..addJpeg("admin/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(
c.sqliteDb, account, albums.map((a) => a.albumFile!));
await util.insertAlbums(c.sqliteDb, account, albums);
@ -296,11 +297,11 @@ Future<void> _dbUpdateDeleteShares() async {
..addJpeg("admin/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(
c.sqliteDb, account, albums.map((a) => a.albumFile!));
await util.insertAlbums(c.sqliteDb, account, albums);

View file

@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:clock/clock.dart';
import 'package:intl/intl.dart';
import 'package:nc_photos/entity/album.dart';
@ -10,8 +8,8 @@ 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_descriptor.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:np_common/type.dart';
import 'package:np_db/np_db.dart';
import 'package:np_string/np_string.dart';
import 'package:test/test.dart';
@ -1956,10 +1954,6 @@ void _toAppDbJsonShares() {
});
}
String _stripJsonString(String str) {
return jsonEncode(jsonDecode(str));
}
class _NullAlbumUpgraderFactory extends AlbumUpgraderFactory {
const _NullAlbumUpgraderFactory();

View file

@ -345,17 +345,16 @@ void _upgradeV8JsonAutoNoFileId() {
}
void _upgradeV8DbNonManualCover() {
final dbObj = sql.Album(
rowId: 1,
file: 1,
final dbObj = DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "memory",
coverProviderContent: _stripJsonString("""{
coverProviderContent: {
"coverFile": {
"fdPath": "remote.php/dav/files/admin/test1.jpg",
"fdId": 1,
@ -364,23 +363,23 @@ void _upgradeV8DbNonManualCover() {
"fdIsFavorite": false,
"fdDateTime": "2020-01-02T03:04:05.678901Z"
}
}"""),
},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
);
expect(
const AlbumUpgraderV8().doDb(dbObj),
sql.Album(
rowId: 1,
file: 1,
DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "memory",
coverProviderContent: _stripJsonString("""{
coverProviderContent: {
"coverFile": {
"fdPath": "remote.php/dav/files/admin/test1.jpg",
"fdId": 1,
@ -389,47 +388,47 @@ void _upgradeV8DbNonManualCover() {
"fdIsFavorite": false,
"fdDateTime": "2020-01-02T03:04:05.678901Z"
}
}"""),
},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
),
);
}
void _upgradeV8DbManualNow() {
withClock(Clock.fixed(DateTime.utc(2020, 1, 2, 3, 4, 5)), () {
final dbObj = sql.Album(
rowId: 1,
file: 1,
final dbObj = DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "manual",
coverProviderContent: _stripJsonString("""{
coverProviderContent: {
"coverFile": {
"path": "remote.php/dav/files/admin/test1.jpg",
"fileId": 1
}
}"""),
},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
);
expect(
const AlbumUpgraderV8().doDb(dbObj),
sql.Album(
rowId: 1,
file: 1,
DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "manual",
coverProviderContent: _stripJsonString("""{
coverProviderContent: {
"coverFile": {
"fdPath": "remote.php/dav/files/admin/test1.jpg",
"fdId": 1,
@ -438,137 +437,135 @@ void _upgradeV8DbManualNow() {
"fdIsFavorite": false,
"fdDateTime": "2020-01-02T03:04:05.000Z"
}
}"""),
},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
),
);
});
}
void _upgradeV8DbManualExifTime() {
final dbObj = sql.Album(
rowId: 1,
file: 1,
final dbObj = DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "manual",
coverProviderContent: _stripJsonString("""{
coverProviderContent: {
"coverFile": {
"path": "remote.php/dav/files/admin/test1.jpg",
"fileId": 1,
"metadata": {
"exif": {
"DateTimeOriginal": "2020:01:02 03:04:05"
}
"exif": {"DateTimeOriginal": "2020:01:02 03:04:05"}
}
}
}"""),
},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
);
// dart does not provide a way to mock timezone
final dateTime = DateTime(2020, 1, 2, 3, 4, 5).toUtc().toIso8601String();
expect(
const AlbumUpgraderV8().doDb(dbObj),
sql.Album(
rowId: 1,
file: 1,
DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "manual",
coverProviderContent: _stripJsonString("""{
coverProviderContent: {
"coverFile": {
"fdPath": "remote.php/dav/files/admin/test1.jpg",
"fdId": 1,
"fdMime": null,
"fdIsArchived": false,
"fdIsFavorite": false,
"fdDateTime": "$dateTime"
"fdDateTime": dateTime,
}
}"""),
},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
),
);
}
void _upgradeV8DbAutoNull() {
final dbObj = sql.Album(
rowId: 1,
file: 1,
final dbObj = DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "auto",
coverProviderContent: "{}",
coverProviderContent: {},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
);
expect(
const AlbumUpgraderV8().doDb(dbObj),
sql.Album(
rowId: 1,
file: 1,
DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "auto",
coverProviderContent: "{}",
coverProviderContent: {},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
),
);
}
void _upgradeV8DbAutoLastModified() {
final dbObj = sql.Album(
rowId: 1,
file: 1,
final dbObj = DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "auto",
coverProviderContent: _stripJsonString("""{
coverProviderContent: {
"coverFile": {
"path": "remote.php/dav/files/admin/test1.jpg",
"fileId": 1,
"lastModified": "2020-01-02T03:04:05.000Z"
}
}"""),
},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
);
expect(
const AlbumUpgraderV8().doDb(dbObj),
sql.Album(
rowId: 1,
file: 1,
DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "auto",
coverProviderContent: _stripJsonString("""{
coverProviderContent: {
"coverFile": {
"fdPath": "remote.php/dav/files/admin/test1.jpg",
"fdId": 1,
@ -577,48 +574,49 @@ void _upgradeV8DbAutoLastModified() {
"fdIsFavorite": false,
"fdDateTime": "2020-01-02T03:04:05.000Z"
}
}"""),
},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
),
);
}
void _upgradeV8DbAutoNoFileId() {
final dbObj = sql.Album(
rowId: 1,
file: 1,
final dbObj = DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "auto",
coverProviderContent: _stripJsonString("""{
coverProviderContent: {
"coverFile": {
"path": "remote.php/dav/files/admin/test1.jpg",
"lastModified": "2020-01-02T03:04:05.000Z"
}
}"""),
},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
);
expect(
const AlbumUpgraderV8().doDb(dbObj),
sql.Album(
rowId: 1,
file: 1,
DbAlbum(
fileId: 1,
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
version: 8,
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test1",
providerType: "static",
providerContent: """{"items": []}""",
providerContent: {"items": []},
coverProviderType: "auto",
coverProviderContent: "{}",
coverProviderContent: {},
sortProviderType: "null",
sortProviderContent: "{}",
sortProviderContent: {},
shares: [],
),
);
}

View file

@ -1,9 +1,10 @@
import 'package:nc_photos/db/entity_converter.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/database.dart' as sql;
import 'package:np_collection/np_collection.dart';
import 'package:np_common/or_null.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:test/test.dart';
import '../../test_util.dart' as util;
@ -43,11 +44,11 @@ Future<void> _list() async {
..addJpeg("admin/test/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -66,11 +67,11 @@ Future<void> _listSingle() async {
final account = util.buildAccount();
final files = (util.FilesBuilder()..addDir("admin")).build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(c.sqliteDb, account, files[0], const []);
});
@ -90,11 +91,11 @@ Future<void> _removeFile() async {
..addJpeg("admin/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
});
@ -117,11 +118,11 @@ Future<void> _removeEmptyDir() async {
..addDir("admin/test"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
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 []);
@ -150,11 +151,11 @@ Future<void> _removeDir() async {
..addJpeg("admin/test/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
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]]);
@ -180,11 +181,11 @@ Future<void> _removeDirWithSubDir() async {
..addJpeg("admin/test/test2/test3.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
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]]);
@ -215,11 +216,11 @@ Future<void> _updateFileProperty() async {
..addJpeg("admin/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
});
@ -257,11 +258,11 @@ Future<void> _updateMetadata() async {
)),
);
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
});
@ -299,11 +300,11 @@ Future<void> _updateAddMetadata() async {
..addJpeg("admin/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
});
@ -345,11 +346,11 @@ Future<void> _updateDeleteMetadata() async {
)),
);
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
});

View file

@ -1,10 +1,11 @@
import 'package:nc_photos/db/entity_converter.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/file_cache_manager.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:np_collection/np_collection.dart';
import 'package:np_common/or_null.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:np_math/np_math.dart';
import 'package:test/test.dart';
@ -53,11 +54,11 @@ Future<void> _loaderNoCache() async {
.build();
final c = DiContainer(
fileRepo: MockFileMemoryRepo(files),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
});
final cacheSrc = FileSqliteDbDataSource(c);
@ -80,7 +81,7 @@ Future<void> _loaderOutdatedCache() async {
.build();
final c = DiContainer(
fileRepo: MockFileMemoryRepo(files),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
final dbFiles = [
@ -88,7 +89,7 @@ Future<void> _loaderOutdatedCache() async {
...files.slice(1),
];
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, dbFiles);
await util.insertDirRelation(
c.sqliteDb, account, dbFiles[0], dbFiles.slice(1, 3));
@ -119,11 +120,11 @@ Future<void> _loaderQueryRemoteSameEtag() async {
.build();
final c = DiContainer(
fileRepo: MockFileMemoryRepo(files),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -155,7 +156,7 @@ Future<void> _loaderQueryRemoteDiffEtag() async {
.build();
final c = DiContainer(
fileRepo: MockFileMemoryRepo(files),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
final dbFiles = [
@ -163,7 +164,7 @@ Future<void> _loaderQueryRemoteDiffEtag() async {
...files.slice(1),
];
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, dbFiles);
await util.insertDirRelation(
c.sqliteDb, account, dbFiles[0], dbFiles.slice(1, 3));
@ -193,11 +194,11 @@ Future<void> _updaterIdentical() async {
..addJpeg("admin/test/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -228,11 +229,11 @@ Future<void> _updaterNewFile() async {
.build()
.first;
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -259,11 +260,11 @@ Future<void> _updaterDeleteFile() async {
..addJpeg("admin/test/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -293,11 +294,11 @@ Future<void> _updaterDeleteDir() async {
..addJpeg("admin/test/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -331,11 +332,11 @@ Future<void> _updaterUpdateFile() async {
.build();
final newFile = files[1].copyWith(contentLength: 654);
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -369,12 +370,12 @@ Future<void> _updaterNewSharedFile() async {
user1Files
.add(files[1].copyWith(path: "remote.php/dav/files/user1/test1.jpg"));
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccountOf(user1Account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -406,12 +407,12 @@ Future<void> _updaterNewSharedDir() async {
user1Files.add(
files[3].copyWith(path: "remote.php/dav/files/user1/share/test2.jpg"));
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccountOf(user1Account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -444,12 +445,12 @@ Future<void> _updaterDeleteSharedFile() async {
user1Files
.add(files[1].copyWith(path: "remote.php/dav/files/user1/test1.jpg"));
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccountOf(user1Account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -487,12 +488,12 @@ Future<void> _updaterDeleteSharedDir() async {
user1Files.add(
files[3].copyWith(path: "remote.php/dav/files/user1/share/test2.jpg"));
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccountOf(user1Account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -529,11 +530,11 @@ Future<void> _updaterTooManyFiles() async {
}
final newFiles = newFilesBuilder.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -558,11 +559,11 @@ Future<void> _updaterMovedFileToFront() async {
..addJpeg("admin/test2/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -605,11 +606,11 @@ Future<void> _updaterMovedFileToBehind() async {
..addJpeg("admin/test1/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(
c.sqliteDb, account, files[0], files.slice(1, 3));
@ -654,11 +655,11 @@ Future<void> _emptier() async {
..addJpeg("admin/testB/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util
.insertDirRelation(c.sqliteDb, account, files[0], [files[1], files[3]]);

View file

@ -1,7 +1,7 @@
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/object_extension.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:test/test.dart';
import '../../test_util.dart' as util;
@ -18,7 +18,6 @@ void main() {
test("same server", _deleteAccountSameServer);
test("same server shared file", _deleteAccountSameServerSharedFile);
});
test("cleanUpDanglingFiles", _cleanUpDanglingFiles);
test("truncate", _truncate);
});
}
@ -29,19 +28,19 @@ void main() {
Future<void> _insertAccountFirst() async {
final account = util.buildAccount();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.use((db) async {
await db.insertAccountOf(account);
await db.insertAccounts([account.toDb()]);
});
expect(
await util.listSqliteDbServerAccounts(c.sqliteDb),
{
const util.SqlAccountWithServer(
sql.Server(rowId: 1, address: "http://example.com"),
sql.Account(rowId: 1, server: 1, userId: "admin"),
compat.Server(rowId: 1, address: "http://example.com"),
compat.Account(rowId: 1, server: 1, userId: "admin"),
),
},
);
@ -54,26 +53,26 @@ Future<void> _insertAccountSameServer() async {
final account = util.buildAccount();
final user1Account = util.buildAccount(userId: "user1");
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
});
await c.sqliteDb.use((db) async {
await db.insertAccountOf(user1Account);
await db.insertAccounts([user1Account.toDb()]);
});
expect(
await util.listSqliteDbServerAccounts(c.sqliteDb),
{
const util.SqlAccountWithServer(
sql.Server(rowId: 1, address: "http://example.com"),
sql.Account(rowId: 1, server: 1, userId: "admin"),
compat.Server(rowId: 1, address: "http://example.com"),
compat.Account(rowId: 1, server: 1, userId: "admin"),
),
const util.SqlAccountWithServer(
sql.Server(rowId: 1, address: "http://example.com"),
sql.Account(rowId: 2, server: 1, userId: "user1"),
compat.Server(rowId: 1, address: "http://example.com"),
compat.Account(rowId: 2, server: 1, userId: "user1"),
),
},
);
@ -86,22 +85,22 @@ Future<void> _insertAccountSameAccount() async {
final account = util.buildAccount();
final account2 = util.buildAccount();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
});
await c.sqliteDb.use((db) async {
await db.insertAccountOf(account2);
await db.insertAccounts([account2.toDb()]);
});
expect(
await util.listSqliteDbServerAccounts(c.sqliteDb),
{
const util.SqlAccountWithServer(
sql.Server(rowId: 1, address: "http://example.com"),
sql.Account(rowId: 1, server: 1, userId: "admin"),
compat.Server(rowId: 1, address: "http://example.com"),
compat.Account(rowId: 1, server: 1, userId: "admin"),
),
},
);
@ -119,16 +118,16 @@ Future<void> _deleteAccount() async {
..addJpeg("admin/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
await c.sqliteDb.use((db) async {
await db.deleteAccountOf(account);
await db.deleteAccount(account.toDb());
});
expect(
await util.listSqliteDbServerAccounts(c.sqliteDb),
@ -157,12 +156,12 @@ Future<void> _deleteAccountSameServer() async {
..addJpeg("user1/test2.jpg", ownerId: "user1"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccountOf(user1Account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
@ -172,14 +171,14 @@ Future<void> _deleteAccountSameServer() async {
});
await c.sqliteDb.use((db) async {
await db.deleteAccountOf(account);
await db.deleteAccount(account.toDb());
});
expect(
await util.listSqliteDbServerAccounts(c.sqliteDb),
{
const util.SqlAccountWithServer(
sql.Server(rowId: 1, address: "http://example.com"),
sql.Account(rowId: 2, server: 1, userId: "user1"),
compat.Server(rowId: 1, address: "http://example.com"),
compat.Account(rowId: 2, server: 1, userId: "user1"),
),
},
);
@ -208,12 +207,12 @@ Future<void> _deleteAccountSameServerSharedFile() async {
user1Files
.add(files[0].copyWith(path: "remote.php/dav/files/user1/test1.jpg"));
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccountOf(user1Account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]);
@ -223,14 +222,14 @@ Future<void> _deleteAccountSameServerSharedFile() async {
});
await c.sqliteDb.use((db) async {
await db.deleteAccountOf(account);
await db.deleteAccount(account.toDb());
});
expect(
await util.listSqliteDbServerAccounts(c.sqliteDb),
{
const util.SqlAccountWithServer(
sql.Server(rowId: 1, address: "http://example.com"),
sql.Account(rowId: 2, server: 1, userId: "user1"),
compat.Server(rowId: 1, address: "http://example.com"),
compat.Account(rowId: 2, server: 1, userId: "user1"),
),
},
);
@ -240,44 +239,6 @@ Future<void> _deleteAccountSameServerSharedFile() async {
);
}
/// Clean up Files without an associated entry in AccountFiles
///
/// Expect: Dangling files deleted
Future<void> _cleanUpDanglingFiles() 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 c.sqliteDb.applyFuture((db) async {
await db.into(db.files).insert(sql.FilesCompanion.insert(
server: 1,
fileId: files.length,
));
});
});
expect(
await c.sqliteDb.select(c.sqliteDb.files).map((f) => f.fileId).get(),
[0, 1, 2],
);
await c.sqliteDb.use((db) async {
await db.cleanUpDanglingFiles();
});
expect(
await c.sqliteDb.select(c.sqliteDb.files).map((f) => f.fileId).get(),
[0, 1],
);
}
/// Truncate the db
///
/// Expect: All tables emptied;
@ -289,11 +250,11 @@ Future<void> _truncate() async {
..addJpeg("admin/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});

View file

@ -0,0 +1,496 @@
part of 'test_util.dart';
extension DiContainerExtension on DiContainer {
// ignore: deprecated_member_use
compat.SqliteDb get sqliteDb => (npDb as NpDbSqlite).compatDb;
}
class _ByAccount {
const _ByAccount.sql(compat.Account account) : this._(sqlAccount: account);
// const _ByAccount.app(Account account) : this._(appAccount: account);
const _ByAccount._({
this.sqlAccount,
this.appAccount,
}) : assert((sqlAccount != null) != (appAccount != null));
final compat.Account? sqlAccount;
final Account? appAccount;
}
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 on compat.SqliteDb {
/// Query AccountFiles, Accounts and Files row ID by app File
///
/// Only one of [sqlAccount] and [appAccount] must be passed
Future<_AccountFileRowIds?> accountFileRowIdsOfOrNull(
FileDescriptor file, {
compat.Account? sqlAccount,
Account? appAccount,
}) {
assert((sqlAccount != null) != (appAccount != null));
final query = queryFiles().let((q) {
q.setQueryMode(_FilesQueryMode.expression, expressions: [
accountFiles.rowId,
accountFiles.account,
accountFiles.file,
]);
if (sqlAccount != null) {
q.setSqlAccount(sqlAccount);
} else {
q.setAppAccount(appAccount!);
}
try {
q.byFileId(file.fdId);
} catch (_) {
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(
FileDescriptor file, {
compat.Account? sqlAccount,
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(
_ByAccount account, Iterable<int> fileIds) {
final query = queryFiles().let((q) {
q.setQueryMode(_FilesQueryMode.expression, expressions: [
accountFiles.rowId,
accountFiles.account,
accountFiles.file,
files.fileId,
]);
if (account.sqlAccount != null) {
q.setSqlAccount(account.sqlAccount!);
} else {
q.setAppAccount(account.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();
}
_FilesQueryBuilder queryFiles() => _FilesQueryBuilder(this);
}
class _SqliteAlbumConverter {
static Album fromSql(
compat.Album album, File albumFile, List<compat.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(),
// replace with the original etag when this album was cached
albumFile: albumFile.copyWith(etag: OrNull(album.fileEtag)),
savedVersion: album.version,
);
}
static compat.CompleteAlbumCompanion toSql(
Album album, int albumFileRowId, String albumFileEtag) {
final providerJson = album.provider.toJson();
final coverProviderJson = album.coverProvider.toJson();
final sortProviderJson = album.sortProvider.toJson();
final dbAlbum = compat.AlbumsCompanion.insert(
file: albumFileRowId,
fileEtag: sql.Value(albumFileEtag),
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) => compat.AlbumSharesCompanion(
userId: sql.Value(s.userId.toCaseInsensitiveString()),
displayName: sql.Value(s.displayName),
sharedAt: sql.Value(s.sharedAt),
))
.toList();
return compat.CompleteAlbumCompanion(dbAlbum, 1, dbAlbumShares ?? []);
}
}
class _SqliteFileConverter {
static File fromSql(String userId, compat.CompleteFile f) {
final metadata = f.image?.let((obj) => Metadata(
lastUpdated: obj.lastUpdated,
fileEtag: obj.fileEtag,
imageWidth: obj.width,
imageHeight: obj.height,
exif: obj.exifRaw?.let((e) => Exif.fromJson(jsonDecode(e))),
));
final location = f.imageLocation?.let((obj) => ImageLocation(
version: obj.version,
name: obj.name,
latitude: obj.latitude,
longitude: obj.longitude,
countryCode: obj.countryCode,
admin1: obj.admin1,
admin2: obj.admin2,
));
return File(
path: "remote.php/dav/files/$userId/${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(),
ownerDisplayName: f.file.ownerDisplayName,
trashbinFilename: f.trash?.filename,
trashbinOriginalLocation: f.trash?.originalLocation,
trashbinDeletionTime: f.trash?.deletionTime,
metadata: metadata,
isArchived: f.accountFile.isArchived,
overrideDateTime: f.accountFile.overrideDateTime,
location: location,
);
}
static compat.CompleteFileCompanion toSql(
compat.Account? account, File file) {
final dbFile = compat.FilesCompanion(
server: account == null
? const sql.Value.absent()
: sql.Value(account.server),
fileId: sql.Value(file.fileId!),
contentLength: sql.Value(file.contentLength),
contentType: sql.Value(file.contentType),
etag: sql.Value(file.etag),
lastModified: sql.Value(file.lastModified),
isCollection: sql.Value(file.isCollection),
usedBytes: sql.Value(file.usedBytes),
hasPreview: sql.Value(file.hasPreview),
ownerId: sql.Value(file.ownerId!.toCaseInsensitiveString()),
ownerDisplayName: sql.Value(file.ownerDisplayName),
);
final dbAccountFile = compat.AccountFilesCompanion(
account:
account == null ? const sql.Value.absent() : sql.Value(account.rowId),
relativePath: sql.Value(file.strippedPathWithEmpty),
isFavorite: sql.Value(file.isFavorite),
isArchived: sql.Value(file.isArchived),
overrideDateTime: sql.Value(file.overrideDateTime),
bestDateTime: sql.Value(file.bestDateTime),
);
final dbImage = file.metadata?.let((m) => compat.ImagesCompanion.insert(
lastUpdated: m.lastUpdated,
fileEtag: sql.Value(m.fileEtag),
width: sql.Value(m.imageWidth),
height: sql.Value(m.imageHeight),
exifRaw: sql.Value(m.exif?.toJson().let((j) => jsonEncode(j))),
dateTimeOriginal: sql.Value(m.exif?.dateTimeOriginal),
));
final dbImageLocation =
file.location?.let((l) => compat.ImageLocationsCompanion.insert(
version: l.version,
name: sql.Value(l.name),
latitude: sql.Value(l.latitude),
longitude: sql.Value(l.longitude),
countryCode: sql.Value(l.countryCode),
admin1: sql.Value(l.admin1),
admin2: sql.Value(l.admin2),
));
final dbTrash = file.trashbinDeletionTime == null
? null
: compat.TrashesCompanion.insert(
filename: file.trashbinFilename!,
originalLocation: file.trashbinOriginalLocation!,
deletionTime: file.trashbinDeletionTime!,
);
return compat.CompleteFileCompanion(
dbFile, dbAccountFile, dbImage, dbImageLocation, dbTrash);
}
}
enum _FilesQueryMode {
file,
completeFile,
expression,
}
typedef _FilesQueryRelativePathBuilder = sql.Expression<bool> Function(
sql.GeneratedColumn<String> relativePath);
/// 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<sql.Expression>? expressions,
}) {
assert((mode == _FilesQueryMode.expression) !=
(expressions?.isEmpty != false));
_queryMode = mode;
_selectExpressions = expressions;
}
void setSqlAccount(compat.Account account) {
assert(_appAccount == null);
_sqlAccount = account;
}
void setAppAccount(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 byOrRelativePath(String path) {
_byOrRelativePathBuilder((relativePath) => relativePath.equals(path));
}
void byOrRelativePathPattern(String pattern) {
_byOrRelativePathBuilder((relativePath) => relativePath.like(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;
}
void byLocation(String location) {
_byLocation = location;
}
sql.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([
sql.innerJoin(
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId),
useColumns: _queryMode == _FilesQueryMode.completeFile),
if (_appAccount != null) ...[
sql.innerJoin(
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account),
useColumns: false),
sql.innerJoin(
db.servers, db.servers.rowId.equalsExp(db.accounts.server),
useColumns: false),
],
if (_byDirRowId != null)
sql.innerJoin(db.dirFiles, db.dirFiles.child.equalsExp(db.files.rowId),
useColumns: false),
if (_queryMode == _FilesQueryMode.completeFile) ...[
sql.leftOuterJoin(
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
sql.leftOuterJoin(db.imageLocations,
db.imageLocations.accountFile.equalsExp(db.accountFiles.rowId)),
sql.leftOuterJoin(
db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
],
]) as sql.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!.userId.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 (_byOrRelativePathBuilders?.isNotEmpty == true) {
final expression = _byOrRelativePathBuilders!
.sublist(1)
.fold<sql.Expression<bool>>(
_byOrRelativePathBuilders![0](db.accountFiles.relativePath),
(previousValue, builder) =>
previousValue | builder(db.accountFiles.relativePath));
query.where(expression);
}
if (_byMimePatterns?.isNotEmpty == true) {
final expression = _byMimePatterns!.sublist(1).fold<sql.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!));
}
if (_byLocation != null) {
var clause = db.imageLocations.name.like(_byLocation!) |
db.imageLocations.admin1.like(_byLocation!) |
db.imageLocations.admin2.like(_byLocation!);
final countryCode = nameToAlpha2Code(_byLocation!.toCi());
if (countryCode != null) {
clause = clause | db.imageLocations.countryCode.equals(countryCode);
} else if (_byLocation!.length == 2 &&
alpha2CodeToName(_byLocation!.toUpperCase()) != null) {
clause = clause |
db.imageLocations.countryCode.equals(_byLocation!.toUpperCase());
}
query.where(clause);
}
return query;
}
void _byOrRelativePathBuilder(_FilesQueryRelativePathBuilder builder) {
(_byOrRelativePathBuilders ??= []).add(builder);
}
final compat.SqliteDb db;
_FilesQueryMode _queryMode = _FilesQueryMode.file;
Iterable<sql.Expression>? _selectExpressions;
compat.Account? _sqlAccount;
Account? _appAccount;
bool _isAccountless = false;
int? _byRowId;
int? _byFileId;
Iterable<int>? _byFileIds;
String? _byRelativePath;
List<_FilesQueryRelativePathBuilder>? _byOrRelativePathBuilders;
List<String>? _byMimePatterns;
bool? _byFavorite;
int? _byDirRowId;
int? _byServerRowId;
String? _byLocation;
}

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' as sql;
import 'package:drift/native.dart' as sql;
@ -5,22 +7,31 @@ import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.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/item.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/file_descriptor.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:np_async/np_async.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_common/object_util.dart';
import 'package:np_common/or_null.dart';
import 'package:np_db/np_db.dart';
import 'package:np_db_sqlite/np_db_sqlite.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:np_geocoder/np_geocoder.dart';
import 'package:np_string/np_string.dart';
import 'package:tuple/tuple.dart';
part 'test_compat_util.dart';
class FilesBuilder {
FilesBuilder({
int initialFileId = 0,
@ -271,8 +282,8 @@ class SqlAccountWithServer with EquatableMixin {
@override
get props => [server, account];
final sql.Server server;
final sql.Account account;
final compat.Server server;
final compat.Account account;
}
void initLog() {
@ -436,18 +447,22 @@ Sharee buildSharee({
shareWith: shareWith,
);
sql.SqliteDb buildTestDb() {
sql.driftRuntimeOptions.debugPrint = _debugPrintSql;
return sql.SqliteDb(
executor: sql.NativeDatabase.memory(
logStatements: true,
NpDb buildTestDb() {
final db = NpDbSqlite();
db.initWithDb(
db: compat.SqliteDb(
executor: sql.NativeDatabase.memory(
logStatements: true,
),
),
);
sql.driftRuntimeOptions.debugPrint = _debugPrintSql;
return db;
}
Future<void> insertFiles(
sql.SqliteDb db, Account account, Iterable<File> files) async {
final dbAccount = await db.accountOf(account);
compat.SqliteDb db, Account account, Iterable<File> files) async {
final dbAccount = await db.accountOf(compat.ByAccount.db(account.toDb()));
for (final f in files) {
final sharedQuery = db.selectOnly(db.files).join([
sql.innerJoin(
@ -459,7 +474,7 @@ Future<void> insertFiles(
..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);
final insert = _SqliteFileConverter.toSql(dbAccount, f);
if (rowId == null) {
final dbFile = await db.into(db.files).insertReturning(insert.file);
rowId = dbFile.rowId;
@ -483,18 +498,18 @@ Future<void> insertFiles(
}
}
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(
sql.ByAccount.sql(dbAccount), [dir.fileId!]))
Future<void> insertDirRelation(compat.SqliteDb db, Account account, File dir,
Iterable<File> children) async {
final dbAccount = await db.accountOf(compat.ByAccount.db(account.toDb()));
final dirRowIds = (await db
.accountFileRowIdsByFileIds(_ByAccount.sql(dbAccount), [dir.fileId!]))
.first;
final childRowIds = await db.accountFileRowIdsByFileIds(
sql.ByAccount.sql(dbAccount), [dir, ...children].map((f) => f.fileId!));
_ByAccount.sql(dbAccount), [dir, ...children].map((f) => f.fileId!));
await db.batch((batch) {
batch.insertAll(
db.dirFiles,
childRowIds.map((c) => sql.DirFilesCompanion.insert(
childRowIds.map((c) => compat.DirFilesCompanion.insert(
dir: dirRowIds.fileRowId,
child: c.fileRowId,
)),
@ -503,15 +518,15 @@ Future<void> insertDirRelation(
}
Future<void> insertAlbums(
sql.SqliteDb db, Account account, Iterable<Album> albums) async {
final dbAccount = await db.accountOf(account);
compat.SqliteDb db, Account account, Iterable<Album> albums) async {
final dbAccount = await db.accountOf(compat.ByAccount.db(account.toDb()));
for (final a in albums) {
final rowIds =
await db.accountFileRowIdsOf(a.albumFile!, sqlAccount: dbAccount);
final insert =
SqliteAlbumConverter.toSql(a, rowIds.fileRowId, a.albumFile!.etag!);
_SqliteAlbumConverter.toSql(a, rowIds.fileRowId, a.albumFile!.etag!);
final dbAlbum = await db.into(db.albums).insertReturning(insert.album);
for (final s in insert.albumShares) {
for (final s in insert.shares) {
await db
.into(db.albumShares)
.insert(s.copyWith(album: sql.Value(dbAlbum.rowId)));
@ -519,7 +534,7 @@ Future<void> insertAlbums(
}
}
Future<Set<File>> listSqliteDbFiles(sql.SqliteDb db) async {
Future<Set<File>> listSqliteDbFiles(compat.SqliteDb db) async {
final query = db.select(db.files).join([
sql.innerJoin(
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),
@ -532,9 +547,9 @@ Future<Set<File>> listSqliteDbFiles(sql.SqliteDb db) async {
sql.leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
]);
return (await query
.map((r) => SqliteFileConverter.fromSql(
.map((r) => _SqliteFileConverter.fromSql(
r.readTable(db.accounts).userId,
sql.CompleteFile(
compat.CompleteFile(
r.readTable(db.files),
r.readTable(db.accountFiles),
r.readTableOrNull(db.images),
@ -546,7 +561,7 @@ Future<Set<File>> listSqliteDbFiles(sql.SqliteDb db) async {
.toSet();
}
Future<Map<File, Set<File>>> listSqliteDbDirs(sql.SqliteDb db) async {
Future<Map<File, Set<File>>> listSqliteDbDirs(compat.SqliteDb db) async {
final query = db.select(db.files).join([
sql.innerJoin(
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),
@ -559,7 +574,7 @@ Future<Map<File, Set<File>>> listSqliteDbDirs(sql.SqliteDb db) async {
sql.leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
]);
final fileMap = Map.fromEntries(await query.map((r) {
final f = sql.CompleteFile(
final f = compat.CompleteFile(
r.readTable(db.files),
r.readTable(db.accountFiles),
r.readTableOrNull(db.images),
@ -568,7 +583,7 @@ Future<Map<File, Set<File>>> listSqliteDbDirs(sql.SqliteDb db) async {
);
return MapEntry(
f.file.rowId,
SqliteFileConverter.fromSql(r.readTable(db.accounts).userId, f),
_SqliteFileConverter.fromSql(r.readTable(db.accounts).userId, f),
);
}).get());
@ -581,7 +596,7 @@ Future<Map<File, Set<File>>> listSqliteDbDirs(sql.SqliteDb db) async {
return result;
}
Future<Set<Album>> listSqliteDbAlbums(sql.SqliteDb db) async {
Future<Set<Album>> listSqliteDbAlbums(compat.SqliteDb db) async {
final albumQuery = db.select(db.albums).join([
sql.innerJoin(db.files, db.files.rowId.equalsExp(db.albums.file)),
sql.innerJoin(
@ -590,9 +605,9 @@ Future<Set<Album>> listSqliteDbAlbums(sql.SqliteDb db) async {
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account)),
]);
final albums = await albumQuery.map((r) {
final albumFile = SqliteFileConverter.fromSql(
final albumFile = _SqliteFileConverter.fromSql(
r.readTable(db.accounts).userId,
sql.CompleteFile(
compat.CompleteFile(
r.readTable(db.files),
r.readTable(db.accountFiles),
null,
@ -602,7 +617,7 @@ Future<Set<Album>> listSqliteDbAlbums(sql.SqliteDb db) async {
);
return Tuple2(
r.read(db.albums.rowId)!,
SqliteAlbumConverter.fromSql(r.readTable(db.albums), albumFile, []),
_SqliteAlbumConverter.fromSql(r.readTable(db.albums), albumFile, []),
);
}).get();
@ -627,7 +642,7 @@ Future<Set<Album>> listSqliteDbAlbums(sql.SqliteDb db) async {
}
Future<Set<SqlAccountWithServer>> listSqliteDbServerAccounts(
sql.SqliteDb db) async {
compat.SqliteDb db) async {
final query = db.select(db.servers).join([
sql.leftOuterJoin(
db.accounts, db.accounts.server.equalsExp(db.servers.rowId)),

View file

@ -1,6 +1,7 @@
import 'package:clock/clock.dart';
import 'package:event_bus/event_bus.dart';
import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
@ -10,8 +11,8 @@ import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/pref/provider/memory.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/use_case/album/add_file_to_album.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:np_string/np_string.dart';
import 'package:test/test.dart';
@ -50,12 +51,12 @@ Future<void> _addFile() async {
fileRepo: MockFileMemoryRepo(),
albumRepo: MockAlbumMemoryRepo([album]),
shareRepo: MockShareRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
pref: Pref.scoped(PrefMemoryProvider()),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, [file]);
});
@ -116,12 +117,12 @@ Future<void> _addExistingFile() async {
fileRepo: MockFileMemoryRepo(),
albumRepo: MockAlbumMemoryRepo([album]),
shareRepo: MockShareRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
pref: Pref.scoped(PrefMemoryProvider()),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -194,13 +195,13 @@ Future<void> _addExistingSharedFile() async {
fileRepo: MockFileMemoryRepo(),
albumRepo: MockAlbumMemoryRepo([album]),
shareRepo: MockShareRepo(),
sqliteDb: util.buildTestDb(),
npDb: 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 c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
});
@ -252,14 +253,14 @@ Future<void> _addFileToSharedAlbumOwned() async {
shareRepo: MockShareMemoryRepo([
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
]),
sqliteDb: util.buildTestDb(),
npDb: 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.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, [file]);
});
@ -295,14 +296,14 @@ Future<void> _addFileOwnedByUserToSharedAlbumOwned() async {
shareRepo: MockShareMemoryRepo([
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
]),
sqliteDb: util.buildTestDb(),
npDb: 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.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, [file]);
});
@ -344,14 +345,14 @@ Future<void> _addFileToMultiuserSharedAlbumNotOwned() async {
util.buildShare(
id: "1", file: albumFile, uidOwner: "user1", shareWith: "user2"),
]),
sqliteDb: util.buildTestDb(),
npDb: 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.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, [file]);
});

View file

@ -1,167 +1,167 @@
import 'package:drift/drift.dart' as sql;
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/face_recognition_person.dart';
import 'package:nc_photos/entity/face_recognition_person/data_source.dart';
import 'package:nc_photos/entity/face_recognition_person/repo.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/use_case/face_recognition_person/sync_face_recognition_person.dart';
import 'package:test/test.dart';
import 'package:tuple/tuple.dart';
// import 'package:drift/drift.dart' as sql;
// import 'package:nc_photos/di_container.dart';
// import 'package:nc_photos/entity/face_recognition_person.dart';
// import 'package:nc_photos/entity/face_recognition_person/data_source.dart';
// import 'package:nc_photos/entity/face_recognition_person/repo.dart';
// import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
// import 'package:nc_photos/entity/sqlite/type_converter.dart';
// import 'package:nc_photos/use_case/face_recognition_person/sync_face_recognition_person.dart';
// import 'package:test/test.dart';
// import 'package:tuple/tuple.dart';
import '../../mock_type.dart';
import '../../test_util.dart' as util;
// import '../../mock_type.dart';
// import '../../test_util.dart' as util;
void main() {
group("SyncFaceRecognitionPerson", () {
test("new", _new);
test("remove", _remove);
test("update", _update);
});
}
// void main() {
// group("SyncFaceRecognitionPerson", () {
// test("new", _new);
// test("remove", _remove);
// test("update", _update);
// });
// }
/// Sync with remote where there are new persons
///
/// Remote: [test1, test2, test3]
/// Local: [test1]
/// Expect: [test1, test2, test3]
Future<void> _new() async {
final account = util.buildAccount();
final c = DiContainer.late();
c.sqliteDb = util.buildTestDb();
addTearDown(() => c.sqliteDb.close());
c.faceRecognitionPersonRepoRemote = MockFaceRecognitionPersonMemoryRepo({
account.id: [
const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
const FaceRecognitionPerson(name: "test2", thumbFaceId: 2, count: 10),
const FaceRecognitionPerson(name: "test3", thumbFaceId: 3, count: 100),
],
});
c.faceRecognitionPersonRepoLocal = BasicFaceRecognitionPersonRepo(
FaceRecognitionPersonSqliteDbDataSource(c.sqliteDb));
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.batch((batch) {
batch.insert(
c.sqliteDb.faceRecognitionPersons,
sql.FaceRecognitionPersonsCompanion.insert(
account: 1, name: "test1", thumbFaceId: 1, count: 1),
);
});
});
// /// Sync with remote where there are new persons
// ///
// /// Remote: [test1, test2, test3]
// /// Local: [test1]
// /// Expect: [test1, test2, test3]
// Future<void> _new() async {
// final account = util.buildAccount();
// final c = DiContainer.late();
// c.sqliteDb = util.buildTestDb();
// addTearDown(() => c.sqliteDb.close());
// c.faceRecognitionPersonRepoRemote = MockFaceRecognitionPersonMemoryRepo({
// account.id: [
// const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
// const FaceRecognitionPerson(name: "test2", thumbFaceId: 2, count: 10),
// const FaceRecognitionPerson(name: "test3", thumbFaceId: 3, count: 100),
// ],
// });
// c.faceRecognitionPersonRepoLocal = BasicFaceRecognitionPersonRepo(
// FaceRecognitionPersonSqliteDbDataSource(c.sqliteDb));
// await c.sqliteDb.transaction(() async {
// await c.sqliteDb.insertAccounts([account.toDb()]);
// await c.sqliteDb.batch((batch) {
// batch.insert(
// c.sqliteDb.faceRecognitionPersons,
// sql.FaceRecognitionPersonsCompanion.insert(
// account: 1, name: "test1", thumbFaceId: 1, count: 1),
// );
// });
// });
await SyncFaceRecognitionPerson(c)(account);
expect(
await _listSqliteDbPersons(c.sqliteDb),
{
account.userId.toCaseInsensitiveString(): {
const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
const FaceRecognitionPerson(name: "test2", thumbFaceId: 2, count: 10),
const FaceRecognitionPerson(name: "test3", thumbFaceId: 3, count: 100),
},
},
);
}
// await SyncFaceRecognitionPerson(c)(account);
// expect(
// await _listSqliteDbPersons(c.sqliteDb),
// {
// account.userId.toCaseInsensitiveString(): {
// const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
// const FaceRecognitionPerson(name: "test2", thumbFaceId: 2, count: 10),
// const FaceRecognitionPerson(name: "test3", thumbFaceId: 3, count: 100),
// },
// },
// );
// }
/// Sync with remote where there are removed persons
///
/// Remote: [test1]
/// Local: [test1, test2, test3]
/// Expect: [test1]
Future<void> _remove() async {
final account = util.buildAccount();
final c = DiContainer.late();
c.sqliteDb = util.buildTestDb();
addTearDown(() => c.sqliteDb.close());
c.faceRecognitionPersonRepoRemote = MockFaceRecognitionPersonMemoryRepo({
account.id: [
const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
],
});
c.faceRecognitionPersonRepoLocal = BasicFaceRecognitionPersonRepo(
FaceRecognitionPersonSqliteDbDataSource(c.sqliteDb));
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.batch((batch) {
batch.insertAll(c.sqliteDb.faceRecognitionPersons, [
sql.FaceRecognitionPersonsCompanion.insert(
account: 1, name: "test1", thumbFaceId: 1, count: 1),
sql.FaceRecognitionPersonsCompanion.insert(
account: 1, name: "test2", thumbFaceId: 2, count: 10),
sql.FaceRecognitionPersonsCompanion.insert(
account: 1, name: "test3", thumbFaceId: 3, count: 100),
]);
});
});
// /// Sync with remote where there are removed persons
// ///
// /// Remote: [test1]
// /// Local: [test1, test2, test3]
// /// Expect: [test1]
// Future<void> _remove() async {
// final account = util.buildAccount();
// final c = DiContainer.late();
// c.sqliteDb = util.buildTestDb();
// addTearDown(() => c.sqliteDb.close());
// c.faceRecognitionPersonRepoRemote = MockFaceRecognitionPersonMemoryRepo({
// account.id: [
// const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
// ],
// });
// c.faceRecognitionPersonRepoLocal = BasicFaceRecognitionPersonRepo(
// FaceRecognitionPersonSqliteDbDataSource(c.sqliteDb));
// await c.sqliteDb.transaction(() async {
// await c.sqliteDb.insertAccounts([account.toDb()]);
// await c.sqliteDb.batch((batch) {
// batch.insertAll(c.sqliteDb.faceRecognitionPersons, [
// sql.FaceRecognitionPersonsCompanion.insert(
// account: 1, name: "test1", thumbFaceId: 1, count: 1),
// sql.FaceRecognitionPersonsCompanion.insert(
// account: 1, name: "test2", thumbFaceId: 2, count: 10),
// sql.FaceRecognitionPersonsCompanion.insert(
// account: 1, name: "test3", thumbFaceId: 3, count: 100),
// ]);
// });
// });
await SyncFaceRecognitionPerson(c)(account);
expect(
await _listSqliteDbPersons(c.sqliteDb),
{
account.userId.toCaseInsensitiveString(): {
const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
},
},
);
}
// await SyncFaceRecognitionPerson(c)(account);
// expect(
// await _listSqliteDbPersons(c.sqliteDb),
// {
// account.userId.toCaseInsensitiveString(): {
// const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
// },
// },
// );
// }
/// Sync with remote where there are updated persons (i.e, same name, different
/// properties)
///
/// Remote: [test1, test2 (face: 3)]
/// Local: [test1, test2 (face: 2)]
/// Expect: [test1, test2 (face: 3)]
Future<void> _update() async {
final account = util.buildAccount();
final c = DiContainer.late();
c.sqliteDb = util.buildTestDb();
addTearDown(() => c.sqliteDb.close());
c.faceRecognitionPersonRepoRemote = MockFaceRecognitionPersonMemoryRepo({
account.id: [
const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
const FaceRecognitionPerson(name: "test2", thumbFaceId: 3, count: 10),
],
});
c.faceRecognitionPersonRepoLocal = BasicFaceRecognitionPersonRepo(
FaceRecognitionPersonSqliteDbDataSource(c.sqliteDb));
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.batch((batch) {
batch.insertAll(c.sqliteDb.faceRecognitionPersons, [
sql.FaceRecognitionPersonsCompanion.insert(
account: 1, name: "test1", thumbFaceId: 1, count: 1),
sql.FaceRecognitionPersonsCompanion.insert(
account: 1, name: "test2", thumbFaceId: 2, count: 10),
]);
});
});
// /// Sync with remote where there are updated persons (i.e, same name, different
// /// properties)
// ///
// /// Remote: [test1, test2 (face: 3)]
// /// Local: [test1, test2 (face: 2)]
// /// Expect: [test1, test2 (face: 3)]
// Future<void> _update() async {
// final account = util.buildAccount();
// final c = DiContainer.late();
// c.sqliteDb = util.buildTestDb();
// addTearDown(() => c.sqliteDb.close());
// c.faceRecognitionPersonRepoRemote = MockFaceRecognitionPersonMemoryRepo({
// account.id: [
// const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
// const FaceRecognitionPerson(name: "test2", thumbFaceId: 3, count: 10),
// ],
// });
// c.faceRecognitionPersonRepoLocal = BasicFaceRecognitionPersonRepo(
// FaceRecognitionPersonSqliteDbDataSource(c.sqliteDb));
// await c.sqliteDb.transaction(() async {
// await c.sqliteDb.insertAccounts([account.toDb()]);
// await c.sqliteDb.batch((batch) {
// batch.insertAll(c.sqliteDb.faceRecognitionPersons, [
// sql.FaceRecognitionPersonsCompanion.insert(
// account: 1, name: "test1", thumbFaceId: 1, count: 1),
// sql.FaceRecognitionPersonsCompanion.insert(
// account: 1, name: "test2", thumbFaceId: 2, count: 10),
// ]);
// });
// });
await SyncFaceRecognitionPerson(c)(account);
expect(
await _listSqliteDbPersons(c.sqliteDb),
{
account.userId.toCaseInsensitiveString(): {
const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
const FaceRecognitionPerson(name: "test2", thumbFaceId: 3, count: 10),
},
},
);
}
// await SyncFaceRecognitionPerson(c)(account);
// expect(
// await _listSqliteDbPersons(c.sqliteDb),
// {
// account.userId.toCaseInsensitiveString(): {
// const FaceRecognitionPerson(name: "test1", thumbFaceId: 1, count: 1),
// const FaceRecognitionPerson(name: "test2", thumbFaceId: 3, count: 10),
// },
// },
// );
// }
Future<Map<String, Set<FaceRecognitionPerson>>> _listSqliteDbPersons(
sql.SqliteDb db) async {
final query = db.select(db.faceRecognitionPersons).join([
sql.innerJoin(db.accounts,
db.accounts.rowId.equalsExp(db.faceRecognitionPersons.account)),
]);
final result = await query
.map((r) => Tuple2(
r.readTable(db.accounts), r.readTable(db.faceRecognitionPersons)))
.get();
final product = <String, Set<FaceRecognitionPerson>>{};
for (final r in result) {
(product[r.item1.userId] ??= {})
.add(SqliteFaceRecognitionPersonConverter.fromSql(r.item2));
}
return product;
}
// Future<Map<String, Set<FaceRecognitionPerson>>> _listSqliteDbPersons(
// sql.SqliteDb db) async {
// final query = db.select(db.faceRecognitionPersons).join([
// sql.innerJoin(db.accounts,
// db.accounts.rowId.equalsExp(db.faceRecognitionPersons.account)),
// ]);
// final result = await query
// .map((r) => Tuple2(
// r.readTable(db.accounts), r.readTable(db.faceRecognitionPersons)))
// .get();
// final product = <String, Set<FaceRecognitionPerson>>{};
// for (final r in result) {
// (product[r.item1.userId] ??= {})
// .add(SqliteFaceRecognitionPersonConverter.fromSql(r.item2));
// }
// return product;
// }

View file

@ -1,6 +1,7 @@
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/use_case/find_file.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:test/test.dart';
import '../test_util.dart' as util;
@ -22,11 +23,11 @@ Future<void> _findFile() async {
..addJpeg("admin/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -40,11 +41,11 @@ Future<void> _findMissingFile() async {
final account = util.buildAccount();
final files = (util.FilesBuilder()..addJpeg("admin/test1.jpg")).build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});

View file

@ -1,9 +1,10 @@
import 'package:clock/clock.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:test/test.dart';
import '../test_util.dart' as util;
@ -27,11 +28,11 @@ Future<void> _one() async {
..addJpeg("admin/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -59,11 +60,11 @@ Future<void> _multiple() async {
..addJpeg("admin/test6.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -87,11 +88,11 @@ Future<void> _missing() async {
..addJpeg("admin/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});

View file

@ -1,7 +1,8 @@
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/use_case/list_location_group.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:test/test.dart';
import '../test_util.dart' as util;
@ -23,11 +24,11 @@ void main() {
Future<void> _empty() async {
final account = util.buildAccount();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
});
final result = await ListLocationGroup(c)(account);
@ -48,11 +49,11 @@ Future<void> _noLocation() async {
..addJpeg("admin/test1.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -90,11 +91,11 @@ Future<void> _nFile1Location() async {
)))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -153,11 +154,11 @@ Future<void> _nFileNLocation() async {
)))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -216,6 +217,13 @@ Future<void> _multipleRoots() async {
countryCode: "AD",
))
..addJpeg("admin/test2/test4.jpg",
location: const ImageLocation(
name: "Some place",
latitude: 1.2,
longitude: 3.4,
countryCode: "AD",
))
..addJpeg("admin/test3/test5.jpg",
location: const ImageLocation(
name: "Some place",
latitude: 1.2,
@ -224,11 +232,11 @@ Future<void> _multipleRoots() async {
)))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});

View file

@ -1,10 +1,11 @@
import 'package:event_bus/event_bus.dart';
import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/pref/provider/memory.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/use_case/album/remove_album.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:test/test.dart';
import '../mock_type.dart';
@ -36,7 +37,7 @@ Future<void> _removeAlbum() async {
albumRepo: MockAlbumMemoryRepo([album1, album2]),
fileRepo: MockFileMemoryRepo([albumFile1, albumFile2]),
shareRepo: MockShareRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
pref: Pref.scoped(PrefMemoryProvider()),
);
addTearDown(() => c.sqliteDb.close());
@ -67,7 +68,7 @@ Future<void> _removeSharedAlbum() async {
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
pref: Pref.scoped(PrefMemoryProvider({
"isLabEnableSharedAlbum": true,
})),
@ -109,7 +110,7 @@ Future<void> _removeSharedAlbumFileInOtherAlbum() async {
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
util.buildShare(id: "2", file: albumFiles[1], shareWith: "user1"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
pref: Pref.scoped(PrefMemoryProvider({
"isLabEnableSharedAlbum": true,
})),
@ -150,15 +151,15 @@ Future<void> _removeSharedAlbumResyncedFile() async {
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
]),
sqliteDb: util.buildTestDb(),
npDb: 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 c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertFiles(c.sqliteDb, user1Account, user1Files);

View file

@ -1,13 +1,14 @@
import 'package:event_bus/event_bus.dart';
import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/db/entity_converter.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/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/use_case/album/remove_from_album.dart';
import 'package:np_common/or_null.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:test/test.dart';
import '../mock_type.dart';
@ -52,11 +53,11 @@ Future<void> _removeLastFile() async {
albumRepo: MockAlbumMemoryRepo([album]),
fileRepo: MockFileMemoryRepo([albumFile, file1]),
shareRepo: MockShareRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -103,11 +104,11 @@ Future<void> _remove1OfNFiles() async {
albumRepo: MockAlbumMemoryRepo([album]),
fileRepo: MockFileMemoryRepo([albumFile, ...files]),
shareRepo: MockShareRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -160,11 +161,11 @@ Future<void> _removeLatestOfNFiles() async {
albumRepo: MockAlbumMemoryRepo([album]),
fileRepo: MockFileMemoryRepo([albumFile, ...files]),
shareRepo: MockShareRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -214,11 +215,11 @@ Future<void> _removeManualCoverFile() async {
albumRepo: MockAlbumMemoryRepo([album]),
fileRepo: MockFileMemoryRepo([albumFile, ...files]),
shareRepo: MockShareRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -268,11 +269,11 @@ Future<void> _removeFromSharedAlbumOwned() async {
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
util.buildShare(id: "1", file: file1, shareWith: "user1"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -314,12 +315,12 @@ Future<void> _removeFromSharedAlbumOwnedWithOtherShare() async {
util.buildShare(
id: "3", uidOwner: "user1", file: file1, shareWith: "user2"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccountOf(user1Account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, user1Account, files);
});
@ -362,11 +363,11 @@ Future<void> _removeFromSharedAlbumOwnedLeaveExtraShare() async {
util.buildShare(id: "1", file: file1, shareWith: "user1"),
util.buildShare(id: "2", file: file1, shareWith: "user2"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -411,11 +412,11 @@ Future<void> _removeFromSharedAlbumOwnedFileInOtherAlbum() async {
util.buildShare(id: "2", file: files[0], shareWith: "user2"),
util.buildShare(id: "3", file: album2File, shareWith: "user1"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -457,11 +458,11 @@ Future<void> _removeFromSharedAlbumNotOwned() async {
util.buildShare(id: "2", file: file1, shareWith: "user1"),
util.buildShare(id: "3", file: file1, shareWith: "user2"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -507,11 +508,11 @@ Future<void> _removeFromSharedAlbumNotOwnedWithOwnerShare() async {
util.buildShare(
id: "3", uidOwner: "user1", file: file1, shareWith: "user2"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});

View file

@ -1,5 +1,6 @@
import 'package:event_bus/event_bus.dart';
import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
@ -7,9 +8,9 @@ import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/pref/provider/memory.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/use_case/remove.dart';
import 'package:np_common/or_null.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:test/test.dart';
import '../mock_type.dart';
@ -46,12 +47,12 @@ Future<void> _removeFile() async {
albumRepo: MockAlbumMemoryRepo(),
fileRepo: MockFileMemoryRepo(files),
shareRepo: MockShareMemoryRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
pref: Pref.scoped(PrefMemoryProvider()),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -72,12 +73,12 @@ Future<void> _removeFileNoCleanUp() async {
albumRepo: MockAlbumMemoryRepo(),
fileRepo: MockFileMemoryRepo(files),
shareRepo: MockShareMemoryRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
pref: Pref.scoped(PrefMemoryProvider()),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -98,12 +99,12 @@ Future<void> _removeAlbumFile() async {
albumRepo: MockAlbumMemoryRepo([album]),
fileRepo: MockFileMemoryRepo([albumFile, ...files]),
shareRepo: MockShareMemoryRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
pref: Pref.scoped(PrefMemoryProvider()),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -142,12 +143,12 @@ Future<void> _removeAlbumFileNoCleanUp() async {
albumRepo: MockAlbumMemoryRepo([album]),
fileRepo: MockFileMemoryRepo([albumFile, ...files]),
shareRepo: MockShareMemoryRepo(),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
pref: Pref.scoped(PrefMemoryProvider()),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -195,12 +196,12 @@ Future<void> _removeSharedAlbumFile() async {
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
pref: Pref.scoped(PrefMemoryProvider()),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -262,13 +263,13 @@ Future<void> _removeSharedAlbumSharedFile() async {
id: "2", file: user1Files[0], uidOwner: "user1", shareWith: "admin"),
util.buildShare(id: "3", file: files[0], shareWith: "user2"),
]),
sqliteDb: util.buildTestDb(),
npDb: 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 c.sqliteDb.insertAccounts([account.toDb()]);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
@ -330,12 +331,12 @@ Future<void> _removeSharedAlbumResyncedFile() async {
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
pref: Pref.scoped(PrefMemoryProvider()),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});

View file

@ -1,8 +1,9 @@
import 'package:nc_photos/db/entity_converter.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/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/use_case/scan_dir_offline.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:test/test.dart';
import '../test_util.dart' as util;
@ -32,11 +33,11 @@ Future<void> _root() async {
..addJpeg("admin/test/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -61,11 +62,11 @@ Future<void> _subDir() async {
..addJpeg("admin/test/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -89,11 +90,11 @@ Future<void> _unsupportedFile() async {
..addGenericFile("admin/test2.pdf", "application/pdf"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -126,13 +127,13 @@ Future<void> _multiAccountRoot() async {
..addJpeg("user1/test/test2.jpg", ownerId: "user1"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
await c.sqliteDb.insertAccountOf(user1Account);
await c.sqliteDb.insertAccounts([user1Account.toDb()]);
await util.insertFiles(c.sqliteDb, user1Account, user1Files);
});

View file

@ -1,10 +1,11 @@
import 'package:drift/drift.dart' as sql;
import 'package:event_bus/event_bus.dart';
import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/favorite.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/use_case/sync_favorite.dart';
import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
import 'package:test/test.dart';
import '../mock_type.dart';
@ -36,11 +37,11 @@ Future<void> _new() async {
const Favorite(fileId: 103),
const Favorite(fileId: 104),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -66,11 +67,11 @@ Future<void> _remove() async {
const Favorite(fileId: 103),
const Favorite(fileId: 104),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.insertAccounts([account.toDb()]);
await util.insertFiles(c.sqliteDb, account, files);
});
@ -81,7 +82,7 @@ Future<void> _remove() async {
);
}
Future<Set<int>> _listSqliteDbFavoriteFileIds(sql.SqliteDb db) async {
Future<Set<int>> _listSqliteDbFavoriteFileIds(compat.SqliteDb db) async {
final query = db.selectOnly(db.files).join([
sql.innerJoin(
db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId)),

View file

@ -1,152 +1,152 @@
import 'package:drift/drift.dart' as sql;
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/entity/tag.dart';
import 'package:nc_photos/entity/tag/data_source.dart';
import 'package:nc_photos/use_case/sync_tag.dart';
import 'package:test/test.dart';
import 'package:tuple/tuple.dart';
// import 'package:drift/drift.dart' as sql;
// import 'package:nc_photos/account.dart';
// import 'package:nc_photos/di_container.dart';
// import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat;
// import 'package:nc_photos/entity/sqlite/type_converter.dart';
// import 'package:nc_photos/entity/tag.dart';
// import 'package:nc_photos/entity/tag/data_source.dart';
// import 'package:nc_photos/use_case/sync_tag.dart';
// import 'package:test/test.dart';
// import 'package:tuple/tuple.dart';
import '../mock_type.dart';
import '../test_util.dart' as util;
// import '../mock_type.dart';
// import '../test_util.dart' as util;
void main() {
group("SyncTag", () {
test("new", _new);
test("remove", _remove);
test("update", _update);
});
}
// void main() {
// group("SyncTag", () {
// test("new", _new);
// test("remove", _remove);
// test("update", _update);
// });
// }
/// Sync with remote where there are new tags
///
/// Remote: [tag0, tag1, tag2]
/// Local: [tag0]
/// Expect: [tag0, tag1, tag2]
Future<void> _new() async {
final account = util.buildAccount();
final c = DiContainer.late();
c.sqliteDb = util.buildTestDb();
addTearDown(() => c.sqliteDb.close());
c.tagRepoRemote = MockTagMemoryRepo({
account.url: [
const Tag(id: 10, displayName: "tag0"),
const Tag(id: 11, displayName: "tag1"),
const Tag(id: 12, displayName: "tag2"),
],
});
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.batch((batch) {
batch.insert(c.sqliteDb.tags,
sql.TagsCompanion.insert(server: 1, tagId: 10, displayName: "tag0"));
});
});
// /// Sync with remote where there are new tags
// ///
// /// Remote: [tag0, tag1, tag2]
// /// Local: [tag0]
// /// Expect: [tag0, tag1, tag2]
// Future<void> _new() async {
// final account = util.buildAccount();
// final c = DiContainer.late();
// c.sqliteDb = util.buildTestDb();
// addTearDown(() => c.sqliteDb.close());
// c.tagRepoRemote = MockTagMemoryRepo({
// account.url: [
// const Tag(id: 10, displayName: "tag0"),
// const Tag(id: 11, displayName: "tag1"),
// const Tag(id: 12, displayName: "tag2"),
// ],
// });
// c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
// await c.sqliteDb.transaction(() async {
// await c.sqliteDb.insertAccounts([account.toDb()]);
// await c.sqliteDb.batch((batch) {
// batch.insert(c.sqliteDb.tags,
// sql.TagsCompanion.insert(server: 1, tagId: 10, displayName: "tag0"));
// });
// });
await SyncTag(c)(account);
expect(
await _listSqliteDbTags(c.sqliteDb),
{
account.url: {
const Tag(id: 10, displayName: "tag0"),
const Tag(id: 11, displayName: "tag1"),
const Tag(id: 12, displayName: "tag2"),
},
},
);
}
// await SyncTag(c)(account);
// expect(
// await _listSqliteDbTags(c.sqliteDb),
// {
// account.url: {
// const Tag(id: 10, displayName: "tag0"),
// const Tag(id: 11, displayName: "tag1"),
// const Tag(id: 12, displayName: "tag2"),
// },
// },
// );
// }
/// Sync with remote where there are removed tags
///
/// Remote: [tag0]
/// Local: [tag0, tag1, tag2]
/// Expect: [tag0]
Future<void> _remove() async {
final account = util.buildAccount();
final c = DiContainer.late();
c.sqliteDb = util.buildTestDb();
addTearDown(() => c.sqliteDb.close());
c.tagRepoRemote = MockTagMemoryRepo({
account.url: [
const Tag(id: 10, displayName: "tag0"),
],
});
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.batch((batch) {
batch.insertAll(c.sqliteDb.tags, [
sql.TagsCompanion.insert(server: 1, tagId: 10, displayName: "tag0"),
sql.TagsCompanion.insert(server: 1, tagId: 11, displayName: "tag1"),
sql.TagsCompanion.insert(server: 1, tagId: 12, displayName: "tag2"),
]);
});
});
// /// Sync with remote where there are removed tags
// ///
// /// Remote: [tag0]
// /// Local: [tag0, tag1, tag2]
// /// Expect: [tag0]
// Future<void> _remove() async {
// final account = util.buildAccount();
// final c = DiContainer.late();
// c.sqliteDb = util.buildTestDb();
// addTearDown(() => c.sqliteDb.close());
// c.tagRepoRemote = MockTagMemoryRepo({
// account.url: [
// const Tag(id: 10, displayName: "tag0"),
// ],
// });
// c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
// await c.sqliteDb.transaction(() async {
// await c.sqliteDb.insertAccounts([account.toDb()]);
// await c.sqliteDb.batch((batch) {
// batch.insertAll(c.sqliteDb.tags, [
// sql.TagsCompanion.insert(server: 1, tagId: 10, displayName: "tag0"),
// sql.TagsCompanion.insert(server: 1, tagId: 11, displayName: "tag1"),
// sql.TagsCompanion.insert(server: 1, tagId: 12, displayName: "tag2"),
// ]);
// });
// });
await SyncTag(c)(account);
expect(
await _listSqliteDbTags(c.sqliteDb),
{
account.url: {
const Tag(id: 10, displayName: "tag0"),
},
},
);
}
// await SyncTag(c)(account);
// expect(
// await _listSqliteDbTags(c.sqliteDb),
// {
// account.url: {
// const Tag(id: 10, displayName: "tag0"),
// },
// },
// );
// }
/// Sync with remote where there are updated tags (i.e, same id, different
/// properties)
///
/// Remote: [tag0, new tag1]
/// Local: [tag0, tag1]
/// Expect: [tag0, new tag1]
Future<void> _update() async {
final account = util.buildAccount();
final c = DiContainer.late();
c.sqliteDb = util.buildTestDb();
addTearDown(() => c.sqliteDb.close());
c.tagRepoRemote = MockTagMemoryRepo({
account.url: [
const Tag(id: 10, displayName: "tag0"),
const Tag(id: 11, displayName: "new tag1"),
],
});
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await c.sqliteDb.batch((batch) {
batch.insertAll(c.sqliteDb.tags, [
sql.TagsCompanion.insert(server: 1, tagId: 10, displayName: "tag0"),
sql.TagsCompanion.insert(server: 1, tagId: 11, displayName: "tag1"),
]);
});
});
// /// Sync with remote where there are updated tags (i.e, same id, different
// /// properties)
// ///
// /// Remote: [tag0, new tag1]
// /// Local: [tag0, tag1]
// /// Expect: [tag0, new tag1]
// Future<void> _update() async {
// final account = util.buildAccount();
// final c = DiContainer.late();
// c.sqliteDb = util.buildTestDb();
// addTearDown(() => c.sqliteDb.close());
// c.tagRepoRemote = MockTagMemoryRepo({
// account.url: [
// const Tag(id: 10, displayName: "tag0"),
// const Tag(id: 11, displayName: "new tag1"),
// ],
// });
// c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
// await c.sqliteDb.transaction(() async {
// await c.sqliteDb.insertAccounts([account.toDb()]);
// await c.sqliteDb.batch((batch) {
// batch.insertAll(c.sqliteDb.tags, [
// sql.TagsCompanion.insert(server: 1, tagId: 10, displayName: "tag0"),
// sql.TagsCompanion.insert(server: 1, tagId: 11, displayName: "tag1"),
// ]);
// });
// });
await SyncTag(c)(account);
expect(
await _listSqliteDbTags(c.sqliteDb),
{
account.url: {
const Tag(id: 10, displayName: "tag0"),
const Tag(id: 11, displayName: "new tag1"),
},
},
);
}
// await SyncTag(c)(account);
// expect(
// await _listSqliteDbTags(c.sqliteDb),
// {
// account.url: {
// const Tag(id: 10, displayName: "tag0"),
// const Tag(id: 11, displayName: "new tag1"),
// },
// },
// );
// }
Future<Map<String, Set<Tag>>> _listSqliteDbTags(sql.SqliteDb db) async {
final query = db.select(db.tags).join([
sql.innerJoin(db.servers, db.servers.rowId.equalsExp(db.tags.server)),
]);
final result = await query
.map((r) => Tuple2(r.readTable(db.servers), r.readTable(db.tags)))
.get();
final product = <String, Set<Tag>>{};
for (final r in result) {
(product[r.item1.address] ??= {}).add(SqliteTagConverter.fromSql(r.item2));
}
return product;
}
// Future<Map<String, Set<Tag>>> _listSqliteDbTags(sql.SqliteDb db) async {
// final query = db.select(db.tags).join([
// sql.innerJoin(db.servers, db.servers.rowId.equalsExp(db.tags.server)),
// ]);
// final result = await query
// .map((r) => Tuple2(r.readTable(db.servers), r.readTable(db.tags)))
// .get();
// final product = <String, Set<Tag>>{};
// for (final r in result) {
// (product[r.item1.address] ??= {}).add(SqliteTagConverter.fromSql(r.item2));
// }
// return product;
// }

View file

@ -36,7 +36,7 @@ Future<void> _unshareWithoutFile() async {
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
util.buildShare(id: "1", file: albumFile, shareWith: "user2"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
@ -75,7 +75,7 @@ Future<void> _unshareWithFile() async {
util.buildShare(id: "2", file: file1, shareWith: "user1"),
util.buildShare(id: "3", file: file1, shareWith: "user2"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
@ -125,7 +125,7 @@ Future<void> _unshareWithFileNotOwned() async {
util.buildShare(
id: "5", uidOwner: "user2", file: files[1], shareWith: "user1"),
]),
sqliteDb: util.buildTestDb(),
npDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());

View file

@ -1,3 +1,14 @@
import 'dart:async';
extension MapEntryListExtension<T, U> on Iterable<MapEntry<T, U>> {
Map<T, U> toMap() => Map.fromEntries(this);
}
extension MapExtension<T, U> on Map<T, U> {
Future<Map<V, W>> asyncMap<V, W>(
FutureOr<MapEntry<V, W>> Function(T key, U value) convert) async {
final results = await Future.wait(
entries.map((e) async => await convert(e.key, e.value)));
return Map.fromEntries(results);
}
}

7
np_datetime/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock

View file

@ -0,0 +1 @@
include: package:np_lints/np.yaml

View file

@ -0,0 +1,3 @@
library np_datetime;
export 'src/time_range.dart';

View file

@ -0,0 +1,51 @@
enum TimeRangeBound {
inclusive,
exclusive,
}
class TimeRange {
const TimeRange({
required this.from,
this.fromBound = TimeRangeBound.inclusive,
required this.to,
this.toBound = TimeRangeBound.exclusive,
});
@override
String toString() {
return "${fromBound == TimeRangeBound.inclusive ? "[" : "("}"
"$from, $to"
"${toBound == TimeRangeBound.inclusive ? "]" : ")"}";
}
final DateTime from;
final TimeRangeBound fromBound;
final DateTime to;
final TimeRangeBound toBound;
}
extension TimeRangeExtension on TimeRange {
/// Return if an arbitrary time [a] is inside this range
///
/// The comparison is independent of whether the time is in UTC or in the
/// local time zone
bool isIn(DateTime a) {
if (a.isBefore(from)) {
return false;
}
if (fromBound == TimeRangeBound.exclusive) {
if (a.isAtSameMomentAs(from)) {
return false;
}
}
if (a.isAfter(to)) {
return false;
}
if (toBound == TimeRangeBound.exclusive) {
if (a.isAtSameMomentAs(to)) {
return false;
}
}
return true;
}
}

13
np_datetime/pubspec.yaml Normal file
View file

@ -0,0 +1,13 @@
name: np_datetime
description: A starting point for Dart libraries or applications.
version: 1.0.0
# repository: https://github.com/my_org/my_repo
publish_to: none
environment:
sdk: '>=2.19.6 <3.0.0'
dev_dependencies:
np_lints:
path: ../np_lints
test: ^1.21.0

32
np_db/.gitignore vendored Normal file
View file

@ -0,0 +1,32 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
.flutter-plugins
.flutter-plugins-dependencies

View file

@ -0,0 +1 @@
include: package:np_lints/np.yaml

11
np_db/build.yaml Normal file
View file

@ -0,0 +1,11 @@
targets:
$default:
builders:
to_string_build:
options:
formatStringNameMapping:
double: "${$?.toStringAsFixed(3)}"
List: "[length: ${$?.length}]"
File: "${$?.path}"
FileDescriptor: "${$?.fdPath}"
useEnumName: true

5
np_db/lib/np_db.dart Normal file
View file

@ -0,0 +1,5 @@
library np_db;
export 'src/api.dart';
export 'src/entity.dart';
export 'src/exception.dart';

377
np_db/lib/src/api.dart Normal file
View file

@ -0,0 +1,377 @@
import 'dart:io' as io;
import 'package:equatable/equatable.dart';
import 'package:logging/logging.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/or_null.dart';
import 'package:np_common/type.dart';
import 'package:np_datetime/np_datetime.dart';
import 'package:np_db/src/entity.dart';
import 'package:np_db_sqlite/np_db_sqlite.dart';
import 'package:to_string/to_string.dart';
part 'api.g.dart';
typedef NpDbComputeCallback<T, U> = Future<U> Function(NpDb db, T message);
/// A data structure that identify a File in db
@ToString(ignoreNull: true)
class DbFileKey {
const DbFileKey({
this.fileId,
this.relativePath,
}) : assert(fileId != null || relativePath != null);
const DbFileKey.byId(int fileId) : this(fileId: fileId);
const DbFileKey.byPath(String relativePath)
: this(relativePath: relativePath);
@override
String toString() => _$toString();
bool compareIdentity(DbFileKey other) =>
fileId == other.fileId || relativePath == other.relativePath;
final int? fileId;
final String? relativePath;
}
class DbSyncResult {
const DbSyncResult({
required this.insert,
required this.delete,
required this.update,
});
final int insert;
final int delete;
final int update;
}
@toString
class DbLocationGroup with EquatableMixin {
const DbLocationGroup({
required this.place,
required this.countryCode,
required this.count,
required this.latestFileId,
required this.latestDateTime,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
place,
countryCode,
count,
latestFileId,
latestDateTime,
];
final String place;
final String countryCode;
final int count;
final int latestFileId;
final DateTime latestDateTime;
}
@toString
class DbLocationGroupResult {
const DbLocationGroupResult({
required this.name,
required this.admin1,
required this.admin2,
required this.countryCode,
});
@override
String toString() => _$toString();
final List<DbLocationGroup> name;
final List<DbLocationGroup> admin1;
final List<DbLocationGroup> admin2;
final List<DbLocationGroup> countryCode;
}
@npLog
abstract class NpDb {
factory NpDb() => NpDbSqlite();
Future<void> initMainIsolate({
required int androidSdk,
});
Future<void> initBackgroundIsolate({
required int androidSdk,
});
/// Dispose the db
///
/// After disposing, you must not call any methods defined here anymore. This
/// is typically used before stopping a background isolate
Future<void> dispose();
Future<io.File> export(io.Directory dir);
/// Start an isolate with a [NpDb] instance provided to you
Future<U> compute<T, U>(NpDbComputeCallback<T, U> callback, T args);
/// Insert [accounts] to db
Future<void> addAccounts(List<DbAccount> accounts);
/// Clear all data in the database and insert [accounts]
///
/// WARNING: ALL data will be dropped!
Future<void> clearAndInitWithAccounts(List<DbAccount> accounts);
Future<void> deleteAccount(DbAccount account);
Future<List<DbAlbum>> getAlbumsByAlbumFileIds({
required DbAccount account,
required List<int> fileIds,
});
Future<void> syncAlbum({
required DbAccount account,
required DbFile albumFile,
required DbAlbum album,
});
/// Return all faces provided by the Face Recognition app
Future<List<DbFaceRecognitionPerson>> getFaceRecognitionPersons({
required DbAccount account,
});
/// Return faces provided by the Face Recognition app with loosely matched
/// [name]
Future<List<DbFaceRecognitionPerson>> searchFaceRecognitionPersonsByName({
required DbAccount account,
required String name,
});
/// Replace all recognized people for [account]
Future<DbSyncResult> syncFaceRecognitionPersons({
required DbAccount account,
required List<DbFaceRecognitionPerson> persons,
});
/// Return files located inside [dir]
Future<List<DbFile>> getFilesByDirKey({
required DbAccount account,
required DbFileKey dir,
});
Future<List<DbFile>> getFilesByDirKeyAndLocation({
required DbAccount account,
required String dirRelativePath,
required String? place,
required String countryCode,
});
/// Return [DbFile]s by their corresponding file ids
///
/// No error will be thrown even if a file in [fileIds] is not found, it is
/// thus the responsibility of the caller to decide how to handle such case.
/// Returned files are NOT guaranteed to be sorted as [fileIds]
Future<List<DbFile>> getFilesByFileIds({
required DbAccount account,
required List<int> fileIds,
});
/// Return [DbFile]s by their date time value
Future<List<DbFile>> getFilesByTimeRange({
required DbAccount account,
required List<String> dirRoots,
required TimeRange range,
});
/// Update one or more file properties of a single file
Future<void> updateFileByFileId({
required DbAccount account,
required int fileId,
String? relativePath,
OrNull<bool>? isFavorite,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
DateTime? bestDateTime,
OrNull<DbImageData>? imageData,
OrNull<DbLocation>? location,
});
/// Batch update one or more file properties of multiple files
///
/// Only a subset of properties can be updated in batch
Future<void> updateFilesByFileIds({
required DbAccount account,
required List<int> fileIds,
OrNull<bool>? isFavorite,
OrNull<bool>? isArchived,
});
/// Add or replace files in db
Future<void> syncDirFiles({
required DbAccount account,
required int dirFileId,
required List<DbFile> files,
});
/// Replace a file in db
Future<void> syncFile({
required DbAccount account,
required DbFile file,
});
/// Add or replace nc albums in db
Future<DbSyncResult> syncFavoriteFiles({
required DbAccount account,
required List<int> favoriteFileIds,
});
/// Return number of files without metadata
Future<int> countFilesByFileIdsMissingMetadata({
required DbAccount account,
required List<int> fileIds,
required List<String> mimes,
});
/// Delete a file or dir from db
Future<void> deleteFile({
required DbAccount account,
required DbFileKey file,
});
/// Return a map of file id to etags for all dirs and sub dirs located under
/// [relativePath], including the path itself
Future<Map<int, String>> getDirFileIdToEtagByLikeRelativePath({
required DbAccount account,
required String relativePath,
});
/// Remove all children of a dir
Future<void> truncateDir({
required DbAccount account,
required DbFileKey dir,
});
/// Return [DbFileDescriptor]s
///
/// Limit results by their corresponding file ids if [fileIds] is not null. No
/// error will be thrown even if a file in [fileIds] is not found, it is thus
/// the responsibility of the caller to decide how to handle such case
///
/// [includeRelativeRoots] define paths to be included; [excludeRelativeRoots]
/// define paths to be excluded. Paths in both lists are matched as prefix
///
/// Limit type of files to be returned by specifying [mimes]. The mime types
/// are matched as is
///
/// Returned files are sorted by [DbFileDescriptor.bestDateTime] in descending
/// order
Future<List<DbFileDescriptor>> getFileDescriptors({
required DbAccount account,
List<int>? fileIds,
List<String>? includeRelativeRoots,
List<String>? excludeRelativeRoots,
String? location,
List<String>? mimes,
int? limit,
});
Future<DbLocationGroupResult> groupLocations({
required DbAccount account,
List<String>? includeRelativeRoots,
List<String>? excludeRelativeRoots,
});
Future<List<DbNcAlbum>> getNcAlbums({
required DbAccount account,
});
Future<void> addNcAlbum({
required DbAccount account,
required DbNcAlbum album,
});
Future<void> deleteNcAlbum({
required DbAccount account,
required DbNcAlbum album,
});
/// Add or replace nc albums in db
Future<DbSyncResult> syncNcAlbums({
required DbAccount account,
required List<DbNcAlbum> albums,
});
Future<List<DbNcAlbumItem>> getNcAlbumItemsByParent({
required DbAccount account,
required DbNcAlbum parent,
});
/// Add or replace nc album items in db
Future<DbSyncResult> syncNcAlbumItems({
required DbAccount account,
required DbNcAlbum album,
required List<DbNcAlbumItem> items,
});
/// Return all faces provided by the Recognize app
Future<List<DbRecognizeFace>> getRecognizeFaces({
required DbAccount account,
});
Future<List<DbRecognizeFaceItem>> getRecognizeFaceItemsByFaceLabel({
required DbAccount account,
required String label,
});
Future<Map<String, List<DbRecognizeFaceItem>>>
getRecognizeFaceItemsByFaceLabels({
required DbAccount account,
required List<String> labels,
ErrorWithValueHandler<String>? onError,
});
Future<Map<String, DbRecognizeFaceItem>>
getLatestRecognizeFaceItemsByFaceLabels({
required DbAccount account,
required List<String> labels,
ErrorWithValueHandler<String>? onError,
});
/// Replace all recognized faces for [account]
///
/// Return true if any of the faces or items are changed
Future<bool> syncRecognizeFacesAndItems({
required DbAccount account,
required Map<DbRecognizeFace, List<DbRecognizeFaceItem>> data,
});
/// Return all tags
Future<List<DbTag>> getTags({
required DbAccount account,
});
/// Return the tag matching [displayName]
Future<DbTag?> getTagByDisplayName({
required DbAccount account,
required String displayName,
});
/// Replace all tags for [account]
Future<DbSyncResult> syncTags({
required DbAccount account,
required List<DbTag> tags,
});
/// Migrate to app v55
Future<void> migrateV55(void Function(int current, int count)? onProgress);
/// Run vacuum statement on a database backed by sqlite
///
/// This method is not necessarily supported by all implementations
Future<void> sqlVacuum();
}

39
np_db/lib/src/api.g.dart Normal file
View file

@ -0,0 +1,39 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$NpDbNpLog on NpDb {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("src.api.NpDb");
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$DbFileKeyToString on DbFileKey {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbFileKey {${fileId == null ? "" : "fileId: $fileId, "}${relativePath == null ? "" : "relativePath: $relativePath"}}";
}
}
extension _$DbLocationGroupToString on DbLocationGroup {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbLocationGroup {place: $place, countryCode: $countryCode, count: $count, latestFileId: $latestFileId, latestDateTime: $latestDateTime}";
}
}
extension _$DbLocationGroupResultToString on DbLocationGroupResult {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbLocationGroupResult {name: [length: ${name.length}], admin1: [length: ${admin1.length}], admin2: [length: ${admin2.length}], countryCode: [length: ${countryCode.length}]}";
}
}

496
np_db/lib/src/entity.dart Normal file
View file

@ -0,0 +1,496 @@
import 'package:copy_with/copy_with.dart';
import 'package:equatable/equatable.dart';
import 'package:np_common/type.dart';
import 'package:np_string/np_string.dart';
import 'package:to_string/to_string.dart';
part 'entity.g.dart';
@genCopyWith
@toString
class DbAccount with EquatableMixin {
const DbAccount({
required this.serverAddress,
required this.userId,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
serverAddress,
userId,
];
final String serverAddress;
final CiString userId;
}
@genCopyWith
@toString
class DbAlbum with EquatableMixin {
const DbAlbum({
required this.fileId,
this.fileEtag,
required this.version,
required this.lastUpdated,
required this.name,
required this.providerType,
required this.providerContent,
required this.coverProviderType,
required this.coverProviderContent,
required this.sortProviderType,
required this.sortProviderContent,
required this.shares,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
fileId,
fileEtag,
version,
lastUpdated,
name,
providerType,
providerContent,
coverProviderType,
coverProviderContent,
sortProviderType,
sortProviderContent,
shares,
];
final int fileId;
final String? fileEtag;
final int version;
final DateTime lastUpdated;
final String name;
final String providerType;
final JsonObj providerContent;
final String coverProviderType;
final JsonObj coverProviderContent;
final String sortProviderType;
final JsonObj sortProviderContent;
final List<DbAlbumShare> shares;
}
@genCopyWith
@toString
class DbAlbumShare with EquatableMixin {
const DbAlbumShare({
required this.userId,
this.displayName,
required this.sharedAt,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
userId,
displayName,
sharedAt,
];
final String userId;
final String? displayName;
final DateTime sharedAt;
}
@genCopyWith
@toString
class DbFaceRecognitionPerson with EquatableMixin {
const DbFaceRecognitionPerson({
required this.name,
required this.thumbFaceId,
required this.count,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
name,
thumbFaceId,
count,
];
final String name;
final int thumbFaceId;
final int count;
}
@genCopyWith
@toString
class DbFile with EquatableMixin {
const DbFile({
required this.fileId,
required this.contentLength,
required this.contentType,
required this.etag,
required this.lastModified,
required this.isCollection,
required this.usedBytes,
required this.hasPreview,
required this.ownerId,
required this.ownerDisplayName,
required this.relativePath,
required this.isFavorite,
required this.isArchived,
required this.overrideDateTime,
required this.bestDateTime,
required this.imageData,
required this.location,
required this.trashData,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
fileId,
contentLength,
contentType,
etag,
lastModified,
isCollection,
usedBytes,
hasPreview,
ownerId,
ownerDisplayName,
relativePath,
isFavorite,
isArchived,
overrideDateTime,
bestDateTime,
imageData,
location,
trashData,
];
final int fileId;
final int? contentLength;
final String? contentType;
final String? etag;
final DateTime? lastModified;
final bool? isCollection;
final int? usedBytes;
final bool? hasPreview;
final CiString? ownerId;
final String? ownerDisplayName;
final String relativePath;
final bool? isFavorite;
final bool? isArchived;
final DateTime? overrideDateTime;
final DateTime bestDateTime;
final DbImageData? imageData;
final DbLocation? location;
final DbTrashData? trashData;
}
@genCopyWith
@toString
class DbFileDescriptor with EquatableMixin {
const DbFileDescriptor({
required this.relativePath,
required this.fileId,
required this.contentType,
required this.isArchived,
required this.isFavorite,
required this.bestDateTime,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
relativePath,
fileId,
contentType,
isArchived,
isFavorite,
bestDateTime,
];
final String relativePath;
final int fileId;
final String? contentType;
final bool? isArchived;
final bool? isFavorite;
final DateTime bestDateTime;
}
@genCopyWith
@toString
class DbImageData with EquatableMixin {
const DbImageData({
required this.lastUpdated,
required this.fileEtag,
required this.width,
required this.height,
required this.exif,
required this.exifDateTimeOriginal,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
lastUpdated,
fileEtag,
width,
height,
exif,
exifDateTimeOriginal,
];
final DateTime lastUpdated;
final String? fileEtag;
final int? width;
final int? height;
final JsonObj? exif;
final DateTime? exifDateTimeOriginal;
}
@genCopyWith
@toString
class DbLocation with EquatableMixin {
const DbLocation({
required this.version,
required this.name,
required this.latitude,
required this.longitude,
required this.countryCode,
required this.admin1,
required this.admin2,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
version,
name,
latitude,
longitude,
countryCode,
admin1,
admin2,
];
final int version;
final String? name;
final double? latitude;
final double? longitude;
final String? countryCode;
final String? admin1;
final String? admin2;
}
@genCopyWith
@toString
class DbNcAlbum with EquatableMixin {
const DbNcAlbum({
required this.relativePath,
this.lastPhoto,
required this.nbItems,
this.location,
this.dateStart,
this.dateEnd,
required this.collaborators,
required this.isOwned,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
relativePath,
lastPhoto,
nbItems,
location,
dateStart,
dateEnd,
collaborators,
isOwned,
];
final String relativePath;
final int? lastPhoto;
final int nbItems;
final String? location;
final DateTime? dateStart;
final DateTime? dateEnd;
final List<JsonObj> collaborators;
final bool isOwned;
}
@genCopyWith
@toString
class DbNcAlbumItem with EquatableMixin {
const DbNcAlbumItem({
required this.relativePath,
required this.fileId,
this.contentLength,
this.contentType,
this.etag,
this.lastModified,
this.hasPreview,
this.isFavorite,
this.fileMetadataWidth,
this.fileMetadataHeight,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
relativePath,
fileId,
contentLength,
contentType,
etag,
lastModified,
hasPreview,
isFavorite,
fileMetadataWidth,
fileMetadataHeight,
];
final String relativePath;
final int fileId;
final int? contentLength;
final String? contentType;
final String? etag;
final DateTime? lastModified;
final bool? hasPreview;
final bool? isFavorite;
final int? fileMetadataWidth;
final int? fileMetadataHeight;
}
@genCopyWith
@toString
class DbRecognizeFace with EquatableMixin {
const DbRecognizeFace({
required this.label,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
label,
];
final String label;
}
@genCopyWith
@toString
class DbRecognizeFaceItem with EquatableMixin {
const DbRecognizeFaceItem({
required this.relativePath,
required this.fileId,
this.contentLength,
this.contentType,
this.etag,
this.lastModified,
this.hasPreview,
this.realPath,
this.isFavorite,
this.fileMetadataWidth,
this.fileMetadataHeight,
this.faceDetections,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
relativePath,
fileId,
contentLength,
contentType,
etag,
lastModified,
hasPreview,
realPath,
isFavorite,
fileMetadataWidth,
fileMetadataHeight,
faceDetections,
];
final String relativePath;
final int fileId;
final int? contentLength;
final String? contentType;
final String? etag;
final DateTime? lastModified;
final bool? hasPreview;
final String? realPath;
final bool? isFavorite;
final int? fileMetadataWidth;
final int? fileMetadataHeight;
final String? faceDetections;
}
@genCopyWith
@toString
class DbTag with EquatableMixin {
const DbTag({
required this.id,
required this.displayName,
required this.userVisible,
required this.userAssignable,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
id,
displayName,
userVisible,
userAssignable,
];
final int id;
final String displayName;
final bool? userVisible;
final bool? userAssignable;
}
@genCopyWith
@toString
class DbTrashData {
const DbTrashData({
required this.filename,
required this.originalLocation,
required this.deletionTime,
});
@override
String toString() => _$toString();
final String filename;
final String originalLocation;
final DateTime deletionTime;
}

744
np_db/lib/src/entity.g.dart Normal file
View file

@ -0,0 +1,744 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'entity.dart';
// **************************************************************************
// CopyWithLintRuleGenerator
// **************************************************************************
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class $DbAccountCopyWithWorker {
DbAccount call({String? serverAddress, CiString? userId});
}
class _$DbAccountCopyWithWorkerImpl implements $DbAccountCopyWithWorker {
_$DbAccountCopyWithWorkerImpl(this.that);
@override
DbAccount call({dynamic serverAddress, dynamic userId}) {
return DbAccount(
serverAddress: serverAddress as String? ?? that.serverAddress,
userId: userId as CiString? ?? that.userId);
}
final DbAccount that;
}
extension $DbAccountCopyWith on DbAccount {
$DbAccountCopyWithWorker get copyWith => _$copyWith;
$DbAccountCopyWithWorker get _$copyWith =>
_$DbAccountCopyWithWorkerImpl(this);
}
abstract class $DbAlbumCopyWithWorker {
DbAlbum call(
{int? fileId,
String? fileEtag,
int? version,
DateTime? lastUpdated,
String? name,
String? providerType,
JsonObj? providerContent,
String? coverProviderType,
JsonObj? coverProviderContent,
String? sortProviderType,
JsonObj? sortProviderContent,
List<DbAlbumShare>? shares});
}
class _$DbAlbumCopyWithWorkerImpl implements $DbAlbumCopyWithWorker {
_$DbAlbumCopyWithWorkerImpl(this.that);
@override
DbAlbum call(
{dynamic fileId,
dynamic fileEtag = copyWithNull,
dynamic version,
dynamic lastUpdated,
dynamic name,
dynamic providerType,
dynamic providerContent,
dynamic coverProviderType,
dynamic coverProviderContent,
dynamic sortProviderType,
dynamic sortProviderContent,
dynamic shares}) {
return DbAlbum(
fileId: fileId as int? ?? that.fileId,
fileEtag:
fileEtag == copyWithNull ? that.fileEtag : fileEtag as String?,
version: version as int? ?? that.version,
lastUpdated: lastUpdated as DateTime? ?? that.lastUpdated,
name: name as String? ?? that.name,
providerType: providerType as String? ?? that.providerType,
providerContent: providerContent as JsonObj? ?? that.providerContent,
coverProviderType:
coverProviderType as String? ?? that.coverProviderType,
coverProviderContent:
coverProviderContent as JsonObj? ?? that.coverProviderContent,
sortProviderType: sortProviderType as String? ?? that.sortProviderType,
sortProviderContent:
sortProviderContent as JsonObj? ?? that.sortProviderContent,
shares: shares as List<DbAlbumShare>? ?? that.shares);
}
final DbAlbum that;
}
extension $DbAlbumCopyWith on DbAlbum {
$DbAlbumCopyWithWorker get copyWith => _$copyWith;
$DbAlbumCopyWithWorker get _$copyWith => _$DbAlbumCopyWithWorkerImpl(this);
}
abstract class $DbAlbumShareCopyWithWorker {
DbAlbumShare call({String? userId, String? displayName, DateTime? sharedAt});
}
class _$DbAlbumShareCopyWithWorkerImpl implements $DbAlbumShareCopyWithWorker {
_$DbAlbumShareCopyWithWorkerImpl(this.that);
@override
DbAlbumShare call(
{dynamic userId, dynamic displayName = copyWithNull, dynamic sharedAt}) {
return DbAlbumShare(
userId: userId as String? ?? that.userId,
displayName: displayName == copyWithNull
? that.displayName
: displayName as String?,
sharedAt: sharedAt as DateTime? ?? that.sharedAt);
}
final DbAlbumShare that;
}
extension $DbAlbumShareCopyWith on DbAlbumShare {
$DbAlbumShareCopyWithWorker get copyWith => _$copyWith;
$DbAlbumShareCopyWithWorker get _$copyWith =>
_$DbAlbumShareCopyWithWorkerImpl(this);
}
abstract class $DbFaceRecognitionPersonCopyWithWorker {
DbFaceRecognitionPerson call({String? name, int? thumbFaceId, int? count});
}
class _$DbFaceRecognitionPersonCopyWithWorkerImpl
implements $DbFaceRecognitionPersonCopyWithWorker {
_$DbFaceRecognitionPersonCopyWithWorkerImpl(this.that);
@override
DbFaceRecognitionPerson call(
{dynamic name, dynamic thumbFaceId, dynamic count}) {
return DbFaceRecognitionPerson(
name: name as String? ?? that.name,
thumbFaceId: thumbFaceId as int? ?? that.thumbFaceId,
count: count as int? ?? that.count);
}
final DbFaceRecognitionPerson that;
}
extension $DbFaceRecognitionPersonCopyWith on DbFaceRecognitionPerson {
$DbFaceRecognitionPersonCopyWithWorker get copyWith => _$copyWith;
$DbFaceRecognitionPersonCopyWithWorker get _$copyWith =>
_$DbFaceRecognitionPersonCopyWithWorkerImpl(this);
}
abstract class $DbFileCopyWithWorker {
DbFile call(
{int? fileId,
int? contentLength,
String? contentType,
String? etag,
DateTime? lastModified,
bool? isCollection,
int? usedBytes,
bool? hasPreview,
CiString? ownerId,
String? ownerDisplayName,
String? relativePath,
bool? isFavorite,
bool? isArchived,
DateTime? overrideDateTime,
DateTime? bestDateTime,
DbImageData? imageData,
DbLocation? location,
DbTrashData? trashData});
}
class _$DbFileCopyWithWorkerImpl implements $DbFileCopyWithWorker {
_$DbFileCopyWithWorkerImpl(this.that);
@override
DbFile call(
{dynamic fileId,
dynamic contentLength = copyWithNull,
dynamic contentType = copyWithNull,
dynamic etag = copyWithNull,
dynamic lastModified = copyWithNull,
dynamic isCollection = copyWithNull,
dynamic usedBytes = copyWithNull,
dynamic hasPreview = copyWithNull,
dynamic ownerId = copyWithNull,
dynamic ownerDisplayName = copyWithNull,
dynamic relativePath,
dynamic isFavorite = copyWithNull,
dynamic isArchived = copyWithNull,
dynamic overrideDateTime = copyWithNull,
dynamic bestDateTime,
dynamic imageData = copyWithNull,
dynamic location = copyWithNull,
dynamic trashData = copyWithNull}) {
return DbFile(
fileId: fileId as int? ?? that.fileId,
contentLength: contentLength == copyWithNull
? that.contentLength
: contentLength as int?,
contentType: contentType == copyWithNull
? that.contentType
: contentType as String?,
etag: etag == copyWithNull ? that.etag : etag as String?,
lastModified: lastModified == copyWithNull
? that.lastModified
: lastModified as DateTime?,
isCollection: isCollection == copyWithNull
? that.isCollection
: isCollection as bool?,
usedBytes:
usedBytes == copyWithNull ? that.usedBytes : usedBytes as int?,
hasPreview:
hasPreview == copyWithNull ? that.hasPreview : hasPreview as bool?,
ownerId: ownerId == copyWithNull ? that.ownerId : ownerId as CiString?,
ownerDisplayName: ownerDisplayName == copyWithNull
? that.ownerDisplayName
: ownerDisplayName as String?,
relativePath: relativePath as String? ?? that.relativePath,
isFavorite:
isFavorite == copyWithNull ? that.isFavorite : isFavorite as bool?,
isArchived:
isArchived == copyWithNull ? that.isArchived : isArchived as bool?,
overrideDateTime: overrideDateTime == copyWithNull
? that.overrideDateTime
: overrideDateTime as DateTime?,
bestDateTime: bestDateTime as DateTime? ?? that.bestDateTime,
imageData: imageData == copyWithNull
? that.imageData
: imageData as DbImageData?,
location:
location == copyWithNull ? that.location : location as DbLocation?,
trashData: trashData == copyWithNull
? that.trashData
: trashData as DbTrashData?);
}
final DbFile that;
}
extension $DbFileCopyWith on DbFile {
$DbFileCopyWithWorker get copyWith => _$copyWith;
$DbFileCopyWithWorker get _$copyWith => _$DbFileCopyWithWorkerImpl(this);
}
abstract class $DbFileDescriptorCopyWithWorker {
DbFileDescriptor call(
{String? relativePath,
int? fileId,
String? contentType,
bool? isArchived,
bool? isFavorite,
DateTime? bestDateTime});
}
class _$DbFileDescriptorCopyWithWorkerImpl
implements $DbFileDescriptorCopyWithWorker {
_$DbFileDescriptorCopyWithWorkerImpl(this.that);
@override
DbFileDescriptor call(
{dynamic relativePath,
dynamic fileId,
dynamic contentType = copyWithNull,
dynamic isArchived = copyWithNull,
dynamic isFavorite = copyWithNull,
dynamic bestDateTime}) {
return DbFileDescriptor(
relativePath: relativePath as String? ?? that.relativePath,
fileId: fileId as int? ?? that.fileId,
contentType: contentType == copyWithNull
? that.contentType
: contentType as String?,
isArchived:
isArchived == copyWithNull ? that.isArchived : isArchived as bool?,
isFavorite:
isFavorite == copyWithNull ? that.isFavorite : isFavorite as bool?,
bestDateTime: bestDateTime as DateTime? ?? that.bestDateTime);
}
final DbFileDescriptor that;
}
extension $DbFileDescriptorCopyWith on DbFileDescriptor {
$DbFileDescriptorCopyWithWorker get copyWith => _$copyWith;
$DbFileDescriptorCopyWithWorker get _$copyWith =>
_$DbFileDescriptorCopyWithWorkerImpl(this);
}
abstract class $DbImageDataCopyWithWorker {
DbImageData call(
{DateTime? lastUpdated,
String? fileEtag,
int? width,
int? height,
JsonObj? exif,
DateTime? exifDateTimeOriginal});
}
class _$DbImageDataCopyWithWorkerImpl implements $DbImageDataCopyWithWorker {
_$DbImageDataCopyWithWorkerImpl(this.that);
@override
DbImageData call(
{dynamic lastUpdated,
dynamic fileEtag = copyWithNull,
dynamic width = copyWithNull,
dynamic height = copyWithNull,
dynamic exif = copyWithNull,
dynamic exifDateTimeOriginal = copyWithNull}) {
return DbImageData(
lastUpdated: lastUpdated as DateTime? ?? that.lastUpdated,
fileEtag:
fileEtag == copyWithNull ? that.fileEtag : fileEtag as String?,
width: width == copyWithNull ? that.width : width as int?,
height: height == copyWithNull ? that.height : height as int?,
exif: exif == copyWithNull ? that.exif : exif as JsonObj?,
exifDateTimeOriginal: exifDateTimeOriginal == copyWithNull
? that.exifDateTimeOriginal
: exifDateTimeOriginal as DateTime?);
}
final DbImageData that;
}
extension $DbImageDataCopyWith on DbImageData {
$DbImageDataCopyWithWorker get copyWith => _$copyWith;
$DbImageDataCopyWithWorker get _$copyWith =>
_$DbImageDataCopyWithWorkerImpl(this);
}
abstract class $DbLocationCopyWithWorker {
DbLocation call(
{int? version,
String? name,
double? latitude,
double? longitude,
String? countryCode,
String? admin1,
String? admin2});
}
class _$DbLocationCopyWithWorkerImpl implements $DbLocationCopyWithWorker {
_$DbLocationCopyWithWorkerImpl(this.that);
@override
DbLocation call(
{dynamic version,
dynamic name = copyWithNull,
dynamic latitude = copyWithNull,
dynamic longitude = copyWithNull,
dynamic countryCode = copyWithNull,
dynamic admin1 = copyWithNull,
dynamic admin2 = copyWithNull}) {
return DbLocation(
version: version as int? ?? that.version,
name: name == copyWithNull ? that.name : name as String?,
latitude:
latitude == copyWithNull ? that.latitude : latitude as double?,
longitude:
longitude == copyWithNull ? that.longitude : longitude as double?,
countryCode: countryCode == copyWithNull
? that.countryCode
: countryCode as String?,
admin1: admin1 == copyWithNull ? that.admin1 : admin1 as String?,
admin2: admin2 == copyWithNull ? that.admin2 : admin2 as String?);
}
final DbLocation that;
}
extension $DbLocationCopyWith on DbLocation {
$DbLocationCopyWithWorker get copyWith => _$copyWith;
$DbLocationCopyWithWorker get _$copyWith =>
_$DbLocationCopyWithWorkerImpl(this);
}
abstract class $DbNcAlbumCopyWithWorker {
DbNcAlbum call(
{String? relativePath,
int? lastPhoto,
int? nbItems,
String? location,
DateTime? dateStart,
DateTime? dateEnd,
List<JsonObj>? collaborators,
bool? isOwned});
}
class _$DbNcAlbumCopyWithWorkerImpl implements $DbNcAlbumCopyWithWorker {
_$DbNcAlbumCopyWithWorkerImpl(this.that);
@override
DbNcAlbum call(
{dynamic relativePath,
dynamic lastPhoto = copyWithNull,
dynamic nbItems,
dynamic location = copyWithNull,
dynamic dateStart = copyWithNull,
dynamic dateEnd = copyWithNull,
dynamic collaborators,
dynamic isOwned}) {
return DbNcAlbum(
relativePath: relativePath as String? ?? that.relativePath,
lastPhoto:
lastPhoto == copyWithNull ? that.lastPhoto : lastPhoto as int?,
nbItems: nbItems as int? ?? that.nbItems,
location:
location == copyWithNull ? that.location : location as String?,
dateStart:
dateStart == copyWithNull ? that.dateStart : dateStart as DateTime?,
dateEnd: dateEnd == copyWithNull ? that.dateEnd : dateEnd as DateTime?,
collaborators: collaborators as List<JsonObj>? ?? that.collaborators,
isOwned: isOwned as bool? ?? that.isOwned);
}
final DbNcAlbum that;
}
extension $DbNcAlbumCopyWith on DbNcAlbum {
$DbNcAlbumCopyWithWorker get copyWith => _$copyWith;
$DbNcAlbumCopyWithWorker get _$copyWith =>
_$DbNcAlbumCopyWithWorkerImpl(this);
}
abstract class $DbNcAlbumItemCopyWithWorker {
DbNcAlbumItem call(
{String? relativePath,
int? fileId,
int? contentLength,
String? contentType,
String? etag,
DateTime? lastModified,
bool? hasPreview,
bool? isFavorite,
int? fileMetadataWidth,
int? fileMetadataHeight});
}
class _$DbNcAlbumItemCopyWithWorkerImpl
implements $DbNcAlbumItemCopyWithWorker {
_$DbNcAlbumItemCopyWithWorkerImpl(this.that);
@override
DbNcAlbumItem call(
{dynamic relativePath,
dynamic fileId,
dynamic contentLength = copyWithNull,
dynamic contentType = copyWithNull,
dynamic etag = copyWithNull,
dynamic lastModified = copyWithNull,
dynamic hasPreview = copyWithNull,
dynamic isFavorite = copyWithNull,
dynamic fileMetadataWidth = copyWithNull,
dynamic fileMetadataHeight = copyWithNull}) {
return DbNcAlbumItem(
relativePath: relativePath as String? ?? that.relativePath,
fileId: fileId as int? ?? that.fileId,
contentLength: contentLength == copyWithNull
? that.contentLength
: contentLength as int?,
contentType: contentType == copyWithNull
? that.contentType
: contentType as String?,
etag: etag == copyWithNull ? that.etag : etag as String?,
lastModified: lastModified == copyWithNull
? that.lastModified
: lastModified as DateTime?,
hasPreview:
hasPreview == copyWithNull ? that.hasPreview : hasPreview as bool?,
isFavorite:
isFavorite == copyWithNull ? that.isFavorite : isFavorite as bool?,
fileMetadataWidth: fileMetadataWidth == copyWithNull
? that.fileMetadataWidth
: fileMetadataWidth as int?,
fileMetadataHeight: fileMetadataHeight == copyWithNull
? that.fileMetadataHeight
: fileMetadataHeight as int?);
}
final DbNcAlbumItem that;
}
extension $DbNcAlbumItemCopyWith on DbNcAlbumItem {
$DbNcAlbumItemCopyWithWorker get copyWith => _$copyWith;
$DbNcAlbumItemCopyWithWorker get _$copyWith =>
_$DbNcAlbumItemCopyWithWorkerImpl(this);
}
abstract class $DbRecognizeFaceCopyWithWorker {
DbRecognizeFace call({String? label});
}
class _$DbRecognizeFaceCopyWithWorkerImpl
implements $DbRecognizeFaceCopyWithWorker {
_$DbRecognizeFaceCopyWithWorkerImpl(this.that);
@override
DbRecognizeFace call({dynamic label}) {
return DbRecognizeFace(label: label as String? ?? that.label);
}
final DbRecognizeFace that;
}
extension $DbRecognizeFaceCopyWith on DbRecognizeFace {
$DbRecognizeFaceCopyWithWorker get copyWith => _$copyWith;
$DbRecognizeFaceCopyWithWorker get _$copyWith =>
_$DbRecognizeFaceCopyWithWorkerImpl(this);
}
abstract class $DbRecognizeFaceItemCopyWithWorker {
DbRecognizeFaceItem call(
{String? relativePath,
int? fileId,
int? contentLength,
String? contentType,
String? etag,
DateTime? lastModified,
bool? hasPreview,
String? realPath,
bool? isFavorite,
int? fileMetadataWidth,
int? fileMetadataHeight,
String? faceDetections});
}
class _$DbRecognizeFaceItemCopyWithWorkerImpl
implements $DbRecognizeFaceItemCopyWithWorker {
_$DbRecognizeFaceItemCopyWithWorkerImpl(this.that);
@override
DbRecognizeFaceItem call(
{dynamic relativePath,
dynamic fileId,
dynamic contentLength = copyWithNull,
dynamic contentType = copyWithNull,
dynamic etag = copyWithNull,
dynamic lastModified = copyWithNull,
dynamic hasPreview = copyWithNull,
dynamic realPath = copyWithNull,
dynamic isFavorite = copyWithNull,
dynamic fileMetadataWidth = copyWithNull,
dynamic fileMetadataHeight = copyWithNull,
dynamic faceDetections = copyWithNull}) {
return DbRecognizeFaceItem(
relativePath: relativePath as String? ?? that.relativePath,
fileId: fileId as int? ?? that.fileId,
contentLength: contentLength == copyWithNull
? that.contentLength
: contentLength as int?,
contentType: contentType == copyWithNull
? that.contentType
: contentType as String?,
etag: etag == copyWithNull ? that.etag : etag as String?,
lastModified: lastModified == copyWithNull
? that.lastModified
: lastModified as DateTime?,
hasPreview:
hasPreview == copyWithNull ? that.hasPreview : hasPreview as bool?,
realPath:
realPath == copyWithNull ? that.realPath : realPath as String?,
isFavorite:
isFavorite == copyWithNull ? that.isFavorite : isFavorite as bool?,
fileMetadataWidth: fileMetadataWidth == copyWithNull
? that.fileMetadataWidth
: fileMetadataWidth as int?,
fileMetadataHeight: fileMetadataHeight == copyWithNull
? that.fileMetadataHeight
: fileMetadataHeight as int?,
faceDetections: faceDetections == copyWithNull
? that.faceDetections
: faceDetections as String?);
}
final DbRecognizeFaceItem that;
}
extension $DbRecognizeFaceItemCopyWith on DbRecognizeFaceItem {
$DbRecognizeFaceItemCopyWithWorker get copyWith => _$copyWith;
$DbRecognizeFaceItemCopyWithWorker get _$copyWith =>
_$DbRecognizeFaceItemCopyWithWorkerImpl(this);
}
abstract class $DbTagCopyWithWorker {
DbTag call(
{int? id, String? displayName, bool? userVisible, bool? userAssignable});
}
class _$DbTagCopyWithWorkerImpl implements $DbTagCopyWithWorker {
_$DbTagCopyWithWorkerImpl(this.that);
@override
DbTag call(
{dynamic id,
dynamic displayName,
dynamic userVisible = copyWithNull,
dynamic userAssignable = copyWithNull}) {
return DbTag(
id: id as int? ?? that.id,
displayName: displayName as String? ?? that.displayName,
userVisible: userVisible == copyWithNull
? that.userVisible
: userVisible as bool?,
userAssignable: userAssignable == copyWithNull
? that.userAssignable
: userAssignable as bool?);
}
final DbTag that;
}
extension $DbTagCopyWith on DbTag {
$DbTagCopyWithWorker get copyWith => _$copyWith;
$DbTagCopyWithWorker get _$copyWith => _$DbTagCopyWithWorkerImpl(this);
}
abstract class $DbTrashDataCopyWithWorker {
DbTrashData call(
{String? filename, String? originalLocation, DateTime? deletionTime});
}
class _$DbTrashDataCopyWithWorkerImpl implements $DbTrashDataCopyWithWorker {
_$DbTrashDataCopyWithWorkerImpl(this.that);
@override
DbTrashData call(
{dynamic filename, dynamic originalLocation, dynamic deletionTime}) {
return DbTrashData(
filename: filename as String? ?? that.filename,
originalLocation: originalLocation as String? ?? that.originalLocation,
deletionTime: deletionTime as DateTime? ?? that.deletionTime);
}
final DbTrashData that;
}
extension $DbTrashDataCopyWith on DbTrashData {
$DbTrashDataCopyWithWorker get copyWith => _$copyWith;
$DbTrashDataCopyWithWorker get _$copyWith =>
_$DbTrashDataCopyWithWorkerImpl(this);
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$DbAccountToString on DbAccount {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbAccount {serverAddress: $serverAddress, userId: $userId}";
}
}
extension _$DbAlbumToString on DbAlbum {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbAlbum {fileId: $fileId, fileEtag: $fileEtag, version: $version, lastUpdated: $lastUpdated, name: $name, providerType: $providerType, providerContent: $providerContent, coverProviderType: $coverProviderType, coverProviderContent: $coverProviderContent, sortProviderType: $sortProviderType, sortProviderContent: $sortProviderContent, shares: [length: ${shares.length}]}";
}
}
extension _$DbAlbumShareToString on DbAlbumShare {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbAlbumShare {userId: $userId, displayName: $displayName, sharedAt: $sharedAt}";
}
}
extension _$DbFaceRecognitionPersonToString on DbFaceRecognitionPerson {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbFaceRecognitionPerson {name: $name, thumbFaceId: $thumbFaceId, count: $count}";
}
}
extension _$DbFileToString on DbFile {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbFile {fileId: $fileId, contentLength: $contentLength, contentType: $contentType, etag: $etag, lastModified: $lastModified, isCollection: $isCollection, usedBytes: $usedBytes, hasPreview: $hasPreview, ownerId: $ownerId, ownerDisplayName: $ownerDisplayName, relativePath: $relativePath, isFavorite: $isFavorite, isArchived: $isArchived, overrideDateTime: $overrideDateTime, bestDateTime: $bestDateTime, imageData: $imageData, location: $location, trashData: $trashData}";
}
}
extension _$DbFileDescriptorToString on DbFileDescriptor {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbFileDescriptor {relativePath: $relativePath, fileId: $fileId, contentType: $contentType, isArchived: $isArchived, isFavorite: $isFavorite, bestDateTime: $bestDateTime}";
}
}
extension _$DbImageDataToString on DbImageData {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbImageData {lastUpdated: $lastUpdated, fileEtag: $fileEtag, width: $width, height: $height, exif: $exif, exifDateTimeOriginal: $exifDateTimeOriginal}";
}
}
extension _$DbLocationToString on DbLocation {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbLocation {version: $version, name: $name, latitude: ${latitude == null ? null : "${latitude!.toStringAsFixed(3)}"}, longitude: ${longitude == null ? null : "${longitude!.toStringAsFixed(3)}"}, countryCode: $countryCode, admin1: $admin1, admin2: $admin2}";
}
}
extension _$DbNcAlbumToString on DbNcAlbum {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbNcAlbum {relativePath: $relativePath, lastPhoto: $lastPhoto, nbItems: $nbItems, location: $location, dateStart: $dateStart, dateEnd: $dateEnd, collaborators: [length: ${collaborators.length}], isOwned: $isOwned}";
}
}
extension _$DbNcAlbumItemToString on DbNcAlbumItem {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbNcAlbumItem {relativePath: $relativePath, fileId: $fileId, contentLength: $contentLength, contentType: $contentType, etag: $etag, lastModified: $lastModified, hasPreview: $hasPreview, isFavorite: $isFavorite, fileMetadataWidth: $fileMetadataWidth, fileMetadataHeight: $fileMetadataHeight}";
}
}
extension _$DbRecognizeFaceToString on DbRecognizeFace {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbRecognizeFace {label: $label}";
}
}
extension _$DbRecognizeFaceItemToString on DbRecognizeFaceItem {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbRecognizeFaceItem {relativePath: $relativePath, fileId: $fileId, contentLength: $contentLength, contentType: $contentType, etag: $etag, lastModified: $lastModified, hasPreview: $hasPreview, realPath: $realPath, isFavorite: $isFavorite, fileMetadataWidth: $fileMetadataWidth, fileMetadataHeight: $fileMetadataHeight, faceDetections: $faceDetections}";
}
}
extension _$DbTagToString on DbTag {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbTag {id: $id, displayName: $displayName, userVisible: $userVisible, userAssignable: $userAssignable}";
}
}
extension _$DbTrashDataToString on DbTrashData {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbTrashData {filename: $filename, originalLocation: $originalLocation, deletionTime: $deletionTime}";
}
}

View file

@ -0,0 +1,13 @@
import 'package:to_string/to_string.dart';
part 'exception.g.dart';
@ToString(ignoreNull: true)
class DbNotFoundException implements Exception {
const DbNotFoundException([this.message]);
@override
String toString() => _$toString();
final String? message;
}

View file

@ -0,0 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'exception.dart';
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$DbNotFoundExceptionToString on DbNotFoundException {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "DbNotFoundException {${message == null ? "" : "message: $message"}}";
}
}

53
np_db/pubspec.yaml Normal file
View file

@ -0,0 +1,53 @@
name: np_db
description: A starting point for Dart libraries or applications.
version: 1.0.0
# repository: https://github.com/my_org/my_repo
publish_to: 'none'
environment:
sdk: '>=2.19.6 <3.0.0'
flutter: ">=3.7.0"
dependencies:
copy_with:
git:
url: https://gitlab.com/nkming2/dart-copy-with
path: copy_with
ref: copy_with-1.3.0
equatable: ^2.0.5
flutter:
sdk: flutter
logging: ^1.1.1
np_codegen:
path: ../codegen
np_common:
path: ../np_common
np_datetime:
path: ../np_datetime
np_db_sqlite:
path: ../np_db_sqlite
np_string:
path: ../np_string
to_string:
git:
url: https://gitlab.com/nkming2/dart-to-string
ref: to_string-1.0.0
path: to_string
dev_dependencies:
build_runner: ^2.2.1
copy_with_build:
git:
url: https://gitlab.com/nkming2/dart-copy-with
path: copy_with_build
ref: copy_with_build-1.7.0
np_codegen_build:
path: ../codegen_build
np_lints:
path: ../np_lints
test: ^1.21.0
to_string_build:
git:
url: https://gitlab.com/nkming2/dart-to-string
ref: to_string_build-1.0.0
path: to_string_build

32
np_db_sqlite/.gitignore vendored Normal file
View file

@ -0,0 +1,32 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
.flutter-plugins
.flutter-plugins-dependencies

10
np_db_sqlite/.metadata Normal file
View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
channel: stable
project_type: package

Some files were not shown because too many files have changed in this diff Show more