mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
497 lines
16 KiB
Dart
497 lines
16 KiB
Dart
|
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;
|
||
|
}
|