ente/lib/photo_sync_manager.dart

219 lines
7.4 KiB
Dart
Raw Normal View History

2020-03-30 14:28:46 +00:00
import 'dart:async';
import 'dart:io';
2020-05-02 16:28:54 +00:00
import 'package:logging/logging.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/db_helper.dart';
import 'package:photos/events/user_authenticated_event.dart';
import 'package:photos/photo_loader.dart';
import 'package:photos/photo_provider.dart';
2020-03-26 14:39:31 +00:00
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
2020-03-24 19:59:36 +00:00
import 'package:photo_manager/photo_manager.dart';
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-04-30 15:09:41 +00:00
bool _isSyncInProgress = false;
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-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.");
2020-04-27 13:02:29 +00:00
return;
}
2020-04-30 15:09:41 +00:00
_isSyncInProgress = true;
2020-05-02 16:28:54 +00:00
_logger.info("Syncing...");
2020-04-30 15:09:41 +00:00
2020-04-11 22:29:09 +00:00
final prefs = await SharedPreferences.getInstance();
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-04-30 15:09:41 +00:00
await PhotoProvider.instance.refreshGalleryList();
final pathEntities = PhotoProvider.instance.list;
2020-04-24 12:40:24 +00:00
final photos = List<Photo>();
for (AssetPathEntity pathEntity in pathEntities) {
if (Platform.isIOS || pathEntity.name != "Recent") {
// "Recents" contain duplicate information on Android
var assetList = await pathEntity.assetList;
for (AssetEntity entity in assetList) {
if (entity.createDateTime.microsecondsSinceEpoch >
lastDBUpdateTimestamp) {
try {
photos.add(await Photo.fromAsset(pathEntity, entity));
} catch (e) {
2020-05-02 16:28:54 +00:00
_logger.severe(e);
2020-04-24 12:40:24 +00:00
}
}
2020-03-30 14:28:46 +00:00
}
}
2020-03-28 13:56:06 +00:00
}
2020-04-27 13:02:29 +00:00
if (photos.isEmpty) {
2020-04-30 15:09:41 +00:00
_isSyncInProgress = false;
_syncPhotos().then((_) {
_deletePhotos();
});
2020-04-27 13:02:29 +00:00
} else {
photos.sort((first, second) =>
first.createTimestamp.compareTo(second.createTimestamp));
_updateDatabase(photos, prefs, lastDBUpdateTimestamp).then((_) {
2020-04-30 15:09:41 +00:00
_isSyncInProgress = false;
_syncPhotos().then((_) {
_deletePhotos();
});
2020-04-24 12:40:24 +00:00
});
2020-04-27 13:02:29 +00:00
}
2020-04-24 12:40:24 +00:00
}
Future<bool> _updateDatabase(final List<Photo> photos,
SharedPreferences prefs, int lastDBUpdateTimestamp) async {
var photosToBeAdded = List<Photo>();
for (Photo photo in photos) {
if (photo.createTimestamp > lastDBUpdateTimestamp) {
photosToBeAdded.add(photo);
}
}
2020-04-11 22:29:09 +00:00
return await _insertPhotosToDB(
2020-04-24 12:40:24 +00:00
photosToBeAdded, prefs, DateTime.now().microsecondsSinceEpoch);
2020-03-24 19:59:36 +00:00
}
2020-03-28 13:56:06 +00:00
_syncPhotos() async {
2020-03-24 19:59:36 +00:00
SharedPreferences prefs = await SharedPreferences.getInstance();
2020-03-28 13:56:06 +00:00
var lastSyncTimestamp = prefs.getInt(_lastSyncTimestampKey);
2020-03-24 19:59:36 +00:00
if (lastSyncTimestamp == null) {
lastSyncTimestamp = 0;
}
2020-05-02 16:28:54 +00:00
_logger.info("Last sync timestamp: " + lastSyncTimestamp.toString());
2020-03-26 14:39:31 +00:00
2020-03-28 18:34:45 +00:00
_getDiff(lastSyncTimestamp).then((diff) {
2020-04-30 15:09:41 +00:00
if (diff != null) {
_downloadDiff(diff, prefs).then((_) {
_uploadDiff(prefs);
});
}
2020-03-28 18:34:45 +00:00
});
2020-03-26 14:39:31 +00:00
// TODO: Fix race conditions triggered due to concurrent syncs.
// Add device_id/last_sync_timestamp to the upload request?
}
2020-03-28 13:56:06 +00:00
Future _uploadDiff(SharedPreferences prefs) async {
List<Photo> photosToBeUploaded =
await DatabaseHelper.instance.getPhotosToBeUploaded();
for (Photo photo in photosToBeUploaded) {
2020-04-12 12:38:49 +00:00
var uploadedPhoto = await _uploadFile(photo);
2020-04-13 15:01:27 +00:00
if (uploadedPhoto == null) {
return;
}
2020-04-24 12:40:24 +00:00
await DatabaseHelper.instance.updatePhoto(photo.generatedId,
uploadedPhoto.remotePath, uploadedPhoto.syncTimestamp);
2020-03-28 13:56:06 +00:00
prefs.setInt(_lastSyncTimestampKey, uploadedPhoto.syncTimestamp);
2020-03-26 14:39:31 +00:00
}
}
2020-03-28 13:56:06 +00:00
Future _downloadDiff(List<Photo> diff, SharedPreferences prefs) async {
2020-03-27 19:54:24 +00:00
var externalPath = (await getApplicationDocumentsDirectory()).path;
2020-03-26 14:39:31 +00:00
var path = externalPath + "/photos/";
2020-03-28 13:56:06 +00:00
for (Photo photo in diff) {
2020-04-24 12:40:24 +00:00
var localPath = path + basename(photo.remotePath);
2020-04-13 11:28:01 +00:00
await _dio
2020-04-30 15:18:26 +00:00
.download(
Configuration.instance.getHttpEndpoint() + "/" + photo.remotePath,
localPath)
2020-05-02 16:28:54 +00:00
.catchError((e) => _logger.severe(e));
2020-04-24 12:40:24 +00:00
// TODO: Save path
photo.pathName = localPath;
2020-04-13 11:28:01 +00:00
await DatabaseHelper.instance.insertPhoto(photo);
PhotoLoader.instance.reloadPhotos();
2020-03-28 13:56:06 +00:00
await prefs.setInt(_lastSyncTimestampKey, photo.syncTimestamp);
}
}
2020-03-26 14:39:31 +00:00
2020-03-28 13:56:06 +00:00
Future<List<Photo>> _getDiff(int lastSyncTimestamp) async {
2020-04-30 15:18:26 +00:00
Response response = await _dio.get(
Configuration.instance.getHttpEndpoint() + "/diff",
2020-04-05 12:22:38 +00:00
queryParameters: {
2020-04-30 15:18:26 +00:00
"token": Configuration.instance.getToken(),
2020-04-05 12:22:38 +00:00
"lastSyncTimestamp": lastSyncTimestamp
2020-05-02 16:28:54 +00:00
}).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 {
2020-03-24 19:59:36 +00:00
var formData = FormData.fromMap({
"file": MultipartFile.fromBytes((await localPhoto.getOriginalBytes()),
filename: localPhoto.title),
"title": localPhoto.title,
"createTimestamp": localPhoto.createTimestamp,
2020-04-30 15:18:26 +00:00
"token": Configuration.instance.getToken(),
2020-03-24 19:59:36 +00:00
});
2020-04-13 15:01:27 +00:00
return _dio
2020-04-30 15:18:26 +00:00
.post(Configuration.instance.getHttpEndpoint() + "/upload",
data: formData)
2020-04-13 15:01:27 +00:00
.then((response) {
2020-05-02 16:28:54 +00:00
_logger.info(response.toString());
2020-04-12 12:38:49 +00:00
var photo = Photo.fromJson(response.data);
return photo;
2020-05-02 16:28:54 +00:00
}).catchError((e) => _logger.severe(e));
2020-04-12 12:38:49 +00:00
}
Future<void> _deletePhotos() async {
DatabaseHelper.instance.getAllDeletedPhotos().then((deletedPhotos) {
for (Photo deletedPhoto in deletedPhotos) {
_deletePhotoOnServer(deletedPhoto)
.then((value) => DatabaseHelper.instance.deletePhoto(deletedPhoto));
}
});
}
Future<void> _deletePhotoOnServer(Photo photo) async {
2020-04-30 15:18:26 +00:00
return _dio.post(Configuration.instance.getHttpEndpoint() + "/delete",
queryParameters: {
"token": Configuration.instance.getToken(),
"fileID": photo.uploadedFileId
2020-05-02 16:28:54 +00:00
}).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 DatabaseHelper.instance.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
PhotoLoader.instance.reloadPhotos();
return await prefs.setInt(_lastDBUpdateTimestampKey, timestamp);
}
2020-03-24 19:59:36 +00:00
}