files_db.dart 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. import 'dart:io';
  2. import 'package:logging/logging.dart';
  3. import 'package:photos/models/backup_status.dart';
  4. import 'package:photos/models/file_load_result.dart';
  5. import 'package:photos/models/file_type.dart';
  6. import 'package:photos/models/location.dart';
  7. import 'package:photos/models/file.dart';
  8. import 'package:path/path.dart';
  9. import 'package:sqflite/sqflite.dart';
  10. import 'package:path_provider/path_provider.dart';
  11. import 'package:sqflite_migration/sqflite_migration.dart';
  12. class FilesDB {
  13. /*
  14. Note: columnUploadedFileID and columnCollectionID have to be compared against
  15. both NULL and -1 because older clients might have entries where the DEFAULT
  16. was unset, and a migration script to set the DEFAULT would break in case of
  17. duplicate entries for un-uploaded files that were created due to a collision
  18. in background and foreground syncs.
  19. */
  20. static final _databaseName = "ente.files.db";
  21. static final Logger _logger = Logger("FilesDB");
  22. static final table = 'files';
  23. static final tempTable = 'temp_files';
  24. static final columnGeneratedID = '_id';
  25. static final columnUploadedFileID = 'uploaded_file_id';
  26. static final columnOwnerID = 'owner_id';
  27. static final columnCollectionID = 'collection_id';
  28. static final columnLocalID = 'local_id';
  29. static final columnTitle = 'title';
  30. static final columnDeviceFolder = 'device_folder';
  31. static final columnLatitude = 'latitude';
  32. static final columnLongitude = 'longitude';
  33. static final columnFileType = 'file_type';
  34. static final columnIsDeleted = 'is_deleted';
  35. static final columnCreationTime = 'creation_time';
  36. static final columnModificationTime = 'modification_time';
  37. static final columnUpdationTime = 'updation_time';
  38. static final columnEncryptedKey = 'encrypted_key';
  39. static final columnKeyDecryptionNonce = 'key_decryption_nonce';
  40. static final columnFileDecryptionHeader = 'file_decryption_header';
  41. static final columnThumbnailDecryptionHeader = 'thumbnail_decryption_header';
  42. static final columnMetadataDecryptionHeader = 'metadata_decryption_header';
  43. static final intitialScript = [...createTable(table), ...addIndex()];
  44. static final migrationScripts = [...alterDeviceFolderToAllowNULL()];
  45. final dbConfig = MigrationConfig(
  46. initializationScript: intitialScript, migrationScripts: migrationScripts);
  47. // make this a singleton class
  48. FilesDB._privateConstructor();
  49. static final FilesDB instance = FilesDB._privateConstructor();
  50. // only have a single app-wide reference to the database
  51. static Database _database;
  52. Future<Database> get database async {
  53. if (_database != null) return _database;
  54. // lazily instantiate the db the first time it is accessed
  55. _database = await _initDatabase();
  56. return _database;
  57. }
  58. // this opens the database (and creates it if it doesn't exist)
  59. _initDatabase() async {
  60. Directory documentsDirectory = await getApplicationDocumentsDirectory();
  61. String path = join(documentsDirectory.path, _databaseName);
  62. return await openDatabaseWithMigration(path, dbConfig);
  63. }
  64. // SQL code to create the database table
  65. static List<String> createTable(String tableName) {
  66. return [
  67. '''
  68. CREATE TABLE $tableName (
  69. $columnGeneratedID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  70. $columnLocalID TEXT,
  71. $columnUploadedFileID INTEGER DEFAULT -1,
  72. $columnOwnerID INTEGER,
  73. $columnCollectionID INTEGER DEFAULT -1,
  74. $columnTitle TEXT NOT NULL,
  75. $columnDeviceFolder TEXT,
  76. $columnLatitude REAL,
  77. $columnLongitude REAL,
  78. $columnFileType INTEGER,
  79. $columnModificationTime TEXT NOT NULL,
  80. $columnEncryptedKey TEXT,
  81. $columnKeyDecryptionNonce TEXT,
  82. $columnFileDecryptionHeader TEXT,
  83. $columnThumbnailDecryptionHeader TEXT,
  84. $columnMetadataDecryptionHeader TEXT,
  85. $columnIsDeleted INTEGER DEFAULT 0,
  86. $columnCreationTime TEXT NOT NULL,
  87. $columnUpdationTime TEXT,
  88. UNIQUE($columnLocalID, $columnUploadedFileID, $columnCollectionID)
  89. );
  90. ''',
  91. ];
  92. }
  93. static List<String> addIndex() {
  94. return [
  95. '''
  96. CREATE INDEX collection_id_index ON $table($columnCollectionID);
  97. CREATE INDEX device_folder_index ON $table($columnDeviceFolder);
  98. CREATE INDEX creation_time_index ON $table($columnCreationTime);
  99. CREATE INDEX updation_time_index ON $table($columnUpdationTime);
  100. '''
  101. ];
  102. }
  103. static List<String> alterDeviceFolderToAllowNULL() {
  104. return [
  105. ...createTable(tempTable),
  106. '''
  107. INSERT INTO $tempTable
  108. SELECT *
  109. FROM $table;
  110. DROP TABLE $table;
  111. ALTER TABLE $tempTable
  112. RENAME TO $table;
  113. '''
  114. ];
  115. }
  116. Future<void> clearTable() async {
  117. final db = await instance.database;
  118. await db.delete(table);
  119. }
  120. Future<void> insertMultiple(List<File> files) async {
  121. final startTime = DateTime.now();
  122. final db = await instance.database;
  123. var batch = db.batch();
  124. int batchCounter = 0;
  125. for (File file in files) {
  126. if (batchCounter == 400) {
  127. await batch.commit(noResult: true);
  128. batch = db.batch();
  129. batchCounter = 0;
  130. }
  131. batch.insert(
  132. table,
  133. _getRowForFile(file),
  134. conflictAlgorithm: ConflictAlgorithm.replace,
  135. );
  136. batchCounter++;
  137. }
  138. await batch.commit(noResult: true);
  139. final endTime = DateTime.now();
  140. final duration = Duration(
  141. microseconds:
  142. endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch);
  143. _logger.info("Batch insert of " +
  144. files.length.toString() +
  145. " took " +
  146. duration.inMilliseconds.toString() +
  147. "ms.");
  148. }
  149. Future<int> insert(File file) async {
  150. final db = await instance.database;
  151. return db.insert(
  152. table,
  153. _getRowForFile(file),
  154. conflictAlgorithm: ConflictAlgorithm.replace,
  155. );
  156. }
  157. Future<File> getFile(int generatedID) async {
  158. final db = await instance.database;
  159. final results = await db.query(table,
  160. where: '$columnGeneratedID = ?', whereArgs: [generatedID]);
  161. if (results.isEmpty) {
  162. return null;
  163. }
  164. return _convertToFiles(results)[0];
  165. }
  166. Future<File> getUploadedFile(int uploadedID, int collectionID) async {
  167. final db = await instance.database;
  168. final results = await db.query(
  169. table,
  170. where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
  171. whereArgs: [
  172. uploadedID,
  173. collectionID,
  174. ],
  175. );
  176. if (results.isEmpty) {
  177. return null;
  178. }
  179. return _convertToFiles(results)[0];
  180. }
  181. Future<Set<int>> getUploadedFileIDs(int collectionID) async {
  182. final db = await instance.database;
  183. final results = await db.query(
  184. table,
  185. columns: [columnUploadedFileID],
  186. where: '$columnCollectionID = ?',
  187. whereArgs: [
  188. collectionID,
  189. ],
  190. );
  191. final ids = Set<int>();
  192. for (final result in results) {
  193. ids.add(result[columnUploadedFileID]);
  194. }
  195. return ids;
  196. }
  197. Future<BackedUpFileIDs> getBackedUpIDs() async {
  198. final db = await instance.database;
  199. final results = await db.query(
  200. table,
  201. columns: [columnLocalID, columnUploadedFileID],
  202. where:
  203. '$columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
  204. );
  205. final localIDs = Set<String>();
  206. final uploadedIDs = Set<int>();
  207. for (final result in results) {
  208. localIDs.add(result[columnLocalID]);
  209. uploadedIDs.add(result[columnUploadedFileID]);
  210. }
  211. return BackedUpFileIDs(localIDs.toList(), uploadedIDs.toList());
  212. }
  213. Future<FileLoadResult> getAllFiles(int startTime, int endTime,
  214. {int limit, bool asc}) async {
  215. final db = await instance.database;
  216. final order = (asc ?? false ? 'ASC' : 'DESC');
  217. final results = await db.query(
  218. table,
  219. where:
  220. '$columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnIsDeleted = 0 AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
  221. whereArgs: [startTime, endTime],
  222. orderBy:
  223. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  224. limit: limit,
  225. );
  226. final files = _convertToFiles(results);
  227. return FileLoadResult(files, files.length == limit);
  228. }
  229. Future<FileLoadResult> getFilesInPaths(
  230. int startTime, int endTime, List<String> paths,
  231. {int limit, bool asc}) async {
  232. final db = await instance.database;
  233. String inParam = "";
  234. for (final path in paths) {
  235. inParam += "'" + path.replaceAll("'", "''") + "',";
  236. }
  237. inParam = inParam.substring(0, inParam.length - 1);
  238. final order = (asc ?? false ? 'ASC' : 'DESC');
  239. final results = await db.query(
  240. table,
  241. where:
  242. '$columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnIsDeleted = 0 AND (($columnLocalID IS NOT NULL AND $columnDeviceFolder IN ($inParam)) OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
  243. whereArgs: [startTime, endTime],
  244. orderBy:
  245. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  246. limit: limit,
  247. );
  248. final uploadedFileIDs = Set<int>();
  249. final files = _convertToFiles(results);
  250. final List<File> deduplicatedFiles = [];
  251. for (final file in files) {
  252. final id = file.uploadedFileID;
  253. if (id != null && id != -1 && uploadedFileIDs.contains(id)) {
  254. continue;
  255. }
  256. uploadedFileIDs.add(id);
  257. deduplicatedFiles.add(file);
  258. }
  259. return FileLoadResult(deduplicatedFiles, files.length == limit);
  260. }
  261. Future<FileLoadResult> getFilesInCollection(
  262. int collectionID, int startTime, int endTime,
  263. {int limit, bool asc}) async {
  264. final db = await instance.database;
  265. final order = (asc ?? false ? 'ASC' : 'DESC');
  266. final results = await db.query(
  267. table,
  268. where:
  269. '$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnIsDeleted = 0',
  270. whereArgs: [collectionID, startTime, endTime],
  271. orderBy:
  272. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  273. limit: limit,
  274. );
  275. final files = _convertToFiles(results);
  276. _logger.info("Fetched " + files.length.toString() + " files");
  277. return FileLoadResult(files, files.length == limit);
  278. }
  279. Future<FileLoadResult> getFilesInPath(String path, int startTime, int endTime,
  280. {int limit, bool asc}) async {
  281. final db = await instance.database;
  282. final order = (asc ?? false ? 'ASC' : 'DESC');
  283. final results = await db.query(
  284. table,
  285. where:
  286. '$columnDeviceFolder = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnLocalID IS NOT NULL AND $columnIsDeleted = 0',
  287. whereArgs: [path, startTime, endTime],
  288. orderBy:
  289. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  290. groupBy: '$columnLocalID',
  291. limit: limit,
  292. );
  293. final files = _convertToFiles(results);
  294. return FileLoadResult(files, files.length == limit);
  295. }
  296. Future<List<File>> getAllVideos() async {
  297. final db = await instance.database;
  298. final results = await db.query(
  299. table,
  300. where:
  301. '$columnLocalID IS NOT NULL AND $columnFileType = 1 AND $columnIsDeleted = 0',
  302. orderBy: '$columnCreationTime DESC',
  303. );
  304. return _convertToFiles(results);
  305. }
  306. Future<List<File>> getAllInPath(String path) async {
  307. final db = await instance.database;
  308. final results = await db.query(
  309. table,
  310. where:
  311. '$columnLocalID IS NOT NULL AND $columnDeviceFolder = ? AND $columnIsDeleted = 0',
  312. whereArgs: [path],
  313. orderBy: '$columnCreationTime DESC',
  314. groupBy: '$columnLocalID',
  315. );
  316. return _convertToFiles(results);
  317. }
  318. Future<List<File>> getFilesCreatedWithinDurations(
  319. List<List<int>> durations) async {
  320. final db = await instance.database;
  321. String whereClause = "";
  322. for (int index = 0; index < durations.length; index++) {
  323. whereClause += "($columnCreationTime > " +
  324. durations[index][0].toString() +
  325. " AND $columnCreationTime < " +
  326. durations[index][1].toString() +
  327. ")";
  328. if (index != durations.length - 1) {
  329. whereClause += " OR ";
  330. }
  331. }
  332. final results = await db.query(
  333. table,
  334. where: whereClause + " AND $columnIsDeleted = 0",
  335. orderBy: '$columnCreationTime ASC',
  336. );
  337. return _convertToFiles(results);
  338. }
  339. Future<List<int>> getDeletedFileIDs() async {
  340. final db = await instance.database;
  341. final rows = await db.query(
  342. table,
  343. columns: [columnUploadedFileID],
  344. distinct: true,
  345. where: '$columnIsDeleted = 1',
  346. orderBy: '$columnCreationTime DESC',
  347. );
  348. final result = List<int>();
  349. for (final row in rows) {
  350. result.add(row[columnUploadedFileID]);
  351. }
  352. return result;
  353. }
  354. Future<List<File>> getFilesToBeUploadedWithinFolders(
  355. Set<String> folders) async {
  356. if (folders.isEmpty) {
  357. return [];
  358. }
  359. final db = await instance.database;
  360. String inParam = "";
  361. for (final folder in folders) {
  362. inParam += "'" + folder.replaceAll("'", "''") + "',";
  363. }
  364. inParam = inParam.substring(0, inParam.length - 1);
  365. final results = await db.query(
  366. table,
  367. where:
  368. '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND $columnDeviceFolder IN ($inParam)',
  369. orderBy: '$columnCreationTime DESC',
  370. groupBy: '$columnLocalID',
  371. );
  372. return _convertToFiles(results);
  373. }
  374. Future<List<File>> getEditedRemoteFiles() async {
  375. final db = await instance.database;
  376. final results = await db.query(
  377. table,
  378. where:
  379. '($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1) AND ($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1)',
  380. orderBy: '$columnCreationTime DESC',
  381. groupBy: '$columnLocalID',
  382. );
  383. return _convertToFiles(results);
  384. }
  385. Future<List<int>> getUploadedFileIDsToBeUpdated() async {
  386. final db = await instance.database;
  387. final rows = await db.query(
  388. table,
  389. columns: [columnUploadedFileID],
  390. where:
  391. '($columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NULL AND $columnIsDeleted = 0)',
  392. orderBy: '$columnCreationTime DESC',
  393. distinct: true,
  394. );
  395. final uploadedFileIDs = List<int>();
  396. for (final row in rows) {
  397. uploadedFileIDs.add(row[columnUploadedFileID]);
  398. }
  399. return uploadedFileIDs;
  400. }
  401. Future<File> getUploadedFileInAnyCollection(int uploadedFileID) async {
  402. final db = await instance.database;
  403. final results = await db.query(
  404. table,
  405. where: '$columnUploadedFileID = ?',
  406. whereArgs: [
  407. uploadedFileID,
  408. ],
  409. limit: 1,
  410. );
  411. if (results.isEmpty) {
  412. return null;
  413. }
  414. return _convertToFiles(results)[0];
  415. }
  416. Future<Set<String>> getExistingLocalFileIDs() async {
  417. final db = await instance.database;
  418. final rows = await db.query(
  419. table,
  420. columns: [columnLocalID],
  421. distinct: true,
  422. where: '$columnLocalID IS NOT NULL',
  423. );
  424. final result = Set<String>();
  425. for (final row in rows) {
  426. result.add(row[columnLocalID]);
  427. }
  428. return result;
  429. }
  430. Future<int> getNumberOfUploadedFiles() async {
  431. final db = await instance.database;
  432. final rows = await db.query(
  433. table,
  434. columns: [columnUploadedFileID],
  435. where:
  436. '($columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NOT NULL AND $columnIsDeleted = 0)',
  437. distinct: true,
  438. );
  439. return rows.length;
  440. }
  441. Future<int> updateUploadedFile(
  442. String localID,
  443. String title,
  444. Location location,
  445. int creationTime,
  446. int modificationTime,
  447. int updationTime,
  448. ) async {
  449. final db = await instance.database;
  450. return await db.update(
  451. table,
  452. {
  453. columnTitle: title,
  454. columnLatitude: location.latitude,
  455. columnLongitude: location.longitude,
  456. columnCreationTime: creationTime,
  457. columnModificationTime: modificationTime,
  458. columnUpdationTime: updationTime,
  459. },
  460. where: '$columnLocalID = ?',
  461. whereArgs: [localID],
  462. );
  463. }
  464. Future<List<File>> getMatchingFiles(
  465. String title,
  466. String deviceFolder,
  467. ) async {
  468. final db = await instance.database;
  469. final rows = await db.query(
  470. table,
  471. where: '''$columnTitle=? AND $columnDeviceFolder=?''',
  472. whereArgs: [
  473. title,
  474. deviceFolder,
  475. ],
  476. );
  477. if (rows.isNotEmpty) {
  478. return _convertToFiles(rows);
  479. } else {
  480. return null;
  481. }
  482. }
  483. Future<int> update(File file) async {
  484. final db = await instance.database;
  485. return await db.update(
  486. table,
  487. _getRowForFile(file),
  488. where: '$columnGeneratedID = ?',
  489. whereArgs: [file.generatedID],
  490. );
  491. }
  492. Future<int> updateUploadedFileAcrossCollections(File file) async {
  493. final db = await instance.database;
  494. return await db.update(
  495. table,
  496. _getRowForFileWithoutCollection(file),
  497. where: '$columnUploadedFileID = ?',
  498. whereArgs: [file.uploadedFileID],
  499. );
  500. }
  501. Future<int> delete(int uploadedFileID) async {
  502. final db = await instance.database;
  503. return db.delete(
  504. table,
  505. where: '$columnUploadedFileID =?',
  506. whereArgs: [uploadedFileID],
  507. );
  508. }
  509. Future<int> deleteMultipleUploadedFiles(List<int> uploadedFileIDs) async {
  510. final db = await instance.database;
  511. return await db.delete(
  512. table,
  513. where: '$columnUploadedFileID IN (${uploadedFileIDs.join(', ')})',
  514. );
  515. }
  516. Future<int> deleteLocalFile(String localID) async {
  517. final db = await instance.database;
  518. return db.delete(
  519. table,
  520. where: '$columnLocalID =?',
  521. whereArgs: [localID],
  522. );
  523. }
  524. Future<void> deleteLocalFiles(List<String> localIDs) async {
  525. String inParam = "";
  526. for (final localID in localIDs) {
  527. inParam += "'" + localID + "',";
  528. }
  529. inParam = inParam.substring(0, inParam.length - 1);
  530. final db = await instance.database;
  531. await db.rawQuery('''
  532. UPDATE $table
  533. SET $columnLocalID = NULL
  534. WHERE $columnLocalID IN ($inParam);
  535. ''');
  536. }
  537. Future<List<File>> getLocalFiles(List<String> localIDs) async {
  538. String inParam = "";
  539. for (final localID in localIDs) {
  540. inParam += "'" + localID + "',";
  541. }
  542. inParam = inParam.substring(0, inParam.length - 1);
  543. final db = await instance.database;
  544. final results = await db.query(
  545. table,
  546. where: '$columnLocalID IN ($inParam)',
  547. );
  548. return _convertToFiles(results);
  549. }
  550. Future<int> deleteFromCollection(int uploadedFileID, int collectionID) async {
  551. final db = await instance.database;
  552. return db.delete(
  553. table,
  554. where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
  555. whereArgs: [uploadedFileID, collectionID],
  556. );
  557. }
  558. Future<int> deleteCollection(int collectionID) async {
  559. final db = await instance.database;
  560. return db.delete(
  561. table,
  562. where: '$columnCollectionID = ?',
  563. whereArgs: [collectionID],
  564. );
  565. }
  566. Future<int> removeFromCollection(int collectionID, List<int> fileIDs) async {
  567. final db = await instance.database;
  568. return db.delete(
  569. table,
  570. where:
  571. '$columnCollectionID =? AND $columnUploadedFileID IN (${fileIDs.join(', ')})',
  572. whereArgs: [collectionID],
  573. );
  574. }
  575. Future<List<File>> getLatestLocalFiles() async {
  576. final db = await instance.database;
  577. final rows = await db.rawQuery('''
  578. SELECT $table.*
  579. FROM $table
  580. INNER JOIN
  581. (
  582. SELECT $columnDeviceFolder, MAX($columnCreationTime) AS max_creation_time
  583. FROM $table
  584. WHERE $table.$columnLocalID IS NOT NULL
  585. GROUP BY $columnDeviceFolder
  586. ) latest_files
  587. ON $table.$columnDeviceFolder = latest_files.$columnDeviceFolder
  588. AND $table.$columnCreationTime = latest_files.max_creation_time;
  589. ''');
  590. final files = _convertToFiles(rows);
  591. // TODO: Do this de-duplication within the SQL Query
  592. final folderMap = Map<String, File>();
  593. for (final file in files) {
  594. if (folderMap.containsKey(file.deviceFolder)) {
  595. if (folderMap[file.deviceFolder].updationTime < file.updationTime) {
  596. continue;
  597. }
  598. }
  599. folderMap[file.deviceFolder] = file;
  600. }
  601. return folderMap.values.toList();
  602. }
  603. Future<List<File>> getLatestCollectionFiles() async {
  604. final db = await instance.database;
  605. final rows = await db.rawQuery('''
  606. SELECT $table.*
  607. FROM $table
  608. INNER JOIN
  609. (
  610. SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time
  611. FROM $table
  612. WHERE ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)
  613. GROUP BY $columnCollectionID
  614. ) latest_files
  615. ON $table.$columnCollectionID = latest_files.$columnCollectionID
  616. AND $table.$columnCreationTime = latest_files.max_creation_time;
  617. ''');
  618. final files = _convertToFiles(rows);
  619. // TODO: Do this de-duplication within the SQL Query
  620. final collectionMap = Map<int, File>();
  621. for (final file in files) {
  622. if (collectionMap.containsKey(file.collectionID)) {
  623. if (collectionMap[file.collectionID].updationTime < file.updationTime) {
  624. continue;
  625. }
  626. }
  627. collectionMap[file.collectionID] = file;
  628. }
  629. return collectionMap.values.toList();
  630. }
  631. Future<File> getLastModifiedFileInCollection(int collectionID) async {
  632. final db = await instance.database;
  633. final rows = await db.query(
  634. table,
  635. where: '$columnCollectionID = ? AND $columnIsDeleted = 0',
  636. whereArgs: [collectionID],
  637. orderBy: '$columnUpdationTime DESC',
  638. limit: 1,
  639. );
  640. if (rows.isNotEmpty) {
  641. return _getFileFromRow(rows[0]);
  642. } else {
  643. return null;
  644. }
  645. }
  646. Future<bool> doesFileExistInCollection(
  647. int uploadedFileID, int collectionID) async {
  648. final db = await instance.database;
  649. final rows = await db.query(
  650. table,
  651. where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
  652. whereArgs: [uploadedFileID, collectionID],
  653. limit: 1,
  654. );
  655. return rows.isNotEmpty;
  656. }
  657. List<File> _convertToFiles(List<Map<String, dynamic>> results) {
  658. final List<File> files = [];
  659. for (final result in results) {
  660. files.add(_getFileFromRow(result));
  661. }
  662. return files;
  663. }
  664. Map<String, dynamic> _getRowForFile(File file) {
  665. final row = new Map<String, dynamic>();
  666. if (file.generatedID != null) {
  667. row[columnGeneratedID] = file.generatedID;
  668. }
  669. row[columnLocalID] = file.localID;
  670. row[columnUploadedFileID] = file.uploadedFileID ?? -1;
  671. row[columnOwnerID] = file.ownerID;
  672. row[columnCollectionID] = file.collectionID ?? -1;
  673. row[columnTitle] = file.title;
  674. row[columnDeviceFolder] = file.deviceFolder;
  675. if (file.location != null) {
  676. row[columnLatitude] = file.location.latitude;
  677. row[columnLongitude] = file.location.longitude;
  678. }
  679. switch (file.fileType) {
  680. case FileType.image:
  681. row[columnFileType] = 0;
  682. break;
  683. case FileType.video:
  684. row[columnFileType] = 1;
  685. break;
  686. default:
  687. row[columnFileType] = -1;
  688. }
  689. row[columnCreationTime] = file.creationTime;
  690. row[columnModificationTime] = file.modificationTime;
  691. row[columnUpdationTime] = file.updationTime;
  692. row[columnEncryptedKey] = file.encryptedKey;
  693. row[columnKeyDecryptionNonce] = file.keyDecryptionNonce;
  694. row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
  695. row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
  696. row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
  697. return row;
  698. }
  699. Map<String, dynamic> _getRowForFileWithoutCollection(File file) {
  700. final row = new Map<String, dynamic>();
  701. row[columnLocalID] = file.localID;
  702. row[columnUploadedFileID] = file.uploadedFileID ?? -1;
  703. row[columnOwnerID] = file.ownerID;
  704. row[columnTitle] = file.title;
  705. row[columnDeviceFolder] = file.deviceFolder;
  706. if (file.location != null) {
  707. row[columnLatitude] = file.location.latitude;
  708. row[columnLongitude] = file.location.longitude;
  709. }
  710. switch (file.fileType) {
  711. case FileType.image:
  712. row[columnFileType] = 0;
  713. break;
  714. case FileType.video:
  715. row[columnFileType] = 1;
  716. break;
  717. default:
  718. row[columnFileType] = -1;
  719. }
  720. row[columnCreationTime] = file.creationTime;
  721. row[columnModificationTime] = file.modificationTime;
  722. row[columnUpdationTime] = file.updationTime;
  723. row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
  724. row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
  725. row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
  726. return row;
  727. }
  728. File _getFileFromRow(Map<String, dynamic> row) {
  729. final file = File();
  730. file.generatedID = row[columnGeneratedID];
  731. file.localID = row[columnLocalID];
  732. file.uploadedFileID =
  733. row[columnUploadedFileID] == -1 ? null : row[columnUploadedFileID];
  734. file.ownerID = row[columnOwnerID];
  735. file.collectionID =
  736. row[columnCollectionID] == -1 ? null : row[columnCollectionID];
  737. file.title = row[columnTitle];
  738. file.deviceFolder = row[columnDeviceFolder];
  739. if (row[columnLatitude] != null && row[columnLongitude] != null) {
  740. file.location = Location(row[columnLatitude], row[columnLongitude]);
  741. }
  742. file.fileType = getFileType(row[columnFileType]);
  743. file.creationTime = int.parse(row[columnCreationTime]);
  744. file.modificationTime = int.parse(row[columnModificationTime]);
  745. file.updationTime = row[columnUpdationTime] == null
  746. ? -1
  747. : int.parse(row[columnUpdationTime]);
  748. file.encryptedKey = row[columnEncryptedKey];
  749. file.keyDecryptionNonce = row[columnKeyDecryptionNonce];
  750. file.fileDecryptionHeader = row[columnFileDecryptionHeader];
  751. file.thumbnailDecryptionHeader = row[columnThumbnailDecryptionHeader];
  752. file.metadataDecryptionHeader = row[columnMetadataDecryptionHeader];
  753. return file;
  754. }
  755. }