import "dart:async"; import "dart:io"; import "package:computer/computer.dart"; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import "package:photos/extensions/stop_watch.dart"; import 'package:photos/models/backup_status.dart'; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; import 'package:photos/models/file_load_result.dart'; import 'package:photos/models/location/location.dart'; import "package:photos/models/metadata/common_keys.dart"; import "package:photos/services/filter/db_filters.dart"; import 'package:photos/utils/file_uploader_util.dart'; import "package:photos/utils/primitive_wrapper.dart"; import "package:photos/utils/sqlite_util.dart"; import 'package:sqlite_async/sqlite_async.dart'; class FilesDB { /* Note: columnUploadedFileID and columnCollectionID have to be compared against both NULL and -1 because older clients might have entries where the DEFAULT was unset, and a migration script to set the DEFAULT would break in case of duplicate entries for un-uploaded files that were created due to a collision in background and foreground syncs. */ static const _databaseName = "ente.files.db"; static final Logger _logger = Logger("FilesDB"); static const filesTable = 'files'; static const tempTable = 'temp_files'; static const columnGeneratedID = '_id'; static const columnUploadedFileID = 'uploaded_file_id'; static const columnOwnerID = 'owner_id'; static const columnCollectionID = 'collection_id'; static const columnLocalID = 'local_id'; static const columnTitle = 'title'; static const columnDeviceFolder = 'device_folder'; static const columnLatitude = 'latitude'; static const columnLongitude = 'longitude'; static const columnFileType = 'file_type'; static const columnFileSubType = 'file_sub_type'; static const columnDuration = 'duration'; static const columnExif = 'exif'; static const columnHash = 'hash'; static const columnMetadataVersion = 'metadata_version'; static const columnIsDeleted = 'is_deleted'; static const columnCreationTime = 'creation_time'; static const columnModificationTime = 'modification_time'; static const columnUpdationTime = 'updation_time'; static const columnAddedTime = 'added_time'; static const columnEncryptedKey = 'encrypted_key'; static const columnKeyDecryptionNonce = 'key_decryption_nonce'; static const columnFileDecryptionHeader = 'file_decryption_header'; static const columnThumbnailDecryptionHeader = 'thumbnail_decryption_header'; static const columnMetadataDecryptionHeader = 'metadata_decryption_header'; static const columnFileSize = 'file_size'; // MMD -> Magic Metadata static const columnMMdEncodedJson = 'mmd_encoded_json'; static const columnMMdVersion = 'mmd_ver'; static const columnPubMMdEncodedJson = 'pub_mmd_encoded_json'; static const columnPubMMdVersion = 'pub_mmd_ver'; // part of magic metadata // Only parse & store selected fields from JSON in separate columns if // we need to write query based on that field static const columnMMdVisibility = 'mmd_visibility'; //If adding or removing a new column, make sure to update the `_columnNames` list //and update `_generateColumnsAndPlaceholdersForInsert` and //`_generateUpdateAssignmentsWithPlaceholders` static final migrationScripts = [ ...createTable(filesTable), ...alterDeviceFolderToAllowNULL(), ...alterTimestampColumnTypes(), ...addIndices(), ...addMetadataColumns(), ...addMagicMetadataColumns(), ...addUniqueConstraintOnCollectionFiles(), ...addPubMagicMetadataColumns(), ...createOnDeviceFilesAndPathCollection(), ...addFileSizeColumn(), ...updateIndexes(), ...createEntityDataTable(), ...addAddedTime(), ]; static const List _columnNames = [ columnGeneratedID, columnLocalID, columnUploadedFileID, columnOwnerID, columnCollectionID, columnTitle, columnDeviceFolder, columnLatitude, columnLongitude, columnFileType, columnModificationTime, columnEncryptedKey, columnKeyDecryptionNonce, columnFileDecryptionHeader, columnThumbnailDecryptionHeader, columnMetadataDecryptionHeader, columnCreationTime, columnUpdationTime, columnFileSubType, columnDuration, columnExif, columnHash, columnMetadataVersion, columnMMdEncodedJson, columnMMdVersion, columnMMdVisibility, columnPubMMdEncodedJson, columnPubMMdVersion, columnFileSize, columnAddedTime, ]; // make this a singleton class FilesDB._privateConstructor(); static final FilesDB instance = FilesDB._privateConstructor(); // only have a single app-wide reference to the database static Future? _sqliteAsyncDBFuture; Future get sqliteAsyncDB async { // lazily instantiate the db the first time it is accessed _sqliteAsyncDBFuture ??= _initSqliteAsyncDatabase(); return _sqliteAsyncDBFuture!; } // this opens the database (and creates it if it doesn't exist) Future _initSqliteAsyncDatabase() async { final Directory documentsDirectory = await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); _logger.info("DB path " + path); final database = SqliteDatabase(path: path); await _migrate(database); return database; } Future _migrate( SqliteDatabase database, ) async { final result = await database.execute('PRAGMA user_version'); final currentVersion = result[0]['user_version'] as int; final toVersion = migrationScripts.length; if (currentVersion < toVersion) { _logger.info("Migrating database from $currentVersion to $toVersion"); await database.writeTransaction((tx) async { for (int i = currentVersion + 1; i <= toVersion; i++) { await tx.execute(migrationScripts[i - 1]); } await tx.execute('PRAGMA user_version = $toVersion'); }); } else if (currentVersion > toVersion) { throw AssertionError( "currentVersion($currentVersion) cannot be greater than toVersion($toVersion)", ); } } // SQL code to create the database table static List createTable(String tableName) { return [ ''' CREATE TABLE $tableName ( $columnGeneratedID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, $columnLocalID TEXT, $columnUploadedFileID INTEGER DEFAULT -1, $columnOwnerID INTEGER, $columnCollectionID INTEGER DEFAULT -1, $columnTitle TEXT NOT NULL, $columnDeviceFolder TEXT, $columnLatitude REAL, $columnLongitude REAL, $columnFileType INTEGER, $columnModificationTime TEXT NOT NULL, $columnEncryptedKey TEXT, $columnKeyDecryptionNonce TEXT, $columnFileDecryptionHeader TEXT, $columnThumbnailDecryptionHeader TEXT, $columnMetadataDecryptionHeader TEXT, $columnIsDeleted INTEGER DEFAULT 0, $columnCreationTime TEXT NOT NULL, $columnUpdationTime TEXT, UNIQUE($columnLocalID, $columnUploadedFileID, $columnCollectionID) ); ''', ]; } static List addIndices() { return [ ''' CREATE INDEX IF NOT EXISTS collection_id_index ON $filesTable($columnCollectionID); ''', ''' CREATE INDEX IF NOT EXISTS device_folder_index ON $filesTable($columnDeviceFolder); ''', ''' CREATE INDEX IF NOT EXISTS creation_time_index ON $filesTable($columnCreationTime); ''', ''' CREATE INDEX IF NOT EXISTS updation_time_index ON $filesTable($columnUpdationTime); ''' ]; } static List alterDeviceFolderToAllowNULL() { return [ ...createTable(tempTable), ''' INSERT INTO $tempTable SELECT * FROM $filesTable; DROP TABLE $filesTable; ALTER TABLE $tempTable RENAME TO $filesTable; ''' ]; } static List alterTimestampColumnTypes() { return [ ''' DROP TABLE IF EXISTS $tempTable; ''', ''' CREATE TABLE $tempTable ( $columnGeneratedID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, $columnLocalID TEXT, $columnUploadedFileID INTEGER DEFAULT -1, $columnOwnerID INTEGER, $columnCollectionID INTEGER DEFAULT -1, $columnTitle TEXT NOT NULL, $columnDeviceFolder TEXT, $columnLatitude REAL, $columnLongitude REAL, $columnFileType INTEGER, $columnModificationTime INTEGER NOT NULL, $columnEncryptedKey TEXT, $columnKeyDecryptionNonce TEXT, $columnFileDecryptionHeader TEXT, $columnThumbnailDecryptionHeader TEXT, $columnMetadataDecryptionHeader TEXT, $columnCreationTime INTEGER NOT NULL, $columnUpdationTime INTEGER, UNIQUE($columnLocalID, $columnUploadedFileID, $columnCollectionID) ); ''', ''' INSERT INTO $tempTable SELECT $columnGeneratedID, $columnLocalID, $columnUploadedFileID, $columnOwnerID, $columnCollectionID, $columnTitle, $columnDeviceFolder, $columnLatitude, $columnLongitude, $columnFileType, CAST($columnModificationTime AS INTEGER), $columnEncryptedKey, $columnKeyDecryptionNonce, $columnFileDecryptionHeader, $columnThumbnailDecryptionHeader, $columnMetadataDecryptionHeader, CAST($columnCreationTime AS INTEGER), CAST($columnUpdationTime AS INTEGER) FROM $filesTable; ''', ''' DROP TABLE $filesTable; ''', ''' ALTER TABLE $tempTable RENAME TO $filesTable; ''', ]; } static List addMetadataColumns() { return [ ''' ALTER TABLE $filesTable ADD COLUMN $columnFileSubType INTEGER; ''', ''' ALTER TABLE $filesTable ADD COLUMN $columnDuration INTEGER; ''', ''' ALTER TABLE $filesTable ADD COLUMN $columnExif TEXT; ''', ''' ALTER TABLE $filesTable ADD COLUMN $columnHash TEXT; ''', ''' ALTER TABLE $filesTable ADD COLUMN $columnMetadataVersion INTEGER; ''', ]; } static List addMagicMetadataColumns() { return [ ''' ALTER TABLE $filesTable ADD COLUMN $columnMMdEncodedJson TEXT DEFAULT '{}'; ''', ''' ALTER TABLE $filesTable ADD COLUMN $columnMMdVersion INTEGER DEFAULT 0; ''', ''' ALTER TABLE $filesTable ADD COLUMN $columnMMdVisibility INTEGER DEFAULT $visibleVisibility; ''' ]; } static List addUniqueConstraintOnCollectionFiles() { return [ ''' DELETE from $filesTable where $columnCollectionID || '-' || $columnUploadedFileID IN (SELECT $columnCollectionID || '-' || $columnUploadedFileID from $filesTable WHERE $columnCollectionID is not NULL AND $columnUploadedFileID is NOT NULL AND $columnCollectionID != -1 AND $columnUploadedFileID != -1 GROUP BY ($columnCollectionID || '-' || $columnUploadedFileID) HAVING count(*) > 1) AND ($columnCollectionID || '-' || $columnUploadedFileID || '-' || $columnGeneratedID) NOT IN (SELECT $columnCollectionID || '-' || $columnUploadedFileID || '-' || max($columnGeneratedID) from $filesTable WHERE $columnCollectionID is not NULL AND $columnUploadedFileID is NOT NULL AND $columnCollectionID != -1 AND $columnUploadedFileID != -1 GROUP BY ($columnCollectionID || '-' || $columnUploadedFileID) HAVING count(*) > 1); ''', ''' CREATE UNIQUE INDEX IF NOT EXISTS cid_uid ON $filesTable ($columnCollectionID, $columnUploadedFileID) WHERE $columnCollectionID is not NULL AND $columnUploadedFileID is not NULL AND $columnCollectionID != -1 AND $columnUploadedFileID != -1; ''' ]; } static List addPubMagicMetadataColumns() { return [ ''' ALTER TABLE $filesTable ADD COLUMN $columnPubMMdEncodedJson TEXT DEFAULT '{}'; ''', ''' ALTER TABLE $filesTable ADD COLUMN $columnPubMMdVersion INTEGER DEFAULT 0; ''' ]; } static List createOnDeviceFilesAndPathCollection() { return [ ''' CREATE TABLE IF NOT EXISTS device_files ( id TEXT NOT NULL, path_id TEXT NOT NULL, UNIQUE(id, path_id) ); ''', ''' CREATE TABLE IF NOT EXISTS device_collections ( id TEXT PRIMARY KEY NOT NULL, name TEXT, modified_at INTEGER NOT NULL DEFAULT 0, should_backup INTEGER NOT NULL DEFAULT 0, count INTEGER NOT NULL DEFAULT 0, collection_id INTEGER DEFAULT -1, upload_strategy INTEGER DEFAULT 0, cover_id TEXT ); ''', ''' CREATE INDEX IF NOT EXISTS df_id_idx ON device_files (id); ''', ''' CREATE INDEX IF NOT EXISTS df_path_id_idx ON device_files (path_id); ''', ]; } static List createEntityDataTable() { return [ ''' CREATE TABLE IF NOT EXISTS entities ( id TEXT PRIMARY KEY NOT NULL, type TEXT NOT NULL, ownerID INTEGER NOT NULL, data TEXT NOT NULL DEFAULT '{}', updatedAt INTEGER NOT NULL ); ''' ]; } static List addFileSizeColumn() { return [ ''' ALTER TABLE $filesTable ADD COLUMN $columnFileSize INTEGER; ''', ]; } static List updateIndexes() { return [ ''' DROP INDEX IF EXISTS device_folder_index; ''', ''' CREATE INDEX IF NOT EXISTS file_hash_index ON $filesTable($columnHash); ''', ]; } static List addAddedTime() { return [ ''' ALTER TABLE $filesTable ADD COLUMN $columnAddedTime INTEGER NOT NULL DEFAULT -1; ''', ''' CREATE INDEX IF NOT EXISTS added_time_index ON $filesTable($columnAddedTime); ''' ]; } Future clearTable() async { final db = await instance.sqliteAsyncDB; await db.execute('DELETE FROM $filesTable'); await db.execute('DELETE FROM device_files'); await db.execute('DELETE FROM device_collections'); await db.execute('DELETE FROM entities'); } Future deleteDB() async { if (kDebugMode) { debugPrint("Deleting files db"); final Directory documentsDirectory = await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); File(path).deleteSync(recursive: true); _sqliteAsyncDBFuture = null; } } Future insertMultiple( List files, { SqliteAsyncConflictAlgorithm conflictAlgorithm = SqliteAsyncConflictAlgorithm.replace, }) async { if (files.isEmpty) return; final startTime = DateTime.now(); final db = await sqliteAsyncDB; ///Strong batch counter in an object so that it gets passed by reference ///Primitives are passed by value final genIdNotNullbatchCounter = PrimitiveWrapper(0); final genIdNullbatchCounter = PrimitiveWrapper(0); final genIdNullParameterSets = >[]; final genIdNotNullParameterSets = >[]; final genIdNullcolumnNames = _columnNames.where((element) => element != columnGeneratedID); for (EnteFile file in files) { final fileGenIdIsNull = file.generatedID == null; if (!fileGenIdIsNull) { await _batchAndInsertFile( file, conflictAlgorithm, db, genIdNotNullParameterSets, genIdNotNullbatchCounter, isGenIdNull: fileGenIdIsNull, ); } else { await _batchAndInsertFile( file, conflictAlgorithm, db, genIdNullParameterSets, genIdNullbatchCounter, isGenIdNull: fileGenIdIsNull, ); } } if (genIdNotNullbatchCounter.value > 0) { await _insertBatch( conflictAlgorithm, _columnNames, db, genIdNotNullParameterSets, ); genIdNotNullbatchCounter.value = 0; genIdNotNullParameterSets.clear(); } if (genIdNullbatchCounter.value > 0) { await _insertBatch( conflictAlgorithm, genIdNullcolumnNames, db, genIdNullParameterSets, ); genIdNullbatchCounter.value = 0; genIdNullParameterSets.clear(); } final endTime = DateTime.now(); final duration = Duration( microseconds: endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch, ); _logger.info( "Batch insert of " + files.length.toString() + " took " + duration.inMilliseconds.toString() + "ms.", ); } Future insert(EnteFile file) async { _logger.info("Inserting $file"); final db = await instance.sqliteAsyncDB; final columnsAndPlaceholders = _generateColumnsAndPlaceholdersForInsert(fileGenId: file.generatedID); final values = _getParameterSetForFile(file); await db.execute( 'INSERT OR REPLACE INTO $filesTable (${columnsAndPlaceholders["columns"]}) VALUES (${columnsAndPlaceholders["placeholders"]})', values, ); } Future insertAndGetId(EnteFile file) async { _logger.info("Inserting $file"); final db = await instance.sqliteAsyncDB; final columnsAndPlaceholders = _generateColumnsAndPlaceholdersForInsert(fileGenId: file.generatedID); final values = _getParameterSetForFile(file); return await db.writeTransaction((tx) async { await tx.execute( 'INSERT OR REPLACE INTO $filesTable (${columnsAndPlaceholders["columns"]}) VALUES (${columnsAndPlaceholders["placeholders"]})', values, ); final result = await tx.get('SELECT last_insert_rowid()'); return result["last_insert_rowid()"] as int; }); } Future getFile(int generatedID) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE $columnGeneratedID = ?', [generatedID], ); if (results.isEmpty) { return null; } return convertToFiles(results)[0]; } Future getUploadedFile(int uploadedID, int collectionID) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE $columnUploadedFileID = ? AND $columnCollectionID = ?', [ uploadedID, collectionID, ], ); if (results.isEmpty) { return null; } return convertToFiles(results)[0]; } Future getAnyUploadedFile(int uploadedID) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE $columnUploadedFileID = ?', [uploadedID], ); if (results.isEmpty) { return null; } return convertToFiles(results)[0]; } Future> getUploadedFileIDs(int collectionID) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT $columnUploadedFileID FROM $filesTable' ' WHERE $columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)', [ collectionID, ], ); final ids = {}; for (final result in results) { ids.add(result[columnUploadedFileID] as int); } return ids; } Future<(Set, Map)> getUploadAndHash( int collectionID, ) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT $columnUploadedFileID, $columnHash FROM $filesTable' ' WHERE $columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)', [ collectionID, ], ); final ids = {}; final hash = {}; for (final result in results) { ids.add(result[columnUploadedFileID] as int); if (result[columnHash] != null) { hash[result[columnHash] as String] = result[columnUploadedFileID] as int; } } return (ids, hash); } Future getBackedUpIDs() async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT $columnLocalID, $columnUploadedFileID, $columnFileSize FROM $filesTable' ' WHERE $columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)', ); final Set localIDs = {}; final Set uploadedIDs = {}; int localSize = 0; for (final result in results) { final String localID = result[columnLocalID] as String; final int? fileSize = result[columnFileSize] as int?; if (!localIDs.contains(localID) && fileSize != null) { localSize += fileSize; } localIDs.add(result[columnLocalID] as String); uploadedIDs.add(result[columnUploadedFileID] as int); } return BackedUpFileIDs(localIDs.toList(), uploadedIDs.toList(), localSize); } Future getAllPendingOrUploadedFiles( int startTime, int endTime, int ownerID, { int? limit, bool? asc, int visibility = visibleVisibility, DBFilterOptions? filterOptions, bool applyOwnerCheck = false, bool ignoreSharedFiles = false, }) async { final stopWatch = EnteWatch('getAllPendingOrUploadedFiles')..start(); final order = (asc ?? false ? 'ASC' : 'DESC'); final subQueries = []; late List? args; if (applyOwnerCheck) { subQueries.add( 'SELECT * FROM $filesTable WHERE $columnCreationTime >= ? AND $columnCreationTime <= ? ' 'AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) ' 'AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)'); args = [startTime, endTime, ownerID]; } else { subQueries.add( 'SELECT * FROM $filesTable WHERE $columnCreationTime >= ? AND $columnCreationTime <= ? ' 'AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)'); args = [startTime, endTime]; } subQueries.add(' AND $columnMMdVisibility = ?'); args.add(visibility); if (ignoreSharedFiles == true) { subQueries.add(' AND $columnOwnerID = ?'); args.add(ownerID); } subQueries.add( ' ORDER BY $columnCreationTime $order, $columnModificationTime $order', ); if (limit != null) { subQueries.add(' LIMIT ?'); args.add(limit); } final finalQuery = subQueries.join(); final db = await instance.sqliteAsyncDB; final results = await db.getAll(finalQuery, args); stopWatch.log('queryDone'); final files = convertToFiles(results); stopWatch.log('convertDone'); final filteredFiles = await applyDBFilters(files, filterOptions); stopWatch.log('filteringDone'); stopWatch.stop(); return FileLoadResult(filteredFiles, files.length == limit); } Future getAllLocalAndUploadedFiles( int startTime, int endTime, int ownerID, { int? limit, bool? asc, bool ignoreSharedFiles = false, required DBFilterOptions filterOptions, }) async { final db = await instance.sqliteAsyncDB; final order = (asc ?? false ? 'ASC' : 'DESC'); final args = [startTime, endTime, visibleVisibility]; final subQueries = []; subQueries.add( 'SELECT * FROM $filesTable WHERE $columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)' ' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))'); if (ignoreSharedFiles == true) { subQueries.add(' AND $columnOwnerID = ?'); args.add(ownerID); } subQueries.add( ' ORDER BY $columnCreationTime $order, $columnModificationTime $order', ); if (limit != null) { subQueries.add(' LIMIT ?'); args.add(limit); } final finalQuery = subQueries.join(); final results = await db.getAll( finalQuery, args, ); final files = convertToFiles(results); final List filteredFiles = await applyDBFilters(files, filterOptions); return FileLoadResult(filteredFiles, files.length == limit); } List deduplicateByLocalID(List files) { final localIDs = {}; final List deduplicatedFiles = []; for (final file in files) { final id = file.localID; if (id == null) { continue; } if (localIDs.contains(id)) { continue; } localIDs.add(id); deduplicatedFiles.add(file); } return deduplicatedFiles; } Future getFilesInCollection( int collectionID, int startTime, int endTime, { int? limit, bool? asc, int visibility = visibleVisibility, }) async { final db = await instance.sqliteAsyncDB; final order = (asc ?? false ? 'ASC' : 'DESC'); String query = 'SELECT * FROM $filesTable WHERE $columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ? ORDER BY $columnCreationTime $order, $columnModificationTime $order'; final List args = [collectionID, startTime, endTime]; if (limit != null) { query += ' LIMIT ?'; args.add(limit); } final results = await db.getAll( query, args, ); final files = convertToFiles(results); return FileLoadResult(files, files.length == limit); } Future> getAllFilesCollection(int collectionID) async { final db = await instance.sqliteAsyncDB; const String whereClause = '$columnCollectionID = ?'; final List whereArgs = [collectionID]; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE $whereClause', whereArgs, ); final files = convertToFiles(results); return files; } Future> getAllFilesFromCollections( Iterable collectionID, ) async { final db = await instance.sqliteAsyncDB; final String sql = 'SELECT * FROM $filesTable WHERE $columnCollectionID IN (${collectionID.join(',')})'; final results = await db.getAll(sql); final files = convertToFiles(results); return files; } Future> getNewFilesInCollection( int collectionID, int addedTime, ) async { final db = await instance.sqliteAsyncDB; const String whereClause = '$columnCollectionID = ? AND $columnAddedTime > ?'; final List whereArgs = [collectionID, addedTime]; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE $whereClause', whereArgs, ); final files = convertToFiles(results); return files; } Future getFilesInCollections( List collectionIDs, int startTime, int endTime, int userID, { int? limit, bool? asc, }) async { if (collectionIDs.isEmpty) { return FileLoadResult([], false); } final inParam = collectionIDs.map((id) => "'$id'").join(','); final db = await instance.sqliteAsyncDB; final order = (asc ?? false ? 'ASC' : 'DESC'); final String whereClause = '$columnCollectionID IN ($inParam) AND $columnCreationTime >= ? AND ' '$columnCreationTime <= ? AND $columnOwnerID = ?'; final List whereArgs = [startTime, endTime, userID]; String query = 'SELECT * FROM $filesTable WHERE $whereClause ORDER BY ' '$columnCreationTime $order, $columnModificationTime $order'; if (limit != null) { query += ' LIMIT ?'; whereArgs.add(limit); } final results = await db.getAll( query, whereArgs, ); final files = convertToFiles(results); final dedupeResult = await applyDBFilters(files, DBFilterOptions.dedupeOption); _logger.info("Fetched " + dedupeResult.length.toString() + " files"); return FileLoadResult(files, files.length == limit); } Future> getFilesCreatedWithinDurations( List> durations, Set ignoredCollectionIDs, { int? visibility, String order = 'ASC', }) async { if (durations.isEmpty) { return []; } final db = await instance.sqliteAsyncDB; String whereClause = durations .map( (duration) => "($columnCreationTime >= ${duration[0]} AND $columnCreationTime < ${duration[1]})", ) .join(" OR "); whereClause = "( $whereClause )"; if (visibility != null) { whereClause += ' AND $columnMMdVisibility = $visibility'; } final query = 'SELECT * FROM $filesTable WHERE $whereClause ORDER BY $columnCreationTime $order'; final results = await db.getAll( query, ); final files = convertToFiles(results); return applyDBFilters( files, DBFilterOptions(ignoredCollectionIDs: ignoredCollectionIDs), ); } // Files which user added to a collection manually but they are not // uploaded yet or files belonging to a collection which is marked for backup Future> getFilesPendingForUpload() async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE ($columnUploadedFileID IS NULL OR ' '$columnUploadedFileID IS -1) AND $columnCollectionID IS NOT NULL AND ' '$columnCollectionID IS NOT -1 AND $columnLocalID IS NOT NULL AND ' '$columnLocalID IS NOT -1 GROUP BY $columnLocalID ' 'ORDER BY $columnCreationTime DESC', ); final files = convertToFiles(results); // future-safe filter just to ensure that the query doesn't end up returning files // which should not be backed up files.removeWhere( (e) => e.collectionID == null || e.localID == null || e.uploadedFileID != null, ); return files; } Future> getUnUploadedLocalFiles() async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE ($columnUploadedFileID IS NULL OR ' '$columnUploadedFileID IS -1) AND $columnLocalID IS NOT NULL ' 'GROUP BY $columnLocalID ORDER BY $columnCreationTime DESC', ); return convertToFiles(results); } Future> getUploadedFileIDsToBeUpdated(int ownerID) async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( 'SELECT DISTINCT $columnUploadedFileID FROM $filesTable WHERE ' '($columnLocalID IS NOT NULL AND $columnOwnerID = ? AND ' '($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) ' 'AND $columnUpdationTime IS NULL) ORDER BY $columnCreationTime DESC ', [ownerID], ); final uploadedFileIDs = []; for (final row in rows) { uploadedFileIDs.add(row[columnUploadedFileID] as int); } return uploadedFileIDs; } Future> getFilesInAllCollection( int uploadedFileID, int userID, ) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE $columnLocalID IS NOT NULL AND ' '$columnOwnerID = ? AND $columnUploadedFileID = ?', [userID, uploadedFileID], ); if (results.isEmpty) { return []; } return convertToFiles(results); } Future> getExistingLocalFileIDs(int ownerID) async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( 'SELECT DISTINCT $columnLocalID FROM $filesTable ' 'WHERE $columnLocalID IS NOT NULL AND ($columnOwnerID IS NULL OR ' '$columnOwnerID = ?)', [ownerID], ); final result = {}; for (final row in rows) { result.add(row[columnLocalID] as String); } return result; } Future> getLocalIDsMarkedForOrAlreadyUploaded(int ownerID) async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( 'SELECT DISTINCT $columnLocalID FROM $filesTable ' 'WHERE $columnLocalID IS NOT NULL AND ($columnCollectionID IS NOT NULL ' 'AND $columnCollectionID != -1) AND ($columnOwnerID = ? OR ' '$columnOwnerID IS NULL)', [ownerID], ); final result = {}; for (final row in rows) { result.add(row[columnLocalID] as String); } return result; } Future> getLocalFileIDsForCollection(int collectionID) async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( 'SELECT $columnLocalID FROM $filesTable ' 'WHERE $columnLocalID IS NOT NULL AND $columnCollectionID = ?', [collectionID], ); final result = {}; for (final row in rows) { result.add(row[columnLocalID] as String); } return result; } // Sets the collectionID for the files with given LocalIDs if the // corresponding file entries are not already mapped to some other collection Future setCollectionIDForUnMappedLocalFiles( int collectionID, Set localIDs, ) async { final db = await instance.sqliteAsyncDB; final inParam = localIDs.map((id) => "'$id'").join(','); await db.execute( ''' UPDATE $filesTable SET $columnCollectionID = $collectionID WHERE $columnLocalID IN ($inParam) AND ($columnCollectionID IS NULL OR $columnCollectionID = -1); ''', ); } Future markFilesForReUpload( int ownerID, String localID, String? title, Location? location, int creationTime, int modificationTime, FileType fileType, ) async { final db = await instance.sqliteAsyncDB; await db.execute( ''' UPDATE $filesTable SET $columnTitle = ?, $columnLatitude = ?, $columnLongitude = ?, $columnCreationTime = ?, $columnModificationTime = ?, $columnUpdationTime = NULL, $columnFileType = ? WHERE $columnLocalID = ? AND ($columnOwnerID = ? OR $columnOwnerID IS NULL); ''', [ title, location?.latitude, location?.longitude, creationTime, modificationTime, getInt(fileType), localID, ownerID, ], ); } /* This method should only return localIDs which are not uploaded yet and can be mapped to incoming remote entry */ Future> getUnlinkedLocalMatchesForRemoteFile( int ownerID, String localID, FileType fileType, { required String title, required String deviceFolder, }) async { final db = await instance.sqliteAsyncDB; // on iOS, match using localID and fileType. title can either match or // might be null based on how the file was imported String query = '''SELECT * FROM $filesTable WHERE ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND $columnLocalID = ? AND $columnFileType = ? AND ($columnTitle=? OR $columnTitle IS NULL) '''; List whereArgs = [ ownerID, localID, getInt(fileType), title, ]; if (Platform.isAndroid) { query = '''SELECT * FROM $filesTable WHERE ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND $columnLocalID = ? AND $columnFileType = ? AND $columnTitle=? AND $columnDeviceFolder= ? '''; whereArgs = [ ownerID, localID, getInt(fileType), title, deviceFolder, ]; } final rows = await db.getAll( query, whereArgs, ); return convertToFiles(rows); } Future> getUserOwnedFilesWithSameHashForGivenListOfFiles( List files, int userID, ) async { final db = await sqliteAsyncDB; final List hashes = []; for (final file in files) { if (file.hash != null && file.hash != '') { hashes.add(file.hash!); } } if (hashes.isEmpty) { return {}; } final inParam = hashes.map((e) => "'$e'").join(','); final rows = await db.getAll(''' SELECT * FROM $filesTable WHERE $columnHash IN ($inParam) AND $columnOwnerID = $userID; '''); final matchedFiles = convertToFiles(rows); return Map.fromIterable(matchedFiles, key: (e) => e.hash); } Future> getUploadedFilesWithHashes( FileHashData hashData, FileType fileType, int ownerID, ) async { String inParam = "'${hashData.fileHash}'"; if (fileType == FileType.livePhoto && hashData.zipHash != null) { inParam += ",'${hashData.zipHash}'"; } final db = await instance.sqliteAsyncDB; final rows = await db.getAll( 'SELECT * FROM $filesTable WHERE ($columnUploadedFileID != NULL OR ' '$columnUploadedFileID != -1) AND $columnOwnerID = ? AND ' '$columnFileType = ? AND $columnHash IN ($inParam)', [ ownerID, getInt(fileType), ], ); return convertToFiles(rows); } Future update(EnteFile file) async { final db = await instance.sqliteAsyncDB; final parameterSet = _getParameterSetForFile(file)..add(file.generatedID); final updateAssignments = _generateUpdateAssignmentsWithPlaceholders( fileGenId: file.generatedID, ); await db.execute( 'UPDATE $filesTable ' 'SET $updateAssignments WHERE $columnGeneratedID = ?', parameterSet, ); } Future updateUploadedFileAcrossCollections(EnteFile file) async { final db = await instance.sqliteAsyncDB; final parameterSet = _getParameterSetForFile(file, omitCollectionId: true) ..add(file.uploadedFileID); final updateAssignments = _generateUpdateAssignmentsWithPlaceholders( fileGenId: file.generatedID, omitCollectionId: true, ); await db.execute( 'UPDATE $filesTable ' 'SET $updateAssignments WHERE $columnUploadedFileID = ?', parameterSet, ); } Future updateLocalIDForUploaded(int uploadedID, String localID) async { final db = await instance.sqliteAsyncDB; await db.execute( 'UPDATE $filesTable SET $columnLocalID = ? WHERE $columnUploadedFileID = ?' ' AND $columnLocalID IS NULL', [localID, uploadedID], ); } Future deleteByGeneratedID(int genID) async { final db = await instance.sqliteAsyncDB; await db.execute( 'DELETE FROM $filesTable WHERE $columnGeneratedID = ?', [genID], ); } Future deleteMultipleUploadedFiles(List uploadedFileIDs) async { final db = await instance.sqliteAsyncDB; final inParam = uploadedFileIDs.join(','); await db.execute( 'DELETE FROM $filesTable WHERE $columnUploadedFileID IN ($inParam)', ); } Future deleteMultipleByGeneratedIDs(List generatedIDs) async { if (generatedIDs.isEmpty) { return; } final db = await instance.sqliteAsyncDB; final inParam = generatedIDs.join(','); await db.execute( 'DELETE FROM $filesTable WHERE $columnGeneratedID IN ($inParam)', ); } Future deleteLocalFile(EnteFile file) async { final db = await instance.sqliteAsyncDB; if (file.localID != null) { // delete all files with same local ID unawaited( db.execute( 'DELETE FROM $filesTable WHERE $columnLocalID = ? AND ($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1)', [file.localID], ), ); } else { unawaited( db.execute( 'DELETE FROM $filesTable WHERE $columnGeneratedID = ? AND ($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1)', [file.generatedID], ), ); } } Future deleteLocalFiles(List localIDs) async { final inParam = localIDs.map((id) => "'$id'").join(','); final db = await instance.sqliteAsyncDB; await db.execute( ''' UPDATE $filesTable SET $columnLocalID = NULL WHERE $columnLocalID IN ($inParam); ''', ); } Future> getLocalFiles(List localIDs) async { final inParam = localIDs.map((id) => "'$id'").join(','); final db = await instance.sqliteAsyncDB; final results = await db.getAll( ''' SELECT * FROM $filesTable WHERE $columnLocalID IN ($inParam); ''', ); return convertToFiles(results); } Future deleteUnSyncedLocalFiles(List localIDs) async { final inParam = localIDs.map((id) => "'$id'").join(','); final db = await instance.sqliteAsyncDB; unawaited( db.execute( ''' DELETE FROM $filesTable WHERE ($columnUploadedFileID is NULL OR $columnUploadedFileID = -1 ) AND $columnLocalID IN ($inParam) ''', ), ); } Future deleteFilesFromCollection( int collectionID, List uploadedFileIDs, ) async { final db = await instance.sqliteAsyncDB; return db.writeTransaction((tx) async { await tx.execute( ''' DELETE FROM $filesTable WHERE $columnCollectionID = ? AND $columnUploadedFileID IN (${uploadedFileIDs.join(', ')}); ''', [collectionID], ); final res = await tx.get('SELECT changes()'); return res['changes()'] as int; }); } Future collectionFileCount(int collectionID) async { final db = await instance.sqliteAsyncDB; final row = await db.get( 'SELECT COUNT(*) FROM $filesTable where $columnCollectionID = ' '$collectionID AND $columnUploadedFileID IS NOT -1', ); return row['COUNT(*)'] as int; } Future archivedFilesCount( int visibility, int ownerID, Set hiddenCollections, ) async { final db = await instance.sqliteAsyncDB; final count = await db.getAll( 'SELECT COUNT(distinct($columnUploadedFileID)) as COUNT FROM $filesTable where ' '$columnMMdVisibility' ' = $visibility AND $columnOwnerID = $ownerID AND $columnCollectionID NOT IN (${hiddenCollections.join(', ')})', ); return count.first['COUNT'] as int; } Future deleteCollection(int collectionID) async { final db = await instance.sqliteAsyncDB; unawaited( db.execute( 'DELETE FROM $filesTable WHERE $columnCollectionID = ?', [collectionID], ), ); } Future removeFromCollection(int collectionID, List fileIDs) async { final db = await instance.sqliteAsyncDB; final inParam = fileIDs.join(','); unawaited( db.execute( ''' DELETE FROM $filesTable WHERE $columnCollectionID = ? AND $columnUploadedFileID IN ($inParam); ''', [collectionID], ), ); } Future> getPendingUploadForCollection(int collectionID) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE $columnCollectionID = ? AND ' '($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1)', [collectionID], ); return convertToFiles(results); } Future> getLocalIDsPresentInEntries( List existingFiles, int collectionID, ) async { final inParam = existingFiles .where((file) => file.localID != null) .map((file) => "'${file.localID}'") .join(','); final db = await instance.sqliteAsyncDB; final rows = await db.getAll( ''' SELECT $columnLocalID FROM $filesTable WHERE $columnLocalID IN ($inParam) AND $columnCollectionID != $collectionID AND $columnLocalID IS NOT NULL; ''', ); final result = {}; for (final row in rows) { result.add(row[columnLocalID] as String); } return result; } // getCollectionLatestFileTime returns map of collectionID to the max // creationTime of the files in the collection. Future> getCollectionIDToMaxCreationTime() async { final enteWatch = EnteWatch("getCollectionIDToMaxCreationTime")..start(); final db = await instance.sqliteAsyncDB; final rows = await db.getAll( ''' SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time FROM $filesTable WHERE ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1 AND $columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) GROUP BY $columnCollectionID; ''', ); final result = {}; for (final row in rows) { result[row[columnCollectionID] as int] = row['max_creation_time'] as int; } enteWatch.log("query done"); return result; } Future> getFileIDToCreationTime() async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( ''' SELECT $columnUploadedFileID, $columnCreationTime FROM $filesTable WHERE ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1); ''', ); final result = {}; for (final row in rows) { result[row[columnUploadedFileID] as int] = row[columnCreationTime] as int; } return result; } // getCollectionFileFirstOrLast returns the first or last uploaded file in // the collection based on the given collectionID and the order. Future getCollectionFileFirstOrLast( int collectionID, bool sortAsc, ) async { final db = await instance.sqliteAsyncDB; final order = sortAsc ? 'ASC' : 'DESC'; final rows = await db.getAll( ''' SELECT * FROM $filesTable WHERE $columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) ORDER BY $columnCreationTime $order, $columnModificationTime $order LIMIT 1; ''', [collectionID], ); if (rows.isEmpty) { return null; } return convertToFiles(rows).first; } Future markForReUploadIfLocationMissing(List localIDs) async { if (localIDs.isEmpty) { return; } final inParam = localIDs.map((id) => "'$id'").join(','); final db = await instance.sqliteAsyncDB; await db.execute( ''' UPDATE $filesTable SET $columnUpdationTime = NULL WHERE $columnLocalID IN ($inParam) AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0); ''', ); } Future doesFileExistInCollection( int uploadedFileID, int collectionID, ) async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( 'SELECT * FROM $filesTable WHERE $columnUploadedFileID = ? AND ' '$columnCollectionID = ? LIMIT 1', [uploadedFileID, collectionID], ); return rows.isNotEmpty; } Future> getFileIDToFileFromIDs(List ids) async { final result = {}; if (ids.isEmpty) { return result; } final inParam = ids.map((id) => "'$id'").join(','); final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE $columnUploadedFileID IN ($inParam)', ); final files = convertToFiles(results); for (final file in files) { result[file.uploadedFileID!] = file; } return result; } Future> getFilesFromIDs( List ids, { bool asc = false, bool dedupeByUploadId = false, Set collectionsToIgnore = const {}, }) async { final order = (asc ? 'ASC' : 'DESC'); if (ids.isEmpty) { return []; } final inParam = ids.map((id) => "'$id'").join(','); final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE $columnUploadedFileID IN ($inParam) ORDER BY $columnCreationTime $order', ); final files = convertToFiles(results); final result = await applyDBFilters( files, DBFilterOptions( ignoredCollectionIDs: collectionsToIgnore, dedupeUploadID: dedupeByUploadId, ), ); return result; } Future>> getAllFilesGroupByCollectionID( List ids, ) async { final result = >{}; if (ids.isEmpty) { return result; } final inParam = ids.map((id) => "'$id'").join(','); final db = await instance.sqliteAsyncDB; final results = await db.getAll( 'SELECT * FROM $filesTable WHERE $columnUploadedFileID IN ($inParam)', ); final files = convertToFiles(results); for (EnteFile eachFile in files) { if (!result.containsKey(eachFile.collectionID)) { result[eachFile.collectionID as int] = []; } result[eachFile.collectionID]!.add(eachFile); } return result; } Future> getAllCollectionIDsOfFile( int uploadedFileID, ) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( ''' SELECT DISTINCT $columnCollectionID FROM $filesTable WHERE $columnUploadedFileID = ? AND $columnCollectionID != -1 ''', [uploadedFileID], ); final collectionIDsOfFile = {}; for (var result in results) { collectionIDsOfFile.add(result['collection_id'] as int); } return collectionIDsOfFile; } ///Each collectionIDs in list aren't necessarily unique Future> getAllCollectionIDsOfFiles( List uploadedFileIDs, ) async { final db = await instance.sqliteAsyncDB; final inParam = uploadedFileIDs.join(','); final results = await db.getAll( ''' SELECT $columnCollectionID FROM $filesTable WHERE $columnUploadedFileID IN ($inParam) AND $columnCollectionID != -1 ''', ); final collectionIDsOfFiles = []; for (var result in results) { collectionIDsOfFiles.add(result['collection_id'] as int); } return collectionIDsOfFiles; } List convertToFilesForIsolate(Map args) { final List files = []; for (final result in args["result"]) { files.add(_getFileFromRow(result)); } return files; } List convertToFiles(List> results) { final List files = []; for (final result in results) { files.add(_getFileFromRow(result)); } return files; } Future> getGeneratedIDForFilesOlderThan( int cutOffTime, int ownerID, ) async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( ''' SELECT DISTINCT $columnGeneratedID FROM $filesTable WHERE $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) ''', [cutOffTime, ownerID], ); final result = []; for (final row in rows) { result.add(row[columnGeneratedID].toString()); } return result; } // For givenUserID, get List of unique LocalIDs for files which are // uploaded by the given user and location is missing Future> getLocalIDsForFilesWithoutLocation(int ownerID) async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( ''' SELECT DISTINCT $columnLocalID FROM $filesTable WHERE $columnOwnerID = ? AND $columnLocalID IS NOT NULL AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLatitude = 0.0 or $columnLongitude = 0.0) ''', [ownerID], ); final result = []; for (final row in rows) { result.add(row[columnLocalID].toString()); } return result; } // For a given userID, return unique uploadedFileId for the given userID Future> getUploadIDsWithMissingSize(int userId) async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( ''' SELECT DISTINCT $columnUploadedFileID FROM $filesTable WHERE $columnOwnerID = ? AND $columnFileSize IS NULL ''', [userId], ); final result = []; for (final row in rows) { result.add(row[columnUploadedFileID] as int); } return result; } // For a given userID, return unique localID for all uploaded live photos Future> getLivePhotosForUser(int userId) async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( ''' SELECT DISTINCT $columnLocalID FROM $filesTable WHERE $columnOwnerID = ? AND $columnFileType = ? AND $columnLocalID IS NOT NULL ''', [userId, getInt(FileType.livePhoto)], ); final result = []; for (final row in rows) { result.add(row[columnLocalID] as String); } return result; } Future> getLocalFilesBackedUpWithoutLocation(int userId) async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( ''' SELECT DISTINCT $columnLocalID FROM $filesTable WHERE $columnOwnerID = ? AND $columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLatitude = 0.0 or $columnLongitude = 0.0) ''', [userId], ); final result = []; for (final row in rows) { result.add(row[columnLocalID] as String); } return result; } // updateSizeForUploadIDs takes a map of upploadedFileID and fileSize and // update the fileSize for the given uploadedFileID Future updateSizeForUploadIDs( Map uploadedFileIDToSize, ) async { if (uploadedFileIDToSize.isEmpty) { return; } final db = await instance.sqliteAsyncDB; final parameterSets = >[]; for (final uploadedFileID in uploadedFileIDToSize.keys) { parameterSets.add([ uploadedFileIDToSize[uploadedFileID], uploadedFileID, ]); } await db.executeBatch( ''' UPDATE $filesTable SET $columnFileSize = ? WHERE $columnUploadedFileID = ?; ''', parameterSets, ); } Future> getAllFilesAfterDate({ required FileType fileType, required DateTime beginDate, required int userID, }) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( ''' SELECT * FROM $filesTable WHERE $columnFileType = ? AND $columnCreationTime > ? AND $columnUploadedFileID != -1 AND $columnOwnerID = $userID ORDER BY $columnCreationTime DESC ''', [getInt(fileType), beginDate.microsecondsSinceEpoch], ); return convertToFiles(results); } Future> getAllFilesFromDB( Set collectionsToIgnore, { bool dedupeByUploadId = true, }) async { final db = await instance.sqliteAsyncDB; final result = await db.getAll( 'SELECT * FROM $filesTable ORDER BY $columnCreationTime DESC', ); _logger.info("${result.length} rows in filesDB"); final List files = await Computer.shared() .compute(convertToFilesForIsolate, param: {"result": result}); final List deduplicatedFiles = await applyDBFilters( files, DBFilterOptions( ignoredCollectionIDs: collectionsToIgnore, dedupeUploadID: dedupeByUploadId, ), ); return deduplicatedFiles; } Future> fetchFilesCountbyType(int userID) async { final db = await instance.sqliteAsyncDB; final result = await db.getAll( ''' SELECT $columnFileType, COUNT(DISTINCT $columnUploadedFileID) FROM $filesTable WHERE $columnUploadedFileID != -1 AND $columnOwnerID IS $userID GROUP BY $columnFileType ''', ); final filesCount = {}; for (var e in result) { filesCount.addAll( {getFileType(e[columnFileType] as int): e.values.last as int}, ); } return filesCount; } Future fetchAllUploadedAndSharedFilesWithLocation( int startTime, int endTime, { int? limit, bool? asc, required DBFilterOptions? filterOptions, }) async { final db = await instance.sqliteAsyncDB; final order = (asc ?? false ? 'ASC' : 'DESC'); String query = ''' SELECT * FROM $filesTable WHERE $columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0) AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)) ORDER BY $columnCreationTime $order, $columnModificationTime $order '''; final args = [startTime, endTime]; if (limit != null) { query += ' LIMIT ?'; args.add(limit); } final results = await db.getAll( query, args, ); final files = convertToFiles(results); final List filteredFiles = await applyDBFilters(files, filterOptions); return FileLoadResult(filteredFiles, files.length == limit); } Future> getAllFileIDs() async { final db = await instance.sqliteAsyncDB; final results = await db.getAll(''' SELECT DISTINCT $columnUploadedFileID FROM $filesTable WHERE $columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1 '''); final ids = []; for (final result in results) { ids.add(result[columnUploadedFileID] as int); } return ids; } ///Returns "columnName1 = ?, columnName2 = ?, ..." String _generateUpdateAssignmentsWithPlaceholders({ required int? fileGenId, bool omitCollectionId = false, }) { final assignments = []; for (String columnName in _columnNames) { if (columnName == columnGeneratedID && fileGenId == null) { continue; } if (columnName == columnCollectionID && omitCollectionId) { continue; } assignments.add("$columnName = ?"); } return assignments.join(","); } Map _generateColumnsAndPlaceholdersForInsert({ required int? fileGenId, }) { final columnNames = []; for (String columnName in _columnNames) { if (columnName == columnGeneratedID && fileGenId == null) { continue; } columnNames.add(columnName); } return { "columns": columnNames.join(","), "placeholders": List.filled(columnNames.length, "?").join(","), }; } List _getParameterSetForFile( EnteFile file, { bool omitCollectionId = false, }) { final values = []; double? latitude = file.location?.latitude; double? longitude = file.location?.longitude; int? creationTime = file.creationTime; if (file.pubMagicMetadata != null) { if (file.pubMagicMetadata!.editedTime != null) { creationTime = file.pubMagicMetadata!.editedTime; } if (file.pubMagicMetadata!.lat != null && file.pubMagicMetadata!.long != null) { latitude = file.pubMagicMetadata!.lat; longitude = file.pubMagicMetadata!.long; } } if (file.generatedID != null) { values.add(file.generatedID); } values.addAll([ file.localID, file.uploadedFileID ?? -1, file.ownerID, file.collectionID ?? -1, file.title, file.deviceFolder, latitude, longitude, getInt(file.fileType), file.modificationTime, file.encryptedKey, file.keyDecryptionNonce, file.fileDecryptionHeader, file.thumbnailDecryptionHeader, file.metadataDecryptionHeader, creationTime, file.updationTime, file.fileSubType ?? -1, file.duration ?? 0, file.exif, file.hash, file.metadataVersion, file.mMdEncodedJson ?? '{}', file.mMdVersion, file.magicMetadata.visibility, file.pubMmdEncodedJson ?? '{}', file.pubMmdVersion, file.fileSize, file.addedTime ?? DateTime.now().microsecondsSinceEpoch, ]); if (omitCollectionId) { values.removeAt(3); } return values; } Future _batchAndInsertFile( EnteFile file, SqliteAsyncConflictAlgorithm conflictAlgorithm, SqliteDatabase db, List> parameterSets, PrimitiveWrapper batchCounter, { required bool isGenIdNull, }) async { parameterSets.add(_getParameterSetForFile(file)); batchCounter.value++; final columnNames = isGenIdNull ? _columnNames.where((column) => column != columnGeneratedID) : _columnNames; if (batchCounter.value == 400) { _logger.info("Inserting batch with genIdNull: $isGenIdNull"); await _insertBatch(conflictAlgorithm, columnNames, db, parameterSets); batchCounter.value = 0; parameterSets.clear(); } } Future _insertBatch( SqliteAsyncConflictAlgorithm conflictAlgorithm, Iterable columnNames, SqliteDatabase db, List> parameterSets, ) async { final valuesPlaceholders = List.filled(columnNames.length, "?").join(","); final columnNamesJoined = columnNames.join(","); await db.executeBatch( ''' INSERT OR ${conflictAlgorithm.name.toUpperCase()} INTO $filesTable($columnNamesJoined) VALUES($valuesPlaceholders) ''', parameterSets, ); } EnteFile _getFileFromRow(Map row) { final file = EnteFile(); file.generatedID = row[columnGeneratedID]; file.localID = row[columnLocalID]; file.uploadedFileID = row[columnUploadedFileID] == -1 ? null : row[columnUploadedFileID]; file.ownerID = row[columnOwnerID]; file.collectionID = row[columnCollectionID] == -1 ? null : row[columnCollectionID]; file.title = row[columnTitle]; file.deviceFolder = row[columnDeviceFolder]; if (row[columnLatitude] != null && row[columnLongitude] != null) { file.location = Location( latitude: row[columnLatitude], longitude: row[columnLongitude], ); } file.fileType = getFileType(row[columnFileType]); file.creationTime = row[columnCreationTime]; file.modificationTime = row[columnModificationTime]; file.updationTime = row[columnUpdationTime] ?? -1; file.addedTime = row[columnAddedTime]; file.encryptedKey = row[columnEncryptedKey]; file.keyDecryptionNonce = row[columnKeyDecryptionNonce]; file.fileDecryptionHeader = row[columnFileDecryptionHeader]; file.thumbnailDecryptionHeader = row[columnThumbnailDecryptionHeader]; file.metadataDecryptionHeader = row[columnMetadataDecryptionHeader]; file.fileSubType = row[columnFileSubType] ?? -1; file.duration = row[columnDuration] ?? 0; file.exif = row[columnExif]; file.hash = row[columnHash]; file.metadataVersion = row[columnMetadataVersion] ?? 0; file.fileSize = row[columnFileSize]; file.mMdVersion = row[columnMMdVersion] ?? 0; file.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}'; file.pubMmdVersion = row[columnPubMMdVersion] ?? 0; file.pubMmdEncodedJson = row[columnPubMMdEncodedJson] ?? '{}'; return file; } }