files_db.dart 30 KB

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