files_db.dart 56 KB

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