From 9b1f09919cd7aece7bd03fe0d7d4d6e335d16913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 26 Jul 2024 11:21:57 +0200 Subject: [PATCH 01/23] WIP endpoints --- kDriveCore/Data/Api/Endpoint.swift | 59 +++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index 0b19f7f28..257d607f3 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -622,16 +622,6 @@ public extension Endpoint { return .driveInfo(drive: drive).appending(path: "/files/search", queryItems: queryItems) } - // MARK: Share link - - static func shareLinkFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/files/links") - } - - static func shareLink(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/link") - } - // MARK: Trash static func trash(drive: AbstractDrive) -> Endpoint { @@ -733,4 +723,53 @@ public extension Endpoint { static func sendUserInvitation(drive: AbstractDrive, id: Int) -> Endpoint { return .userInvitation(drive: drive, id: id).appending(path: "/send") } + + // MARK: Share Links + + static var shareUrlV2: Endpoint { + return Endpoint(hostKeypath: \.driveHost, path: "/2/app") + } + + static var shareUrlV3: Endpoint { + return Endpoint(hostKeypath: \.driveHost, path: "/3/app") + } + + static func shareLinkFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/files/links") + } + + static func shareLink(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/link") + } + + // Share link info + // fun getShareLinkInfo(driveld: Int, linkUvid: String) = "$SHARE_URL_V2$driveId/share/$linkUuid/init" + var shareLinkInfo(driveId: Int, linkUuid: String) -> Endpoint { + Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/init") + } + + // Share link file + // fun getShareLinkFile(driveld: Int, linkUvid: String, fileId: Int) = "$SHARE_URL_V3$driveId/share/$linkUuid/files/$fileId" + func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files/\(fileId)") + } + + // Share link file children + // fun getShareLinkFileChildren(driveld: Int, linkUuid: String, fileId: Int, sortType: SortType): String { + func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int /* , sortType: SortType */ ) -> Endpoint {} + + val orderQuery = "?order_by=${sortType.orderBy}&order=${sortType.order}" + return "$SHARE_URL_V3$driveId/share/$linkUvid/files/$fileId/files$orderQuery" + + // Share link file thumbnail + // fun getShareLinkFileThumbnail(driveld: Int, linkUvid: String, file: File): String { + + // Share mink file preview + // fun getShareLinkFilePreview(driveId: Int, linkUvid: String, file: File): String { + + // Download share link file + // fun downloadShareLinkFile(driveld: Int, linkUvid: String, file: File): String { + + // Share link file + // private fun shareLinkFile(driveld: Int, linkUvid: String, file: File): String { } From 00271f39a94bce0faa43a6a159ce01f61c4209f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 16 Aug 2024 09:37:14 +0200 Subject: [PATCH 02/23] feat: In memory realm for external share context --- .../DriveFileManager+Transactionable.swift | 5 +++++ .../Data/Cache/DriveFileManager/DriveFileManager.swift | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift index 8133f6d0b..3b3a4f967 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift @@ -29,6 +29,9 @@ public enum DriveFileManagerContext { /// Dedicated dataset to store shared with me case sharedWithMe + /// Dedicated in memory dataset for a public share link + case publicShare(shareId: String) + func realmURL(driveId: Int, driveUserId: Int) -> URL { switch self { case .drive: @@ -37,6 +40,8 @@ public enum DriveFileManagerContext { return DriveFileManager.constants.realmRootURL.appendingPathComponent("\(driveUserId)-shared.realm") case .fileProvider: return DriveFileManager.constants.realmRootURL.appendingPathComponent("\(driveUserId)-\(driveId)-fp.realm") + case .publicShare: + fatalError("noop") } } } diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index 47effd0f5..f801b66fb 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift @@ -84,8 +84,17 @@ public final class DriveFileManager { /// Build a realm configuration for a specific Drive public static func configuration(context: DriveFileManagerContext, driveId: Int, driveUserId: Int) -> Realm.Configuration { let realmURL = context.realmURL(driveId: driveId, driveUserId: driveUserId) + + let inMemoryIdentifier: String? + if case let .publicShare(identifier) = context { + inMemoryIdentifier = "inMemory:\(identifier)" + } else { + inMemoryIdentifier = nil + } + return Realm.Configuration( fileURL: realmURL, + inMemoryIdentifier: inMemoryIdentifier, schemaVersion: RealmSchemaVersion.drive, migrationBlock: { migration, oldSchemaVersion in let currentDriveSchemeVersion = RealmSchemaVersion.drive From 729fd5e3ecdfec00280d3805e5b6caf7636830bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 16 Aug 2024 09:39:18 +0200 Subject: [PATCH 03/23] fix: Project builds --- kDriveCore/Data/Api/Endpoint.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index 1057f552c..9230a7614 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -748,9 +748,9 @@ public extension Endpoint { // Share link info // fun getShareLinkInfo(driveld: Int, linkUvid: String) = "$SHARE_URL_V2$driveId/share/$linkUuid/init" - var shareLinkInfo(driveId: Int, linkUuid: String) -> Endpoint { - Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/init") - } +// var shareLinkInfo(driveId: Int, linkUuid: String) -> Endpoint { +// Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/init") +// } // Share link file // fun getShareLinkFile(driveld: Int, linkUvid: String, fileId: Int) = "$SHARE_URL_V3$driveId/share/$linkUuid/files/$fileId" @@ -760,10 +760,10 @@ public extension Endpoint { // Share link file children // fun getShareLinkFileChildren(driveld: Int, linkUuid: String, fileId: Int, sortType: SortType): String { - func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int /* , sortType: SortType */ ) -> Endpoint {} +// func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int /* , sortType: SortType */ ) -> Endpoint {} - val orderQuery = "?order_by=${sortType.orderBy}&order=${sortType.order}" - return "$SHARE_URL_V3$driveId/share/$linkUvid/files/$fileId/files$orderQuery" +// val orderQuery = "?order_by=${sortType.orderBy}&order=${sortType.order}" +// return "$SHARE_URL_V3$driveId/share/$linkUvid/files/$fileId/files$orderQuery" // Share link file thumbnail // fun getShareLinkFileThumbnail(driveld: Int, linkUvid: String, file: File): String { From 2d3828110df47acc20f3255d16d742b94e6a822d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 19 Aug 2024 15:06:23 +0200 Subject: [PATCH 04/23] feat: Public share metadata WIP --- kDrive/AppDelegate.swift | 12 ++++++ kDrive/SceneDelegate.swift | 14 ++++--- kDrive/Utils/UniversalLinksHelper.swift | 44 +++++++++++++++++++- kDriveCore/Data/Api/DriveApiFetcher.swift | 47 ++++++++++++++++++++++ kDriveCore/Data/Api/Endpoint.swift | 6 +-- kDriveCore/Data/Cache/AccountManager.swift | 33 +++++++++++++++ 6 files changed, 145 insertions(+), 11 deletions(-) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index 1615b37f8..d9f0a56dd 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -103,6 +103,18 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } application.registerForRemoteNotifications() + // swiftlint:disable force_try + Task { + try! await Task.sleep(nanoseconds:5_000_000_000) + print("coucou") + let somePublicShare = URL(string: "") + //await UIApplication.shared.open(somePublicShare!) // opens safari + + let components = URLComponents(url: somePublicShare!, resolvingAgainstBaseURL: true) + await UniversalLinksHelper.handlePath(components!.path) + } + + return true } diff --git a/kDrive/SceneDelegate.swift b/kDrive/SceneDelegate.swift index 543ecc087..3b9f90a9e 100644 --- a/kDrive/SceneDelegate.swift +++ b/kDrive/SceneDelegate.swift @@ -216,13 +216,15 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDel func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { Log.sceneDelegate("scene continue userActivity") - guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, - let incomingURL = userActivity.webpageURL, - let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { - return - } + Task { + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let incomingURL = userActivity.webpageURL, + let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { + return + } - UniversalLinksHelper.handlePath(components.path) + await UniversalLinksHelper.handlePath(components.path) + } } func scene(_ scene: UIScene, didFailToContinueUserActivityWithType userActivityType: String, error: Error) { diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 50b68adf5..10158ff9d 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -35,26 +35,44 @@ enum UniversalLinksHelper { regex: Regex(pattern: #"^/app/drive/([0-9]+)/redirect/([0-9]+)$"#)!, displayMode: .file ) + + /// Matches a public share link + static let publicShareLink = Link( + regex: Regex(pattern: #"^/app/share/([0-9]+)/([a-z0-9-]+)$"#)!, + displayMode: .file + ) + /// Matches a directory list link static let directoryLink = Link(regex: Regex(pattern: #"^/app/drive/([0-9]+)/files/([0-9]+)$"#)!, displayMode: .file) + /// Matches a file preview link static let filePreview = Link( regex: Regex(pattern: #"^/app/drive/([0-9]+)/files/([0-9]+/)?preview/[a-z]+/([0-9]+)$"#)!, displayMode: .file ) + /// Matches an office file link static let officeLink = Link(regex: Regex(pattern: #"^/app/office/([0-9]+)/([0-9]+)$"#)!, displayMode: .office) - static let all = [privateShareLink, directoryLink, filePreview, officeLink] + static let all = [privateShareLink, publicShareLink, directoryLink, filePreview, officeLink] } private enum DisplayMode { case office, file } - static func handlePath(_ path: String) -> Bool { + @discardableResult + static func handlePath(_ path: String) async -> Bool { DDLogInfo("[UniversalLinksHelper] Trying to open link with path: \(path)") + // Public share link regex + let shareLink = Link.publicShareLink + let matches = shareLink.regex.matches(in: path) + if await processPublicShareLink(matches: matches, displayMode: shareLink.displayMode) { + return true + } + + // Common regex for link in Link.all { let matches = link.regex.matches(in: path) if processRegex(matches: matches, displayMode: link.displayMode) { @@ -66,6 +84,28 @@ enum UniversalLinksHelper { return false } + private static func processPublicShareLink(matches: [[String]], displayMode: DisplayMode) async -> Bool { + @InjectService var accountManager: AccountManageable + + guard let firstMatch = matches.first, + let driveId = firstMatch[safe: 1], + let driveIdInt = Int(driveId), + let shareLinkUid = firstMatch[safe: 2] else { + return false + } + + // request metadata + guard let metadata = try? await PublicShareApiFetcher().getMetadata(driveId: driveIdInt, shareLinkUid: shareLinkUid) + else { + return false + } + + // get file ID from metadata + let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(for: shareLinkUid) + openFile(id: metadata.file_id, driveFileManager: publicShareDriveFileManager, office: displayMode == .office) + return true + } + private static func processRegex(matches: [[String]], displayMode: DisplayMode) -> Bool { @InjectService var accountManager: AccountManageable diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 9f4baaaf2..a95971d83 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -52,6 +52,53 @@ public class AuthenticatedImageRequestModifier: ImageDownloadRequestModifier { } } +public struct PublicShareMetadata: Decodable { + public let url: URL + public let fileId: Int + public let right: String + // public let validUntil: Date? + // public let capabilities: Rights + +// public let createdBy: TimeInterval +// public let createdAt: TimeInterval +// public let updatedAt: TimeInterval +// public let accessBlocked: Bool + + enum CodingKeys: String, CodingKey { + case url + case fileId = "file_id" + case right + // case validUntil = "valid_until" + // case capabilities +// case createdBy = "created_by" +// case createdAt = "created_at" +// case updatedAt = "updated_at" +// case accessBlocked = "access_blocked" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + url = try container.decode(URL.self, forKey: .url) + fileId = try container.decode(Int.self, forKey: .fileId) + right = try container.decode(String.self, forKey: .right) + } +} + +public class PublicShareApiFetcher: ApiFetcher { + override public init() { + super.init() + } + + public func getMetadata(driveId: Int, shareLinkUid: String) async throws -> PublicShareMetadata { + let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url + let request = Session.default.request(shareLinkInfoUrl) + let metadata: PublicShareMetadata = try await perform(request: request) + print("metadata\(metadata)") + return metadata + } +} + public class DriveApiFetcher: ApiFetcher { @LazyInjectService var accountManager: AccountManageable @LazyInjectService var tokenable: InfomaniakTokenable diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index 9230a7614..6d7156c01 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -748,9 +748,9 @@ public extension Endpoint { // Share link info // fun getShareLinkInfo(driveld: Int, linkUvid: String) = "$SHARE_URL_V2$driveId/share/$linkUuid/init" -// var shareLinkInfo(driveId: Int, linkUuid: String) -> Endpoint { -// Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/init") -// } + static func shareLinkInfo(driveId: Int, shareLinkUid: String) -> Endpoint { + Self.shareUrlV2.appending(path: "/\(driveId)/share/\(shareLinkUid)/init") + } // Share link file // fun getShareLinkFile(driveld: Int, linkUvid: String, fileId: Int) = "$SHARE_URL_V3$driveId/share/$linkUuid/files/$fileId" diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index 5e780e683..f58e1603e 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -24,6 +24,19 @@ import InfomaniakLogin import RealmSwift import Sentry +// TODO: Delete +public class SomeRefreshTokenDelegate: RefreshTokenDelegate { + public init() {} + + public func didUpdateToken(newToken: ApiToken, oldToken: ApiToken) { + print("noop") + } + + public func didFailRefreshToken(_ token: ApiToken) { + print("noop") + } +} + public protocol UpdateAccountDelegate: AnyObject { @MainActor func didUpdateCurrentAccountInformations(_ currentAccount: Account) } @@ -63,6 +76,10 @@ public protocol AccountManageable: AnyObject { func reloadTokensAndAccounts() func getDriveFileManager(for driveId: Int, userId: Int) -> DriveFileManager? func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager + + /// Create on the fly an "in memory" DriveFileManager for a specific share + func getInMemoryDriveFileManager(for publicShareId: String) -> DriveFileManager + func getApiFetcher(for userId: Int, token: ApiToken) -> DriveApiFetcher func getTokenForUserId(_ id: Int) -> ApiToken? func didUpdateToken(newToken: ApiToken, oldToken: ApiToken) @@ -194,6 +211,22 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { } } + public func getInMemoryDriveFileManager(for publicShareId: String) -> DriveFileManager { + if let inMemoryDriveFileManager = driveFileManagers[publicShareId] { + return inMemoryDriveFileManager + } + + // Big hack, refactor to allow for non authenticated requests + guard let someToken = apiFetchers.values.first?.currentToken else { + fatalError("probably no account availlable") + } + + let apiFetcher = DriveApiFetcher(token: someToken, delegate: SomeRefreshTokenDelegate()) + let context = DriveFileManagerContext.publicShare(shareId: publicShareId) + let noopDrive = Drive() + return DriveFileManager(drive: noopDrive, apiFetcher: apiFetcher, context: context) + } + public func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager { let userDrives = driveInfosManager.getDrives(for: userId) From e821a54e2f0c5ab3c1ced837f51508df62fbd3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 20 Aug 2024 14:23:55 +0200 Subject: [PATCH 05/23] feat(Rights): Updated to be compatible with public share --- .../DriveFileManagerConstants.swift | 2 +- kDriveCore/Data/Models/Rights.swift | 52 ++++++++++++++----- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift index aee4ac897..b7894640e 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift @@ -28,7 +28,7 @@ public enum RealmSchemaVersion { static let upload: UInt64 = 21 /// Current version of the Drive Realm - static let drive: UInt64 = 11 + static let drive: UInt64 = 12 } public class DriveFileManagerConstants { diff --git a/kDriveCore/Data/Models/Rights.swift b/kDriveCore/Data/Models/Rights.swift index 6d599a289..322db9750 100644 --- a/kDriveCore/Data/Models/Rights.swift +++ b/kDriveCore/Data/Models/Rights.swift @@ -44,7 +44,8 @@ public class Rights: EmbeddedObject, Codable { /// Right to use and give team access @Persisted public var canUseTeam: Bool - // Directory capabilities + // MARK: Directory capabilities + /// Right to add new child directory @Persisted public var canCreateDirectory: Bool /// Right to add new child file @@ -56,6 +57,21 @@ public class Rights: EmbeddedObject, Codable { /// Right to use convert directory into dropbox @Persisted public var canBecomeDropbox: Bool + // MARK: Public share + + /// Can edit + @Persisted public var canEdit: Bool + /// Can see stats + @Persisted public var canSeeStats: Bool + /// Can see info + @Persisted public var canSeeInfo: Bool + /// Can download + @Persisted public var canDownload: Bool + /// Can comment + @Persisted public var canComment: Bool + /// Can request access + @Persisted public var canRequestAccess: Bool + enum CodingKeys: String, CodingKey { case canShow case canRead @@ -73,26 +89,38 @@ public class Rights: EmbeddedObject, Codable { case canUpload case canMoveInto case canBecomeDropbox + case canEdit + case canSeeStats + case canSeeInfo + case canDownload + case canComment + case canRequestAccess } public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - canShow = try container.decode(Bool.self, forKey: .canShow) - canRead = try container.decode(Bool.self, forKey: .canRead) - canWrite = try container.decode(Bool.self, forKey: .canWrite) - canShare = try container.decode(Bool.self, forKey: .canShare) - canLeave = try container.decode(Bool.self, forKey: .canLeave) - canDelete = try container.decode(Bool.self, forKey: .canDelete) - canRename = try container.decode(Bool.self, forKey: .canRename) - canMove = try container.decode(Bool.self, forKey: .canMove) - canBecomeSharelink = try container.decode(Bool.self, forKey: .canBecomeSharelink) - canUseFavorite = try container.decode(Bool.self, forKey: .canUseFavorite) - canUseTeam = try container.decode(Bool.self, forKey: .canUseTeam) + canShow = try container.decodeIfPresent(Bool.self, forKey: .canShow) ?? true + canRead = try container.decodeIfPresent(Bool.self, forKey: .canRead) ?? true + canWrite = try container.decodeIfPresent(Bool.self, forKey: .canWrite) ?? false + canShare = try container.decodeIfPresent(Bool.self, forKey: .canShare) ?? false + canLeave = try container.decodeIfPresent(Bool.self, forKey: .canLeave) ?? false + canDelete = try container.decodeIfPresent(Bool.self, forKey: .canDelete) ?? false + canRename = try container.decodeIfPresent(Bool.self, forKey: .canRename) ?? false + canMove = try container.decodeIfPresent(Bool.self, forKey: .canMove) ?? false + canBecomeSharelink = try container.decodeIfPresent(Bool.self, forKey: .canBecomeSharelink) ?? false + canUseFavorite = try container.decodeIfPresent(Bool.self, forKey: .canUseFavorite) ?? false + canUseTeam = try container.decodeIfPresent(Bool.self, forKey: .canUseTeam) ?? false canCreateDirectory = try container.decodeIfPresent(Bool.self, forKey: .canCreateDirectory) ?? false canCreateFile = try container.decodeIfPresent(Bool.self, forKey: .canCreateFile) ?? false canUpload = try container.decodeIfPresent(Bool.self, forKey: .canUpload) ?? false canMoveInto = try container.decodeIfPresent(Bool.self, forKey: .canMoveInto) ?? false canBecomeDropbox = try container.decodeIfPresent(Bool.self, forKey: .canBecomeDropbox) ?? false + canEdit = try container.decodeIfPresent(Bool.self, forKey: .canEdit) ?? false + canSeeStats = try container.decodeIfPresent(Bool.self, forKey: .canSeeStats) ?? false + canSeeInfo = try container.decodeIfPresent(Bool.self, forKey: .canSeeInfo) ?? false + canDownload = try container.decodeIfPresent(Bool.self, forKey: .canDownload) ?? false + canComment = try container.decodeIfPresent(Bool.self, forKey: .canComment) ?? false + canRequestAccess = try container.decodeIfPresent(Bool.self, forKey: .canRequestAccess) ?? false } override public init() { From 254255e4b7baa5005a5fa5a07bf36fafa6ef5922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 20 Aug 2024 14:32:08 +0200 Subject: [PATCH 06/23] feat(PublicShareMetadata): Parsing pass --- kDrive/Utils/UniversalLinksHelper.swift | 2 +- kDriveCore/Data/Api/DriveApiFetcher.swift | 48 ++++++++++++------- .../DriveFileManager+Transactionable.swift | 5 +- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 10158ff9d..c4dc11fc9 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -102,7 +102,7 @@ enum UniversalLinksHelper { // get file ID from metadata let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(for: shareLinkUid) - openFile(id: metadata.file_id, driveFileManager: publicShareDriveFileManager, office: displayMode == .office) + openFile(id: metadata.fileId, driveFileManager: publicShareDriveFileManager, office: displayMode == .office) return true } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index a95971d83..49fca7c40 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -56,32 +56,47 @@ public struct PublicShareMetadata: Decodable { public let url: URL public let fileId: Int public let right: String - // public let validUntil: Date? - // public let capabilities: Rights -// public let createdBy: TimeInterval -// public let createdAt: TimeInterval -// public let updatedAt: TimeInterval -// public let accessBlocked: Bool + public let validUntil: TimeInterval? + public let capabilities: Rights + + public let createdBy: TimeInterval + public let createdAt: TimeInterval + public let updatedAt: TimeInterval + public let accessBlocked: Bool enum CodingKeys: String, CodingKey { case url - case fileId = "file_id" + case fileId case right - // case validUntil = "valid_until" - // case capabilities -// case createdBy = "created_by" -// case createdAt = "created_at" -// case updatedAt = "updated_at" -// case accessBlocked = "access_blocked" + case validUntil + case capabilities + case createdBy + case createdAt + case updatedAt + case accessBlocked } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - url = try container.decode(URL.self, forKey: .url) - fileId = try container.decode(Int.self, forKey: .fileId) - right = try container.decode(String.self, forKey: .right) + do { + url = try container.decode(URL.self, forKey: .url) + fileId = try container.decode(Int.self, forKey: .fileId) + right = try container.decode(String.self, forKey: .right) + + validUntil = try container.decodeIfPresent(TimeInterval.self, forKey: .validUntil) + capabilities = try container.decode(Rights.self, forKey: .capabilities) + + createdBy = try container.decode(TimeInterval.self, forKey: .createdBy) + createdAt = try container.decode(TimeInterval.self, forKey: .createdAt) + updatedAt = try container.decode(TimeInterval.self, forKey: .updatedAt) + + accessBlocked = try container.decode(Bool.self, forKey: .accessBlocked) + } catch { + // TODO: remove + fatalError("error:\(error)") + } } } @@ -94,7 +109,6 @@ public class PublicShareApiFetcher: ApiFetcher { let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url let request = Session.default.request(shareLinkInfoUrl) let metadata: PublicShareMetadata = try await perform(request: request) - print("metadata\(metadata)") return metadata } } diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift index 3b3a4f967..f733d2743 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift @@ -32,7 +32,7 @@ public enum DriveFileManagerContext { /// Dedicated in memory dataset for a public share link case publicShare(shareId: String) - func realmURL(driveId: Int, driveUserId: Int) -> URL { + func realmURL(driveId: Int, driveUserId: Int) -> URL? { switch self { case .drive: return DriveFileManager.constants.realmRootURL.appendingPathComponent("\(driveUserId)-\(driveId).realm") @@ -41,7 +41,8 @@ public enum DriveFileManagerContext { case .fileProvider: return DriveFileManager.constants.realmRootURL.appendingPathComponent("\(driveUserId)-\(driveId)-fp.realm") case .publicShare: - fatalError("noop") + // Public share are stored in memory only + return nil } } } From 30389e1dd933b12c9498fc2201ceb41d26c774d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 22 Aug 2024 18:21:25 +0200 Subject: [PATCH 07/23] refactor(Endpoint): Split files into dedicated extensiions feat(Endpoint): Finish the external links endpoint implementation --- kDrive/Utils/UniversalLinksHelper.swift | 15 +- kDriveCore/Data/Api/Endpoint+Files.swift | 306 ++++++++++++++++++ kDriveCore/Data/Api/Endpoint+Share.swift | 87 ++++++ kDriveCore/Data/Api/Endpoint+Trash.swift | 70 +++++ kDriveCore/Data/Api/Endpoint.swift | 379 +---------------------- 5 files changed, 478 insertions(+), 379 deletions(-) create mode 100644 kDriveCore/Data/Api/Endpoint+Files.swift create mode 100644 kDriveCore/Data/Api/Endpoint+Share.swift create mode 100644 kDriveCore/Data/Api/Endpoint+Trash.swift diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index c4dc11fc9..0c40cee6f 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -102,7 +102,7 @@ enum UniversalLinksHelper { // get file ID from metadata let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(for: shareLinkUid) - openFile(id: metadata.fileId, driveFileManager: publicShareDriveFileManager, office: displayMode == .office) + openPublicShare(id: metadata.fileId, driveFileManager: publicShareDriveFileManager) return true } @@ -123,6 +123,19 @@ enum UniversalLinksHelper { return true } + private static func openPublicShare(id: Int, driveFileManager: DriveFileManager) { + Task { + do { + let file = try await driveFileManager.file(id: id) + @InjectService var appNavigable: AppNavigable + await appNavigable.present(file: file, driveFileManager: driveFileManager, office: false) + } catch { + DDLogError("[UniversalLinksHelper] Failed to get file [\(driveFileManager.drive.id) - \(id)]: \(error)") + await UIConstants.showSnackBarIfNeeded(error: error) + } + } + } + private static func openFile(id: Int, driveFileManager: DriveFileManager, office: Bool) { Task { do { diff --git a/kDriveCore/Data/Api/Endpoint+Files.swift b/kDriveCore/Data/Api/Endpoint+Files.swift new file mode 100644 index 000000000..251ff0792 --- /dev/null +++ b/kDriveCore/Data/Api/Endpoint+Files.swift @@ -0,0 +1,306 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation +import InfomaniakCore +import RealmSwift + +// MARK: - Files + +public extension Endpoint { + // MARK: Dropbox + + static func dropboxes(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/dropboxes") + } + + static func dropbox(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/dropbox", queryItems: [ + URLQueryItem(name: "with", value: "user,capabilities") + ]) + } + + static func dropboxInvite(file: AbstractFile) -> Endpoint { + return .dropbox(file: file).appending(path: "/invite") + } + + // MARK: Favorite + + static func favorites(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/favorites", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func favorite(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/favorite") + } + + // MARK: File access + + static func invitation(drive: AbstractDrive, id: Int) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/files/invitations/\(id)") + } + + static func access(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/access", queryItems: [ + URLQueryItem(name: "with", value: "user"), + noAvatarDefault() + ]) + } + + static func checkAccess(file: AbstractFile) -> Endpoint { + return .access(file: file).appending(path: "/check") + } + + static func invitationsAccess(file: AbstractFile) -> Endpoint { + return .access(file: file).appending(path: "/invitations") + } + + static func teamsAccess(file: AbstractFile) -> Endpoint { + return .access(file: file).appending(path: "/teams") + } + + static func teamAccess(file: AbstractFile, id: Int) -> Endpoint { + return .teamsAccess(file: file).appending(path: "/\(id)") + } + + static func usersAccess(file: AbstractFile) -> Endpoint { + return .access(file: file).appending(path: "/users") + } + + static func userAccess(file: AbstractFile, id: Int) -> Endpoint { + return .usersAccess(file: file).appending(path: "/\(id)") + } + + static func forceAccess(file: AbstractFile) -> Endpoint { + return .access(file: file).appending(path: "/force") + } + + // MARK: File permission + + static func acl(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/acl") + } + + static func permissions(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/permission") + } + + static func userPermission(file: AbstractFile) -> Endpoint { + return .permissions(file: file).appending(path: "/user") + } + + static func teamPermission(file: AbstractFile) -> Endpoint { + return .permissions(file: file).appending(path: "/team") + } + + static func inheritPermission(file: AbstractFile) -> Endpoint { + return .permissions(file: file).appending(path: "/inherit") + } + + static func permission(file: AbstractFile, id: Int) -> Endpoint { + return .permissions(file: file).appending(path: "/\(id)") + } + + // MARK: File version + + static func versions(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/versions") + } + + static func version(file: AbstractFile, id: Int) -> Endpoint { + return .versions(file: file).appending(path: "/\(id)") + } + + static func downloadVersion(file: AbstractFile, id: Int) -> Endpoint { + return .version(file: file, id: id).appending(path: "/download") + } + + static func restoreVersion(file: AbstractFile, id: Int) -> Endpoint { + return .version(file: file, id: id).appending(path: "/restore") + } + + // MARK: File/directory + + static func file(_ file: AbstractFile) -> Endpoint { + return .driveInfo(drive: ProxyDrive(id: file.driveId)).appending(path: "/files/\(file.id)", + queryItems: [FileWith.fileExtra.toQueryItem()]) + } + + static func fileInfo(_ file: AbstractFile) -> Endpoint { + return .driveInfo(drive: ProxyDrive(id: file.driveId)).appending( + path: "/files/\(file.id)", + queryItems: [FileWith.fileExtra.toQueryItem(), noAvatarDefault()] + ) + } + + static func fileInfoV2(_ file: AbstractFile) -> Endpoint { + return .driveInfoV2(drive: ProxyDrive(id: file.driveId)).appending(path: "/files/\(file.id)", + queryItems: [FileWith.fileExtra.toQueryItem()]) + } + + static func files(of directory: AbstractFile) -> Endpoint { + return .fileInfo(directory).appending(path: "/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func createDirectory(in file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/directory", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func createFile(in file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/file", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func thumbnail(file: AbstractFile, at date: Date) -> Endpoint { + return .fileInfoV2(file).appending(path: "/thumbnail", queryItems: [ + URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") + ]) + } + + static func preview(file: AbstractFile, at date: Date) -> Endpoint { + return .fileInfoV2(file).appending(path: "/preview", queryItems: [ + URLQueryItem(name: "width", value: "2500"), + URLQueryItem(name: "height", value: "1500"), + URLQueryItem(name: "quality", value: "80"), + URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") + ]) + } + + static func download(file: AbstractFile, as asType: String? = nil) -> Endpoint { + let queryItems: [URLQueryItem]? + if let asType { + queryItems = [URLQueryItem(name: "as", value: asType)] + } else { + queryItems = nil + } + return .fileInfoV2(file).appending(path: "/download", queryItems: queryItems) + } + + static func convert(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/convert", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func move(file: AbstractFile, destination: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/move/\(destination.id)") + } + + static func duplicate(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/duplicate", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func copy(file: AbstractFile, destination: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/copy/\(destination.id)", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func rename(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/rename", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func count(of directory: AbstractFile) -> Endpoint { + return .fileInfoV2(directory).appending(path: "/count") + } + + static func size(file: AbstractFile, depth: String) -> Endpoint { + return .fileInfo(file).appending(path: "/size", queryItems: [ + URLQueryItem(name: "depth", value: depth) + ]) + } + + static func unlock(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/lock") + } + + static func directoryColor(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/color") + } + + // MARK: Root directory + + static func lockedFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/lock") + } + + static func rootFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/1/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func bulkFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/files/bulk") + } + + static func lastModifiedFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/last_modified", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func largestFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/largest") + } + + static func mostVersionedFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/most_versions") + } + + static func countByTypeFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/file_types") + } + + static func createTeamDirectory(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/team_directory", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func existFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/exists") + } + + static func sharedFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/shared") + } + + static func mySharedFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending( + path: "/files/my_shared", + queryItems: [(FileWith.fileMinimal + [.users]).toQueryItem(), noAvatarDefault()] + ) + } + + static func sharedWithMeFiles(drive: AbstractDrive) -> Endpoint { + return .driveV3.appending(path: "/files/shared_with_me", + queryItems: [(FileWith.fileMinimal).toQueryItem()]) + } + + static func countInRoot(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/count") + } + + // MARK: Listing + + static func fileListing(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/listing", queryItems: [FileWith.fileListingMinimal.toQueryItem()]) + } + + static func fileListingContinue(file: AbstractFile, cursor: String) -> Endpoint { + return .fileInfo(file).appending(path: "/listing/continue", queryItems: [URLQueryItem(name: "cursor", value: cursor), + FileWith.fileListingMinimal.toQueryItem()]) + } + + static func filePartialListing(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending( + path: "/files/listing/partial", + queryItems: [URLQueryItem(name: "with", value: "file")] + ) + } +} diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift new file mode 100644 index 000000000..f12998f60 --- /dev/null +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -0,0 +1,87 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation +import InfomaniakCore +import RealmSwift + +// MARK: - Share Links + +public extension Endpoint { + private static let sharedFileWithQuery = "with=capabilities,conversion_capabilities,supported_by" + + /// It is necessary to keep V1 here for backward compatibility of old links + static var shareUrlV1: Endpoint { + return Endpoint(hostKeypath: \.driveHost, path: "/app") + } + + static var shareUrlV2: Endpoint { + return Endpoint(hostKeypath: \.driveHost, path: "/2/app") + } + + static var shareUrlV3: Endpoint { + return Endpoint(hostKeypath: \.driveHost, path: "/3/app") + } + + static func shareLinkFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/files/links") + } + + static func shareLink(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/link") + } + + /// Share link info + static func shareLinkInfo(driveId: Int, shareLinkUid: String) -> Endpoint { + shareUrlV2.appending(path: "/\(driveId)/share/\(shareLinkUid)/init") + } + + /// Share link file + func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files/\(fileId)") + } + + /// Share link file children + func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int, sortType: SortType) -> Endpoint { + let orderQuery = "order_by=\(sortType.value.apiValue)&order=\(sortType.value.order)" + return Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files?\(Self.sharedFileWithQuery)&\(orderQuery)") + } + + /// Share link file thumbnail + func shareLinkFileThumbnail(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/thumbnail") + } + + /// Share mink file preview + func shareLinkFilePreview(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/preview") + } + + /// Download share link file + func downloadShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/download") + } + + func showOfficeShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + return Self.shareUrlV1.appending(path: "share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") + } + + func importShareLinkFiles(driveId: Int) -> Endpoint { + return Self.shareUrlV2.appending(path: "\(driveId)/imports/sharelink") + } +} diff --git a/kDriveCore/Data/Api/Endpoint+Trash.swift b/kDriveCore/Data/Api/Endpoint+Trash.swift new file mode 100644 index 000000000..d90a1a6d6 --- /dev/null +++ b/kDriveCore/Data/Api/Endpoint+Trash.swift @@ -0,0 +1,70 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation +import InfomaniakCore +import RealmSwift + +// MARK: - Trash + +public extension Endpoint { + static func trash(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/trash", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func trashV2(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/trash") + } + + static func emptyTrash(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/trash") + } + + static func trashCount(drive: AbstractDrive) -> Endpoint { + return .trash(drive: drive).appending(path: "/count") + } + + static func trashedInfo(file: AbstractFile) -> Endpoint { + return .trash(drive: ProxyDrive(id: file.driveId)).appending( + path: "/\(file.id)", + queryItems: [FileWith.fileExtra.toQueryItem(), noAvatarDefault()] + ) + } + + static func trashedInfoV2(file: AbstractFile) -> Endpoint { + return .trashV2(drive: ProxyDrive(id: file.driveId)).appending(path: "/\(file.id)") + } + + static func trashedFiles(of directory: AbstractFile) -> Endpoint { + return .trashedInfo(file: directory).appending(path: "/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func restore(file: AbstractFile) -> Endpoint { + return .trashedInfoV2(file: file).appending(path: "/restore") + } + + static func trashThumbnail(file: AbstractFile, at date: Date) -> Endpoint { + return .trashedInfoV2(file: file).appending(path: "/thumbnail", queryItems: [ + URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") + ]) + } + + static func trashCount(of directory: AbstractFile) -> Endpoint { + return .trashedInfo(file: directory).appending(path: "/count") + } +} diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index 6d7156c01..9485a410d 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -167,7 +167,7 @@ extension File: AbstractFile {} // MARK: - Endpoints public extension Endpoint { - private static var driveV3: Endpoint { + static var driveV3: Endpoint { return Endpoint(hostKeypath: \.apiDriveHost, path: "/3/drive") } @@ -195,24 +195,6 @@ public extension Endpoint { return .driveInfoV2(drive: drive).appending(path: "/cancel") } - // MARK: Listing - - static func fileListing(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/listing", queryItems: [FileWith.fileListingMinimal.toQueryItem()]) - } - - static func fileListingContinue(file: AbstractFile, cursor: String) -> Endpoint { - return .fileInfo(file).appending(path: "/listing/continue", queryItems: [URLQueryItem(name: "cursor", value: cursor), - FileWith.fileListingMinimal.toQueryItem()]) - } - - static func filePartialListing(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending( - path: "/files/listing/partial", - queryItems: [URLQueryItem(name: "with", value: "file")] - ) - } - // MARK: Activities static func recentActivity(drive: AbstractDrive) -> Endpoint { @@ -310,211 +292,6 @@ public extension Endpoint { return .driveInfo(drive: drive).appending(path: "/settings") } - // MARK: Dropbox - - static func dropboxes(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/dropboxes") - } - - static func dropbox(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/dropbox", queryItems: [ - URLQueryItem(name: "with", value: "user,capabilities") - ]) - } - - static func dropboxInvite(file: AbstractFile) -> Endpoint { - return .dropbox(file: file).appending(path: "/invite") - } - - // MARK: Favorite - - static func favorites(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/favorites", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func favorite(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/favorite") - } - - // MARK: File access - - static func invitation(drive: AbstractDrive, id: Int) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/files/invitations/\(id)") - } - - static func access(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/access", queryItems: [ - URLQueryItem(name: "with", value: "user"), - noAvatarDefault() - ]) - } - - static func checkAccess(file: AbstractFile) -> Endpoint { - return .access(file: file).appending(path: "/check") - } - - static func invitationsAccess(file: AbstractFile) -> Endpoint { - return .access(file: file).appending(path: "/invitations") - } - - static func teamsAccess(file: AbstractFile) -> Endpoint { - return .access(file: file).appending(path: "/teams") - } - - static func teamAccess(file: AbstractFile, id: Int) -> Endpoint { - return .teamsAccess(file: file).appending(path: "/\(id)") - } - - static func usersAccess(file: AbstractFile) -> Endpoint { - return .access(file: file).appending(path: "/users") - } - - static func userAccess(file: AbstractFile, id: Int) -> Endpoint { - return .usersAccess(file: file).appending(path: "/\(id)") - } - - static func forceAccess(file: AbstractFile) -> Endpoint { - return .access(file: file).appending(path: "/force") - } - - // MARK: File permission - - static func acl(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/acl") - } - - static func permissions(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/permission") - } - - static func userPermission(file: AbstractFile) -> Endpoint { - return .permissions(file: file).appending(path: "/user") - } - - static func teamPermission(file: AbstractFile) -> Endpoint { - return .permissions(file: file).appending(path: "/team") - } - - static func inheritPermission(file: AbstractFile) -> Endpoint { - return .permissions(file: file).appending(path: "/inherit") - } - - static func permission(file: AbstractFile, id: Int) -> Endpoint { - return .permissions(file: file).appending(path: "/\(id)") - } - - // MARK: File version - - static func versions(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/versions") - } - - static func version(file: AbstractFile, id: Int) -> Endpoint { - return .versions(file: file).appending(path: "/\(id)") - } - - static func downloadVersion(file: AbstractFile, id: Int) -> Endpoint { - return .version(file: file, id: id).appending(path: "/download") - } - - static func restoreVersion(file: AbstractFile, id: Int) -> Endpoint { - return .version(file: file, id: id).appending(path: "/restore") - } - - // MARK: File/directory - - static func file(_ file: AbstractFile) -> Endpoint { - return .driveInfo(drive: ProxyDrive(id: file.driveId)).appending(path: "/files/\(file.id)", - queryItems: [FileWith.fileExtra.toQueryItem()]) - } - - static func fileInfo(_ file: AbstractFile) -> Endpoint { - return .driveInfo(drive: ProxyDrive(id: file.driveId)).appending( - path: "/files/\(file.id)", - queryItems: [FileWith.fileExtra.toQueryItem(), noAvatarDefault()] - ) - } - - static func fileInfoV2(_ file: AbstractFile) -> Endpoint { - return .driveInfoV2(drive: ProxyDrive(id: file.driveId)).appending(path: "/files/\(file.id)", - queryItems: [FileWith.fileExtra.toQueryItem()]) - } - - static func files(of directory: AbstractFile) -> Endpoint { - return .fileInfo(directory).appending(path: "/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func createDirectory(in file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/directory", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func createFile(in file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/file", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func thumbnail(file: AbstractFile, at date: Date) -> Endpoint { - return .fileInfoV2(file).appending(path: "/thumbnail", queryItems: [ - URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") - ]) - } - - static func preview(file: AbstractFile, at date: Date) -> Endpoint { - return .fileInfoV2(file).appending(path: "/preview", queryItems: [ - URLQueryItem(name: "width", value: "2500"), - URLQueryItem(name: "height", value: "1500"), - URLQueryItem(name: "quality", value: "80"), - URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") - ]) - } - - static func download(file: AbstractFile, as asType: String? = nil) -> Endpoint { - let queryItems: [URLQueryItem]? - if let asType { - queryItems = [URLQueryItem(name: "as", value: asType)] - } else { - queryItems = nil - } - return .fileInfoV2(file).appending(path: "/download", queryItems: queryItems) - } - - static func convert(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/convert", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func move(file: AbstractFile, destination: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/move/\(destination.id)") - } - - static func duplicate(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/duplicate", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func copy(file: AbstractFile, destination: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/copy/\(destination.id)", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func rename(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/rename", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func count(of directory: AbstractFile) -> Endpoint { - return .fileInfoV2(directory).appending(path: "/count") - } - - static func size(file: AbstractFile, depth: String) -> Endpoint { - return .fileInfo(file).appending(path: "/size", queryItems: [ - URLQueryItem(name: "depth", value: depth) - ]) - } - - static func unlock(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/lock") - } - - static func directoryColor(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/color") - } - // MARK: - Import static func cancelImport(drive: AbstractDrive, id: Int) -> Endpoint { @@ -531,64 +308,6 @@ public extension Endpoint { return .driveInfo(drive: drive).appending(path: "/preference") } - // MARK: Root directory - - static func lockedFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/lock") - } - - static func rootFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/1/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func bulkFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/files/bulk") - } - - static func lastModifiedFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/last_modified", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func largestFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/largest") - } - - static func mostVersionedFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/most_versions") - } - - static func countByTypeFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/file_types") - } - - static func createTeamDirectory(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/team_directory", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func existFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/exists") - } - - static func sharedFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/shared") - } - - static func mySharedFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending( - path: "/files/my_shared", - queryItems: [(FileWith.fileMinimal + [.users]).toQueryItem(), noAvatarDefault()] - ) - } - - static func sharedWithMeFiles(drive: AbstractDrive) -> Endpoint { - return .driveV3.appending(path: "/files/shared_with_me", - queryItems: [(FileWith.fileMinimal).toQueryItem()]) - } - - static func countInRoot(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/count") - } - // MARK: Search static func search( @@ -626,53 +345,6 @@ public extension Endpoint { return .driveInfo(drive: drive).appending(path: "/files/search", queryItems: queryItems) } - // MARK: Trash - - static func trash(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/trash", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func trashV2(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/trash") - } - - static func emptyTrash(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/trash") - } - - static func trashCount(drive: AbstractDrive) -> Endpoint { - return .trash(drive: drive).appending(path: "/count") - } - - static func trashedInfo(file: AbstractFile) -> Endpoint { - return .trash(drive: ProxyDrive(id: file.driveId)).appending( - path: "/\(file.id)", - queryItems: [FileWith.fileExtra.toQueryItem(), noAvatarDefault()] - ) - } - - static func trashedInfoV2(file: AbstractFile) -> Endpoint { - return .trashV2(drive: ProxyDrive(id: file.driveId)).appending(path: "/\(file.id)") - } - - static func trashedFiles(of directory: AbstractFile) -> Endpoint { - return .trashedInfo(file: directory).appending(path: "/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func restore(file: AbstractFile) -> Endpoint { - return .trashedInfoV2(file: file).appending(path: "/restore") - } - - static func trashThumbnail(file: AbstractFile, at date: Date) -> Endpoint { - return .trashedInfoV2(file: file).appending(path: "/thumbnail", queryItems: [ - URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") - ]) - } - - static func trashCount(of directory: AbstractFile) -> Endpoint { - return .trashedInfo(file: directory).appending(path: "/count") - } - // MARK: Upload // Direct Upload @@ -727,53 +399,4 @@ public extension Endpoint { static func sendUserInvitation(drive: AbstractDrive, id: Int) -> Endpoint { return .userInvitation(drive: drive, id: id).appending(path: "/send") } - - // MARK: Share Links - - static var shareUrlV2: Endpoint { - return Endpoint(hostKeypath: \.driveHost, path: "/2/app") - } - - static var shareUrlV3: Endpoint { - return Endpoint(hostKeypath: \.driveHost, path: "/3/app") - } - - static func shareLinkFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/files/links") - } - - static func shareLink(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/link") - } - - // Share link info - // fun getShareLinkInfo(driveld: Int, linkUvid: String) = "$SHARE_URL_V2$driveId/share/$linkUuid/init" - static func shareLinkInfo(driveId: Int, shareLinkUid: String) -> Endpoint { - Self.shareUrlV2.appending(path: "/\(driveId)/share/\(shareLinkUid)/init") - } - - // Share link file - // fun getShareLinkFile(driveld: Int, linkUvid: String, fileId: Int) = "$SHARE_URL_V3$driveId/share/$linkUuid/files/$fileId" - func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files/\(fileId)") - } - - // Share link file children - // fun getShareLinkFileChildren(driveld: Int, linkUuid: String, fileId: Int, sortType: SortType): String { -// func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int /* , sortType: SortType */ ) -> Endpoint {} - -// val orderQuery = "?order_by=${sortType.orderBy}&order=${sortType.order}" -// return "$SHARE_URL_V3$driveId/share/$linkUvid/files/$fileId/files$orderQuery" - - // Share link file thumbnail - // fun getShareLinkFileThumbnail(driveld: Int, linkUvid: String, file: File): String { - - // Share mink file preview - // fun getShareLinkFilePreview(driveId: Int, linkUvid: String, file: File): String { - - // Download share link file - // fun downloadShareLinkFile(driveld: Int, linkUvid: String, file: File): String { - - // Share link file - // private fun shareLinkFile(driveld: Int, linkUvid: String, file: File): String { } From 4dfc1770c7e1b441c59243a1bd64ccf18b26c116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 29 Aug 2024 17:28:03 +0200 Subject: [PATCH 08/23] feat: Fetch root folder for a public share --- kDrive/AppRouter.swift | 15 ++++++++++++ .../UI/Controller/Files/FilePresenter.swift | 20 ++++++++++++++++ kDrive/Utils/UniversalLinksHelper.swift | 23 ++++++++++++++----- kDriveCore/Data/Api/DriveApiFetcher.swift | 7 ++++++ kDriveCore/Data/Api/Endpoint+Share.swift | 10 ++++---- kDriveCore/Utils/AppNavigable.swift | 3 +++ 6 files changed, 67 insertions(+), 11 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 68f9498dd..5eb8eef0f 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -537,6 +537,21 @@ public struct AppRouter: AppNavigable { // MARK: RouterFileNavigable + @MainActor public func presentPublicShare(rootFolder: File, driveFileManager: DriveFileManager) { + // TODO: Present on top of existing views + guard let window, + let rootViewController = window.rootViewController else { + fatalError("TODO: lazy load a rootViewController") + } + + let filePresenter = FilePresenter(viewController: rootViewController) + filePresenter.presentPublicShareDirectory( + rootFolder: rootFolder, + rootViewController: rootViewController, + driveFileManager: driveFileManager + ) + } + @MainActor public func present(file: File, driveFileManager: DriveFileManager) { present(file: file, driveFileManager: driveFileManager, office: false) } diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index 7403b4bb5..d22004a5c 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -132,6 +132,26 @@ final class FilePresenter { } } + public func presentPublicShareDirectory( + rootFolder: File, + rootViewController: UIViewController, + driveFileManager: DriveFileManager + ) { + let viewModel: FileListViewModel // TODO: use InMemoryFileListViewModel + viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: rootFolder) + + // TODO: Fix access right +// guard !rootFolder.isDisabled else { +// return +// } + + let nextVC = FileListViewController(viewModel: viewModel) + print("nextVC:\(nextVC) viewModel:\(viewModel) navigationController:\(navigationController)") +// navigationController?.pushViewController(nextVC, animated: true) + + rootViewController.present(nextVC, animated: true) + } + public func presentDirectory( for file: File, driveFileManager: DriveFileManager, diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 0c40cee6f..720e4a882 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -95,14 +95,19 @@ enum UniversalLinksHelper { } // request metadata - guard let metadata = try? await PublicShareApiFetcher().getMetadata(driveId: driveIdInt, shareLinkUid: shareLinkUid) + let apiFetcher = PublicShareApiFetcher() + guard let metadata = try? await apiFetcher.getMetadata(driveId: driveIdInt, shareLinkUid: shareLinkUid) else { return false } // get file ID from metadata let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(for: shareLinkUid) - openPublicShare(id: metadata.fileId, driveFileManager: publicShareDriveFileManager) + openPublicShare(driveId: driveIdInt, + linkUuid: shareLinkUid, + fileId: metadata.fileId, + driveFileManager: publicShareDriveFileManager, + apiFetcher: apiFetcher) return true } @@ -123,14 +128,20 @@ enum UniversalLinksHelper { return true } - private static func openPublicShare(id: Int, driveFileManager: DriveFileManager) { + private static func openPublicShare(driveId: Int, + linkUuid: String, + fileId: Int, + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher) { Task { do { - let file = try await driveFileManager.file(id: id) + let rootFolder = try await apiFetcher.getShareLinkFile(driveId: driveId, + linkUuid: linkUuid, + fileId: fileId) @InjectService var appNavigable: AppNavigable - await appNavigable.present(file: file, driveFileManager: driveFileManager, office: false) + await appNavigable.presentPublicShare(rootFolder: rootFolder, driveFileManager: driveFileManager) } catch { - DDLogError("[UniversalLinksHelper] Failed to get file [\(driveFileManager.drive.id) - \(id)]: \(error)") + DDLogError("[UniversalLinksHelper] Failed to get public folder [driveId:\(driveId) linkUuid:\(linkUuid) fileId:\(fileId)]: \(error)") await UIConstants.showSnackBarIfNeeded(error: error) } } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 919bfe871..64a204d02 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -111,6 +111,13 @@ public class PublicShareApiFetcher: ApiFetcher { let metadata: PublicShareMetadata = try await perform(request: request) return metadata } + + public func getShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) async throws -> File { + let shareLinkFileUrl = Endpoint.shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).url + let request = Session.default.request(shareLinkFileUrl) + let shareLinkFile: File = try await perform(request: request) + return shareLinkFile + } } public class DriveApiFetcher: ApiFetcher { diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index f12998f60..11dbed94b 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -52,8 +52,8 @@ public extension Endpoint { } /// Share link file - func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files/\(fileId)") + static func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + Self.shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") } /// Share link file children @@ -63,17 +63,17 @@ public extension Endpoint { } /// Share link file thumbnail - func shareLinkFileThumbnail(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + static func shareLinkFileThumbnail(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/thumbnail") } /// Share mink file preview - func shareLinkFilePreview(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + static func shareLinkFilePreview(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/preview") } /// Download share link file - func downloadShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + static func downloadShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/download") } diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index eeed66e66..a36e22cde 100644 --- a/kDriveCore/Utils/AppNavigable.swift +++ b/kDriveCore/Utils/AppNavigable.swift @@ -56,6 +56,9 @@ public protocol RouterFileNavigable { /// - office: Open in only office @MainActor func present(file: File, driveFileManager: DriveFileManager, office: Bool) + /// Present a file list for a public share + @MainActor func presentPublicShare(rootFolder: File, driveFileManager: DriveFileManager) + /// Present a list of files from a folder /// - Parameters: /// - frozenFolder: Folder to display From 638b423cbad2d8541281805df48862583833b70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 2 Sep 2024 15:29:39 +0200 Subject: [PATCH 09/23] feat: Cursored public share children query --- kDrive/AppRouter.swift | 17 +++-- .../UI/Controller/Files/FilePresenter.swift | 13 ++-- .../SharedWithMe/SharedWithMeViewModel.swift | 63 +++++++++++++++++++ kDrive/Utils/UniversalLinksHelper.swift | 16 +++-- kDriveCore/Data/Api/DriveApiFetcher.swift | 20 ------ kDriveCore/Data/Api/Endpoint+Share.swift | 16 +++-- .../Data/Api/PublicShareApiFetcher.swift | 62 ++++++++++++++++++ .../DriveFileManager/DriveFileManager.swift | 23 ++++++- kDriveCore/Data/Models/File.swift | 13 ++++ kDriveCore/Utils/AppNavigable.swift | 7 ++- 10 files changed, 208 insertions(+), 42 deletions(-) create mode 100644 kDriveCore/Data/Api/PublicShareApiFetcher.swift diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 5eb8eef0f..df8a74abb 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -537,7 +537,12 @@ public struct AppRouter: AppNavigable { // MARK: RouterFileNavigable - @MainActor public func presentPublicShare(rootFolder: File, driveFileManager: DriveFileManager) { + @MainActor public func presentPublicShare( + rootFolder: File, + publicShareProxy: PublicShareProxy, + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher + ) { // TODO: Present on top of existing views guard let window, let rootViewController = window.rootViewController else { @@ -545,11 +550,11 @@ public struct AppRouter: AppNavigable { } let filePresenter = FilePresenter(viewController: rootViewController) - filePresenter.presentPublicShareDirectory( - rootFolder: rootFolder, - rootViewController: rootViewController, - driveFileManager: driveFileManager - ) + filePresenter.presentPublicShareDirectory(publicShareProxy: publicShareProxy, + rootFolder: rootFolder, + rootViewController: rootViewController, + driveFileManager: driveFileManager, + apiFetcher: apiFetcher) } @MainActor public func present(file: File, driveFileManager: DriveFileManager) { diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index d22004a5c..de756d923 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -133,12 +133,17 @@ final class FilePresenter { } public func presentPublicShareDirectory( + publicShareProxy: PublicShareProxy, rootFolder: File, rootViewController: UIViewController, - driveFileManager: DriveFileManager + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher ) { - let viewModel: FileListViewModel // TODO: use InMemoryFileListViewModel - viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: rootFolder) + let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, + sortType: .nameAZ, + driveFileManager: driveFileManager, + currentDirectory: rootFolder, + apiFetcher: apiFetcher) // TODO: Fix access right // guard !rootFolder.isDisabled else { @@ -148,7 +153,7 @@ final class FilePresenter { let nextVC = FileListViewController(viewModel: viewModel) print("nextVC:\(nextVC) viewModel:\(viewModel) navigationController:\(navigationController)") // navigationController?.pushViewController(nextVC, animated: true) - + rootViewController.present(nextVC, animated: true) } diff --git a/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift b/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift index 0f8a41ce3..3410a6a1c 100644 --- a/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift +++ b/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift @@ -20,6 +20,69 @@ import kDriveCore import RealmSwift import UIKit +/// Public share view model, loading content from memory realm +final class PublicShareViewModel: InMemoryFileListViewModel { + var publicShareProxy: PublicShareProxy? + let rootProxy: ProxyFile + var publicShareApiFetcher: PublicShareApiFetcher? + + required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { + guard let currentDirectory else { + fatalError("woops") + } + + let configuration = Configuration(selectAllSupported: false, + rootTitle: "public share", + emptyViewType: .emptyFolder, + supportsDrop: false, + matomoViewPath: [MatomoUtils.Views.menu.displayName, "publicShare"]) + + rootProxy = currentDirectory.proxify() + super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: currentDirectory) + observedFiles = AnyRealmCollection(currentDirectory.children) + print("• observedFiles :\(observedFiles.count)") + } + + convenience init( + publicShareProxy: PublicShareProxy, + sortType: SortType, + driveFileManager: DriveFileManager, + currentDirectory: File, + apiFetcher: PublicShareApiFetcher + ) { + self.init(driveFileManager: driveFileManager, currentDirectory: currentDirectory) + self.publicShareProxy = publicShareProxy + self.sortType = sortType + publicShareApiFetcher = apiFetcher + } + + override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { + print("• loadFiles:\(cursor):\(forceRefresh)") + guard !isLoading || cursor != nil, + let publicShareProxy, + let publicShareApiFetcher else { + return + } + + // Only show loading indicator if we have nothing in cache + if !currentDirectory.canLoadChildrenFromCache { + startRefreshing(cursor: cursor) + } + defer { + endRefreshing() + } + + let (_, nextCursor) = try await driveFileManager.publicShareFiles(rootProxy: rootProxy, + publicShareProxy: publicShareProxy, + publicShareApiFetcher: publicShareApiFetcher) + print("• nextCursor:\(nextCursor)") + endRefreshing() + if let nextCursor { + try await loadFiles(cursor: nextCursor) + } + } +} + class SharedWithMeViewModel: FileListViewModel { required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { let sharedWithMeRootFile = driveFileManager.getManagedFile(from: DriveFileManager.sharedWithMeRootFile) diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 720e4a882..32e7734a1 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -136,12 +136,20 @@ enum UniversalLinksHelper { Task { do { let rootFolder = try await apiFetcher.getShareLinkFile(driveId: driveId, - linkUuid: linkUuid, - fileId: fileId) + linkUuid: linkUuid, + fileId: fileId) @InjectService var appNavigable: AppNavigable - await appNavigable.presentPublicShare(rootFolder: rootFolder, driveFileManager: driveFileManager) + let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: fileId, shareLinkUid: linkUuid) + await appNavigable.presentPublicShare( + rootFolder: rootFolder, + publicShareProxy: publicShareProxy, + driveFileManager: driveFileManager, + apiFetcher: apiFetcher + ) } catch { - DDLogError("[UniversalLinksHelper] Failed to get public folder [driveId:\(driveId) linkUuid:\(linkUuid) fileId:\(fileId)]: \(error)") + DDLogError( + "[UniversalLinksHelper] Failed to get public folder [driveId:\(driveId) linkUuid:\(linkUuid) fileId:\(fileId)]: \(error)" + ) await UIConstants.showSnackBarIfNeeded(error: error) } } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 64a204d02..7bef795dc 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -100,26 +100,6 @@ public struct PublicShareMetadata: Decodable { } } -public class PublicShareApiFetcher: ApiFetcher { - override public init() { - super.init() - } - - public func getMetadata(driveId: Int, shareLinkUid: String) async throws -> PublicShareMetadata { - let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url - let request = Session.default.request(shareLinkInfoUrl) - let metadata: PublicShareMetadata = try await perform(request: request) - return metadata - } - - public func getShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) async throws -> File { - let shareLinkFileUrl = Endpoint.shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).url - let request = Session.default.request(shareLinkFileUrl) - let shareLinkFile: File = try await perform(request: request) - return shareLinkFile - } -} - public class DriveApiFetcher: ApiFetcher { @LazyInjectService var accountManager: AccountManageable @LazyInjectService var tokenable: InfomaniakTokenable diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 11dbed94b..21b5d4e4b 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -23,7 +23,6 @@ import RealmSwift // MARK: - Share Links public extension Endpoint { - private static let sharedFileWithQuery = "with=capabilities,conversion_capabilities,supported_by" /// It is necessary to keep V1 here for backward compatibility of old links static var shareUrlV1: Endpoint { @@ -57,9 +56,14 @@ public extension Endpoint { } /// Share link file children - func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int, sortType: SortType) -> Endpoint { - let orderQuery = "order_by=\(sortType.value.apiValue)&order=\(sortType.value.order)" - return Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files?\(Self.sharedFileWithQuery)&\(orderQuery)") + static func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int, sortType: SortType) -> Endpoint { + let orderByQuery = URLQueryItem(name: "order_by", value: sortType.value.apiValue) + let orderQuery = URLQueryItem(name: "order", value: sortType.value.order) + let withQuery = URLQueryItem(name: "with", value: "capabilities,conversion_capabilities,supported_by") + + let shareLinkQueryItems = [orderByQuery, orderQuery, withQuery] + let fileChildrenEndpoint = Self.shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files") + return fileChildrenEndpoint.appending(path: "", queryItems: shareLinkQueryItems) } /// Share link file thumbnail @@ -78,10 +82,10 @@ public extension Endpoint { } func showOfficeShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - return Self.shareUrlV1.appending(path: "share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") + return Self.shareUrlV1.appending(path: "/share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") } func importShareLinkFiles(driveId: Int) -> Endpoint { - return Self.shareUrlV2.appending(path: "\(driveId)/imports/sharelink") + return Self.shareUrlV2.appending(path: "/\(driveId)/imports/sharelink") } } diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift new file mode 100644 index 000000000..5804f362b --- /dev/null +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -0,0 +1,62 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Alamofire +import InfomaniakCore +import InfomaniakDI +import InfomaniakLogin +import Kingfisher + +public class PublicShareApiFetcher: ApiFetcher { + override public init() { + super.init() + } + + public func getMetadata(driveId: Int, shareLinkUid: String) async throws -> PublicShareMetadata { + let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url + let request = Session.default.request(shareLinkInfoUrl) + let metadata: PublicShareMetadata = try await perform(request: request) + return metadata + } + + public func getShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) async throws -> File { + let shareLinkFileUrl = Endpoint.shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).url + let request = Session.default.request(shareLinkFileUrl) + let shareLinkFile: File = try await perform(request: request) + return shareLinkFile + } + + /// Query a specific page + public func shareLinkFileChildren(publicShareProxy: PublicShareProxy, + sortType: SortType, + cursor: String? = nil) async throws -> ValidServerResponse<[File]> { + let shareLinkFileChildren = Endpoint.shareLinkFileChildren( + driveId: publicShareProxy.driveId, + linkUuid: publicShareProxy.shareLinkUid, + fileId: publicShareProxy.fileId, + sortType: sortType + ) + .cursored(cursor) + .sorted(by: [sortType]) + + let shareLinkFileChildrenUrl = shareLinkFileChildren.url + let request = Session.default.request(shareLinkFileChildrenUrl) + let shareLinkFiles: ValidServerResponse<[File]> = try await perform(request: request) + return shareLinkFiles + } +} diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index 827c4a344..b772a6a2a 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift @@ -86,7 +86,7 @@ public final class DriveFileManager { let realmURL = context.realmURL(driveId: driveId, driveUserId: driveUserId) let inMemoryIdentifier: String? - if case let .publicShare(identifier) = context { + if case .publicShare(let identifier) = context { inMemoryIdentifier = "inMemory:\(identifier)" } else { inMemoryIdentifier = nil @@ -389,6 +389,27 @@ public final class DriveFileManager { forceRefresh: forceRefresh) } + public func publicShareFiles(rootProxy: ProxyFile, + publicShareProxy: PublicShareProxy, + cursor: String? = nil, + sortType: SortType = .nameAZ, + forceRefresh: Bool = false, + publicShareApiFetcher: PublicShareApiFetcher) async throws + -> (files: [File], nextCursor: String?) { + try await files(in: rootProxy, + fetchFiles: { + let mySharedFiles = try await publicShareApiFetcher.shareLinkFileChildren( + publicShareProxy: publicShareProxy, + sortType: sortType + ) + return mySharedFiles + }, + cursor: cursor, + sortType: sortType, + keepProperties: [.standard, .path, .version], + forceRefresh: forceRefresh) + } + public func searchFile(query: String? = nil, date: DateInterval? = nil, fileType: ConvertedType? = nil, diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index 4026bbc22..c0e0cec26 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -182,6 +182,19 @@ public enum ConvertedType: String, CaseIterable { public static let ignoreThumbnailTypes = downloadableTypes } +/// Minimal data needed to query a PublicShare +public struct PublicShareProxy { + let driveId: Int + let fileId: Int + let shareLinkUid: String + + public init(driveId: Int, fileId: Int, shareLinkUid: String) { + self.driveId = driveId + self.fileId = fileId + self.shareLinkUid = shareLinkUid + } +} + public enum SortType: String { case nameAZ case nameZA diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index a36e22cde..221728bd2 100644 --- a/kDriveCore/Utils/AppNavigable.swift +++ b/kDriveCore/Utils/AppNavigable.swift @@ -57,7 +57,12 @@ public protocol RouterFileNavigable { @MainActor func present(file: File, driveFileManager: DriveFileManager, office: Bool) /// Present a file list for a public share - @MainActor func presentPublicShare(rootFolder: File, driveFileManager: DriveFileManager) + @MainActor func presentPublicShare( + rootFolder: File, + publicShareProxy: PublicShareProxy, + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher + ) /// Present a list of files from a folder /// - Parameters: From fd7dfabbf4676df1ea523c57a39ab76140ed92a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 3 Sep 2024 18:03:12 +0200 Subject: [PATCH 10/23] feat: Can natively show a public share within the app --- kDrive/AppRouter.swift | 4 +- .../UI/Controller/Files/FilePresenter.swift | 5 +- .../PublicShareViewModel.swift} | 40 ++------------ .../Menu/Share/SharedWithMeViewModel.swift | 53 +++++++++++++++++++ kDrive/Utils/UniversalLinksHelper.swift | 9 +++- kDriveCore/Data/Api/Endpoint+Share.swift | 2 +- kDriveCore/Data/Cache/AccountManager.swift | 19 +++++-- .../DriveFileManager/DriveFileManager.swift | 15 ++++++ .../DriveInfosManager/DriveInfosManager.swift | 8 +++ kDriveCore/Data/Models/Drive/Drive.swift | 3 +- kDriveCore/Utils/AppNavigable.swift | 4 +- 11 files changed, 112 insertions(+), 50 deletions(-) rename kDrive/UI/Controller/Menu/{SharedWithMe/SharedWithMeViewModel.swift => Share/PublicShareViewModel.swift} (63%) create mode 100644 kDrive/UI/Controller/Menu/Share/SharedWithMeViewModel.swift diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index df8a74abb..9720079c5 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -538,7 +538,7 @@ public struct AppRouter: AppNavigable { // MARK: RouterFileNavigable @MainActor public func presentPublicShare( - rootFolder: File, + frozenRootFolder: File, publicShareProxy: PublicShareProxy, driveFileManager: DriveFileManager, apiFetcher: PublicShareApiFetcher @@ -551,7 +551,7 @@ public struct AppRouter: AppNavigable { let filePresenter = FilePresenter(viewController: rootViewController) filePresenter.presentPublicShareDirectory(publicShareProxy: publicShareProxy, - rootFolder: rootFolder, + frozenRootFolder: frozenRootFolder, rootViewController: rootViewController, driveFileManager: driveFileManager, apiFetcher: apiFetcher) diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index de756d923..3ef0861a4 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -134,7 +134,7 @@ final class FilePresenter { public func presentPublicShareDirectory( publicShareProxy: PublicShareProxy, - rootFolder: File, + frozenRootFolder: File, rootViewController: UIViewController, driveFileManager: DriveFileManager, apiFetcher: PublicShareApiFetcher @@ -142,7 +142,7 @@ final class FilePresenter { let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, sortType: .nameAZ, driveFileManager: driveFileManager, - currentDirectory: rootFolder, + currentDirectory: frozenRootFolder, apiFetcher: apiFetcher) // TODO: Fix access right @@ -150,6 +150,7 @@ final class FilePresenter { // return // } + // TODO: Build clean context aware navigation let nextVC = FileListViewController(viewModel: viewModel) print("nextVC:\(nextVC) viewModel:\(viewModel) navigationController:\(navigationController)") // navigationController?.pushViewController(nextVC, animated: true) diff --git a/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift similarity index 63% rename from kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift rename to kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 3410a6a1c..0898ce34d 100644 --- a/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -1,6 +1,6 @@ /* Infomaniak kDrive - iOS App - Copyright (C) 2021 Infomaniak Network SA + Copyright (C) 2024 Infomaniak Network SA This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,9 +28,10 @@ final class PublicShareViewModel: InMemoryFileListViewModel { required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { guard let currentDirectory else { - fatalError("woops") + fatalError("PublicShareViewModel requires a currentDirectory to work") } + // TODO: i18n let configuration = Configuration(selectAllSupported: false, rootTitle: "public share", emptyViewType: .emptyFolder, @@ -40,7 +41,6 @@ final class PublicShareViewModel: InMemoryFileListViewModel { rootProxy = currentDirectory.proxify() super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: currentDirectory) observedFiles = AnyRealmCollection(currentDirectory.children) - print("• observedFiles :\(observedFiles.count)") } convenience init( @@ -57,7 +57,6 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { - print("• loadFiles:\(cursor):\(forceRefresh)") guard !isLoading || cursor != nil, let publicShareProxy, let publicShareApiFetcher else { @@ -75,39 +74,6 @@ final class PublicShareViewModel: InMemoryFileListViewModel { let (_, nextCursor) = try await driveFileManager.publicShareFiles(rootProxy: rootProxy, publicShareProxy: publicShareProxy, publicShareApiFetcher: publicShareApiFetcher) - print("• nextCursor:\(nextCursor)") - endRefreshing() - if let nextCursor { - try await loadFiles(cursor: nextCursor) - } - } -} - -class SharedWithMeViewModel: FileListViewModel { - required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { - let sharedWithMeRootFile = driveFileManager.getManagedFile(from: DriveFileManager.sharedWithMeRootFile) - let configuration = Configuration(selectAllSupported: false, - rootTitle: KDriveCoreStrings.Localizable.sharedWithMeTitle, - emptyViewType: .noSharedWithMe, - supportsDrop: false, - matomoViewPath: [MatomoUtils.Views.menu.displayName, "SharedWithMe"]) - - super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: sharedWithMeRootFile) - observedFiles = AnyRealmCollection(AnyRealmCollection(sharedWithMeRootFile.children).filesSorted(by: sortType)) - } - - override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { - guard !isLoading || cursor != nil else { return } - - // Only show loading indicator if we have nothing in cache - if !currentDirectory.canLoadChildrenFromCache { - startRefreshing(cursor: cursor) - } - defer { - endRefreshing() - } - - let (_, nextCursor) = try await driveFileManager.sharedWithMeFiles(cursor: cursor, sortType: sortType, forceRefresh: true) endRefreshing() if let nextCursor { try await loadFiles(cursor: nextCursor) diff --git a/kDrive/UI/Controller/Menu/Share/SharedWithMeViewModel.swift b/kDrive/UI/Controller/Menu/Share/SharedWithMeViewModel.swift new file mode 100644 index 000000000..0f8a41ce3 --- /dev/null +++ b/kDrive/UI/Controller/Menu/Share/SharedWithMeViewModel.swift @@ -0,0 +1,53 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2021 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import kDriveCore +import RealmSwift +import UIKit + +class SharedWithMeViewModel: FileListViewModel { + required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { + let sharedWithMeRootFile = driveFileManager.getManagedFile(from: DriveFileManager.sharedWithMeRootFile) + let configuration = Configuration(selectAllSupported: false, + rootTitle: KDriveCoreStrings.Localizable.sharedWithMeTitle, + emptyViewType: .noSharedWithMe, + supportsDrop: false, + matomoViewPath: [MatomoUtils.Views.menu.displayName, "SharedWithMe"]) + + super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: sharedWithMeRootFile) + observedFiles = AnyRealmCollection(AnyRealmCollection(sharedWithMeRootFile.children).filesSorted(by: sortType)) + } + + override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { + guard !isLoading || cursor != nil else { return } + + // Only show loading indicator if we have nothing in cache + if !currentDirectory.canLoadChildrenFromCache { + startRefreshing(cursor: cursor) + } + defer { + endRefreshing() + } + + let (_, nextCursor) = try await driveFileManager.sharedWithMeFiles(cursor: cursor, sortType: sortType, forceRefresh: true) + endRefreshing() + if let nextCursor { + try await loadFiles(cursor: nextCursor) + } + } +} diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 32e7734a1..3c8ea1dd0 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -138,10 +138,17 @@ enum UniversalLinksHelper { let rootFolder = try await apiFetcher.getShareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId) + // Root folder must be in database for the FileListViewModel to work + try driveFileManager.database.writeTransaction { writableRealm in + writableRealm.add(rootFolder) + } + + let frozenRootFolder = rootFolder.freeze() + @InjectService var appNavigable: AppNavigable let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: fileId, shareLinkUid: linkUuid) await appNavigable.presentPublicShare( - rootFolder: rootFolder, + frozenRootFolder: frozenRootFolder, publicShareProxy: publicShareProxy, driveFileManager: driveFileManager, apiFetcher: apiFetcher diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 21b5d4e4b..765077eb3 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -62,7 +62,7 @@ public extension Endpoint { let withQuery = URLQueryItem(name: "with", value: "capabilities,conversion_capabilities,supported_by") let shareLinkQueryItems = [orderByQuery, orderQuery, withQuery] - let fileChildrenEndpoint = Self.shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files") + let fileChildrenEndpoint = Self.shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)/files") return fileChildrenEndpoint.appending(path: "", queryItems: shareLinkQueryItems) } diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index f58e1603e..3f81233e7 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -216,15 +216,26 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { return inMemoryDriveFileManager } - // Big hack, refactor to allow for non authenticated requests + // TODO: Big hack, refactor to allow for non authenticated requests guard let someToken = apiFetchers.values.first?.currentToken else { - fatalError("probably no account availlable") + fatalError("probably no account available") } + // FileViewModel K.O. without a valid drive in Realm, therefore add one + let publicShareDrive = Drive() + publicShareDrive.objectId = publicShareId + @LazyInjectService var driveInfosManager: DriveInfosManager + do { + try driveInfosManager.storePublicShareDrive(drive: publicShareDrive) + } catch { + fatalError("unable to update public share drive in base, \(error)") + } + let forzenPublicShareDrive = publicShareDrive.freeze() + let apiFetcher = DriveApiFetcher(token: someToken, delegate: SomeRefreshTokenDelegate()) let context = DriveFileManagerContext.publicShare(shareId: publicShareId) - let noopDrive = Drive() - return DriveFileManager(drive: noopDrive, apiFetcher: apiFetcher, context: context) + + return DriveFileManager(drive: forzenPublicShareDrive, apiFetcher: apiFetcher, context: context) } public func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager { diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index b772a6a2a..5ed94c414 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift @@ -26,6 +26,21 @@ import InfomaniakLogin import RealmSwift import SwiftRegex +// TODO: Move to core +extension TransactionExecutor: CustomStringConvertible { + public var description: String { + var render = "TransactionExecutor: realm access issue" + try? writeTransaction { realm in + render = """ + TransactionExecutor: + realmURL:\(realm.configuration.fileURL) + inMemory:\(realm.configuration.inMemoryIdentifier) + """ + } + return render + } +} + // MARK: - Transactionable public final class DriveFileManager { diff --git a/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager.swift b/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager.swift index e75e51a16..93af4f34d 100644 --- a/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager.swift +++ b/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager.swift @@ -134,6 +134,14 @@ public final class DriveInfosManager: DriveInfosManagerQueryable { drive.sharedWithMe = sharedWithMe } + // TODO: Add a flag that this drive can be cleaned + /// Store a specific public share Drive in realm for use by FileListViewControllers + public func storePublicShareDrive(drive: Drive) throws { + try driveInfoDatabase.writeTransaction { writableRealm in + writableRealm.add(drive, update: .modified) + } + } + @discardableResult func storeDriveResponse(user: InfomaniakCore.UserProfile, driveResponse: DriveResponse) -> [Drive] { var driveList = [Drive]() diff --git a/kDriveCore/Data/Models/Drive/Drive.swift b/kDriveCore/Data/Models/Drive/Drive.swift index e6dcbb652..907be538b 100644 --- a/kDriveCore/Data/Models/Drive/Drive.swift +++ b/kDriveCore/Data/Models/Drive/Drive.swift @@ -186,8 +186,9 @@ public final class Drive: Object, Codable { // File is not managed by Realm: cannot use the `.sorted(by:)` method :( fileCategoriesIds = file.categories.sorted { $0.addedAt.compare($1.addedAt) == .orderedAscending }.map(\.categoryId) } - let filteredCategories = categories.filter(NSPredicate(format: "id IN %@", fileCategoriesIds)) + // Sort the categories + let filteredCategories = categories.filter("id IN %@", fileCategoriesIds) return fileCategoriesIds.compactMap { id in filteredCategories.first { $0.id == id } } } diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index 221728bd2..5f92d00fc 100644 --- a/kDriveCore/Utils/AppNavigable.swift +++ b/kDriveCore/Utils/AppNavigable.swift @@ -56,9 +56,9 @@ public protocol RouterFileNavigable { /// - office: Open in only office @MainActor func present(file: File, driveFileManager: DriveFileManager, office: Bool) - /// Present a file list for a public share + /// Present a file list for a public share, regardless of authenticated state @MainActor func presentPublicShare( - rootFolder: File, + frozenRootFolder: File, publicShareProxy: PublicShareProxy, driveFileManager: DriveFileManager, apiFetcher: PublicShareApiFetcher From d9bc2d3c1daa1fca7f663a27a51fd35fc06dffe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 16 Sep 2024 16:52:18 +0200 Subject: [PATCH 11/23] fix(FilePresenter): Present public share in context --- kDrive/AppRouter.swift | 36 +++++++++++++++---- .../UI/Controller/Files/FilePresenter.swift | 26 -------------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 6c0f38556..d5ddfe1d5 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -284,7 +284,8 @@ public struct AppRouter: AppNavigable { let fileIds = sceneUserInfo[SceneRestorationValues.Carousel.filesIds.rawValue] as? [Int], let currentIndex = sceneUserInfo[SceneRestorationValues.Carousel.currentIndex.rawValue] as? Int, let normalFolderHierarchy = sceneUserInfo[SceneRestorationValues.Carousel.normalFolderHierarchy.rawValue] as? Bool, - let presentationOrigin = sceneUserInfo[SceneRestorationValues.Carousel.presentationOrigin.rawValue] as? PresentationOrigin else { + let presentationOrigin = + sceneUserInfo[SceneRestorationValues.Carousel.presentationOrigin.rawValue] as? PresentationOrigin else { Log.sceneDelegate("metadata issue for PreviewController :\(sceneUserInfo)", level: .error) return } @@ -596,12 +597,33 @@ public struct AppRouter: AppNavigable { fatalError("TODO: lazy load a rootViewController") } - let filePresenter = FilePresenter(viewController: rootViewController) - filePresenter.presentPublicShareDirectory(publicShareProxy: publicShareProxy, - frozenRootFolder: frozenRootFolder, - rootViewController: rootViewController, - driveFileManager: driveFileManager, - apiFetcher: apiFetcher) + guard let rootViewController = window.rootViewController as? MainTabViewController else { + fatalError("Root is not a MainTabViewController") + return + } + + // TODO: Fix access right + // guard !rootFolder.isDisabled else { + // return + // } + + rootViewController.dismiss(animated: false) { + rootViewController.selectedIndex = MainTabBarIndex.files.rawValue + + guard let navigationController = rootViewController.selectedViewController as? UINavigationController else { + return + } + + let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, + sortType: .nameAZ, + driveFileManager: driveFileManager, + currentDirectory: frozenRootFolder, + apiFetcher: apiFetcher) + let viewController = FileListViewController(viewModel: viewModel) + print("viewController:\(viewController) viewModel:\(viewModel) navigationController:\(navigationController)") + + navigationController.pushViewController(viewController, animated: true) + } } @MainActor public func present(file: File, driveFileManager: DriveFileManager) { diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index 5922e9633..d66710d6b 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -132,32 +132,6 @@ final class FilePresenter { } } - public func presentPublicShareDirectory( - publicShareProxy: PublicShareProxy, - frozenRootFolder: File, - rootViewController: UIViewController, - driveFileManager: DriveFileManager, - apiFetcher: PublicShareApiFetcher - ) { - let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, - sortType: .nameAZ, - driveFileManager: driveFileManager, - currentDirectory: frozenRootFolder, - apiFetcher: apiFetcher) - - // TODO: Fix access right -// guard !rootFolder.isDisabled else { -// return -// } - - // TODO: Build clean context aware navigation - let nextVC = FileListViewController(viewModel: viewModel) - print("nextVC:\(nextVC) viewModel:\(viewModel) navigationController:\(navigationController)") -// navigationController?.pushViewController(nextVC, animated: true) - - rootViewController.present(nextVC, animated: true) - } - public func presentDirectory( for file: File, driveFileManager: DriveFileManager, From 7c8c2c7aa147193148ba588590112151cb409b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 16 Sep 2024 17:44:26 +0200 Subject: [PATCH 12/23] fix: Capabilities for Public Share --- kDrive/AppRouter.swift | 7 ++++--- kDriveCore/Data/Api/PublicShareApiFetcher.swift | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index d5ddfe1d5..66a8b000f 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -603,9 +603,10 @@ public struct AppRouter: AppNavigable { } // TODO: Fix access right - // guard !rootFolder.isDisabled else { - // return - // } + guard !frozenRootFolder.isDisabled else { + fatalError("isDisabled") + return + } rootViewController.dismiss(animated: false) { rootViewController.selectedIndex = MainTabBarIndex.files.rawValue diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index 5804f362b..789de315e 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -36,7 +36,10 @@ public class PublicShareApiFetcher: ApiFetcher { public func getShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) async throws -> File { let shareLinkFileUrl = Endpoint.shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).url - let request = Session.default.request(shareLinkFileUrl) + let requestParameters: [String: String] = [ + APIUploadParameter.with.rawValue: FileWith.capabilities.rawValue + ] + let request = Session.default.request(shareLinkFileUrl, parameters: requestParameters) let shareLinkFile: File = try await perform(request: request) return shareLinkFile } From 70d65a0bccb603739f723fb8f4f40948c7a376f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 17 Sep 2024 17:11:22 +0200 Subject: [PATCH 13/23] feat: Thumbnails on public share --- .../View/Files/FileCollectionViewCell.swift | 82 +++++++++++++++---- .../Files/FileGridCollectionViewCell.swift | 7 +- kDrive/Utils/UniversalLinksHelper.swift | 6 +- kDriveCore/Data/Api/Endpoint+Share.swift | 10 ++- kDriveCore/Data/Cache/AccountManager.swift | 9 +- .../DriveFileManager+Transactionable.swift | 2 +- .../DriveFileManager/DriveFileManager.swift | 44 ++++++++++ kDriveCore/Data/Models/File+Image.swift | 32 ++++++++ 8 files changed, 165 insertions(+), 27 deletions(-) diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift index 23901692f..e4172dd71 100644 --- a/kDrive/UI/View/Files/FileCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift @@ -48,6 +48,16 @@ protocol FileCellDelegate: AnyObject { var file: File var selectionMode: Bool var isSelected = false + + /// UUID of the public share if file exists within a public share + let publicShareId: String? + + /// Drive ID of the public share if file exists within a public share + let publicDriveId: Int? + + /// Root file ID of the public share if file exists within a public share + let publicRootFileId: Int? + private var downloadProgressObserver: ObservationToken? private var downloadObserver: ObservationToken? var thumbnailDownloadTask: Kingfisher.DownloadTask? @@ -114,6 +124,10 @@ protocol FileCellDelegate: AnyObject { init(driveFileManager: DriveFileManager, file: File, selectionMode: Bool) { self.file = file self.selectionMode = selectionMode + publicShareId = driveFileManager.publicShareId + publicDriveId = driveFileManager.publicDriveId + publicRootFileId = driveFileManager.publicRootFileId + categories = driveFileManager.drive.categories(for: file) } @@ -138,26 +152,53 @@ protocol FileCellDelegate: AnyObject { } func setThumbnail(on imageView: UIImageView) { + // check if public share / use specific endpoint guard !file.isInvalidated, - (file.convertedType == .image || file.convertedType == .video) && file.supportedBy.contains(.thumbnail) - else { return } + (file.convertedType == .image || file.convertedType == .video) && file.supportedBy.contains(.thumbnail) else { + return + } + // Configure placeholder imageView.image = nil imageView.contentMode = .scaleAspectFill imageView.layer.cornerRadius = UIConstants.imageCornerRadius imageView.layer.masksToBounds = true imageView.backgroundColor = KDriveResourcesAsset.loaderDefaultColor.color - // Fetch thumbnail - thumbnailDownloadTask = file.getThumbnail { [requestFileId = file.id, weak self] image, _ in - guard let self, - !self.file.isInvalidated, - !self.isSelected else { - return + + if let publicShareId = publicShareId, + let publicDriveId = publicDriveId { + // Fetch public share thumbnail + thumbnailDownloadTask = file.getPublicShareThumbnail(publicShareId: publicShareId, + publicDriveId: publicDriveId, + publicFileId: file.id) { [ + requestFileId = file.id, + weak self + ] image, _ in + guard let self, + !self.file.isInvalidated, + !self.isSelected else { + return + } + + if file.id == requestFileId { + imageView.image = image + imageView.backgroundColor = nil + } } - if file.id == requestFileId { - imageView.image = image - imageView.backgroundColor = nil + } else { + // Fetch thumbnail + thumbnailDownloadTask = file.getThumbnail { [requestFileId = file.id, weak self] image, _ in + guard let self, + !self.file.isInvalidated, + !self.isSelected else { + return + } + + if file.id == requestFileId { + imageView.image = image + imageView.backgroundColor = nil + } } } } @@ -302,7 +343,7 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell { func configure(with viewModel: FileViewModel) { self.viewModel = viewModel - configureLogoImage() + configureLogoImage(viewModel: viewModel) titleLabel.text = viewModel.title detailLabel?.text = viewModel.subtitle favoriteImageView?.isHidden = !viewModel.isFavorite @@ -321,7 +362,12 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell { } func configureWith(driveFileManager: DriveFileManager, file: File, selectionMode: Bool = false) { - configure(with: FileViewModel(driveFileManager: driveFileManager, file: file, selectionMode: selectionMode)) + let fileViewModel = FileViewModel( + driveFileManager: driveFileManager, + file: file, + selectionMode: selectionMode + ) + configure(with: fileViewModel) } /// Update the cell selection mode. @@ -333,18 +379,20 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell { } func configureForSelection() { - guard viewModel?.selectionMode == true else { return } + guard let viewModel, + viewModel.selectionMode == true else { + return + } if isSelected { configureCheckmarkImage() configureImport(shouldDisplay: false) } else { - configureLogoImage() + configureLogoImage(viewModel: viewModel) } } - private func configureLogoImage() { - guard let viewModel else { return } + private func configureLogoImage(viewModel: FileViewModel) { logoImage.isAccessibilityElement = true logoImage.accessibilityLabel = viewModel.iconAccessibilityLabel logoImage.image = viewModel.icon diff --git a/kDrive/UI/View/Files/FileGridCollectionViewCell.swift b/kDrive/UI/View/Files/FileGridCollectionViewCell.swift index 4ba0608a7..58b6e7ec2 100644 --- a/kDrive/UI/View/Files/FileGridCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileGridCollectionViewCell.swift @@ -110,7 +110,12 @@ class FileGridCollectionViewCell: FileCollectionViewCell { } override func configureWith(driveFileManager: DriveFileManager, file: File, selectionMode: Bool = false) { - configure(with: FileGridViewModel(driveFileManager: driveFileManager, file: file, selectionMode: selectionMode)) + let viewModel = FileGridViewModel( + driveFileManager: driveFileManager, + file: file, + selectionMode: selectionMode + ) + configure(with: viewModel) } override func configureLoading() { diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 3c8ea1dd0..38c72c2cf 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -102,7 +102,11 @@ enum UniversalLinksHelper { } // get file ID from metadata - let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(for: shareLinkUid) + let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager( + for: shareLinkUid, + driveId: driveIdInt, + rootFileId: metadata.fileId + ) openPublicShare(driveId: driveIdInt, linkUuid: shareLinkUid, fileId: metadata.fileId, diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 765077eb3..dfb913047 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -23,7 +23,6 @@ import RealmSwift // MARK: - Share Links public extension Endpoint { - /// It is necessary to keep V1 here for backward compatibility of old links static var shareUrlV1: Endpoint { return Endpoint(hostKeypath: \.driveHost, path: "/app") @@ -52,7 +51,12 @@ public extension Endpoint { /// Share link file static func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - Self.shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") + shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") + } + + /// Some legacy calls like thumbnails require a V2 call + static func shareLinkFileV2(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + shareUrlV2.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") } /// Share link file children @@ -68,7 +72,7 @@ public extension Endpoint { /// Share link file thumbnail static func shareLinkFileThumbnail(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/thumbnail") + return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/thumbnail") } /// Share mink file preview diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index 3f81233e7..047d4d4d9 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -78,8 +78,7 @@ public protocol AccountManageable: AnyObject { func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager /// Create on the fly an "in memory" DriveFileManager for a specific share - func getInMemoryDriveFileManager(for publicShareId: String) -> DriveFileManager - + func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager func getApiFetcher(for userId: Int, token: ApiToken) -> DriveApiFetcher func getTokenForUserId(_ id: Int) -> ApiToken? func didUpdateToken(newToken: ApiToken, oldToken: ApiToken) @@ -211,7 +210,7 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { } } - public func getInMemoryDriveFileManager(for publicShareId: String) -> DriveFileManager { + public func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager { if let inMemoryDriveFileManager = driveFileManagers[publicShareId] { return inMemoryDriveFileManager } @@ -233,7 +232,9 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { let forzenPublicShareDrive = publicShareDrive.freeze() let apiFetcher = DriveApiFetcher(token: someToken, delegate: SomeRefreshTokenDelegate()) - let context = DriveFileManagerContext.publicShare(shareId: publicShareId) + let context = DriveFileManagerContext.publicShare(shareId: publicShareId, + driveId: driveId, + rootFileId: rootFileId) return DriveFileManager(drive: forzenPublicShareDrive, apiFetcher: apiFetcher, context: context) } diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift index f733d2743..b42faa6da 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift @@ -30,7 +30,7 @@ public enum DriveFileManagerContext { case sharedWithMe /// Dedicated in memory dataset for a public share link - case publicShare(shareId: String) + case publicShare(shareId: String, driveId: Int, rootFileId: Int) func realmURL(driveId: Int, driveUserId: Int) -> URL? { switch self { diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index c022ca1a7..07d161d00 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift @@ -99,6 +99,9 @@ public final class DriveFileManager { /// Fetch and write into DB with this object public let database: Transactionable + /// Context this object was initialized with + public let context: DriveFileManagerContext + /// Build a realm configuration for a specific Drive public static func configuration(context: DriveFileManagerContext, driveId: Int, driveUserId: Int) -> Realm.Configuration { let realmURL = context.realmURL(driveId: driveId, driveUserId: driveUserId) @@ -215,9 +218,50 @@ public final class DriveFileManager { ) } + public var isPublicShare: Bool { + switch context { + case .drive: + return false + case .fileProvider: + return false + case .sharedWithMe: + return false + case .publicShare(let shareId): + return true + } + } + + public var publicShareId: String? { + switch context { + case .publicShare(let shareId, _, _): + return shareId + default: + return nil + } + } + + public var publicDriveId: Int? { + switch context { + case .publicShare(_, let driveId, _): + return driveId + default: + return nil + } + } + + public var publicRootFileId: Int? { + switch context { + case .publicShare(_, _, let rootFileId): + return rootFileId + default: + return nil + } + } + init(drive: Drive, apiFetcher: DriveApiFetcher, context: DriveFileManagerContext = .drive) { self.drive = drive self.apiFetcher = apiFetcher + self.context = context realmConfiguration = Self.configuration(context: context, driveId: drive.id, driveUserId: drive.userId) let realmURL = context.realmURL(driveId: drive.id, driveUserId: drive.userId) diff --git a/kDriveCore/Data/Models/File+Image.swift b/kDriveCore/Data/Models/File+Image.swift index 87fab6c8e..5fa873adc 100644 --- a/kDriveCore/Data/Models/File+Image.swift +++ b/kDriveCore/Data/Models/File+Image.swift @@ -16,10 +16,42 @@ along with this program. If not, see . */ +import InfomaniakCore import Kingfisher import UIKit public extension File { + /// Get a Thumbnail for a file from a public share + @discardableResult + func getPublicShareThumbnail(publicShareId: String, + publicDriveId: Int, + publicFileId: Int, + completion: @escaping ((UIImage, Bool) -> Void)) -> Kingfisher.DownloadTask? { + guard supportedBy.contains(.thumbnail), + let currentDriveFileManager = accountManager.currentDriveFileManager else { + completion(icon, false) + return nil + } + + let thumbnailURL = Endpoint.shareLinkFileThumbnail(driveId: publicDriveId, + linkUuid: publicShareId, + fileId: publicFileId).url + + return KingfisherManager.shared.retrieveImage(with: thumbnailURL) { result in + if let image = try? result.get().image { + completion(image, true) + } else { + // The file can become invalidated while retrieving the icon online + completion( + self.isInvalidated ? ConvertedType.unknown.icon : self + .icon, + false + ) + } + } + } + + /// Get a Thumbnail for a file for the current DriveFileManager @discardableResult func getThumbnail(completion: @escaping ((UIImage, Bool) -> Void)) -> Kingfisher.DownloadTask? { if supportedBy.contains(.thumbnail), let currentDriveFileManager = accountManager.currentDriveFileManager { From d3525540136ba43d3115f98a88b51b8fe9779c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 18 Sep 2024 18:21:42 +0200 Subject: [PATCH 14/23] feat: Can navigate hierarchy of public share folders refactor: Simplified code --- .../Files/File List/FileListViewModel.swift | 5 ++- .../UI/Controller/Files/FilePresenter.swift | 6 ++++ .../Files/Preview/PreviewViewController.swift | 3 +- .../View/Files/FileCollectionViewCell.swift | 22 ++++--------- .../Data/Api/PublicShareApiFetcher.swift | 6 ++-- kDriveCore/Data/Cache/AccountManager.swift | 5 ++- .../DriveFileManager+Transactionable.swift | 2 +- .../DriveFileManager/DriveFileManager.swift | 33 ++++--------------- kDriveCore/Data/Models/File.swift | 6 ++-- 9 files changed, 34 insertions(+), 54 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index b74bfff24..b9562b2eb 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -319,7 +319,10 @@ class FileListViewModel: SelectDelegate { } func didSelectFile(at indexPath: IndexPath) { - guard let file: File = getFile(at: indexPath) else { return } + guard let file: File = getFile(at: indexPath) else { + return + } + if ReachabilityListener.instance.currentStatus == .offline && !file.isDirectory && !file.isAvailableOffline { return } diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index d66710d6b..8a3f33229 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -145,6 +145,12 @@ final class FilePresenter { let viewModel: FileListViewModel if driveFileManager.drive.sharedWithMe { viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: file) + } else if let publicShareProxy = driveFileManager.publicShareProxy { + viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, + sortType: .nameAZ, + driveFileManager: driveFileManager, + currentDirectory: file, + apiFetcher: PublicShareApiFetcher()) } else if file.isTrashed || file.deletedAt != nil { viewModel = TrashListViewModel(driveFileManager: driveFileManager, currentDirectory: file) } else { diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index 303894bd4..d27d25558 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -603,7 +603,8 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, previewPageViewController.driveFileManager = driveFileManager previewPageViewController.normalFolderHierarchy = normalFolderHierarchy previewPageViewController.presentationOrigin = presentationOrigin - // currentIndex should be set at the end of the function as the it takes time and the viewDidLoad() is called before the function returns + // currentIndex should be set at the end of the function as the it takes time + // and the viewDidLoad() is called before the function returns // this should be fixed in the future with the refactor of the init previewPageViewController.currentIndex = IndexPath(row: index, section: 0) return previewPageViewController diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift index e4172dd71..09e0b416b 100644 --- a/kDrive/UI/View/Files/FileCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift @@ -49,14 +49,8 @@ protocol FileCellDelegate: AnyObject { var selectionMode: Bool var isSelected = false - /// UUID of the public share if file exists within a public share - let publicShareId: String? - - /// Drive ID of the public share if file exists within a public share - let publicDriveId: Int? - - /// Root file ID of the public share if file exists within a public share - let publicRootFileId: Int? + /// Public share data if file exists within a public share + let publicShareProxy: PublicShareProxy? private var downloadProgressObserver: ObservationToken? private var downloadObserver: ObservationToken? @@ -124,10 +118,7 @@ protocol FileCellDelegate: AnyObject { init(driveFileManager: DriveFileManager, file: File, selectionMode: Bool) { self.file = file self.selectionMode = selectionMode - publicShareId = driveFileManager.publicShareId - publicDriveId = driveFileManager.publicDriveId - publicRootFileId = driveFileManager.publicRootFileId - + publicShareProxy = driveFileManager.publicShareProxy categories = driveFileManager.drive.categories(for: file) } @@ -165,11 +156,10 @@ protocol FileCellDelegate: AnyObject { imageView.layer.masksToBounds = true imageView.backgroundColor = KDriveResourcesAsset.loaderDefaultColor.color - if let publicShareId = publicShareId, - let publicDriveId = publicDriveId { + if let publicShareProxy = publicShareProxy { // Fetch public share thumbnail - thumbnailDownloadTask = file.getPublicShareThumbnail(publicShareId: publicShareId, - publicDriveId: publicDriveId, + thumbnailDownloadTask = file.getPublicShareThumbnail(publicShareId: publicShareProxy.shareLinkUid, + publicDriveId: publicShareProxy.driveId, publicFileId: file.id) { [ requestFileId = file.id, weak self diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index 789de315e..341b22741 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -29,6 +29,7 @@ public class PublicShareApiFetcher: ApiFetcher { public func getMetadata(driveId: Int, shareLinkUid: String) async throws -> PublicShareMetadata { let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url + // TODO: Use authenticated token if availlable let request = Session.default.request(shareLinkInfoUrl) let metadata: PublicShareMetadata = try await perform(request: request) return metadata @@ -45,13 +46,14 @@ public class PublicShareApiFetcher: ApiFetcher { } /// Query a specific page - public func shareLinkFileChildren(publicShareProxy: PublicShareProxy, + public func shareLinkFileChildren(rootFolderId: Int, + publicShareProxy: PublicShareProxy, sortType: SortType, cursor: String? = nil) async throws -> ValidServerResponse<[File]> { let shareLinkFileChildren = Endpoint.shareLinkFileChildren( driveId: publicShareProxy.driveId, linkUuid: publicShareProxy.shareLinkUid, - fileId: publicShareProxy.fileId, + fileId: rootFolderId, sortType: sortType ) .cursored(cursor) diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index 047d4d4d9..53ee93823 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -232,9 +232,8 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { let forzenPublicShareDrive = publicShareDrive.freeze() let apiFetcher = DriveApiFetcher(token: someToken, delegate: SomeRefreshTokenDelegate()) - let context = DriveFileManagerContext.publicShare(shareId: publicShareId, - driveId: driveId, - rootFileId: rootFileId) + let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: rootFileId, shareLinkUid: publicShareId) + let context = DriveFileManagerContext.publicShare(shareProxy: publicShareProxy) return DriveFileManager(drive: forzenPublicShareDrive, apiFetcher: apiFetcher, context: context) } diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift index b42faa6da..bf1be04df 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift @@ -30,7 +30,7 @@ public enum DriveFileManagerContext { case sharedWithMe /// Dedicated in memory dataset for a public share link - case publicShare(shareId: String, driveId: Int, rootFileId: Int) + case publicShare(shareProxy: PublicShareProxy) func realmURL(driveId: Int, driveUserId: Int) -> URL? { switch self { diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index 07d161d00..c924e5704 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift @@ -220,39 +220,17 @@ public final class DriveFileManager { public var isPublicShare: Bool { switch context { - case .drive: - return false - case .fileProvider: - return false - case .sharedWithMe: - return false - case .publicShare(let shareId): + case .publicShare: return true - } - } - - public var publicShareId: String? { - switch context { - case .publicShare(let shareId, _, _): - return shareId default: - return nil - } - } - - public var publicDriveId: Int? { - switch context { - case .publicShare(_, let driveId, _): - return driveId - default: - return nil + return false } } - public var publicRootFileId: Int? { + public var publicShareProxy: PublicShareProxy? { switch context { - case .publicShare(_, _, let rootFileId): - return rootFileId + case .publicShare(let shareProxy): + return shareProxy default: return nil } @@ -461,6 +439,7 @@ public final class DriveFileManager { try await files(in: rootProxy, fetchFiles: { let mySharedFiles = try await publicShareApiFetcher.shareLinkFileChildren( + rootFolderId: rootProxy.id, publicShareProxy: publicShareProxy, sortType: sortType ) diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index c3f24eea1..e4625c92d 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -184,9 +184,9 @@ public enum ConvertedType: String, CaseIterable { /// Minimal data needed to query a PublicShare public struct PublicShareProxy { - let driveId: Int - let fileId: Int - let shareLinkUid: String + public let driveId: Int + public let fileId: Int + public let shareLinkUid: String public init(driveId: Int, fileId: Int, shareLinkUid: String) { self.driveId = driveId From 07e1c480edc0a0b5a966ef1b04620f33d8da511d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 19 Sep 2024 11:47:16 +0200 Subject: [PATCH 15/23] feat: Preview for public share --- .../Files/Preview/PreviewViewController.swift | 6 ++- ...DownloadingPreviewCollectionViewCell.swift | 20 ++++++++ kDriveCore/Data/Api/Endpoint+Share.swift | 2 +- kDriveCore/Data/Models/File+Image.swift | 49 +++++++++++++------ 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index d27d25558..077718391 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -644,7 +644,11 @@ extension PreviewViewController: UICollectionViewDataSource { ) { let file = previewFiles[indexPath.row] if let cell = cell as? DownloadingPreviewCollectionViewCell { - cell.progressiveLoadingForFile(file) + if let publicShareProxy = driveFileManager.publicShareProxy { + cell.progressiveLoadingForPublicShareFile(file, publicShareProxy: publicShareProxy) + } else { + cell.progressiveLoadingForFile(file) + } } } diff --git a/kDrive/UI/View/Files/Preview/DownloadingPreviewCollectionViewCell.swift b/kDrive/UI/View/Files/Preview/DownloadingPreviewCollectionViewCell.swift index 18d434818..f26e027c0 100644 --- a/kDrive/UI/View/Files/Preview/DownloadingPreviewCollectionViewCell.swift +++ b/kDrive/UI/View/Files/Preview/DownloadingPreviewCollectionViewCell.swift @@ -97,6 +97,26 @@ class DownloadingPreviewCollectionViewCell: UICollectionViewCell, UIScrollViewDe return previewImageView } + func progressiveLoadingForPublicShareFile(_ file: File, publicShareProxy: PublicShareProxy) { + self.file = file + file.getPublicShareThumbnail(publicShareId: publicShareProxy.shareLinkUid, + publicDriveId: publicShareProxy.driveId, + publicFileId: file.id) { thumbnail, _ in + self.previewImageView.image = thumbnail + } + + previewDownloadTask = file.getPublicSharePreview(publicShareId: publicShareProxy.shareLinkUid, + publicDriveId: publicShareProxy.driveId, + publicFileId: file.id) { [weak previewImageView] preview in + guard let previewImageView else { + return + } + if let preview { + previewImageView.image = preview + } + } + } + func progressiveLoadingForFile(_ file: File) { self.file = file file.getThumbnail { thumbnail, _ in diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index dfb913047..2ea731f11 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -77,7 +77,7 @@ public extension Endpoint { /// Share mink file preview static func shareLinkFilePreview(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/preview") + return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/preview") } /// Download share link file diff --git a/kDriveCore/Data/Models/File+Image.swift b/kDriveCore/Data/Models/File+Image.swift index 5fa873adc..e7a07e627 100644 --- a/kDriveCore/Data/Models/File+Image.swift +++ b/kDriveCore/Data/Models/File+Image.swift @@ -27,8 +27,7 @@ public extension File { publicDriveId: Int, publicFileId: Int, completion: @escaping ((UIImage, Bool) -> Void)) -> Kingfisher.DownloadTask? { - guard supportedBy.contains(.thumbnail), - let currentDriveFileManager = accountManager.currentDriveFileManager else { + guard supportedBy.contains(.thumbnail) else { completion(icon, false) return nil } @@ -72,22 +71,40 @@ public extension File { } @discardableResult - func getPreview(completion: @escaping ((UIImage?) -> Void)) -> Kingfisher.DownloadTask? { - if let currentDriveFileManager = accountManager.currentDriveFileManager { - return KingfisherManager.shared.retrieveImage(with: imagePreviewUrl, - options: [ - .requestModifier(currentDriveFileManager.apiFetcher - .authenticatedKF), - .preloadAllAnimationData - ]) { result in - if let image = try? result.get().image { - completion(image) - } else { - completion(nil) - } + func getPublicSharePreview(publicShareId: String, + publicDriveId: Int, + publicFileId: Int, + completion: @escaping ((UIImage?) -> Void)) -> Kingfisher.DownloadTask? { + let previewURL = Endpoint.shareLinkFilePreview(driveId: publicDriveId, + linkUuid: publicShareId, + fileId: publicFileId).url + + return KingfisherManager.shared.retrieveImage(with: previewURL) { result in + if let image = try? result.get().image { + completion(image) + } else { + completion(nil) } - } else { + } + } + + @discardableResult + func getPreview(completion: @escaping ((UIImage?) -> Void)) -> Kingfisher.DownloadTask? { + guard let currentDriveFileManager = accountManager.currentDriveFileManager else { return nil } + + return KingfisherManager.shared.retrieveImage(with: imagePreviewUrl, + options: [ + .requestModifier(currentDriveFileManager.apiFetcher + .authenticatedKF), + .preloadAllAnimationData + ]) { result in + if let image = try? result.get().image { + completion(image) + } else { + completion(nil) + } + } } } From c26647fb530645986b55f19697eada1db364d96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 19 Sep 2024 17:06:00 +0200 Subject: [PATCH 16/23] feat: Download file from public share --- .../MultipleSelectionFileListViewModel.swift | 9 ++- ...sFloatingPanelViewController+Actions.swift | 8 ++- ...leActionsFloatingPanelViewController.swift | 15 ++++- kDriveCore/Data/Api/Endpoint+Files.swift | 12 +++- kDriveCore/Data/Api/Endpoint+Share.swift | 4 +- .../DownloadQueue/DownloadOperation.swift | 60 ++++++++++++++++++- .../Data/DownloadQueue/DownloadQueue.swift | 35 +++++++++++ 7 files changed, 132 insertions(+), 11 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift index ea8257905..e8e854b7c 100644 --- a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift @@ -107,8 +107,15 @@ class MultipleSelectionFileListViewModel { init(configuration: FileListViewModel.Configuration, driveFileManager: DriveFileManager, currentDirectory: File) { isMultipleSelectionEnabled = false selectedCount = 0 - multipleSelectionActions = [.move, .delete, .more] + self.driveFileManager = driveFileManager + + if driveFileManager.isPublicShare { + multipleSelectionActions = [] + } else { + multipleSelectionActions = [.move, .delete, .more] + } + self.currentDirectory = currentDirectory self.configuration = configuration } diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift index e00aaaed8..ca4297391 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift @@ -58,6 +58,11 @@ extension FileActionsFloatingPanelViewController { } private func setupActions() { + guard !driveFileManager.isPublicShare else { + actions = [] + return + } + actions = (file.isDirectory ? FloatingPanelAction.folderListActions : FloatingPanelAction.listActions).filter { action in switch action { case .openWith: @@ -175,7 +180,8 @@ extension FileActionsFloatingPanelViewController { if file.isMostRecentDownloaded { presentShareSheet(from: indexPath) } else { - downloadFile(action: action, indexPath: indexPath) { [weak self] in + downloadFile(action: action, + indexPath: indexPath) { [weak self] in self?.presentShareSheet(from: indexPath) } } diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index 64a2439a2..7cdb28b7a 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -388,7 +388,9 @@ final class FileActionsFloatingPanelViewController: UICollectionViewController { present(activityViewController, animated: true) } - func downloadFile(action: FloatingPanelAction, indexPath: IndexPath, completion: @escaping () -> Void) { + func downloadFile(action: FloatingPanelAction, + indexPath: IndexPath, + completion: @escaping () -> Void) { guard let observerViewController = UIApplication.shared.windows.first?.rootViewController else { return } downloadAction = action setLoading(true, action: action, at: indexPath) @@ -405,7 +407,16 @@ final class FileActionsFloatingPanelViewController: UICollectionViewController { } } } - DownloadQueue.instance.addToQueue(file: file, userId: accountManager.currentUserId) + + if let publicShareProxy = driveFileManager.publicShareProxy { + DownloadQueue.instance.addPublicShareToQueue(file: file, + userId: accountManager.currentUserId, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) + } else { + DownloadQueue.instance.addToQueue(file: file, + userId: accountManager.currentUserId) + } } func copyShareLinkToPasteboard(from indexPath: IndexPath, link: String) { diff --git a/kDriveCore/Data/Api/Endpoint+Files.swift b/kDriveCore/Data/Api/Endpoint+Files.swift index 251ff0792..e7bd35552 100644 --- a/kDriveCore/Data/Api/Endpoint+Files.swift +++ b/kDriveCore/Data/Api/Endpoint+Files.swift @@ -180,14 +180,22 @@ public extension Endpoint { ]) } - static func download(file: AbstractFile, as asType: String? = nil) -> Endpoint { + static func download(file: AbstractFile, + publicShareProxy: PublicShareProxy? = nil, + as asType: String? = nil) -> Endpoint { let queryItems: [URLQueryItem]? if let asType { queryItems = [URLQueryItem(name: "as", value: asType)] } else { queryItems = nil } - return .fileInfoV2(file).appending(path: "/download", queryItems: queryItems) + if let publicShareProxy { + return .downloadShareLinkFile(driveId: publicShareProxy.driveId, + linkUuid: publicShareProxy.shareLinkUid, + fileId: file.id) + } else { + return .fileInfoV2(file).appending(path: "/download", queryItems: queryItems) + } } static func convert(file: AbstractFile) -> Endpoint { diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 2ea731f11..2d3d01edd 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -75,14 +75,14 @@ public extension Endpoint { return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/thumbnail") } - /// Share mink file preview + /// Share link file preview static func shareLinkFilePreview(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/preview") } /// Download share link file static func downloadShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/download") + return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/download") } func showOfficeShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation.swift index 47f9a226e..2002c47cc 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation.swift @@ -38,6 +38,7 @@ public class DownloadOperation: Operation, DownloadOperationable { private let fileManager = FileManager.default private let driveFileManager: DriveFileManager private let urlSession: FileDownloadSession + private let publicShareProxy: PublicShareProxy? private let itemIdentifier: NSFileProviderItemIdentifier? private var progressObservation: NSKeyValueObservation? private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid @@ -93,19 +94,25 @@ public class DownloadOperation: Operation, DownloadOperationable { file: File, driveFileManager: DriveFileManager, urlSession: FileDownloadSession, + publicShareProxy: PublicShareProxy? = nil, itemIdentifier: NSFileProviderItemIdentifier? = nil ) { self.file = File(value: file) self.driveFileManager = driveFileManager self.urlSession = urlSession + self.publicShareProxy = publicShareProxy self.itemIdentifier = itemIdentifier } - public init(file: File, driveFileManager: DriveFileManager, task: URLSessionDownloadTask, urlSession: FileDownloadSession) { + public init(file: File, + driveFileManager: DriveFileManager, + task: URLSessionDownloadTask, + urlSession: FileDownloadSession) { self.file = file self.driveFileManager = driveFileManager self.urlSession = urlSession self.task = task + publicShareProxy = nil itemIdentifier = nil } @@ -170,6 +177,53 @@ public class DownloadOperation: Operation, DownloadOperationable { } override public func main() { + DDLogInfo("[DownloadOperation] Start for \(file.id) with session \(urlSession.identifier)") + + if let publicShareProxy { + downloadPublicShareFile(publicShareProxy: publicShareProxy) + } else { + downloadFile() + } + } + + private func downloadPublicShareFile(publicShareProxy: PublicShareProxy) { + DDLogInfo("[DownloadOperation] Downloading publicShare \(file.id) with session \(urlSession.identifier)") + + let url = Endpoint.download(file: file, publicShareProxy: publicShareProxy).url + + // Add download task to Realm + let downloadTask = DownloadTask( + fileId: file.id, + isDirectory: file.isDirectory, + driveId: file.driveId, + userId: driveFileManager.drive.userId, + sessionId: urlSession.identifier, + sessionUrl: url.absoluteString + ) + + try? uploadsDatabase.writeTransaction { writableRealm in + writableRealm.add(downloadTask, update: .modified) + } + + let request = URLRequest(url: url) + task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) + progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { [fileId = file.id] _, value in + guard let newValue = value.newValue else { + return + } + DownloadQueue.instance.publishProgress(newValue, for: fileId) + } + if let itemIdentifier { + driveInfosManager.getFileProviderManager(for: driveFileManager.drive) { manager in + manager.register(self.task!, forItemWithIdentifier: itemIdentifier) { _ in + // META: keep SonarCloud happy + } + } + } + task?.resume() + } + + private func downloadFile() { DDLogInfo("[DownloadOperation] Downloading \(file.id) with session \(urlSession.identifier)") let url = Endpoint.download(file: file).url @@ -207,7 +261,7 @@ public class DownloadOperation: Operation, DownloadOperationable { } task?.resume() } else { - error = .localError // Other error? + error = .unknownToken // Other error? end(sessionUrl: url) } } @@ -288,7 +342,7 @@ public class DownloadOperation: Operation, DownloadOperationable { return } - assert(file.isDownloaded, "Expecting to be downloaded at the end of the downloadOperation") + assert(file.isDownloaded, "Expecting to be downloaded at the end of the downloadOperation error:\(error)") try? uploadsDatabase.writeTransaction { writableRealm in guard let task = writableRealm.objects(DownloadTask.self) diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index a0f7450b6..2b5dd97db 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -112,6 +112,41 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { // MARK: - Public methods + public func addPublicShareToQueue(file: File, + userId: Int, + driveFileManager: DriveFileManager, + publicShareProxy: PublicShareProxy, + itemIdentifier: NSFileProviderItemIdentifier? = nil) { + Log.downloadQueue("addPublicShareToQueue file:\(file.id)") + let file = file.freezeIfNeeded() + + dispatchQueue.async { + guard !self.hasOperation(for: file.id) else { + Log.downloadQueue("Already in download queue, skipping \(file.id)", level: .error) + return + } + + OperationQueueHelper.disableIdleTimer(true) + + let operation = DownloadOperation( + file: file, + driveFileManager: driveFileManager, + urlSession: self.bestSession, + publicShareProxy: publicShareProxy, + itemIdentifier: itemIdentifier + ) + operation.completionBlock = { + self.dispatchQueue.async { + self.operationsInQueue.removeValue(forKey: file.id) + self.publishFileDownloaded(fileId: file.id, error: operation.error) + OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty) + } + } + self.operationQueue.addOperation(operation) + self.operationsInQueue[file.id] = operation + } + } + public func addToQueue(file: File, userId: Int, itemIdentifier: NSFileProviderItemIdentifier? = nil) { From 76bf4e073f44317ba2188ba215f50991444fee00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 20 Sep 2024 11:17:33 +0200 Subject: [PATCH 17/23] feat: Actions match spec on public share --- .../Create File/FloatingPanelLayouts.swift | 141 ++++++++++++++++++ .../Create File/FloatingPanelUtils.swift | 65 -------- .../File List/FileListViewController.swift | 31 +++- ...sFloatingPanelViewController+Actions.swift | 15 +- ...leActionsFloatingPanelViewController.swift | 8 + 5 files changed, 188 insertions(+), 72 deletions(-) create mode 100644 kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift diff --git a/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift b/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift new file mode 100644 index 000000000..1e1b5f92a --- /dev/null +++ b/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift @@ -0,0 +1,141 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import FloatingPanel +import kDriveCore +import kDriveResources +import UIKit + +/// Layout used for a folder within a public share +class PublicShareFolderFloatingPanelLayout: FloatingPanelLayout { + var position: FloatingPanelPosition = .bottom + var initialState: FloatingPanelState = .tip + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] + private var backdropAlpha: CGFloat + + init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) { + self.initialState = initialState + self.backdropAlpha = backdropAlpha + let extendedAnchor = FloatingPanelLayoutAnchor( + absoluteInset: 140.0 + safeAreaInset, + edge: .bottom, + referenceGuide: .superview + ) + anchors = [ + .full: extendedAnchor, + .half: extendedAnchor, + .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview) + ] + } + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + return backdropAlpha + } +} + +/// Layout used for a file within a public share +class PublicShareFileFloatingPanelLayout: FloatingPanelLayout { + var position: FloatingPanelPosition = .bottom + var initialState: FloatingPanelState = .tip + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] + private var backdropAlpha: CGFloat + + init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) { + self.initialState = initialState + self.backdropAlpha = backdropAlpha + let extendedAnchor = FloatingPanelLayoutAnchor( + absoluteInset: 248.0 + safeAreaInset, + edge: .bottom, + referenceGuide: .superview + ) + anchors = [ + .full: extendedAnchor, + .half: extendedAnchor, + .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview) + ] + } + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + return backdropAlpha + } +} + +class FileFloatingPanelLayout: FloatingPanelLayout { + var position: FloatingPanelPosition = .bottom + var initialState: FloatingPanelState = .tip + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] + private var backdropAlpha: CGFloat + + init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) { + self.initialState = initialState + self.backdropAlpha = backdropAlpha + if hideTip { + anchors = [ + .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), + .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea) + ] + } else { + anchors = [ + .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), + .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea), + .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview) + ] + } + } + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + return backdropAlpha + } +} + +class PlusButtonFloatingPanelLayout: FloatingPanelLayout { + var position: FloatingPanelPosition = .bottom + var height: CGFloat = 16 + + init(height: CGFloat) { + self.height = height + } + + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { + return [ + .full: FloatingPanelLayoutAnchor(absoluteInset: height, edge: .bottom, referenceGuide: .safeArea) + ] + } + + var initialState: FloatingPanelState = .full + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + return 0.2 + } +} + +class InformationViewFloatingPanelLayout: FloatingPanelLayout { + var position: FloatingPanelPosition = .bottom + + var initialState: FloatingPanelState = .full + + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { + return [ + .full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea) + ] + } + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + return 0.3 + } +} diff --git a/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift b/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift index 793edb132..e5ca7bc58 100644 --- a/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift +++ b/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift @@ -73,68 +73,3 @@ class AdaptiveDriveFloatingPanelController: DriveFloatingPanelController { track(scrollView: scrollView) } } - -class FileFloatingPanelLayout: FloatingPanelLayout { - var position: FloatingPanelPosition = .bottom - var initialState: FloatingPanelState = .tip - var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] - private var backdropAlpha: CGFloat - - init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) { - self.initialState = initialState - self.backdropAlpha = backdropAlpha - if hideTip { - anchors = [ - .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), - .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea) - ] - } else { - anchors = [ - .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), - .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea), - .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview) - ] - } - } - - func backdropAlpha(for state: FloatingPanelState) -> CGFloat { - return backdropAlpha - } -} - -class PlusButtonFloatingPanelLayout: FloatingPanelLayout { - var position: FloatingPanelPosition = .bottom - var height: CGFloat = 16 - - init(height: CGFloat) { - self.height = height - } - - var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { - return [ - .full: FloatingPanelLayoutAnchor(absoluteInset: height, edge: .bottom, referenceGuide: .safeArea) - ] - } - - var initialState: FloatingPanelState = .full - - func backdropAlpha(for state: FloatingPanelState) -> CGFloat { - return 0.2 - } -} - -class InformationViewFloatingPanelLayout: FloatingPanelLayout { - var position: FloatingPanelPosition = .bottom - - var initialState: FloatingPanelState = .full - - var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { - return [ - .full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea) - ] - } - - func backdropAlpha(for state: FloatingPanelState) -> CGFloat { - return 0.3 - } -} diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 757dd1de9..d205d65e7 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -25,6 +25,7 @@ import kDriveCore import kDriveResources import RealmSwift import UIKit +import FloatingPanel extension SwipeCellAction { static let share = SwipeCellAction( @@ -346,6 +347,30 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } } + private func fileLayout(files: [File]) -> FloatingPanelLayout { + guard driveFileManager.isPublicShare else { + return FileFloatingPanelLayout( + initialState: .half, + hideTip: true, + backdropAlpha: 0.2 + ) + } + + if files.first?.isDirectory ?? false { + return PublicShareFolderFloatingPanelLayout( + initialState: .half, + hideTip: true, + backdropAlpha: 0.2 + ) + } else { + return PublicShareFileFloatingPanelLayout( + initialState: .half, + hideTip: true, + backdropAlpha: 0.2 + ) + } + } + private func showQuickActionsPanel(files: [File], actionType: FileListQuickActionType) { #if !ISEXTENSION var floatingPanelViewController: DriveFloatingPanelController @@ -357,11 +382,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV fileInformationsViewController.presentingParent = self fileInformationsViewController.normalFolderHierarchy = viewModel.configuration.normalFolderHierarchy - floatingPanelViewController.layout = FileFloatingPanelLayout( - initialState: .half, - hideTip: true, - backdropAlpha: 0.2 - ) + floatingPanelViewController.layout = fileLayout(files: files) if let file = files.first { fileInformationsViewController.setFile(file, driveFileManager: driveFileManager) diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift index ca4297391..004eba077 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift @@ -36,7 +36,14 @@ extension FileActionsFloatingPanelViewController { private func setupQuickActions() { let offline = ReachabilityListener.instance.currentStatus == .offline - quickActions = file.isDirectory ? FloatingPanelAction.folderQuickActions : FloatingPanelAction.quickActions + if driveFileManager.isPublicShare { + quickActions = [] + } else if file.isDirectory { + quickActions = FloatingPanelAction.folderQuickActions + } else { + quickActions = FloatingPanelAction.quickActions + } + for action in quickActions { switch action { case .shareAndRights: @@ -59,7 +66,11 @@ extension FileActionsFloatingPanelViewController { private func setupActions() { guard !driveFileManager.isPublicShare else { - actions = [] + if file.isDirectory { + actions = FloatingPanelAction.publicShareFolderActions + } else { + actions = FloatingPanelAction.publicShareActions + } return } diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index 7cdb28b7a..066d5540c 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -205,6 +205,14 @@ public class FloatingPanelAction: Equatable { return [informations, add, shareAndRights, shareLink].map { $0.reset() } } + static var publicShareActions: [FloatingPanelAction] { + return [openWith, sendCopy, download].map { $0.reset() } + } + + static var publicShareFolderActions: [FloatingPanelAction] { + return [download].map { $0.reset() } + } + static var multipleSelectionActions: [FloatingPanelAction] { return [manageCategories, favorite, offline, download, move, duplicate].map { $0.reset() } } From 14c2ca0134040569daf1b27170feb61ee6ce4fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 23 Sep 2024 08:03:14 +0200 Subject: [PATCH 18/23] fix: Tuist config update to build --- Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift b/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift index e980f412f..29445bbf3 100644 --- a/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift +++ b/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift @@ -68,6 +68,7 @@ public extension Target { "kDrive/UI/Controller/DriveUpdateRequiredViewController.swift", "kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift", "kDrive/UI/Controller/Create File/FloatingPanelUtils.swift", + "kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift", "kDrive/UI/Controller/Files/Categories/**", "kDrive/UI/Controller/Files/Rights and Share/**", "kDrive/UI/Controller/Files/Save File/**", From 7224695841c9ca16ee752ce44a0bda2fdfe9664f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 23 Sep 2024 15:14:19 +0200 Subject: [PATCH 19/23] feat: Add to my drive button --- kDrive/AppDelegate.swift | 12 --- kDrive/AppRouter.swift | 7 +- kDrive/SceneDelegate.swift | 1 + .../File List/FileListViewController.swift | 78 ++++++++++++++++++- 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index e366d0867..b61288e25 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -103,18 +103,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } application.registerForRemoteNotifications() - // swiftlint:disable force_try - Task { - try! await Task.sleep(nanoseconds:5_000_000_000) - print("coucou") - let somePublicShare = URL(string: "") - //await UIApplication.shared.open(somePublicShare!) // opens safari - - let components = URLComponents(url: somePublicShare!, resolvingAgainstBaseURL: true) - await UniversalLinksHelper.handlePath(components!.path) - } - - return true } diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 66a8b000f..5bef6867c 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -591,7 +591,6 @@ public struct AppRouter: AppNavigable { driveFileManager: DriveFileManager, apiFetcher: PublicShareApiFetcher ) { - // TODO: Present on top of existing views guard let window, let rootViewController = window.rootViewController else { fatalError("TODO: lazy load a rootViewController") @@ -621,9 +620,11 @@ public struct AppRouter: AppNavigable { currentDirectory: frozenRootFolder, apiFetcher: apiFetcher) let viewController = FileListViewController(viewModel: viewModel) - print("viewController:\(viewController) viewModel:\(viewModel) navigationController:\(navigationController)") + let publicShareNavigationController = UINavigationController(rootViewController: viewController) + publicShareNavigationController.modalPresentationStyle = .fullScreen + publicShareNavigationController.modalTransitionStyle = .coverVertical - navigationController.pushViewController(viewController, animated: true) + navigationController.present(publicShareNavigationController, animated: true, completion: nil) } } diff --git a/kDrive/SceneDelegate.swift b/kDrive/SceneDelegate.swift index 317186adf..25a69a2c9 100644 --- a/kDrive/SceneDelegate.swift +++ b/kDrive/SceneDelegate.swift @@ -234,6 +234,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDel guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let incomingURL = userActivity.webpageURL, let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { + Log.sceneDelegate("scene continue userActivity - unable", level: .error) return } diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 2971dd5b6..146bbba86 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -19,13 +19,13 @@ import CocoaLumberjackSwift import Combine import DifferenceKit +import FloatingPanel import InfomaniakCore import InfomaniakDI import kDriveCore import kDriveResources import RealmSwift import UIKit -import FloatingPanel extension SwipeCellAction { static let share = SwipeCellAction( @@ -142,6 +142,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV ) setupViewModel() + setupFooterIfNeeded() } override func viewWillAppear(_ animated: Bool) { @@ -251,6 +252,46 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } } + func setupFooterIfNeeded() { + guard driveFileManager.isPublicShare else { + return + } + + let addToKDriveButton = IKButton(type: .custom) + addToKDriveButton.setTitle("Add to My Drive", for: .normal) + addToKDriveButton.addTarget(self, action: #selector(addToMyDriveButtonTapped), for: .touchUpInside) + addToKDriveButton.setBackgroundColors(normal: .systemBlue, highlighted: .darkGray) + addToKDriveButton.translatesAutoresizingMaskIntoConstraints = false + addToKDriveButton.cornerRadius = 8.0 + addToKDriveButton.clipsToBounds = true + + view.addSubview(addToKDriveButton) + view.bringSubviewToFront(addToKDriveButton) + + let leadingConstraint = addToKDriveButton.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, + constant: 16) + leadingConstraint.priority = .defaultHigh + let trailingConstraint = addToKDriveButton.trailingAnchor.constraint( + greaterThanOrEqualTo: view.trailingAnchor, + constant: -16 + ) + trailingConstraint.priority = .defaultHigh + let widthConstraint = addToKDriveButton.widthAnchor.constraint(lessThanOrEqualToConstant: 360) + + NSLayoutConstraint.activate([ + addToKDriveButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + leadingConstraint, + trailingConstraint, + addToKDriveButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), + addToKDriveButton.heightAnchor.constraint(equalToConstant: 60), + widthConstraint + ]) + } + + @objc func addToMyDriveButtonTapped() { + print("button tapped") + } + func reloadCollectionViewWith(files: [File]) { let changeSet = StagedChangeset(source: displayedFiles, target: files) collectionView.reload(using: changeSet, @@ -915,3 +956,38 @@ extension FileListViewController: UICollectionViewDropDelegate { } } } + +// Move to CoreUIKit or use something else ? +extension UIImage { + convenience init?(color: UIColor) { + let size = CGSize(width: 1, height: 1) + UIGraphicsBeginImageContext(size) + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + guard let cgImage = image.cgImage else { + return nil + } + + self.init(cgImage: cgImage) + } +} + +// Move to CoreUIKit or use something else ? +extension IKButton { + func setBackgroundColors(normal normalColor: UIColor, highlighted highlightedColor: UIColor) { + if let normalImage = UIImage(color: normalColor) { + setBackgroundImage(normalImage, for: .normal) + } + + if let highlightedImage = UIImage(color: highlightedColor) { + setBackgroundImage(highlightedImage, for: .highlighted) + } + } +} From a1bf2c429d5b8335e8b4a4ea07267321d9edb0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 24 Sep 2024 18:06:56 +0200 Subject: [PATCH 20/23] WIP --- kDrive/AppDelegate.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index b61288e25..3b9c5ef2d 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -103,6 +103,17 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } application.registerForRemoteNotifications() + // swiftlint:disable force_try + Task { + try! await Task.sleep(nanoseconds:5_000_000_000) + print("coucou") + let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/01953831-16d3-4df6-8b48-33c8001c7981") + //await UIApplication.shared.open(somePublicShare!) // opens safari + + let components = URLComponents(url: somePublicShare!, resolvingAgainstBaseURL: true) + await UniversalLinksHelper.handlePath(components!.path) + } + return true } From 31e6119d713800ca78ed733cae33e60573912d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 25 Sep 2024 14:45:42 +0200 Subject: [PATCH 21/23] chore: Matomo for public share tasks --- ...leActionsFloatingPanelViewController.swift | 12 +++++++- ...SelectionFloatingPanelViewController.swift | 12 +++++++- kDrive/Utils/MatomoUtils+UI.swift | 6 ++-- kDrive/Utils/UniversalLinksHelper.swift | 10 +++++++ kDriveCore/Data/Api/DriveApiFetcher.swift | 4 +++ kDriveCore/Utils/DeeplinkParser.swift | 3 ++ kDriveCore/Utils/MatomoUtils.swift | 29 +++++++++++++++++-- 7 files changed, 68 insertions(+), 8 deletions(-) diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index b4f2643c0..117f817b2 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -496,7 +496,17 @@ final class FileActionsFloatingPanelViewController: UICollectionViewController { case .actions: action = actions[indexPath.item] } - MatomoUtils.trackFileAction(action: action, file: file, fromPhotoList: presentingParent is PhotoListViewController) + + let eventCategory: MatomoUtils.EventCategory + if presentingParent is PhotoListViewController { + eventCategory = .picturesFileAction + } else if driveFileManager.isPublicShare { + eventCategory = .publicShareAction + } else { + eventCategory = .fileListFileAction + } + + MatomoUtils.trackFileAction(action: action, file: file, category: eventCategory) handleAction(action, at: indexPath) } } diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift index a22f6035a..a17b0aa76 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift @@ -209,6 +209,16 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let action = actions[indexPath.item] handleAction(action, at: indexPath) - MatomoUtils.trackBuklAction(action: action, files: files, fromPhotoList: presentingParent is PhotoListViewController) + + let eventCategory: MatomoUtils.EventCategory + if presentingParent is PhotoListViewController { + eventCategory = .picturesFileAction + } else if driveFileManager.isPublicShare { + eventCategory = .publicShareAction + } else { + eventCategory = .fileListFileAction + } + + MatomoUtils.trackBuklAction(action: action, files: files, category: eventCategory) } } diff --git a/kDrive/Utils/MatomoUtils+UI.swift b/kDrive/Utils/MatomoUtils+UI.swift index e2a828f5c..88082c8ee 100644 --- a/kDrive/Utils/MatomoUtils+UI.swift +++ b/kDrive/Utils/MatomoUtils+UI.swift @@ -45,8 +45,7 @@ extension MatomoUtils { #if !ISEXTENSION - static func trackFileAction(action: FloatingPanelAction, file: File, fromPhotoList: Bool) { - let category: EventCategory = fromPhotoList ? .picturesFileAction : .fileListFileAction + static func trackFileAction(action: FloatingPanelAction, file: File, category: EventCategory) { switch action { // Quick Actions case .sendCopy: @@ -77,9 +76,8 @@ extension MatomoUtils { } } - static func trackBuklAction(action: FloatingPanelAction, files: [File], fromPhotoList: Bool) { + static func trackBuklAction(action: FloatingPanelAction, files: [File], category: EventCategory) { let numberOfFiles = files.count - let category: EventCategory = fromPhotoList ? .picturesFileAction : .fileListFileAction switch action { // Quick Actions case .duplicate: diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 38c72c2cf..10718c026 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -101,6 +101,16 @@ enum UniversalLinksHelper { return false } + let trackerName: String + if metadata.isPasswordNeeded { + trackerName = "publicShareWithPassword" + } else if metadata.isExpired { + trackerName = "publicShareExpired" + } else { + trackerName = "publicShare" + } + MatomoUtils.trackDeeplink(name: trackerName) + // get file ID from metadata let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager( for: shareLinkUid, diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 108cebef3..ed27a7d51 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -59,6 +59,10 @@ public struct PublicShareMetadata: Decodable { public let validUntil: TimeInterval? public let capabilities: Rights + + // TODO: Test parsing + public let isPasswordNeeded: Bool = false + public let isExpired: Bool = false public let createdBy: TimeInterval public let createdAt: TimeInterval diff --git a/kDriveCore/Utils/DeeplinkParser.swift b/kDriveCore/Utils/DeeplinkParser.swift index 1af20238b..80ab6d180 100644 --- a/kDriveCore/Utils/DeeplinkParser.swift +++ b/kDriveCore/Utils/DeeplinkParser.swift @@ -17,6 +17,7 @@ */ import InfomaniakDI +import MatomoTracker import SwiftUI /// Deeplink entrypoint @@ -50,6 +51,7 @@ public struct DeeplinkParser: DeeplinkParsable { let driveId = params.first(where: { $0.name == "driveId" })?.value, let driveIdInt = Int(driveId), let userIdInt = Int(userId) { await router.navigate(to: .store(driveId: driveIdInt, userId: userIdInt)) + MatomoUtils.trackDeeplink(name: DeeplinkPath.store.rawValue) return true } else if components.host == DeeplinkPath.file.rawValue, @@ -57,6 +59,7 @@ public struct DeeplinkParser: DeeplinkParsable { let fileUrl = URL(fileURLWithPath: filePath) let file = ImportedFile(name: fileUrl.lastPathComponent, path: fileUrl, uti: fileUrl.uti ?? .data) await router.navigate(to: .saveFile(file: file)) + MatomoUtils.trackDeeplink(name: DeeplinkPath.file.rawValue) return true } diff --git a/kDriveCore/Utils/MatomoUtils.swift b/kDriveCore/Utils/MatomoUtils.swift index c63b72b29..444fb4e49 100644 --- a/kDriveCore/Utils/MatomoUtils.swift +++ b/kDriveCore/Utils/MatomoUtils.swift @@ -44,7 +44,7 @@ public enum MatomoUtils { public enum EventCategory: String { case newElement, fileListFileAction, picturesFileAction, fileInfo, shareAndRights, colorFolder, categories, search, fileList, comment, drive, account, settings, photoSync, home, displayList, inApp, trash, - dropbox, preview, mediaPlayer, shortcuts, appReview + dropbox, preview, mediaPlayer, shortcuts, appReview, deeplink, publicShareAction, publicSharePasswordAction } public enum UserAction: String { @@ -64,7 +64,12 @@ public enum MatomoUtils { shared.track(view: view) } - public static func track(eventWithCategory category: EventCategory, action: UserAction = .click, name: String, value: Float? = nil) { + public static func track( + eventWithCategory category: EventCategory, + action: UserAction = .click, + name: String, + value: Float? = nil + ) { shared.track(eventWithCategory: category.rawValue, action: action.rawValue, name: name, value: value) } @@ -122,4 +127,24 @@ public enum MatomoUtils { public static func trackMediaPlayer(leaveAt percentage: Double?) { track(eventWithCategory: .mediaPlayer, name: "duration", value: Float(percentage ?? 0)) } + + // MARK: - Deeplink + + public static func trackDeeplink(name: String) { + track(eventWithCategory: .deeplink, name: name) + } + + // MARK: - Public Share + + public static func trackAddToMykDrive() { + track(eventWithCategory: .publicShareAction, name: "addToMykDrive") + } + + public static func trackAddBulkToMykDrive() { + track(eventWithCategory: .publicShareAction, name: "bulkaddToMykDrive") + } + + public static func trackPublicSharePasswordAction() { + track(eventWithCategory: .publicSharePasswordAction, name: "openInBrowser") + } } From a4b8d70c8a40ac098c7238fecba9e446610af61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 26 Sep 2024 10:22:35 +0200 Subject: [PATCH 22/23] chore: Align Matomo with android on what exists --- kDriveCore/Utils/MatomoUtils.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kDriveCore/Utils/MatomoUtils.swift b/kDriveCore/Utils/MatomoUtils.swift index 444fb4e49..1d0c46478 100644 --- a/kDriveCore/Utils/MatomoUtils.swift +++ b/kDriveCore/Utils/MatomoUtils.swift @@ -137,11 +137,11 @@ public enum MatomoUtils { // MARK: - Public Share public static func trackAddToMykDrive() { - track(eventWithCategory: .publicShareAction, name: "addToMykDrive") + track(eventWithCategory: .publicShareAction, name: "saveToKDrive") } public static func trackAddBulkToMykDrive() { - track(eventWithCategory: .publicShareAction, name: "bulkaddToMykDrive") + track(eventWithCategory: .publicShareAction, name: "bulkSaveToKDrive") } public static func trackPublicSharePasswordAction() { From e7d49dcd3466ccc92913abbe504824a01f595510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 26 Sep 2024 16:32:28 +0200 Subject: [PATCH 23/23] feat: Download all files from current folder as a ZIP within a public share --- .../File List/ConcreteFileListViewModel.swift | 4 +- .../File List/FileListViewController.swift | 6 +- .../Files/File List/FileListViewModel.swift | 3 +- ...leActionsFloatingPanelViewController.swift | 1 - .../Files/Search/SearchFilesViewModel.swift | 19 +++--- .../Menu/PhotoList/PhotoListViewModel.swift | 4 +- .../Menu/Share/PublicShareViewModel.swift | 59 +++++++++++++++++++ .../Menu/Trash/TrashListViewModel.swift | 4 +- kDrive/UI/View/Files/FileListBarButton.swift | 6 +- .../Data/DownloadQueue/DownloadQueue.swift | 1 - kDriveCore/Data/Models/File.swift | 3 +- 11 files changed, 87 insertions(+), 23 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/ConcreteFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/ConcreteFileListViewModel.swift index 701e596fe..a5bd73668 100644 --- a/kDrive/UI/Controller/Files/File List/ConcreteFileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/ConcreteFileListViewModel.swift @@ -59,13 +59,13 @@ class ConcreteFileListViewModel: FileListViewModel { try await loadFiles() } - override func barButtonPressed(type: FileListBarButtonType) { + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { if type == .search { let viewModel = SearchFilesViewModel(driveFileManager: driveFileManager) let searchViewController = SearchViewController.instantiateInNavigationController(viewModel: viewModel) onPresentViewController?(.modal, searchViewController, true) } else { - super.barButtonPressed(type: type) + super.barButtonPressed(sender: sender, type: type) } } } diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 146bbba86..5ed785f0f 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -258,7 +258,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } let addToKDriveButton = IKButton(type: .custom) - addToKDriveButton.setTitle("Add to My Drive", for: .normal) + addToKDriveButton.setTitle("Add to My kDrive", for: .normal) addToKDriveButton.addTarget(self, action: #selector(addToMyDriveButtonTapped), for: .touchUpInside) addToKDriveButton.setBackgroundColors(normal: .systemBlue, highlighted: .darkGray) addToKDriveButton.translatesAutoresizingMaskIntoConstraints = false @@ -289,7 +289,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } @objc func addToMyDriveButtonTapped() { - print("button tapped") + print("TODO: addToMyDriveButtonTapped") } func reloadCollectionViewWith(files: [File]) { @@ -525,7 +525,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } @objc func barButtonPressed(_ sender: FileListBarButton) { - viewModel.barButtonPressed(type: sender.type) + viewModel.barButtonPressed(sender: sender, type: sender.type) } @objc func forceRefresh() { diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index f43164e12..cf1db00fc 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -34,6 +34,7 @@ enum FileListBarButtonType { case searchFilters case photoSort case addFolder + case downloadAll } enum FileListQuickActionType { @@ -279,7 +280,7 @@ class FileListViewModel: SelectDelegate { }.store(in: &bindStore) } - func barButtonPressed(type: FileListBarButtonType) { + func barButtonPressed(sender: Any? = nil, type: FileListBarButtonType) { if multipleSelectionViewModel?.isMultipleSelectionEnabled == true { multipleSelectionViewModel?.barButtonPressed(type: type) } diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index 117f817b2..d462c2d3d 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -418,7 +418,6 @@ final class FileActionsFloatingPanelViewController: UICollectionViewController { if let publicShareProxy = driveFileManager.publicShareProxy { DownloadQueue.instance.addPublicShareToQueue(file: file, - userId: accountManager.currentUserId, driveFileManager: driveFileManager, publicShareProxy: publicShareProxy) } else { diff --git a/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift b/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift index 99f975988..32d1acab3 100644 --- a/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift +++ b/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift @@ -84,7 +84,8 @@ class SearchFilesViewModel: FileListViewModel { filters = Filters() let searchFakeRoot = driveFileManager.getManagedFile(from: DriveFileManager.searchFilesRootFile) super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: searchFakeRoot) - observedFiles = AnyRealmCollection(AnyRealmCollection(searchFakeRoot.children).sorted(by: [sortType.value.sortDescriptor])) + observedFiles = AnyRealmCollection(AnyRealmCollection(searchFakeRoot.children) + .sorted(by: [sortType.value.sortDescriptor])) } override func startObservation() { @@ -145,16 +146,16 @@ class SearchFilesViewModel: FileListViewModel { private func searchOffline() { observedFiles = AnyRealmCollection(driveFileManager.searchOffline(query: currentSearchText, - date: filters.date?.dateInterval, - fileType: filters.fileType, - categories: Array(filters.categories), - fileExtensions: filters.fileExtensions, - belongToAllCategories: filters.belongToAllCategories, - sortType: sortType)) + date: filters.date?.dateInterval, + fileType: filters.fileType, + categories: Array(filters.categories), + fileExtensions: filters.fileExtensions, + belongToAllCategories: filters.belongToAllCategories, + sortType: sortType)) startObservation() } - override func barButtonPressed(type: FileListBarButtonType) { + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { if type == .searchFilters { let navigationController = SearchFiltersViewController .instantiateInNavigationController(driveFileManager: driveFileManager) @@ -163,7 +164,7 @@ class SearchFilesViewModel: FileListViewModel { searchFiltersViewController?.delegate = self onPresentViewController?(.modal, navigationController, true) } else { - super.barButtonPressed(type: type) + super.barButtonPressed(sender: sender, type: type) } } diff --git a/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift b/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift index 72d46e53e..d6378a65e 100644 --- a/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift +++ b/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift @@ -144,7 +144,7 @@ class PhotoListViewModel: FileListViewModel { self.nextCursor = nextCursor } - override func barButtonPressed(type: FileListBarButtonType) { + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { if type == .search { let viewModel = SearchFilesViewModel(driveFileManager: driveFileManager, filters: Filters(fileType: .image)) let searchViewController = SearchViewController.instantiateInNavigationController(viewModel: viewModel) @@ -156,7 +156,7 @@ class PhotoListViewModel: FileListViewModel { delegate: self) onPresentViewController?(.modal, floatingPanelViewController, true) } else { - super.barButtonPressed(type: type) + super.barButtonPressed(sender: sender, type: type) } } diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 0898ce34d..69273f79f 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -16,12 +16,16 @@ along with this program. If not, see . */ +import InfomaniakCore +import InfomaniakDI import kDriveCore import RealmSwift import UIKit /// Public share view model, loading content from memory realm final class PublicShareViewModel: InMemoryFileListViewModel { + private var downloadObserver: ObservationToken? + var publicShareProxy: PublicShareProxy? let rootProxy: ProxyFile var publicShareApiFetcher: PublicShareApiFetcher? @@ -36,6 +40,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { rootTitle: "public share", emptyViewType: .emptyFolder, supportsDrop: false, + rightBarButtons: [.downloadAll], matomoViewPath: [MatomoUtils.Views.menu.displayName, "publicShare"]) rootProxy = currentDirectory.proxify() @@ -79,4 +84,58 @@ final class PublicShareViewModel: InMemoryFileListViewModel { try await loadFiles(cursor: nextCursor) } } + + // TODO: Move away from view model + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { + guard downloadObserver == nil else { + return + } + + guard type == .downloadAll, + let publicShareProxy = publicShareProxy else { + return + } + + // TODO: Abstract sheet presentation + @InjectService var appNavigable: AppNavigable + guard let topMostViewController = appNavigable.topMostViewController else { + return + } + + downloadObserver = DownloadQueue.instance + .observeFileDownloaded(self, fileId: currentDirectory.id) { [weak self] _, error in + Task { @MainActor in + guard let self = self else { + return + } + + defer { + self.downloadObserver?.cancel() + self.downloadObserver = nil + } + + guard let senderItem = sender as? UIBarButtonItem else { + return + } + + guard error == nil else { + UIConstants.showSnackBarIfNeeded(error: DriveError.downloadFailed) + return + } + + // present share sheet + let activityViewController = UIActivityViewController( + activityItems: [self.currentDirectory.localUrl], + applicationActivities: nil + ) + + activityViewController.popoverPresentationController?.barButtonItem = senderItem + topMostViewController.present(activityViewController, animated: true) + } + } + + DownloadQueue.instance.addPublicShareToQueue(file: currentDirectory, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) + } } diff --git a/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift b/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift index 67ab3d778..e77094413 100644 --- a/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift +++ b/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift @@ -92,7 +92,7 @@ class TrashListViewModel: InMemoryFileListViewModel { forceRefresh() } - override func barButtonPressed(type: FileListBarButtonType) { + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { if type == .emptyTrash { let alert = AlertTextViewController(title: KDriveResourcesStrings.Localizable.modalEmptyTrashTitle, message: KDriveResourcesStrings.Localizable.modalEmptyTrashDescription, @@ -103,7 +103,7 @@ class TrashListViewModel: InMemoryFileListViewModel { } onPresentViewController?(.modal, alert, true) } else { - super.barButtonPressed(type: type) + super.barButtonPressed(sender: sender, type: type) } } diff --git a/kDrive/UI/View/Files/FileListBarButton.swift b/kDrive/UI/View/Files/FileListBarButton.swift index c55363306..62984460f 100644 --- a/kDrive/UI/View/Files/FileListBarButton.swift +++ b/kDrive/UI/View/Files/FileListBarButton.swift @@ -17,8 +17,8 @@ */ import Foundation -import UIKit import kDriveResources +import UIKit final class FileListBarButton: UIBarButtonItem { private(set) var type: FileListBarButtonType = .cancel @@ -49,6 +49,10 @@ final class FileListBarButton: UIBarButtonItem { case .addFolder: self.init(image: KDriveResourcesAsset.folderAdd.image, style: .plain, target: target, action: action) accessibilityLabel = KDriveResourcesStrings.Localizable.createFolderTitle + case .downloadAll: + let image = KDriveResourcesAsset.download.image + self.init(image: image, style: .plain, target: target, action: action) + accessibilityLabel = KDriveResourcesStrings.Localizable.buttonDownload } self.type = type } diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index 2b5dd97db..db2eaf452 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -113,7 +113,6 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { // MARK: - Public methods public func addPublicShareToQueue(file: File, - userId: Int, driveFileManager: DriveFileManager, publicShareProxy: PublicShareProxy, itemIdentifier: NSFileProviderItemIdentifier? = nil) { diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index e4625c92d..79c36649d 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -554,7 +554,8 @@ public final class File: Object, Codable { public var isDownloaded: Bool { let localPath = localUrl.path - guard fileManager.fileExists(atPath: localPath) else { + let temporaryPath = temporaryUrl.path + guard fileManager.fileExists(atPath: localPath) || fileManager.fileExists(atPath: temporaryPath) else { DDLogError("[File] no local copy to read from") return false }