files_db.dart 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685
  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/file.dart';
  9. import 'package:photos/models/file/file_type.dart';
  10. import 'package:photos/models/file_load_result.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 =
  543. await applyDBFilters(files, filterOptions);
  544. return FileLoadResult(filteredFiles, files.length == limit);
  545. }
  546. List<EnteFile> deduplicateByLocalID(List<EnteFile> files) {
  547. final localIDs = <String>{};
  548. final List<EnteFile> deduplicatedFiles = [];
  549. for (final file in files) {
  550. final id = file.localID;
  551. if (id == null) {
  552. continue;
  553. }
  554. if (localIDs.contains(id)) {
  555. continue;
  556. }
  557. localIDs.add(id);
  558. deduplicatedFiles.add(file);
  559. }
  560. return deduplicatedFiles;
  561. }
  562. Future<FileLoadResult> getFilesInCollection(
  563. int collectionID,
  564. int startTime,
  565. int endTime, {
  566. int? limit,
  567. bool? asc,
  568. int visibility = visibleVisibility,
  569. }) async {
  570. final db = await instance.database;
  571. final order = (asc ?? false ? 'ASC' : 'DESC');
  572. const String whereClause =
  573. '$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ?';
  574. final List<Object> whereArgs = [collectionID, startTime, endTime];
  575. final results = await db.query(
  576. filesTable,
  577. where: whereClause,
  578. whereArgs: whereArgs,
  579. orderBy:
  580. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  581. limit: limit,
  582. );
  583. final files = convertToFiles(results);
  584. return FileLoadResult(files, files.length == limit);
  585. }
  586. Future<List<EnteFile>> getAllFilesCollection(int collectionID) async {
  587. final db = await instance.database;
  588. const String whereClause = '$columnCollectionID = ?';
  589. final List<Object> whereArgs = [collectionID];
  590. final results = await db.query(
  591. filesTable,
  592. where: whereClause,
  593. whereArgs: whereArgs,
  594. );
  595. final files = convertToFiles(results);
  596. return files;
  597. }
  598. Future<List<EnteFile>> getNewFilesInCollection(
  599. int collectionID,
  600. int addedTime,
  601. ) async {
  602. final db = await instance.database;
  603. const String whereClause =
  604. '$columnCollectionID = ? AND $columnAddedTime > ?';
  605. final List<Object> whereArgs = [collectionID, addedTime];
  606. final results = await db.query(
  607. filesTable,
  608. where: whereClause,
  609. whereArgs: whereArgs,
  610. );
  611. final files = convertToFiles(results);
  612. return files;
  613. }
  614. Future<FileLoadResult> getFilesInCollections(
  615. List<int> collectionIDs,
  616. int startTime,
  617. int endTime,
  618. int userID, {
  619. int? limit,
  620. bool? asc,
  621. }) async {
  622. if (collectionIDs.isEmpty) {
  623. return FileLoadResult(<EnteFile>[], false);
  624. }
  625. String inParam = "";
  626. for (final id in collectionIDs) {
  627. inParam += "'" + id.toString() + "',";
  628. }
  629. inParam = inParam.substring(0, inParam.length - 1);
  630. final db = await instance.database;
  631. final order = (asc ?? false ? 'ASC' : 'DESC');
  632. final String whereClause =
  633. '$columnCollectionID IN ($inParam) AND $columnCreationTime >= ? AND '
  634. '$columnCreationTime <= ? AND $columnOwnerID = ?';
  635. final List<Object> whereArgs = [startTime, endTime, userID];
  636. final results = await db.query(
  637. filesTable,
  638. where: whereClause,
  639. whereArgs: whereArgs,
  640. orderBy:
  641. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  642. limit: limit,
  643. );
  644. final files = convertToFiles(results);
  645. final dedupeResult =
  646. await applyDBFilters(files, DBFilterOptions.dedupeOption);
  647. _logger.info("Fetched " + dedupeResult.length.toString() + " files");
  648. return FileLoadResult(files, files.length == limit);
  649. }
  650. Future<List<EnteFile>> getFilesCreatedWithinDurations(
  651. List<List<int>> durations,
  652. Set<int> ignoredCollectionIDs, {
  653. int? visibility,
  654. String order = 'ASC',
  655. }) async {
  656. if (durations.isEmpty) {
  657. return <EnteFile>[];
  658. }
  659. final db = await instance.database;
  660. String whereClause = "( ";
  661. for (int index = 0; index < durations.length; index++) {
  662. whereClause += "($columnCreationTime >= " +
  663. durations[index][0].toString() +
  664. " AND $columnCreationTime < " +
  665. durations[index][1].toString() +
  666. ")";
  667. if (index != durations.length - 1) {
  668. whereClause += " OR ";
  669. } else if (visibility != null) {
  670. whereClause += ' AND $columnMMdVisibility = $visibility';
  671. }
  672. }
  673. whereClause += ")";
  674. final results = await db.query(
  675. filesTable,
  676. where: whereClause,
  677. orderBy: '$columnCreationTime ' + order,
  678. );
  679. final files = convertToFiles(results);
  680. return applyDBFilters(
  681. files,
  682. DBFilterOptions(ignoredCollectionIDs: ignoredCollectionIDs),
  683. );
  684. }
  685. // Files which user added to a collection manually but they are not
  686. // uploaded yet or files belonging to a collection which is marked for backup
  687. Future<List<EnteFile>> getFilesPendingForUpload() async {
  688. final db = await instance.database;
  689. final results = await db.query(
  690. filesTable,
  691. where:
  692. '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND '
  693. '$columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1 AND '
  694. '$columnLocalID IS NOT NULL AND $columnLocalID IS NOT -1',
  695. orderBy: '$columnCreationTime DESC',
  696. groupBy: columnLocalID,
  697. );
  698. final files = convertToFiles(results);
  699. // future-safe filter just to ensure that the query doesn't end up returning files
  700. // which should not be backed up
  701. files.removeWhere(
  702. (e) =>
  703. e.collectionID == null ||
  704. e.localID == null ||
  705. e.uploadedFileID != null,
  706. );
  707. return files;
  708. }
  709. Future<List<EnteFile>> getUnUploadedLocalFiles() async {
  710. final db = await instance.database;
  711. final results = await db.query(
  712. filesTable,
  713. where:
  714. '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND $columnLocalID IS NOT NULL',
  715. orderBy: '$columnCreationTime DESC',
  716. groupBy: columnLocalID,
  717. );
  718. return convertToFiles(results);
  719. }
  720. Future<List<int>> getUploadedFileIDsToBeUpdated(int ownerID) async {
  721. final db = await instance.database;
  722. final rows = await db.query(
  723. filesTable,
  724. columns: [columnUploadedFileID],
  725. where: '($columnLocalID IS NOT NULL AND $columnOwnerID = ? AND '
  726. '($columnUploadedFileID '
  727. 'IS NOT '
  728. 'NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NULL)',
  729. whereArgs: [ownerID],
  730. orderBy: '$columnCreationTime DESC',
  731. distinct: true,
  732. );
  733. final uploadedFileIDs = <int>[];
  734. for (final row in rows) {
  735. uploadedFileIDs.add(row[columnUploadedFileID] as int);
  736. }
  737. return uploadedFileIDs;
  738. }
  739. Future<List<EnteFile>> getFilesInAllCollection(
  740. int uploadedFileID,
  741. int userID,
  742. ) async {
  743. final db = await instance.database;
  744. final results = await db.query(
  745. filesTable,
  746. where: '$columnLocalID IS NOT NULL AND $columnOwnerID = ? AND '
  747. '$columnUploadedFileID = ?',
  748. whereArgs: [
  749. userID,
  750. uploadedFileID,
  751. ],
  752. );
  753. if (results.isEmpty) {
  754. return <EnteFile>[];
  755. }
  756. return convertToFiles(results);
  757. }
  758. Future<Set<String>> getExistingLocalFileIDs(int ownerID) async {
  759. final db = await instance.database;
  760. final rows = await db.query(
  761. filesTable,
  762. columns: [columnLocalID],
  763. distinct: true,
  764. where: '$columnLocalID IS NOT NULL AND ($columnOwnerID IS NULL OR '
  765. '$columnOwnerID = ?)',
  766. whereArgs: [ownerID],
  767. );
  768. final result = <String>{};
  769. for (final row in rows) {
  770. result.add(row[columnLocalID] as String);
  771. }
  772. return result;
  773. }
  774. Future<Set<String>> getLocalIDsMarkedForOrAlreadyUploaded(int ownerID) async {
  775. final db = await instance.database;
  776. final rows = await db.query(
  777. filesTable,
  778. columns: [columnLocalID],
  779. distinct: true,
  780. where: '$columnLocalID IS NOT NULL AND ($columnCollectionID IS NOT NULL '
  781. 'AND '
  782. '$columnCollectionID != -1) AND ($columnOwnerID = ? OR '
  783. '$columnOwnerID IS NULL)',
  784. whereArgs: [ownerID],
  785. );
  786. final result = <String>{};
  787. for (final row in rows) {
  788. result.add(row[columnLocalID] as String);
  789. }
  790. return result;
  791. }
  792. Future<Set<String>> getLocalFileIDsForCollection(int collectionID) async {
  793. final db = await instance.database;
  794. final rows = await db.query(
  795. filesTable,
  796. columns: [columnLocalID],
  797. where: '$columnLocalID IS NOT NULL AND $columnCollectionID = ?',
  798. whereArgs: [collectionID],
  799. );
  800. final result = <String>{};
  801. for (final row in rows) {
  802. result.add(row[columnLocalID] as String);
  803. }
  804. return result;
  805. }
  806. // Sets the collectionID for the files with given LocalIDs if the
  807. // corresponding file entries are not already mapped to some other collection
  808. Future<int> setCollectionIDForUnMappedLocalFiles(
  809. int collectionID,
  810. Set<String> localIDs,
  811. ) async {
  812. final db = await instance.database;
  813. String inParam = "";
  814. for (final localID in localIDs) {
  815. inParam += "'" + localID + "',";
  816. }
  817. inParam = inParam.substring(0, inParam.length - 1);
  818. return await db.rawUpdate(
  819. '''
  820. UPDATE $filesTable
  821. SET $columnCollectionID = $collectionID
  822. WHERE $columnLocalID IN ($inParam) AND ($columnCollectionID IS NULL OR
  823. $columnCollectionID = -1);
  824. ''',
  825. );
  826. }
  827. Future<int> markFilesForReUpload(
  828. int ownerID,
  829. String localID,
  830. String? title,
  831. Location? location,
  832. int creationTime,
  833. int modificationTime,
  834. FileType fileType,
  835. ) async {
  836. final db = await instance.database;
  837. return await db.update(
  838. filesTable,
  839. {
  840. columnTitle: title,
  841. columnLatitude: location?.latitude,
  842. columnLongitude: location?.longitude,
  843. columnCreationTime: creationTime,
  844. columnModificationTime: modificationTime,
  845. // #hack reset updation time to null for re-upload
  846. columnUpdationTime: null,
  847. columnFileType: getInt(fileType),
  848. },
  849. where:
  850. '$columnLocalID = ? AND ($columnOwnerID = ? OR $columnOwnerID IS NULL)',
  851. whereArgs: [localID, ownerID],
  852. );
  853. }
  854. /*
  855. This method should only return localIDs which are not uploaded yet
  856. and can be mapped to incoming remote entry
  857. */
  858. Future<List<EnteFile>> getUnlinkedLocalMatchesForRemoteFile(
  859. int ownerID,
  860. String localID,
  861. FileType fileType, {
  862. required String title,
  863. required String deviceFolder,
  864. }) async {
  865. final db = await instance.database;
  866. // on iOS, match using localID and fileType. title can either match or
  867. // might be null based on how the file was imported
  868. String whereClause = ''' ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND
  869. $columnLocalID = ? AND $columnFileType = ? AND
  870. ($columnTitle=? OR $columnTitle IS NULL) ''';
  871. List<Object> whereArgs = [
  872. ownerID,
  873. localID,
  874. getInt(fileType),
  875. title,
  876. ];
  877. if (Platform.isAndroid) {
  878. whereClause = ''' ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND
  879. $columnLocalID = ? AND $columnFileType = ? AND $columnTitle=? AND $columnDeviceFolder= ?
  880. ''';
  881. whereArgs = [
  882. ownerID,
  883. localID,
  884. getInt(fileType),
  885. title,
  886. deviceFolder,
  887. ];
  888. }
  889. final rows = await db.query(
  890. filesTable,
  891. where: whereClause,
  892. whereArgs: whereArgs,
  893. );
  894. return convertToFiles(rows);
  895. }
  896. Future<List<EnteFile>> getUploadedFilesWithHashes(
  897. FileHashData hashData,
  898. FileType fileType,
  899. int ownerID,
  900. ) async {
  901. String inParam = "'${hashData.fileHash}'";
  902. if (fileType == FileType.livePhoto && hashData.zipHash != null) {
  903. inParam += ",'${hashData.zipHash}'";
  904. }
  905. final db = await instance.database;
  906. final rows = await db.query(
  907. filesTable,
  908. where: '($columnUploadedFileID != NULL OR $columnUploadedFileID != -1) '
  909. 'AND $columnOwnerID = ? AND $columnFileType ='
  910. ' ? '
  911. 'AND $columnHash IN ($inParam)',
  912. whereArgs: [
  913. ownerID,
  914. getInt(fileType),
  915. ],
  916. );
  917. return convertToFiles(rows);
  918. }
  919. Future<int> update(EnteFile file) async {
  920. final db = await instance.database;
  921. return await db.update(
  922. filesTable,
  923. _getRowForFile(file),
  924. where: '$columnGeneratedID = ?',
  925. whereArgs: [file.generatedID],
  926. );
  927. }
  928. Future<int> updateUploadedFileAcrossCollections(EnteFile file) async {
  929. final db = await instance.database;
  930. return await db.update(
  931. filesTable,
  932. _getRowForFileWithoutCollection(file),
  933. where: '$columnUploadedFileID = ?',
  934. whereArgs: [file.uploadedFileID],
  935. );
  936. }
  937. Future<int> updateLocalIDForUploaded(int uploadedID, String localID) async {
  938. final db = await instance.database;
  939. return await db.update(
  940. filesTable,
  941. {columnLocalID: localID},
  942. where: '$columnUploadedFileID = ? AND $columnLocalID IS NULL',
  943. whereArgs: [uploadedID],
  944. );
  945. }
  946. Future<int> delete(int uploadedFileID) async {
  947. final db = await instance.database;
  948. return db.delete(
  949. filesTable,
  950. where: '$columnUploadedFileID =?',
  951. whereArgs: [uploadedFileID],
  952. );
  953. }
  954. Future<int> deleteByGeneratedID(int genID) async {
  955. final db = await instance.database;
  956. return db.delete(
  957. filesTable,
  958. where: '$columnGeneratedID =?',
  959. whereArgs: [genID],
  960. );
  961. }
  962. Future<int> deleteMultipleUploadedFiles(List<int> uploadedFileIDs) async {
  963. final db = await instance.database;
  964. return await db.delete(
  965. filesTable,
  966. where: '$columnUploadedFileID IN (${uploadedFileIDs.join(', ')})',
  967. );
  968. }
  969. Future<int> deleteMultipleByGeneratedIDs(List<int> generatedIDs) async {
  970. if (generatedIDs.isEmpty) {
  971. return 0;
  972. }
  973. final db = await instance.database;
  974. return await db.delete(
  975. filesTable,
  976. where: '$columnGeneratedID IN (${generatedIDs.join(', ')})',
  977. );
  978. }
  979. Future<int> deleteLocalFile(EnteFile file) async {
  980. final db = await instance.database;
  981. if (file.localID != null) {
  982. // delete all files with same local ID
  983. return db.delete(
  984. filesTable,
  985. where: '$columnLocalID =?',
  986. whereArgs: [file.localID],
  987. );
  988. } else {
  989. return db.delete(
  990. filesTable,
  991. where: '$columnGeneratedID =?',
  992. whereArgs: [file.generatedID],
  993. );
  994. }
  995. }
  996. Future<void> deleteLocalFiles(List<String> localIDs) async {
  997. String inParam = "";
  998. for (final localID in localIDs) {
  999. inParam += "'" + localID + "',";
  1000. }
  1001. inParam = inParam.substring(0, inParam.length - 1);
  1002. final db = await instance.database;
  1003. await db.rawQuery(
  1004. '''
  1005. UPDATE $filesTable
  1006. SET $columnLocalID = NULL
  1007. WHERE $columnLocalID IN ($inParam);
  1008. ''',
  1009. );
  1010. }
  1011. Future<List<EnteFile>> getLocalFiles(List<String> localIDs) async {
  1012. String inParam = "";
  1013. for (final localID in localIDs) {
  1014. inParam += "'" + localID + "',";
  1015. }
  1016. inParam = inParam.substring(0, inParam.length - 1);
  1017. final db = await instance.database;
  1018. final results = await db.query(
  1019. filesTable,
  1020. where: '$columnLocalID IN ($inParam)',
  1021. );
  1022. return convertToFiles(results);
  1023. }
  1024. Future<int> deleteUnSyncedLocalFiles(List<String> localIDs) async {
  1025. String inParam = "";
  1026. for (final localID in localIDs) {
  1027. inParam += "'" + localID + "',";
  1028. }
  1029. inParam = inParam.substring(0, inParam.length - 1);
  1030. final db = await instance.database;
  1031. return db.delete(
  1032. filesTable,
  1033. where:
  1034. '($columnUploadedFileID is NULL OR $columnUploadedFileID = -1 ) AND $columnLocalID IN ($inParam)',
  1035. );
  1036. }
  1037. Future<int> deleteFromCollection(int uploadedFileID, int collectionID) async {
  1038. final db = await instance.database;
  1039. return db.delete(
  1040. filesTable,
  1041. where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
  1042. whereArgs: [uploadedFileID, collectionID],
  1043. );
  1044. }
  1045. Future<int> deleteFilesFromCollection(
  1046. int collectionID,
  1047. List<int> uploadedFileIDs,
  1048. ) async {
  1049. final db = await instance.database;
  1050. return db.delete(
  1051. filesTable,
  1052. where:
  1053. '$columnCollectionID = ? AND $columnUploadedFileID IN (${uploadedFileIDs.join(', ')})',
  1054. whereArgs: [collectionID],
  1055. );
  1056. }
  1057. Future<int> collectionFileCount(int collectionID) async {
  1058. final db = await instance.database;
  1059. final count = Sqflite.firstIntValue(
  1060. await db.rawQuery(
  1061. 'SELECT COUNT(*) FROM $filesTable where $columnCollectionID = '
  1062. '$collectionID AND $columnUploadedFileID IS NOT -1',
  1063. ),
  1064. );
  1065. return count ?? 0;
  1066. }
  1067. Future<int> archivedFilesCount(
  1068. int visibility,
  1069. int ownerID,
  1070. Set<int> hiddenCollections,
  1071. ) async {
  1072. final db = await instance.database;
  1073. final count = Sqflite.firstIntValue(
  1074. await db.rawQuery(
  1075. 'SELECT COUNT(distinct($columnUploadedFileID)) FROM $filesTable where '
  1076. '$columnMMdVisibility'
  1077. ' = $visibility AND $columnOwnerID = $ownerID AND $columnCollectionID NOT IN (${hiddenCollections.join(', ')})',
  1078. ),
  1079. );
  1080. return count ?? 0;
  1081. }
  1082. Future<int> deleteCollection(int collectionID) async {
  1083. final db = await instance.database;
  1084. return db.delete(
  1085. filesTable,
  1086. where: '$columnCollectionID = ?',
  1087. whereArgs: [collectionID],
  1088. );
  1089. }
  1090. Future<int> removeFromCollection(int collectionID, List<int> fileIDs) async {
  1091. final db = await instance.database;
  1092. return db.delete(
  1093. filesTable,
  1094. where:
  1095. '$columnCollectionID =? AND $columnUploadedFileID IN (${fileIDs.join(', ')})',
  1096. whereArgs: [collectionID],
  1097. );
  1098. }
  1099. Future<List<EnteFile>> getPendingUploadForCollection(int collectionID) async {
  1100. final db = await instance.database;
  1101. final results = await db.query(
  1102. filesTable,
  1103. where: '$columnCollectionID = ? AND ($columnUploadedFileID IS NULL OR '
  1104. '$columnUploadedFileID = -1)',
  1105. whereArgs: [collectionID],
  1106. );
  1107. return convertToFiles(results);
  1108. }
  1109. Future<Set<String>> getLocalIDsPresentInEntries(
  1110. List<EnteFile> existingFiles,
  1111. int collectionID,
  1112. ) async {
  1113. String inParam = "";
  1114. for (final existingFile in existingFiles) {
  1115. if (existingFile.localID != null) {
  1116. inParam += "'" + existingFile.localID! + "',";
  1117. }
  1118. }
  1119. inParam = inParam.substring(0, inParam.length - 1);
  1120. final db = await instance.database;
  1121. final rows = await db.rawQuery(
  1122. '''
  1123. SELECT $columnLocalID
  1124. FROM $filesTable
  1125. WHERE $columnLocalID IN ($inParam) AND $columnCollectionID !=
  1126. $collectionID AND $columnLocalID IS NOT NULL;
  1127. ''',
  1128. );
  1129. final result = <String>{};
  1130. for (final row in rows) {
  1131. result.add(row[columnLocalID] as String);
  1132. }
  1133. return result;
  1134. }
  1135. // getCollectionLatestFileTime returns map of collectionID to the max
  1136. // creationTime of the files in the collection.
  1137. Future<Map<int, int>> getCollectionIDToMaxCreationTime() async {
  1138. final enteWatch = EnteWatch("getCollectionIDToMaxCreationTime")..start();
  1139. final db = await instance.database;
  1140. final rows = await db.rawQuery(
  1141. '''
  1142. SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time
  1143. FROM $filesTable
  1144. WHERE
  1145. ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1
  1146. AND $columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS
  1147. NOT -1)
  1148. GROUP BY $columnCollectionID;
  1149. ''',
  1150. );
  1151. final result = <int, int>{};
  1152. for (final row in rows) {
  1153. result[row[columnCollectionID] as int] = row['max_creation_time'] as int;
  1154. }
  1155. enteWatch.log("query done");
  1156. return result;
  1157. }
  1158. // getCollectionFileFirstOrLast returns the first or last uploaded file in
  1159. // the collection based on the given collectionID and the order.
  1160. Future<EnteFile?> getCollectionFileFirstOrLast(
  1161. int collectionID,
  1162. bool sortAsc,
  1163. ) async {
  1164. final db = await instance.database;
  1165. final order = sortAsc ? 'ASC' : 'DESC';
  1166. final rows = await db.query(
  1167. filesTable,
  1168. where: '$columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL '
  1169. 'AND $columnUploadedFileID IS NOT -1)',
  1170. whereArgs: [collectionID],
  1171. orderBy:
  1172. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  1173. limit: 1,
  1174. );
  1175. if (rows.isEmpty) {
  1176. return null;
  1177. }
  1178. return convertToFiles(rows).first;
  1179. }
  1180. Future<void> markForReUploadIfLocationMissing(List<String> localIDs) async {
  1181. if (localIDs.isEmpty) {
  1182. return;
  1183. }
  1184. String inParam = "";
  1185. for (final localID in localIDs) {
  1186. inParam += "'" + localID + "',";
  1187. }
  1188. inParam = inParam.substring(0, inParam.length - 1);
  1189. final db = await instance.database;
  1190. await db.rawUpdate(
  1191. '''
  1192. UPDATE $filesTable
  1193. SET $columnUpdationTime = NULL
  1194. WHERE $columnLocalID IN ($inParam)
  1195. AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0);
  1196. ''',
  1197. );
  1198. }
  1199. Future<bool> doesFileExistInCollection(
  1200. int uploadedFileID,
  1201. int collectionID,
  1202. ) async {
  1203. final db = await instance.database;
  1204. final rows = await db.query(
  1205. filesTable,
  1206. where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
  1207. whereArgs: [uploadedFileID, collectionID],
  1208. limit: 1,
  1209. );
  1210. return rows.isNotEmpty;
  1211. }
  1212. Future<Map<int, EnteFile>> getFilesFromIDs(List<int> ids) async {
  1213. final result = <int, EnteFile>{};
  1214. if (ids.isEmpty) {
  1215. return result;
  1216. }
  1217. String inParam = "";
  1218. for (final id in ids) {
  1219. inParam += "'" + id.toString() + "',";
  1220. }
  1221. inParam = inParam.substring(0, inParam.length - 1);
  1222. final db = await instance.database;
  1223. final results = await db.query(
  1224. filesTable,
  1225. where: '$columnUploadedFileID IN ($inParam)',
  1226. );
  1227. final files = convertToFiles(results);
  1228. for (final file in files) {
  1229. result[file.uploadedFileID!] = file;
  1230. }
  1231. return result;
  1232. }
  1233. Future<Map<int, EnteFile>> getFilesFromGeneratedIDs(List<int> ids) async {
  1234. final result = <int, EnteFile>{};
  1235. if (ids.isEmpty) {
  1236. return result;
  1237. }
  1238. String inParam = "";
  1239. for (final id in ids) {
  1240. inParam += "'" + id.toString() + "',";
  1241. }
  1242. inParam = inParam.substring(0, inParam.length - 1);
  1243. final db = await instance.database;
  1244. final results = await db.query(
  1245. filesTable,
  1246. where: '$columnGeneratedID IN ($inParam)',
  1247. );
  1248. final files = convertToFiles(results);
  1249. for (final file in files) {
  1250. result[file.generatedID as int] = file;
  1251. }
  1252. return result;
  1253. }
  1254. Future<Map<int, List<EnteFile>>> getAllFilesGroupByCollectionID(
  1255. List<int> ids,
  1256. ) async {
  1257. final result = <int, List<EnteFile>>{};
  1258. if (ids.isEmpty) {
  1259. return result;
  1260. }
  1261. String inParam = "";
  1262. for (final id in ids) {
  1263. inParam += "'" + id.toString() + "',";
  1264. }
  1265. inParam = inParam.substring(0, inParam.length - 1);
  1266. final db = await instance.database;
  1267. final results = await db.query(
  1268. filesTable,
  1269. where: '$columnUploadedFileID IN ($inParam)',
  1270. );
  1271. final files = convertToFiles(results);
  1272. for (EnteFile eachFile in files) {
  1273. if (!result.containsKey(eachFile.collectionID)) {
  1274. result[eachFile.collectionID as int] = <EnteFile>[];
  1275. }
  1276. result[eachFile.collectionID]!.add(eachFile);
  1277. }
  1278. return result;
  1279. }
  1280. Future<Set<int>> getAllCollectionIDsOfFile(
  1281. int uploadedFileID,
  1282. ) async {
  1283. final db = await instance.database;
  1284. final results = await db.query(
  1285. filesTable,
  1286. where: '$columnUploadedFileID = ? AND $columnCollectionID != -1',
  1287. columns: [columnCollectionID],
  1288. whereArgs: [uploadedFileID],
  1289. distinct: true,
  1290. );
  1291. final collectionIDsOfFile = <int>{};
  1292. for (var result in results) {
  1293. collectionIDsOfFile.add(result['collection_id'] as int);
  1294. }
  1295. return collectionIDsOfFile;
  1296. }
  1297. List<EnteFile> convertToFiles(List<Map<String, dynamic>> results) {
  1298. final List<EnteFile> files = [];
  1299. for (final result in results) {
  1300. files.add(_getFileFromRow(result));
  1301. }
  1302. return files;
  1303. }
  1304. Future<List<String>> getGeneratedIDForFilesOlderThan(
  1305. int cutOffTime,
  1306. int ownerID,
  1307. ) async {
  1308. final db = await instance.database;
  1309. final rows = await db.query(
  1310. filesTable,
  1311. columns: [columnGeneratedID],
  1312. distinct: true,
  1313. where:
  1314. '$columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?)',
  1315. whereArgs: [cutOffTime, ownerID],
  1316. );
  1317. final result = <String>[];
  1318. for (final row in rows) {
  1319. result.add(row[columnGeneratedID].toString());
  1320. }
  1321. return result;
  1322. }
  1323. // For givenUserID, get List of unique LocalIDs for files which are
  1324. // uploaded by the given user and location is missing
  1325. Future<List<String>> getLocalIDsForFilesWithoutLocation(int ownerID) async {
  1326. final db = await instance.database;
  1327. final rows = await db.query(
  1328. filesTable,
  1329. columns: [columnLocalID],
  1330. distinct: true,
  1331. where: '$columnOwnerID = ? AND $columnLocalID IS NOT NULL AND '
  1332. '($columnLatitude IS NULL OR '
  1333. '$columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0)',
  1334. whereArgs: [ownerID],
  1335. );
  1336. final result = <String>[];
  1337. for (final row in rows) {
  1338. result.add(row[columnLocalID].toString());
  1339. }
  1340. return result;
  1341. }
  1342. // For a given userID, return unique uploadedFileId for the given userID
  1343. Future<List<int>> getUploadIDsWithMissingSize(int userId) async {
  1344. final db = await instance.database;
  1345. final rows = await db.query(
  1346. filesTable,
  1347. columns: [columnUploadedFileID],
  1348. distinct: true,
  1349. where: '$columnOwnerID = ? AND $columnFileSize IS NULL',
  1350. whereArgs: [userId],
  1351. );
  1352. final result = <int>[];
  1353. for (final row in rows) {
  1354. result.add(row[columnUploadedFileID] as int);
  1355. }
  1356. return result;
  1357. }
  1358. // For a given userID, return unique uploadedFileId for the given userID
  1359. Future<List<String>> getLivePhotosWithBadSize(
  1360. int userId,
  1361. int sizeInBytes,
  1362. ) async {
  1363. final db = await instance.database;
  1364. final rows = await db.query(
  1365. filesTable,
  1366. columns: [columnLocalID],
  1367. distinct: true,
  1368. where: '$columnOwnerID = ? AND '
  1369. '($columnFileSize IS NULL OR $columnFileSize = ?) AND '
  1370. '$columnFileType = ? AND $columnLocalID IS NOT NULL',
  1371. whereArgs: [userId, sizeInBytes, getInt(FileType.livePhoto)],
  1372. );
  1373. final result = <String>[];
  1374. for (final row in rows) {
  1375. result.add(row[columnLocalID] as String);
  1376. }
  1377. return result;
  1378. }
  1379. // updateSizeForUploadIDs takes a map of upploadedFileID and fileSize and
  1380. // update the fileSize for the given uploadedFileID
  1381. Future<void> updateSizeForUploadIDs(
  1382. Map<int, int> uploadedFileIDToSize,
  1383. ) async {
  1384. if (uploadedFileIDToSize.isEmpty) {
  1385. return;
  1386. }
  1387. final db = await instance.database;
  1388. final batch = db.batch();
  1389. for (final uploadedFileID in uploadedFileIDToSize.keys) {
  1390. batch.update(
  1391. filesTable,
  1392. {columnFileSize: uploadedFileIDToSize[uploadedFileID]},
  1393. where: '$columnUploadedFileID = ?',
  1394. whereArgs: [uploadedFileID],
  1395. );
  1396. }
  1397. await batch.commit(noResult: true);
  1398. }
  1399. Future<List<EnteFile>> getAllFilesFromDB(Set<int> collectionsToIgnore) async {
  1400. final db = await instance.database;
  1401. final List<Map<String, dynamic>> result =
  1402. await db.query(filesTable, orderBy: '$columnCreationTime DESC');
  1403. final List<EnteFile> files = convertToFiles(result);
  1404. final List<EnteFile> deduplicatedFiles = await applyDBFilters(
  1405. files,
  1406. DBFilterOptions(ignoredCollectionIDs: collectionsToIgnore),
  1407. );
  1408. return deduplicatedFiles;
  1409. }
  1410. Future<Map<FileType, int>> fetchFilesCountbyType(int userID) async {
  1411. final db = await instance.database;
  1412. final result = await db.rawQuery(
  1413. "SELECT $columnFileType, COUNT(DISTINCT $columnUploadedFileID) FROM $filesTable WHERE $columnUploadedFileID != -1 AND $columnOwnerID == $userID GROUP BY $columnFileType",
  1414. );
  1415. final filesCount = <FileType, int>{};
  1416. for (var e in result) {
  1417. filesCount.addAll(
  1418. {getFileType(e[columnFileType] as int): e.values.last as int},
  1419. );
  1420. }
  1421. return filesCount;
  1422. }
  1423. Future<FileLoadResult> fetchAllUploadedAndSharedFilesWithLocation(
  1424. int startTime,
  1425. int endTime, {
  1426. int? limit,
  1427. bool? asc,
  1428. required DBFilterOptions? filterOptions,
  1429. }) async {
  1430. final db = await instance.database;
  1431. final order = (asc ?? false ? 'ASC' : 'DESC');
  1432. final results = await db.query(
  1433. filesTable,
  1434. where:
  1435. '$columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0)'
  1436. ' AND $columnCreationTime >= ? AND $columnCreationTime <= ?'
  1437. ' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
  1438. whereArgs: [startTime, endTime],
  1439. orderBy:
  1440. '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
  1441. limit: limit,
  1442. );
  1443. final files = convertToFiles(results);
  1444. final List<EnteFile> filteredFiles =
  1445. await applyDBFilters(files, filterOptions);
  1446. return FileLoadResult(filteredFiles, files.length == limit);
  1447. }
  1448. Map<String, dynamic> _getRowForFile(EnteFile file) {
  1449. final row = <String, dynamic>{};
  1450. if (file.generatedID != null) {
  1451. row[columnGeneratedID] = file.generatedID;
  1452. }
  1453. row[columnLocalID] = file.localID;
  1454. row[columnUploadedFileID] = file.uploadedFileID ?? -1;
  1455. row[columnOwnerID] = file.ownerID;
  1456. row[columnCollectionID] = file.collectionID ?? -1;
  1457. row[columnTitle] = file.title;
  1458. row[columnDeviceFolder] = file.deviceFolder;
  1459. // if (file.location == null ||
  1460. // (file.location!.latitude == null && file.location!.longitude == null)) {
  1461. // file.location = Location.randomLocation();
  1462. // }
  1463. if (file.location != null) {
  1464. row[columnLatitude] = file.location!.latitude;
  1465. row[columnLongitude] = file.location!.longitude;
  1466. }
  1467. row[columnFileType] = getInt(file.fileType);
  1468. row[columnCreationTime] = file.creationTime;
  1469. row[columnModificationTime] = file.modificationTime;
  1470. row[columnUpdationTime] = file.updationTime;
  1471. row[columnAddedTime] =
  1472. file.addedTime ?? DateTime.now().microsecondsSinceEpoch;
  1473. row[columnEncryptedKey] = file.encryptedKey;
  1474. row[columnKeyDecryptionNonce] = file.keyDecryptionNonce;
  1475. row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
  1476. row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
  1477. row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
  1478. row[columnFileSubType] = file.fileSubType ?? -1;
  1479. row[columnDuration] = file.duration ?? 0;
  1480. row[columnExif] = file.exif;
  1481. row[columnHash] = file.hash;
  1482. row[columnMetadataVersion] = file.metadataVersion;
  1483. row[columnFileSize] = file.fileSize;
  1484. row[columnMMdVersion] = file.mMdVersion;
  1485. row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}';
  1486. row[columnMMdVisibility] = file.magicMetadata.visibility;
  1487. row[columnPubMMdVersion] = file.pubMmdVersion;
  1488. row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}';
  1489. // override existing fields to avoid re-writing all queries and logic
  1490. if (file.pubMagicMetadata != null) {
  1491. if (file.pubMagicMetadata!.editedTime != null) {
  1492. row[columnCreationTime] = file.pubMagicMetadata!.editedTime;
  1493. }
  1494. if (file.pubMagicMetadata!.lat != null &&
  1495. file.pubMagicMetadata!.long != null) {
  1496. row[columnLatitude] = file.pubMagicMetadata!.lat;
  1497. row[columnLongitude] = file.pubMagicMetadata!.long;
  1498. }
  1499. }
  1500. return row;
  1501. }
  1502. Map<String, dynamic> _getRowForFileWithoutCollection(EnteFile file) {
  1503. final row = <String, dynamic>{};
  1504. row[columnLocalID] = file.localID;
  1505. row[columnUploadedFileID] = file.uploadedFileID ?? -1;
  1506. row[columnOwnerID] = file.ownerID;
  1507. row[columnTitle] = file.title;
  1508. row[columnDeviceFolder] = file.deviceFolder;
  1509. if (file.location != null) {
  1510. row[columnLatitude] = file.location!.latitude;
  1511. row[columnLongitude] = file.location!.longitude;
  1512. }
  1513. row[columnFileType] = getInt(file.fileType);
  1514. row[columnCreationTime] = file.creationTime;
  1515. row[columnModificationTime] = file.modificationTime;
  1516. row[columnUpdationTime] = file.updationTime;
  1517. row[columnAddedTime] =
  1518. file.addedTime ?? DateTime.now().microsecondsSinceEpoch;
  1519. row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
  1520. row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
  1521. row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
  1522. row[columnFileSubType] = file.fileSubType ?? -1;
  1523. row[columnDuration] = file.duration ?? 0;
  1524. row[columnExif] = file.exif;
  1525. row[columnHash] = file.hash;
  1526. row[columnMetadataVersion] = file.metadataVersion;
  1527. row[columnMMdVersion] = file.mMdVersion;
  1528. row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}';
  1529. row[columnMMdVisibility] = file.magicMetadata.visibility;
  1530. row[columnPubMMdVersion] = file.pubMmdVersion;
  1531. row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}';
  1532. if (file.pubMagicMetadata != null &&
  1533. file.pubMagicMetadata!.editedTime != null) {
  1534. // override existing creationTime to avoid re-writing all queries related
  1535. // to loading the gallery
  1536. row[columnCreationTime] = file.pubMagicMetadata!.editedTime!;
  1537. }
  1538. return row;
  1539. }
  1540. EnteFile _getFileFromRow(Map<String, dynamic> row) {
  1541. final file = EnteFile();
  1542. file.generatedID = row[columnGeneratedID];
  1543. file.localID = row[columnLocalID];
  1544. file.uploadedFileID =
  1545. row[columnUploadedFileID] == -1 ? null : row[columnUploadedFileID];
  1546. file.ownerID = row[columnOwnerID];
  1547. file.collectionID =
  1548. row[columnCollectionID] == -1 ? null : row[columnCollectionID];
  1549. file.title = row[columnTitle];
  1550. file.deviceFolder = row[columnDeviceFolder];
  1551. if (row[columnLatitude] != null && row[columnLongitude] != null) {
  1552. file.location = Location(
  1553. latitude: row[columnLatitude],
  1554. longitude: row[columnLongitude],
  1555. );
  1556. }
  1557. file.fileType = getFileType(row[columnFileType]);
  1558. file.creationTime = row[columnCreationTime];
  1559. file.modificationTime = row[columnModificationTime];
  1560. file.updationTime = row[columnUpdationTime] ?? -1;
  1561. file.addedTime = row[columnAddedTime];
  1562. file.encryptedKey = row[columnEncryptedKey];
  1563. file.keyDecryptionNonce = row[columnKeyDecryptionNonce];
  1564. file.fileDecryptionHeader = row[columnFileDecryptionHeader];
  1565. file.thumbnailDecryptionHeader = row[columnThumbnailDecryptionHeader];
  1566. file.metadataDecryptionHeader = row[columnMetadataDecryptionHeader];
  1567. file.fileSubType = row[columnFileSubType] ?? -1;
  1568. file.duration = row[columnDuration] ?? 0;
  1569. file.exif = row[columnExif];
  1570. file.hash = row[columnHash];
  1571. file.metadataVersion = row[columnMetadataVersion] ?? 0;
  1572. file.fileSize = row[columnFileSize];
  1573. file.mMdVersion = row[columnMMdVersion] ?? 0;
  1574. file.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}';
  1575. file.pubMmdVersion = row[columnPubMMdVersion] ?? 0;
  1576. file.pubMmdEncodedJson = row[columnPubMMdEncodedJson] ?? '{}';
  1577. return file;
  1578. }
  1579. }