2022-09-08 05:19:21 +00:00
|
|
|
// @dart=2.9
|
2022-07-23 05:06:19 +00:00
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:logging/logging.dart';
|
2022-07-24 17:22:12 +00:00
|
|
|
import 'package:photo_manager/photo_manager.dart';
|
2022-07-23 05:06:19 +00:00
|
|
|
import 'package:photos/db/files_db.dart';
|
2022-09-06 16:49:02 +00:00
|
|
|
import 'package:photos/models/device_collection.dart';
|
2022-08-31 19:50:11 +00:00
|
|
|
import 'package:photos/models/file.dart';
|
2022-08-23 04:07:21 +00:00
|
|
|
import 'package:photos/models/file_load_result.dart';
|
2022-09-07 06:03:48 +00:00
|
|
|
import 'package:photos/models/upload_strategy.dart';
|
2022-08-25 09:23:33 +00:00
|
|
|
import 'package:photos/services/local/local_sync_util.dart';
|
2022-07-23 05:06:19 +00:00
|
|
|
import 'package:sqflite/sqlite_api.dart';
|
2022-08-23 05:38:18 +00:00
|
|
|
import 'package:tuple/tuple.dart';
|
2022-07-23 05:06:19 +00:00
|
|
|
|
|
|
|
extension DeviceFiles on FilesDB {
|
|
|
|
static final Logger _logger = Logger("DeviceFilesDB");
|
2022-08-23 11:14:25 +00:00
|
|
|
static const _sqlBoolTrue = 1;
|
|
|
|
static const _sqlBoolFalse = 0;
|
2022-07-23 05:06:19 +00:00
|
|
|
|
2022-08-23 08:00:09 +00:00
|
|
|
Future<void> insertPathIDToLocalIDMapping(
|
2022-09-01 15:09:47 +00:00
|
|
|
Map<String, Set<String>> mappingToAdd, {
|
|
|
|
ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.ignore,
|
|
|
|
}) async {
|
2022-08-23 08:00:09 +00:00
|
|
|
debugPrint("Inserting missing PathIDToLocalIDMapping");
|
|
|
|
final db = await database;
|
|
|
|
var batch = db.batch();
|
|
|
|
int batchCounter = 0;
|
|
|
|
for (MapEntry e in mappingToAdd.entries) {
|
2022-08-30 07:33:54 +00:00
|
|
|
final String pathID = e.key;
|
2022-08-23 08:00:09 +00:00
|
|
|
for (String localID in e.value) {
|
|
|
|
if (batchCounter == 400) {
|
|
|
|
await batch.commit(noResult: true);
|
|
|
|
batch = db.batch();
|
|
|
|
batchCounter = 0;
|
|
|
|
}
|
|
|
|
batch.insert(
|
|
|
|
"device_files",
|
|
|
|
{
|
|
|
|
"id": localID,
|
|
|
|
"path_id": pathID,
|
|
|
|
},
|
|
|
|
conflictAlgorithm: conflictAlgorithm,
|
|
|
|
);
|
|
|
|
batchCounter++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await batch.commit(noResult: true);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> deletePathIDToLocalIDMapping(
|
2022-08-29 18:05:13 +00:00
|
|
|
Map<String, Set<String>> mappingsToRemove,
|
|
|
|
) async {
|
2022-08-23 08:00:09 +00:00
|
|
|
debugPrint("removing PathIDToLocalIDMapping");
|
|
|
|
final db = await database;
|
|
|
|
var batch = db.batch();
|
|
|
|
int batchCounter = 0;
|
|
|
|
for (MapEntry e in mappingsToRemove.entries) {
|
2022-08-30 07:33:54 +00:00
|
|
|
final String pathID = e.key;
|
2022-08-23 08:00:09 +00:00
|
|
|
for (String localID in e.value) {
|
|
|
|
if (batchCounter == 400) {
|
|
|
|
await batch.commit(noResult: true);
|
|
|
|
batch = db.batch();
|
|
|
|
batchCounter = 0;
|
|
|
|
}
|
|
|
|
batch.delete(
|
|
|
|
"device_files",
|
|
|
|
where: 'id = ? AND path_id = ?',
|
|
|
|
whereArgs: [localID, pathID],
|
|
|
|
);
|
|
|
|
batchCounter++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await batch.commit(noResult: true);
|
|
|
|
}
|
|
|
|
|
2022-07-23 05:44:51 +00:00
|
|
|
Future<Map<String, int>> getDevicePathIDToImportedFileCount() async {
|
2022-07-26 09:32:39 +00:00
|
|
|
try {
|
|
|
|
final db = await database;
|
|
|
|
final rows = await db.rawQuery(
|
|
|
|
'''
|
2022-07-23 05:44:51 +00:00
|
|
|
SELECT count(*) as count, path_id
|
2022-07-26 08:31:07 +00:00
|
|
|
FROM device_files
|
2022-07-23 05:44:51 +00:00
|
|
|
GROUP BY path_id
|
|
|
|
''',
|
2022-07-26 08:31:07 +00:00
|
|
|
);
|
|
|
|
final result = <String, int>{};
|
|
|
|
for (final row in rows) {
|
|
|
|
result[row['path_id']] = row["count"];
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
} catch (e) {
|
|
|
|
_logger.severe("failed to getDevicePathIDToImportedFileCount", e);
|
|
|
|
rethrow;
|
2022-07-23 05:44:51 +00:00
|
|
|
}
|
|
|
|
}
|
2022-07-24 17:22:12 +00:00
|
|
|
|
2022-09-01 09:19:35 +00:00
|
|
|
Future<Map<String, Set<String>>> getDevicePathIDToLocalIDMap() async {
|
2022-08-23 08:00:09 +00:00
|
|
|
try {
|
|
|
|
final db = await database;
|
|
|
|
final rows = await db.rawQuery(
|
|
|
|
''' SELECT id, path_id FROM device_files; ''',
|
|
|
|
);
|
|
|
|
final result = <String, Set<String>>{};
|
|
|
|
for (final row in rows) {
|
|
|
|
final String pathID = row['path_id'];
|
|
|
|
if (!result.containsKey(pathID)) {
|
|
|
|
result[pathID] = <String>{};
|
|
|
|
}
|
|
|
|
result[pathID].add(row['id']);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
} catch (e) {
|
|
|
|
_logger.severe("failed to getDevicePathIDToLocalIDMap", e);
|
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-24 17:22:12 +00:00
|
|
|
Future<Set<String>> getDevicePathIDs() async {
|
2022-08-25 10:45:08 +00:00
|
|
|
final Database db = await database;
|
|
|
|
final rows = await db.rawQuery(
|
2022-07-24 17:22:12 +00:00
|
|
|
'''
|
2022-08-31 18:39:39 +00:00
|
|
|
SELECT id FROM device_collections
|
2022-07-24 17:22:12 +00:00
|
|
|
''',
|
|
|
|
);
|
|
|
|
final Set<String> result = <String>{};
|
|
|
|
for (final row in rows) {
|
2022-07-25 07:15:17 +00:00
|
|
|
result.add(row['id']);
|
2022-07-24 17:22:12 +00:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2022-08-23 05:38:18 +00:00
|
|
|
// todo: covert it to batch
|
2022-08-25 09:23:33 +00:00
|
|
|
Future<void> insertLocalAssets(
|
|
|
|
List<LocalPathAsset> localPathAssets, {
|
2022-09-01 15:16:57 +00:00
|
|
|
bool shouldAutoBackup = false,
|
2022-08-23 11:14:25 +00:00
|
|
|
}) async {
|
2022-08-25 09:23:33 +00:00
|
|
|
final Database db = await database;
|
|
|
|
final Map<String, Set<String>> pathIDToLocalIDsMap = {};
|
2022-07-25 07:15:17 +00:00
|
|
|
try {
|
|
|
|
final Set<String> existingPathIds = await getDevicePathIDs();
|
2022-08-25 09:23:33 +00:00
|
|
|
for (LocalPathAsset localPathAsset in localPathAssets) {
|
|
|
|
pathIDToLocalIDsMap[localPathAsset.pathID] = localPathAsset.localIDs;
|
|
|
|
if (existingPathIds.contains(localPathAsset.pathID)) {
|
2022-07-25 07:15:17 +00:00
|
|
|
await db.rawUpdate(
|
2022-08-31 18:39:39 +00:00
|
|
|
"UPDATE device_collections SET name = ? where id = "
|
2022-07-25 07:15:17 +00:00
|
|
|
"?",
|
2022-08-25 09:23:33 +00:00
|
|
|
[localPathAsset.pathName, localPathAsset.pathID],
|
2022-07-25 07:15:17 +00:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
await db.insert(
|
2022-08-31 18:39:39 +00:00
|
|
|
"device_collections",
|
2022-07-25 07:15:17 +00:00
|
|
|
{
|
2022-08-25 09:23:33 +00:00
|
|
|
"id": localPathAsset.pathID,
|
|
|
|
"name": localPathAsset.pathName,
|
2022-09-01 15:16:57 +00:00
|
|
|
"should_backup": shouldAutoBackup ? _sqlBoolTrue : _sqlBoolFalse
|
2022-07-25 07:15:17 +00:00
|
|
|
},
|
2022-08-25 09:23:33 +00:00
|
|
|
conflictAlgorithm: ConflictAlgorithm.ignore,
|
2022-07-25 07:15:17 +00:00
|
|
|
);
|
|
|
|
}
|
2022-07-24 17:22:12 +00:00
|
|
|
}
|
2022-08-25 09:23:33 +00:00
|
|
|
// add the mappings for localIDs
|
2022-08-26 07:05:10 +00:00
|
|
|
if (pathIDToLocalIDsMap.isNotEmpty) {
|
2022-09-01 15:09:47 +00:00
|
|
|
await insertPathIDToLocalIDMapping(pathIDToLocalIDsMap);
|
2022-08-26 07:05:10 +00:00
|
|
|
}
|
2022-07-25 07:15:17 +00:00
|
|
|
} catch (e) {
|
|
|
|
_logger.severe("failed to save path names", e);
|
2022-07-26 05:32:04 +00:00
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-23 08:00:09 +00:00
|
|
|
Future<bool> updateDeviceCoverWithCount(
|
2022-08-24 10:56:44 +00:00
|
|
|
List<Tuple2<AssetPathEntity, String>> devicePathInfo, {
|
2022-08-31 18:51:17 +00:00
|
|
|
bool shouldBackup = false,
|
2022-08-23 11:14:25 +00:00
|
|
|
}) async {
|
2022-08-23 08:00:09 +00:00
|
|
|
bool hasUpdated = false;
|
2022-07-26 05:32:04 +00:00
|
|
|
try {
|
|
|
|
final Database db = await database;
|
2022-08-23 05:38:18 +00:00
|
|
|
final Set<String> existingPathIds = await getDevicePathIDs();
|
2022-08-24 10:56:44 +00:00
|
|
|
for (Tuple2<AssetPathEntity, String> tup in devicePathInfo) {
|
2022-08-30 07:33:54 +00:00
|
|
|
final AssetPathEntity pathEntity = tup.item1;
|
|
|
|
final String localID = tup.item2;
|
|
|
|
final bool shouldUpdate = existingPathIds.contains(pathEntity.id);
|
2022-08-23 08:00:09 +00:00
|
|
|
if (shouldUpdate) {
|
2022-09-07 08:15:48 +00:00
|
|
|
final rowUpdated = await db.rawUpdate(
|
2022-08-31 18:39:39 +00:00
|
|
|
"UPDATE device_collections SET name = ?, cover_id = ?, count"
|
2022-09-07 08:15:48 +00:00
|
|
|
" = ? where id = ? AND (name != ? OR cover_id != ? OR count != ?)",
|
|
|
|
[
|
|
|
|
pathEntity.name,
|
|
|
|
localID,
|
|
|
|
pathEntity.assetCount,
|
|
|
|
pathEntity.id,
|
|
|
|
pathEntity.name,
|
|
|
|
localID,
|
|
|
|
pathEntity.assetCount,
|
|
|
|
],
|
2022-08-23 05:38:18 +00:00
|
|
|
);
|
2022-09-07 08:15:48 +00:00
|
|
|
if (rowUpdated > 0) {
|
|
|
|
_logger.fine("Updated $rowUpdated rows for ${pathEntity.name}");
|
|
|
|
hasUpdated = true;
|
|
|
|
}
|
2022-08-23 05:38:18 +00:00
|
|
|
} else {
|
2022-08-23 08:00:09 +00:00
|
|
|
hasUpdated = true;
|
2022-08-23 05:38:18 +00:00
|
|
|
await db.insert(
|
2022-08-31 18:39:39 +00:00
|
|
|
"device_collections",
|
2022-08-23 05:38:18 +00:00
|
|
|
{
|
|
|
|
"id": pathEntity.id,
|
|
|
|
"name": pathEntity.name,
|
|
|
|
"count": pathEntity.assetCount,
|
|
|
|
"cover_id": localID,
|
2022-08-31 18:51:17 +00:00
|
|
|
"should_backup": shouldBackup ? _sqlBoolTrue : _sqlBoolFalse
|
2022-08-23 05:38:18 +00:00
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2022-08-23 08:00:09 +00:00
|
|
|
// delete existing pathIDs which are missing on device
|
|
|
|
existingPathIds.removeAll(devicePathInfo.map((e) => e.item1.id).toSet());
|
|
|
|
if (existingPathIds.isNotEmpty) {
|
|
|
|
hasUpdated = true;
|
|
|
|
_logger.info('Deleting following pathIds from local $existingPathIds ');
|
|
|
|
for (String pathID in existingPathIds) {
|
|
|
|
await db.delete(
|
2022-08-31 18:39:39 +00:00
|
|
|
"device_collections",
|
2022-08-23 08:00:09 +00:00
|
|
|
where: 'id = ?',
|
|
|
|
whereArgs: [pathID],
|
|
|
|
);
|
|
|
|
await db.delete(
|
|
|
|
"device_files",
|
|
|
|
where: 'path_id = ?',
|
|
|
|
whereArgs: [pathID],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return hasUpdated;
|
2022-07-26 05:32:04 +00:00
|
|
|
} catch (e) {
|
|
|
|
_logger.severe("failed to save path names", e);
|
|
|
|
rethrow;
|
2022-07-24 17:22:12 +00:00
|
|
|
}
|
|
|
|
}
|
2022-07-26 07:38:04 +00:00
|
|
|
|
2022-08-23 11:14:25 +00:00
|
|
|
Future<void> updateDevicePathSyncStatus(Map<String, bool> syncStatus) async {
|
|
|
|
final db = await database;
|
|
|
|
var batch = db.batch();
|
|
|
|
int batchCounter = 0;
|
|
|
|
for (MapEntry e in syncStatus.entries) {
|
2022-08-30 07:33:54 +00:00
|
|
|
final String pathID = e.key;
|
2022-08-23 11:14:25 +00:00
|
|
|
if (batchCounter == 400) {
|
|
|
|
await batch.commit(noResult: true);
|
|
|
|
batch = db.batch();
|
|
|
|
batchCounter = 0;
|
|
|
|
}
|
|
|
|
batch.update(
|
2022-08-31 18:39:39 +00:00
|
|
|
"device_collections",
|
2022-08-23 11:14:25 +00:00
|
|
|
{
|
2022-08-31 18:51:17 +00:00
|
|
|
"should_backup": e.value ? _sqlBoolTrue : _sqlBoolFalse,
|
2022-08-23 11:14:25 +00:00
|
|
|
},
|
|
|
|
where: 'id = ?',
|
|
|
|
whereArgs: [pathID],
|
|
|
|
);
|
|
|
|
batchCounter++;
|
|
|
|
}
|
|
|
|
await batch.commit(noResult: true);
|
|
|
|
}
|
|
|
|
|
2022-08-31 18:39:39 +00:00
|
|
|
Future<void> updateDeviceCollection(
|
2022-08-24 04:38:16 +00:00
|
|
|
String pathID,
|
|
|
|
int collectionID,
|
|
|
|
) async {
|
|
|
|
final db = await database;
|
|
|
|
await db.update(
|
2022-08-31 18:39:39 +00:00
|
|
|
"device_collections",
|
2022-08-24 04:38:16 +00:00
|
|
|
{"collection_id": collectionID},
|
|
|
|
where: 'id = ?',
|
|
|
|
whereArgs: [pathID],
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-08-31 18:39:39 +00:00
|
|
|
Future<FileLoadResult> getFilesInDeviceCollection(
|
|
|
|
DeviceCollection deviceCollection,
|
2022-08-23 04:07:21 +00:00
|
|
|
int startTime,
|
|
|
|
int endTime, {
|
|
|
|
int limit,
|
|
|
|
bool asc,
|
|
|
|
}) async {
|
|
|
|
final db = await database;
|
|
|
|
final order = (asc ?? false ? 'ASC' : 'DESC');
|
2022-08-30 07:33:54 +00:00
|
|
|
final String rawQuery = '''
|
2022-08-23 04:07:21 +00:00
|
|
|
SELECT *
|
|
|
|
FROM ${FilesDB.filesTable}
|
|
|
|
WHERE ${FilesDB.columnLocalID} IS NOT NULL AND
|
|
|
|
${FilesDB.columnCreationTime} >= $startTime AND
|
|
|
|
${FilesDB.columnCreationTime} <= $endTime AND
|
|
|
|
${FilesDB.columnLocalID} IN
|
2022-08-31 18:39:39 +00:00
|
|
|
(SELECT id FROM device_files where path_id = '${deviceCollection.id}' )
|
2022-08-26 04:56:19 +00:00
|
|
|
ORDER BY ${FilesDB.columnCreationTime} $order , ${FilesDB.columnModificationTime} $order
|
|
|
|
''' +
|
|
|
|
(limit != null ? ' limit $limit;' : ';');
|
2022-08-23 04:07:21 +00:00
|
|
|
final results = await db.rawQuery(rawQuery);
|
|
|
|
final files = convertToFiles(results);
|
2022-08-26 11:11:44 +00:00
|
|
|
final dedupe = deduplicateByLocalID(files);
|
|
|
|
return FileLoadResult(dedupe, files.length == limit);
|
2022-08-23 04:07:21 +00:00
|
|
|
}
|
|
|
|
|
2022-08-31 19:50:11 +00:00
|
|
|
Future<List<DeviceCollection>> getDeviceCollections({
|
|
|
|
bool includeCoverThumbnail = false,
|
|
|
|
}) async {
|
|
|
|
debugPrint(
|
2022-09-07 08:15:48 +00:00
|
|
|
"Fetching DeviceCollections From DB with thumbnail = "
|
|
|
|
"$includeCoverThumbnail",
|
|
|
|
);
|
2022-07-26 09:32:39 +00:00
|
|
|
try {
|
|
|
|
final db = await database;
|
2022-08-31 19:50:11 +00:00
|
|
|
final coverFiles = <File>[];
|
|
|
|
if (includeCoverThumbnail) {
|
|
|
|
final fileRows = await db.rawQuery(
|
|
|
|
'''SELECT * FROM FILES where local_id in (select cover_id from device_collections) group by local_id;
|
2022-07-26 07:38:04 +00:00
|
|
|
''',
|
2022-08-31 19:50:11 +00:00
|
|
|
);
|
|
|
|
final files = convertToFiles(fileRows);
|
|
|
|
coverFiles.addAll(files);
|
|
|
|
}
|
2022-08-31 18:39:39 +00:00
|
|
|
final deviceCollectionRows = await db.rawQuery(
|
|
|
|
'''SELECT * from device_collections''',
|
2022-07-26 07:38:04 +00:00
|
|
|
);
|
2022-08-31 18:39:39 +00:00
|
|
|
final List<DeviceCollection> deviceCollections = [];
|
|
|
|
for (var row in deviceCollectionRows) {
|
|
|
|
final DeviceCollection deviceCollection = DeviceCollection(
|
2022-07-26 09:32:39 +00:00
|
|
|
row["id"],
|
|
|
|
row['name'],
|
|
|
|
count: row['count'],
|
|
|
|
collectionID: row["collection_id"],
|
|
|
|
coverId: row["cover_id"],
|
2022-08-31 18:51:17 +00:00
|
|
|
shouldBackup: (row["should_backup"] ?? _sqlBoolFalse) == _sqlBoolTrue,
|
2022-09-07 06:03:48 +00:00
|
|
|
uploadStrategy: getUploadType(row["upload_strategy"] ?? 0),
|
2022-07-26 09:32:39 +00:00
|
|
|
);
|
2022-08-31 19:50:11 +00:00
|
|
|
if (includeCoverThumbnail) {
|
|
|
|
deviceCollection.thumbnail = coverFiles.firstWhere(
|
|
|
|
(element) => element.localID == deviceCollection.coverId,
|
|
|
|
orElse: () => null,
|
|
|
|
);
|
|
|
|
if (deviceCollection.thumbnail == null) {
|
2022-08-31 19:57:48 +00:00
|
|
|
//todo: find another image which is already imported in db for
|
|
|
|
// this collection
|
2022-08-31 19:50:11 +00:00
|
|
|
_logger.warning(
|
2022-08-31 19:57:48 +00:00
|
|
|
'Failed to find coverThumbnail for ${deviceCollection.name}',
|
|
|
|
);
|
2022-08-31 19:50:11 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
2022-09-01 09:19:35 +00:00
|
|
|
deviceCollections.add(deviceCollection);
|
2022-07-26 09:32:39 +00:00
|
|
|
}
|
|
|
|
return deviceCollections;
|
|
|
|
} catch (e) {
|
2022-08-31 18:39:39 +00:00
|
|
|
_logger.severe('Failed to getDeviceCollections', e);
|
2022-07-26 09:32:39 +00:00
|
|
|
rethrow;
|
2022-07-26 07:38:04 +00:00
|
|
|
}
|
|
|
|
}
|
2022-07-23 05:06:19 +00:00
|
|
|
}
|