files_db.dart 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630
  1. import 'dart:developer' as dev;
  2. import 'dart:io' as io;
  3. import 'package:flutter/foundation.dart';
  4. import 'package:logging/logging.dart';
  5. import 'package:path/path.dart';
  6. import 'package:path_provider/path_provider.dart';
  7. import 'package:photos/models/backup_status.dart';
  8. import 'package:photos/models/file.dart';
  9. import 'package:photos/models/file_load_result.dart';
  10. import 'package:photos/models/file_type.dart';
  11. import 'package:photos/models/location/location.dart';
  12. import "package:photos/models/metadata/common_keys.dart";
  13. import 'package:photos/utils/file_uploader_util.dart';
  14. import 'package:sqflite/sqflite.dart';
  15. import 'package:sqflite_migration/sqflite_migration.dart';
  16. class FilesDB {
  17. /*
  18. Note: columnUploadedFileID and columnCollectionID have to be compared against
  19. both NULL and -1 because older clients might have entries where the DEFAULT
  20. was unset, and a migration script to set the DEFAULT would break in case of
  21. duplicate entries for un-uploaded files that were created due to a collision
  22. in background and foreground syncs.
  23. */
  24. static const _databaseName = "ente.files.db";
  25. static final Logger _logger = Logger("FilesDB");
  26. static const filesTable = 'files';
  27. static const tempTable = 'temp_files';
  28. static const columnGeneratedID = '_id';
  29. static const columnUploadedFileID = 'uploaded_file_id';
  30. static const columnOwnerID = 'owner_id';
  31. static const columnCollectionID = 'collection_id';
  32. static const columnLocalID = 'local_id';
  33. static const columnTitle = 'title';
  34. static const columnDeviceFolder = 'device_folder';
  35. static const columnLatitude = 'latitude';
  36. static const columnLongitude = 'longitude';
  37. static const columnFileType = 'file_type';
  38. static const columnFileSubType = 'file_sub_type';
  39. static const columnDuration = 'duration';
  40. static const columnExif = 'exif';
  41. static const columnHash = 'hash';
  42. static const columnMetadataVersion = 'metadata_version';
  43. static const columnIsDeleted = 'is_deleted';
  44. static const columnCreationTime = 'creation_time';
  45. static const columnModificationTime = 'modification_time';
  46. static const columnUpdationTime = 'updation_time';
  47. static const columnEncryptedKey = 'encrypted_key';
  48. static const columnKeyDecryptionNonce = 'key_decryption_nonce';
  49. static const columnFileDecryptionHeader = 'file_decryption_header';
  50. static const columnThumbnailDecryptionHeader = 'thumbnail_decryption_header';
  51. static const columnMetadataDecryptionHeader = 'metadata_decryption_header';
  52. static const columnFileSize = 'file_size';
  53. // MMD -> Magic Metadata
  54. static const columnMMdEncodedJson = 'mmd_encoded_json';
  55. static const columnMMdVersion = 'mmd_ver';
  56. static const columnPubMMdEncodedJson = 'pub_mmd_encoded_json';
  57. static const columnPubMMdVersion = 'pub_mmd_ver';
  58. // part of magic metadata
  59. // Only parse & store selected fields from JSON in separate columns if
  60. // we need to write query based on that field
  61. static const columnMMdVisibility = 'mmd_visibility';
  62. static final initializationScript = [...createTable(filesTable)];
  63. static final migrationScripts = [
  64. ...alterDeviceFolderToAllowNULL(),
  65. ...alterTimestampColumnTypes(),
  66. ...addIndices(),
  67. ...addMetadataColumns(),
  68. ...addMagicMetadataColumns(),
  69. ...addUniqueConstraintOnCollectionFiles(),
  70. ...addPubMagicMetadataColumns(),
  71. ...createOnDeviceFilesAndPathCollection(),
  72. ...addFileSizeColumn(),
  73. ...updateIndexes(),
  74. ...createEntityDataTable(),
  75. ];
  76. final dbConfig = MigrationConfig(
  77. initializationScript: initializationScript,
  78. migrationScripts: migrationScripts,
  79. );
  80. // make this a singleton class
  81. FilesDB._privateConstructor();
  82. static final FilesDB instance = FilesDB._privateConstructor();
  83. // only have a single app-wide reference to the database
  84. static Future<Database>? _dbFuture;
  85. Future<Database> get database async {
  86. // lazily instantiate the db the first time it is accessed
  87. _dbFuture ??= _initDatabase();
  88. return _dbFuture!;
  89. }
  90. // this opens the database (and creates it if it doesn't exist)
  91. Future<Database> _initDatabase() async {
  92. final io.Directory documentsDirectory =
  93. await getApplicationDocumentsDirectory();
  94. final String path = join(documentsDirectory.path, _databaseName);
  95. _logger.info("DB path " + path);
  96. return await openDatabaseWithMigration(path, dbConfig);
  97. }
  98. // SQL code to create the database table
  99. static List<String> createTable(String tableName) {
  100. return [
  101. '''
  102. CREATE TABLE $tableName (
  103. $columnGeneratedID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  104. $columnLocalID TEXT,
  105. $columnUploadedFileID INTEGER DEFAULT -1,
  106. $columnOwnerID INTEGER,
  107. $columnCollectionID INTEGER DEFAULT -1,
  108. $columnTitle TEXT NOT NULL,
  109. $columnDeviceFolder TEXT,
  110. $columnLatitude REAL,
  111. $columnLongitude REAL,
  112. $columnFileType INTEGER,
  113. $columnModificationTime TEXT NOT NULL,
  114. $columnEncryptedKey TEXT,
  115. $columnKeyDecryptionNonce TEXT,
  116. $columnFileDecryptionHeader TEXT,
  117. $columnThumbnailDecryptionHeader TEXT,
  118. $columnMetadataDecryptionHeader TEXT,
  119. $columnIsDeleted INTEGER DEFAULT 0,
  120. $columnCreationTime TEXT NOT NULL,
  121. $columnUpdationTime TEXT,
  122. UNIQUE($columnLocalID, $columnUploadedFileID, $columnCollectionID)
  123. );
  124. ''',
  125. ];
  126. }
  127. static List<String> addIndices() {
  128. return [
  129. '''
  130. CREATE INDEX IF NOT EXISTS collection_id_index ON $filesTable($columnCollectionID);
  131. ''',
  132. '''
  133. CREATE INDEX IF NOT EXISTS device_folder_index ON $filesTable($columnDeviceFolder);
  134. ''',
  135. '''
  136. CREATE INDEX IF NOT EXISTS creation_time_index ON $filesTable($columnCreationTime);
  137. ''',
  138. '''
  139. CREATE INDEX IF NOT EXISTS updation_time_index ON $filesTable($columnUpdationTime);
  140. '''
  141. ];
  142. }
  143. static List<String> alterDeviceFolderToAllowNULL() {
  144. return [
  145. ...createTable(tempTable),
  146. '''
  147. INSERT INTO $tempTable
  148. SELECT *
  149. FROM $filesTable;
  150. DROP TABLE $filesTable;
  151. ALTER TABLE $tempTable
  152. RENAME TO $filesTable;
  153. '''
  154. ];
  155. }
  156. static List<String> alterTimestampColumnTypes() {
  157. return [
  158. '''
  159. DROP TABLE IF EXISTS $tempTable;
  160. ''',
  161. '''
  162. CREATE TABLE $tempTable (
  163. $columnGeneratedID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  164. $columnLocalID TEXT,
  165. $columnUploadedFileID INTEGER DEFAULT -1,
  166. $columnOwnerID INTEGER,
  167. $columnCollectionID INTEGER DEFAULT -1,
  168. $columnTitle TEXT NOT NULL,
  169. $columnDeviceFolder TEXT,
  170. $columnLatitude REAL,
  171. $columnLongitude REAL,
  172. $columnFileType INTEGER,
  173. $columnModificationTime INTEGER NOT NULL,
  174. $columnEncryptedKey TEXT,
  175. $columnKeyDecryptionNonce TEXT,
  176. $columnFileDecryptionHeader TEXT,
  177. $columnThumbnailDecryptionHeader TEXT,
  178. $columnMetadataDecryptionHeader TEXT,
  179. $columnCreationTime INTEGER NOT NULL,
  180. $columnUpdationTime INTEGER,
  181. UNIQUE($columnLocalID, $columnUploadedFileID, $columnCollectionID)
  182. );
  183. ''',
  184. '''
  185. INSERT INTO $tempTable
  186. SELECT
  187. $columnGeneratedID,
  188. $columnLocalID,
  189. $columnUploadedFileID,
  190. $columnOwnerID,
  191. $columnCollectionID,
  192. $columnTitle,
  193. $columnDeviceFolder,
  194. $columnLatitude,
  195. $columnLongitude,
  196. $columnFileType,
  197. CAST($columnModificationTime AS INTEGER),
  198. $columnEncryptedKey,
  199. $columnKeyDecryptionNonce,
  200. $columnFileDecryptionHeader,
  201. $columnThumbnailDecryptionHeader,
  202. $columnMetadataDecryptionHeader,
  203. CAST($columnCreationTime AS INTEGER),
  204. CAST($columnUpdationTime AS INTEGER)
  205. FROM $filesTable;
  206. ''',
  207. '''
  208. DROP TABLE $filesTable;
  209. ''',
  210. '''
  211. ALTER TABLE $tempTable
  212. RENAME TO $filesTable;
  213. ''',
  214. ];
  215. }
  216. static List<String> addMetadataColumns() {
  217. return [
  218. '''
  219. ALTER TABLE $filesTable ADD COLUMN $columnFileSubType INTEGER;
  220. ''',
  221. '''
  222. ALTER TABLE $filesTable ADD COLUMN $columnDuration INTEGER;
  223. ''',
  224. '''
  225. ALTER TABLE $filesTable ADD COLUMN $columnExif TEXT;
  226. ''',
  227. '''
  228. ALTER TABLE $filesTable ADD COLUMN $columnHash TEXT;
  229. ''',
  230. '''
  231. ALTER TABLE $filesTable ADD COLUMN $columnMetadataVersion INTEGER;
  232. ''',
  233. ];
  234. }
  235. static List<String> addMagicMetadataColumns() {
  236. return [
  237. '''
  238. ALTER TABLE $filesTable ADD COLUMN $columnMMdEncodedJson TEXT DEFAULT '{}';
  239. ''',
  240. '''
  241. ALTER TABLE $filesTable ADD COLUMN $columnMMdVersion INTEGER DEFAULT 0;
  242. ''',
  243. '''
  244. ALTER TABLE $filesTable ADD COLUMN $columnMMdVisibility INTEGER DEFAULT $visibleVisibility;
  245. '''
  246. ];
  247. }
  248. static List<String> addUniqueConstraintOnCollectionFiles() {
  249. return [
  250. '''
  251. DELETE from $filesTable where $columnCollectionID || '-' || $columnUploadedFileID IN
  252. (SELECT $columnCollectionID || '-' || $columnUploadedFileID from $filesTable WHERE
  253. $columnCollectionID is not NULL AND $columnUploadedFileID is NOT NULL
  254. AND $columnCollectionID != -1 AND $columnUploadedFileID != -1
  255. GROUP BY ($columnCollectionID || '-' || $columnUploadedFileID) HAVING count(*) > 1)
  256. AND ($columnCollectionID || '-' || $columnUploadedFileID || '-' || $columnGeneratedID) NOT IN
  257. (SELECT $columnCollectionID || '-' || $columnUploadedFileID || '-' || max($columnGeneratedID)
  258. from $filesTable WHERE
  259. $columnCollectionID is not NULL AND $columnUploadedFileID is NOT NULL
  260. AND $columnCollectionID != -1 AND $columnUploadedFileID != -1 GROUP BY
  261. ($columnCollectionID || '-' || $columnUploadedFileID) HAVING count(*) > 1);
  262. ''',
  263. '''
  264. CREATE UNIQUE INDEX IF NOT EXISTS cid_uid ON $filesTable ($columnCollectionID, $columnUploadedFileID)
  265. WHERE $columnCollectionID is not NULL AND $columnUploadedFileID is not NULL
  266. AND $columnCollectionID != -1 AND $columnUploadedFileID != -1;
  267. '''
  268. ];
  269. }
  270. static List<String> addPubMagicMetadataColumns() {
  271. return [
  272. '''
  273. ALTER TABLE $filesTable ADD COLUMN $columnPubMMdEncodedJson TEXT DEFAULT '{}';
  274. ''',
  275. '''
  276. ALTER TABLE $filesTable ADD COLUMN $columnPubMMdVersion INTEGER DEFAULT 0;
  277. '''
  278. ];
  279. }
  280. static List<String> createOnDeviceFilesAndPathCollection() {
  281. return [
  282. '''
  283. CREATE TABLE IF NOT EXISTS device_files (
  284. id TEXT NOT NULL,
  285. path_id TEXT NOT NULL,
  286. UNIQUE(id, path_id)
  287. );
  288. ''',
  289. '''
  290. CREATE TABLE IF NOT EXISTS device_collections (
  291. id TEXT PRIMARY KEY NOT NULL,
  292. name TEXT,
  293. modified_at INTEGER NOT NULL DEFAULT 0,
  294. should_backup INTEGER NOT NULL DEFAULT 0,
  295. count INTEGER NOT NULL DEFAULT 0,
  296. collection_id INTEGER DEFAULT -1,
  297. upload_strategy INTEGER DEFAULT 0,
  298. cover_id TEXT
  299. );
  300. ''',
  301. '''
  302. CREATE INDEX IF NOT EXISTS df_id_idx ON device_files (id);
  303. ''',
  304. '''
  305. CREATE INDEX IF NOT EXISTS df_path_id_idx ON device_files (path_id);
  306. ''',
  307. ];
  308. }
  309. static List<String> createEntityDataTable() {
  310. return [
  311. '''
  312. CREATE TABLE IF NOT EXISTS entities (
  313. id TEXT PRIMARY KEY NOT NULL,
  314. type TEXT NOT NULL,
  315. ownerID INTEGER NOT NULL,
  316. data TEXT NOT NULL DEFAULT '{}',
  317. updatedAt INTEGER NOT NULL
  318. );
  319. '''
  320. ];
  321. }
  322. static List<String> addFileSizeColumn() {
  323. return [
  324. '''
  325. ALTER TABLE $filesTable ADD COLUMN $columnFileSize INTEGER;
  326. ''',
  327. ];
  328. }
  329. static List<String> updateIndexes() {
  330. return [
  331. '''
  332. DROP INDEX IF EXISTS device_folder_index;
  333. ''',
  334. '''
  335. CREATE INDEX IF NOT EXISTS file_hash_index ON $filesTable($columnHash);
  336. ''',
  337. ];
  338. }
  339. Future<void> clearTable() async {
  340. final db = await instance.database;
  341. await db.delete(filesTable);
  342. await db.delete("device_files");
  343. await db.delete("device_collections");
  344. await db.delete("entities");
  345. }
  346. Future<void> deleteDB() async {
  347. if (kDebugMode) {
  348. debugPrint("Deleting files db");
  349. final io.Directory documentsDirectory =
  350. await getApplicationDocumentsDirectory();
  351. final String path = join(documentsDirectory.path, _databaseName);
  352. io.File(path).deleteSync(recursive: true);
  353. _dbFuture = null;
  354. }
  355. }
  356. Future<void> insertMultiple(
  357. List<File> files, {
  358. ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.replace,
  359. }) async {
  360. final startTime = DateTime.now();
  361. final db = await instance.database;
  362. var batch = db.batch();
  363. int batchCounter = 0;
  364. for (File file in files) {
  365. if (batchCounter == 400) {
  366. await batch.commit(noResult: true);
  367. batch = db.batch();
  368. batchCounter = 0;
  369. }
  370. batch.insert(
  371. filesTable,
  372. _getRowForFile(file),
  373. conflictAlgorithm: conflictAlgorithm,
  374. );
  375. batchCounter++;
  376. }
  377. await batch.commit(noResult: true);
  378. final endTime = DateTime.now();
  379. final duration = Duration(
  380. microseconds:
  381. endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch,
  382. );
  383. _logger.info(
  384. "Batch insert of " +
  385. files.length.toString() +
  386. " took " +
  387. duration.inMilliseconds.toString() +
  388. "ms.",
  389. );
  390. }
  391. Future<int> insert(File file) async {
  392. final db = await instance.database;
  393. return db.insert(
  394. filesTable,
  395. _getRowForFile(file),
  396. conflictAlgorithm: ConflictAlgorithm.replace,
  397. );
  398. }
  399. Future<File?> getFile(int generatedID) async {
  400. final db = await instance.database;
  401. final results = await db.query(
  402. filesTable,
  403. where: '$columnGeneratedID = ?',
  404. whereArgs: [generatedID],
  405. );
  406. if (results.isEmpty) {
  407. return null;
  408. }
  409. return convertToFiles(results)[0];
  410. }
  411. Future<File?> getUploadedFile(int uploadedID, int collectionID) async {
  412. final db = await instance.database;
  413. final results = await db.query(
  414. filesTable,
  415. where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
  416. whereArgs: [
  417. uploadedID,
  418. collectionID,
  419. ],
  420. );
  421. if (results.isEmpty) {
  422. return null;
  423. }
  424. return convertToFiles(results)[0];
  425. }
  426. Future<Set<int>> getUploadedFileIDs(int collectionID) async {
  427. final db = await instance.database;
  428. final results = await db.query(
  429. filesTable,
  430. columns: [columnUploadedFileID],
  431. where:
  432. '$columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
  433. whereArgs: [
  434. collectionID,
  435. ],
  436. );
  437. final ids = <int>{};
  438. for (final result in results) {
  439. ids.add(result[columnUploadedFileID] as int);
  440. }
  441. return ids;
  442. }
  443. Future<BackedUpFileIDs> getBackedUpIDs() async {
  444. final db = await instance.database;
  445. final results = await db.query(
  446. filesTable,
  447. columns: [columnLocalID, columnUploadedFileID],
  448. where:
  449. '$columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
  450. );
  451. final localIDs = <String>{};
  452. final uploadedIDs = <int>{};
  453. for (final result in results) {
  454. localIDs.add(result[columnLocalID] as String);
  455. uploadedIDs.add(result[columnUploadedFileID] as int);
  456. }
  457. return BackedUpFileIDs(localIDs.toList(), uploadedIDs.toList());
  458. }
  459. Future<FileLoadResult> getAllPendingOrUploadedFiles(
  460. int startTime,
  461. int endTime,
  462. int ownerID, {
  463. int? limit,
  464. bool? asc,
  465. int visibility = visibleVisibility,
  466. Set<int>? ignoredCollectionIDs,
  467. }) async {
  468. final stopWatch = Stopwatch()..start();
  469. final db = await instance.database;
  470. final order = (asc ?? false ? 'ASC' : 'DESC');
  471. final results = await db.query(
  472. filesTable,
  473. where:
  474. '$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)'
  475. ' AND $columnMMdVisibility = ?',
  476. whereArgs: [startTime, endTime, ownerID, visibility],
  477. orderBy:
  478. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  479. limit: limit,
  480. );
  481. final files = convertToFiles(results);
  482. final List<File> deduplicatedFiles =
  483. _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
  484. dev.log(
  485. "getAllPendingOrUploadedFiles time taken: ${stopWatch.elapsedMilliseconds} ms",
  486. );
  487. stopWatch.stop();
  488. return FileLoadResult(deduplicatedFiles, files.length == limit);
  489. }
  490. Future<FileLoadResult> getAllLocalAndUploadedFiles(
  491. int startTime,
  492. int endTime,
  493. int ownerID, {
  494. int? limit,
  495. bool? asc,
  496. Set<int>? ignoredCollectionIDs,
  497. }) async {
  498. final db = await instance.database;
  499. final order = (asc ?? false ? 'ASC' : 'DESC');
  500. final results = await db.query(
  501. filesTable,
  502. where:
  503. '$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
  504. ' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
  505. whereArgs: [startTime, endTime, ownerID, visibleVisibility],
  506. orderBy:
  507. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  508. limit: limit,
  509. );
  510. final files = convertToFiles(results);
  511. final List<File> deduplicatedFiles =
  512. _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
  513. return FileLoadResult(deduplicatedFiles, files.length == limit);
  514. }
  515. List<File> deduplicateByLocalID(List<File> files) {
  516. final localIDs = <String>{};
  517. final List<File> deduplicatedFiles = [];
  518. for (final file in files) {
  519. final id = file.localID;
  520. if (id == null) {
  521. continue;
  522. }
  523. if (localIDs.contains(id)) {
  524. continue;
  525. }
  526. localIDs.add(id);
  527. deduplicatedFiles.add(file);
  528. }
  529. return deduplicatedFiles;
  530. }
  531. List<File> _deduplicatedAndFilterIgnoredFiles(
  532. List<File> files,
  533. Set<int>? ignoredCollectionIDs,
  534. ) {
  535. final Set<int> uploadedFileIDs = <int>{};
  536. // ignoredFileUploadIDs is to keep a track of files which are part of
  537. // archived collection
  538. final Set<int> ignoredFileUploadIDs = <int>{};
  539. final List<File> deduplicatedFiles = [];
  540. for (final file in files) {
  541. final id = file.uploadedFileID;
  542. final bool isFileUploaded = id != null && id != -1;
  543. final bool isCollectionIgnored = ignoredCollectionIDs != null &&
  544. ignoredCollectionIDs.contains(file.collectionID);
  545. if (isCollectionIgnored || ignoredFileUploadIDs.contains(id)) {
  546. if (isFileUploaded) {
  547. ignoredFileUploadIDs.add(id);
  548. // remove the file from the list of deduplicated files
  549. if (uploadedFileIDs.contains(id)) {
  550. deduplicatedFiles
  551. .removeWhere((element) => element.uploadedFileID == id);
  552. uploadedFileIDs.remove(id);
  553. }
  554. }
  555. continue;
  556. }
  557. if (isFileUploaded && uploadedFileIDs.contains(id)) {
  558. continue;
  559. }
  560. if (isFileUploaded) {
  561. uploadedFileIDs.add(id);
  562. }
  563. deduplicatedFiles.add(file);
  564. }
  565. return deduplicatedFiles;
  566. }
  567. Future<FileLoadResult> getFilesInCollection(
  568. int collectionID,
  569. int startTime,
  570. int endTime, {
  571. int? limit,
  572. bool? asc,
  573. int visibility = visibleVisibility,
  574. }) async {
  575. final db = await instance.database;
  576. final order = (asc ?? false ? 'ASC' : 'DESC');
  577. const String whereClause =
  578. '$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ?';
  579. final List<Object> whereArgs = [collectionID, startTime, endTime];
  580. final results = await db.query(
  581. filesTable,
  582. where: whereClause,
  583. whereArgs: whereArgs,
  584. orderBy:
  585. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  586. limit: limit,
  587. );
  588. final files = convertToFiles(results);
  589. return FileLoadResult(files, files.length == limit);
  590. }
  591. Future<List<File>> getAllFilesCollection(int collectionID) async {
  592. final db = await instance.database;
  593. const String whereClause = '$columnCollectionID = ?';
  594. final List<Object> whereArgs = [collectionID];
  595. final results = await db.query(
  596. filesTable,
  597. where: whereClause,
  598. whereArgs: whereArgs,
  599. );
  600. final files = convertToFiles(results);
  601. return files;
  602. }
  603. Future<FileLoadResult> getFilesInCollections(
  604. List<int> collectionIDs,
  605. int startTime,
  606. int endTime,
  607. int userID, {
  608. int? limit,
  609. bool? asc,
  610. }) async {
  611. if (collectionIDs.isEmpty) {
  612. return FileLoadResult(<File>[], false);
  613. }
  614. String inParam = "";
  615. for (final id in collectionIDs) {
  616. inParam += "'" + id.toString() + "',";
  617. }
  618. inParam = inParam.substring(0, inParam.length - 1);
  619. final db = await instance.database;
  620. final order = (asc ?? false ? 'ASC' : 'DESC');
  621. final String whereClause =
  622. '$columnCollectionID IN ($inParam) AND $columnCreationTime >= ? AND '
  623. '$columnCreationTime <= ? AND $columnOwnerID = ?';
  624. final List<Object> whereArgs = [startTime, endTime, userID];
  625. final results = await db.query(
  626. filesTable,
  627. where: whereClause,
  628. whereArgs: whereArgs,
  629. orderBy:
  630. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  631. limit: limit,
  632. );
  633. final files = convertToFiles(results);
  634. final dedupeResult = _deduplicatedAndFilterIgnoredFiles(files, {});
  635. _logger.info("Fetched " + dedupeResult.length.toString() + " files");
  636. return FileLoadResult(files, files.length == limit);
  637. }
  638. Future<List<File>> getFilesCreatedWithinDurations(
  639. List<List<int>> durations,
  640. Set<int> ignoredCollectionIDs, {
  641. String order = 'ASC',
  642. }) async {
  643. if (durations.isEmpty) {
  644. return <File>[];
  645. }
  646. final db = await instance.database;
  647. String whereClause = "( ";
  648. for (int index = 0; index < durations.length; index++) {
  649. whereClause += "($columnCreationTime >= " +
  650. durations[index][0].toString() +
  651. " AND $columnCreationTime < " +
  652. durations[index][1].toString() +
  653. ")";
  654. if (index != durations.length - 1) {
  655. whereClause += " OR ";
  656. }
  657. }
  658. whereClause += ") AND $columnMMdVisibility = $visibleVisibility";
  659. final results = await db.query(
  660. filesTable,
  661. where: whereClause,
  662. orderBy: '$columnCreationTime ' + order,
  663. );
  664. final files = convertToFiles(results);
  665. return _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
  666. }
  667. // Files which user added to a collection manually but they are not
  668. // uploaded yet or files belonging to a collection which is marked for backup
  669. Future<List<File>> getFilesPendingForUpload() async {
  670. final db = await instance.database;
  671. final results = await db.query(
  672. filesTable,
  673. where:
  674. '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND '
  675. '$columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1 AND '
  676. '$columnLocalID IS NOT NULL AND $columnLocalID IS NOT -1',
  677. orderBy: '$columnCreationTime DESC',
  678. groupBy: columnLocalID,
  679. );
  680. final files = convertToFiles(results);
  681. // future-safe filter just to ensure that the query doesn't end up returning files
  682. // which should not be backed up
  683. files.removeWhere(
  684. (e) =>
  685. e.collectionID == null ||
  686. e.localID == null ||
  687. e.uploadedFileID != null,
  688. );
  689. return files;
  690. }
  691. Future<List<File>> getUnUploadedLocalFiles() async {
  692. final db = await instance.database;
  693. final results = await db.query(
  694. filesTable,
  695. where:
  696. '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND $columnLocalID IS NOT NULL',
  697. orderBy: '$columnCreationTime DESC',
  698. groupBy: columnLocalID,
  699. );
  700. return convertToFiles(results);
  701. }
  702. Future<List<int>> getUploadedFileIDsToBeUpdated(int ownerID) async {
  703. final db = await instance.database;
  704. final rows = await db.query(
  705. filesTable,
  706. columns: [columnUploadedFileID],
  707. where: '($columnLocalID IS NOT NULL AND $columnOwnerID = ? AND '
  708. '($columnUploadedFileID '
  709. 'IS NOT '
  710. 'NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NULL)',
  711. whereArgs: [ownerID],
  712. orderBy: '$columnCreationTime DESC',
  713. distinct: true,
  714. );
  715. final uploadedFileIDs = <int>[];
  716. for (final row in rows) {
  717. uploadedFileIDs.add(row[columnUploadedFileID] as int);
  718. }
  719. return uploadedFileIDs;
  720. }
  721. Future<File?> getUploadedLocalFileInAnyCollection(
  722. int uploadedFileID,
  723. int userID,
  724. ) async {
  725. final db = await instance.database;
  726. final results = await db.query(
  727. filesTable,
  728. where: '$columnLocalID IS NOT NULL AND $columnOwnerID = ? AND '
  729. '$columnUploadedFileID = ?',
  730. whereArgs: [
  731. userID,
  732. uploadedFileID,
  733. ],
  734. limit: 1,
  735. );
  736. if (results.isEmpty) {
  737. return null;
  738. }
  739. return convertToFiles(results)[0];
  740. }
  741. Future<Set<String>> getExistingLocalFileIDs(int ownerID) async {
  742. final db = await instance.database;
  743. final rows = await db.query(
  744. filesTable,
  745. columns: [columnLocalID],
  746. distinct: true,
  747. where: '$columnLocalID IS NOT NULL AND ($columnOwnerID IS NULL OR '
  748. '$columnOwnerID = ?)',
  749. whereArgs: [ownerID],
  750. );
  751. final result = <String>{};
  752. for (final row in rows) {
  753. result.add(row[columnLocalID] as String);
  754. }
  755. return result;
  756. }
  757. Future<Set<String>> getLocalIDsMarkedForOrAlreadyUploaded(int ownerID) async {
  758. final db = await instance.database;
  759. final rows = await db.query(
  760. filesTable,
  761. columns: [columnLocalID],
  762. distinct: true,
  763. where: '$columnLocalID IS NOT NULL AND ($columnCollectionID IS NOT NULL '
  764. 'AND '
  765. '$columnCollectionID != -1) AND ($columnOwnerID = ? OR '
  766. '$columnOwnerID IS NULL)',
  767. whereArgs: [ownerID],
  768. );
  769. final result = <String>{};
  770. for (final row in rows) {
  771. result.add(row[columnLocalID] as String);
  772. }
  773. return result;
  774. }
  775. Future<Set<String>> getLocalFileIDsForCollection(int collectionID) async {
  776. final db = await instance.database;
  777. final rows = await db.query(
  778. filesTable,
  779. columns: [columnLocalID],
  780. where: '$columnLocalID IS NOT NULL AND $columnCollectionID = ?',
  781. whereArgs: [collectionID],
  782. );
  783. final result = <String>{};
  784. for (final row in rows) {
  785. result.add(row[columnLocalID] as String);
  786. }
  787. return result;
  788. }
  789. // Sets the collectionID for the files with given LocalIDs if the
  790. // corresponding file entries are not already mapped to some other collection
  791. Future<int> setCollectionIDForUnMappedLocalFiles(
  792. int collectionID,
  793. Set<String> localIDs,
  794. ) async {
  795. final db = await instance.database;
  796. String inParam = "";
  797. for (final localID in localIDs) {
  798. inParam += "'" + localID + "',";
  799. }
  800. inParam = inParam.substring(0, inParam.length - 1);
  801. return await db.rawUpdate(
  802. '''
  803. UPDATE $filesTable
  804. SET $columnCollectionID = $collectionID
  805. WHERE $columnLocalID IN ($inParam) AND ($columnCollectionID IS NULL OR
  806. $columnCollectionID = -1);
  807. ''',
  808. );
  809. }
  810. Future<int> updateUploadedFile(
  811. String localID,
  812. String? title,
  813. Location? location,
  814. int creationTime,
  815. int modificationTime,
  816. int? updationTime,
  817. ) async {
  818. final db = await instance.database;
  819. return await db.update(
  820. filesTable,
  821. {
  822. columnTitle: title,
  823. columnLatitude: location?.latitude,
  824. columnLongitude: location?.longitude,
  825. columnCreationTime: creationTime,
  826. columnModificationTime: modificationTime,
  827. columnUpdationTime: updationTime,
  828. },
  829. where: '$columnLocalID = ?',
  830. whereArgs: [localID],
  831. );
  832. }
  833. /*
  834. This method should only return localIDs which are not uploaded yet
  835. and can be mapped to incoming remote entry
  836. */
  837. Future<List<File>> getUnlinkedLocalMatchesForRemoteFile(
  838. int ownerID,
  839. String localID,
  840. FileType fileType, {
  841. required String title,
  842. required String deviceFolder,
  843. }) async {
  844. final db = await instance.database;
  845. // on iOS, match using localID and fileType. title can either match or
  846. // might be null based on how the file was imported
  847. String whereClause = ''' ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND
  848. $columnLocalID = ? AND $columnFileType = ? AND
  849. ($columnTitle=? OR $columnTitle IS NULL) ''';
  850. List<Object> whereArgs = [
  851. ownerID,
  852. localID,
  853. getInt(fileType),
  854. title,
  855. ];
  856. if (io.Platform.isAndroid) {
  857. whereClause = ''' ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND
  858. $columnLocalID = ? AND $columnFileType = ? AND $columnTitle=? AND $columnDeviceFolder= ?
  859. ''';
  860. whereArgs = [
  861. ownerID,
  862. localID,
  863. getInt(fileType),
  864. title,
  865. deviceFolder,
  866. ];
  867. }
  868. final rows = await db.query(
  869. filesTable,
  870. where: whereClause,
  871. whereArgs: whereArgs,
  872. );
  873. return convertToFiles(rows);
  874. }
  875. Future<List<File>> getUploadedFilesWithHashes(
  876. FileHashData hashData,
  877. FileType fileType,
  878. int ownerID,
  879. ) async {
  880. String inParam = "'${hashData.fileHash}'";
  881. if (fileType == FileType.livePhoto && hashData.zipHash != null) {
  882. inParam += ",'${hashData.zipHash}'";
  883. }
  884. final db = await instance.database;
  885. final rows = await db.query(
  886. filesTable,
  887. where: '($columnUploadedFileID != NULL OR $columnUploadedFileID != -1) '
  888. 'AND $columnOwnerID = ? AND $columnFileType ='
  889. ' ? '
  890. 'AND $columnHash IN ($inParam)',
  891. whereArgs: [
  892. ownerID,
  893. getInt(fileType),
  894. ],
  895. );
  896. return convertToFiles(rows);
  897. }
  898. Future<int> update(File file) async {
  899. final db = await instance.database;
  900. return await db.update(
  901. filesTable,
  902. _getRowForFile(file),
  903. where: '$columnGeneratedID = ?',
  904. whereArgs: [file.generatedID],
  905. );
  906. }
  907. Future<int> updateUploadedFileAcrossCollections(File file) async {
  908. final db = await instance.database;
  909. return await db.update(
  910. filesTable,
  911. _getRowForFileWithoutCollection(file),
  912. where: '$columnUploadedFileID = ?',
  913. whereArgs: [file.uploadedFileID],
  914. );
  915. }
  916. Future<int> updateLocalIDForUploaded(int uploadedID, String localID) async {
  917. final db = await instance.database;
  918. return await db.update(
  919. filesTable,
  920. {columnLocalID: localID},
  921. where: '$columnUploadedFileID = ? AND $columnLocalID IS NULL',
  922. whereArgs: [uploadedID],
  923. );
  924. }
  925. Future<int> delete(int uploadedFileID) async {
  926. final db = await instance.database;
  927. return db.delete(
  928. filesTable,
  929. where: '$columnUploadedFileID =?',
  930. whereArgs: [uploadedFileID],
  931. );
  932. }
  933. Future<int> deleteByGeneratedID(int genID) async {
  934. final db = await instance.database;
  935. return db.delete(
  936. filesTable,
  937. where: '$columnGeneratedID =?',
  938. whereArgs: [genID],
  939. );
  940. }
  941. Future<int> deleteMultipleUploadedFiles(List<int> uploadedFileIDs) async {
  942. final db = await instance.database;
  943. return await db.delete(
  944. filesTable,
  945. where: '$columnUploadedFileID IN (${uploadedFileIDs.join(', ')})',
  946. );
  947. }
  948. Future<int> deleteMultipleByGeneratedIDs(List<int> generatedIDs) async {
  949. if (generatedIDs.isEmpty) {
  950. return 0;
  951. }
  952. final db = await instance.database;
  953. return await db.delete(
  954. filesTable,
  955. where: '$columnGeneratedID IN (${generatedIDs.join(', ')})',
  956. );
  957. }
  958. Future<int> deleteLocalFile(File file) async {
  959. final db = await instance.database;
  960. if (file.localID != null) {
  961. // delete all files with same local ID
  962. return db.delete(
  963. filesTable,
  964. where: '$columnLocalID =?',
  965. whereArgs: [file.localID],
  966. );
  967. } else {
  968. return db.delete(
  969. filesTable,
  970. where: '$columnGeneratedID =?',
  971. whereArgs: [file.generatedID],
  972. );
  973. }
  974. }
  975. Future<void> deleteLocalFiles(List<String> localIDs) async {
  976. String inParam = "";
  977. for (final localID in localIDs) {
  978. inParam += "'" + localID + "',";
  979. }
  980. inParam = inParam.substring(0, inParam.length - 1);
  981. final db = await instance.database;
  982. await db.rawQuery(
  983. '''
  984. UPDATE $filesTable
  985. SET $columnLocalID = NULL
  986. WHERE $columnLocalID IN ($inParam);
  987. ''',
  988. );
  989. }
  990. Future<List<File>> getLocalFiles(List<String> localIDs) async {
  991. String inParam = "";
  992. for (final localID in localIDs) {
  993. inParam += "'" + localID + "',";
  994. }
  995. inParam = inParam.substring(0, inParam.length - 1);
  996. final db = await instance.database;
  997. final results = await db.query(
  998. filesTable,
  999. where: '$columnLocalID IN ($inParam)',
  1000. );
  1001. return convertToFiles(results);
  1002. }
  1003. Future<int> deleteUnSyncedLocalFiles(List<String> localIDs) async {
  1004. String inParam = "";
  1005. for (final localID in localIDs) {
  1006. inParam += "'" + localID + "',";
  1007. }
  1008. inParam = inParam.substring(0, inParam.length - 1);
  1009. final db = await instance.database;
  1010. return db.delete(
  1011. filesTable,
  1012. where:
  1013. '($columnUploadedFileID is NULL OR $columnUploadedFileID = -1 ) AND $columnLocalID IN ($inParam)',
  1014. );
  1015. }
  1016. Future<int> deleteFromCollection(int uploadedFileID, int collectionID) async {
  1017. final db = await instance.database;
  1018. return db.delete(
  1019. filesTable,
  1020. where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
  1021. whereArgs: [uploadedFileID, collectionID],
  1022. );
  1023. }
  1024. Future<int> deleteFilesFromCollection(
  1025. int collectionID,
  1026. List<int> uploadedFileIDs,
  1027. ) async {
  1028. final db = await instance.database;
  1029. return db.delete(
  1030. filesTable,
  1031. where:
  1032. '$columnCollectionID = ? AND $columnUploadedFileID IN (${uploadedFileIDs.join(', ')})',
  1033. whereArgs: [collectionID],
  1034. );
  1035. }
  1036. Future<int> collectionFileCount(int collectionID) async {
  1037. final db = await instance.database;
  1038. final count = Sqflite.firstIntValue(
  1039. await db.rawQuery(
  1040. 'SELECT COUNT(*) FROM $filesTable where $columnCollectionID = '
  1041. '$collectionID AND $columnUploadedFileID IS NOT -1',
  1042. ),
  1043. );
  1044. return count ?? 0;
  1045. }
  1046. Future<int> archivedFilesCount(
  1047. int visibility,
  1048. int ownerID,
  1049. Set<int> hiddenCollections,
  1050. ) async {
  1051. final db = await instance.database;
  1052. final count = Sqflite.firstIntValue(
  1053. await db.rawQuery(
  1054. 'SELECT COUNT(distinct($columnUploadedFileID)) FROM $filesTable where '
  1055. '$columnMMdVisibility'
  1056. ' = $visibility AND $columnOwnerID = $ownerID AND $columnCollectionID NOT IN (${hiddenCollections.join(', ')})',
  1057. ),
  1058. );
  1059. return count ?? 0;
  1060. }
  1061. Future<int> deleteCollection(int collectionID) async {
  1062. final db = await instance.database;
  1063. return db.delete(
  1064. filesTable,
  1065. where: '$columnCollectionID = ?',
  1066. whereArgs: [collectionID],
  1067. );
  1068. }
  1069. Future<int> removeFromCollection(int collectionID, List<int> fileIDs) async {
  1070. final db = await instance.database;
  1071. return db.delete(
  1072. filesTable,
  1073. where:
  1074. '$columnCollectionID =? AND $columnUploadedFileID IN (${fileIDs.join(', ')})',
  1075. whereArgs: [collectionID],
  1076. );
  1077. }
  1078. Future<List<File>> getPendingUploadForCollection(int collectionID) async {
  1079. final db = await instance.database;
  1080. final results = await db.query(
  1081. filesTable,
  1082. where: '$columnCollectionID = ? AND ($columnUploadedFileID IS NULL OR '
  1083. '$columnUploadedFileID = -1)',
  1084. whereArgs: [collectionID],
  1085. );
  1086. return convertToFiles(results);
  1087. }
  1088. Future<Set<String>> getLocalIDsPresentInEntries(
  1089. List<File> existingFiles,
  1090. int collectionID,
  1091. ) async {
  1092. String inParam = "";
  1093. for (final existingFile in existingFiles) {
  1094. if (existingFile.localID != null) {
  1095. inParam += "'" + existingFile.localID! + "',";
  1096. }
  1097. }
  1098. inParam = inParam.substring(0, inParam.length - 1);
  1099. final db = await instance.database;
  1100. final rows = await db.rawQuery(
  1101. '''
  1102. SELECT $columnLocalID
  1103. FROM $filesTable
  1104. WHERE $columnLocalID IN ($inParam) AND $columnCollectionID !=
  1105. $collectionID AND $columnLocalID IS NOT NULL;
  1106. ''',
  1107. );
  1108. final result = <String>{};
  1109. for (final row in rows) {
  1110. result.add(row[columnLocalID] as String);
  1111. }
  1112. return result;
  1113. }
  1114. Future<List<File>> getLatestCollectionFiles() async {
  1115. debugPrint("Fetching latestCollectionFiles from db");
  1116. const String query = '''
  1117. SELECT $filesTable.*
  1118. FROM $filesTable
  1119. INNER JOIN
  1120. (
  1121. SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time
  1122. FROM $filesTable
  1123. WHERE
  1124. ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1
  1125. AND $columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS
  1126. NOT -1)
  1127. GROUP BY $columnCollectionID
  1128. ) latest_files
  1129. ON $filesTable.$columnCollectionID = latest_files.$columnCollectionID
  1130. AND $filesTable.$columnCreationTime = latest_files.max_creation_time;
  1131. ''';
  1132. final db = await instance.database;
  1133. final rows = await db.rawQuery(
  1134. query,
  1135. );
  1136. final files = convertToFiles(rows);
  1137. // TODO: Do this de-duplication within the SQL Query
  1138. final collectionMap = <int, File>{};
  1139. for (final file in files) {
  1140. if (collectionMap.containsKey(file.collectionID)) {
  1141. if (collectionMap[file.collectionID]!.updationTime! <
  1142. file.updationTime!) {
  1143. continue;
  1144. }
  1145. }
  1146. collectionMap[file.collectionID!] = file;
  1147. }
  1148. return collectionMap.values.toList();
  1149. }
  1150. Future<void> markForReUploadIfLocationMissing(List<String> localIDs) async {
  1151. if (localIDs.isEmpty) {
  1152. return;
  1153. }
  1154. String inParam = "";
  1155. for (final localID in localIDs) {
  1156. inParam += "'" + localID + "',";
  1157. }
  1158. inParam = inParam.substring(0, inParam.length - 1);
  1159. final db = await instance.database;
  1160. await db.rawUpdate(
  1161. '''
  1162. UPDATE $filesTable
  1163. SET $columnUpdationTime = NULL
  1164. WHERE $columnLocalID IN ($inParam)
  1165. AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0);
  1166. ''',
  1167. );
  1168. }
  1169. Future<bool> doesFileExistInCollection(
  1170. int uploadedFileID,
  1171. int collectionID,
  1172. ) async {
  1173. final db = await instance.database;
  1174. final rows = await db.query(
  1175. filesTable,
  1176. where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
  1177. whereArgs: [uploadedFileID, collectionID],
  1178. limit: 1,
  1179. );
  1180. return rows.isNotEmpty;
  1181. }
  1182. Future<Map<int, File>> getFilesFromIDs(List<int> ids) async {
  1183. final result = <int, File>{};
  1184. if (ids.isEmpty) {
  1185. return result;
  1186. }
  1187. String inParam = "";
  1188. for (final id in ids) {
  1189. inParam += "'" + id.toString() + "',";
  1190. }
  1191. inParam = inParam.substring(0, inParam.length - 1);
  1192. final db = await instance.database;
  1193. final results = await db.query(
  1194. filesTable,
  1195. where: '$columnUploadedFileID IN ($inParam)',
  1196. );
  1197. final files = convertToFiles(results);
  1198. for (final file in files) {
  1199. result[file.uploadedFileID!] = file;
  1200. }
  1201. return result;
  1202. }
  1203. Future<Map<int, File>> getFilesFromGeneratedIDs(List<int> ids) async {
  1204. final result = <int, File>{};
  1205. if (ids.isEmpty) {
  1206. return result;
  1207. }
  1208. String inParam = "";
  1209. for (final id in ids) {
  1210. inParam += "'" + id.toString() + "',";
  1211. }
  1212. inParam = inParam.substring(0, inParam.length - 1);
  1213. final db = await instance.database;
  1214. final results = await db.query(
  1215. filesTable,
  1216. where: '$columnGeneratedID IN ($inParam)',
  1217. );
  1218. final files = convertToFiles(results);
  1219. for (final file in files) {
  1220. result[file.generatedID as int] = file;
  1221. }
  1222. return result;
  1223. }
  1224. Future<Map<int, List<File>>> getAllFilesGroupByCollectionID(
  1225. List<int> ids,
  1226. ) async {
  1227. final result = <int, List<File>>{};
  1228. if (ids.isEmpty) {
  1229. return result;
  1230. }
  1231. String inParam = "";
  1232. for (final id in ids) {
  1233. inParam += "'" + id.toString() + "',";
  1234. }
  1235. inParam = inParam.substring(0, inParam.length - 1);
  1236. final db = await instance.database;
  1237. final results = await db.query(
  1238. filesTable,
  1239. where: '$columnUploadedFileID IN ($inParam)',
  1240. );
  1241. final files = convertToFiles(results);
  1242. for (File eachFile in files) {
  1243. if (!result.containsKey(eachFile.collectionID)) {
  1244. result[eachFile.collectionID as int] = <File>[];
  1245. }
  1246. result[eachFile.collectionID]!.add(eachFile);
  1247. }
  1248. return result;
  1249. }
  1250. Future<Set<int>> getAllCollectionIDsOfFile(
  1251. int uploadedFileID,
  1252. ) async {
  1253. final db = await instance.database;
  1254. final results = await db.query(
  1255. filesTable,
  1256. where: '$columnUploadedFileID = ? AND $columnCollectionID != -1',
  1257. columns: [columnCollectionID],
  1258. whereArgs: [uploadedFileID],
  1259. distinct: true,
  1260. );
  1261. final collectionIDsOfFile = <int>{};
  1262. for (var result in results) {
  1263. collectionIDsOfFile.add(result['collection_id'] as int);
  1264. }
  1265. return collectionIDsOfFile;
  1266. }
  1267. List<File> convertToFiles(List<Map<String, dynamic>> results) {
  1268. final List<File> files = [];
  1269. for (final result in results) {
  1270. files.add(_getFileFromRow(result));
  1271. }
  1272. return files;
  1273. }
  1274. Future<List<String>> getGeneratedIDForFilesOlderThan(
  1275. int cutOffTime,
  1276. int ownerID,
  1277. ) async {
  1278. final db = await instance.database;
  1279. final rows = await db.query(
  1280. filesTable,
  1281. columns: [columnGeneratedID],
  1282. distinct: true,
  1283. where:
  1284. '$columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?)',
  1285. whereArgs: [cutOffTime, ownerID],
  1286. );
  1287. final result = <String>[];
  1288. for (final row in rows) {
  1289. result.add(row[columnGeneratedID].toString());
  1290. }
  1291. return result;
  1292. }
  1293. // For givenUserID, get List of unique LocalIDs for files which are
  1294. // uploaded by the given user and location is missing
  1295. Future<List<String>> getLocalIDsForFilesWithoutLocation(int ownerID) async {
  1296. final db = await instance.database;
  1297. final rows = await db.query(
  1298. filesTable,
  1299. columns: [columnLocalID],
  1300. distinct: true,
  1301. where: '$columnOwnerID = ? AND $columnLocalID IS NOT NULL AND '
  1302. '($columnLatitude IS NULL OR '
  1303. '$columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0)',
  1304. whereArgs: [ownerID],
  1305. );
  1306. final result = <String>[];
  1307. for (final row in rows) {
  1308. result.add(row[columnLocalID].toString());
  1309. }
  1310. return result;
  1311. }
  1312. // returns the localID of all files which are uploaded and belong to the
  1313. // user and upload time is greater than 20 April 2023 epoch time and less than
  1314. // 15 May 2023 epoch time
  1315. Future<List<String>> getFilesWithLocationUploadedBtw20AprTo15May2023(
  1316. int ownerID,
  1317. ) async {
  1318. final db = await database;
  1319. final result = await db.query(
  1320. filesTable,
  1321. columns: [columnLocalID],
  1322. distinct: true,
  1323. where: ''
  1324. '($columnUploadedFileID IS NOT NULL'
  1325. ' AND $columnUploadedFileID IS NOT -1)'
  1326. ' AND $columnOwnerID = ?'
  1327. ' AND $columnUpdationTime > ? AND $columnUpdationTime < ? '
  1328. 'AND ($columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL) '
  1329. 'AND ($columnLongitude IS NOT 0.0 AND $columnLongitude IS NOT 0.0)',
  1330. whereArgs: [
  1331. ownerID,
  1332. 1681952400000000,
  1333. 1684112400000000,
  1334. ],
  1335. );
  1336. return result.map((row) => row[columnLocalID].toString()).toList();
  1337. }
  1338. // For given list of localIDs and ownerID, get a list of uploaded files
  1339. // owned by given user
  1340. Future<List<File>> getFilesForLocalIDs(
  1341. List<String> localIDs,
  1342. int ownerID,
  1343. ) async {
  1344. final db = await instance.database;
  1345. final rows = await db.query(
  1346. filesTable,
  1347. where:
  1348. '$columnLocalID IN (${localIDs.map((e) => "'$e'").join(',')}) AND $columnOwnerID = ?',
  1349. whereArgs: [ownerID],
  1350. );
  1351. return _deduplicatedAndFilterIgnoredFiles(convertToFiles(rows), {});
  1352. }
  1353. Future<List<File>> getAllFilesFromDB(Set<int> collectionsToIgnore) async {
  1354. final db = await instance.database;
  1355. final List<Map<String, dynamic>> result =
  1356. await db.query(filesTable, orderBy: '$columnCreationTime DESC');
  1357. final List<File> files = convertToFiles(result);
  1358. final List<File> deduplicatedFiles =
  1359. _deduplicatedAndFilterIgnoredFiles(files, collectionsToIgnore);
  1360. return deduplicatedFiles;
  1361. }
  1362. Future<Map<FileType, int>> fetchFilesCountbyType(int userID) async {
  1363. final db = await instance.database;
  1364. final result = await db.rawQuery(
  1365. "SELECT $columnFileType, COUNT(DISTINCT $columnUploadedFileID) FROM $filesTable WHERE $columnUploadedFileID != -1 AND $columnOwnerID == $userID GROUP BY $columnFileType",
  1366. );
  1367. final filesCount = <FileType, int>{};
  1368. for (var e in result) {
  1369. filesCount.addAll(
  1370. {getFileType(e[columnFileType] as int): e.values.last as int},
  1371. );
  1372. }
  1373. return filesCount;
  1374. }
  1375. Future<FileLoadResult> fetchAllUploadedAndSharedFilesWithLocation(
  1376. int startTime,
  1377. int endTime, {
  1378. int? limit,
  1379. bool? asc,
  1380. Set<int>? ignoredCollectionIDs,
  1381. }) async {
  1382. final db = await instance.database;
  1383. final order = (asc ?? false ? 'ASC' : 'DESC');
  1384. final results = await db.query(
  1385. filesTable,
  1386. where:
  1387. '$columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0)'
  1388. ' AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND '
  1389. '($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
  1390. ' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
  1391. whereArgs: [startTime, endTime, visibleVisibility],
  1392. orderBy:
  1393. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  1394. limit: limit,
  1395. );
  1396. final files = convertToFiles(results);
  1397. final List<File> deduplicatedFiles =
  1398. _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
  1399. return FileLoadResult(deduplicatedFiles, files.length == limit);
  1400. }
  1401. Map<String, dynamic> _getRowForFile(File file) {
  1402. final row = <String, dynamic>{};
  1403. if (file.generatedID != null) {
  1404. row[columnGeneratedID] = file.generatedID;
  1405. }
  1406. row[columnLocalID] = file.localID;
  1407. row[columnUploadedFileID] = file.uploadedFileID ?? -1;
  1408. row[columnOwnerID] = file.ownerID;
  1409. row[columnCollectionID] = file.collectionID ?? -1;
  1410. row[columnTitle] = file.title;
  1411. row[columnDeviceFolder] = file.deviceFolder;
  1412. // if (file.location == null ||
  1413. // (file.location!.latitude == null && file.location!.longitude == null)) {
  1414. // file.location = Location.randomLocation();
  1415. // }
  1416. if (file.location != null) {
  1417. row[columnLatitude] = file.location!.latitude;
  1418. row[columnLongitude] = file.location!.longitude;
  1419. }
  1420. row[columnFileType] = getInt(file.fileType);
  1421. row[columnCreationTime] = file.creationTime;
  1422. row[columnModificationTime] = file.modificationTime;
  1423. row[columnUpdationTime] = file.updationTime;
  1424. row[columnEncryptedKey] = file.encryptedKey;
  1425. row[columnKeyDecryptionNonce] = file.keyDecryptionNonce;
  1426. row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
  1427. row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
  1428. row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
  1429. row[columnFileSubType] = file.fileSubType ?? -1;
  1430. row[columnDuration] = file.duration ?? 0;
  1431. row[columnExif] = file.exif;
  1432. row[columnHash] = file.hash;
  1433. row[columnMetadataVersion] = file.metadataVersion;
  1434. row[columnFileSize] = file.fileSize;
  1435. row[columnMMdVersion] = file.mMdVersion;
  1436. row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}';
  1437. row[columnMMdVisibility] = file.magicMetadata.visibility;
  1438. row[columnPubMMdVersion] = file.pubMmdVersion;
  1439. row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}';
  1440. // override existing fields to avoid re-writing all queries and logic
  1441. if (file.pubMagicMetadata != null) {
  1442. if (file.pubMagicMetadata!.editedTime != null) {
  1443. row[columnCreationTime] = file.pubMagicMetadata!.editedTime;
  1444. }
  1445. if (file.pubMagicMetadata!.lat != null &&
  1446. file.pubMagicMetadata!.long != null) {
  1447. row[columnLatitude] = file.pubMagicMetadata!.lat;
  1448. row[columnLongitude] = file.pubMagicMetadata!.long;
  1449. }
  1450. }
  1451. return row;
  1452. }
  1453. Map<String, dynamic> _getRowForFileWithoutCollection(File file) {
  1454. final row = <String, dynamic>{};
  1455. row[columnLocalID] = file.localID;
  1456. row[columnUploadedFileID] = file.uploadedFileID ?? -1;
  1457. row[columnOwnerID] = file.ownerID;
  1458. row[columnTitle] = file.title;
  1459. row[columnDeviceFolder] = file.deviceFolder;
  1460. if (file.location != null) {
  1461. row[columnLatitude] = file.location!.latitude;
  1462. row[columnLongitude] = file.location!.longitude;
  1463. }
  1464. row[columnFileType] = getInt(file.fileType);
  1465. row[columnCreationTime] = file.creationTime;
  1466. row[columnModificationTime] = file.modificationTime;
  1467. row[columnUpdationTime] = file.updationTime;
  1468. row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
  1469. row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
  1470. row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
  1471. row[columnFileSubType] = file.fileSubType ?? -1;
  1472. row[columnDuration] = file.duration ?? 0;
  1473. row[columnExif] = file.exif;
  1474. row[columnHash] = file.hash;
  1475. row[columnMetadataVersion] = file.metadataVersion;
  1476. row[columnMMdVersion] = file.mMdVersion;
  1477. row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}';
  1478. row[columnMMdVisibility] = file.magicMetadata.visibility;
  1479. row[columnPubMMdVersion] = file.pubMmdVersion;
  1480. row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}';
  1481. if (file.pubMagicMetadata != null &&
  1482. file.pubMagicMetadata!.editedTime != null) {
  1483. // override existing creationTime to avoid re-writing all queries related
  1484. // to loading the gallery
  1485. row[columnCreationTime] = file.pubMagicMetadata!.editedTime!;
  1486. }
  1487. return row;
  1488. }
  1489. File _getFileFromRow(Map<String, dynamic> row) {
  1490. final file = File();
  1491. file.generatedID = row[columnGeneratedID];
  1492. file.localID = row[columnLocalID];
  1493. file.uploadedFileID =
  1494. row[columnUploadedFileID] == -1 ? null : row[columnUploadedFileID];
  1495. file.ownerID = row[columnOwnerID];
  1496. file.collectionID =
  1497. row[columnCollectionID] == -1 ? null : row[columnCollectionID];
  1498. file.title = row[columnTitle];
  1499. file.deviceFolder = row[columnDeviceFolder];
  1500. if (row[columnLatitude] != null && row[columnLongitude] != null) {
  1501. file.location = Location(
  1502. latitude: row[columnLatitude],
  1503. longitude: row[columnLongitude],
  1504. );
  1505. }
  1506. file.fileType = getFileType(row[columnFileType]);
  1507. file.creationTime = row[columnCreationTime];
  1508. file.modificationTime = row[columnModificationTime];
  1509. file.updationTime = row[columnUpdationTime] ?? -1;
  1510. file.encryptedKey = row[columnEncryptedKey];
  1511. file.keyDecryptionNonce = row[columnKeyDecryptionNonce];
  1512. file.fileDecryptionHeader = row[columnFileDecryptionHeader];
  1513. file.thumbnailDecryptionHeader = row[columnThumbnailDecryptionHeader];
  1514. file.metadataDecryptionHeader = row[columnMetadataDecryptionHeader];
  1515. file.fileSubType = row[columnFileSubType] ?? -1;
  1516. file.duration = row[columnDuration] ?? 0;
  1517. file.exif = row[columnExif];
  1518. file.hash = row[columnHash];
  1519. file.metadataVersion = row[columnMetadataVersion] ?? 0;
  1520. file.fileSize = row[columnFileSize];
  1521. file.mMdVersion = row[columnMMdVersion] ?? 0;
  1522. file.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}';
  1523. file.pubMmdVersion = row[columnPubMMdVersion] ?? 0;
  1524. file.pubMmdEncodedJson = row[columnPubMMdEncodedJson] ?? '{}';
  1525. return file;
  1526. }
  1527. }