This commit is contained in:
Neeraj Gupta
2025-07-28 16:53:57 +05:30
parent b8dab3ea1c
commit 5e3e3b4427
12 changed files with 125 additions and 178 deletions

View File

@@ -421,36 +421,6 @@ class FilesDB with SqlDbBase {
await db.execute('DELETE FROM entities');
}
// blocked upload queue (shared assets rendering)
Future<int> 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;
});
}
// upload queue
Future<EnteFile?> 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<BackedUpFileIDs> getBackedUpIDs() async {
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
@@ -616,23 +586,6 @@ class FilesDB with SqlDbBase {
return files;
}
// todo:rewrite (upload related)
// Future<List<int>> 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 = <int>[];
// for (final row in rows) {
// uploadedFileIDs.add(row[columnUploadedFileID] as int);
// }
// return uploadedFileIDs;
// }
// todo:rewrite (upload related)
Future<void> markFilesForReUpload(
int ownerID,
@@ -835,25 +788,6 @@ class FilesDB with SqlDbBase {
return deduplicatedFiles;
}
Map<String, String> _generateColumnsAndPlaceholdersForInsert({
required int? fileGenId,
}) {
final columnNames = <String>[];
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<EnteFile> convertToFilesForIsolate(Map args) {
final List<EnteFile> files = [];
for (final result in args["result"]) {
@@ -870,66 +804,6 @@ class FilesDB with SqlDbBase {
return files;
}
List<Object?> _getParameterSetForFile(
EnteFile file, {
bool omitCollectionId = false,
}) {
final values = <Object?>[];
double? latitude = file.location?.latitude;
double? longitude = file.location?.longitude;
int? creationTime = file.creationTime;
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,
'no_encrypted_key', // encryptedKey is not used in this context
// file.keyDecryptionNonce,
'no_key_decryption_nonce', // keyDecryptionNonce is not used in this context
// file.fileDecryptionHeader,
'no_file_decryption_header', // fileDecryptionHeader is not used in this context
// file.thumbnailDecryptionHeader,
'no_thumbnail_decryption_header', // thumbnailDecryptionHeader is not used in this context
'na',
creationTime,
file.updationTime,
file.fileSubType ?? -1,
file.duration ?? 0,
file.exif,
file.hash,
file.metadataVersion,
// file.mMdEncodedJson ?? '{}',
{}, // mMdEncodedJson is not used in this context
0, // version
0, // default visibility
'{}', // pubMmdEncodedJson is not used in this context
0, // pubMmdVersion is not used in this context
// file.pubMmdEncodedJson ?? '{}',
// file.pubMmdVersion,
file.fileSize,
DateTime.now().microsecondsSinceEpoch, // added Time
]);
if (omitCollectionId) {
values.removeAt(3);
}
return values;
}
EnteFile _getFileFromRow(Map<String, dynamic> row) {
final file = EnteFile();
file.generatedID = row[columnGeneratedID];

View File

@@ -49,6 +49,34 @@ class LocalDB with SqlDbBase {
);
}
// Store time and location metadata inside edited_assets
Future<void> trackEdit(
String id,
int createdAt,
int modifiedAt,
double? lat,
double? lng,
) async {
final stopwatch = Stopwatch()..start();
await _sqliteDB.execute(
'INSERT INTO edited_assets (id, created_at, modified_at, latitude, longitude) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET created_at = ?, modified_at = ?, latitude = ?, longitude = ?',
[id, createdAt, modifiedAt, lat, lng, createdAt, modifiedAt, lat, lng],
);
debugPrint(
'$runtimeType editCopy complete in ${stopwatch.elapsed.inMilliseconds}ms for $id',
);
}
) async {
final stopwatch = Stopwatch()..start();
await _sqliteDB.execute(
'INSERT INTO edited_assets (id, created_at, modified_at, latitude, longitude) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET created_at = ?, modified_at = ?, latitude = ?, longitude = ?',
[id, createdAt, modifiedAt, lat, lng, createdAt, modifiedAt, lat, lng],
);
debugPrint(
'$runtimeType editCopy complete in ${stopwatch.elapsed.inMilliseconds}ms for $id',
);
}
Future<void> updateMetadata(
String id, {
DroidMetadata? droid,

View File

@@ -215,6 +215,17 @@ class LocalDBMigration {
'''
CREATE INDEX IF NOT EXISTS assets_created_at_desc ON assets(created_at DESC);
''',
'''
CREATE TABLE edited_assets (
id String NOT NULL,
created_at INTEGER NOT NULL,
modified_at INTEGER NOT NULL,
latitude REAL,
longitude REAL,
PRIMARY KEY (id)
FOREIGN KEY (id) REFERENCES assets(id) ON DELETE CASCADE
);
''',
];
static Future<void> migrate(

View File

@@ -43,6 +43,14 @@ extension UploadQueueTable on LocalDB {
}
}
Future<bool> existsQueueEntry(AssetUploadQueue entry) async {
final result = await sqliteDB.getAll(
'SELECT 1 FROM asset_upload_queue WHERE asset_id = ? AND owner_id = ? AND dest_collection_id = ?',
[entry.id, entry.ownerId, entry.destCollectionId],
);
return result.isNotEmpty;
}
Future<int> delete(AssetUploadQueue entry) async {
final stopwatch = Stopwatch()..start();
final result = await sqliteDB.execute(

View File

@@ -17,6 +17,8 @@ class FileUploadItem {
this.assetQueue,
this.status = UploadStatus.notStarted,
});
String get lockKey => assetQueue?.id ?? file.localID!;
}
enum UploadStatus { notStarted, inProgress, inBackground, completed }

View File

@@ -51,6 +51,7 @@ import "package:photos/utils/file_key.dart";
import "package:photos/utils/network_util.dart";
import 'package:photos/utils/standalone/data.dart';
import 'package:shared_preferences/shared_preferences.dart';
import "package:timezone/timezone.dart";
import "package:uuid/uuid.dart";
class FileUploader {
@@ -1153,20 +1154,20 @@ class FileUploader {
.where((e) => e.value.status == UploadStatus.inBackground)
.toList();
for (final upload in blockedUploads) {
final file = upload.value.file;
final String lockKey = upload.value.lockKey;
final isStillLocked = await _uploadLocks.isLocked(
file.localID!,
lockKey,
ProcessType.background.toString(),
);
if (!isStillLocked) {
if (!isStillLocked && upload.value.assetQueue != null) {
final completer = _queue.remove(upload.key)?.completer;
final dbFile =
await FilesDB.instance.getFile(upload.value.file.generatedID!);
if (dbFile?.uploadedFileID != null) {
final exists = await localDB.existsQueueEntry(upload.value.assetQueue!);
if (exists) {
_logger.info(
"Background upload success detected ${upload.value.file.tag}",
);
completer?.complete(dbFile);
// todo:neeraj Instead of cancel, fire event
completer?.completeError(SilentlyCancelUploadsError());
_allBackups[upload.key] = _allBackups[upload.key]!
.copyWith(status: BackupItemStatus.uploaded);
} else {
@@ -1177,7 +1178,7 @@ class FileUploader {
// by the background process. Release any lock taken by the foreground process
// and complete the completer with error.
await _uploadLocks.releaseLock(
file.localID!,
lockKey,
ProcessType.foreground.toString(),
);
completer?.completeError(SilentlyCancelUploadsError());

View File

@@ -191,23 +191,23 @@ class AlbumHomeWidgetService {
await remoteCache.geFilesForCollection(collection.id);
// Then open the specific file
final file = await FilesDB.instance.getFile(fileId);
// final file = await FilesDB.instance.getFile(fileId);
final file = null;
if (file == null) {
_logger.warning("Cannot launch widget: file with ID $fileId not found");
return;
}
routeToPage(
context,
DetailPage(
DetailPageConfiguration(
getAllFilesCollection,
getAllFilesCollection.indexOf(file),
"albumwidget",
} else {
routeToPage(
context,
DetailPage(
DetailPageConfiguration(
getAllFilesCollection,
getAllFilesCollection.indexOf(file),
"albumwidget",
),
),
),
forceCustomPageRoute: true,
).ignore();
forceCustomPageRoute: true,
).ignore();
}
await _refreshAlbumsWidget();
}

View File

@@ -31,6 +31,25 @@ class DBFilterOptions {
this.ignoreSharedItems = false,
});
// CopyWith method to create a new instance with some options changed
DBFilterOptions copyWith({
Set<int>? ignoredCollectionIDs,
bool? hideIgnoredForUpload,
bool? dedupeUploadID,
bool? ignoreSavedFiles,
bool? onlyUploadedFiles,
bool? ignoreSharedItems,
}) {
return DBFilterOptions(
ignoredCollectionIDs: ignoredCollectionIDs ?? this.ignoredCollectionIDs,
hideIgnoredForUpload: hideIgnoredForUpload ?? this.hideIgnoredForUpload,
dedupeUploadID: dedupeUploadID ?? this.dedupeUploadID,
ignoreSavedFiles: ignoreSavedFiles ?? this.ignoreSavedFiles,
onlyUploadedFiles: onlyUploadedFiles ?? this.onlyUploadedFiles,
ignoreSharedItems: ignoreSharedItems ?? this.ignoreSharedItems,
);
}
static DBFilterOptions dedupeOption = DBFilterOptions(
dedupeUploadID: true,
);

View File

@@ -162,12 +162,6 @@ class PeopleHomeWidgetService {
String personId,
BuildContext context,
) async {
final file = await FilesDB.instance.getFile(fileId);
if (file == null) {
_logger.warning("Cannot launch widget: file with ID $fileId not found");
return;
}
final person = await PersonService.instance.getPerson(personId);
if (person == null) {
_logger
@@ -184,6 +178,12 @@ class PeopleHomeWidgetService {
forceCustomPageRoute: true,
).ignore();
// final file = await FilesDB.instance.getFile(fileId);
final file = null;
if (file == null) {
_logger.warning("Cannot launch widget: file with ID $fileId not found");
return;
}
final clusterFiles =
await SearchService.instance.getClusterFilesForPersonID(
personId,

View File

@@ -310,11 +310,7 @@ class RemoteSyncService {
try {
final uploadedFile = await _uploader
.upload(file, queueEntry.destCollectionId, queue: queueEntry);
await _collectionsService.addOrCopyToCollection(
queueEntry.destCollectionId,
[uploadedFile],
);
_onFileUploaded(uploadedFile, queueEntry: queueEntry);
_onFileUploaded(uploadedFile);
} catch (error, stackTrace) {
_onFileUploadError(error, stackTrace, file);
}
@@ -322,7 +318,7 @@ class RemoteSyncService {
void _onFileUploaded(
EnteFile file, {
AssetUploadQueue? queueEntry,
}) {
Bus.instance.fire(
CollectionUpdatedEvent(file.collectionID, [file], "fileUpload"),

View File

@@ -104,7 +104,9 @@ class _HomeGalleryWidgetV2State extends State<HomeGalleryWidgetV2> {
final List<EnteFile> allFiles = await merge(
localFiles: localFiles,
remoteFiles: enteFiles,
filterOptions: homeGalleryFilters,
filterOptions: homeGalleryFilters.copyWith(
ignoreSharedItems: _shouldHideSharedItems,
),
);
_logger.info(
"Merged files: ${allFiles.length} (local: ${localFiles.length}, remote: ${enteFiles.length}) files $tl, total ${tl.elapsed}",

View File

@@ -4,12 +4,13 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart';
import "package:photos/core/configuration.dart";
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import "package:photos/db/local/table/upload_queue_table.dart";
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/generated/l10n.dart';
import 'package:photos/models/file/file.dart';
import 'package:photos/models/location/location.dart';
import "package:photos/service_locator.dart";
import 'package:photos/services/sync/sync_service.dart';
import 'package:photos/ui/notification/toast.dart';
import 'package:photos/ui/viewer/file/detail_page.dart';
@@ -37,32 +38,33 @@ Future<void> saveAsset({
originalFile.deviceFolder ?? '',
newAsset,
);
newFile.creationTime = originalFile.creationTime;
newFile.collectionID = originalFile.collectionID;
newFile.location = originalFile.location;
if (!newFile.hasLocation && originalFile.localID != null) {
final assetEntity = await originalFile.getAsset;
if (assetEntity != null) {
final latLong = await assetEntity.latlngAsync();
newFile.location = Location(
latitude: latLong.latitude,
longitude: latLong.longitude,
);
}
await localDB.trackEdit(
newAsset.id,
originalFile.creationTime!,
originalFile.modificationTime!,
originalFile.location?.latitude,
originalFile.location?.longitude,
);
if (originalFile.collectionID != null) {
await localDB.insertOrUpdateQueue(
{newAsset.id},
originalFile.collectionID!,
Configuration.instance.getUserID()!,
);
}
newFile.generatedID = await FilesDB.instance.insertAndGetId(newFile);
Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave"));
unawaited(SyncService.instance.sync());
showShortToast(context, S.of(context).editsSaved);
logger.info("Original file " + originalFile.toString());
logger.info("Saved edits to file " + newFile.toString());
final files = detailPageConfig.files;
// the index could be -1 if the files fetched doesn't contain the newly
// edited files
int selectionIndex =
files.indexWhere((file) => file.generatedID == newFile.generatedID);
int selectionIndex = files.indexWhere((file) =>
originalFile.localID != null && file.localID == newFile.localID);
if (selectionIndex == -1) {
files.add(newFile);
selectionIndex = files.length - 1;
@@ -83,5 +85,9 @@ Future<void> saveAsset({
logger.severe(e, s);
} finally {
await PhotoManager.startChangeNotify();
Future.delayed(
const Duration(seconds: 2),
() => SyncService.instance.sync().ignore(),
);
}
}