device_files_db.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import 'package:flutter/foundation.dart';
  2. import 'package:logging/logging.dart';
  3. import 'package:photo_manager/photo_manager.dart';
  4. import 'package:photos/db/files_db.dart';
  5. import 'package:photos/models/device_collection.dart';
  6. import 'package:photos/models/file.dart';
  7. import 'package:photos/models/file_load_result.dart';
  8. import 'package:photos/services/local/local_sync_util.dart';
  9. import 'package:sqflite/sqlite_api.dart';
  10. import 'package:tuple/tuple.dart';
  11. extension DeviceFiles on FilesDB {
  12. static final Logger _logger = Logger("DeviceFilesDB");
  13. static const _sqlBoolTrue = 1;
  14. static const _sqlBoolFalse = 0;
  15. Future<void> insertPathIDToLocalIDMapping(
  16. Map<String, Set<String>> mappingToAdd, {
  17. ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.ignore,
  18. }) async {
  19. debugPrint("Inserting missing PathIDToLocalIDMapping");
  20. final db = await database;
  21. var batch = db.batch();
  22. int batchCounter = 0;
  23. for (MapEntry e in mappingToAdd.entries) {
  24. final String pathID = e.key;
  25. for (String localID in e.value) {
  26. if (batchCounter == 400) {
  27. await batch.commit(noResult: true);
  28. batch = db.batch();
  29. batchCounter = 0;
  30. }
  31. batch.insert(
  32. "device_files",
  33. {
  34. "id": localID,
  35. "path_id": pathID,
  36. },
  37. conflictAlgorithm: conflictAlgorithm,
  38. );
  39. batchCounter++;
  40. }
  41. }
  42. await batch.commit(noResult: true);
  43. }
  44. Future<void> deletePathIDToLocalIDMapping(
  45. Map<String, Set<String>> mappingsToRemove,
  46. ) async {
  47. debugPrint("removing PathIDToLocalIDMapping");
  48. final db = await database;
  49. var batch = db.batch();
  50. int batchCounter = 0;
  51. for (MapEntry e in mappingsToRemove.entries) {
  52. final String pathID = e.key;
  53. for (String localID in e.value) {
  54. if (batchCounter == 400) {
  55. await batch.commit(noResult: true);
  56. batch = db.batch();
  57. batchCounter = 0;
  58. }
  59. batch.delete(
  60. "device_files",
  61. where: 'id = ? AND path_id = ?',
  62. whereArgs: [localID, pathID],
  63. );
  64. batchCounter++;
  65. }
  66. }
  67. await batch.commit(noResult: true);
  68. }
  69. Future<Map<String, int>> getDevicePathIDToImportedFileCount() async {
  70. try {
  71. final db = await database;
  72. final rows = await db.rawQuery(
  73. '''
  74. SELECT count(*) as count, path_id
  75. FROM device_files
  76. GROUP BY path_id
  77. ''',
  78. );
  79. final result = <String, int>{};
  80. for (final row in rows) {
  81. result[row['path_id']] = row["count"];
  82. }
  83. return result;
  84. } catch (e) {
  85. _logger.severe("failed to getDevicePathIDToImportedFileCount", e);
  86. rethrow;
  87. }
  88. }
  89. Future<Map<String, Set<String>>> getDevicePathIDToLocalIDMap() async {
  90. try {
  91. final db = await database;
  92. final rows = await db.rawQuery(
  93. ''' SELECT id, path_id FROM device_files; ''',
  94. );
  95. final result = <String, Set<String>>{};
  96. for (final row in rows) {
  97. final String pathID = row['path_id'];
  98. if (!result.containsKey(pathID)) {
  99. result[pathID] = <String>{};
  100. }
  101. result[pathID].add(row['id']);
  102. }
  103. return result;
  104. } catch (e) {
  105. _logger.severe("failed to getDevicePathIDToLocalIDMap", e);
  106. rethrow;
  107. }
  108. }
  109. Future<Set<String>> getDevicePathIDs() async {
  110. final Database db = await database;
  111. final rows = await db.rawQuery(
  112. '''
  113. SELECT id FROM device_collections
  114. ''',
  115. );
  116. final Set<String> result = <String>{};
  117. for (final row in rows) {
  118. result.add(row['id']);
  119. }
  120. return result;
  121. }
  122. // todo: covert it to batch
  123. Future<void> insertLocalAssets(
  124. List<LocalPathAsset> localPathAssets, {
  125. bool shouldAutoBackup = false,
  126. }) async {
  127. final Database db = await database;
  128. final Map<String, Set<String>> pathIDToLocalIDsMap = {};
  129. try {
  130. final Set<String> existingPathIds = await getDevicePathIDs();
  131. for (LocalPathAsset localPathAsset in localPathAssets) {
  132. pathIDToLocalIDsMap[localPathAsset.pathID] = localPathAsset.localIDs;
  133. if (existingPathIds.contains(localPathAsset.pathID)) {
  134. await db.rawUpdate(
  135. "UPDATE device_collections SET name = ? where id = "
  136. "?",
  137. [localPathAsset.pathName, localPathAsset.pathID],
  138. );
  139. } else {
  140. await db.insert(
  141. "device_collections",
  142. {
  143. "id": localPathAsset.pathID,
  144. "name": localPathAsset.pathName,
  145. "should_backup": shouldAutoBackup ? _sqlBoolTrue : _sqlBoolFalse
  146. },
  147. conflictAlgorithm: ConflictAlgorithm.ignore,
  148. );
  149. }
  150. }
  151. // add the mappings for localIDs
  152. if (pathIDToLocalIDsMap.isNotEmpty) {
  153. await insertPathIDToLocalIDMapping(pathIDToLocalIDsMap);
  154. }
  155. } catch (e) {
  156. _logger.severe("failed to save path names", e);
  157. rethrow;
  158. }
  159. }
  160. Future<bool> updateDeviceCoverWithCount(
  161. List<Tuple2<AssetPathEntity, String>> devicePathInfo, {
  162. bool shouldBackup = false,
  163. }) async {
  164. bool hasUpdated = false;
  165. try {
  166. final Database db = await database;
  167. final Set<String> existingPathIds = await getDevicePathIDs();
  168. for (Tuple2<AssetPathEntity, String> tup in devicePathInfo) {
  169. final AssetPathEntity pathEntity = tup.item1;
  170. final String localID = tup.item2;
  171. final bool shouldUpdate = existingPathIds.contains(pathEntity.id);
  172. if (shouldUpdate) {
  173. await db.rawUpdate(
  174. "UPDATE device_collections SET name = ?, cover_id = ?, count"
  175. " = ? where id = ?",
  176. [pathEntity.name, localID, pathEntity.assetCount, pathEntity.id],
  177. );
  178. } else {
  179. hasUpdated = true;
  180. await db.insert(
  181. "device_collections",
  182. {
  183. "id": pathEntity.id,
  184. "name": pathEntity.name,
  185. "count": pathEntity.assetCount,
  186. "cover_id": localID,
  187. "should_backup": shouldBackup ? _sqlBoolTrue : _sqlBoolFalse
  188. },
  189. );
  190. }
  191. }
  192. // delete existing pathIDs which are missing on device
  193. existingPathIds.removeAll(devicePathInfo.map((e) => e.item1.id).toSet());
  194. if (existingPathIds.isNotEmpty) {
  195. hasUpdated = true;
  196. _logger.info('Deleting following pathIds from local $existingPathIds ');
  197. for (String pathID in existingPathIds) {
  198. await db.delete(
  199. "device_collections",
  200. where: 'id = ?',
  201. whereArgs: [pathID],
  202. );
  203. await db.delete(
  204. "device_files",
  205. where: 'path_id = ?',
  206. whereArgs: [pathID],
  207. );
  208. }
  209. }
  210. return hasUpdated;
  211. } catch (e) {
  212. _logger.severe("failed to save path names", e);
  213. rethrow;
  214. }
  215. }
  216. Future<void> updateDevicePathSyncStatus(Map<String, bool> syncStatus) async {
  217. final db = await database;
  218. var batch = db.batch();
  219. int batchCounter = 0;
  220. for (MapEntry e in syncStatus.entries) {
  221. final String pathID = e.key;
  222. if (batchCounter == 400) {
  223. await batch.commit(noResult: true);
  224. batch = db.batch();
  225. batchCounter = 0;
  226. }
  227. batch.update(
  228. "device_collections",
  229. {
  230. "should_backup": e.value ? _sqlBoolTrue : _sqlBoolFalse,
  231. },
  232. where: 'id = ?',
  233. whereArgs: [pathID],
  234. );
  235. batchCounter++;
  236. }
  237. await batch.commit(noResult: true);
  238. }
  239. Future<void> updateDeviceCollection(
  240. String pathID,
  241. int collectionID,
  242. ) async {
  243. final db = await database;
  244. await db.update(
  245. "device_collections",
  246. {"collection_id": collectionID},
  247. where: 'id = ?',
  248. whereArgs: [pathID],
  249. );
  250. return;
  251. }
  252. Future<FileLoadResult> getFilesInDeviceCollection(
  253. DeviceCollection deviceCollection,
  254. int startTime,
  255. int endTime, {
  256. int limit,
  257. bool asc,
  258. }) async {
  259. final db = await database;
  260. final order = (asc ?? false ? 'ASC' : 'DESC');
  261. final String rawQuery = '''
  262. SELECT *
  263. FROM ${FilesDB.filesTable}
  264. WHERE ${FilesDB.columnLocalID} IS NOT NULL AND
  265. ${FilesDB.columnCreationTime} >= $startTime AND
  266. ${FilesDB.columnCreationTime} <= $endTime AND
  267. ${FilesDB.columnLocalID} IN
  268. (SELECT id FROM device_files where path_id = '${deviceCollection.id}' )
  269. ORDER BY ${FilesDB.columnCreationTime} $order , ${FilesDB.columnModificationTime} $order
  270. ''' +
  271. (limit != null ? ' limit $limit;' : ';');
  272. final results = await db.rawQuery(rawQuery);
  273. final files = convertToFiles(results);
  274. return FileLoadResult(files, files.length == limit);
  275. }
  276. Future<List<DeviceCollection>> getDeviceCollections({
  277. bool includeCoverThumbnail = false,
  278. }) async {
  279. debugPrint(
  280. "Fetching DeviceCollections From DB with thumnail = $includeCoverThumbnail");
  281. try {
  282. final db = await database;
  283. final coverFiles = <File>[];
  284. if (includeCoverThumbnail) {
  285. final fileRows = await db.rawQuery(
  286. '''SELECT * FROM FILES where local_id in (select cover_id from device_collections) group by local_id;
  287. ''',
  288. );
  289. final files = convertToFiles(fileRows);
  290. coverFiles.addAll(files);
  291. }
  292. final deviceCollectionRows = await db.rawQuery(
  293. '''SELECT * from device_collections''',
  294. );
  295. final List<DeviceCollection> deviceCollections = [];
  296. for (var row in deviceCollectionRows) {
  297. final DeviceCollection deviceCollection = DeviceCollection(
  298. row["id"],
  299. row['name'],
  300. count: row['count'],
  301. collectionID: row["collection_id"],
  302. coverId: row["cover_id"],
  303. shouldBackup: (row["should_backup"] ?? _sqlBoolFalse) == _sqlBoolTrue,
  304. );
  305. if (includeCoverThumbnail) {
  306. deviceCollection.thumbnail = coverFiles.firstWhere(
  307. (element) => element.localID == deviceCollection.coverId,
  308. orElse: () => null,
  309. );
  310. if (deviceCollection.thumbnail == null) {
  311. //todo: find another image which is already imported in db for
  312. // this collection
  313. _logger.warning(
  314. 'Failed to find coverThumbnail for ${deviceCollection.name}',
  315. );
  316. continue;
  317. }
  318. }
  319. deviceCollections.add(deviceCollection);
  320. }
  321. return deviceCollections;
  322. } catch (e) {
  323. _logger.severe('Failed to getDeviceCollections', e);
  324. rethrow;
  325. }
  326. }
  327. }