files_db.dart 32 KB

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