import 'dart:io'; import 'package:logging/logging.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 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 columnIsDeleted = 'is_deleted'; static final columnCreationTime = 'creation_time'; static final columnModificationTime = 'modification_time'; static final columnUpdationTime = 'updation_time'; // 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, $columnTitle TEXT NOT NULL, $columnDeviceFolder TEXT NOT NULL, $columnLatitude REAL, $columnLongitude REAL, $columnFileType INTEGER, $columnRemoteFolderId INTEGER, $columnIsDeleted INTEGER DEFAULT 0, $columnCreationTime TEXT NOT NULL, $columnModificationTime TEXT NOT NULL, $columnUpdationTime 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> getAllLocalFiles() async { final db = await instance.database; final results = await db.query( table, where: '$columnLocalId IS NOT NULL AND $columnIsDeleted = 0', 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> getFilesToBeUploaded() async { final db = await instance.database; final results = await db.query( table, where: '$columnUploadedFileId IS NULL', 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) async { final db = await instance.database; final values = new Map(); values[columnUploadedFileId] = uploadedId; values[columnUpdationTime] = updationTime; 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[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[columnRemoteFolderId] = file.remoteFolderId; row[columnCreationTime] = file.creationTime; row[columnModificationTime] = file.modificationTime; row[columnUpdationTime] = file.updationTime; return row; } File _getFileFromRow(Map row) { final file = File(); file.generatedId = row[columnGeneratedId]; file.localId = row[columnLocalId]; file.uploadedFileId = 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.creationTime = int.parse(row[columnCreationTime]); file.modificationTime = int.parse(row[columnModificationTime]); file.updationTime = row[columnUpdationTime] == null ? -1 : int.parse(row[columnUpdationTime]); return file; } }