files_db.dart 37 KB

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