ente/lib/photo_sync_manager.dart

301 lines
9.9 KiB
Dart
Raw Normal View History

2020-03-30 14:28:46 +00:00
import 'dart:async';
import 'dart:io';
import 'dart:math';
2020-03-30 14:28:46 +00:00
2020-05-02 16:28:54 +00:00
import 'package:logging/logging.dart';
import 'package:photos/core/event_bus.dart';
2020-05-18 15:38:15 +00:00
import 'package:photos/db/photo_db.dart';
import 'package:photos/events/photo_upload_event.dart';
import 'package:photos/events/user_authenticated_event.dart';
2020-05-04 20:44:34 +00:00
import 'package:photos/photo_repository.dart';
2020-03-26 14:39:31 +00:00
import 'package:path_provider/path_provider.dart';
2020-03-24 19:59:36 +00:00
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/utils/file_name_util.dart';
2020-03-24 19:59:36 +00:00
import 'package:shared_preferences/shared_preferences.dart';
import 'package:dio/dio.dart';
import 'package:photos/models/photo.dart';
2020-03-26 14:39:31 +00:00
import 'package:photos/core/configuration.dart';
import 'package:photos/events/remote_sync_event.dart';
2020-04-30 15:09:41 +00:00
2020-03-24 19:59:36 +00:00
class PhotoSyncManager {
2020-05-02 16:28:54 +00:00
final _logger = Logger("PhotoSyncManager");
2020-03-28 13:56:06 +00:00
final _dio = Dio();
2020-05-18 15:38:15 +00:00
final _db = PhotoDB.instance;
2020-04-30 15:09:41 +00:00
bool _isSyncInProgress = false;
Future<void> _existingSync;
2020-04-27 13:02:29 +00:00
2020-03-28 13:56:06 +00:00
static final _lastSyncTimestampKey = "last_sync_timestamp_0";
static final _lastDBUpdateTimestampKey = "last_db_update_timestamp";
2020-05-17 10:18:09 +00:00
static final _diffLimit = 100;
2020-03-24 19:59:36 +00:00
2020-04-30 15:09:41 +00:00
PhotoSyncManager._privateConstructor() {
Bus.instance.on<UserAuthenticatedEvent>().listen((event) {
sync();
});
}
2020-04-27 13:02:29 +00:00
static final PhotoSyncManager instance =
PhotoSyncManager._privateConstructor();
2020-03-28 13:56:06 +00:00
2020-04-30 15:09:41 +00:00
Future<void> sync() async {
if (_isSyncInProgress) {
2020-05-02 16:28:54 +00:00
_logger.warning("Sync already in progress, skipping.");
return _existingSync;
2020-04-27 13:02:29 +00:00
}
2020-04-30 15:09:41 +00:00
_isSyncInProgress = true;
_existingSync = Future<void>(() async {
_logger.info("Syncing...");
try {
await _doSync();
} catch (e) {
throw e;
} finally {
_isSyncInProgress = false;
}
});
return _existingSync;
}
2020-04-30 15:09:41 +00:00
2020-06-15 19:57:48 +00:00
Future<bool> hasScannedDisk() async {
final prefs = await SharedPreferences.getInstance();
return prefs.containsKey(_lastDBUpdateTimestampKey);
}
Future<void> _doSync() async {
2020-04-11 22:29:09 +00:00
final prefs = await SharedPreferences.getInstance();
final syncStartTimestamp = DateTime.now().microsecondsSinceEpoch;
2020-03-28 13:56:06 +00:00
var lastDBUpdateTimestamp = prefs.getInt(_lastDBUpdateTimestampKey);
if (lastDBUpdateTimestamp == null) {
lastDBUpdateTimestamp = 0;
2020-04-11 22:29:09 +00:00
await _initializeDirectories();
2020-03-30 14:28:46 +00:00
}
2020-04-24 12:40:24 +00:00
2020-06-17 11:52:31 +00:00
final pathEntities =
await _getGalleryList(lastDBUpdateTimestamp, syncStartTimestamp);
2020-04-24 12:40:24 +00:00
final photos = List<Photo>();
AssetPathEntity recents;
2020-04-24 12:40:24 +00:00
for (AssetPathEntity pathEntity in pathEntities) {
if (pathEntity.name == "Recent" || pathEntity.name == "Recents") {
recents = pathEntity;
} else {
await _addToPhotos(pathEntity, lastDBUpdateTimestamp, photos);
2020-03-30 14:28:46 +00:00
}
2020-03-28 13:56:06 +00:00
}
if (recents != null) {
await _addToPhotos(recents, lastDBUpdateTimestamp, photos);
}
if (photos.isNotEmpty) {
2020-04-27 13:02:29 +00:00
photos.sort((first, second) =>
first.createTimestamp.compareTo(second.createTimestamp));
await _updateDatabase(
photos, prefs, lastDBUpdateTimestamp, syncStartTimestamp);
2020-06-17 18:38:49 +00:00
await PhotoRepository.instance.reloadPhotos();
2020-04-27 13:02:29 +00:00
}
await _syncWithRemote(prefs);
2020-04-24 12:40:24 +00:00
}
2020-06-17 11:52:31 +00:00
Future<List<AssetPathEntity>> _getGalleryList(
final int fromTimestamp, final int toTimestamp) async {
var result = await PhotoManager.requestPermission();
if (!result) {
print("Did not get permission");
}
final filterOptionGroup = FilterOptionGroup();
filterOptionGroup.setOption(AssetType.image, FilterOption(needTitle: true));
filterOptionGroup.dateTimeCond = DateTimeCond(
min: DateTime.fromMicrosecondsSinceEpoch(fromTimestamp),
max: DateTime.fromMicrosecondsSinceEpoch(toTimestamp),
);
var galleryList = await PhotoManager.getAssetPathList(
hasAll: true,
type: RequestType.image,
filterOption: filterOptionGroup,
);
galleryList.sort((s1, s2) {
return s2.assetCount.compareTo(s1.assetCount);
});
return galleryList;
}
Future _addToPhotos(AssetPathEntity pathEntity, int lastDBUpdateTimestamp,
List<Photo> photos) async {
final assetList = await pathEntity.assetList;
for (AssetEntity entity in assetList) {
if (max(entity.createDateTime.microsecondsSinceEpoch,
entity.modifiedDateTime.microsecondsSinceEpoch) >
lastDBUpdateTimestamp) {
try {
final photo = await Photo.fromAsset(pathEntity, entity);
if (!photos.contains(photo)) {
photos.add(photo);
}
} catch (e) {
_logger.severe(e);
}
}
}
}
Future<void> _syncWithRemote(SharedPreferences prefs) async {
// TODO: Fix race conditions triggered due to concurrent syncs.
// Add device_id/last_sync_timestamp to the upload request?
if (!Configuration.instance.hasConfiguredAccount()) {
return Future.error("Account not configured yet");
}
await _downloadDiff(prefs);
await _uploadDiff(prefs);
await _deletePhotosOnServer();
}
Future<bool> _updateDatabase(
final List<Photo> photos,
SharedPreferences prefs,
int lastDBUpdateTimestamp,
int syncStartTimestamp) async {
2020-04-24 12:40:24 +00:00
var photosToBeAdded = List<Photo>();
for (Photo photo in photos) {
if (photo.createTimestamp > lastDBUpdateTimestamp) {
photosToBeAdded.add(photo);
}
}
return await _insertPhotosToDB(photosToBeAdded, prefs, syncStartTimestamp);
2020-03-24 19:59:36 +00:00
}
Future<void> _downloadDiff(SharedPreferences prefs) async {
var diff = await _getDiff(_getLastSyncTimestamp(prefs), _diffLimit);
if (diff != null && diff.isNotEmpty) {
await _storeDiff(diff, prefs);
PhotoRepository.instance.reloadPhotos();
if (diff.length == _diffLimit) {
return await _downloadDiff(prefs);
}
}
2020-03-26 14:39:31 +00:00
}
int _getLastSyncTimestamp(SharedPreferences prefs) {
var lastSyncTimestamp = prefs.getInt(_lastSyncTimestampKey);
if (lastSyncTimestamp == null) {
lastSyncTimestamp = 0;
}
return lastSyncTimestamp;
}
Future<void> _uploadDiff(SharedPreferences prefs) async {
List<Photo> photosToBeUploaded = await _db.getPhotosToBeUploaded();
for (int i = 0; i < photosToBeUploaded.length; i++) {
Photo photo = photosToBeUploaded[i];
_logger.info("Uploading " + photo.toString());
2020-05-17 12:39:38 +00:00
try {
var uploadedPhoto = await _uploadFile(photo);
await _db.updatePhoto(photo.generatedId, uploadedPhoto.uploadedFileId,
uploadedPhoto.remotePath, uploadedPhoto.updateTimestamp);
prefs.setInt(_lastSyncTimestampKey, uploadedPhoto.updateTimestamp);
Bus.instance.fire(PhotoUploadEvent(
completed: i + 1, total: photosToBeUploaded.length));
2020-05-17 12:39:38 +00:00
} catch (e) {
Bus.instance.fire(PhotoUploadEvent(hasError: true));
throw e;
2020-04-13 15:01:27 +00:00
}
2020-03-26 14:39:31 +00:00
}
}
2020-05-17 12:39:38 +00:00
Future _storeDiff(List<Photo> diff, SharedPreferences prefs) async {
2020-03-28 13:56:06 +00:00
for (Photo photo in diff) {
try {
var existingPhoto = await _db.getMatchingPhoto(photo.localId,
photo.title, photo.deviceFolder, photo.createTimestamp,
alternateTitle: getHEICFileNameForJPG(photo));
await _db.updatePhoto(existingPhoto.generatedId, photo.uploadedFileId,
2020-05-26 19:33:29 +00:00
photo.remotePath, photo.updateTimestamp, photo.thumbnailPath);
} catch (e) {
await _db.insertPhoto(photo);
}
// _logger.info(
// "Setting update timestamp to " + photo.updateTimestamp.toString());
await prefs.setInt(_lastSyncTimestampKey, photo.updateTimestamp);
2020-03-28 13:56:06 +00:00
}
}
2020-03-26 14:39:31 +00:00
2020-05-17 10:18:09 +00:00
Future<List<Photo>> _getDiff(int lastSyncTimestamp, int limit) async {
2020-04-30 15:18:26 +00:00
Response response = await _dio.get(
2020-05-17 10:18:09 +00:00
Configuration.instance.getHttpEndpoint() + "/files/diff",
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
queryParameters: {
"sinceTimestamp": lastSyncTimestamp,
"limit": limit,
},
).catchError((e) => _logger.severe(e));
2020-03-30 15:08:50 +00:00
if (response != null) {
2020-04-30 15:09:41 +00:00
Bus.instance.fire(RemoteSyncEvent(true));
2020-03-30 15:08:50 +00:00
return (response.data["diff"] as List)
.map((photo) => new Photo.fromJson(photo))
.toList();
} else {
2020-04-30 15:09:41 +00:00
Bus.instance.fire(RemoteSyncEvent(false));
return null;
2020-03-30 15:08:50 +00:00
}
2020-03-24 19:59:36 +00:00
}
2020-04-12 12:38:49 +00:00
Future<Photo> _uploadFile(Photo localPhoto) async {
var title = getJPGFileNameForHEIC(localPhoto);
2020-03-24 19:59:36 +00:00
var formData = FormData.fromMap({
"file": MultipartFile.fromBytes((await localPhoto.getBytes()),
filename: title),
"deviceFileID": localPhoto.localId,
2020-05-17 12:39:38 +00:00
"deviceFolder": localPhoto.deviceFolder,
"title": title,
"createTimestamp": localPhoto.createTimestamp,
2020-03-24 19:59:36 +00:00
});
2020-04-13 15:01:27 +00:00
return _dio
2020-05-17 10:18:09 +00:00
.post(
2020-05-17 12:39:38 +00:00
Configuration.instance.getHttpEndpoint() + "/files",
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
data: formData,
)
.then((response) {
return Photo.fromJson(response.data);
});
2020-04-12 12:38:49 +00:00
}
Future<void> _deletePhotosOnServer() async {
return _db.getAllDeletedPhotos().then((deletedPhotos) async {
2020-04-12 12:38:49 +00:00
for (Photo deletedPhoto in deletedPhotos) {
await _deletePhotoOnServer(deletedPhoto);
await _db.deletePhoto(deletedPhoto);
2020-04-12 12:38:49 +00:00
}
});
}
Future<void> _deletePhotoOnServer(Photo photo) async {
2020-05-17 10:18:09 +00:00
return _dio
.delete(
Configuration.instance.getHttpEndpoint() +
"/files/" +
photo.uploadedFileId.toString(),
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
.catchError((e) => _logger.severe(e));
2020-03-29 14:04:26 +00:00
}
2020-04-11 22:29:09 +00:00
Future _initializeDirectories() async {
var externalPath = (await getApplicationDocumentsDirectory()).path;
new Directory(externalPath + "/photos/thumbnails")
.createSync(recursive: true);
}
Future<bool> _insertPhotosToDB(
List<Photo> photos, SharedPreferences prefs, int timestamp) async {
await _db.insertPhotos(photos);
2020-05-02 16:28:54 +00:00
_logger.info("Inserted " + photos.length.toString() + " photos.");
2020-04-11 22:29:09 +00:00
return await prefs.setInt(_lastDBUpdateTimestampKey, timestamp);
}
2020-03-24 19:59:36 +00:00
}