files_db.dart 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094
  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, bool asc, int visibility = kVisibilityVisible}) async {
  370. final db = await instance.database;
  371. final order = (asc ?? false ? 'ASC' : 'DESC');
  372. final results = await db.query(
  373. table,
  374. where:
  375. '$columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnOwnerID = ? AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)'
  376. ' AND $columnMMdVisibility = ?',
  377. whereArgs: [startTime, endTime, ownerID, visibility],
  378. orderBy:
  379. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  380. limit: limit,
  381. );
  382. final files = _convertToFiles(results);
  383. List<File> deduplicatedFiles = _deduplicatedFiles(files);
  384. return FileLoadResult(deduplicatedFiles, files.length == limit);
  385. }
  386. Future<FileLoadResult> getAllLocalAndUploadedFiles(
  387. int startTime, int endTime, int ownerID,
  388. {int limit, bool asc}) async {
  389. final db = await instance.database;
  390. final order = (asc ?? false ? 'ASC' : 'DESC');
  391. final results = await db.query(
  392. table,
  393. where:
  394. '$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
  395. ' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
  396. whereArgs: [startTime, endTime, ownerID, kVisibilityVisible],
  397. orderBy:
  398. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  399. limit: limit,
  400. );
  401. final files = _convertToFiles(results);
  402. return FileLoadResult(files, files.length == limit);
  403. }
  404. Future<FileLoadResult> getImportantFiles(
  405. int startTime, int endTime, int ownerID, List<String> paths,
  406. {int limit, bool asc}) async {
  407. final db = await instance.database;
  408. String inParam = "";
  409. for (final path in paths) {
  410. inParam += "'" + path.replaceAll("'", "''") + "',";
  411. }
  412. inParam = inParam.substring(0, inParam.length - 1);
  413. final order = (asc ?? false ? 'ASC' : 'DESC');
  414. final results = await db.query(
  415. table,
  416. where:
  417. '$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
  418. 'AND (($columnLocalID IS NOT NULL AND $columnDeviceFolder IN ($inParam)) OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
  419. whereArgs: [startTime, endTime, ownerID, kVisibilityVisible],
  420. orderBy:
  421. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  422. limit: limit,
  423. );
  424. final files = _convertToFiles(results);
  425. List<File> deduplicatedFiles = _deduplicatedFiles(files);
  426. return FileLoadResult(deduplicatedFiles, files.length == limit);
  427. }
  428. List<File> _deduplicatedFiles(List<File> files) {
  429. final uploadedFileIDs = <int>{};
  430. final List<File> deduplicatedFiles = [];
  431. for (final file in files) {
  432. final id = file.uploadedFileID;
  433. if (id != null && id != -1 && uploadedFileIDs.contains(id)) {
  434. continue;
  435. }
  436. uploadedFileIDs.add(id);
  437. deduplicatedFiles.add(file);
  438. }
  439. return deduplicatedFiles;
  440. }
  441. Future<FileLoadResult> getFilesInCollection(
  442. int collectionID, int startTime, int endTime,
  443. {int limit, bool asc}) async {
  444. final db = await instance.database;
  445. final order = (asc ?? false ? 'ASC' : 'DESC');
  446. final results = await db.query(
  447. table,
  448. where:
  449. '$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ?',
  450. whereArgs: [collectionID, startTime, endTime],
  451. orderBy:
  452. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  453. limit: limit,
  454. );
  455. final files = _convertToFiles(results);
  456. _logger.info("Fetched " + files.length.toString() + " files");
  457. return FileLoadResult(files, files.length == limit);
  458. }
  459. Future<FileLoadResult> getFilesInPath(String path, int startTime, int endTime,
  460. {int limit, bool asc}) async {
  461. final db = await instance.database;
  462. final order = (asc ?? false ? 'ASC' : 'DESC');
  463. final results = await db.query(
  464. table,
  465. where:
  466. '$columnDeviceFolder = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnLocalID IS NOT NULL',
  467. whereArgs: [path, startTime, endTime],
  468. orderBy:
  469. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  470. groupBy: columnLocalID,
  471. limit: limit,
  472. );
  473. final files = _convertToFiles(results);
  474. return FileLoadResult(files, files.length == limit);
  475. }
  476. Future<List<File>> getAllVideos() async {
  477. final db = await instance.database;
  478. final results = await db.query(
  479. table,
  480. where: '$columnLocalID IS NOT NULL AND $columnFileType = 1',
  481. orderBy: '$columnCreationTime DESC',
  482. );
  483. return _convertToFiles(results);
  484. }
  485. Future<List<File>> getAllInPath(String path) async {
  486. final db = await instance.database;
  487. final results = await db.query(
  488. table,
  489. where: '$columnLocalID IS NOT NULL AND $columnDeviceFolder = ?',
  490. whereArgs: [path],
  491. orderBy: '$columnCreationTime DESC',
  492. groupBy: columnLocalID,
  493. );
  494. return _convertToFiles(results);
  495. }
  496. Future<List<File>> getFilesCreatedWithinDurations(
  497. List<List<int>> durations) async {
  498. final db = await instance.database;
  499. String whereClause = "";
  500. for (int index = 0; index < durations.length; index++) {
  501. whereClause += "($columnCreationTime > " +
  502. durations[index][0].toString() +
  503. " AND $columnCreationTime < " +
  504. durations[index][1].toString() +
  505. ")";
  506. if (index != durations.length - 1) {
  507. whereClause += " OR ";
  508. }
  509. }
  510. final results = await db.query(
  511. table,
  512. where: whereClause,
  513. orderBy: '$columnCreationTime ASC',
  514. );
  515. return _convertToFiles(results);
  516. }
  517. Future<List<File>> getFilesToBeUploadedWithinFolders(
  518. Set<String> folders) async {
  519. if (folders.isEmpty) {
  520. return [];
  521. }
  522. final db = await instance.database;
  523. String inParam = "";
  524. for (final folder in folders) {
  525. inParam += "'" + folder.replaceAll("'", "''") + "',";
  526. }
  527. inParam = inParam.substring(0, inParam.length - 1);
  528. final results = await db.query(
  529. table,
  530. where:
  531. '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND $columnDeviceFolder IN ($inParam)',
  532. orderBy: '$columnCreationTime DESC',
  533. groupBy: columnLocalID,
  534. );
  535. return _convertToFiles(results);
  536. }
  537. Future<List<File>> getAllLocalFiles() async {
  538. final db = await instance.database;
  539. final results = await db.query(
  540. table,
  541. where:
  542. '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND $columnLocalID IS NOT NULL',
  543. orderBy: '$columnCreationTime DESC',
  544. groupBy: columnLocalID,
  545. );
  546. return _convertToFiles(results);
  547. }
  548. Future<List<File>> getEditedRemoteFiles() async {
  549. final db = await instance.database;
  550. final results = await db.query(
  551. table,
  552. where:
  553. '($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1) AND ($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1)',
  554. orderBy: '$columnCreationTime DESC',
  555. groupBy: columnLocalID,
  556. );
  557. return _convertToFiles(results);
  558. }
  559. Future<List<int>> getUploadedFileIDsToBeUpdated() async {
  560. final db = await instance.database;
  561. final rows = await db.query(
  562. table,
  563. columns: [columnUploadedFileID],
  564. where:
  565. '($columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NULL)',
  566. orderBy: '$columnCreationTime DESC',
  567. distinct: true,
  568. );
  569. final uploadedFileIDs = <int>[];
  570. for (final row in rows) {
  571. uploadedFileIDs.add(row[columnUploadedFileID]);
  572. }
  573. return uploadedFileIDs;
  574. }
  575. Future<File> getUploadedFileInAnyCollection(int uploadedFileID) async {
  576. final db = await instance.database;
  577. final results = await db.query(
  578. table,
  579. where: '$columnUploadedFileID = ?',
  580. whereArgs: [
  581. uploadedFileID,
  582. ],
  583. limit: 1,
  584. );
  585. if (results.isEmpty) {
  586. return null;
  587. }
  588. return _convertToFiles(results)[0];
  589. }
  590. Future<Set<String>> getExistingLocalFileIDs() async {
  591. final db = await instance.database;
  592. final rows = await db.query(
  593. table,
  594. columns: [columnLocalID],
  595. distinct: true,
  596. where: '$columnLocalID IS NOT NULL',
  597. );
  598. final result = <String>{};
  599. for (final row in rows) {
  600. result.add(row[columnLocalID]);
  601. }
  602. return result;
  603. }
  604. Future<int> getNumberOfUploadedFiles() async {
  605. final db = await instance.database;
  606. final rows = await db.query(
  607. table,
  608. columns: [columnUploadedFileID],
  609. where:
  610. '($columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NOT NULL)',
  611. distinct: true,
  612. );
  613. return rows.length;
  614. }
  615. Future<int> updateUploadedFile(
  616. String localID,
  617. String title,
  618. Location location,
  619. int creationTime,
  620. int modificationTime,
  621. int updationTime,
  622. ) async {
  623. final db = await instance.database;
  624. return await db.update(
  625. table,
  626. {
  627. columnTitle: title,
  628. columnLatitude: location.latitude,
  629. columnLongitude: location.longitude,
  630. columnCreationTime: creationTime,
  631. columnModificationTime: modificationTime,
  632. columnUpdationTime: updationTime,
  633. },
  634. where: '$columnLocalID = ?',
  635. whereArgs: [localID],
  636. );
  637. }
  638. Future<List<File>> getMatchingFiles(
  639. String title,
  640. String deviceFolder,
  641. ) async {
  642. final db = await instance.database;
  643. final rows = await db.query(
  644. table,
  645. where: '''$columnTitle=? AND $columnDeviceFolder=?''',
  646. whereArgs: [
  647. title,
  648. deviceFolder,
  649. ],
  650. );
  651. if (rows.isNotEmpty) {
  652. return _convertToFiles(rows);
  653. } else {
  654. return null;
  655. }
  656. }
  657. Future<int> update(File file) async {
  658. final db = await instance.database;
  659. return await db.update(
  660. table,
  661. _getRowForFile(file),
  662. where: '$columnGeneratedID = ?',
  663. whereArgs: [file.generatedID],
  664. );
  665. }
  666. Future<int> updateUploadedFileAcrossCollections(File file) async {
  667. final db = await instance.database;
  668. return await db.update(
  669. table,
  670. _getRowForFileWithoutCollection(file),
  671. where: '$columnUploadedFileID = ?',
  672. whereArgs: [file.uploadedFileID],
  673. );
  674. }
  675. Future<int> delete(int uploadedFileID) async {
  676. final db = await instance.database;
  677. return db.delete(
  678. table,
  679. where: '$columnUploadedFileID =?',
  680. whereArgs: [uploadedFileID],
  681. );
  682. }
  683. Future<int> deleteMultipleUploadedFiles(List<int> uploadedFileIDs) async {
  684. final db = await instance.database;
  685. return await db.delete(
  686. table,
  687. where: '$columnUploadedFileID IN (${uploadedFileIDs.join(', ')})',
  688. );
  689. }
  690. Future<int> deleteLocalFile(File file) async {
  691. final db = await instance.database;
  692. if (file.localID != null) {
  693. // delete all files with same local ID
  694. return db.delete(
  695. table,
  696. where: '$columnLocalID =?',
  697. whereArgs: [file.localID],
  698. );
  699. } else {
  700. return db.delete(
  701. table,
  702. where: '$columnGeneratedID =?',
  703. whereArgs: [file.generatedID],
  704. );
  705. }
  706. }
  707. Future<void> deleteLocalFiles(List<String> localIDs) async {
  708. String inParam = "";
  709. for (final localID in localIDs) {
  710. inParam += "'" + localID + "',";
  711. }
  712. inParam = inParam.substring(0, inParam.length - 1);
  713. final db = await instance.database;
  714. await db.rawQuery('''
  715. UPDATE $table
  716. SET $columnLocalID = NULL
  717. WHERE $columnLocalID IN ($inParam);
  718. ''');
  719. }
  720. Future<List<File>> getLocalFiles(List<String> localIDs) async {
  721. String inParam = "";
  722. for (final localID in localIDs) {
  723. inParam += "'" + localID + "',";
  724. }
  725. inParam = inParam.substring(0, inParam.length - 1);
  726. final db = await instance.database;
  727. final results = await db.query(
  728. table,
  729. where: '$columnLocalID IN ($inParam)',
  730. );
  731. return _convertToFiles(results);
  732. }
  733. Future<int> deleteUnSyncedLocalFiles(List<String> localIDs) async {
  734. String inParam = "";
  735. for (final localID in localIDs) {
  736. inParam += "'" + localID + "',";
  737. }
  738. inParam = inParam.substring(0, inParam.length - 1);
  739. final db = await instance.database;
  740. return db.delete(
  741. table,
  742. where:
  743. '($columnUploadedFileID is NULL OR $columnUploadedFileID = -1 ) AND $columnLocalID IN ($inParam)',
  744. );
  745. }
  746. Future<int> deleteFromCollection(int uploadedFileID, int collectionID) async {
  747. final db = await instance.database;
  748. return db.delete(
  749. table,
  750. where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
  751. whereArgs: [uploadedFileID, collectionID],
  752. );
  753. }
  754. Future<int> deleteFilesFromCollection(
  755. int collectionID, List<int> uploadedFileIDs) async {
  756. final db = await instance.database;
  757. return db.delete(
  758. table,
  759. where:
  760. '$columnCollectionID = ? AND $columnUploadedFileID IN (${uploadedFileIDs.join(', ')})',
  761. whereArgs: [collectionID],
  762. );
  763. }
  764. Future<int> deleteCollection(int collectionID) async {
  765. final db = await instance.database;
  766. return db.delete(
  767. table,
  768. where: '$columnCollectionID = ?',
  769. whereArgs: [collectionID],
  770. );
  771. }
  772. Future<int> removeFromCollection(int collectionID, List<int> fileIDs) async {
  773. final db = await instance.database;
  774. return db.delete(
  775. table,
  776. where:
  777. '$columnCollectionID =? AND $columnUploadedFileID IN (${fileIDs.join(', ')})',
  778. whereArgs: [collectionID],
  779. );
  780. }
  781. Future<List<File>> getLatestLocalFiles() async {
  782. final db = await instance.database;
  783. final rows = await db.rawQuery('''
  784. SELECT $table.*
  785. FROM $table
  786. INNER JOIN
  787. (
  788. SELECT $columnDeviceFolder, MAX($columnCreationTime) AS max_creation_time
  789. FROM $table
  790. WHERE $table.$columnLocalID IS NOT NULL
  791. GROUP BY $columnDeviceFolder
  792. ) latest_files
  793. ON $table.$columnDeviceFolder = latest_files.$columnDeviceFolder
  794. AND $table.$columnCreationTime = latest_files.max_creation_time;
  795. ''');
  796. final files = _convertToFiles(rows);
  797. // TODO: Do this de-duplication within the SQL Query
  798. final folderMap = <String, File>{};
  799. for (final file in files) {
  800. if (folderMap.containsKey(file.deviceFolder)) {
  801. if (folderMap[file.deviceFolder].updationTime < file.updationTime) {
  802. continue;
  803. }
  804. }
  805. folderMap[file.deviceFolder] = file;
  806. }
  807. return folderMap.values.toList();
  808. }
  809. Future<List<File>> getLatestCollectionFiles() async {
  810. final db = await instance.database;
  811. final rows = await db.rawQuery('''
  812. SELECT $table.*
  813. FROM $table
  814. INNER JOIN
  815. (
  816. SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time
  817. FROM $table
  818. WHERE ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)
  819. GROUP BY $columnCollectionID
  820. ) latest_files
  821. ON $table.$columnCollectionID = latest_files.$columnCollectionID
  822. AND $table.$columnCreationTime = latest_files.max_creation_time;
  823. ''');
  824. final files = _convertToFiles(rows);
  825. // TODO: Do this de-duplication within the SQL Query
  826. final collectionMap = <int, File>{};
  827. for (final file in files) {
  828. if (collectionMap.containsKey(file.collectionID)) {
  829. if (collectionMap[file.collectionID].updationTime < file.updationTime) {
  830. continue;
  831. }
  832. }
  833. collectionMap[file.collectionID] = file;
  834. }
  835. return collectionMap.values.toList();
  836. }
  837. Future<File> getLastModifiedFileInCollection(int collectionID) async {
  838. final db = await instance.database;
  839. final rows = await db.query(
  840. table,
  841. where: '$columnCollectionID = ?',
  842. whereArgs: [collectionID],
  843. orderBy: '$columnUpdationTime DESC',
  844. limit: 1,
  845. );
  846. if (rows.isNotEmpty) {
  847. return _getFileFromRow(rows[0]);
  848. } else {
  849. return null;
  850. }
  851. }
  852. Future<Map<String, int>> getFileCountInDeviceFolders() async {
  853. final db = await instance.database;
  854. final rows = await db.rawQuery('''
  855. SELECT COUNT($columnGeneratedID) as count, $columnDeviceFolder
  856. FROM $table
  857. WHERE $columnLocalID IS NOT NULL
  858. GROUP BY $columnDeviceFolder
  859. ''');
  860. final result = <String, int>{};
  861. for (final row in rows) {
  862. result[row[columnDeviceFolder]] = row["count"];
  863. }
  864. return result;
  865. }
  866. Future<bool> doesFileExistInCollection(
  867. int uploadedFileID, int collectionID) async {
  868. final db = await instance.database;
  869. final rows = await db.query(
  870. table,
  871. where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
  872. whereArgs: [uploadedFileID, collectionID],
  873. limit: 1,
  874. );
  875. return rows.isNotEmpty;
  876. }
  877. Future<Map<int, File>> getFilesFromIDs(List<int> ids) async {
  878. final result = <int, File>{};
  879. if (ids.isEmpty) {
  880. return result;
  881. }
  882. String inParam = "";
  883. for (final id in ids) {
  884. inParam += "'" + id.toString() + "',";
  885. }
  886. inParam = inParam.substring(0, inParam.length - 1);
  887. final db = await instance.database;
  888. final results = await db.query(
  889. table,
  890. where: '$columnUploadedFileID IN ($inParam)',
  891. );
  892. final files = _convertToFiles(results);
  893. for (final file in files) {
  894. result[file.uploadedFileID] = file;
  895. }
  896. return result;
  897. }
  898. List<File> _convertToFiles(List<Map<String, dynamic>> results) {
  899. final List<File> files = [];
  900. for (final result in results) {
  901. files.add(_getFileFromRow(result));
  902. }
  903. return files;
  904. }
  905. Map<String, dynamic> _getRowForFile(File file) {
  906. final row = <String, dynamic>{};
  907. if (file.generatedID != null) {
  908. row[columnGeneratedID] = file.generatedID;
  909. }
  910. row[columnLocalID] = file.localID;
  911. row[columnUploadedFileID] = file.uploadedFileID ?? -1;
  912. row[columnOwnerID] = file.ownerID;
  913. row[columnCollectionID] = file.collectionID ?? -1;
  914. row[columnTitle] = file.title;
  915. row[columnDeviceFolder] = file.deviceFolder;
  916. if (file.location != null) {
  917. row[columnLatitude] = file.location.latitude;
  918. row[columnLongitude] = file.location.longitude;
  919. }
  920. row[columnFileType] = getInt(file.fileType);
  921. row[columnCreationTime] = file.creationTime;
  922. row[columnModificationTime] = file.modificationTime;
  923. row[columnUpdationTime] = file.updationTime;
  924. row[columnEncryptedKey] = file.encryptedKey;
  925. row[columnKeyDecryptionNonce] = file.keyDecryptionNonce;
  926. row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
  927. row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
  928. row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
  929. row[columnFileSubType] = file.fileSubType ?? -1;
  930. row[columnDuration] = file.duration ?? 0;
  931. row[columnExif] = file.exif;
  932. row[columnHash] = file.hash;
  933. row[columnMetadataVersion] = file.metadataVersion;
  934. row[columnMMdVersion] = file.mMdVersion ?? 0;
  935. row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}';
  936. row[columnMMdVisibility] =
  937. file.magicMetadata?.visibility ?? kVisibilityVisible;
  938. row[columnPubMMdVersion] = file.pubMmdVersion ?? 0;
  939. row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}';
  940. if (file.pubMagicMetadata != null &&
  941. file.pubMagicMetadata.editedTime != null) {
  942. // override existing creationTime to avoid re-writing all queries related
  943. // to loading the gallery
  944. row[columnCreationTime] = file.pubMagicMetadata.editedTime;
  945. }
  946. return row;
  947. }
  948. Map<String, dynamic> _getRowForFileWithoutCollection(File file) {
  949. final row = <String, dynamic>{};
  950. row[columnLocalID] = file.localID;
  951. row[columnUploadedFileID] = file.uploadedFileID ?? -1;
  952. row[columnOwnerID] = file.ownerID;
  953. row[columnTitle] = file.title;
  954. row[columnDeviceFolder] = file.deviceFolder;
  955. if (file.location != null) {
  956. row[columnLatitude] = file.location.latitude;
  957. row[columnLongitude] = file.location.longitude;
  958. }
  959. row[columnFileType] = getInt(file.fileType);
  960. row[columnCreationTime] = file.creationTime;
  961. row[columnModificationTime] = file.modificationTime;
  962. row[columnUpdationTime] = file.updationTime;
  963. row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
  964. row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
  965. row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
  966. row[columnFileSubType] = file.fileSubType ?? -1;
  967. row[columnDuration] = file.duration ?? 0;
  968. row[columnExif] = file.exif;
  969. row[columnHash] = file.hash;
  970. row[columnMetadataVersion] = file.metadataVersion;
  971. row[columnMMdVersion] = file.mMdVersion ?? 0;
  972. row[columnMMdEncodedJson] == file.mMdEncodedJson ?? '{}';
  973. row[columnMMdVisibility] =
  974. file.magicMetadata?.visibility ?? kVisibilityVisible;
  975. row[columnPubMMdVersion] = file.pubMmdVersion ?? 0;
  976. row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}';
  977. if (file.pubMagicMetadata != null &&
  978. file.pubMagicMetadata.editedTime != null) {
  979. // override existing creationTime to avoid re-writing all queries related
  980. // to loading the gallery
  981. row[columnCreationTime] = file.pubMagicMetadata.editedTime;
  982. }
  983. }
  984. File _getFileFromRow(Map<String, dynamic> row) {
  985. final file = File();
  986. file.generatedID = row[columnGeneratedID];
  987. file.localID = row[columnLocalID];
  988. file.uploadedFileID =
  989. row[columnUploadedFileID] == -1 ? null : row[columnUploadedFileID];
  990. file.ownerID = row[columnOwnerID];
  991. file.collectionID =
  992. row[columnCollectionID] == -1 ? null : row[columnCollectionID];
  993. file.title = row[columnTitle];
  994. file.deviceFolder = row[columnDeviceFolder];
  995. if (row[columnLatitude] != null && row[columnLongitude] != null) {
  996. file.location = Location(row[columnLatitude], row[columnLongitude]);
  997. }
  998. file.fileType = getFileType(row[columnFileType]);
  999. file.creationTime = row[columnCreationTime];
  1000. file.modificationTime = row[columnModificationTime];
  1001. file.updationTime = row[columnUpdationTime] ?? -1;
  1002. file.encryptedKey = row[columnEncryptedKey];
  1003. file.keyDecryptionNonce = row[columnKeyDecryptionNonce];
  1004. file.fileDecryptionHeader = row[columnFileDecryptionHeader];
  1005. file.thumbnailDecryptionHeader = row[columnThumbnailDecryptionHeader];
  1006. file.metadataDecryptionHeader = row[columnMetadataDecryptionHeader];
  1007. file.fileSubType = row[columnFileSubType] ?? -1;
  1008. file.duration = row[columnDuration] ?? 0;
  1009. file.exif = row[columnExif];
  1010. file.hash = row[columnHash];
  1011. file.metadataVersion = row[columnMetadataVersion] ?? 0;
  1012. file.mMdVersion = row[columnMMdVersion] ?? 0;
  1013. file.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}';
  1014. file.pubMmdVersion = row[columnPubMMdVersion] ?? 0;
  1015. file.pubMmdEncodedJson = row[columnPubMMdEncodedJson] ?? '{}';
  1016. return file;
  1017. }
  1018. }