2022-09-26 07:36:56 +00:00
|
|
|
import 'package:dio/dio.dart';
|
2023-12-14 00:45:48 +00:00
|
|
|
import "package:flutter/material.dart";
|
2023-12-12 10:16:34 +00:00
|
|
|
import "package:latlong2/latlong.dart";
|
2022-09-26 07:36:56 +00:00
|
|
|
import 'package:logging/logging.dart';
|
2022-11-14 08:33:49 +00:00
|
|
|
import 'package:path/path.dart';
|
|
|
|
import 'package:photos/core/configuration.dart';
|
2023-02-03 07:39:04 +00:00
|
|
|
import 'package:photos/core/network/network.dart';
|
2022-11-14 08:33:49 +00:00
|
|
|
import 'package:photos/db/files_db.dart';
|
|
|
|
import 'package:photos/extensions/list.dart';
|
2023-12-14 00:45:48 +00:00
|
|
|
import "package:photos/generated/l10n.dart";
|
2023-08-25 04:39:30 +00:00
|
|
|
import 'package:photos/models/file/file.dart';
|
2023-03-29 11:16:07 +00:00
|
|
|
import "package:photos/models/file_load_result.dart";
|
2023-05-25 05:51:56 +00:00
|
|
|
import "package:photos/models/metadata/file_magic.dart";
|
2022-11-14 08:33:49 +00:00
|
|
|
import 'package:photos/services/file_magic_service.dart';
|
2023-03-29 11:16:07 +00:00
|
|
|
import "package:photos/services/ignored_files_service.dart";
|
2023-12-14 00:45:48 +00:00
|
|
|
import "package:photos/ui/components/action_sheet_widget.dart";
|
|
|
|
import "package:photos/ui/components/buttons/button_widget.dart";
|
|
|
|
import "package:photos/ui/components/models/button_type.dart";
|
2022-11-14 08:33:49 +00:00
|
|
|
import 'package:photos/utils/date_time_util.dart';
|
2023-12-14 00:45:48 +00:00
|
|
|
import "package:photos/utils/exif_util.dart";
|
2022-09-26 07:36:56 +00:00
|
|
|
|
|
|
|
class FilesService {
|
2022-10-14 15:03:55 +00:00
|
|
|
late Dio _enteDio;
|
2022-09-26 07:36:56 +00:00
|
|
|
late Logger _logger;
|
2022-11-14 08:33:49 +00:00
|
|
|
late FilesDB _filesDB;
|
|
|
|
late Configuration _config;
|
|
|
|
|
2022-09-26 07:36:56 +00:00
|
|
|
FilesService._privateConstructor() {
|
2023-02-03 07:39:04 +00:00
|
|
|
_enteDio = NetworkClient.instance.enteDio;
|
2022-09-26 07:36:56 +00:00
|
|
|
_logger = Logger("FilesService");
|
2022-11-14 08:33:49 +00:00
|
|
|
_filesDB = FilesDB.instance;
|
|
|
|
_config = Configuration.instance;
|
2022-09-26 07:36:56 +00:00
|
|
|
}
|
2022-11-14 08:33:49 +00:00
|
|
|
|
2022-09-26 07:36:56 +00:00
|
|
|
static final FilesService instance = FilesService._privateConstructor();
|
|
|
|
|
|
|
|
Future<int> getFileSize(int uploadedFileID) async {
|
|
|
|
try {
|
2022-10-14 15:03:55 +00:00
|
|
|
final response = await _enteDio.post(
|
|
|
|
"/files/size",
|
2022-09-26 07:36:56 +00:00
|
|
|
data: {
|
2023-08-19 11:39:56 +00:00
|
|
|
"fileIDs": [uploadedFileID],
|
2022-09-26 07:36:56 +00:00
|
|
|
},
|
|
|
|
);
|
|
|
|
return response.data["size"];
|
|
|
|
} catch (e) {
|
|
|
|
_logger.severe(e);
|
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
2022-11-14 08:33:49 +00:00
|
|
|
|
2023-05-31 04:40:59 +00:00
|
|
|
Future<bool> hasMigratedSizes() async {
|
|
|
|
try {
|
|
|
|
final List<int> uploadIDsWithMissingSize =
|
|
|
|
await _filesDB.getUploadIDsWithMissingSize(_config.getUserID()!);
|
|
|
|
if (uploadIDsWithMissingSize.isEmpty) {
|
|
|
|
return Future.value(true);
|
|
|
|
}
|
2023-12-04 04:55:17 +00:00
|
|
|
await backFillSizes(uploadIDsWithMissingSize);
|
2023-05-31 04:40:59 +00:00
|
|
|
return Future.value(true);
|
|
|
|
} catch (e, s) {
|
2023-05-31 09:57:01 +00:00
|
|
|
_logger.severe("error during has migrated sizes", e, s);
|
2023-05-31 04:40:59 +00:00
|
|
|
return Future.value(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-04 04:55:17 +00:00
|
|
|
Future<void> backFillSizes(List<int> uploadIDsWithMissingSize) async {
|
|
|
|
final batchedFiles = uploadIDsWithMissingSize.chunks(1000);
|
|
|
|
for (final batch in batchedFiles) {
|
|
|
|
final Map<int, int> uploadIdToSize = await getFilesSizeFromInfo(batch);
|
|
|
|
await _filesDB.updateSizeForUploadIDs(uploadIdToSize);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-30 15:33:02 +00:00
|
|
|
Future<Map<int, int>> getFilesSizeFromInfo(List<int> uploadedFileID) async {
|
|
|
|
try {
|
|
|
|
final response = await _enteDio.post(
|
|
|
|
"/files/info",
|
2023-05-31 13:10:07 +00:00
|
|
|
data: {"fileIDs": uploadedFileID},
|
2023-05-30 15:33:02 +00:00
|
|
|
);
|
|
|
|
final Map<int, int> idToSize = {};
|
|
|
|
final List result = response.data["filesInfo"] as List;
|
|
|
|
for (var fileInfo in result) {
|
|
|
|
final int uploadedFileID = fileInfo["id"];
|
|
|
|
final int size = fileInfo["fileInfo"]["fileSize"];
|
|
|
|
idToSize[uploadedFileID] = size;
|
|
|
|
}
|
|
|
|
return idToSize;
|
2023-05-31 09:57:01 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
_logger.severe("failed to fetch size from fileInfo", e, s);
|
2023-05-30 15:33:02 +00:00
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-12 10:16:34 +00:00
|
|
|
Future<void> bulkEditLocationData(
|
|
|
|
List<EnteFile> files,
|
2023-12-14 00:45:48 +00:00
|
|
|
LatLng? location,
|
|
|
|
BuildContext context,
|
2023-12-12 10:16:34 +00:00
|
|
|
) async {
|
|
|
|
final List<EnteFile> uploadedFiles =
|
|
|
|
files.where((element) => element.uploadedFileID != null).toList();
|
|
|
|
|
|
|
|
final List<EnteFile> remoteFilesToUpdate = [];
|
|
|
|
final Map<int, Map<String, dynamic>> fileIDToUpdateMetadata = {};
|
|
|
|
|
2023-12-14 00:45:48 +00:00
|
|
|
if (location == null) {
|
|
|
|
await _processFilesForBulkRemoveLocation(
|
|
|
|
remoteFilesToUpdate,
|
|
|
|
uploadedFiles,
|
|
|
|
fileIDToUpdateMetadata,
|
|
|
|
context,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
for (EnteFile remoteFile in uploadedFiles) {
|
|
|
|
// discard files not owned by user and also dedupe already processed
|
|
|
|
// files
|
|
|
|
if (remoteFile.ownerID != _config.getUserID()! ||
|
|
|
|
fileIDToUpdateMetadata.containsKey(remoteFile.uploadedFileID!)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
remoteFilesToUpdate.add(remoteFile);
|
|
|
|
fileIDToUpdateMetadata[remoteFile.uploadedFileID!] = {
|
|
|
|
latKey: location.latitude,
|
|
|
|
longKey: location.longitude,
|
|
|
|
};
|
|
|
|
}
|
2023-12-12 10:16:34 +00:00
|
|
|
}
|
2023-12-14 00:45:48 +00:00
|
|
|
|
2023-12-12 10:16:34 +00:00
|
|
|
if (remoteFilesToUpdate.isNotEmpty) {
|
|
|
|
await FileMagicService.instance.updatePublicMagicMetadata(
|
|
|
|
remoteFilesToUpdate,
|
|
|
|
null,
|
|
|
|
metadataUpdateMap: fileIDToUpdateMetadata,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-14 00:45:48 +00:00
|
|
|
Future<void> _processFilesForBulkRemoveLocation(
|
|
|
|
List<EnteFile> remoteFilesToUpdate,
|
|
|
|
List<EnteFile> uploadedFiles,
|
|
|
|
Map<int, Map<String, dynamic>> fileIDToUpdateMetadata,
|
|
|
|
BuildContext context,
|
|
|
|
) async {
|
|
|
|
final filesWithOgLocation = <EnteFile>[];
|
|
|
|
final filesWithoutOgLocation = <EnteFile>[];
|
|
|
|
for (EnteFile remoteFile in uploadedFiles) {
|
|
|
|
// discard files not owned by user and also dedupe files
|
|
|
|
if (remoteFile.ownerID != _config.getUserID()! ||
|
|
|
|
filesWithoutOgLocation.any(
|
|
|
|
(file) => file.uploadedFileID == remoteFile.uploadedFileID,
|
|
|
|
) ||
|
|
|
|
filesWithOgLocation.any(
|
|
|
|
(file) => file.uploadedFileID == remoteFile.uploadedFileID,
|
|
|
|
)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
final exif = await getExif(remoteFile);
|
|
|
|
final locationFromExif = gpsDataFromExif(exif).toLocationObj();
|
|
|
|
locationFromExif == null
|
|
|
|
? filesWithoutOgLocation.add(remoteFile)
|
|
|
|
: filesWithOgLocation.add(remoteFile);
|
|
|
|
}
|
|
|
|
if (filesWithOgLocation.isEmpty && filesWithoutOgLocation.isEmpty) {
|
|
|
|
_logger.info(
|
|
|
|
"Skipping bulkRemoveLocation, no owned files to remove location data from",
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (filesWithoutOgLocation.isEmpty) {
|
|
|
|
remoteFilesToUpdate.addAll(filesWithOgLocation);
|
|
|
|
} else {
|
|
|
|
final buttons = <ButtonWidget>[
|
|
|
|
ButtonWidget(
|
|
|
|
labelText: "All items",
|
|
|
|
buttonType: ButtonType.neutral,
|
|
|
|
buttonSize: ButtonSize.large,
|
|
|
|
shouldStickToDarkTheme: true,
|
|
|
|
buttonAction: ButtonAction.first,
|
|
|
|
shouldSurfaceExecutionStates: true,
|
|
|
|
isInAlert: true,
|
|
|
|
onTap: () async {
|
|
|
|
remoteFilesToUpdate.addAll(
|
|
|
|
[...filesWithOgLocation, ...filesWithoutOgLocation],
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
ButtonWidget(
|
|
|
|
labelText: "Items with location data",
|
|
|
|
buttonType: ButtonType.neutral,
|
|
|
|
buttonSize: ButtonSize.large,
|
|
|
|
shouldStickToDarkTheme: true,
|
|
|
|
buttonAction: ButtonAction.first,
|
|
|
|
shouldSurfaceExecutionStates: true,
|
|
|
|
isInAlert: true,
|
|
|
|
onTap: () async {
|
|
|
|
remoteFilesToUpdate.addAll(filesWithOgLocation);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
ButtonWidget(
|
|
|
|
labelText: S.of(context).cancel,
|
|
|
|
buttonType: ButtonType.secondary,
|
|
|
|
buttonSize: ButtonSize.large,
|
|
|
|
shouldStickToDarkTheme: true,
|
|
|
|
buttonAction: ButtonAction.fourth,
|
|
|
|
isInAlert: true,
|
|
|
|
),
|
|
|
|
];
|
|
|
|
|
|
|
|
await showActionSheet(
|
|
|
|
context: context,
|
|
|
|
buttons: buttons,
|
|
|
|
title: "Choose items to reset",
|
|
|
|
body:
|
|
|
|
"Some items selected never had location data in the first place.\n\nResetting location data for such items will delete its custom location data",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var element in remoteFilesToUpdate) {
|
|
|
|
fileIDToUpdateMetadata[element.uploadedFileID!] = {
|
|
|
|
latKey: null,
|
|
|
|
longKey: null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-24 06:43:00 +00:00
|
|
|
// Note: this method is not used anywhere, but it is kept for future
|
|
|
|
// reference when we add bulk EditTime feature
|
2022-11-14 08:33:49 +00:00
|
|
|
Future<void> bulkEditTime(
|
2023-08-24 16:56:24 +00:00
|
|
|
List<EnteFile> files,
|
2022-11-14 08:33:49 +00:00
|
|
|
EditTimeSource source,
|
|
|
|
) async {
|
2023-08-24 16:56:24 +00:00
|
|
|
final ListMatch<EnteFile> result = files.splitMatch(
|
2022-11-14 08:33:49 +00:00
|
|
|
(element) => element.isUploaded,
|
|
|
|
);
|
2023-08-24 16:56:24 +00:00
|
|
|
final List<EnteFile> uploadedFiles = result.matched;
|
2022-11-14 08:33:49 +00:00
|
|
|
// editTime For LocalFiles
|
2023-08-24 16:56:24 +00:00
|
|
|
final List<EnteFile> localOnlyFiles = result.unmatched;
|
|
|
|
for (EnteFile localFile in localOnlyFiles) {
|
2022-11-14 08:41:42 +00:00
|
|
|
final timeResult = _parseTime(localFile, source);
|
2022-11-14 08:33:49 +00:00
|
|
|
if (timeResult != null) {
|
2022-11-14 08:41:42 +00:00
|
|
|
localFile.creationTime = timeResult;
|
2022-11-14 08:33:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
await _filesDB.insertMultiple(localOnlyFiles);
|
|
|
|
|
2023-08-24 16:56:24 +00:00
|
|
|
final List<EnteFile> remoteFilesToUpdate = [];
|
2022-11-14 08:33:49 +00:00
|
|
|
final Map<int, Map<String, int>> fileIDToUpdateMetadata = {};
|
2023-08-24 16:56:24 +00:00
|
|
|
for (EnteFile remoteFile in uploadedFiles) {
|
2022-11-14 08:33:49 +00:00
|
|
|
// discard files not owned by user and also dedupe already processed
|
|
|
|
// files
|
|
|
|
if (remoteFile.ownerID != _config.getUserID()! ||
|
2022-11-14 11:26:32 +00:00
|
|
|
fileIDToUpdateMetadata.containsKey(remoteFile.uploadedFileID!)) {
|
2022-11-14 08:33:49 +00:00
|
|
|
continue;
|
|
|
|
}
|
2022-11-14 08:41:42 +00:00
|
|
|
final timeResult = _parseTime(remoteFile, source);
|
2022-11-14 08:33:49 +00:00
|
|
|
if (timeResult != null) {
|
|
|
|
remoteFilesToUpdate.add(remoteFile);
|
|
|
|
fileIDToUpdateMetadata[remoteFile.uploadedFileID!] = {
|
2023-05-25 05:51:56 +00:00
|
|
|
editTimeKey: timeResult,
|
2022-11-14 08:33:49 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (remoteFilesToUpdate.isNotEmpty) {
|
|
|
|
await FileMagicService.instance.updatePublicMagicMetadata(
|
|
|
|
remoteFilesToUpdate,
|
|
|
|
null,
|
|
|
|
metadataUpdateMap: fileIDToUpdateMetadata,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2022-11-14 08:41:42 +00:00
|
|
|
|
2023-08-24 16:56:24 +00:00
|
|
|
int? _parseTime(EnteFile file, EditTimeSource source) {
|
2022-11-14 08:41:42 +00:00
|
|
|
assert(
|
|
|
|
source == EditTimeSource.fileName,
|
|
|
|
"edit source ${source.name} is not supported yet",
|
|
|
|
);
|
|
|
|
final timeResult = parseDateTimeFromFileNameV2(
|
|
|
|
basenameWithoutExtension(file.title ?? ""),
|
|
|
|
);
|
|
|
|
return timeResult?.microsecondsSinceEpoch;
|
|
|
|
}
|
2023-03-29 11:16:07 +00:00
|
|
|
|
|
|
|
Future<void> removeIgnoredFiles(Future<FileLoadResult> result) async {
|
2023-08-23 08:56:06 +00:00
|
|
|
final ignoredIDs = await IgnoredFilesService.instance.idToIgnoreReasonMap;
|
2023-03-29 11:16:07 +00:00
|
|
|
(await result).files.removeWhere(
|
|
|
|
(f) =>
|
|
|
|
f.uploadedFileID == null &&
|
|
|
|
IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, f),
|
|
|
|
);
|
|
|
|
}
|
2022-09-26 07:36:56 +00:00
|
|
|
}
|
2022-11-14 07:16:51 +00:00
|
|
|
|
|
|
|
enum EditTimeSource {
|
|
|
|
// parse the time from fileName
|
|
|
|
fileName,
|
|
|
|
// parse the time from exif data of file.
|
|
|
|
exif,
|
|
|
|
// use the which user provided as input
|
|
|
|
manualFix,
|
|
|
|
// adjust the time of selected photos by +/- time.
|
|
|
|
// required for cases when the original device in which photos were taken
|
|
|
|
// had incorrect time (quite common with physical cameras)
|
|
|
|
manualAdjusted,
|
|
|
|
}
|