files_db.dart 47 KB

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