import 'dart:io'; import 'package:logging/logging.dart'; import 'package:photos/models/decryption_params.dart'; import 'package:photos/models/file_type.dart'; import 'package:photos/models/location.dart'; import 'package:photos/models/file.dart'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path_provider/path_provider.dart'; class FilesDB { static final _databaseName = "ente.files.db"; static final _databaseVersion = 1; static final Logger _logger = Logger("FilesDB"); static final table = 'files'; static final columnGeneratedID = '_id'; static final columnUploadedFileID = 'uploaded_file_id'; static final columnOwnerID = 'owner_id'; static final columnLocalID = 'local_id'; static final columnTitle = 'title'; static final columnDeviceFolder = 'device_folder'; static final columnLatitude = 'latitude'; static final columnLongitude = 'longitude'; static final columnFileType = 'file_type'; static final columnRemoteFolderID = 'remote_folder_id'; static final columnIsEncrypted = 'is_encrypted'; static final columnIsDeleted = 'is_deleted'; static final columnCreationTime = 'creation_time'; static final columnModificationTime = 'modification_time'; static final columnUpdationTime = 'updation_time'; static final columnFileDecryptionParams = 'file_decryption_params'; static final columnThumbnailDecryptionParams = 'thumbnail_decryption_params'; static final columnMetadataDecryptionParams = 'metadata_decryption_params'; // make this a singleton class FilesDB._privateConstructor(); static final FilesDB instance = FilesDB._privateConstructor(); // only have a single app-wide reference to the database static Database _database; Future get database async { if (_database != null) return _database; // lazily instantiate the db the first time it is accessed _database = await _initDatabase(); return _database; } // this opens the database (and creates it if it doesn't exist) _initDatabase() async { Directory documentsDirectory = await getApplicationDocumentsDirectory(); String path = join(documentsDirectory.path, _databaseName); return await openDatabase(path, version: _databaseVersion, onCreate: _onCreate); } // SQL code to create the database table Future _onCreate(Database db, int version) async { await db.execute(''' CREATE TABLE $table ( $columnGeneratedID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, $columnLocalID TEXT, $columnUploadedFileID INTEGER, $columnOwnerID INTEGER, $columnTitle TEXT NOT NULL, $columnDeviceFolder TEXT NOT NULL, $columnLatitude REAL, $columnLongitude REAL, $columnFileType INTEGER, $columnRemoteFolderID INTEGER, $columnIsEncrypted INTEGER DEFAULT 0, $columnIsDeleted INTEGER DEFAULT 0, $columnCreationTime TEXT NOT NULL, $columnModificationTime TEXT NOT NULL, $columnUpdationTime TEXT, $columnFileDecryptionParams TEXT, $columnThumbnailDecryptionParams TEXT, $columnMetadataDecryptionParams TEXT ) '''); } Future insert(File file) async { final db = await instance.database; return await db.insert(table, _getRowForFile(file)); } Future> insertMultiple(List files) async { final db = await instance.database; var batch = db.batch(); int batchCounter = 0; for (File file in files) { if (batchCounter == 400) { await batch.commit(); batch = db.batch(); } batch.insert(table, _getRowForFile(file)); batchCounter++; } return await batch.commit(); } Future> getOwnedFiles(int ownerID) async { final db = await instance.database; final whereArgs = List(); if (ownerID != null) { whereArgs.add(ownerID); } final results = await db.query( table, // where: '$columnIsDeleted = 0' + // (ownerID == null ? '' : ' AND $columnOwnerID = ?'), // whereArgs: whereArgs, orderBy: '$columnCreationTime DESC', ); return _convertToFiles(results); } Future> getAllVideos() async { final db = await instance.database; final results = await db.query( table, where: '$columnLocalID IS NOT NULL AND $columnFileType = 1 AND $columnIsDeleted = 0', orderBy: '$columnCreationTime DESC', ); return _convertToFiles(results); } Future> getAllInFolder( int folderID, int beforeCreationTime, int limit) async { final db = await instance.database; final results = await db.query( table, where: '$columnRemoteFolderID = ? AND $columnIsDeleted = 0 AND $columnCreationTime < ?', whereArgs: [folderID, beforeCreationTime], orderBy: '$columnCreationTime DESC', limit: limit, ); return _convertToFiles(results); } Future> getFilesCreatedWithinDuration( int startCreationTime, int endCreationTime) async { final db = await instance.database; final results = await db.query( table, where: '$columnCreationTime > ? AND $columnCreationTime < ? AND $columnIsDeleted = 0', whereArgs: [startCreationTime, endCreationTime], orderBy: '$columnCreationTime ASC', ); return _convertToFiles(results); } Future> getAllDeleted() async { final db = await instance.database; final results = await db.query( table, where: '$columnIsDeleted = 1', orderBy: '$columnCreationTime DESC', ); return _convertToFiles(results); } Future> getFilesToBeUploadedWithinFolders( Set folders) async { final db = await instance.database; String inParam = ""; for (final folder in folders) { inParam += "'" + folder + "',"; } inParam = inParam.substring(0, inParam.length - 1); final results = await db.query( table, where: '$columnUploadedFileID IS NULL AND $columnDeviceFolder IN ($inParam)', orderBy: '$columnCreationTime DESC', ); return _convertToFiles(results); } Future getMatchingFile(String localID, String title, String deviceFolder, int creationTime, int modificationTime, {String alternateTitle}) async { final db = await instance.database; final rows = await db.query( table, where: '''$columnLocalID=? AND ($columnTitle=? OR $columnTitle=?) AND $columnDeviceFolder=? AND $columnCreationTime=? AND $columnModificationTime=?''', whereArgs: [ localID, title, alternateTitle, deviceFolder, creationTime, modificationTime, ], ); if (rows.isNotEmpty) { return _getFileFromRow(rows[0]); } else { throw ("No matching file found"); } } Future getMatchingRemoteFile(int uploadedFileID) async { final db = await instance.database; final rows = await db.query( table, where: '$columnUploadedFileID=?', whereArgs: [uploadedFileID], ); if (rows.isNotEmpty) { return _getFileFromRow(rows[0]); } else { throw ("No matching file found"); } } Future update( int generatedID, int uploadedID, int updationTime, DecryptionParams fileDecryptionParams, DecryptionParams thumbnailDecryptionParams, DecryptionParams metadataDecryptionParams, ) async { final db = await instance.database; final values = new Map(); values[columnUploadedFileID] = uploadedID; values[columnUpdationTime] = updationTime; values[columnFileDecryptionParams] = fileDecryptionParams.toJson(); values[columnThumbnailDecryptionParams] = thumbnailDecryptionParams.toJson(); values[columnMetadataDecryptionParams] = metadataDecryptionParams.toJson(); return await db.update( table, values, where: '$columnGeneratedID = ?', whereArgs: [generatedID], ); } // TODO: Remove deleted files on remote Future markForDeletion(File file) async { final db = await instance.database; final values = new Map(); values[columnIsDeleted] = 1; return db.update( table, values, where: '$columnGeneratedID =?', whereArgs: [file.generatedID], ); } Future delete(File file) async { final db = await instance.database; return db.delete( table, where: '$columnGeneratedID =?', whereArgs: [file.generatedID], ); } Future deleteFilesInRemoteFolder(int folderID) async { final db = await instance.database; return db.delete( table, where: '$columnRemoteFolderID =?', whereArgs: [folderID], ); } Future> getLocalPaths() async { final db = await instance.database; final rows = await db.query( table, columns: [columnDeviceFolder], distinct: true, where: '$columnRemoteFolderID IS NULL', ); List result = List(); for (final row in rows) { result.add(row[columnDeviceFolder]); } return result; } Future getLatestFileInPath(String path) async { final db = await instance.database; final rows = await db.query( table, where: '$columnDeviceFolder =?', whereArgs: [path], orderBy: '$columnCreationTime DESC', limit: 1, ); if (rows.isNotEmpty) { return _getFileFromRow(rows[0]); } else { throw ("No file found in path"); } } Future getLatestFileInRemoteFolder(int folderID) async { final db = await instance.database; final rows = await db.query( table, where: '$columnRemoteFolderID =?', whereArgs: [folderID], orderBy: '$columnCreationTime DESC', limit: 1, ); if (rows.isNotEmpty) { return _getFileFromRow(rows[0]); } else { throw ("No file found in remote folder " + folderID.toString()); } } Future getLastSyncedFileInRemoteFolder(int folderID) async { final db = await instance.database; final rows = await db.query( table, where: '$columnRemoteFolderID =?', whereArgs: [folderID], orderBy: '$columnUpdationTime DESC', limit: 1, ); if (rows.isNotEmpty) { return _getFileFromRow(rows[0]); } else { throw ("No file found in remote folder " + folderID.toString()); } } Future getLatestFileAmongGeneratedIDs(List generatedIDs) async { final db = await instance.database; final rows = await db.query( table, where: '$columnGeneratedID IN (${generatedIDs.join(",")})', orderBy: '$columnCreationTime DESC', limit: 1, ); if (rows.isNotEmpty) { return _getFileFromRow(rows[0]); } else { throw ("No file found with ids " + generatedIDs.join(", ").toString()); } } List _convertToFiles(List> results) { final files = List(); for (final result in results) { files.add(_getFileFromRow(result)); } return files; } Map _getRowForFile(File file) { final row = new Map(); row[columnLocalID] = file.localID; row[columnUploadedFileID] = file.uploadedFileID; row[columnOwnerID] = file.ownerID; row[columnTitle] = file.title; row[columnDeviceFolder] = file.deviceFolder; if (file.location != null) { row[columnLatitude] = file.location.latitude; row[columnLongitude] = file.location.longitude; } switch (file.fileType) { case FileType.image: row[columnFileType] = 0; break; case FileType.video: row[columnFileType] = 1; break; default: row[columnFileType] = -1; } row[columnIsEncrypted] = file.isEncrypted ? 1 : 0; row[columnRemoteFolderID] = file.remoteFolderID; row[columnCreationTime] = file.creationTime; row[columnModificationTime] = file.modificationTime; row[columnUpdationTime] = file.updationTime; row[columnFileDecryptionParams] = file.fileDecryptionParams == null ? null : file.fileDecryptionParams.toJson(); row[columnThumbnailDecryptionParams] = file.thumbnailDecryptionParams == null ? null : file.thumbnailDecryptionParams.toJson(); row[columnMetadataDecryptionParams] = file.metadataDecryptionParams == null ? null : file.metadataDecryptionParams.toJson(); return row; } File _getFileFromRow(Map row) { final file = File(); file.generatedID = row[columnGeneratedID]; file.localID = row[columnLocalID]; file.uploadedFileID = row[columnUploadedFileID]; file.ownerID = row[columnUploadedFileID]; file.title = row[columnTitle]; file.deviceFolder = row[columnDeviceFolder]; if (row[columnLatitude] != null && row[columnLongitude] != null) { file.location = Location(row[columnLatitude], row[columnLongitude]); } file.fileType = getFileType(row[columnFileType]); file.remoteFolderID = row[columnRemoteFolderID]; file.isEncrypted = row[columnIsEncrypted] == 1; file.creationTime = int.parse(row[columnCreationTime]); file.modificationTime = int.parse(row[columnModificationTime]); file.updationTime = row[columnUpdationTime] == null ? -1 : int.parse(row[columnUpdationTime]); file.fileDecryptionParams = DecryptionParams.fromJson(row[columnFileDecryptionParams]); file.thumbnailDecryptionParams = DecryptionParams.fromJson(row[columnThumbnailDecryptionParams]); file.metadataDecryptionParams = DecryptionParams.fromJson(row[columnMetadataDecryptionParams]); return file; } }