files_db.dart 34 KB

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