diff --git a/lib/db/folder_db.dart b/lib/db/folder_db.dart new file mode 100644 index 000000000..aa5e44a4e --- /dev/null +++ b/lib/db/folder_db.dart @@ -0,0 +1,105 @@ +import 'dart:io'; + +import 'package:path/path.dart'; +import 'package:photos/models/folder.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:path_provider/path_provider.dart'; + +class FolderDB { + static final _databaseName = "ente.db"; + static final _databaseVersion = 1; + + static final table = 'folders'; + + static final columnId = 'id'; + static final columnName = 'name'; + static final columnOwner = 'owner'; + static final columnDeviceFolder = 'device_folder'; + static final columnSharedWith = 'shared_with'; + static final columnUpdateTimestamp = 'update_timestamp'; + + FolderDB._privateConstructor(); + static final FolderDB instance = FolderDB._privateConstructor(); + + static Database _database; + Future get database async { + if (_database != null) return _database; + _database = await _initDatabase(); + return _database; + } + + _initDatabase() async { + Directory documentsDirectory = await getApplicationDocumentsDirectory(); + String path = join(documentsDirectory.path, _databaseName); + return await openDatabase(path, + version: _databaseVersion, onCreate: _onCreate); + } + + Future _onCreate(Database db, int version) async { + await db.execute(''' + CREATE TABLE $table ( + $columnId INTEGER PRIMARY KEY NOT NULL, + $columnName TEXT NOT NULL, + $columnOwner TEXT NOT NULL, + $columnDeviceFolder TEXT NOT NULL, + $columnSharedWith TEXT NOT NULL, + $columnUpdateTimestamp INTEGER NOT NULL, + UNIQUE($columnOwner, $columnDeviceFolder) + ) + '''); + } + + Future putFolder(Folder folder) async { + final db = await instance.database; + return await db.insert(table, _getRowForFolder(folder), + conflictAlgorithm: ConflictAlgorithm.replace); + } + + Future> getFolders() async { + final db = await instance.database; + final results = await db.query( + table, + orderBy: '$columnUpdateTimestamp DESC', + ); + return _convertToFolders(results); + } + + Future deleteFolder(Folder folder) async { + final db = await instance.database; + return db.delete( + table, + where: '$columnId =?', + whereArgs: [folder.id], + ); + } + + List _convertToFolders(List> results) { + var folders = List(); + for (var result in results) { + folders.add(_getFolderFromRow(result)); + } + return folders; + } + + Map _getRowForFolder(Folder folder) { + var row = new Map(); + row[columnId] = folder.id; + row[columnName] = folder.name; + row[columnOwner] = folder.owner; + row[columnDeviceFolder] = folder.deviceFolder; + row[columnSharedWith] = folder.sharedWith.toString(); + row[columnUpdateTimestamp] = folder.updateTimestamp; + return row; + } + + Folder _getFolderFromRow(Map row) { + return Folder( + row[columnId], + row[columnName], + row[columnOwner], + row[columnDeviceFolder], + Set.from(row[columnSharedWith]), + row[columnUpdateTimestamp], + ); + } +} diff --git a/lib/db/photo_db.dart b/lib/db/photo_db.dart index e5ad77c46..74e490a8c 100644 --- a/lib/db/photo_db.dart +++ b/lib/db/photo_db.dart @@ -16,6 +16,7 @@ class PhotoDB { static final columnLocalId = 'local_id'; static final columnTitle = 'title'; static final columnDeviceFolder = 'device_folder'; + static final columnRemoteFolderId = 'remote_folder_id'; static final columnRemotePath = 'remote_path'; static final columnIsDeleted = 'is_deleted'; static final columnCreateTimestamp = 'create_timestamp'; @@ -51,6 +52,7 @@ class PhotoDB { $columnUploadedFileId INTEGER NOT NULL, $columnTitle TEXT NOT NULL, $columnDeviceFolder TEXT NOT NULL, + $columnRemoteFolderId INTEGER DEFAULT -1, $columnRemotePath TEXT, $columnIsDeleted INTEGER DEFAULT 0, $columnCreateTimestamp TEXT NOT NULL, @@ -175,6 +177,15 @@ class PhotoDB { ); } + Future deletePhotosInRemoteFolder(int folderId) async { + final db = await instance.database; + return db.delete( + table, + where: '$columnRemoteFolderId =?', + whereArgs: [folderId], + ); + } + Future> getDistinctPaths() async { final db = await instance.database; final rows = await db.query( @@ -205,6 +216,22 @@ class PhotoDB { } } + Future getLatestPhotoInRemoteFolder(int folderId) async { + final db = await instance.database; + var rows = await db.query( + table, + where: '$columnRemoteFolderId =?', + whereArgs: [folderId], + orderBy: '$columnCreateTimestamp DESC', + limit: 1, + ); + if (rows.isNotEmpty) { + return _getPhotoFromRow(rows[0]); + } else { + throw ("No photo found in path"); + } + } + Future getLatestPhotoAmongGeneratedIds( List generatedIds) async { final db = await instance.database; @@ -236,6 +263,7 @@ class PhotoDB { photo.uploadedFileId == null ? -1 : photo.uploadedFileId; row[columnTitle] = photo.title; row[columnDeviceFolder] = photo.deviceFolder; + row[columnRemoteFolderId] = photo.remoteFolderId; row[columnRemotePath] = photo.remotePath; row[columnCreateTimestamp] = photo.createTimestamp; row[columnSyncTimestamp] = photo.syncTimestamp; @@ -249,6 +277,7 @@ class PhotoDB { photo.uploadedFileId = row[columnUploadedFileId]; photo.title = row[columnTitle]; photo.deviceFolder = row[columnDeviceFolder]; + photo.remoteFolderId = row[columnRemoteFolderId]; photo.remotePath = row[columnRemotePath]; photo.createTimestamp = int.parse(row[columnCreateTimestamp]); photo.syncTimestamp = row[columnSyncTimestamp] == null diff --git a/lib/folder_service.dart b/lib/folder_service.dart index 550f8c2a7..bd8faf6fc 100644 --- a/lib/folder_service.dart +++ b/lib/folder_service.dart @@ -1,18 +1,77 @@ import 'dart:developer'; import 'package:dio/dio.dart'; +import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; +import 'package:photos/db/folder_db.dart'; +import 'package:photos/db/photo_db.dart'; import 'package:photos/models/folder.dart'; +import 'package:photos/models/photo.dart'; class FolderSharingService { + final _logger = Logger("FolderSharingService"); final _dio = Dio(); + static final _diffLimit = 100; FolderSharingService._privateConstructor(); static final FolderSharingService instance = FolderSharingService._privateConstructor(); void sync() { - // TODO + getFolders().then((f) async { + var folders = f.toSet(); + var currentFolders = await FolderDB.instance.getFolders(); + for (final currentFolder in currentFolders) { + if (!folders.contains(currentFolder)) { + await FolderDB.instance.deleteFolder(currentFolder); + await PhotoDB.instance.deletePhotosInRemoteFolder(currentFolder.id); + } + } + for (final folder in folders) { + await syncDiff(folder); + await FolderDB.instance.putFolder(folder); + } + }); + } + + Future syncDiff(Folder folder) async { + int lastSyncTimestamp = 0; + try { + Photo photo = + await PhotoDB.instance.getLatestPhotoInRemoteFolder(folder.id); + lastSyncTimestamp = photo.syncTimestamp; + } catch (e) { + // Folder has never been synced + } + var photos = await getDiff(folder.id, lastSyncTimestamp, _diffLimit); + await PhotoDB.instance.insertPhotos(photos); + if (photos.length == _diffLimit) { + await syncDiff(folder); + } + } + + Future> getDiff( + int folderId, int sinceTimestamp, int limit) async { + Response response = await _dio.get( + Configuration.instance.getHttpEndpoint() + + "/folders/diff" + + folderId.toString(), + options: + Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}), + queryParameters: { + "sinceTimestamp": sinceTimestamp, + "limit": limit, + }, + ).catchError((e) => _logger.severe(e)); + if (response != null) { + return (response.data["diff"] as List).map((p) { + Photo photo = new Photo.fromJson(p); + photo.remoteFolderId = folderId; + return photo; + }).toList(); + } else { + return null; + } } Future> getFolders() async { @@ -29,46 +88,46 @@ class FolderSharingService { }); } - Future> getSharingStatus(String path) async { + Future getFolder(String deviceFolder) async { + return _dio + .get( + Configuration.instance.getHttpEndpoint() + "/folders/folder/", + queryParameters: { + "deviceFolder": deviceFolder, + }, + options: Options( + headers: {"X-Auth-Token": Configuration.instance.getToken()}), + ) + .then((response) => Folder.fromMap(response.data)); + } + + Future> getSharingStatus(Folder folder) async { return _dio .get( Configuration.instance.getHttpEndpoint() + "/users", options: Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}), ) - .then((usersResponse) { - return getFolders().then((folders) { - Folder sharedFolder; - for (var folder in folders) { - if (folder.owner == Configuration.instance.getUsername() && - folder.deviceFolder == path) { - sharedFolder = folder; - break; - } + .then((response) { + final users = (response.data["users"] as List).toList(); + final result = Map(); + for (final user in users) { + if (user != Configuration.instance.getUsername()) { + result[user] = folder.sharedWith.contains(user); } - var sharedUsers = Set(); - if (sharedFolder != null) { - sharedUsers.addAll(sharedFolder.sharedWith); - } - final result = Map(); - (usersResponse.data["users"] as List).forEach((user) { - if (user != Configuration.instance.getUsername()) { - result[user] = sharedUsers.contains(user); - } - }); - return result; - }); + } + return result; }); } - Future shareFolder(String name, String path, Set users) { - var folder = Folder(0, name, Configuration.instance.getUsername(), path, - users.toList(), -1); + Future updateFolder(Folder folder) { + log("Updating folder: " + folder.toString()); return _dio .put(Configuration.instance.getHttpEndpoint() + "/folders/", options: Options( headers: {"X-Auth-Token": Configuration.instance.getToken()}), data: folder.toMap()) - .then((response) => log(response.toString())); + .then((response) => log(response.toString())) + .catchError((error) => log(error.toString())); } } diff --git a/lib/models/folder.dart b/lib/models/folder.dart index c759f40fd..81609dc6e 100644 --- a/lib/models/folder.dart +++ b/lib/models/folder.dart @@ -7,7 +7,7 @@ class Folder { final String name; final String owner; final String deviceFolder; - final List sharedWith; + final Set sharedWith; final int updateTimestamp; Folder( @@ -27,7 +27,7 @@ class Folder { map['name'], map['owner'], map['deviceFolder'], - List.from(map['sharedWith']), + Set.from(map['sharedWith']), map['updateTimestamp'], ); } @@ -43,7 +43,7 @@ class Folder { 'name': name, 'owner': owner, 'deviceFolder': deviceFolder, - 'sharedWith': sharedWith, + 'sharedWith': sharedWith.toList(), 'updateTimestamp': updateTimestamp, }; } @@ -61,7 +61,7 @@ class Folder { o.name == name && o.owner == owner && o.deviceFolder == deviceFolder && - listEquals(o.sharedWith, sharedWith) && + setEquals(o.sharedWith, sharedWith) && o.updateTimestamp == updateTimestamp; } diff --git a/lib/models/photo.dart b/lib/models/photo.dart index 332725cb1..b9a9e44b7 100644 --- a/lib/models/photo.dart +++ b/lib/models/photo.dart @@ -11,6 +11,7 @@ class Photo { String localId; String title; String deviceFolder; + int remoteFolderId; String remotePath; int createTimestamp; int syncTimestamp; diff --git a/lib/ui/share_folder_widget.dart b/lib/ui/share_folder_widget.dart index 22cb31a27..0b0c965bc 100644 --- a/lib/ui/share_folder_widget.dart +++ b/lib/ui/share_folder_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:photos/folder_service.dart'; +import 'package:photos/models/folder.dart'; import 'package:photos/ui/loading_widget.dart'; class ShareFolderWidget extends StatefulWidget { @@ -18,15 +19,18 @@ class ShareFolderWidget extends StatefulWidget { } class _ShareFolderWidgetState extends State { - Map _sharingStatus; + Folder _folder; @override Widget build(BuildContext context) { return FutureBuilder>( - future: FolderSharingService.instance.getSharingStatus(widget.path), + future: + FolderSharingService.instance.getFolder(widget.path).then((folder) { + _folder = folder; + return FolderSharingService.instance.getSharingStatus(folder); + }), builder: (context, snapshot) { if (snapshot.hasData) { - _sharingStatus = snapshot.data; return _getSharingDialog(snapshot.data); } else if (snapshot.hasError) { return Text(snapshot.error.toString()); @@ -52,16 +56,14 @@ class _ShareFolderWidgetState extends State { child: Text("Share"), onPressed: () async { var sharedWith = Set(); - for (var user in _sharingStatus.keys) { - if (_sharingStatus[user]) { + for (var user in sharingStatus.keys) { + if (sharingStatus[user]) { sharedWith.add(user); } } - await FolderSharingService.instance.shareFolder( - "namewa", - widget.path, - sharedWith, - ); + _folder.sharedWith.clear(); + _folder.sharedWith.addAll(sharedWith); + await FolderSharingService.instance.updateFolder(_folder); Navigator.of(context).pop(); }, ),