mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-23 01:06:21 +01:00
252 lines
8.1 KiB
Dart
252 lines
8.1 KiB
Dart
import 'dart:convert';
|
|
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/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/or_null.dart';
|
|
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
|
import 'package:nc_photos/use_case/get_file_binary.dart';
|
|
import 'package:nc_photos/use_case/ls_single_file.dart';
|
|
import 'package:nc_photos/use_case/put_file_binary.dart';
|
|
import 'package:np_codegen/np_codegen.dart';
|
|
import 'package:np_common/type.dart';
|
|
|
|
part 'data_source2.g.dart';
|
|
|
|
@npLog
|
|
class AlbumRemoteDataSource2 implements AlbumDataSource2 {
|
|
const AlbumRemoteDataSource2();
|
|
|
|
@override
|
|
Future<List<Album>> getAlbums(
|
|
Account account,
|
|
List<File> albumFiles, {
|
|
ErrorWithValueHandler<File>? onError,
|
|
}) async {
|
|
final results = await Future.wait(albumFiles.map((f) async {
|
|
try {
|
|
return await _getSingle(account, f);
|
|
} catch (e, stackTrace) {
|
|
onError?.call(f, e, stackTrace);
|
|
return null;
|
|
}
|
|
}));
|
|
return results.whereNotNull().toList();
|
|
}
|
|
|
|
@override
|
|
Future<Album> create(Account account, Album album) async {
|
|
_log.info("[create] ${album.name}");
|
|
final fileName = _makeAlbumFileName();
|
|
final filePath =
|
|
"${remote_storage_util.getRemoteAlbumsDir(account)}/$fileName";
|
|
final c = KiwiContainer().resolve<DiContainer>();
|
|
await PutFileBinary(c.fileRepo)(
|
|
account,
|
|
filePath,
|
|
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())),
|
|
shouldCreateMissingDir: true,
|
|
);
|
|
// query album file
|
|
final newFile = await LsSingleFile(c)(account, filePath);
|
|
return album.copyWith(albumFile: OrNull(newFile));
|
|
}
|
|
|
|
@override
|
|
Future<void> update(Account account, Album album) async {
|
|
_log.info("[update] ${album.albumFile!.path}");
|
|
const fileRepo = FileRepo(FileWebdavDataSource());
|
|
await PutFileBinary(fileRepo)(
|
|
account,
|
|
album.albumFile!.path,
|
|
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())),
|
|
);
|
|
}
|
|
|
|
Future<Album> _getSingle(Account account, File albumFile) async {
|
|
_log.info("[_getSingle] Getting ${albumFile.path}");
|
|
const fileRepo = FileRepo(FileWebdavDataSource());
|
|
final data = await GetFileBinary(fileRepo)(account, albumFile);
|
|
try {
|
|
final album = Album.fromJson(
|
|
jsonDecode(utf8.decode(data)),
|
|
upgraderFactory: DefaultAlbumUpgraderFactory(
|
|
account: account,
|
|
albumFile: albumFile,
|
|
logFilePath: albumFile.path,
|
|
),
|
|
);
|
|
return album!.copyWith(
|
|
lastUpdated: OrNull(null),
|
|
albumFile: OrNull(albumFile),
|
|
);
|
|
} catch (e, stacktrace) {
|
|
dynamic d = data;
|
|
try {
|
|
d = utf8.decode(data);
|
|
} catch (_) {}
|
|
_log.severe("[_getSingle] Invalid json data: $d", e, stacktrace);
|
|
throw const FormatException("Invalid album format");
|
|
}
|
|
}
|
|
|
|
String _makeAlbumFileName() {
|
|
// just make up something
|
|
final timestamp = clock.now().millisecondsSinceEpoch;
|
|
final random = Random().nextInt(0xFFFFFF);
|
|
return "${timestamp.toRadixString(16)}-${random.toRadixString(16).padLeft(6, '0')}.nc_album.json";
|
|
}
|
|
}
|
|
|
|
@npLog
|
|
class AlbumSqliteDbDataSource2 implements AlbumDataSource2 {
|
|
const AlbumSqliteDbDataSource2(this.sqliteDb);
|
|
|
|
@override
|
|
Future<List<Album>> getAlbums(
|
|
Account account,
|
|
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
|
|
return albumFiles
|
|
.map((f) {
|
|
final item = fileIdMap[f.fileId];
|
|
if (item == 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;
|
|
}
|
|
}
|
|
})
|
|
.whereNotNull()
|
|
.toList();
|
|
}
|
|
|
|
@override
|
|
Future<Album> create(Account account, Album album) async {
|
|
_log.info("[create] ${album.name}");
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@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!))),
|
|
);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|