files_db.dart 53 KB

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