2020-03-30 14:28:46 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:io';
|
2020-05-05 20:42:21 +00:00
|
|
|
import 'dart:math';
|
2020-03-30 14:28:46 +00:00
|
|
|
|
2020-05-02 16:28:54 +00:00
|
|
|
import 'package:logging/logging.dart';
|
2020-05-04 20:08:20 +00:00
|
|
|
import 'package:photos/core/event_bus.dart';
|
2020-07-06 19:09:47 +00:00
|
|
|
import 'package:photos/db/file_db.dart';
|
2020-06-15 18:42:25 +00:00
|
|
|
import 'package:photos/events/photo_upload_event.dart';
|
2020-05-04 20:08:20 +00:00
|
|
|
import 'package:photos/events/user_authenticated_event.dart';
|
2020-06-19 23:03:26 +00:00
|
|
|
import 'package:photos/file_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';
|
2020-07-06 19:09:47 +00:00
|
|
|
import 'package:photos/models/file_type.dart';
|
2020-06-17 20:16:46 +00:00
|
|
|
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';
|
2020-06-19 23:03:26 +00:00
|
|
|
import 'package:photos/models/file.dart';
|
2020-03-26 14:39:31 +00:00
|
|
|
|
2020-05-04 20:08:20 +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-06-19 23:03:26 +00:00
|
|
|
final _db = FileDB.instance;
|
2020-04-30 15:09:41 +00:00
|
|
|
bool _isSyncInProgress = false;
|
2020-06-15 18:42:25 +00:00
|
|
|
Future<void> _existingSync;
|
2020-04-27 13:02:29 +00:00
|
|
|
|
2020-07-13 22:06:46 +00:00
|
|
|
static final _lastSyncTimeKey = "last_sync_time";
|
|
|
|
static final _lastDBUpdationTimeKey = "last_db_updation_time";
|
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.");
|
2020-06-15 18:42:25 +00:00
|
|
|
return _existingSync;
|
2020-04-27 13:02:29 +00:00
|
|
|
}
|
2020-04-30 15:09:41 +00:00
|
|
|
_isSyncInProgress = true;
|
2020-06-15 18:42:25 +00:00
|
|
|
_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();
|
2020-07-13 22:06:46 +00:00
|
|
|
return prefs.containsKey(_lastDBUpdationTimeKey);
|
2020-06-15 19:57:48 +00:00
|
|
|
}
|
|
|
|
|
2020-06-15 18:42:25 +00:00
|
|
|
Future<void> _doSync() async {
|
2020-04-11 22:29:09 +00:00
|
|
|
final prefs = await SharedPreferences.getInstance();
|
2020-07-13 22:06:46 +00:00
|
|
|
final syncStartTime = DateTime.now().microsecondsSinceEpoch;
|
|
|
|
var lastDBUpdationTime = prefs.getInt(_lastDBUpdationTimeKey);
|
|
|
|
if (lastDBUpdationTime == null) {
|
|
|
|
lastDBUpdationTime = 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 =
|
2020-07-13 22:06:46 +00:00
|
|
|
await _getGalleryList(lastDBUpdationTime, syncStartTime);
|
2020-06-19 23:03:26 +00:00
|
|
|
final files = List<File>();
|
2020-05-27 16:31:12 +00:00
|
|
|
AssetPathEntity recents;
|
2020-04-24 12:40:24 +00:00
|
|
|
for (AssetPathEntity pathEntity in pathEntities) {
|
2020-05-27 16:31:12 +00:00
|
|
|
if (pathEntity.name == "Recent" || pathEntity.name == "Recents") {
|
|
|
|
recents = pathEntity;
|
|
|
|
} else {
|
2020-07-13 22:06:46 +00:00
|
|
|
await _addToPhotos(pathEntity, lastDBUpdationTime, files);
|
2020-03-30 14:28:46 +00:00
|
|
|
}
|
2020-03-28 13:56:06 +00:00
|
|
|
}
|
2020-05-27 16:31:12 +00:00
|
|
|
if (recents != null) {
|
2020-07-13 22:06:46 +00:00
|
|
|
await _addToPhotos(recents, lastDBUpdationTime, files);
|
2020-05-27 16:31:12 +00:00
|
|
|
}
|
|
|
|
|
2020-06-19 23:03:26 +00:00
|
|
|
if (files.isNotEmpty) {
|
2020-07-06 19:09:47 +00:00
|
|
|
files.sort(
|
|
|
|
(first, second) => first.creationTime.compareTo(second.creationTime));
|
2020-06-15 18:42:25 +00:00
|
|
|
await _updateDatabase(
|
2020-07-13 22:06:46 +00:00
|
|
|
files, prefs, lastDBUpdationTime, syncStartTime);
|
2020-06-19 23:03:26 +00:00
|
|
|
await FileRepository.instance.reloadFiles();
|
2020-04-27 13:02:29 +00:00
|
|
|
}
|
2020-06-15 18:42:25 +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));
|
2020-06-19 23:03:26 +00:00
|
|
|
filterOptionGroup.setOption(AssetType.video, FilterOption(needTitle: true));
|
2020-06-17 11:52:31 +00:00
|
|
|
filterOptionGroup.dateTimeCond = DateTimeCond(
|
|
|
|
min: DateTime.fromMicrosecondsSinceEpoch(fromTimestamp),
|
|
|
|
max: DateTime.fromMicrosecondsSinceEpoch(toTimestamp),
|
|
|
|
);
|
|
|
|
var galleryList = await PhotoManager.getAssetPathList(
|
|
|
|
hasAll: true,
|
2020-06-19 23:03:26 +00:00
|
|
|
type: RequestType.common,
|
2020-06-17 11:52:31 +00:00
|
|
|
filterOption: filterOptionGroup,
|
|
|
|
);
|
|
|
|
|
|
|
|
galleryList.sort((s1, s2) {
|
|
|
|
return s2.assetCount.compareTo(s1.assetCount);
|
|
|
|
});
|
|
|
|
|
|
|
|
return galleryList;
|
|
|
|
}
|
|
|
|
|
2020-07-13 22:06:46 +00:00
|
|
|
Future _addToPhotos(AssetPathEntity pathEntity, int lastDBUpdationTime,
|
2020-06-19 23:03:26 +00:00
|
|
|
List<File> files) async {
|
2020-05-27 16:31:12 +00:00
|
|
|
final assetList = await pathEntity.assetList;
|
|
|
|
for (AssetEntity entity in assetList) {
|
|
|
|
if (max(entity.createDateTime.microsecondsSinceEpoch,
|
|
|
|
entity.modifiedDateTime.microsecondsSinceEpoch) >
|
2020-07-13 22:06:46 +00:00
|
|
|
lastDBUpdationTime) {
|
2020-05-27 16:31:12 +00:00
|
|
|
try {
|
2020-06-19 23:03:26 +00:00
|
|
|
final file = await File.fromAsset(pathEntity, entity);
|
|
|
|
if (!files.contains(file)) {
|
|
|
|
files.add(file);
|
2020-05-27 16:31:12 +00:00
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
_logger.severe(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-15 18:42:25 +00:00
|
|
|
Future<void> _syncWithRemote(SharedPreferences prefs) async {
|
2020-05-25 20:43:17 +00:00
|
|
|
// TODO: Fix race conditions triggered due to concurrent syncs.
|
|
|
|
// Add device_id/last_sync_timestamp to the upload request?
|
2020-05-28 10:19:25 +00:00
|
|
|
if (!Configuration.instance.hasConfiguredAccount()) {
|
2020-06-15 18:42:25 +00:00
|
|
|
return Future.error("Account not configured yet");
|
2020-05-28 10:19:25 +00:00
|
|
|
}
|
2020-06-15 18:42:25 +00:00
|
|
|
await _downloadDiff(prefs);
|
|
|
|
await _uploadDiff(prefs);
|
|
|
|
await _deletePhotosOnServer();
|
2020-05-25 20:43:17 +00:00
|
|
|
}
|
|
|
|
|
2020-06-19 23:03:26 +00:00
|
|
|
Future<bool> _updateDatabase(final List<File> files, SharedPreferences prefs,
|
2020-07-13 22:06:46 +00:00
|
|
|
int lastDBUpdationTime, int syncStartTimestamp) async {
|
2020-06-19 23:03:26 +00:00
|
|
|
var filesToBeAdded = List<File>();
|
|
|
|
for (File file in files) {
|
2020-07-13 22:06:46 +00:00
|
|
|
if (file.creationTime > lastDBUpdationTime) {
|
2020-06-19 23:03:26 +00:00
|
|
|
filesToBeAdded.add(file);
|
2020-04-24 12:40:24 +00:00
|
|
|
}
|
|
|
|
}
|
2020-06-19 23:03:26 +00:00
|
|
|
return await _insertFilesToDB(filesToBeAdded, prefs, syncStartTimestamp);
|
2020-03-24 19:59:36 +00:00
|
|
|
}
|
|
|
|
|
2020-05-25 20:43:17 +00:00
|
|
|
Future<void> _downloadDiff(SharedPreferences prefs) async {
|
2020-06-17 20:16:46 +00:00
|
|
|
var diff = await _getDiff(_getLastSyncTimestamp(prefs), _diffLimit);
|
2020-05-25 20:43:17 +00:00
|
|
|
if (diff != null && diff.isNotEmpty) {
|
|
|
|
await _storeDiff(diff, prefs);
|
2020-06-19 23:03:26 +00:00
|
|
|
FileRepository.instance.reloadFiles();
|
2020-05-25 20:43:17 +00:00
|
|
|
if (diff.length == _diffLimit) {
|
|
|
|
return await _downloadDiff(prefs);
|
2020-05-17 18:42:03 +00:00
|
|
|
}
|
|
|
|
}
|
2020-03-26 14:39:31 +00:00
|
|
|
}
|
|
|
|
|
2020-05-17 18:42:03 +00:00
|
|
|
int _getLastSyncTimestamp(SharedPreferences prefs) {
|
2020-07-13 22:06:46 +00:00
|
|
|
var lastSyncTimestamp = prefs.getInt(_lastSyncTimeKey);
|
2020-05-17 18:42:03 +00:00
|
|
|
if (lastSyncTimestamp == null) {
|
|
|
|
lastSyncTimestamp = 0;
|
|
|
|
}
|
|
|
|
return lastSyncTimestamp;
|
|
|
|
}
|
|
|
|
|
2020-06-15 18:42:25 +00:00
|
|
|
Future<void> _uploadDiff(SharedPreferences prefs) async {
|
2020-06-19 23:03:26 +00:00
|
|
|
List<File> photosToBeUploaded = await _db.getFilesToBeUploaded();
|
2020-06-15 18:42:25 +00:00
|
|
|
for (int i = 0; i < photosToBeUploaded.length; i++) {
|
2020-06-19 23:03:26 +00:00
|
|
|
File file = photosToBeUploaded[i];
|
2020-07-13 21:33:43 +00:00
|
|
|
if (file.fileType == FileType.video) {
|
|
|
|
continue;
|
|
|
|
}
|
2020-06-19 23:03:26 +00:00
|
|
|
_logger.info("Uploading " + file.toString());
|
2020-05-17 12:39:38 +00:00
|
|
|
try {
|
2020-06-19 23:03:26 +00:00
|
|
|
var uploadedFile = await _uploadFile(file);
|
|
|
|
await _db.update(file.generatedId, uploadedFile.uploadedFileId,
|
2020-07-08 20:51:36 +00:00
|
|
|
uploadedFile.updationTime);
|
2020-07-13 22:06:46 +00:00
|
|
|
prefs.setInt(_lastSyncTimeKey, uploadedFile.updationTime);
|
2020-06-15 18:42:25 +00:00
|
|
|
|
|
|
|
Bus.instance.fire(PhotoUploadEvent(
|
|
|
|
completed: i + 1, total: photosToBeUploaded.length));
|
2020-05-17 12:39:38 +00:00
|
|
|
} catch (e) {
|
2020-06-15 18:42:25 +00:00
|
|
|
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-06-19 23:03:26 +00:00
|
|
|
Future _storeDiff(List<File> diff, SharedPreferences prefs) async {
|
|
|
|
for (File file in diff) {
|
2020-05-17 18:42:03 +00:00
|
|
|
try {
|
2020-07-06 19:09:47 +00:00
|
|
|
var existingPhoto = await _db.getMatchingFile(file.localId, file.title,
|
|
|
|
file.deviceFolder, file.creationTime, file.modificationTime,
|
2020-06-19 23:03:26 +00:00
|
|
|
alternateTitle: getHEICFileNameForJPG(file));
|
2020-07-08 20:51:36 +00:00
|
|
|
await _db.update(
|
|
|
|
existingPhoto.generatedId, file.uploadedFileId, file.updationTime);
|
2020-05-17 18:42:03 +00:00
|
|
|
} catch (e) {
|
2020-06-19 23:03:26 +00:00
|
|
|
await _db.insert(file);
|
2020-05-17 18:42:03 +00:00
|
|
|
}
|
2020-07-13 22:06:46 +00:00
|
|
|
await prefs.setInt(_lastSyncTimeKey, file.updationTime);
|
2020-03-28 13:56:06 +00:00
|
|
|
}
|
|
|
|
}
|
2020-03-26 14:39:31 +00:00
|
|
|
|
2020-07-13 22:06:46 +00:00
|
|
|
Future<List<File>> _getDiff(int lastSyncTime, 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: {
|
2020-07-13 22:06:46 +00:00
|
|
|
"sinceTimestamp": lastSyncTime,
|
2020-05-17 10:18:09 +00:00
|
|
|
"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)
|
2020-06-19 23:03:26 +00:00
|
|
|
.map((file) => new File.fromJson(file))
|
2020-03-30 15:08:50 +00:00
|
|
|
.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-06-19 23:03:26 +00:00
|
|
|
Future<File> _uploadFile(File localPhoto) async {
|
2020-06-21 23:47:55 +00:00
|
|
|
final title = getJPGFileNameForHEIC(localPhoto);
|
|
|
|
final formData = FormData.fromMap({
|
|
|
|
"file": MultipartFile.fromFileSync(
|
|
|
|
(await (await localPhoto.getAsset()).originFile).path,
|
2020-06-12 16:54:58 +00:00
|
|
|
filename: title),
|
2020-05-17 14:50:50 +00:00
|
|
|
"deviceFileID": localPhoto.localId,
|
2020-05-17 12:39:38 +00:00
|
|
|
"deviceFolder": localPhoto.deviceFolder,
|
2020-06-12 16:54:58 +00:00
|
|
|
"title": title,
|
2020-07-06 19:09:47 +00:00
|
|
|
"creationTime": localPhoto.creationTime,
|
|
|
|
"modificationTime": localPhoto.modificationTime,
|
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) {
|
2020-06-19 23:03:26 +00:00
|
|
|
return File.fromJson(response.data);
|
2020-05-17 12:39:38 +00:00
|
|
|
});
|
2020-04-12 12:38:49 +00:00
|
|
|
}
|
|
|
|
|
2020-05-17 18:42:03 +00:00
|
|
|
Future<void> _deletePhotosOnServer() async {
|
2020-06-19 23:03:26 +00:00
|
|
|
return _db.getAllDeleted().then((deletedPhotos) async {
|
|
|
|
for (File deletedPhoto in deletedPhotos) {
|
|
|
|
await _deleteFileOnServer(deletedPhoto);
|
|
|
|
await _db.delete(deletedPhoto);
|
2020-04-12 12:38:49 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-06-19 23:03:26 +00:00
|
|
|
Future<void> _deleteFileOnServer(File file) async {
|
2020-05-17 10:18:09 +00:00
|
|
|
return _dio
|
|
|
|
.delete(
|
|
|
|
Configuration.instance.getHttpEndpoint() +
|
|
|
|
"/files/" +
|
2020-06-19 23:03:26 +00:00
|
|
|
file.uploadedFileId.toString(),
|
2020-05-17 10:18:09 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-06-19 23:03:26 +00:00
|
|
|
Future<bool> _insertFilesToDB(
|
|
|
|
List<File> files, SharedPreferences prefs, int timestamp) async {
|
|
|
|
await _db.insertMultiple(files);
|
|
|
|
_logger.info("Inserted " + files.length.toString() + " files.");
|
2020-07-13 22:06:46 +00:00
|
|
|
return await prefs.setInt(_lastDBUpdationTimeKey, timestamp);
|
2020-04-11 22:29:09 +00:00
|
|
|
}
|
2020-03-24 19:59:36 +00:00
|
|
|
}
|