files_db.dart 36 KB

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