mirror of
https://github.com/ProtonDriveApps/mac-drive.git
synced 2026-05-15 09:50:33 +00:00
2.6.0
This commit is contained in:
+92
@@ -0,0 +1,92 @@
|
||||
Created by https://www.gitignore.io/api/xcode,osx
|
||||
|
||||
### OSX ###
|
||||
*.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Xcode ###
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
.build/
|
||||
DerivedData/
|
||||
|
||||
## Fastlane
|
||||
outputs
|
||||
*.p12
|
||||
*.provisionprofile
|
||||
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata/
|
||||
|
||||
## Other
|
||||
*.moved-aside
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
Carthage/
|
||||
.vscode
|
||||
|
||||
### Xcode Patch ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
/*.gcno
|
||||
# End of https://www.gitignore.io/api/xcode,osx
|
||||
|
||||
# Fastlane
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/test_output
|
||||
|
||||
# Obfuscated constants
|
||||
**/ObfuscatedConstants.swift
|
||||
|
||||
# DDK
|
||||
PDDesktopDevKit/Libraries
|
||||
PDDesktopDevKit/Package.resolved
|
||||
PDFileProviderOperations/Package.resolved
|
||||
|
||||
# Packagemodifier
|
||||
Scripts/opensource/packagemodifier/.build
|
||||
Scripts/opensource/packagemodifier/Packages
|
||||
Scripts/opensource/packagemodifier/.swiftpm/configuration/registries.json
|
||||
Scripts/opensource/packagemodifier/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
|
||||
# XcodeGen
|
||||
ProtonDrive-iOS/ProtonDrive-iOS.xcodeproj
|
||||
|
||||
# Dynamic domain
|
||||
.dynamic_domain
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>PDClient-Package.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>12</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
public enum APIErrorCodes: Int, Equatable {
|
||||
case itemOrItsParentDeletedErrorCode = 2501 // NOT_EXISTS
|
||||
case protonDocumentCannotBeCreatedFromMacOSAppErrorCode = 200701 // FILE_CREATION_NOT_ENABLED_FOR_DOCUMENTS
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct DeleteBookmarkEndpoint: Endpoint {
|
||||
|
||||
public struct Response: Codable {
|
||||
public var code: Int
|
||||
}
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
public init(token: String, service: APIService, credential: ClientCredential) {
|
||||
let url = service.url(of: "/v2/urls/\(token)/bookmark")
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "DELETE"
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
self.request = request
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Response Structure
|
||||
public struct ListBookmarksResponse: Codable {
|
||||
public let bookmarks: [BookmarkResponse]
|
||||
public let code: Int
|
||||
|
||||
public struct BookmarkResponse: Codable {
|
||||
public let encryptedUrlPassword: String
|
||||
public let createTime: Int
|
||||
public let token: TokenResponse
|
||||
|
||||
public struct TokenResponse: Codable {
|
||||
public let token: String
|
||||
public let linkType: Int
|
||||
public let linkID: String
|
||||
public let sharePasswordSalt: String
|
||||
public let sharePassphrase: String
|
||||
public let shareKey: String
|
||||
public let nodePassphrase: String
|
||||
public let nodeKey: String
|
||||
public let name: String
|
||||
public let contentKeyPacket: String?
|
||||
public let MIMEType: String?
|
||||
public let permissions: Int
|
||||
public let size: Int?
|
||||
public let thumbnailURLInfo: ThumbnailURLInfoResponse?
|
||||
public let nodeHashKey: String?
|
||||
|
||||
public struct ThumbnailURLInfoResponse: Codable {
|
||||
public let url: String?
|
||||
public let bareURL: String?
|
||||
public let token: String?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Endpoint
|
||||
/// Fetch the list of bookmarks
|
||||
/// - GET: /drive/v2/bookmarks
|
||||
public struct ListBookmarksEndpoint: Endpoint {
|
||||
public typealias Response = ListBookmarksResponse
|
||||
|
||||
public let request: URLRequest
|
||||
|
||||
public init(service: APIService, credential: ClientCredential) {
|
||||
let url = service.url(of: "/v2/shared-bookmarks")
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
@@ -49,31 +49,6 @@ extension Client {
|
||||
return response.shareURLs
|
||||
}
|
||||
|
||||
public func listVolumeTrash(volumeID: VolumeID, page: Int, pageSize: Int) async throws -> ListVolumeTrashEndpoint.Response {
|
||||
let credential = try credential()
|
||||
|
||||
let endpoint = ListVolumeTrashEndpoint(
|
||||
parameters: .init(
|
||||
volumeId: volumeID,
|
||||
page: page,
|
||||
pageSize: pageSize
|
||||
),
|
||||
service: service,
|
||||
credential: credential
|
||||
)
|
||||
|
||||
return try await request(endpoint)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func trashNodes(parameters: TrashLinksParameters, breadcrumbs: Breadcrumbs) async throws -> MultipleLinkResponse {
|
||||
guard let credential = self.credentialProvider.clientCredential() else {
|
||||
throw Errors.couldNotObtainCredential
|
||||
}
|
||||
let endpoint = try TrashLinkEndpoint(parameters: parameters, service: service, credential: credential, breadcrumbs: breadcrumbs.collect())
|
||||
return try await request(endpoint)
|
||||
}
|
||||
|
||||
public func getFolderChildren(_ shareID: ShareID, folderID: FolderID, parameters: [FolderChildrenEndpointParameters]? = nil) async throws -> [Link] {
|
||||
let endpoint = FolderChildrenEndpoint(shareID: shareID, folderID: folderID, parameters: parameters, service: service, credential: try credential())
|
||||
return try await request(endpoint, completionExecutor: .asyncExecutor(dispatchQueue: backgroundQueue)).links
|
||||
@@ -139,12 +114,6 @@ extension Client {
|
||||
_ = try await request(endpoint)
|
||||
}
|
||||
|
||||
public func moveEntry(shareID: Share.ShareID, nodeID: Link.LinkID, parameters: MoveEntryEndpoint.Parameters) async throws {
|
||||
let credential = try credential()
|
||||
let endpoint = MoveEntryEndpoint(shareID: shareID, nodeID: nodeID, parameters: parameters, service: service, credential: credential)
|
||||
_ = try await request(endpoint)
|
||||
}
|
||||
|
||||
public func createShare(volumeID: Volume.VolumeID, parameters: NewShareParameters) async throws -> NewShareShort {
|
||||
let credential = try credential()
|
||||
let endpoint = NewShareEndpoint(volumeID: volumeID, parameters: parameters, service: self.service, credential: credential)
|
||||
@@ -152,21 +121,55 @@ extension Client {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol MoveNodeClient {
|
||||
func moveEntry(shareID: Share.ShareID, nodeID: Link.LinkID, parameters: MoveEntryEndpoint.Parameters) async throws
|
||||
func moveMultiple(volumeID: Volume.VolumeID, parameters: MoveMultipleEndpoint.Parameters) async throws
|
||||
func transferMultiple(volumeID: Volume.VolumeID, parameters: TransferMultipleEndpoint.Parameters) async throws
|
||||
}
|
||||
|
||||
extension Client: MoveNodeClient {
|
||||
public func moveEntry(shareID: Share.ShareID, nodeID: Link.LinkID, parameters: MoveEntryEndpoint.Parameters) async throws {
|
||||
let credential = try credential()
|
||||
let endpoint = MoveEntryEndpoint(shareID: shareID, nodeID: nodeID, parameters: parameters, service: service, credential: credential)
|
||||
_ = try await request(endpoint)
|
||||
}
|
||||
|
||||
public func moveMultiple(volumeID: VolumeID, parameters: MoveMultipleEndpoint.Parameters) async throws {
|
||||
let credential = try credential()
|
||||
let endpoint = MoveMultipleEndpoint(volumeID: volumeID, parameters: parameters, service: service, credential: credential)
|
||||
_ = try await request(endpoint)
|
||||
}
|
||||
|
||||
public func transferMultiple(volumeID: VolumeID, parameters: TransferMultipleEndpoint.Parameters) async throws {
|
||||
let credential = try credential()
|
||||
let endpoint = TransferMultipleEndpoint(volumeID: volumeID, parameters: parameters, service: service, credential: credential)
|
||||
_ = try await request(endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol SharesListing {
|
||||
func listShares() async throws -> [ListSharesEndpoint.Response.Share]
|
||||
func listShares(parameters: ListSharesEndpoint.Parameters) async throws -> [ListSharesEndpoint.Response.Share]
|
||||
}
|
||||
|
||||
extension Client: SharesListing {
|
||||
public func listShares() async throws -> [ListSharesEndpoint.Response.Share] {
|
||||
let parameters = ListSharesEndpoint.Parameters(shareType: nil, showAll: .default)
|
||||
return try await listShares(parameters: parameters)
|
||||
}
|
||||
|
||||
public func listShares(parameters: ListSharesEndpoint.Parameters) async throws -> [ListSharesEndpoint.Response.Share] {
|
||||
let endpoint = ListSharesEndpoint(parameters: parameters, service: service, credential: try credential())
|
||||
let response = try await request(endpoint)
|
||||
|
||||
return response.shares
|
||||
}
|
||||
}
|
||||
|
||||
extension Client {
|
||||
public protocol BootstrapRootClient {
|
||||
func bootstrapRoot(shareID: String, rootLinkID: String) async throws -> Root
|
||||
}
|
||||
|
||||
extension Client: BootstrapRootClient {
|
||||
public func bootstrapRoot(shareID: String, rootLinkID: String) async throws -> Root {
|
||||
async let share = try bootstrapShare(id: shareID)
|
||||
async let root = try getLinkMetadata(parameters: .init(shareId: shareID, linkIds: [rootLinkID]))
|
||||
@@ -190,6 +193,18 @@ extension Client {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol UserSettingAPIClient {
|
||||
func getDriveEntitlements() async throws -> DriveEntitlementsEndpoint.DriveEntitlements
|
||||
}
|
||||
|
||||
extension Client: UserSettingAPIClient {
|
||||
public func getDriveEntitlements() async throws -> DriveEntitlementsEndpoint.DriveEntitlements {
|
||||
let credential = try credential()
|
||||
let endpoint = DriveEntitlementsEndpoint(service: service, credential: credential)
|
||||
return try await request(endpoint).entitlements
|
||||
}
|
||||
}
|
||||
|
||||
extension Client {
|
||||
public func postVolume(parameters: NewVolumeParameters) async throws -> NewVolume {
|
||||
let credential = try credential()
|
||||
@@ -420,3 +435,137 @@ extension Client: LinksMetadataRepository {
|
||||
return try await request(endpoint, completionExecutor: .asyncExecutor(dispatchQueue: backgroundQueue))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RemoteLinksMetadataByVolumeDataSource
|
||||
public protocol RemoteLinksMetadataByVolumeDataSource {
|
||||
func getMetadata(forLinks links: [String], inVolume volume: String) async throws -> LinksResponseByVolume
|
||||
}
|
||||
|
||||
extension Client: RemoteLinksMetadataByVolumeDataSource {
|
||||
public func getMetadata(forLinks links: [String], inVolume volume: String) async throws -> LinksResponseByVolume {
|
||||
let parameters = LinksMetadataByVolumeParameters(volumeId: volume, linkIds: links)
|
||||
let endpoint = LinksMetadataByVolumeEndpoint(service: service, credential: try credential(), parameters: parameters)
|
||||
return try await request(endpoint, completionExecutor: .asyncExecutor(dispatchQueue: backgroundQueue))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RemoteShareMetadataDataSource
|
||||
public protocol RemoteShareMetadataDataSource {
|
||||
func getMetadata(forShare share: String) async throws -> ShareMetadata
|
||||
}
|
||||
|
||||
extension Client: RemoteShareMetadataDataSource {
|
||||
public func getMetadata(forShare share: String) async throws -> ShareMetadata {
|
||||
let credential = try credential()
|
||||
let endpoint = GetShareBootstrapEndpoint(shareID: share, service: service, credential: credential)
|
||||
return try await request(endpoint, completionExecutor: .asyncExecutor(dispatchQueue: backgroundQueue))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TrashRepository
|
||||
public protocol TrashRepository {
|
||||
func emptyVolumeTrash(volumeId: Volume.VolumeID) async throws
|
||||
func deleteTrashed(volumeId: Volume.VolumeID, linkIds: [Link.LinkID]) async throws -> [PartialFailure]
|
||||
func trashVolumeNodes(parameters: TrashVolumeLinksParameters, breadcrumbs: Breadcrumbs) async throws -> MultipleLinkResponse
|
||||
func restoreVolumeTrashedNodes(volumeID: String, linkIDs: [String]) async throws -> [PartialFailure]
|
||||
|
||||
func retoreTrashNode(shareID: Client.ShareID, linkIDs: [Client.LinkID]) async throws -> [PartialFailure]
|
||||
}
|
||||
|
||||
extension Client: TrashRepository {
|
||||
public func emptyVolumeTrash(volumeId: Volume.VolumeID) async throws {
|
||||
let endpoint = EmptyVolumeTrashEndpoint(volumeId: volumeId, service: service, credential: try credential())
|
||||
do {
|
||||
_ = try await request(endpoint)
|
||||
} catch {
|
||||
if error.httpCode == 202 {
|
||||
// ProtonCore throws when `statusCode != 200`. This is a hack around the limitation since this API
|
||||
// actually returns 202 in success case.
|
||||
return
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteTrashed(volumeId: Volume.VolumeID, linkIds: [Link.LinkID]) async throws -> [PartialFailure] {
|
||||
let parameters = DeleteMultipleParameters(volumeId: volumeId, linkIds: linkIds)
|
||||
let endpoint = DeleteMultipleEndpoint(parameters: parameters, service: service, credential: try credential())
|
||||
let response = try await request(endpoint)
|
||||
return response.responses.compactMap(PartialFailure.init)
|
||||
}
|
||||
|
||||
public func listVolumeTrash(volumeID: VolumeID, page: Int, pageSize: Int) async throws -> ListVolumeTrashEndpoint.Response {
|
||||
let credential = try credential()
|
||||
|
||||
let endpoint = ListVolumeTrashEndpoint(
|
||||
parameters: .init(
|
||||
volumeId: volumeID,
|
||||
page: page,
|
||||
pageSize: pageSize
|
||||
),
|
||||
service: service,
|
||||
credential: credential
|
||||
)
|
||||
|
||||
return try await request(endpoint)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func trashVolumeNodes(parameters: TrashVolumeLinksParameters, breadcrumbs: Breadcrumbs) async throws -> MultipleLinkResponse {
|
||||
guard let credential = self.credentialProvider.clientCredential() else {
|
||||
throw Errors.couldNotObtainCredential
|
||||
}
|
||||
let endpoint = try TrashVolumeLinkEndpoint(parameters: parameters, service: service, credential: credential, breadcrumbs: breadcrumbs)
|
||||
return try await request(endpoint)
|
||||
}
|
||||
|
||||
public func restoreVolumeTrashedNodes(volumeID: String, linkIDs: [String]) async throws -> [PartialFailure] {
|
||||
let parameters: RestoreVolumeLinkEndpoint.Parameters = .init(volumeID: volumeID, linkIDs: linkIDs)
|
||||
let endpoint = RestoreVolumeLinkEndpoint(parameters: parameters, service: service, credential: try credential())
|
||||
let response = try await request(endpoint)
|
||||
return response.responses.compactMap(PartialFailure.init)
|
||||
}
|
||||
|
||||
// Legacy, todo when album / computer is released, this can be removed
|
||||
@discardableResult
|
||||
public func trashNodes(parameters: TrashLinksParameters, breadcrumbs: Breadcrumbs) async throws -> MultipleLinkResponse {
|
||||
guard let credential = self.credentialProvider.clientCredential() else {
|
||||
throw Errors.couldNotObtainCredential
|
||||
}
|
||||
let endpoint = try TrashLinkEndpoint(parameters: parameters, service: service, credential: credential, breadcrumbs: breadcrumbs.collect())
|
||||
return try await request(endpoint)
|
||||
}
|
||||
|
||||
// Legacy, todo when album / computer is released, this can be removed
|
||||
public func retoreTrashNode(shareID: ShareID, linkIDs: [LinkID]) async throws -> [PartialFailure] {
|
||||
let parameters = RestoreLinkEndpoint.Parameters(shareID: shareID, linkIDs: linkIDs)
|
||||
let endpoint = RestoreLinkEndpoint(parameters: parameters, service: service, credential: try credential())
|
||||
let response = try await request(endpoint)
|
||||
return response.responses.compactMap(PartialFailure.init)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol CopyRepository {
|
||||
func copyLinkToVolume(parameters: CopyLinkToVolumeEndpoint.Parameters) async throws -> Link.LinkID
|
||||
}
|
||||
|
||||
extension Client: CopyRepository {
|
||||
public func copyLinkToVolume(parameters: CopyLinkToVolumeEndpoint.Parameters) async throws -> Link.LinkID {
|
||||
let endpoint = CopyLinkToVolumeEndpoint(parameters: parameters, service: service, credential: try credential())
|
||||
let response = try await request(endpoint)
|
||||
return response.linkID
|
||||
}
|
||||
}
|
||||
|
||||
public protocol DriveChecklistDataSource {
|
||||
func getDriveChecklist() async throws -> DriveChecklistStatusResponse
|
||||
}
|
||||
|
||||
extension Client: DriveChecklistDataSource {
|
||||
public func getDriveChecklist() async throws -> DriveChecklistStatusResponse {
|
||||
let endpoint = DriveChecklistEndpoint(service: service, credential: try credential())
|
||||
let response = try await request(endpoint)
|
||||
return DriveChecklistStatusResponse(from: response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
// along with Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
import PDLoadTesting
|
||||
import ProtonCoreUtilities
|
||||
|
||||
extension Client {
|
||||
@@ -85,7 +84,7 @@ extension Client {
|
||||
return completion(.failure(Errors.couldNotObtainCredential))
|
||||
}
|
||||
let endpoint = FolderChildrenEndpoint(shareID: shareID, folderID: folderID, parameters: parameters, service: self.service, credential: credential)
|
||||
request(endpoint) {
|
||||
request(endpoint, completionExecutor: .asyncExecutor(dispatchQueue: backgroundQueue)) {
|
||||
completion( $0.flatMap { .success($0.links) })
|
||||
}
|
||||
}
|
||||
@@ -181,34 +180,11 @@ extension Client {
|
||||
let endpoint = NewBlocksEndpoint(parameters: parameters, service: service, credential: credential)
|
||||
request(endpoint) { result in
|
||||
completion(result.flatMap {
|
||||
if LoadTesting.isEnabled {
|
||||
return .success((
|
||||
blocks: Self.fixUploadLinks(requestURL: endpoint.request.url, links: $0.uploadLinks),
|
||||
thumbnails: Self.fixUploadLinks(requestURL: endpoint.request.url, links: $0.thumbnailLinks)
|
||||
))
|
||||
} else {
|
||||
return .success((blocks: $0.uploadLinks, thumbnails: $0.thumbnailLinks ?? []))
|
||||
}
|
||||
.success((blocks: $0.uploadLinks, thumbnails: $0.thumbnailLinks ?? []))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private static func fixUploadLinks(requestURL: URL?, links: [ContentUploadLink]?) -> [ContentUploadLink] {
|
||||
guard LoadTesting.isEnabled else {
|
||||
assertionFailure("This method should only be called when load testing is enabled")
|
||||
return links ?? []
|
||||
}
|
||||
guard let apiURL = requestURL?.absoluteString, let links else { return links ?? [] }
|
||||
return links.map { link in
|
||||
guard apiURL.hasPrefix("http://") && link.URL.hasPrefix("https://") else {
|
||||
return link
|
||||
}
|
||||
let host = apiURL.replacingOccurrences(of: "drive/blocks", with: "")
|
||||
let fixedURL = host + link.URL.replacingOccurrences(of: "https:\\/\\/\\w+\\/", with: "", options: .regularExpression)
|
||||
return ContentUploadLink(token: link.token, URL: fixedURL)
|
||||
}
|
||||
}
|
||||
|
||||
public func postVolume(parameters: NewVolumeParameters, completion: @escaping (Result<NewVolume, Error>) -> Void) {
|
||||
guard let credential = self.credentialProvider.clientCredential() else {
|
||||
return completion(.failure(Errors.couldNotObtainCredential))
|
||||
@@ -412,6 +388,7 @@ extension Client {
|
||||
}
|
||||
|
||||
extension Client {
|
||||
@discardableResult
|
||||
public func trash(shareID: ShareID, parentID: LinkID, linkIDs: [LinkID]) async throws -> [PartialFailure] {
|
||||
let parameters = TrashLinksParameters(shareId: shareID, parentLinkId: parentID, linkIds: linkIDs)
|
||||
let endpoint = try TrashLinkEndpoint(parameters: parameters, service: service, credential: try credential(), breadcrumbs: .startCollecting())
|
||||
@@ -430,13 +407,6 @@ extension Client {
|
||||
_ = try await request(endpoint)
|
||||
}
|
||||
|
||||
public func retoreTrashNode(shareID: ShareID, linkIDs: [LinkID]) async throws -> [PartialFailure] {
|
||||
let parameters = RestoreLinkEndpoint.Parameters(shareID: shareID, linkIDs: linkIDs)
|
||||
let endpoint = RestoreLinkEndpoint(parameters: parameters, service: service, credential: try credential())
|
||||
let response = try await request(endpoint)
|
||||
return response.responses.compactMap(PartialFailure.init)
|
||||
}
|
||||
|
||||
public func deleteTrashed(shareID: ShareID, linkIDs: [LinkID]) async throws -> [PartialFailure] {
|
||||
let parameters = DeleteLinkEndpoint.Parameters(shareID: shareID, linkIDs: linkIDs)
|
||||
let endpoint = DeleteLinkEndpoint(parameters: parameters, service: service, credential: try credential())
|
||||
@@ -472,10 +442,10 @@ public struct PartialFailure {
|
||||
self.error = NSError(domain: error, code: code, localizedDescription: description)
|
||||
}
|
||||
|
||||
init?(_ linkReponse: MultipleLinkResponse.LinkResponse) {
|
||||
guard let error = linkReponse.response.error else { return nil }
|
||||
self.id = linkReponse.linkID
|
||||
self.error = NSError(domain: error, code: linkReponse.response.code)
|
||||
public init?(_ linkResponse: MultipleLinkResponse.LinkResponse) {
|
||||
guard let error = linkResponse.response.error else { return nil }
|
||||
self.id = linkResponse.linkID
|
||||
self.error = NSError(domain: error, code: linkResponse.response.code)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import Foundation
|
||||
import ProtonCoreUtilities
|
||||
import ProtonCoreServices
|
||||
|
||||
public protocol CredentialProvider: AnyObject {
|
||||
/// Obtaining credential optionally
|
||||
@@ -43,7 +44,15 @@ public class Client {
|
||||
}
|
||||
|
||||
public func credential() throws -> ClientCredential {
|
||||
return try credentialProvider.getCredential()
|
||||
do {
|
||||
let credential = try credentialProvider.getCredential()
|
||||
return credential
|
||||
} catch {
|
||||
#if os(iOS)
|
||||
networking.authDelegate?.onAuthenticatedSessionInvalidated(sessionUID: "")
|
||||
#endif
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func request<E: Endpoint, Response>(_ endpoint: E, completionExecutor: CompletionBlockExecutor = .asyncMainExecutor, completion: @escaping (Result<Response, Error>) -> Void) where Response == E.Response {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct CodeResponse: Codable {
|
||||
public let code: Int
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol LinksMetadataDataSource {
|
||||
func getLinksMetadata(with parameters: LinksMetadataParameters) async throws -> LinksResponse
|
||||
}
|
||||
|
||||
public protocol PhotosListingDataSource {
|
||||
func getPhotosList(with parameters: PhotosListRequestParameters) async throws -> PhotosListResponse
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
public enum ShareTargetType: Int, CaseIterable {
|
||||
case root = 0
|
||||
case folder = 1
|
||||
case file = 2
|
||||
case album = 3
|
||||
case photo = 4
|
||||
case document = 5
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol SharedByMeLinkIdsDataSource {
|
||||
func getSharedByMeLinks(parameters: SharedByMeListParameters) async throws -> SharedByMeListResponse
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct AcceptInvitationParameters {
|
||||
let invitationID: String
|
||||
let bodyParameters: BodyParameters
|
||||
|
||||
public init(invitationID: String, sessionKeySignature: String) {
|
||||
self.invitationID = invitationID
|
||||
self.bodyParameters = BodyParameters(sessionKeySignature: sessionKeySignature)
|
||||
}
|
||||
|
||||
struct BodyParameters: Encodable {
|
||||
let sessionKeySignature: String
|
||||
}
|
||||
}
|
||||
|
||||
public struct AcceptInvitationResponse: Codable {
|
||||
public let code: Int
|
||||
}
|
||||
|
||||
/// Accept invitation
|
||||
/// - POST: /drive/v2/shares/invitations/{invitationID}/accept
|
||||
public struct AcceptInvitationEndpoint: Endpoint {
|
||||
public typealias Response = AcceptInvitationResponse
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
public init(parameters: AcceptInvitationParameters, service: APIService, credential: ClientCredential) throws {
|
||||
// url
|
||||
|
||||
let url = service.url(of: "/v2/shares/invitations/\(parameters.invitationID)/accept")
|
||||
|
||||
// request
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
// headers
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
request.httpBody = try JSONEncoder(strategy: .capitalizeFirstLetter).encode(parameters.bodyParameters)
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Client: SharedByMeLinkIdsDataSource, LinksMetadataDataSource {
|
||||
public func getSharedByMeLinks(parameters: SharedByMeListParameters) async throws -> SharedByMeListResponse {
|
||||
let endpoint = SharedByMeEndpoint(service: service, credential: try credential(), parameters: parameters)
|
||||
let response = try await request(endpoint, completionExecutor: .immediateExecutor)
|
||||
return response
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ListUserInvitationsParameters {
|
||||
let anchorID: String?
|
||||
let pageSize: Int?
|
||||
let shareTypes: Set<ShareTargetType>
|
||||
|
||||
public init(anchorID: String?, pageSize: Int? = nil, shareTypes: Set<ShareTargetType>) {
|
||||
self.anchorID = anchorID
|
||||
self.pageSize = pageSize
|
||||
self.shareTypes = shareTypes
|
||||
}
|
||||
}
|
||||
|
||||
public struct ListUserInvitationsResponse: Codable {
|
||||
public let invitations: [InvitationResponse]
|
||||
public let anchorID: String?
|
||||
public let more: Bool
|
||||
public let code: Int
|
||||
|
||||
public struct InvitationResponse: Codable {
|
||||
public let volumeID: String
|
||||
public let shareID: String
|
||||
public let invitationID: String
|
||||
}
|
||||
}
|
||||
|
||||
/// Get List invitations of a user
|
||||
/// - GET: /drive/v2/shares/invitations
|
||||
public struct ListUserInvitationsEndpoint: Endpoint {
|
||||
public typealias Response = ListUserInvitationsResponse
|
||||
|
||||
public let request: URLRequest
|
||||
|
||||
public init(service: APIService, credential: ClientCredential, parameters: ListUserInvitationsParameters) {
|
||||
var queries: [URLQueryItem] = []
|
||||
if let anchorID = parameters.anchorID {
|
||||
queries.append(URLQueryItem(name: "AnchorID", value: anchorID))
|
||||
}
|
||||
if let pageSize = parameters.pageSize {
|
||||
queries.append(URLQueryItem(name: "PageSize", value: String(pageSize)))
|
||||
}
|
||||
let shareTypes = parameters.shareTypes.map(\.rawValue).map(String.init)
|
||||
shareTypes.forEach { shareType in
|
||||
// Query key contains `[]` to convey array type
|
||||
queries.append(URLQueryItem(name: "ShareTargetTypes[]", value: shareType))
|
||||
}
|
||||
|
||||
let url = service.url(of: "/v2/shares/invitations", queries: queries)
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
self.request = request
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct RejectInvitationParameters {
|
||||
let invitationID: String
|
||||
|
||||
public init(invitationID: String) {
|
||||
self.invitationID = invitationID
|
||||
}
|
||||
}
|
||||
|
||||
public struct RejectInvitationResponse: Codable {
|
||||
public let code: Int
|
||||
}
|
||||
|
||||
/// Reject invitation
|
||||
/// - POST: /drive/v2/shares/invitations/{invitationID}/reject
|
||||
public struct RejectInvitationEndpoint: Endpoint {
|
||||
public typealias Response = RejectInvitationResponse
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
public init(parameters: RejectInvitationParameters, service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
|
||||
let url = service.url(of: "/v2/shares/invitations/\(parameters.invitationID)/reject")
|
||||
|
||||
// request
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
// headers
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ReturnInvitationInformationParameters {
|
||||
let invitationID: String
|
||||
|
||||
public init(invitationID: String) {
|
||||
self.invitationID = invitationID
|
||||
}
|
||||
}
|
||||
|
||||
public struct ReturnInvitationInformationResponse: Codable {
|
||||
public let invitation: InvitationResponse
|
||||
public let share: ShareResponse
|
||||
public let link: LinkResponse
|
||||
public let code: Int
|
||||
|
||||
public struct InvitationResponse: Codable {
|
||||
public let invitationID: String
|
||||
public let inviterEmail: String
|
||||
public let inviteeEmail: String
|
||||
public let permissions: Int
|
||||
public let keyPacket: String
|
||||
public let keyPacketSignature: String
|
||||
public let createTime: Int
|
||||
|
||||
init(invitationID: String, inviterEmail: String, inviteeEmail: String, permissions: Int, keyPacket: String, keyPacketSignature: String, createTime: Int) {
|
||||
self.invitationID = invitationID
|
||||
self.inviterEmail = inviterEmail
|
||||
self.inviteeEmail = inviteeEmail
|
||||
self.permissions = permissions
|
||||
self.keyPacket = keyPacket
|
||||
self.keyPacketSignature = keyPacketSignature
|
||||
self.createTime = createTime
|
||||
}
|
||||
}
|
||||
|
||||
public struct ShareResponse: Codable {
|
||||
public let shareID: String
|
||||
public let volumeID: String
|
||||
public let passphrase: String
|
||||
public let shareKey: String
|
||||
public let creatorEmail: String
|
||||
|
||||
init(shareID: String, volumeID: String, passphrase: String, shareKey: String, creatorEmail: String) {
|
||||
self.shareID = shareID
|
||||
self.volumeID = volumeID
|
||||
self.passphrase = passphrase
|
||||
self.shareKey = shareKey
|
||||
self.creatorEmail = creatorEmail
|
||||
}
|
||||
}
|
||||
|
||||
public struct LinkResponse: Codable {
|
||||
public let type: Int
|
||||
public let linkID: String
|
||||
public let name: String
|
||||
public let MIMEType: String?
|
||||
|
||||
init(type: Int, linkID: String, name: String, MIMEType: String?) {
|
||||
self.type = type
|
||||
self.linkID = linkID
|
||||
self.name = name
|
||||
self.MIMEType = MIMEType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return Invitation Information
|
||||
/// - GET: /drive/v2/shares/invitations/{InvitationID}
|
||||
public struct ReturnInvitationInformationEndpoint: Endpoint {
|
||||
public typealias Response = ReturnInvitationInformationResponse
|
||||
|
||||
public let request: URLRequest
|
||||
|
||||
public init(service: APIService, credential: ClientCredential, parameters: ReturnInvitationInformationParameters) {
|
||||
let url = service.url(of: "/v2/shares/invitations/\(parameters.invitationID)")
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct SharedByMeListParameters {
|
||||
let volumeId: String
|
||||
let anchorID: String?
|
||||
|
||||
public init(volumeId: String, anchorID: String?) {
|
||||
self.volumeId = volumeId
|
||||
self.anchorID = anchorID
|
||||
}
|
||||
}
|
||||
|
||||
public struct SharedByMeListResponse: Codable {
|
||||
public let links: [Link]
|
||||
public let anchorID: String?
|
||||
public let more: Bool
|
||||
public let code: Int
|
||||
|
||||
public struct Link: Codable {
|
||||
public let contextShareID: String
|
||||
public let shareID: String
|
||||
public let linkID: String
|
||||
}
|
||||
}
|
||||
|
||||
/// Get Revision
|
||||
/// - GET: /drive/v2/volumes/{volumeID}/shares
|
||||
public struct SharedByMeEndpoint: Endpoint {
|
||||
public typealias Response = SharedByMeListResponse
|
||||
|
||||
public let request: URLRequest
|
||||
|
||||
public init(service: APIService, credential: ClientCredential, parameters: SharedByMeListParameters) {
|
||||
var queries: [URLQueryItem] = []
|
||||
if let anchorID = parameters.anchorID {
|
||||
queries.append(URLQueryItem(name: "AnchorID", value: anchorID))
|
||||
}
|
||||
let url = service.url(of: "/v2/volumes/\(parameters.volumeId)/shares", queries: queries)
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
self.request = request
|
||||
}
|
||||
|
||||
}
|
||||
+5
-1
@@ -39,7 +39,11 @@ public struct SharedWithMeEndpoint: Endpoint {
|
||||
public let request: URLRequest
|
||||
|
||||
public init(service: APIService, credential: ClientCredential, parameters: Parameters) {
|
||||
let url = service.url(of: "/v2/sharedwithme")
|
||||
var queries: [URLQueryItem] = []
|
||||
if let anchorID = parameters.anchorID {
|
||||
queries.append(URLQueryItem(name: "AnchorID", value: anchorID))
|
||||
}
|
||||
let url = service.url(of: "/v2/sharedwithme", queries: queries)
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
|
||||
import Foundation
|
||||
import ProtonCoreNetworking
|
||||
import ProtonCoreUtilities
|
||||
|
||||
public protocol CoreAPIService {
|
||||
func perform(request route: Request, completion: @escaping (_ task: URLSessionDataTask?, _ result: Result<JSONDictionary, ResponseError>) -> Void)
|
||||
func perform(request route: Request, callCompletionBlockUsing executor: CompletionBlockExecutor, completion: @escaping (_ task: URLSessionDataTask?, _ result: Result<JSONDictionary, ResponseError>) -> Void)
|
||||
func perform(request route: Request, dataTaskBlock: @escaping (URLSessionDataTask) -> Void, completion: @escaping (_ task: URLSessionDataTask?, _ result: Result<JSONDictionary, ResponseError>) -> Void)
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct DeleteDeviceEndpoint: Endpoint {
|
||||
public struct DeleteDeviceEndpoint: Endpoint {
|
||||
public struct Response: Codable {
|
||||
var code: Int
|
||||
}
|
||||
|
||||
var request: URLRequest
|
||||
public var request: URLRequest
|
||||
|
||||
init(deviceID: String, service: APIService, credential: ClientCredential) {
|
||||
public init(deviceID: String, service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
var url = service.url(of: "/devices")
|
||||
url.appendPathComponent(deviceID)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
// Empty volume trash
|
||||
// POST: /volumes/{volumeID}/trash
|
||||
struct EmptyVolumeTrashEndpoint: Endpoint {
|
||||
typealias Response = CodeResponse
|
||||
|
||||
var request: URLRequest
|
||||
|
||||
init(volumeId: Volume.VolumeID, service: APIService, credential: ClientCredential) {
|
||||
var url = service.url(of: "/volumes")
|
||||
url.appendPathComponent(volumeId)
|
||||
url.appendPathComponent("/trash")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "DELETE"
|
||||
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ struct RemoveShareMemberEndpoint: Endpoint {
|
||||
var request: URLRequest
|
||||
|
||||
init(shareID: String, memberID: String, service: APIService, credential: ClientCredential) {
|
||||
var url = service.url(of: "/v2/shares/\(shareID)/members/\(memberID)")
|
||||
let url = service.url(of: "/v2/shares/\(shareID)/members/\(memberID)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "DELETE"
|
||||
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
// along with Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import ProtonCoreUtilities
|
||||
import ProtonCoreServices
|
||||
|
||||
/// DriveAPIService:
|
||||
/// Protocol that will allow us to decouple from Core's statically defined APIService and implement polymorphisim in a clean way
|
||||
public protocol DriveAPIService {
|
||||
var authDelegate: AuthDelegate? { get }
|
||||
|
||||
func request<E: Endpoint, R>(from endpoint: E, completionExecutor: CompletionBlockExecutor, completion: @escaping (Result<R, Error>) -> Void) where R == E.Response
|
||||
}
|
||||
|
||||
@@ -24,21 +24,52 @@ public enum ExternalFeatureFlag: CaseIterable {
|
||||
case pushNotificationIsEnabled
|
||||
case logCollectionEnabled
|
||||
case logCollectionDisabled
|
||||
case driveiOSDebugMode
|
||||
case oneDollarPlanUpsellEnabled
|
||||
case driveDisablePhotosForB2B
|
||||
case parallelEncryptionAndVerification
|
||||
case driveDDKEnabled
|
||||
case driveMacSyncRecoveryDisabled
|
||||
case driveMacKeepDownloaded
|
||||
|
||||
// MARK: - Sharing
|
||||
// Sharing
|
||||
case driveSharingMigration
|
||||
case driveiOSSharing
|
||||
case driveSharingDevelopment
|
||||
case driveSharingInvitations
|
||||
case driveSharingExternalInvitations
|
||||
case driveSharingDisabled
|
||||
case driveSharingExternalInvitationsDisabled
|
||||
case driveSharingEditingDisabled
|
||||
case drivePublicShareEditMode
|
||||
case drivePublicShareEditModeDisabled
|
||||
case acceptRejectInvitation
|
||||
case driveShareURLBookmarking
|
||||
case driveShareURLBookmarksDisabled
|
||||
|
||||
// ProtonDoc
|
||||
case driveDocsWebView
|
||||
case driveDocsDisabled
|
||||
|
||||
// Rating booster
|
||||
// Legacy feature flags we used before migrating to Unleash
|
||||
case ratingIOSDrive
|
||||
case driveRatingBooster
|
||||
|
||||
// Entitlement
|
||||
case driveDynamicEntitlementConfiguration
|
||||
|
||||
// Refactor
|
||||
case driveiOSRefreshableBlockDownloadLink
|
||||
|
||||
// Computers
|
||||
case driveiOSComputers
|
||||
case driveiOSComputersDisabled
|
||||
|
||||
// Albums
|
||||
case driveAlbumsDisabled
|
||||
case driveCopyDisabled
|
||||
case drivePhotosTagsMigration
|
||||
case drivePhotosTagsMigrationDisabled
|
||||
|
||||
// Sheets
|
||||
case docsSheetsEnabled
|
||||
case docsSheetsDisabled
|
||||
case docsCreateNewSheetOnMobileEnabled
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ import Foundation
|
||||
import UnleashProxyClientSwift
|
||||
|
||||
public final class UnleashFeatureFlagsResource: ExternalFeatureFlagsResource {
|
||||
public typealias LogErrorHandler = (Error) -> Void
|
||||
public typealias LogMessageHandler = (String) -> Void
|
||||
|
||||
private let session: UnleashPollerSession
|
||||
private let configurationResolver: ExternalFeatureFlagConfigurationResolver
|
||||
@@ -29,8 +27,6 @@ public final class UnleashFeatureFlagsResource: ExternalFeatureFlagsResource {
|
||||
private var client: UnleashClient?
|
||||
private let updateSubject = PassthroughSubject<Void, Never>()
|
||||
private let refreshInterval: Int
|
||||
private let logMessageHandler: LogMessageHandler
|
||||
private let logErrorHandler: LogErrorHandler
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
public var updatePublisher: AnyPublisher<Void, Never> {
|
||||
@@ -42,15 +38,11 @@ public final class UnleashFeatureFlagsResource: ExternalFeatureFlagsResource {
|
||||
public init(
|
||||
refreshInterval: Int,
|
||||
session: UnleashPollerSession,
|
||||
configurationResolver: ExternalFeatureFlagConfigurationResolver,
|
||||
logMessageHandler: @escaping LogMessageHandler,
|
||||
logErrorHandler: @escaping LogErrorHandler
|
||||
configurationResolver: ExternalFeatureFlagConfigurationResolver
|
||||
) {
|
||||
self.refreshInterval = refreshInterval
|
||||
self.session = session
|
||||
self.configurationResolver = configurationResolver
|
||||
self.logMessageHandler = logMessageHandler
|
||||
self.logErrorHandler = logErrorHandler
|
||||
}
|
||||
|
||||
public func start(completionHandler: @escaping (Error?) -> Void) {
|
||||
@@ -60,12 +52,12 @@ public final class UnleashFeatureFlagsResource: ExternalFeatureFlagsResource {
|
||||
}
|
||||
|
||||
guard let configuration = try? configurationResolver.makeConfiguration(refreshInterval: refreshInterval) else {
|
||||
logErrorHandler(Errors.unableToFetchConfiguration)
|
||||
logError?(Errors.unableToFetchConfiguration.localizedDescription)
|
||||
completionHandler(Errors.unableToFetchConfiguration)
|
||||
return
|
||||
}
|
||||
|
||||
poller = Poller(refreshInterval: configuration.refreshInterval, unleashUrl: configuration.url, apiKey: configuration.apiKey, session: session)
|
||||
poller = Poller(refreshInterval: configuration.refreshInterval, unleashUrl: configuration.url, apiKey: configuration.apiKey, session: session, appName: "Proton Drive", connectionId: UUID())
|
||||
client = UnleashClient(unleashUrl: configuration.url.absoluteString, clientKey: configuration.apiKey, refreshInterval: configuration.refreshInterval, disableMetrics: true, environment: configuration.environment, poller: poller)
|
||||
|
||||
startClient(completionHandler: completionHandler)
|
||||
@@ -87,9 +79,9 @@ public final class UnleashFeatureFlagsResource: ExternalFeatureFlagsResource {
|
||||
self?.client?.start { error in
|
||||
completionHandler(error)
|
||||
if let error {
|
||||
self?.logErrorHandler(error)
|
||||
logError?(error.localizedDescription)
|
||||
} else {
|
||||
self?.logMessageHandler("Unleash started")
|
||||
logInfo?("Unleash started")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,11 +89,11 @@ public final class UnleashFeatureFlagsResource: ExternalFeatureFlagsResource {
|
||||
|
||||
private func subscribeToUpdates() {
|
||||
client?.subscribe(name: "update") { [weak self] in
|
||||
self?.logMessageHandler("Unleash feature flags: updated")
|
||||
logInfo?("Unleash feature flags: updated")
|
||||
self?.updateSubject.send()
|
||||
}
|
||||
client?.subscribe(name: "ready") { [weak self] in
|
||||
self?.logMessageHandler("Unleash feature flags: ready")
|
||||
logInfo?("Unleash feature flags: ready")
|
||||
self?.updateSubject.send()
|
||||
}
|
||||
}
|
||||
@@ -125,6 +117,7 @@ public final class UnleashFeatureFlagsResource: ExternalFeatureFlagsResource {
|
||||
return client?.isEnabled(name: name) ?? false
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private func makeName(from flag: ExternalFeatureFlag) -> String {
|
||||
switch flag {
|
||||
case .photosUploadDisabled:
|
||||
@@ -145,12 +138,17 @@ public final class UnleashFeatureFlagsResource: ExternalFeatureFlagsResource {
|
||||
return "DriveiOSLogCollectionDisabled"
|
||||
case .oneDollarPlanUpsellEnabled:
|
||||
return "DriveOneDollarPlanUpsell"
|
||||
case .driveDisablePhotosForB2B:
|
||||
return "DriveDisablePhotosForB2B"
|
||||
case .driveDDKEnabled:
|
||||
return "DriveDDKEnabled"
|
||||
case .driveMacSyncRecoveryDisabled:
|
||||
return "DriveMacSyncRecoveryDisabled"
|
||||
case .driveMacKeepDownloaded:
|
||||
return "DriveMacKeepDownloaded"
|
||||
// Sharing
|
||||
case .driveSharingMigration:
|
||||
return "DriveSharingMigration"
|
||||
case .driveiOSSharing:
|
||||
return "DriveiOSSharing"
|
||||
case .driveSharingDevelopment:
|
||||
return "DriveSharingDevelopment"
|
||||
case .driveSharingInvitations:
|
||||
return "DriveSharingInvitations"
|
||||
case .driveSharingExternalInvitations:
|
||||
@@ -161,14 +159,51 @@ public final class UnleashFeatureFlagsResource: ExternalFeatureFlagsResource {
|
||||
return "DriveSharingExternalInvitationsDisabled"
|
||||
case .driveSharingEditingDisabled:
|
||||
return "DriveSharingEditingDisabled"
|
||||
case .driveDisablePhotosForB2B:
|
||||
return "DriveDisablePhotosForB2B"
|
||||
case .driveDocsWebView:
|
||||
return "DriveDocsWebView"
|
||||
case .parallelEncryptionAndVerification:
|
||||
return "DriveMacParallelEncryptionAndVerification"
|
||||
case .drivePublicShareEditMode:
|
||||
return "DrivePublicShareEditMode"
|
||||
case .drivePublicShareEditModeDisabled:
|
||||
return "DrivePublicShareEditModeDisabled"
|
||||
case .acceptRejectInvitation:
|
||||
return "DriveMobileSharingInvitationsAcceptReject"
|
||||
case .driveDynamicEntitlementConfiguration:
|
||||
return "DriveDynamicEntitlementConfiguration"
|
||||
// ProtonDoc
|
||||
case .driveDocsDisabled:
|
||||
return "DriveDocsDisabled"
|
||||
// Rating booster
|
||||
// Legacy feature flags we used before migrating to Unleash
|
||||
case .ratingIOSDrive:
|
||||
return "RatingIOSDrive"
|
||||
case .driveRatingBooster:
|
||||
return "DriveRatingBooster"
|
||||
case .driveShareURLBookmarking:
|
||||
return "DriveShareURLBookmarking"
|
||||
case .driveShareURLBookmarksDisabled:
|
||||
return "DriveShareURLBookmarksDisabled"
|
||||
// Refactor
|
||||
case .driveiOSRefreshableBlockDownloadLink:
|
||||
return "DriveiOSRefreshableBlockDownloadLink"
|
||||
case .driveiOSComputers:
|
||||
return "DriveiOSComputers"
|
||||
case .driveiOSComputersDisabled:
|
||||
return "DriveiOSComputersDisabled"
|
||||
// Album
|
||||
case .driveAlbumsDisabled:
|
||||
return "DriveAlbumsDisabled"
|
||||
case .driveCopyDisabled:
|
||||
return "DriveCopyDisabled"
|
||||
case .drivePhotosTagsMigration:
|
||||
return "DrivePhotosTagsMigration"
|
||||
case .drivePhotosTagsMigrationDisabled:
|
||||
return "DrivePhotosTagsMigrationDisabled"
|
||||
case .docsSheetsEnabled:
|
||||
return "DocsSheetsEnabled"
|
||||
case .docsSheetsDisabled:
|
||||
return "DocsSheetsDisabled"
|
||||
case .docsCreateNewSheetOnMobileEnabled:
|
||||
return "DocsCreateNewSheetOnMobileEnabled"
|
||||
case .driveiOSDebugMode:
|
||||
return "DriveiOSDebugMode"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,27 +26,23 @@ public final class UnleashPollerSession: PollerSession {
|
||||
}
|
||||
|
||||
public func perform(_ request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
|
||||
do {
|
||||
let endpoint = UnleashEndpoint(request: request)
|
||||
var dataTask: URLSessionDataTask?
|
||||
networking.perform(request: endpoint, dataTaskBlock: {
|
||||
dataTask = $0
|
||||
}, completion: { task, result in
|
||||
let response = dataTask?.response ?? task?.response
|
||||
switch result {
|
||||
case let .success(responseDictionary):
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: responseDictionary, options: .prettyPrinted)
|
||||
completionHandler(data, response, nil)
|
||||
} catch {
|
||||
completionHandler(nil, response, error)
|
||||
}
|
||||
case let .failure(error):
|
||||
let endpoint = UnleashEndpoint(request: request)
|
||||
var dataTask: URLSessionDataTask?
|
||||
networking.perform(request: endpoint, dataTaskBlock: {
|
||||
dataTask = $0
|
||||
}, completion: { task, result in
|
||||
let response = dataTask?.response ?? task?.response
|
||||
switch result {
|
||||
case let .success(responseDictionary):
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: responseDictionary, options: .prettyPrinted)
|
||||
completionHandler(data, response, nil)
|
||||
} catch {
|
||||
completionHandler(nil, response, error)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
completionHandler(nil, nil, error)
|
||||
}
|
||||
case let .failure(error):
|
||||
completionHandler(nil, response, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// /checklist/get-started
|
||||
/// Fetch the current storage bonus status
|
||||
public struct DriveChecklistEndpoint: Endpoint {
|
||||
public struct Response: Codable {
|
||||
public let items: [String]
|
||||
public let createdAt: TimeInterval?
|
||||
public let expiresAt: TimeInterval?
|
||||
public let userWasRewarded: Bool
|
||||
public let seen: Bool
|
||||
public let completed: Bool
|
||||
public let rewardInGB: Int
|
||||
public let visible: Bool
|
||||
public let code: Int
|
||||
}
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
public init(service: APIService, credential: ClientCredential) {
|
||||
let url = service.url(of: "/v2/checklist/get-started")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
|
||||
public struct DriveChecklistStatusResponse: Codable, Equatable {
|
||||
public let items: [String]
|
||||
public let createdAt: Date
|
||||
public let expiresAt: Date
|
||||
public let userWasRewarded: Bool
|
||||
public let seen: Bool
|
||||
public let completed: Bool
|
||||
public let rewardInGB: Int
|
||||
public let visible: Bool
|
||||
|
||||
public init(
|
||||
items: [String],
|
||||
createdAt: Date,
|
||||
expiresAt: Date,
|
||||
userWasRewarded: Bool,
|
||||
seen: Bool,
|
||||
completed: Bool,
|
||||
rewardInGB: Int,
|
||||
visible: Bool
|
||||
) {
|
||||
self.items = items
|
||||
self.createdAt = createdAt
|
||||
self.expiresAt = expiresAt
|
||||
self.userWasRewarded = userWasRewarded
|
||||
self.seen = seen
|
||||
self.completed = completed
|
||||
self.rewardInGB = rewardInGB
|
||||
self.visible = visible
|
||||
}
|
||||
|
||||
public init(from response: DriveChecklistEndpoint.Response) {
|
||||
self.items = response.items
|
||||
self.createdAt = Date(timeIntervalSince1970: response.createdAt ?? .zero)
|
||||
self.expiresAt = Date(timeIntervalSince1970: response.expiresAt ?? .zero)
|
||||
self.userWasRewarded = response.userWasRewarded
|
||||
self.seen = response.seen
|
||||
self.completed = response.completed
|
||||
self.rewardInGB = response.rewardInGB
|
||||
self.visible = response.visible
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Expose Drive specific features available to the user, along with their specific limits.
|
||||
/// - GET: /drive/entitlements
|
||||
public struct DriveEntitlementsEndpoint: Endpoint {
|
||||
public let request: URLRequest
|
||||
|
||||
public init(service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
let url = service.url(of: "/entitlements")
|
||||
|
||||
// request
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
|
||||
// headers
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
|
||||
extension DriveEntitlementsEndpoint {
|
||||
public struct Response: Codable {
|
||||
public let code: Int
|
||||
public let entitlements: DriveEntitlements
|
||||
}
|
||||
|
||||
public struct DriveEntitlements: Codable {
|
||||
public let publicCollaboration: Bool
|
||||
|
||||
public init(publicCollaboration: Bool) {
|
||||
self.publicCollaboration = publicCollaboration
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,20 @@ public struct Event: Encodable {
|
||||
private struct MinimalLink: Codable {
|
||||
public var linkID: String
|
||||
}
|
||||
|
||||
public init(
|
||||
contextShareID: Share.ShareID,
|
||||
eventID: EventID,
|
||||
eventType: EventType,
|
||||
createTime: TimeInterval,
|
||||
link: Link
|
||||
) {
|
||||
self.contextShareID = contextShareID
|
||||
self.eventID = eventID
|
||||
self.eventType = eventType
|
||||
self.createTime = createTime
|
||||
self.link = link
|
||||
}
|
||||
}
|
||||
|
||||
extension Event: Decodable {
|
||||
|
||||
@@ -59,6 +59,27 @@ public struct GetShareBootstrapEndpoint: Endpoint {
|
||||
public let memberships: [Membership]
|
||||
public let rootLinkRecoveryPassphrase: String?
|
||||
|
||||
public init(code: Int, shareID: String, volumeID: String, type: Int, state: Int, creator: String, locked: Bool?, createTime: Int?, modifyTime: Int?, linkID: String, linkType: LinkType, key: String, passphrase: String, passphraseSignature: String, addressID: String, addressKeyID: String, memberships: [Membership], rootLinkRecoveryPassphrase: String?) {
|
||||
self.code = code
|
||||
self.shareID = shareID
|
||||
self.volumeID = volumeID
|
||||
self.type = type
|
||||
self.state = state
|
||||
self.creator = creator
|
||||
self.locked = locked
|
||||
self.createTime = createTime
|
||||
self.modifyTime = modifyTime
|
||||
self.linkID = linkID
|
||||
self.linkType = linkType
|
||||
self.key = key
|
||||
self.passphrase = passphrase
|
||||
self.passphraseSignature = passphraseSignature
|
||||
self.addressID = addressID
|
||||
self.addressKeyID = addressKeyID
|
||||
self.memberships = memberships
|
||||
self.rootLinkRecoveryPassphrase = rootLinkRecoveryPassphrase
|
||||
}
|
||||
|
||||
public struct Membership: Codable {
|
||||
public let memberID: String
|
||||
public let shareID: String
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct GetTagsMigrationStateEndpoint: Endpoint {
|
||||
public typealias Response = TagsMigrationStateResponse
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
public init(volumeID: String, service: APIService, credential: ClientCredential) {
|
||||
var url = service.url(of: "/photos/volumes")
|
||||
url.appendPathComponent(volumeID)
|
||||
url.appendPathComponent("tags-migration")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
self.request = request
|
||||
}
|
||||
|
||||
public struct TagsMigrationStateResponse: Codable {
|
||||
public let code: Int
|
||||
public let finished: Bool
|
||||
public let anchor: TagsMigrationAnchorResponse?
|
||||
|
||||
public struct TagsMigrationAnchorResponse: Codable {
|
||||
public let lastProcessedLinkID: String
|
||||
public let lastProcessedCaptureTime: Int
|
||||
public let lastMigrationTimestamp: Int
|
||||
public let lastClientUID: String?
|
||||
|
||||
public init(lastProcessedLinkID: String, lastProcessedCaptureTime: Int, lastMigrationTimestamp: Int, lastClientUID: String?) {
|
||||
self.lastProcessedLinkID = lastProcessedLinkID
|
||||
self.lastProcessedCaptureTime = lastProcessedCaptureTime
|
||||
self.lastMigrationTimestamp = lastMigrationTimestamp
|
||||
self.lastClientUID = lastClientUID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,9 +50,9 @@ extension ListDevicesEndpoint {
|
||||
public struct Device: Codable {
|
||||
public let deviceID: String
|
||||
public let volumeID: String
|
||||
public let creationTime: TimeInterval
|
||||
public let modifyTime: TimeInterval?
|
||||
public let lastSyncTime: TimeInterval?
|
||||
public let creationTime: Date
|
||||
public let modifyTime: Date?
|
||||
public let lastSyncTime: Date?
|
||||
public let type: Int
|
||||
public let syncState: Int
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public struct ListSharesEndpoint: Endpoint {
|
||||
let shareType: ShareType?
|
||||
let showAll: ShowAll?
|
||||
|
||||
init(addressId: String? = nil, shareType: ShareType? = nil, showAll: Parameters.ShowAll? = nil) {
|
||||
public init(addressId: String? = nil, shareType: ShareType? = nil, showAll: Parameters.ShowAll? = nil) {
|
||||
self.addressId = addressId
|
||||
self.shareType = shareType
|
||||
self.showAll = showAll
|
||||
@@ -74,33 +74,36 @@ public struct ListSharesEndpoint: Endpoint {
|
||||
|
||||
extension ListSharesEndpoint {
|
||||
public struct Response: Codable {
|
||||
public typealias Share = ShareListing
|
||||
|
||||
public let shares: [Share]
|
||||
public let code: Int
|
||||
|
||||
// MARK: - Share
|
||||
public struct Share: Codable {
|
||||
public let shareID: String
|
||||
public let volumeID: String
|
||||
public let type: ´Type´
|
||||
public let state: State
|
||||
public let creator: String
|
||||
public let locked: Bool?
|
||||
public let createTime: Int?
|
||||
public let modifyTime: Int?
|
||||
public let linkID: String
|
||||
|
||||
public enum ´Type´: Int, Codable {
|
||||
case main = 1
|
||||
case standard = 2
|
||||
case device = 3
|
||||
case photos = 4
|
||||
}
|
||||
|
||||
public enum State: Int, Codable {
|
||||
case active = 1
|
||||
case deleted = 2
|
||||
case restored = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Share Listing
|
||||
|
||||
public struct ShareListing: Codable {
|
||||
public let shareID: String
|
||||
public let volumeID: String
|
||||
public let type: ´Type´
|
||||
public let state: State
|
||||
public let creator: String
|
||||
public let locked: Bool?
|
||||
public let createTime: Int?
|
||||
public let modifyTime: Int?
|
||||
public let linkID: String
|
||||
|
||||
public enum ´Type´: Int, Codable {
|
||||
case main = 1
|
||||
case standard = 2
|
||||
case device = 3
|
||||
case photos = 4
|
||||
}
|
||||
|
||||
public enum State: Int, Codable {
|
||||
case active = 1
|
||||
case deleted = 2
|
||||
case restored = 3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ public struct PhotosListEndpoint: Endpoint {
|
||||
let item = URLQueryItem(name: "PreviousPageLastLinkID", value: lastId)
|
||||
items.append(item)
|
||||
}
|
||||
if let tag = parameters.tag {
|
||||
let item = URLQueryItem(name: "Tag", value: "\(tag)")
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
let url = service.url(of: "/volumes/\(parameters.volumeId)/photos", parameters: items)
|
||||
var headers = service.baseHeaders
|
||||
|
||||
@@ -33,6 +33,10 @@ public struct RevisionEndpoint: Endpoint {
|
||||
url.appendPathComponent(fileID)
|
||||
url.appendPathComponent("/revisions")
|
||||
url.appendPathComponent(revisionID)
|
||||
// A random query parameter to call request rather than get cache
|
||||
url.append(queryItems: [
|
||||
.init(name: "random", value: "\(Int.random(in: 0...100))")
|
||||
])
|
||||
|
||||
// request
|
||||
var request = URLRequest(url: url)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// along with Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
import ProtonCoreNetworking
|
||||
|
||||
// MARK: - Request
|
||||
extension Endpoint {
|
||||
@@ -65,13 +66,39 @@ extension Endpoint {
|
||||
}
|
||||
|
||||
internal func networkingError(_ error: Error) -> String {
|
||||
"""
|
||||
\(responseHeader)
|
||||
\(line)
|
||||
\(printableUrl)
|
||||
"|Networking Error ❌: \(error.localizedDescription)"
|
||||
\(footer)
|
||||
"""
|
||||
var messages = [
|
||||
responseHeader,
|
||||
line,
|
||||
printableUrl,
|
||||
"|Networking Error ❌: \(error.localizedDescription)"
|
||||
]
|
||||
if let responseError = error as? ResponseError {
|
||||
if let underlyingError = responseError.underlyingError {
|
||||
messages.append("|-UNDERLYING ERROR:")
|
||||
messages.append(contentsOf: readMessage(from: underlyingError))
|
||||
}
|
||||
} else {
|
||||
let nsError = error as NSError
|
||||
let underlyingErrors = nsError.underlyingErrors.map { $0 as NSError }
|
||||
if !underlyingErrors.isEmpty {
|
||||
messages.append("|-UNDERLYING ERROR:")
|
||||
for underlyingError in underlyingErrors {
|
||||
messages.append(contentsOf: readMessage(from: underlyingError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages.append(footer)
|
||||
return messages.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private func readMessage(from underlyingError: NSError) -> [String] {
|
||||
var messages = [underlyingError.localizedDescription]
|
||||
for error in underlyingError.underlyingErrors {
|
||||
let nsError = error as NSError
|
||||
messages.append(contentsOf: readMessage(from: nsError))
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
internal func unknownError() -> String {
|
||||
|
||||
+8
-1
@@ -17,6 +17,13 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension JSONEncoder {
|
||||
convenience init(strategy: JSONEncoder.KeyEncodingStrategy) {
|
||||
self.init()
|
||||
keyEncodingStrategy = strategy
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONEncoder.KeyEncodingStrategy {
|
||||
private struct Key: CodingKey {
|
||||
var stringValue: String
|
||||
@@ -33,7 +40,7 @@ extension JSONEncoder.KeyEncodingStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
static var capitalizeFirstLetter: Self {
|
||||
public static var capitalizeFirstLetter: Self {
|
||||
.custom { codingPath in
|
||||
guard let lastKey = codingPath.last else {
|
||||
fatalError("Coding path no key")
|
||||
@@ -18,12 +18,21 @@
|
||||
import Foundation
|
||||
import ProtonCoreServices
|
||||
import ProtonCoreNetworking
|
||||
import ProtonCoreUtilities
|
||||
|
||||
extension PMAPIService: CoreAPIService {
|
||||
|
||||
public func perform(request route: Request, completion: @escaping (URLSessionDataTask?, Result<JSONDictionary, ResponseError>) -> Void) {
|
||||
self.perform(request: route, jsonDictionaryCompletion: completion)
|
||||
}
|
||||
|
||||
public func perform(
|
||||
request route: Request,
|
||||
callCompletionBlockUsing executor: CompletionBlockExecutor,
|
||||
completion: @escaping (URLSessionDataTask?, Result<JSONDictionary, ResponseError>) -> Void
|
||||
) {
|
||||
self.perform(request: route, callCompletionBlockUsing: executor, jsonDictionaryCompletion: completion)
|
||||
}
|
||||
|
||||
public func perform(request route: Request, dataTaskBlock: @escaping (URLSessionDataTask) -> Void, completion: @escaping (_ task: URLSessionDataTask?, _ result: Result<JSONDictionary, ResponseError>) -> Void) {
|
||||
self.perform(request: route, onDataTaskCreated: dataTaskBlock, jsonDictionaryCompletion: completion)
|
||||
|
||||
@@ -20,7 +20,8 @@ import ProtonCoreServices
|
||||
import ProtonCoreUtilities
|
||||
|
||||
// MARK: - Log System for PDClient
|
||||
public var log: ((String) -> Void)?
|
||||
public var logInfo: ((String) -> Void)?
|
||||
public var logError: ((String) -> Void)?
|
||||
|
||||
extension PMAPIService: DriveAPIService {
|
||||
public func request<E, Response>(from endpoint: E, completionExecutor: CompletionBlockExecutor, completion: @escaping (Result<Response, Error>) -> Void) where E: Endpoint, Response == E.Response {
|
||||
@@ -36,33 +37,33 @@ extension DriveAPIService {
|
||||
completionExecutor: CompletionBlockExecutor,
|
||||
completion: @escaping (Result<Response, Error>) -> Void
|
||||
) where E: Endpoint, Response == E.Response {
|
||||
log?(endpoint.prettyDescription)
|
||||
|
||||
logInfo?(endpoint.prettyDescription)
|
||||
|
||||
apiService.perform(request: endpoint, callCompletionBlockUsing: completionExecutor) { task, result in
|
||||
switch result {
|
||||
case .failure(let responseError):
|
||||
log?(endpoint.networkingError(responseError))
|
||||
logError?(endpoint.networkingError(responseError))
|
||||
return completion(.failure(responseError))
|
||||
|
||||
case .success(let responseDict):
|
||||
guard let responseData = try? JSONSerialization.data(withJSONObject: responseDict, options: .prettyPrinted) else {
|
||||
log?(endpoint.unknownError())
|
||||
logError?(endpoint.unknownError())
|
||||
return completion(.failure(URLError(.unknown)))
|
||||
}
|
||||
|
||||
let decoder = endpoint.decoder
|
||||
if let serverError = try? decoder.decode(PDClient.ErrorResponse.self, from: responseData) {
|
||||
let error = NSError(serverError)
|
||||
log?(endpoint.serverError(error))
|
||||
logError?(endpoint.serverError(error))
|
||||
return completion(.failure(error))
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try decoder.decode(E.Response.self, from: responseData)
|
||||
log?(endpoint.prettyResponse(responseData))
|
||||
logInfo?(endpoint.prettyResponse(responseData))
|
||||
return completion(.success(response))
|
||||
} catch {
|
||||
log?(endpoint.deserializingError(error))
|
||||
logError?(endpoint.deserializingError(error))
|
||||
return completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,43 +17,45 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Link: Codable {
|
||||
public struct Link: Codable, Equatable {
|
||||
public typealias LinkID = String
|
||||
|
||||
#if os(iOS)
|
||||
public let volumeID: String
|
||||
public var volumeID: String
|
||||
#else
|
||||
// this must be removed once macOS implements the migration to volumeID-based DB
|
||||
public var volumeID: String { "" }
|
||||
#endif
|
||||
// node
|
||||
public let linkID: LinkID
|
||||
public let parentLinkID: LinkID?
|
||||
public let type: LinkType
|
||||
public let name: String
|
||||
public let nameSignatureEmail: String?
|
||||
public let hash: String
|
||||
public let state: NodeState
|
||||
public let expirationTime: TimeInterval?
|
||||
public let size: Int
|
||||
public let MIMEType: String
|
||||
public let attributes: AttriburesMask
|
||||
public let permissions: PermissionMask
|
||||
public let nodeKey: String
|
||||
public let nodePassphrase: String
|
||||
public let nodePassphraseSignature: String
|
||||
public let signatureEmail: String
|
||||
public let createTime: TimeInterval
|
||||
public let modifyTime: TimeInterval
|
||||
public let trashed: TimeInterval?
|
||||
public let sharingDetails: SharingDetails?
|
||||
public let nbUrls: Int
|
||||
public let activeUrls: Int
|
||||
public let urlsExpired: Int
|
||||
public let XAttr: String?
|
||||
public let fileProperties: FileProperties?
|
||||
public let folderProperties: FolderProperties?
|
||||
public let documentProperties: DocumentProperties?
|
||||
public var linkID: LinkID
|
||||
public var parentLinkID: LinkID?
|
||||
public var type: LinkType
|
||||
public var name: String
|
||||
public var nameSignatureEmail: String?
|
||||
public var hash: String
|
||||
public var state: NodeState
|
||||
public var expirationTime: TimeInterval?
|
||||
public var size: Int
|
||||
public var MIMEType: String
|
||||
public var attributes: AttriburesMask
|
||||
public var permissions: PermissionMask
|
||||
public var nodeKey: String
|
||||
public var nodePassphrase: String
|
||||
public var nodePassphraseSignature: String
|
||||
public var signatureEmail: String
|
||||
public var createTime: TimeInterval
|
||||
public var modifyTime: TimeInterval
|
||||
public var trashed: TimeInterval?
|
||||
public var sharingDetails: SharingDetails?
|
||||
public var nbUrls: Int
|
||||
public var activeUrls: Int
|
||||
public var urlsExpired: Int
|
||||
public var XAttr: String?
|
||||
public var fileProperties: FileProperties?
|
||||
public var folderProperties: FolderProperties?
|
||||
public var documentProperties: DocumentProperties?
|
||||
public var photoProperties: PhotoProperties?
|
||||
public var albumProperties: AlbumProperties?
|
||||
|
||||
public init(linkID: LinkID, parentLinkID: LinkID?, volumeID: String, type: LinkType, name: String,
|
||||
nameSignatureEmail: String?, hash: String, state: NodeState, expirationTime: TimeInterval?,
|
||||
@@ -61,7 +63,9 @@ public struct Link: Codable {
|
||||
nodeKey: String, nodePassphrase: String, nodePassphraseSignature: String,
|
||||
signatureEmail: String, createTime: TimeInterval, modifyTime: TimeInterval,
|
||||
trashed: TimeInterval?, sharingDetails: SharingDetails?, nbUrls: Int, activeUrls: Int,
|
||||
urlsExpired: Int, XAttr: String?, fileProperties: FileProperties?, folderProperties: FolderProperties?, documentProperties: DocumentProperties? = nil) {
|
||||
urlsExpired: Int, XAttr: String?, fileProperties: FileProperties?, folderProperties: FolderProperties?,
|
||||
documentProperties: DocumentProperties? = nil, photoProperties: PhotoProperties? = nil,
|
||||
albumProperties: AlbumProperties? = nil) {
|
||||
self.linkID = linkID
|
||||
self.parentLinkID = parentLinkID
|
||||
#if os(iOS)
|
||||
@@ -92,6 +96,8 @@ public struct Link: Codable {
|
||||
self.fileProperties = fileProperties
|
||||
self.folderProperties = folderProperties
|
||||
self.documentProperties = documentProperties
|
||||
self.photoProperties = photoProperties
|
||||
self.albumProperties = albumProperties
|
||||
}
|
||||
|
||||
// Convenience initializer to allow migration to volume based APIs
|
||||
@@ -126,6 +132,8 @@ public struct Link: Codable {
|
||||
self.fileProperties = link.fileProperties
|
||||
self.folderProperties = link.folderProperties
|
||||
self.documentProperties = link.documentProperties
|
||||
self.photoProperties = link.photoProperties
|
||||
self.albumProperties = link.albumProperties
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,12 +172,13 @@ public extension Link {
|
||||
}
|
||||
}
|
||||
|
||||
public enum LinkType: Int, Codable {
|
||||
public enum LinkType: Int, Codable, CaseIterable, Equatable {
|
||||
case folder = 1
|
||||
case file = 2
|
||||
case album = 3
|
||||
}
|
||||
|
||||
public enum NodeState: Int, Codable {
|
||||
public enum NodeState: Int, Codable, Equatable {
|
||||
case draft = 0
|
||||
case active = 1
|
||||
case deleted = 2
|
||||
@@ -189,37 +198,65 @@ public enum NodeState: Int, Codable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct FileProperties: Codable {
|
||||
public let contentKeyPacket: String
|
||||
public let contentKeyPacketSignature: String?
|
||||
public let activeRevision: RevisionShort?
|
||||
public struct FileProperties: Codable, Equatable {
|
||||
public var contentKeyPacket: String
|
||||
public var contentKeyPacketSignature: String?
|
||||
public var activeRevision: RevisionShort?
|
||||
|
||||
public init(contentKeyPacket: String, contentKeyPacketSignature: String?, activeRevision: RevisionShort?) {
|
||||
self.contentKeyPacket = contentKeyPacket
|
||||
self.contentKeyPacketSignature = contentKeyPacketSignature
|
||||
self.activeRevision = activeRevision
|
||||
}
|
||||
}
|
||||
|
||||
public struct FolderProperties: Codable {
|
||||
public struct FolderProperties: Codable, Equatable {
|
||||
public var nodeHashKey: String
|
||||
|
||||
public init(nodeHashKey: String) {
|
||||
self.nodeHashKey = nodeHashKey
|
||||
}
|
||||
}
|
||||
|
||||
public struct DocumentProperties: Codable {
|
||||
public struct DocumentProperties: Codable, Equatable {
|
||||
public var size: Int
|
||||
}
|
||||
|
||||
public struct SharingDetails: Codable {
|
||||
public let shareID: String
|
||||
public let shareUrl: ShareURL? // can be null if no link is available
|
||||
|
||||
public struct PhotoProperties: Codable, Equatable {
|
||||
public var albums: [PhotoAlbum]
|
||||
// Could become nonoptional, but migration of `PersistedEvent`, relying on `Link`'s structure would be needed.
|
||||
public var tags: [Int]?
|
||||
}
|
||||
|
||||
public struct PhotoAlbum: Codable, Equatable {
|
||||
public var albumLinkID: String
|
||||
}
|
||||
|
||||
public struct AlbumProperties: Codable, Equatable {
|
||||
public var locked: Bool
|
||||
public var coverLinkID: String? // Nullable
|
||||
public var lastActivityTime: TimeInterval // last time a Photo was added to the Album
|
||||
public var nodeHashKey: String
|
||||
public var photoCount: Int
|
||||
}
|
||||
|
||||
public struct SharingDetails: Codable, Equatable {
|
||||
public var shareID: String
|
||||
public var shareUrl: ShareURL? // can be null if no link is available
|
||||
|
||||
public init(shareID: String, shareUrl: ShareURL?) {
|
||||
self.shareID = shareID
|
||||
self.shareUrl = shareUrl
|
||||
}
|
||||
}
|
||||
|
||||
public struct ShareURL: Codable {
|
||||
public let shareUrlID: String
|
||||
public let token: String? // not always provided, according to docs
|
||||
public let expireTime: Date?
|
||||
public let createTime: Date
|
||||
public let numAccesses: Int
|
||||
public let shareID: String
|
||||
public struct ShareURL: Codable, Equatable {
|
||||
public var shareUrlID: String
|
||||
public var token: String? // not always provided, according to docs
|
||||
public var expireTime: Date?
|
||||
public var createTime: Date
|
||||
public var numAccesses: Int
|
||||
public var shareID: String
|
||||
}
|
||||
|
||||
public typealias ShareURLShortMeta = ShareURL
|
||||
|
||||
@@ -19,12 +19,47 @@ import Foundation
|
||||
|
||||
public struct LinksResponse: Codable {
|
||||
public var code: Int
|
||||
public let links: [Link]
|
||||
public let parents: [Link]
|
||||
public var links: [Link]
|
||||
public var parents: [Link]
|
||||
|
||||
public init(code: Int, links: [Link], parents: [Link]) {
|
||||
self.code = code
|
||||
self.links = links
|
||||
self.parents = parents
|
||||
}
|
||||
|
||||
public var sortedLinks: [Link] {
|
||||
let sorter = LinkHierarchySorter()
|
||||
return sorter.sort(links: parents + links)
|
||||
}
|
||||
}
|
||||
|
||||
final class LinkHierarchySorter {
|
||||
|
||||
func sort(links: [Link]) -> [Link] {
|
||||
var sorted: [Link] = []
|
||||
for link in links {
|
||||
guard let parentLinkID = link.parentLinkID else {
|
||||
// Parent id doesn't exist, root?
|
||||
sorted.insert(link, at: 0)
|
||||
continue
|
||||
}
|
||||
|
||||
if let parentIndex = sorted.firstIndex(where: { $0.linkID == parentLinkID }) {
|
||||
// Parent link is in the sorted array, insert behind the parent
|
||||
sorted.insert(link, at: parentIndex + 1)
|
||||
continue
|
||||
}
|
||||
|
||||
if let childrenIndex = sorted.firstIndex(where: { $0.parentLinkID == link.linkID }) {
|
||||
// Children link is in the sorted array, insert ahead the children
|
||||
sorted.insert(link, at: childrenIndex)
|
||||
} else {
|
||||
// There is no parent link yet
|
||||
sorted.append(link)
|
||||
}
|
||||
}
|
||||
return sorted
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,17 +17,17 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct RevisionShort: Codable {
|
||||
public let ID: Revision.RevisionID
|
||||
public let createTime: TimeInterval
|
||||
public let size: Int
|
||||
public let manifestSignature: String? // can be nil if revision is a draft
|
||||
public let signatureAddress: String
|
||||
public let state: NodeState
|
||||
public let thumbnailDownloadUrl: URL?
|
||||
private let thumbnail: Int
|
||||
public let thumbnails: [Thumbnail]?
|
||||
public let photo: Photo?
|
||||
public struct RevisionShort: Codable, Equatable {
|
||||
public var ID: Revision.RevisionID
|
||||
public var createTime: TimeInterval
|
||||
public var size: Int
|
||||
public var manifestSignature: String? // can be nil if revision is a draft
|
||||
public var signatureAddress: String
|
||||
public var state: NodeState
|
||||
public var thumbnailDownloadUrl: URL?
|
||||
private var thumbnail: Int
|
||||
public var thumbnails: [Thumbnail]?
|
||||
public var photo: Photo?
|
||||
|
||||
public var hasThumbnail: Bool {
|
||||
NSNumber.init(value: thumbnail).boolValue
|
||||
@@ -51,38 +51,72 @@ public struct RevisionShort: Codable {
|
||||
public struct Revision: Codable {
|
||||
public typealias RevisionID = String
|
||||
|
||||
public let ID: RevisionID
|
||||
public let createTime: TimeInterval
|
||||
public let size: Int
|
||||
public let manifestSignature: String
|
||||
public let signatureAddress: String
|
||||
public let state: NodeState
|
||||
public let blocks: [Block]
|
||||
public let thumbnail: Int
|
||||
public let thumbnailHash: String?
|
||||
public let thumbnailDownloadUrl: URL?
|
||||
public let XAttr: String?
|
||||
public var ID: RevisionID
|
||||
public var createTime: TimeInterval
|
||||
public var size: Int
|
||||
public var manifestSignature: String
|
||||
public var signatureAddress: String
|
||||
public var state: NodeState
|
||||
public var blocks: [Block]
|
||||
public var thumbnail: Int
|
||||
public var thumbnailHash: String?
|
||||
public var thumbnailDownloadUrl: URL?
|
||||
public var XAttr: String?
|
||||
|
||||
public init(ID: RevisionID,
|
||||
createTime: TimeInterval,
|
||||
size: Int,
|
||||
manifestSignature: String,
|
||||
signatureAddress: String,
|
||||
state: NodeState,
|
||||
blocks: [Block],
|
||||
thumbnail: Int,
|
||||
thumbnailHash: String?,
|
||||
thumbnailDownloadUrl: URL?,
|
||||
XAttr: String?) {
|
||||
self.ID = ID
|
||||
self.createTime = createTime
|
||||
self.size = size
|
||||
self.manifestSignature = manifestSignature
|
||||
self.signatureAddress = signatureAddress
|
||||
self.state = state
|
||||
self.blocks = blocks
|
||||
self.thumbnail = thumbnail
|
||||
self.thumbnailHash = thumbnailHash
|
||||
self.thumbnailDownloadUrl = thumbnailDownloadUrl
|
||||
self.XAttr = XAttr
|
||||
}
|
||||
}
|
||||
|
||||
public struct Block: Codable {
|
||||
public let index: Int
|
||||
public let hash: String
|
||||
public let URL: URL
|
||||
public let encSignature: String
|
||||
public let signatureEmail: String?
|
||||
public var index: Int
|
||||
public var hash: String
|
||||
public var URL: URL
|
||||
public var encSignature: String?
|
||||
public var signatureEmail: String?
|
||||
}
|
||||
|
||||
public struct Thumbnail: Codable {
|
||||
public let thumbnailID: String
|
||||
public let type: Int
|
||||
public let hash: String
|
||||
public let size: Int
|
||||
public struct Thumbnail: Codable, Equatable {
|
||||
public var thumbnailID: String
|
||||
public var type: Int
|
||||
public var hash: String
|
||||
public var size: Int
|
||||
|
||||
public init(thumbnailID: String, type: Int, hash: String, size: Int) {
|
||||
self.thumbnailID = thumbnailID
|
||||
self.type = type
|
||||
self.hash = hash
|
||||
self.size = size
|
||||
}
|
||||
}
|
||||
|
||||
public struct Photo: Codable {
|
||||
public let linkID: String
|
||||
public let captureTime: TimeInterval
|
||||
public let mainPhotoLinkID: String?
|
||||
public let hash: String
|
||||
public let exif: String?
|
||||
public struct Photo: Codable, Equatable {
|
||||
public var linkID: String
|
||||
public var captureTime: TimeInterval
|
||||
public var addedTime: Date?
|
||||
public var mainPhotoLinkID: String?
|
||||
public var relatedPhotosLinkIDs: [String]?
|
||||
public var hash: String // name hash
|
||||
public var contentHash: String? // optional due to backward compatibility of events
|
||||
public var exif: String?
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ public struct Share: Codable {
|
||||
public let type: ´Type´
|
||||
|
||||
public enum ´Type´: Int, Codable {
|
||||
case undefined = 0
|
||||
case main = 1
|
||||
case standard = 2
|
||||
case device = 3
|
||||
|
||||
@@ -30,6 +30,10 @@ public struct AccessPermission: OptionSet, Codable {
|
||||
public var isEditor: Bool {
|
||||
self.contains([.read, .write])
|
||||
}
|
||||
|
||||
public func toRequestPermission() -> ShareURLMeta.Permissions {
|
||||
isEditor ? [.read, .write] : [.read]
|
||||
}
|
||||
}
|
||||
|
||||
public enum ExternalInviteState: Int, Codable {
|
||||
|
||||
@@ -46,6 +46,11 @@ public struct Volume: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum VolumeType: Int, Codable {
|
||||
case main = 1
|
||||
case photo = 2
|
||||
}
|
||||
|
||||
public var volumeID: VolumeID
|
||||
public var createTime: TimeInterval?
|
||||
public var modifyTime: TimeInterval?
|
||||
@@ -55,10 +60,11 @@ public struct Volume: Codable {
|
||||
public var state: State?
|
||||
public var share: Share
|
||||
public var restoreStatus: RestoreStatus?
|
||||
|
||||
public let type: VolumeType
|
||||
|
||||
public init(volumeID: VolumeID, createTime: TimeInterval? = nil, modifyTime: TimeInterval? = nil,
|
||||
uploadedBytes: Int, maxSpace: Int? = nil, usedSpace: Int? = nil, state: State? = nil,
|
||||
share: Share, restoreStatus: RestoreStatus? = nil) {
|
||||
share: Share, restoreStatus: RestoreStatus? = nil, type: VolumeType = .main) {
|
||||
self.volumeID = volumeID
|
||||
self.createTime = createTime
|
||||
self.modifyTime = modifyTime
|
||||
@@ -68,5 +74,6 @@ public struct Volume: Codable {
|
||||
self.state = state
|
||||
self.share = share
|
||||
self.restoreStatus = restoreStatus
|
||||
self.type = type
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// /drive/volumes/{volumeID}/links/{linkID}/copy
|
||||
/// Copy a single file to a volume, providing the new parent link ID.
|
||||
public struct CopyLinkToVolumeEndpoint: Endpoint {
|
||||
public struct Response: Codable {
|
||||
var code: Int
|
||||
var linkID: String
|
||||
}
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
public init(parameters: Parameters, service: APIService, credential: ClientCredential) {
|
||||
var url = service.url(of: "/volumes")
|
||||
url.appendPathComponent(parameters.volumeID)
|
||||
url.appendPathComponent("/links")
|
||||
url.appendPathComponent(parameters.linkID)
|
||||
url.appendPathComponent("/copy")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
// body
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .capitalizeFirstLetter
|
||||
request.httpBody = try? encoder.encode(parameters.body)
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
|
||||
extension CopyLinkToVolumeEndpoint {
|
||||
public struct Parameters {
|
||||
public let volumeID: Volume.VolumeID
|
||||
public let linkID: Link.LinkID
|
||||
public let body: Body
|
||||
|
||||
public init(volumeID: Volume.VolumeID, linkID: Link.LinkID, body: Body) {
|
||||
self.volumeID = volumeID
|
||||
self.linkID = linkID
|
||||
self.body = body
|
||||
}
|
||||
}
|
||||
|
||||
public struct Body: Codable {
|
||||
public let name: String
|
||||
public let nodePassphrase: String
|
||||
public let hash: String
|
||||
public let targetVolumeID: Volume.VolumeID
|
||||
public let targetParentLinkID: Link.LinkID
|
||||
public let nameSignatureEmail: String
|
||||
/// Required when moving an anonymous Link.
|
||||
public let nodePassphraseSignature: String?
|
||||
public let signatureEmail: String?
|
||||
/// Optional, except when moving a Photo-Link.
|
||||
public let photos: Photos?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
nodePassphrase: String,
|
||||
hash: String,
|
||||
targetVolumeID: Volume.VolumeID,
|
||||
targetParentLinkID: Link.LinkID,
|
||||
nameSignatureEmail: String,
|
||||
nodePassphraseSignature: String?,
|
||||
signatureEmail: String?,
|
||||
photos: Photos?
|
||||
) {
|
||||
self.name = name
|
||||
self.nodePassphrase = nodePassphrase
|
||||
self.hash = hash
|
||||
self.targetVolumeID = targetVolumeID
|
||||
self.targetParentLinkID = targetParentLinkID
|
||||
self.nameSignatureEmail = nameSignatureEmail
|
||||
self.nodePassphraseSignature = nodePassphraseSignature
|
||||
self.signatureEmail = signatureEmail
|
||||
self.photos = photos
|
||||
}
|
||||
}
|
||||
|
||||
public struct Photos: Codable {
|
||||
public let contentHash: String
|
||||
public let relatedPhotos: [RelatedPhoto]
|
||||
|
||||
public init(contentHash: String, relatedPhotos: [RelatedPhoto]) {
|
||||
self.contentHash = contentHash
|
||||
self.relatedPhotos = relatedPhotos
|
||||
}
|
||||
}
|
||||
|
||||
public struct RelatedPhoto: Codable {
|
||||
public let linkID: String
|
||||
public let name: String
|
||||
public let nodePassphrase: String
|
||||
public let hash: String
|
||||
public let contentHash: String
|
||||
|
||||
public init(linkID: String, name: String, nodePassphrase: String, hash: String, contentHash: String) {
|
||||
self.linkID = linkID
|
||||
self.name = name
|
||||
self.nodePassphrase = nodePassphrase
|
||||
self.hash = hash
|
||||
self.contentHash = contentHash
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct DeleteMultipleResponse: Codable {
|
||||
let code: Int
|
||||
let responses: LinkCodesResponseElement
|
||||
}
|
||||
|
||||
struct DeleteMultipleParameters {
|
||||
let volumeId: Volume.VolumeID
|
||||
let body: Body
|
||||
|
||||
init(volumeId: Volume.VolumeID, linkIds: [Link.LinkID]) {
|
||||
self.volumeId = volumeId
|
||||
body = Body(linkIds: linkIds)
|
||||
}
|
||||
|
||||
struct Body: Encodable {
|
||||
let linkIds: [Link.LinkID]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case linkIds = "LinkIDs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To delete multiple links
|
||||
// POST: /v2/volumes/{volumeID}/trash/delete_multiple
|
||||
struct DeleteMultipleEndpoint: Endpoint {
|
||||
typealias Response = DeleteMultipleResponse
|
||||
|
||||
var request: URLRequest
|
||||
|
||||
init(parameters: DeleteMultipleParameters, service: APIService, credential: ClientCredential) {
|
||||
var url = service.url(of: "/v2/volumes")
|
||||
url.appendPathComponent(parameters.volumeId)
|
||||
url.appendPathComponent("/trash")
|
||||
url.appendPathComponent("/delete_multiple")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
let body = try? JSONEncoder().encode(parameters.body)
|
||||
assert(body != nil, "Failed body encoding")
|
||||
|
||||
request.httpBody = body
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// List Links
|
||||
/// - POST: /drive/volumes/{volumeID}/links/fetch_metadata
|
||||
public struct LinksMetadataByVolumeEndpoint: Endpoint {
|
||||
public let request: URLRequest
|
||||
public typealias Response = LinksResponseByVolume
|
||||
|
||||
public init(service: APIService, credential: ClientCredential, parameters: LinksMetadataByVolumeParameters) {
|
||||
let url = service.url(of: "/volumes/\(parameters.volumeId)/links/fetch_metadata")
|
||||
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
request.httpBody = try? JSONEncoder().encode(["LinkIDs": parameters.linkIds])
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
|
||||
public struct LinksMetadataByVolumeParameters {
|
||||
public let volumeId: String
|
||||
public let linkIds: [String]
|
||||
|
||||
public init(volumeId: String, linkIds: [String]) {
|
||||
self.volumeId = volumeId
|
||||
self.linkIds = linkIds
|
||||
}
|
||||
}
|
||||
|
||||
public struct LinksResponseByVolume: Codable {
|
||||
public var code: Int
|
||||
public let links: [Link]
|
||||
|
||||
public init(code: Int, links: [Link]) {
|
||||
self.code = code
|
||||
self.links = links
|
||||
}
|
||||
|
||||
public var sortedLinks: [Link] {
|
||||
let sorter = LinkHierarchySorter()
|
||||
return sorter.sort(links: links)
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,20 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct NewBlocksEndpoint: Endpoint {
|
||||
public struct NewBlocksEndpoint: Endpoint {
|
||||
public struct Response: Codable {
|
||||
let code: Int
|
||||
let uploadLinks: [ContentUploadLink]
|
||||
let thumbnailLinks: [ContentUploadLink]?
|
||||
|
||||
public init(code: Int, uploadLinks: [ContentUploadLink], thumbnailLinks: [ContentUploadLink]?) {
|
||||
self.code = code
|
||||
self.uploadLinks = uploadLinks
|
||||
self.thumbnailLinks = thumbnailLinks
|
||||
}
|
||||
}
|
||||
|
||||
var request: URLRequest
|
||||
public var request: URLRequest
|
||||
|
||||
init(parameters: NewPhotoBlocksParameters, service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
|
||||
@@ -20,6 +20,11 @@ import Foundation
|
||||
public struct NewFile: Codable {
|
||||
public var ID: String
|
||||
public var revisionID: String
|
||||
|
||||
public init(ID: String, revisionID: String) {
|
||||
self.ID = ID
|
||||
self.revisionID = revisionID
|
||||
}
|
||||
}
|
||||
|
||||
public struct NewFileParameters: Codable {
|
||||
@@ -48,26 +53,31 @@ public struct NewFileParameters: Codable {
|
||||
self.ClientUID = clientUID
|
||||
}
|
||||
|
||||
var Name: String
|
||||
var Hash: String
|
||||
var ParentLinkID: String
|
||||
var NodeKey: String
|
||||
var NodePassphrase: String
|
||||
var NodePassphraseSignature: String
|
||||
var SignatureAddress: String
|
||||
var ContentKeyPacket: String
|
||||
var ContentKeyPacketSignature: String
|
||||
var MIMEType: String
|
||||
var ClientUID: String
|
||||
public private(set) var Name: String
|
||||
public private(set) var Hash: String
|
||||
public private(set) var ParentLinkID: String
|
||||
public private(set) var NodeKey: String
|
||||
public private(set) var NodePassphrase: String
|
||||
public private(set) var NodePassphraseSignature: String
|
||||
public private(set) var SignatureAddress: String
|
||||
public private(set) var ContentKeyPacket: String
|
||||
public private(set) var ContentKeyPacketSignature: String
|
||||
public private(set) var MIMEType: String
|
||||
public private(set) var ClientUID: String
|
||||
}
|
||||
|
||||
struct NewFileEndpoint: Endpoint {
|
||||
public struct NewFileEndpoint: Endpoint {
|
||||
public struct Response: Codable {
|
||||
var code: Int
|
||||
var file: NewFile
|
||||
public var file: NewFile
|
||||
|
||||
public init(code: Int, file: NewFile) {
|
||||
self.code = code
|
||||
self.file = file
|
||||
}
|
||||
}
|
||||
|
||||
var request: URLRequest
|
||||
public private(set) var request: URLRequest
|
||||
|
||||
init(shareID: Share.ShareID, parameters: NewFileParameters, service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
|
||||
@@ -19,6 +19,10 @@ import Foundation
|
||||
|
||||
public struct NewFolder: Codable {
|
||||
public var ID: String
|
||||
|
||||
public init(ID: String) {
|
||||
self.ID = ID
|
||||
}
|
||||
}
|
||||
|
||||
public class NewFolderParameters: Codable {
|
||||
@@ -52,13 +56,18 @@ public class NewFolderParameters: Codable {
|
||||
var SignatureAddress: String
|
||||
}
|
||||
|
||||
struct NewFolderEndpoint: Endpoint {
|
||||
public struct NewFolderEndpoint: Endpoint {
|
||||
public struct Response: Codable {
|
||||
var code: Int
|
||||
var folder: NewFolder
|
||||
|
||||
public init(code: Int, folder: NewFolder) {
|
||||
self.code = code
|
||||
self.folder = folder
|
||||
}
|
||||
}
|
||||
|
||||
var request: URLRequest
|
||||
public var request: URLRequest
|
||||
|
||||
init(shareID: Share.ShareID, parameters: NewFolderParameters, service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
|
||||
+4
-4
@@ -17,19 +17,19 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct CreateProtonDocumentResponse: Codable {
|
||||
struct CreateProtonFileResponse: Codable {
|
||||
let code: Int
|
||||
let document: DocumentIdentifier
|
||||
}
|
||||
|
||||
/// Create document
|
||||
/// POST: /drive/shares/{shareID}/documents
|
||||
struct NewDocumentEndpoint: Endpoint {
|
||||
typealias Response = CreateProtonDocumentResponse
|
||||
struct NewProtonFileEndpoint: Endpoint {
|
||||
typealias Response = CreateProtonFileResponse
|
||||
|
||||
var request: URLRequest
|
||||
|
||||
init(parameters: NewDocumentParameters, service: APIService, credential: ClientCredential) {
|
||||
init(parameters: NewProtonFileParameters, service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
let url = service.url(of: "/shares/\(parameters.shareId)/documents")
|
||||
|
||||
@@ -19,16 +19,20 @@ import Foundation
|
||||
|
||||
public struct NewRevision: Codable {
|
||||
public var ID: Revision.RevisionID
|
||||
|
||||
public init(ID: Revision.RevisionID) {
|
||||
self.ID = ID
|
||||
}
|
||||
}
|
||||
|
||||
struct NewRevisionEndpoint: Endpoint {
|
||||
public struct NewRevisionEndpoint: Endpoint {
|
||||
public struct Response: Codable {
|
||||
var code: Int
|
||||
var revision: NewRevision
|
||||
public var revision: NewRevision
|
||||
}
|
||||
|
||||
var request: URLRequest
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
init(fileID: Link.LinkID, shareID: Share.ShareID, service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
var url = service.url(of: "/shares")
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import ProtonCoreNetworking
|
||||
import Foundation
|
||||
|
||||
public struct BugReport {
|
||||
public let os: String
|
||||
public let osVersion: String
|
||||
public let client: String
|
||||
public let clientType: Int
|
||||
public let clientVersion: String
|
||||
public let title: String
|
||||
public let description: String
|
||||
public let username: String
|
||||
public let email: String
|
||||
public let files: [URL]
|
||||
|
||||
public init(os: String, osVersion: String, client: String, clientType: Int, clientVersion: String, title: String, description: String, username: String, email: String, files: [URL]) {
|
||||
self.os = os
|
||||
self.osVersion = osVersion
|
||||
self.client = client
|
||||
self.clientType = clientType
|
||||
self.clientVersion = clientVersion
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.username = username
|
||||
self.email = email
|
||||
self.files = files
|
||||
}
|
||||
}
|
||||
|
||||
public final class ReportsBugsEndpoint: Request {
|
||||
public let report: BugReport
|
||||
|
||||
public init( _ report: BugReport) {
|
||||
self.report = report
|
||||
}
|
||||
|
||||
public var path: String {
|
||||
return "/core/v4/reports/bug"
|
||||
}
|
||||
|
||||
public var method: HTTPMethod {
|
||||
return .post
|
||||
}
|
||||
|
||||
public var parameters: [String: Any]? {
|
||||
return [
|
||||
"OS": report.os,
|
||||
"OSVersion": report.osVersion,
|
||||
"Client": report.client,
|
||||
"ClientVersion": report.clientVersion,
|
||||
"ClientType": String(report.clientType),
|
||||
"Title": report.title,
|
||||
"Description": report.description,
|
||||
"Username": report.username,
|
||||
"Email": report.email,
|
||||
]
|
||||
}
|
||||
|
||||
public var retryPolicy: ProtonRetryPolicy.RetryMode {
|
||||
.userInitiated
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct SetTagsMigrationStateEndpoint: Endpoint {
|
||||
public struct Response: Codable {
|
||||
let code: Int
|
||||
}
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
public init(volumeID: String, requestBody: TagsMigrationStateRequest, service: APIService, credential: ClientCredential) {
|
||||
var url = service.url(of: "/photos/volumes")
|
||||
url.appendPathComponent(volumeID)
|
||||
url.appendPathComponent("tags-migration")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
request.httpBody = try? JSONEncoder(strategy: .capitalizeFirstLetter).encode(requestBody)
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
|
||||
public struct TagsMigrationStateRequest: Codable {
|
||||
public let finished: Bool
|
||||
public let anchor: TagsMigrationStateAnchorRequest?
|
||||
|
||||
public init(finished: Bool, anchor: TagsMigrationStateAnchorRequest?) {
|
||||
self.finished = finished
|
||||
self.anchor = anchor
|
||||
}
|
||||
|
||||
public struct TagsMigrationStateAnchorRequest: Codable {
|
||||
public let lastProcessedLinkID: String
|
||||
public let lastProcessedCaptureTime: Int
|
||||
public let currentTimestamp: Int
|
||||
public let clientUID: String
|
||||
|
||||
public init(lastProcessedLinkID: String, lastProcessedCaptureTime: Int, currentTimestamp: Int, clientUID: String) {
|
||||
self.lastProcessedLinkID = lastProcessedLinkID
|
||||
self.lastProcessedCaptureTime = lastProcessedCaptureTime
|
||||
self.currentTimestamp = currentTimestamp
|
||||
self.clientUID = clientUID
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,8 @@ public struct TrashLinksParameters {
|
||||
|
||||
/// Trash Children
|
||||
/// /shares/{enc_shareID}/folders/{enc_linkID}/trash_multiple
|
||||
struct TrashLinkEndpoint: Endpoint {
|
||||
typealias Response = MultipleLinkResponse
|
||||
public struct TrashLinkEndpoint: Endpoint {
|
||||
public typealias Response = MultipleLinkResponse
|
||||
|
||||
struct Body: Encodable {
|
||||
let linkIDs: [Link.LinkID]
|
||||
@@ -42,7 +42,7 @@ struct TrashLinkEndpoint: Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
var request: URLRequest
|
||||
public var request: URLRequest
|
||||
|
||||
init(parameters: TrashLinksParameters, service: APIService, credential: ClientCredential, breadcrumbs: Breadcrumbs) throws {
|
||||
var url = service.url(of: "/shares")
|
||||
@@ -79,14 +79,29 @@ struct TrashLinkEndpoint: Endpoint {
|
||||
public struct MultipleLinkResponse: Codable {
|
||||
public let code: Int
|
||||
public let responses: [LinkResponse]
|
||||
|
||||
public init(code: Int, responses: [LinkResponse]) {
|
||||
self.code = code
|
||||
self.responses = responses
|
||||
}
|
||||
|
||||
public struct LinkResponse: Codable {
|
||||
public let linkID: String
|
||||
public let response: ErrorResponse
|
||||
|
||||
public init(linkID: String, response: ErrorResponse) {
|
||||
self.linkID = linkID
|
||||
self.response = response
|
||||
}
|
||||
}
|
||||
|
||||
public struct ErrorResponse: Codable {
|
||||
public let code: Int
|
||||
public let error: String?
|
||||
|
||||
public init(code: Int, error: String?) {
|
||||
self.code = code
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct TrashVolumeLinksParameters {
|
||||
public let volumeID: String
|
||||
public let linkIds: [String]
|
||||
|
||||
public init(volumeID: String, linkIds: [String]) {
|
||||
self.volumeID = volumeID
|
||||
self.linkIds = linkIds
|
||||
}
|
||||
}
|
||||
|
||||
/// Trash links
|
||||
/// /v2/volumes/{volumeID}/trash_multiple
|
||||
public struct TrashVolumeLinkEndpoint: Endpoint {
|
||||
public typealias Response = MultipleLinkResponse
|
||||
|
||||
struct Body: Encodable {
|
||||
let linkIDs: [Link.LinkID]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case linkIDs = "LinkIDs"
|
||||
}
|
||||
}
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
init(parameters: TrashVolumeLinksParameters, service: APIService, credential: ClientCredential, breadcrumbs: Breadcrumbs) throws {
|
||||
var url = service.url(of: "/v2/volumes")
|
||||
url.appendPathComponent(parameters.volumeID)
|
||||
url.appendPathComponent("/trash_multiple")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
let validLinkIDs = parameters.linkIds.filter { UUID(uuidString: $0) == nil }
|
||||
guard !validLinkIDs.isEmpty else {
|
||||
// if there are no valid linkIDs at all, there's no need to make the trash request at all
|
||||
let invalidLinkIDs = parameters.linkIds.compactMap { UUID(uuidString: $0) }
|
||||
let message = "Tried to trash a folder with invalid linkID(s) \(invalidLinkIDs), breadcrumbs: \(breadcrumbs.collect().reduceIntoErrorMessage())"
|
||||
assertionFailure(message)
|
||||
throw InvalidLinkIdError(detailedMessage: message)
|
||||
}
|
||||
|
||||
let body = try? JSONEncoder().encode(Body(linkIDs: validLinkIDs))
|
||||
assert(body != nil, "Failed body encoding")
|
||||
|
||||
request.httpBody = body
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
@@ -21,43 +21,6 @@ public protocol EditShareURLParameters: Codable {
|
||||
var parameters: [String: Any]? { get }
|
||||
}
|
||||
|
||||
public struct EditShareURLPasswordAndDuration: EditShareURLParameters {
|
||||
let ExpirationDuration: Int?
|
||||
let UrlPasswordSalt: String
|
||||
let SharePasswordSalt: String
|
||||
let SRPVerifier: String
|
||||
let SRPModulusID: String
|
||||
let Flags: ShareURLMeta.Flags
|
||||
let SharePassphraseKeyPacket: String
|
||||
let Password: String
|
||||
|
||||
public private(set) var parameters: [String: Any]?
|
||||
|
||||
public init(_ expirationParameters: EditShareURLExpiration, _ passwordParameters: EditShareURLPassword) {
|
||||
self.ExpirationDuration = expirationParameters.ExpirationDuration
|
||||
self.UrlPasswordSalt = passwordParameters.UrlPasswordSalt
|
||||
self.SharePasswordSalt = passwordParameters.SharePasswordSalt
|
||||
self.SRPVerifier = passwordParameters.SRPVerifier
|
||||
self.SRPModulusID = passwordParameters.SRPModulusID
|
||||
self.Flags = passwordParameters.Flags
|
||||
self.SharePassphraseKeyPacket = passwordParameters.SharePassphraseKeyPacket
|
||||
self.Password = passwordParameters.Password
|
||||
|
||||
parameters = expirationParameters.parameters?.merging(passwordParameters.parameters ?? [:], uniquingKeysWith: { $1 })
|
||||
}
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case ExpirationDuration
|
||||
case UrlPasswordSalt
|
||||
case SharePasswordSalt
|
||||
case SRPVerifier
|
||||
case SRPModulusID
|
||||
case Flags
|
||||
case SharePassphraseKeyPacket
|
||||
case Password
|
||||
}
|
||||
}
|
||||
|
||||
public struct EditShareURLPassword: EditShareURLParameters {
|
||||
let UrlPasswordSalt: String
|
||||
let SharePasswordSalt: String
|
||||
@@ -87,6 +50,18 @@ public struct EditShareURLPassword: EditShareURLParameters {
|
||||
}
|
||||
}
|
||||
|
||||
public struct EditShareURLPermissions: EditShareURLParameters {
|
||||
let Permissions: ShareURLMeta.Permissions
|
||||
|
||||
public init(permissions: ShareURLMeta.Permissions) {
|
||||
self.Permissions = permissions
|
||||
}
|
||||
|
||||
public var parameters: [String: Any]? {
|
||||
["Permissions": Permissions.rawValue]
|
||||
}
|
||||
}
|
||||
|
||||
public struct EditShareURLExpiration: EditShareURLParameters {
|
||||
let ExpirationDuration: Int?
|
||||
|
||||
@@ -101,6 +76,31 @@ public struct EditShareURLExpiration: EditShareURLParameters {
|
||||
}
|
||||
}
|
||||
|
||||
public struct EditShareURLUpdateParameters: EditShareURLParameters {
|
||||
let expirationParameters: EditShareURLExpiration?
|
||||
let passwordParameters: EditShareURLPassword?
|
||||
let permissionParameters: EditShareURLPermissions?
|
||||
|
||||
public init(
|
||||
expirationParameters: EditShareURLExpiration?,
|
||||
passwordParameters: EditShareURLPassword?,
|
||||
permissionParameters: EditShareURLPermissions?
|
||||
) {
|
||||
self.expirationParameters = expirationParameters
|
||||
self.passwordParameters = passwordParameters
|
||||
self.permissionParameters = permissionParameters
|
||||
}
|
||||
|
||||
public var parameters: [String: Any]? {
|
||||
let expiration = expirationParameters?.parameters ?? [:]
|
||||
let password = passwordParameters?.parameters ?? [:]
|
||||
let permission = permissionParameters?.parameters ?? [:]
|
||||
return expiration
|
||||
.merging(password) { current, _ in current }
|
||||
.merging(permission) { current, _ in current }
|
||||
}
|
||||
}
|
||||
|
||||
struct EditShareURLEndpoint<Parameters: EditShareURLParameters>: Endpoint {
|
||||
public struct Response: Codable {
|
||||
var code: Int
|
||||
|
||||
@@ -19,13 +19,17 @@ import Foundation
|
||||
|
||||
public struct MoveEntryEndpoint: Endpoint {
|
||||
public struct Parameters: Codable {
|
||||
let Name: String
|
||||
let NodePassphrase: String
|
||||
let Hash: String
|
||||
let ParentLinkID: String
|
||||
let NameSignatureEmail: String
|
||||
let OriginalHash: String
|
||||
let NewShareID: String?
|
||||
public let Name: String
|
||||
public let NodePassphrase: String
|
||||
public let Hash: String
|
||||
public let ParentLinkID: String
|
||||
public let NameSignatureEmail: String
|
||||
public let OriginalHash: String
|
||||
public let NewShareID: String?
|
||||
public let NodePassphraseSignature: String?
|
||||
public let SignatureEmail: String?
|
||||
/// Optional, except when moving a Photo-Link.
|
||||
public let ContentHash: String?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
@@ -34,20 +38,30 @@ public struct MoveEntryEndpoint: Endpoint {
|
||||
parentLinkID: String,
|
||||
nameSignatureEmail: String,
|
||||
originalHash: String,
|
||||
newShareID: String?
|
||||
newShareID: String?,
|
||||
nodePassphraseSignature: String? = nil,
|
||||
signatureEmail: String? = nil,
|
||||
contentHash: String? = nil
|
||||
) {
|
||||
self.Name = name
|
||||
self.NodePassphrase = nodePassphrase
|
||||
self.NodePassphraseSignature = nodePassphraseSignature
|
||||
self.Hash = hash
|
||||
self.ParentLinkID = parentLinkID
|
||||
self.NameSignatureEmail = nameSignatureEmail
|
||||
self.OriginalHash = originalHash
|
||||
self.NewShareID = newShareID
|
||||
self.SignatureEmail = signatureEmail
|
||||
self.ContentHash = contentHash
|
||||
}
|
||||
}
|
||||
|
||||
public struct Response: Codable {
|
||||
var code: Int
|
||||
|
||||
public init(code: Int) {
|
||||
self.code = code
|
||||
}
|
||||
}
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct MoveMultipleEndpoint: Endpoint {
|
||||
public struct Parameters: Codable {
|
||||
public let ParentLinkID: String
|
||||
public let Links: [Link]
|
||||
public let NameSignatureEmail: String
|
||||
public let SignatureEmail: String
|
||||
public let NewShareID: String?
|
||||
|
||||
public init(
|
||||
parentLinkID: String,
|
||||
links: [Link],
|
||||
nameSignatureEmail: String,
|
||||
signatureEmail: String,
|
||||
newShareID: String?
|
||||
) {
|
||||
self.ParentLinkID = parentLinkID
|
||||
self.Links = links
|
||||
self.NameSignatureEmail = nameSignatureEmail
|
||||
self.SignatureEmail = signatureEmail
|
||||
self.NewShareID = newShareID
|
||||
}
|
||||
}
|
||||
|
||||
public struct Link: Codable {
|
||||
public let LinkID: String
|
||||
public let Name: String
|
||||
public let NodePassphrase: String
|
||||
public let Hash: String
|
||||
/// Current name hash before move operation.
|
||||
public let OriginalHash: String
|
||||
/// except when moving a Photo-Link. Photo content hash
|
||||
public let ContentHash: String?
|
||||
/// Required when moving an anonymous Link. It must be signed by the SignatureEmail address.
|
||||
public let NodePassphraseSignature: String?
|
||||
|
||||
public init(
|
||||
linkID: String,
|
||||
name: String,
|
||||
nodePassphrase: String,
|
||||
hash: String,
|
||||
originalHash: String,
|
||||
contentHash: String?,
|
||||
nodePassphraseSignature: String?
|
||||
) {
|
||||
self.LinkID = linkID
|
||||
self.Name = name
|
||||
self.NodePassphrase = nodePassphrase
|
||||
self.Hash = hash
|
||||
self.OriginalHash = originalHash
|
||||
self.ContentHash = contentHash
|
||||
self.NodePassphraseSignature = nodePassphraseSignature
|
||||
}
|
||||
}
|
||||
|
||||
public struct Response: Codable {
|
||||
var code: Int
|
||||
|
||||
public init(code: Int) {
|
||||
self.code = code
|
||||
}
|
||||
}
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
public init(volumeID: Volume.VolumeID, parameters: Parameters, service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
var url = service.url(of: "/volumes")
|
||||
url.appendPathComponent(volumeID)
|
||||
url.appendPathComponent("/links")
|
||||
url.appendPathComponent("/move-multiple")
|
||||
|
||||
// request
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
|
||||
// headers
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
request.httpBody = try? JSONEncoder().encode(parameters)
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
import Foundation
|
||||
|
||||
public struct RenameNodeParameters: Codable {
|
||||
public init(name: String, hash: String, MIMEType: String?, signatureAddress: String) {
|
||||
public init(name: String, hash: String?, MIMEType: String?, signatureAddress: String) {
|
||||
self.Name = name
|
||||
self.Hash = hash
|
||||
self.MIMEType = MIMEType
|
||||
@@ -26,17 +26,21 @@ public struct RenameNodeParameters: Codable {
|
||||
}
|
||||
|
||||
var Name: String
|
||||
var Hash: String
|
||||
var Hash: String?
|
||||
var MIMEType: String?
|
||||
var SignatureAddress: String
|
||||
}
|
||||
|
||||
struct RenameNodeEndpoint: Endpoint {
|
||||
public struct RenameNodeEndpoint: Endpoint {
|
||||
public struct Response: Codable {
|
||||
var code: Int
|
||||
|
||||
public init(code: Int) {
|
||||
self.code = code
|
||||
}
|
||||
}
|
||||
|
||||
var request: URLRequest
|
||||
public var request: URLRequest
|
||||
|
||||
init(shareID: Share.ShareID, nodeID: Link.LinkID, parameters: RenameNodeParameters, service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
|
||||
+17
-4
@@ -17,11 +17,16 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct RestoreLinkEndpoint: Endpoint {
|
||||
public struct RestoreLinkEndpoint: Endpoint {
|
||||
|
||||
public struct Response: Codable {
|
||||
let responses: [ResponseElement]
|
||||
let code: Int
|
||||
|
||||
public init(responses: [ResponseElement], code: Int) {
|
||||
self.responses = responses
|
||||
self.code = code
|
||||
}
|
||||
}
|
||||
|
||||
struct Parameters {
|
||||
@@ -42,7 +47,7 @@ struct RestoreLinkEndpoint: Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
var request: URLRequest
|
||||
public var request: URLRequest
|
||||
|
||||
init(parameters: Parameters, service: APIService, credential: ClientCredential) {
|
||||
var url = service.url(of: "/shares")
|
||||
@@ -66,12 +71,20 @@ struct RestoreLinkEndpoint: Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
struct ResponseElement: Codable {
|
||||
public typealias LinkCodesResponseElement = [LinkCodeResponseElement]
|
||||
public typealias LinkCodeResponseElement = ResponseElement
|
||||
|
||||
public struct ResponseElement: Codable {
|
||||
let linkID: String?
|
||||
let response: ResponseResponse?
|
||||
|
||||
public init(linkID: String?, response: ResponseResponse?) {
|
||||
self.linkID = linkID
|
||||
self.response = response
|
||||
}
|
||||
}
|
||||
|
||||
struct ResponseResponse: Codable {
|
||||
public struct ResponseResponse: Codable {
|
||||
let error, errorDescription: String?
|
||||
let code: Int
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct RestoreVolumeLinkEndpoint: Endpoint {
|
||||
|
||||
public struct Response: Codable {
|
||||
let responses: [ResponseElement]
|
||||
let code: Int
|
||||
|
||||
public init(responses: [ResponseElement], code: Int) {
|
||||
self.responses = responses
|
||||
self.code = code
|
||||
}
|
||||
}
|
||||
|
||||
struct Parameters {
|
||||
let volumeID: Volume.VolumeID
|
||||
let body: Body
|
||||
|
||||
init(volumeID: Volume.VolumeID, linkIDs: [Link.LinkID]) {
|
||||
self.volumeID = volumeID
|
||||
body = Body(linkIDs: linkIDs)
|
||||
}
|
||||
|
||||
struct Body: Encodable {
|
||||
let linkIDs: [Link.LinkID]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case linkIDs = "LinkIDs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
init(parameters: Parameters, service: APIService, credential: ClientCredential) {
|
||||
var url = service.url(of: "/v2/volumes")
|
||||
url.appendPathComponent(parameters.volumeID)
|
||||
url.appendPathComponent("/trash")
|
||||
url.appendPathComponent("/restore_multiple")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
let body = try? JSONEncoder().encode(parameters.body)
|
||||
assert(body != nil, "Failed body encoding")
|
||||
|
||||
request.httpBody = body
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Transfer photos from and to albums
|
||||
/// photos/volumes/{volumeID}/links/transfer-multiple
|
||||
public struct TransferMultipleEndpoint: Endpoint {
|
||||
public struct Parameters: Codable {
|
||||
public let parentLinkID: String
|
||||
public let links: [MoveMultipleEndpoint.Link]
|
||||
public let nameSignatureEmail: String
|
||||
public let signatureEmail: String?
|
||||
|
||||
public init(
|
||||
parentLinkID: String,
|
||||
links: [MoveMultipleEndpoint.Link],
|
||||
nameSignatureEmail: String,
|
||||
signatureEmail: String?
|
||||
) {
|
||||
self.parentLinkID = parentLinkID
|
||||
self.links = links
|
||||
self.nameSignatureEmail = nameSignatureEmail
|
||||
self.signatureEmail = signatureEmail
|
||||
}
|
||||
}
|
||||
|
||||
public struct Response: Codable {
|
||||
var code: Int
|
||||
|
||||
public init(code: Int) {
|
||||
self.code = code
|
||||
}
|
||||
}
|
||||
|
||||
public var request: URLRequest
|
||||
|
||||
public init(volumeID: Volume.VolumeID, parameters: Parameters, service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
var url = service.url(of: "photos/volumes")
|
||||
url.appendPathComponent(volumeID)
|
||||
url.appendPathComponent("/links")
|
||||
url.appendPathComponent("/transfer-multiple")
|
||||
|
||||
// request
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
|
||||
// headers
|
||||
var headers = service.baseHeaders
|
||||
headers.merge(service.authHeaders(credential), uniquingKeysWith: { $1 })
|
||||
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
request.httpBody = try? JSONEncoder(strategy: .capitalizeFirstLetter).encode(parameters)
|
||||
|
||||
self.request = request
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,10 @@ public struct UpdateRevisionBlocks: Codable {
|
||||
}
|
||||
|
||||
public struct UpdateRevisionParameters: Codable {
|
||||
let ManifestSignature: String
|
||||
let SignatureAddress: String
|
||||
let XAttr: String?
|
||||
let Photo: Photo?
|
||||
public let ManifestSignature: String
|
||||
public let SignatureAddress: String
|
||||
public let XAttr: String?
|
||||
public let Photo: Photo?
|
||||
|
||||
public init(
|
||||
manifestSignature: String,
|
||||
@@ -50,22 +50,28 @@ public struct UpdateRevisionParameters: Codable {
|
||||
public let MainPhotoLinkID: String?
|
||||
public let Exif: String?
|
||||
public let ContentHash: String
|
||||
public let Tags: [Int]
|
||||
|
||||
public init(captureTime: Int, mainPhotoLinkID: String?, exif: String?, contentHash: String) {
|
||||
public init(captureTime: Int, mainPhotoLinkID: String?, exif: String?, contentHash: String, tags: [Int] = []) {
|
||||
self.CaptureTime = captureTime
|
||||
self.MainPhotoLinkID = mainPhotoLinkID
|
||||
self.Exif = exif
|
||||
self.ContentHash = contentHash
|
||||
self.Tags = tags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UpdateRevisionEndpoint: Endpoint {
|
||||
public struct UpdateRevisionEndpoint: Endpoint {
|
||||
public struct Response: Codable {
|
||||
var code: Int
|
||||
|
||||
public init(code: Int) {
|
||||
self.code = code
|
||||
}
|
||||
}
|
||||
|
||||
var request: URLRequest
|
||||
public var request: URLRequest
|
||||
|
||||
init(shareID: Share.ShareID, fileID: Link.LinkID, revisionID: Revision.RevisionID, parameters: UpdateRevisionParameters, service: APIService, credential: ClientCredential) {
|
||||
// url
|
||||
|
||||
@@ -19,27 +19,35 @@ import Foundation
|
||||
// MARK: - List Photos Share
|
||||
public protocol PhotoShareListing {
|
||||
func getPhotosRoot() async throws -> PhotosRoot
|
||||
func getPhotosRoot(listing: ShareListing) async throws -> PhotosRoot
|
||||
func getActivePhotoShares() async throws -> [ShareListing]
|
||||
}
|
||||
|
||||
extension Client: PhotoShareListing {
|
||||
public func getPhotosRoot() async throws -> PhotosRoot {
|
||||
let response = try await listPhotoShares()
|
||||
async let share = try bootstrapPhotosShare(shareID: response.shareID)
|
||||
async let root = try bootstrapPhotosRoot(shareID: response.shareID, nodeID: response.linkID, breadcrumbs: .startCollecting())
|
||||
return try await getPhotosRoot(listing: response)
|
||||
}
|
||||
|
||||
public func getPhotosRoot(listing: ShareListing) async throws -> PhotosRoot {
|
||||
async let share = try bootstrapPhotosShare(shareID: listing.shareID)
|
||||
async let root = try bootstrapPhotosRoot(shareID: listing.shareID, nodeID: listing.linkID, breadcrumbs: .startCollecting())
|
||||
return try await PhotosRoot(link: root, share: share)
|
||||
}
|
||||
|
||||
public func listPhotoShares() async throws -> ListSharesEndpoint.Response.Share {
|
||||
public func listPhotoShares() async throws -> ShareListing {
|
||||
let shares = try await getActivePhotoShares()
|
||||
guard let share = shares.first else {
|
||||
throw NSError(domain: "No Photos Share found", code: 0)
|
||||
}
|
||||
return share
|
||||
}
|
||||
|
||||
public func getActivePhotoShares() async throws -> [ShareListing] {
|
||||
let parameters = ListSharesEndpoint.Parameters(shareType: .photos, showAll: .default)
|
||||
let endpoint = ListSharesEndpoint(parameters: parameters, service: service, credential: try credential())
|
||||
let response = try await request(endpoint)
|
||||
|
||||
guard let shareDevice = response.shares.first(where: { $0.state == .active && $0.locked != true }) else {
|
||||
throw NSError(domain: "No Photos Share found", code: 0)
|
||||
}
|
||||
|
||||
return shareDevice
|
||||
return response.shares.filter { $0.state == .active && $0.locked != true }
|
||||
}
|
||||
|
||||
func bootstrapPhotosShare(shareID: String) async throws -> Share {
|
||||
@@ -114,3 +122,37 @@ extension Client: PhotosDuplicatesRepository {
|
||||
return try await request(endpoint, completionExecutor: .asyncExecutor(dispatchQueue: backgroundQueue))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TagMigration
|
||||
public protocol TagsMigrationAPIClient {
|
||||
func getTagsMigrationState(volumeID: String) async throws -> TagsMigrationState
|
||||
func setTagsMigrationState(volumeID: String, request: TagsMigrationStateRequest) async throws
|
||||
}
|
||||
|
||||
extension Client: TagsMigrationAPIClient {
|
||||
public func getTagsMigrationState(volumeID: String) async throws -> TagsMigrationState {
|
||||
let endpoint = GetTagsMigrationStateEndpoint(volumeID: volumeID, service: service, credential: try credential())
|
||||
return try await request(endpoint).toDomain()
|
||||
}
|
||||
|
||||
public func setTagsMigrationState(volumeID: String, request requestBody: TagsMigrationStateRequest) async throws {
|
||||
let endpoint = SetTagsMigrationStateEndpoint(volumeID: volumeID, requestBody: requestBody, service: service, credential: try credential())
|
||||
_ = try await request(endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
extension GetTagsMigrationStateEndpoint.TagsMigrationStateResponse {
|
||||
func toDomain() -> TagsMigrationState {
|
||||
TagsMigrationState(
|
||||
isFinished: self.finished,
|
||||
anchor: anchor.map { response in
|
||||
TagsMigrationState.Anchor(
|
||||
lastProcessedLinkID: response.lastProcessedLinkID,
|
||||
lastProcessedCaptureTime: Date(timeIntervalSince1970: TimeInterval(response.lastProcessedCaptureTime)),
|
||||
lastMigrationTimestamp: Date(timeIntervalSince1970: TimeInterval(response.lastMigrationTimestamp)),
|
||||
lastClientUID: response.lastClientUID
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,4 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
public protocol PhotosListing {
|
||||
func getPhotosList(with parameters: PhotosListRequestParameters) async throws -> PhotosListResponse
|
||||
func getLinksMetadata(with parameters: LinksMetadataParameters) async throws -> LinksResponse
|
||||
}
|
||||
public protocol PhotosListing: PhotosListingDataSource, LinksMetadataDataSource { }
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct TagsMigrationState: Equatable {
|
||||
public let isFinished: Bool
|
||||
public let anchor: Anchor?
|
||||
|
||||
public init(isFinished: Bool, anchor: Anchor?) {
|
||||
self.isFinished = isFinished
|
||||
self.anchor = anchor
|
||||
}
|
||||
|
||||
public struct Anchor: Equatable {
|
||||
public let lastProcessedLinkID: String
|
||||
public let lastProcessedCaptureTime: Date
|
||||
public let lastMigrationTimestamp: Date
|
||||
public let lastClientUID: String?
|
||||
|
||||
public init(lastProcessedLinkID: String, lastProcessedCaptureTime: Date, lastMigrationTimestamp: Date, lastClientUID: String?) {
|
||||
self.lastProcessedLinkID = lastProcessedLinkID
|
||||
self.lastProcessedCaptureTime = lastProcessedCaptureTime
|
||||
self.lastMigrationTimestamp = lastMigrationTimestamp
|
||||
self.lastClientUID = lastClientUID
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class MockTagsMigrationAPIClient: TagsMigrationAPIClient {
|
||||
public var simulatedState: TagsMigrationState
|
||||
|
||||
public init(
|
||||
isFinished: Bool = false,
|
||||
anchor: TagsMigrationState.Anchor? = TagsMigrationState.Anchor(
|
||||
lastProcessedLinkID: "mock-link-id",
|
||||
lastProcessedCaptureTime: Date(timeIntervalSince1970: 1_680_000_000),
|
||||
lastMigrationTimestamp: Date(timeIntervalSince1970: 1_680_000_300),
|
||||
lastClientUID: "mock-client-uid"
|
||||
)
|
||||
) {
|
||||
self.simulatedState = TagsMigrationState(isFinished: isFinished, anchor: anchor)
|
||||
}
|
||||
|
||||
public func getTagsMigrationState(volumeID: String) async throws -> TagsMigrationState {
|
||||
return simulatedState
|
||||
}
|
||||
|
||||
public func setTagsMigrationState(volumeID: String, request: TagsMigrationStateRequest) async throws {
|
||||
simulatedState = TagsMigrationState(
|
||||
isFinished: request.finished,
|
||||
anchor: request.anchor.map {
|
||||
TagsMigrationState.Anchor(
|
||||
lastProcessedLinkID: $0.lastProcessedLinkID,
|
||||
lastProcessedCaptureTime: Date(timeIntervalSince1970: TimeInterval($0.lastProcessedCaptureTime)),
|
||||
lastMigrationTimestamp: Date(timeIntervalSince1970: TimeInterval($0.currentTimestamp)),
|
||||
lastClientUID: $0.clientUID
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,12 @@ public struct PhotosListRequestParameters {
|
||||
public let volumeId: String
|
||||
public let lastId: String?
|
||||
public let pageSize: Int
|
||||
public let tag: Int?
|
||||
|
||||
public init(volumeId: String, lastId: String?, pageSize: Int) {
|
||||
public init(volumeId: String, lastId: String?, pageSize: Int, tag: Int?) {
|
||||
self.volumeId = volumeId
|
||||
self.lastId = lastId
|
||||
self.pageSize = pageSize
|
||||
self.tag = tag
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,22 +23,37 @@ public struct PhotosListResponse: Codable, Equatable {
|
||||
public struct Photo: Codable, Equatable {
|
||||
public let linkID: String
|
||||
public let captureTime: Int
|
||||
public let relatedPhotos: [RelatedPhoto]
|
||||
// public let nameHash: String // TODO: next MR, BE not ready
|
||||
/// nameHash
|
||||
public let hash: String
|
||||
public let contentHash: String
|
||||
// TODO:album remove optional when backend ready
|
||||
public let tags: [Int]?
|
||||
|
||||
public init(linkID: String, captureTime: Int, relatedPhotos: [RelatedPhoto] = []) {
|
||||
public let relatedPhotos: [Photo]? // Optional to allow using the same type for related photos (which don't have it)
|
||||
public let addedTime: Date?
|
||||
|
||||
public init(
|
||||
linkID: String,
|
||||
captureTime: Int,
|
||||
addedTime: Date?,
|
||||
hash: String,
|
||||
contentHash: String,
|
||||
relatedPhotos: [Photo] = [],
|
||||
tags: [Int]? = nil
|
||||
) {
|
||||
self.linkID = linkID
|
||||
self.captureTime = captureTime
|
||||
self.addedTime = addedTime
|
||||
self.hash = hash
|
||||
self.contentHash = contentHash
|
||||
self.relatedPhotos = relatedPhotos
|
||||
self.tags = tags
|
||||
}
|
||||
}
|
||||
|
||||
public struct RelatedPhoto: Codable, Equatable {
|
||||
public let linkID: String
|
||||
public let captureTime: Int
|
||||
}
|
||||
|
||||
public init(photos: [Photo]) {
|
||||
self.photos = photos
|
||||
}
|
||||
}
|
||||
|
||||
public typealias RemotePhotoListing = PhotosListResponse.Photo
|
||||
|
||||
+15
-7
@@ -21,7 +21,12 @@ public struct DocumentIdentifier: Codable {
|
||||
public let revisionID: String
|
||||
}
|
||||
|
||||
public struct NewDocumentPayload: Codable, Equatable {
|
||||
public struct NewProtonFilePayload: Codable, Equatable {
|
||||
public enum DocumentType: Int, Codable {
|
||||
case document = 1
|
||||
case sheet = 2
|
||||
}
|
||||
|
||||
let name: String
|
||||
let hash: String
|
||||
let parentLinkID: String
|
||||
@@ -32,6 +37,7 @@ public struct NewDocumentPayload: Codable, Equatable {
|
||||
let contentKeyPacket: String
|
||||
let contentKeyPacketSignature: String
|
||||
let manifestSignature: String
|
||||
let documentType: DocumentType
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
@@ -43,7 +49,8 @@ public struct NewDocumentPayload: Codable, Equatable {
|
||||
signatureAddress: String,
|
||||
contentKeyPacket: String,
|
||||
contentKeyPacketSignature: String,
|
||||
manifestSignature: String
|
||||
manifestSignature: String,
|
||||
documentType: DocumentType
|
||||
) {
|
||||
self.name = name
|
||||
self.hash = hash
|
||||
@@ -55,19 +62,20 @@ public struct NewDocumentPayload: Codable, Equatable {
|
||||
self.contentKeyPacket = contentKeyPacket
|
||||
self.contentKeyPacketSignature = contentKeyPacketSignature
|
||||
self.manifestSignature = manifestSignature
|
||||
self.documentType = documentType
|
||||
}
|
||||
}
|
||||
|
||||
public struct NewDocumentParameters {
|
||||
public struct NewProtonFileParameters {
|
||||
let shareId: String
|
||||
let payload: NewDocumentPayload
|
||||
let payload: NewProtonFilePayload
|
||||
|
||||
public init(shareId: String, payload: NewDocumentPayload) {
|
||||
public init(shareId: String, payload: NewProtonFilePayload) {
|
||||
self.shareId = shareId
|
||||
self.payload = payload
|
||||
}
|
||||
}
|
||||
|
||||
public protocol NewDocumentRepository {
|
||||
func create(with parameters: NewDocumentParameters) async throws -> DocumentIdentifier
|
||||
public protocol NewProtonFileRepository {
|
||||
func create(with parameters: NewProtonFileParameters) async throws -> DocumentIdentifier
|
||||
}
|
||||
+3
-3
@@ -17,9 +17,9 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Client: NewDocumentRepository {
|
||||
public func create(with parameters: NewDocumentParameters) async throws -> DocumentIdentifier {
|
||||
let endpoint = NewDocumentEndpoint(parameters: parameters, service: service, credential: try credential())
|
||||
extension Client: NewProtonFileRepository {
|
||||
public func create(with parameters: NewProtonFileParameters) async throws -> DocumentIdentifier {
|
||||
let endpoint = NewProtonFileEndpoint(parameters: parameters, service: service, credential: try credential())
|
||||
return try await request(endpoint, completionExecutor: .asyncExecutor(dispatchQueue: backgroundQueue)).document
|
||||
}
|
||||
}
|
||||
+15
-14
@@ -27,15 +27,6 @@
|
||||
"version" : "1.8.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "ellipticcurvekeypair",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/agens-no/EllipticCurveKeyPair",
|
||||
"state" : {
|
||||
"revision" : "944ae5c89ca045e9f1a113b736706c73fc51d1c2",
|
||||
"version" : "2.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "lottie-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -59,8 +50,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ProtonMail/protoncore_ios.git",
|
||||
"state" : {
|
||||
"revision" : "b4b7037f9b0d4da80b3b9dec8ced5af4139fee3b",
|
||||
"version" : "25.0.2"
|
||||
"revision" : "d72ee3f8ec940f15374f4d98e3e2ece5340ac419",
|
||||
"version" : "32.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -86,8 +77,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/getsentry/sentry-cocoa.git",
|
||||
"state" : {
|
||||
"revision" : "3b9a8e69ca296bd8cd0e317ad7a448e5daf4a342",
|
||||
"version" : "8.18.0"
|
||||
"revision" : "930b78a63f47549c81e6e63c9172584f7d3dfdd6",
|
||||
"version" : "8.52.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -131,7 +122,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ProtonMail/TrustKit.git",
|
||||
"state" : {
|
||||
"revision" : "d107d7cc825f38ae2d6dc7c54af71d58145c3506"
|
||||
"revision" : "d107d7cc825f38ae2d6dc7c54af71d58145c3506",
|
||||
"version" : "1.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -142,6 +134,15 @@
|
||||
"revision" : "e5a609875de09a5fcea3231b9a7f4c181b1b427b",
|
||||
"version" : "1.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "yams",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/jpsim/Yams.git",
|
||||
"state" : {
|
||||
"revision" : "3d6871d5b4a5cd519adf233fbb576e0a2af71c17",
|
||||
"version" : "5.4.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
|
||||
@@ -6,17 +6,15 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "PDClient",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.iOS(.v16),
|
||||
.macOS(.v13),
|
||||
],
|
||||
products: [
|
||||
.library(name: "PDClient", targets: ["PDClient"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "PDLoadTesting", path: "../PDLoadTesting"),
|
||||
|
||||
// exact version is defined by protoncore_ios
|
||||
.package(url: "https://github.com/ProtonMail/protoncore_ios.git", .suitable),
|
||||
.package(url: "https://github.com/ProtonMail/protoncore_ios.git", exact: "32.7.1"),
|
||||
.package(url: "https://github.com/ProtonMail/apple-fusion.git", .suitable),
|
||||
.package(url: "https://github.com/getsentry/sentry-cocoa.git", .suitable),
|
||||
.package(url: "https://github.com/Unleash/unleash-proxy-client-swift.git", .suitable),
|
||||
@@ -25,11 +23,10 @@ let package = Package(
|
||||
.target(
|
||||
name: "PDClient",
|
||||
dependencies: [
|
||||
.product(name: "PDLoadTesting", package: "PDLoadTesting"),
|
||||
|
||||
.product(name: "ProtonCoreUtilities", package: "protoncore_ios"),
|
||||
.product(name: "ProtonCoreEnvironment", package: "protoncore_ios"),
|
||||
.product(name: "ProtonCoreNetworking", package: "protoncore_ios"),
|
||||
.product(name: "ProtonCoreServices", package: "protoncore_ios"),
|
||||
.product(name: "ProtonCoreUtilities", package: "protoncore_ios"),
|
||||
|
||||
.product(name: "Sentry", package: "sentry-cocoa"),
|
||||
.product(name: "UnleashProxyClientSwift", package: "unleash-proxy-client-swift"),
|
||||
|
||||
+2
-8
@@ -2,13 +2,7 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>PDCore-Package.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>7</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "alamofire",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/Alamofire",
|
||||
"state" : {
|
||||
"revision" : "d120af1e8638c7da36c8481fd61a66c0c08dc4fc",
|
||||
"version" : "5.4.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "apple-fusion",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ProtonMail/apple-fusion.git",
|
||||
"state" : {
|
||||
"revision" : "4df5ec5ce3605e9c364803c28cd06730fdb7fac4",
|
||||
"version" : "2.1.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cryptoswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/krzyzanowskim/CryptoSwift",
|
||||
"state" : {
|
||||
"revision" : "729e01bc9b9dab466ac85f21fb9ee2bc1c61b258",
|
||||
"version" : "1.8.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "lottie-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/airbnb/lottie-ios",
|
||||
"state" : {
|
||||
"revision" : "45517c3cfec9469bbdd4f86e32393c28ae9df0bc",
|
||||
"version" : "4.3.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "ohhttpstubs",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/AliSoftware/OHHTTPStubs",
|
||||
"state" : {
|
||||
"revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9",
|
||||
"version" : "9.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "protoncore_ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ProtonMail/protoncore_ios.git",
|
||||
"state" : {
|
||||
"revision" : "d72ee3f8ec940f15374f4d98e3e2ece5340ac419",
|
||||
"version" : "32.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "reachability.swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ashleymills/Reachability.swift",
|
||||
"state" : {
|
||||
"revision" : "21d1dc412cfecbe6e34f1f4c4eb88d3f912654a6",
|
||||
"version" : "5.2.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state" : {
|
||||
"revision" : "7f9fb5d43ecd4aa714c00746f54873f354403438",
|
||||
"version" : "5.15.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sentry-cocoa",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/getsentry/sentry-cocoa.git",
|
||||
"state" : {
|
||||
"revision" : "930b78a63f47549c81e6e63c9172584f7d3dfdd6",
|
||||
"version" : "8.52.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-custom-dump",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-custom-dump",
|
||||
"state" : {
|
||||
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
|
||||
"version" : "1.3.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-snapshot-testing",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
||||
"state" : {
|
||||
"revision" : "37230a37e83f1b7023be08e1b1a2603fcb1567fb",
|
||||
"version" : "1.18.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-syntax",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||
"state" : {
|
||||
"revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2",
|
||||
"version" : "601.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftotp",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/lachlanbell/SwiftOTP",
|
||||
"state" : {
|
||||
"revision" : "93d4942c90dd498580988ffb9971e2ddf91d5a28",
|
||||
"version" : "2.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "trustkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ProtonMail/TrustKit",
|
||||
"state" : {
|
||||
"revision" : "d107d7cc825f38ae2d6dc7c54af71d58145c3506",
|
||||
"version" : "1.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "xctest-dynamic-overlay",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||
"state" : {
|
||||
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
|
||||
"version" : "1.5.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "yams",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/jpsim/Yams.git",
|
||||
"state" : {
|
||||
"revision" : "3d6871d5b4a5cd519adf233fbb576e0a2af71c17",
|
||||
"version" : "5.4.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// swift-tools-version: 5.9
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "PDContacts",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.macOS(.v13)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||
.library(name: "PDContacts", targets: ["PDContacts"]),
|
||||
],
|
||||
dependencies: [
|
||||
// exact version is defined by PDClient
|
||||
.package(url: "https://github.com/ProtonMail/protoncore_ios.git", exact: "32.7.1"),
|
||||
.package(url: "https://github.com/ProtonMail/apple-fusion.git", .suitable),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||
// Targets can depend on other targets in this package and products from dependencies.
|
||||
.target(
|
||||
name: "PDContacts",
|
||||
dependencies: [
|
||||
.product(name: "ProtonCoreNetworking", package: "protoncore_ios"),
|
||||
.product(name: "ProtonCoreServices", package: "protoncore_ios")
|
||||
],
|
||||
path: "Sources"
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
extension Range where Bound == Version {
|
||||
static let suitable = Self(uncheckedBounds: ("0.0.0", "99.0.0"))
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import ProtonCoreNetworking
|
||||
import ProtonCoreServices
|
||||
|
||||
public protocol ContactsManagerProtocol {
|
||||
var contactUpdatedNotifier: AnyPublisher<Void, Never> { get }
|
||||
|
||||
func fetchIntegralContacts() async throws -> ([Contact], [ContactGroup])
|
||||
func fetchUserContacts() async throws -> [Contact]
|
||||
func fetchUserContactGroups() async throws -> [ContactGroup]
|
||||
func fetchActivePublicKeys(email: String, internalOnly: Bool) async throws -> PublicKeyResponse
|
||||
func create(contact: Contact, with emails: [ContactEmail])
|
||||
func delete(contactID: String)
|
||||
func update(contact: Contact, with emails: [ContactEmail])
|
||||
func delete(groupID: String)
|
||||
func create(group: ContactGroup)
|
||||
func update(group: ContactGroup)
|
||||
}
|
||||
|
||||
public final class ContactsManager: ContactsManagerProtocol {
|
||||
|
||||
private let jsonDecoder: JSONDecoder = JSONDecoder()
|
||||
private let service: APIService
|
||||
private var log: ((String) -> Void)?
|
||||
private var error: ((String) -> Void)?
|
||||
private var contacts: [Contact] = []
|
||||
private var contactGroups: [ContactGroup] = []
|
||||
private var isContactInitialized = false
|
||||
private var isContactGroupInitialized = false
|
||||
private var keyCache: [KeyQuery: PublicKeyResponse] = [:]
|
||||
private let contactUpdateSubject = PassthroughSubject<Void, Never>()
|
||||
public var contactUpdatedNotifier: AnyPublisher<Void, Never> { contactUpdateSubject.eraseToAnyPublisher() }
|
||||
|
||||
public init(service: APIService, log: ((String) -> Void)?, error: ((String) -> Void)?) {
|
||||
self.service = service
|
||||
self.log = log
|
||||
self.error = error
|
||||
}
|
||||
|
||||
/// Retrieve user integral contacts:
|
||||
/// if they are available in the in-memory cache, return them from there;
|
||||
/// otherwise, query the backend.
|
||||
/// - Returns: (user contacts, user contact groups)
|
||||
public func fetchIntegralContacts() async throws -> ([Contact], [ContactGroup]) {
|
||||
let contacts = try await fetchUserContacts()
|
||||
let contactGroups = try await fetchUserContactGroups()
|
||||
return (contacts, contactGroups)
|
||||
}
|
||||
|
||||
/// Retrieve user contacts:
|
||||
/// if they are available in the in-memory cache, return them from there;
|
||||
/// otherwise, query the backend.
|
||||
/// - Returns: user contacts
|
||||
public func fetchUserContacts() async throws -> [Contact] {
|
||||
if isContactInitialized { return contacts }
|
||||
|
||||
async let contactsAsync = fetchAllContacts()
|
||||
async let emailsAsync = fetchAllEmails()
|
||||
let (contactsRes, emailsRes) = await (contactsAsync, emailsAsync)
|
||||
switch (contactsRes, emailsRes) {
|
||||
case (.success(let contacts), .success(let emails)):
|
||||
self.contacts = map(contacts: contacts, contactEmails: emails)
|
||||
isContactInitialized = true
|
||||
log?("Successfully retrieved user contacts; total number of contacts: \(contacts.count)")
|
||||
return self.contacts
|
||||
case (.failure(let error), _):
|
||||
throw error
|
||||
case (_, .failure(let error)):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve user contact group:
|
||||
/// if they are available in the in-memory cache, return them from there;
|
||||
/// otherwise, query the backend.
|
||||
/// - Returns: user contacts
|
||||
public func fetchUserContactGroups() async throws -> [ContactGroup] {
|
||||
defer {
|
||||
log?("Successfully retrieved user contact groups; total number of groups: \(contactGroups.count)")
|
||||
}
|
||||
if isContactGroupInitialized { return contactGroups }
|
||||
let contactGroups = try await fetchContactGroupLabel().labels
|
||||
let contacts = try await fetchUserContacts()
|
||||
self.contactGroups = map(contactGroups: contactGroups, contacts: contacts)
|
||||
isContactGroupInitialized = true
|
||||
return self.contactGroups
|
||||
}
|
||||
|
||||
/// - Parameters:
|
||||
/// - email: Mail address, e.g. tester@pm.me
|
||||
/// - internalOnly: If true, it will not perform any external lookup, and only provide information from the Proton DB
|
||||
public func fetchActivePublicKeys(email: String, internalOnly: Bool = true) async throws -> PublicKeyResponse {
|
||||
let query = KeyQuery(email: email, internalOnly: internalOnly)
|
||||
if let cache = keyCache[query] {
|
||||
return cache
|
||||
}
|
||||
|
||||
let request = PublicKeyRequest(email: email, internalOnly: internalOnly)
|
||||
log(request: request)
|
||||
do {
|
||||
let response = try await service.perform(request: request)
|
||||
let jsonDict = response.1
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: jsonDict)
|
||||
let res = try jsonDecoder.decode(PublicKeyResponse.self, from: jsonData)
|
||||
keyCache[query] = res
|
||||
return res
|
||||
} catch {
|
||||
log(failedRequest: request, error: error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public func delete(contactID: String) {
|
||||
contacts.removeAll(where: { $0.id == contactID })
|
||||
for var group in contactGroups {
|
||||
group.delete(contactID: contactID)
|
||||
}
|
||||
contactUpdateSubject.send()
|
||||
}
|
||||
|
||||
public func create(contact: Contact, with emails: [ContactEmail]) {
|
||||
guard !contacts.map(\.id).contains(contact.id) else { return }
|
||||
var contact = contact
|
||||
for email in emails {
|
||||
contact.append(contactEmail: email)
|
||||
}
|
||||
contacts.append(contact)
|
||||
contactUpdateSubject.send()
|
||||
}
|
||||
|
||||
public func update(contact: Contact, with emails: [ContactEmail]) {
|
||||
var contact = contact
|
||||
for email in emails {
|
||||
contact.append(contactEmail: email)
|
||||
}
|
||||
if let index = contacts.firstIndex(where: { $0.id == contact.id }) {
|
||||
contacts[index] = contact
|
||||
} else {
|
||||
contacts.append(contact)
|
||||
}
|
||||
for index in contactGroups.indices {
|
||||
if contact.labelIDs.contains(contactGroups[index].id) {
|
||||
contactGroups[index].updateOrInsert(contact: contact)
|
||||
} else {
|
||||
contactGroups[index].delete(contactID: contact.id)
|
||||
}
|
||||
}
|
||||
contactUpdateSubject.send()
|
||||
}
|
||||
|
||||
public func delete(groupID: String) {
|
||||
contactGroups.removeAll(where: { $0.id == groupID })
|
||||
for index in contacts.indices {
|
||||
contacts[index].remove(labelID: groupID)
|
||||
}
|
||||
contactUpdateSubject.send()
|
||||
}
|
||||
|
||||
public func create(group: ContactGroup) {
|
||||
guard !contactGroups.map(\.id).contains(group.id) else { return }
|
||||
contactGroups.append(group)
|
||||
contactUpdateSubject.send()
|
||||
}
|
||||
|
||||
public func update(group: ContactGroup) {
|
||||
if let existingGroup = contactGroups.first(where: { $0.id == group.id }) {
|
||||
let contacts = existingGroup.contacts
|
||||
var group = group
|
||||
group.append(contentsOf: contacts)
|
||||
guard let index = contactGroups.firstIndex(where: { $0.id == group.id }) else { return }
|
||||
contactGroups[index] = group
|
||||
} else {
|
||||
create(group: group)
|
||||
}
|
||||
contactUpdateSubject.send()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch data
|
||||
extension ContactsManager {
|
||||
private func fetchAllContacts() async -> Result<[Contact], Error> {
|
||||
var contacts: [Contact] = []
|
||||
var page = 0
|
||||
do {
|
||||
while true {
|
||||
let response = try await fetchContacts(page: page)
|
||||
contacts.append(contentsOf: response.contacts)
|
||||
if contacts.count == response.total {
|
||||
break
|
||||
} else {
|
||||
page += 1
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
return .success(contacts)
|
||||
}
|
||||
|
||||
private func fetchContacts(page: Int) async throws -> ContactResponse {
|
||||
let request = ContactRequest(page: page)
|
||||
return try await perform(request: request)
|
||||
}
|
||||
|
||||
private func fetchAllEmails() async -> Result<[ContactEmail], Error> {
|
||||
var emails: [ContactEmail] = []
|
||||
var page = 0
|
||||
do {
|
||||
while true {
|
||||
let response = try await fetchEmails(page: page)
|
||||
emails.append(contentsOf: response.contactEmails)
|
||||
if emails.count == response.total {
|
||||
break
|
||||
} else {
|
||||
page += 1
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
return .success(emails)
|
||||
}
|
||||
|
||||
private func fetchEmails(page: Int) async throws -> EmailResponse {
|
||||
let request = EmailRequest(page: page)
|
||||
return try await perform(request: request)
|
||||
}
|
||||
|
||||
private func fetchContactGroupLabel() async throws -> GroupLabelResponse {
|
||||
let request = GroupLabelRequest()
|
||||
return try await perform(request: request)
|
||||
}
|
||||
|
||||
private func perform<T: Decodable>(request: Request) async throws -> T {
|
||||
log(request: request)
|
||||
do {
|
||||
let jsonDict = try await service.perform(request: request).1
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: jsonDict)
|
||||
let res = try jsonDecoder.decode(T.self, from: jsonData)
|
||||
return res
|
||||
} catch {
|
||||
log(failedRequest: request, error: error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private func map(contacts: [Contact], contactEmails: [ContactEmail]) -> [Contact] {
|
||||
var contacts = contacts
|
||||
for email in contactEmails {
|
||||
guard let contactIdx = contacts.firstIndex(where: { $0.id == email.contactID }) else { continue }
|
||||
contacts[contactIdx].append(contactEmail: email)
|
||||
}
|
||||
return contacts
|
||||
}
|
||||
|
||||
private func map(contactGroups: [ContactGroup], contacts: [Contact]) -> [ContactGroup] {
|
||||
var contactGroups = contactGroups
|
||||
for idx in 0..<contactGroups.count {
|
||||
let id = contactGroups[idx].id
|
||||
let contact = contacts.filter { $0.labelIDs.contains(id) }
|
||||
contactGroups[idx].append(contentsOf: contact)
|
||||
}
|
||||
return contactGroups
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Log
|
||||
extension ContactsManager {
|
||||
private func log(request: Request) {
|
||||
let logStr = "REQUEST: 🌐🌐🌐🌐 \(request.method.rawValue) - \(request.self)"
|
||||
log?(logStr)
|
||||
}
|
||||
|
||||
private func log(failedRequest: Request, error: Error) {
|
||||
let responseHeader = "RESPONSE: 📩📩📩📩 \(failedRequest.method.rawValue) - \(failedRequest.self)"
|
||||
let desc = """
|
||||
\(responseHeader)
|
||||
++++++++++++++++++++++++++++++++
|
||||
|- Error ❌: \(error.localizedDescription)
|
||||
--------------------------------
|
||||
"""
|
||||
|
||||
self.error?(desc)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import ProtonCoreNetworking
|
||||
|
||||
struct ContactRequest: Request {
|
||||
let endpoint: String = "/contacts/v4/contacts"
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
|
||||
var path: String {
|
||||
"\(endpoint)?Page=\(page)&PageSize=\(pageSize)"
|
||||
}
|
||||
|
||||
init(page: Int = 0, pageSize: Int = 1000) {
|
||||
self.page = page
|
||||
self.pageSize = pageSize
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ContactResponse: Codable {
|
||||
let code: Int
|
||||
let contacts: [Contact]
|
||||
let total: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case code = "Code", contacts = "Contacts", total = "Total"
|
||||
}
|
||||
|
||||
init(code: Int, contacts: [Contact], total: Int) {
|
||||
self.code = code
|
||||
self.contacts = contacts
|
||||
self.total = total
|
||||
}
|
||||
}
|
||||
|
||||
public struct Contact: Codable, Equatable {
|
||||
/// Encrypted contact ID
|
||||
public let id: String
|
||||
public let name: String
|
||||
public let uid: String
|
||||
public let createTime: Date
|
||||
public let modifyTime: Date
|
||||
public private(set) var labelIDs: [String]
|
||||
public private(set) var contactEmails: [ContactEmail] = []
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id = "ID"
|
||||
case name = "Name"
|
||||
case uid = "UID"
|
||||
case createTime = "CreateTime"
|
||||
case modifyTime = "ModifyTime"
|
||||
case labelIDs = "LabelIDs"
|
||||
}
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
name: String,
|
||||
uid: String,
|
||||
createTime: Date,
|
||||
modifyTime: Date,
|
||||
labelIDs: [String]
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.uid = uid
|
||||
self.createTime = createTime
|
||||
self.modifyTime = modifyTime
|
||||
self.labelIDs = labelIDs
|
||||
}
|
||||
|
||||
public mutating func append(contactEmail: ContactEmail) {
|
||||
if contactEmails.map(\.email).contains(contactEmail.email) { return }
|
||||
contactEmails.append(contactEmail)
|
||||
contactEmails.sort(by: { $0.lastUsedTime >= $1.lastUsedTime })
|
||||
}
|
||||
|
||||
mutating func add(labelID: String) {
|
||||
labelIDs.append(labelID)
|
||||
}
|
||||
|
||||
mutating func remove(labelID: String) {
|
||||
labelIDs.removeAll(where: { $0 == labelID })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import ProtonCoreNetworking
|
||||
|
||||
struct EmailRequest: Request {
|
||||
let endpoint: String = "/contacts/v4/contacts/emails"
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
|
||||
var path: String {
|
||||
"\(endpoint)?Page=\(page)&PageSize=\(pageSize)"
|
||||
}
|
||||
|
||||
init(page: Int = 0, pageSize: Int = 1000) {
|
||||
self.page = page
|
||||
self.pageSize = pageSize
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EmailResponse: Codable {
|
||||
let code: Int
|
||||
let contactEmails: [ContactEmail]
|
||||
let total: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case code = "Code", contactEmails = "ContactEmails", total = "Total"
|
||||
}
|
||||
|
||||
init(code: Int, contactEmails: [ContactEmail], total: Int) {
|
||||
self.code = code
|
||||
self.contactEmails = contactEmails
|
||||
self.total = total
|
||||
}
|
||||
}
|
||||
|
||||
public struct ContactEmail: Codable, Equatable {
|
||||
public let contactID: String
|
||||
public let email: String
|
||||
/// false if contact contains custom sending preferences or keys
|
||||
/// true otherwise
|
||||
public let defaults: Bool
|
||||
public let order: Int
|
||||
/// Tells whether this is an official Proton address
|
||||
public let isProton: Bool
|
||||
/// 2001-01-01 00:00:00 +0000 aka timeIntervalSinceReferenceDate : 0.0
|
||||
/// This indicates that the address is never utilized.
|
||||
public let lastUsedTime: Date
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case contactID = "ContactID"
|
||||
case email = "Email"
|
||||
case defaults = "Defaults"
|
||||
case order = "Order"
|
||||
case isProton = "IsProton"
|
||||
case lastUsedTime = "LastUsedTime"
|
||||
}
|
||||
|
||||
public init(contactID: String, email: String, defaults: Bool, order: Int, isProton: Bool, lastUsedTime: Date) {
|
||||
self.contactID = contactID
|
||||
self.email = email
|
||||
self.defaults = defaults
|
||||
self.order = order
|
||||
self.isProton = isProton
|
||||
self.lastUsedTime = lastUsedTime
|
||||
}
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.contactID = try container.decode(String.self, forKey: .contactID)
|
||||
self.email = try container.decode(String.self, forKey: .email)
|
||||
let defaultsValue = try container.decode(Int.self, forKey: .defaults)
|
||||
self.defaults = defaultsValue == 1 ? true : false
|
||||
self.order = try container.decode(Int.self, forKey: .order)
|
||||
let isProtonValue = try container.decode(Int.self, forKey: .isProton)
|
||||
self.isProton = isProtonValue == 1 ? true : false
|
||||
self.lastUsedTime = try container.decode(Date.self, forKey: .lastUsedTime)
|
||||
}
|
||||
|
||||
public func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(contactID, forKey: .contactID)
|
||||
try container.encode(email, forKey: .email)
|
||||
try container.encode(defaults ? 1 : 0, forKey: .defaults)
|
||||
try container.encode(order, forKey: .order)
|
||||
try container.encode(isProton ? 1 : 0, forKey: .isProton)
|
||||
try container.encode(lastUsedTime, forKey: .lastUsedTime)
|
||||
}
|
||||
}
|
||||
+9
-4
@@ -15,8 +15,13 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
public struct ProtonDocumentConstants {
|
||||
public static let fileExtension = "protondoc"
|
||||
public static let uti = "me.proton.drive.doc"
|
||||
public static let mimeType = "application/vnd.proton.doc"
|
||||
import ProtonCoreNetworking
|
||||
|
||||
struct GroupLabelRequest: Request {
|
||||
let endpoint: String = "/core/v4/labels"
|
||||
let type: Int = 2
|
||||
|
||||
var path: String {
|
||||
"\(endpoint)?Type=\(type)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GroupLabelResponse: Codable {
|
||||
let code: Int
|
||||
let labels: [ContactGroup]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case code = "Code", labels = "Labels"
|
||||
}
|
||||
|
||||
init(code: Int, labels: [ContactGroup]) {
|
||||
self.code = code
|
||||
self.labels = labels
|
||||
}
|
||||
}
|
||||
|
||||
public struct ContactGroup: Codable {
|
||||
public let id: String
|
||||
public let name: String
|
||||
public let order: Int
|
||||
/// HEX color code, e.g. #3CBB3A
|
||||
public let color: String
|
||||
public private(set) var contacts: [Contact] = []
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id = "ID", name = "Name", order = "Order", color = "Color"
|
||||
}
|
||||
|
||||
public init(id: String, name: String, order: Int, color: String) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.order = order
|
||||
self.color = color
|
||||
}
|
||||
|
||||
public mutating func append(contentsOf contacts: [Contact]) {
|
||||
self.contacts.append(contentsOf: contacts)
|
||||
}
|
||||
|
||||
mutating func delete(contactID: String) {
|
||||
contacts.removeAll(where: { $0.id == contactID })
|
||||
}
|
||||
|
||||
mutating func updateOrInsert(contact: Contact) {
|
||||
if let index = contacts.firstIndex(where: { $0.id == contact.id }) {
|
||||
contacts[index] = contact
|
||||
} else {
|
||||
contacts.append(contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import ProtonCoreNetworking
|
||||
|
||||
struct PublicKeyRequest: Request {
|
||||
let endpoint = "/core/v4/keys/all"
|
||||
let email: String
|
||||
let internalOnly: Int
|
||||
|
||||
var path: String {
|
||||
"\(endpoint)?Email=\(email)&InternalOnly=\(internalOnly)"
|
||||
}
|
||||
|
||||
init(email: String, internalOnly: Bool) {
|
||||
self.email = email
|
||||
self.internalOnly = internalOnly ? 1 : 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct PublicKeyResponse: Decodable {
|
||||
public let code: Int
|
||||
public let isProton: Bool
|
||||
/// True when domain has valid proton MX
|
||||
public let protonMX: Bool
|
||||
/// List of warnings to show to the user related to phishing and message routing
|
||||
public let warnings: [String]
|
||||
public let address: Address
|
||||
public let unverified: Address?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case Code, IsProton, ProtonMX, Warnings, Address, Unverified
|
||||
}
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.code = try container.decode(Int.self, forKey: .Code)
|
||||
let isProtonValue = try container.decode(Int.self, forKey: .IsProton)
|
||||
self.isProton = isProtonValue == 1 ? true : false
|
||||
self.protonMX = try container.decode(Bool.self, forKey: .ProtonMX)
|
||||
self.warnings = try container.decode([String].self, forKey: .Warnings)
|
||||
self.address = try container.decode(Address.self, forKey: .Address)
|
||||
self.unverified = try container.decodeIfPresent(Address.self, forKey: .Unverified)
|
||||
}
|
||||
|
||||
public init(
|
||||
code: Int,
|
||||
isProton: Bool,
|
||||
protonMX: Bool,
|
||||
warnings: [String],
|
||||
address: Address,
|
||||
unverified: Address? = nil
|
||||
) {
|
||||
self.code = code
|
||||
self.isProton = isProton
|
||||
self.protonMX = protonMX
|
||||
self.warnings = warnings
|
||||
self.address = address
|
||||
self.unverified = unverified
|
||||
}
|
||||
}
|
||||
|
||||
public struct Address: Decodable {
|
||||
public let keys: [Key]
|
||||
// SignedKeyList is not implemented
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case keys = "Keys"
|
||||
}
|
||||
|
||||
public init(keys: [Key]) {
|
||||
self.keys = keys
|
||||
}
|
||||
}
|
||||
|
||||
public struct Key: Decodable {
|
||||
/// Key usage flags
|
||||
public let flags: Flags
|
||||
/// Armored OpenPGP public key
|
||||
public let publicKey: String
|
||||
/// Always (0) internal for verified keys
|
||||
public let source: Int
|
||||
|
||||
public struct Flags: OptionSet, Decodable {
|
||||
public let rawValue: Int
|
||||
|
||||
/// 2^0 = 1 means the key is not compromised (i.e. if we can trust signatures coming from it)
|
||||
public static let notCompromised = Self(rawValue: 1 << 0)
|
||||
|
||||
/// 2^1 = 2 means the key is still in use (i.e. not obsolete, we can encrypt messages to it)
|
||||
public static let notObsolete = Self(rawValue: 2 << 0)
|
||||
|
||||
public init(rawValue: Int) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case flags = "Flags", publicKey = "PublicKey", source = "Source"
|
||||
}
|
||||
|
||||
public init(flags: Flags, publicKey: String, source: Int) {
|
||||
self.flags = flags
|
||||
self.publicKey = publicKey
|
||||
self.source = source
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyQuery: Hashable {
|
||||
let email: String
|
||||
let internalOnly: Bool
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(email)
|
||||
hasher.combine(internalOnly)
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -15,8 +15,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import ProtonCoreLoginUI
|
||||
|
||||
public enum DriveCoreAlert: Equatable {
|
||||
case logout
|
||||
case trustKitFailure
|
||||
@@ -45,7 +43,7 @@ public enum DriveCoreAlert: Equatable {
|
||||
public var message: String {
|
||||
switch self {
|
||||
case .logout:
|
||||
return LUITranslation.info_session_expired.l10n
|
||||
return "Your session has expired. Please log in again."
|
||||
case .trustKitFailure:
|
||||
return "TLS certificate validation failed. Your connection may be monitored and the app is temporarily blocked for your safety.\n\nswitch networks immediately"
|
||||
case .trustKitHardFailure:
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Combine
|
||||
|
||||
// Object that holds population state of the user. Is the source of truth for start app flow.
|
||||
// Just after sign in it will be `unpopulated` and become `populated` as soon as roots are fetched.
|
||||
public protocol PopulatedStateControllerProtocol {
|
||||
var state: AnyPublisher<PopulatedState, Never> { get }
|
||||
func setState(_ state: PopulatedState)
|
||||
}
|
||||
|
||||
public final class PopulatedStateController: PopulatedStateControllerProtocol {
|
||||
private let subject = CurrentValueSubject<PopulatedState, Never>(.unpopulated)
|
||||
|
||||
public init() { }
|
||||
|
||||
public var state: AnyPublisher<PopulatedState, Never> {
|
||||
subject
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public func setState(_ state: PopulatedState) {
|
||||
subject.send(state)
|
||||
}
|
||||
}
|
||||
|
||||
public enum PopulatedState: Equatable {
|
||||
case populated
|
||||
case unpopulated
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Combine
|
||||
|
||||
public final class PopulatedStateControllerStub: PopulatedStateControllerProtocol {
|
||||
private let subject = CurrentValueSubject<PopulatedState, Never>(.populated)
|
||||
|
||||
public init() { }
|
||||
|
||||
public var state: AnyPublisher<PopulatedState, Never> {
|
||||
subject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public func setState(_ state: PopulatedState) {
|
||||
// Do nothing to ensure the state is always `.populated`
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,10 @@ public final class iOSApplicationRunningStateResource: ApplicationRunningStateRe
|
||||
case .background:
|
||||
return .background
|
||||
case .inactive:
|
||||
Log.error("Requesting state while in suspended state", domain: .application)
|
||||
// Can happen due to race condition during transition or perhaps during extension BG mode.
|
||||
return .background
|
||||
@unknown default:
|
||||
Log.error("Unknown application state", domain: .application)
|
||||
Log.error("Unknown application state", error: nil, domain: .application)
|
||||
return .foreground
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,7 @@ public protocol ThrowingAsynchronousInteractor {
|
||||
|
||||
func execute(with input: Input) async throws -> Output
|
||||
}
|
||||
|
||||
public protocol ThrowingAsynchronousWithoutDataInteractor {
|
||||
func execute() async throws
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive 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.
|
||||
//
|
||||
// Proton Drive 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 Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Combine
|
||||
import ProtonCoreNetworking
|
||||
|
||||
/// Adds retry mechanism with exponential backoff
|
||||
public final class NetworkErrorHandlingCommand: Command {
|
||||
private let interactor: any ThrowingAsynchronousWithoutDataInteractor
|
||||
private var task: Task<Void, Never>?
|
||||
|
||||
public init(interactor: any ThrowingAsynchronousWithoutDataInteractor) {
|
||||
self.interactor = interactor
|
||||
}
|
||||
|
||||
deinit {
|
||||
task?.cancel()
|
||||
}
|
||||
|
||||
public func execute() {
|
||||
task = Task.detached(priority: .background) { [weak self] in
|
||||
await self?.executeWithPotentialRetry()
|
||||
}
|
||||
}
|
||||
|
||||
private func executeWithPotentialRetry(attempt: Int = 0) async {
|
||||
guard !Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
guard attempt < 10 else {
|
||||
Log.error("Reached maximal number of network issues. Cancelling command now.", error: nil, domain: .networking)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try await interactor.execute()
|
||||
} catch let error as ResponseError {
|
||||
if error.isRetryableIncludingInternetIssues {
|
||||
Log.info("Received networking issue, will retry after exponential backoff", domain: .networking)
|
||||
try? await Task.sleep(for: .seconds(ExponentialBackoffWithJitter.getDelay(attempt: attempt)))
|
||||
await executeWithPotentialRetry(attempt: attempt + 1)
|
||||
} else {
|
||||
Log.error(nil, error: error, domain: .networking)
|
||||
}
|
||||
} catch {
|
||||
Log.error(nil, error: error, domain: .networking)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -26,7 +26,7 @@ public protocol AuthenticatedWebSessionInteractor {
|
||||
func execute() async throws -> AuthenticatedWebSessionData
|
||||
}
|
||||
|
||||
final class ProtonDocumentAuthenticatedWebSessionInteractor: AuthenticatedWebSessionInteractor {
|
||||
final class ProtonFileAuthenticatedWebSessionInteractor: AuthenticatedWebSessionInteractor {
|
||||
private let sessionStore: SessionStore
|
||||
private let selectorRepository: ChildSessionSelectorRepositoryProtocol
|
||||
private let encryptionResource: AESGCMEncryptionResource
|
||||
|
||||
@@ -108,7 +108,7 @@ public class CloudSlot: CloudSlotProtocol {
|
||||
private let client: Client
|
||||
private let sessionVault: SessionVault
|
||||
|
||||
private var moc: NSManagedObjectContext {
|
||||
public var moc: NSManagedObjectContext {
|
||||
self.storage.backgroundContext
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ public protocol CloudSlotProtocol: AnyObject,
|
||||
CloudFileCleaner,
|
||||
FolderCreatorProtocol,
|
||||
NodeRenamerProtocol,
|
||||
NodeMoverProtocol,
|
||||
CloudNodeMoverProtocol,
|
||||
CloudEventProvider,
|
||||
ThumbnailCloudClient,
|
||||
CloudAsyncVolumeCreatorProtocol,
|
||||
@@ -164,7 +164,9 @@ public protocol CloudSlotProtocol: AnyObject,
|
||||
CloudUpdaterProtocol,
|
||||
CloudTrasherProtocol,
|
||||
ThumbnailsUpdateRepository
|
||||
{ }
|
||||
{
|
||||
var moc: NSManagedObjectContext { get }
|
||||
}
|
||||
|
||||
public protocol CloudShareScannerProtocol {
|
||||
func scanShare(shareID: String, handler: @escaping (Result<Share, Error>) -> Void)
|
||||
@@ -185,14 +187,19 @@ public protocol CloudTrashScannerProtocol {
|
||||
|
||||
public protocol CloudChildrenScannerProtocol {
|
||||
func scanChildren(of parentID: NodeIdentifier, parameters: [FolderChildrenEndpointParameters]?, handler: @escaping (Result<[Node], Error>) -> Void)
|
||||
func scanChildren(of parentID: NodeIdentifier, parameters: [FolderChildrenEndpointParameters]?) async throws -> [Node]
|
||||
}
|
||||
|
||||
public protocol CloudNodeScannerProtocol {
|
||||
func scanNode(_ nodeID: NodeIdentifier, linkProcessingErrorTransformer: @escaping (Link, Error) -> Error, handler: @escaping (Result<Node, Error>) -> Void)
|
||||
|
||||
func scanNode(_ nodeID: NodeIdentifier, linkProcessingErrorTransformer: @escaping (Link, Error) -> Error) async throws -> Node
|
||||
}
|
||||
|
||||
public protocol CloudRevisionScannerProtocol {
|
||||
/// Legacy function, can be removed after 2025 Feb, once macOS migrated to DDK
|
||||
func scanRevision(_ revisionID: RevisionIdentifier, handler: @escaping (Result<Revision, Error>) -> Void)
|
||||
func scanRevision(_ revisionID: RevisionIdentifier) async throws -> Revision
|
||||
}
|
||||
|
||||
public protocol CloudFileCleaner {
|
||||
@@ -208,7 +215,7 @@ public protocol NodeRenamerProtocol {
|
||||
func rename(_ node: Node, to newName: String, mimeType: String?) async throws
|
||||
}
|
||||
|
||||
public protocol NodeMoverProtocol {
|
||||
public protocol CloudNodeMoverProtocol {
|
||||
func move(node: Node, to newParent: Folder, name: String) async throws
|
||||
}
|
||||
|
||||
@@ -229,7 +236,7 @@ public protocol CloudAsyncVolumeCreatorProtocol {
|
||||
}
|
||||
|
||||
public protocol ThumbnailCloudClient {
|
||||
func downloadThumbnailURL(parameters: RevisionThumbnailParameters, completion: @escaping(Result<URL, Error>) -> Void)
|
||||
func downloadThumbnailURL(parameters: RevisionThumbnailParameters, completion: @escaping (Result<URL, Error>) -> Void)
|
||||
}
|
||||
|
||||
public protocol CloudFileDraftCreator {
|
||||
@@ -308,6 +315,8 @@ extension CloudSlot {
|
||||
guard let volume = volumes.first(where: { $0.state == .active }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
update(volumes, in: moc)
|
||||
|
||||
let mainShare = try await scanRootShare(volume.share.shareID)
|
||||
if isPhotosEnabled {
|
||||
@@ -362,7 +371,7 @@ extension CloudSlot {
|
||||
let linksResponse = try await client.getLinksMetadata(with: .init(shareId: batch.shareID, linkIds: batch.linkIDs))
|
||||
try await moc.perform { [weak self] in
|
||||
guard let self else { return }
|
||||
_ = try self.update(links: linksResponse.parents + linksResponse.links, shareId: batch.shareID, managedObjectContext: self.moc)
|
||||
_ = try self.update(links: linksResponse.sortedLinks, shareId: batch.shareID, managedObjectContext: self.moc)
|
||||
try self.moc.saveOrRollback()
|
||||
}
|
||||
} catch {
|
||||
@@ -399,6 +408,28 @@ extension CloudSlot {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func scanChildren(of parentID: NodeIdentifier, parameters: [FolderChildrenEndpointParameters]?) async throws -> [Node] {
|
||||
let mode: UpdateMode = (parameters?.containsPagination() ?? false) ? .append : .replace
|
||||
let childrenLinksMeta = try await client.getFolderChildren(
|
||||
parentID.shareID,
|
||||
folderID: parentID.nodeID,
|
||||
parameters: parameters
|
||||
)
|
||||
return try await moc.perform { [weak self] in
|
||||
guard let self else { return [] }
|
||||
let childrenLinksMetaWithoutDrafts = childrenLinksMeta.filter { $0.state != .draft }
|
||||
let objs = self.update(
|
||||
childrenLinksMetaWithoutDrafts,
|
||||
under: parentID.nodeID,
|
||||
of: parentID.shareID,
|
||||
mode: mode,
|
||||
in: self.moc
|
||||
)
|
||||
try self.moc.saveOrRollback()
|
||||
return objs
|
||||
}
|
||||
}
|
||||
|
||||
public func scanNode(_ nodeID: NodeIdentifier,
|
||||
linkProcessingErrorTransformer: @escaping (Link, Error) -> Error = { $1 },
|
||||
@@ -422,6 +453,17 @@ extension CloudSlot {
|
||||
}
|
||||
}
|
||||
|
||||
public func scanNode(
|
||||
_ nodeID: NodeIdentifier,
|
||||
linkProcessingErrorTransformer: @escaping (PDClient.Link, any Error) -> any Error
|
||||
) async throws -> Node {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
scanNode(nodeID,
|
||||
linkProcessingErrorTransformer: linkProcessingErrorTransformer,
|
||||
handler: continuation.resume(with:))
|
||||
}
|
||||
}
|
||||
|
||||
public func scanRevision(_ revisionID: RevisionIdentifier,
|
||||
handler: @escaping (Result<Revision, Error>) -> Void)
|
||||
{
|
||||
@@ -441,6 +483,19 @@ extension CloudSlot {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func scanRevision(_ revisionID: RevisionIdentifier) async throws -> Revision {
|
||||
let revisionMeta = try await client.getRevision(
|
||||
revisionID: revisionID.revision,
|
||||
fileID: revisionID.file,
|
||||
shareID: revisionID.share
|
||||
)
|
||||
return try await moc.perform {
|
||||
let obj = self.update(revisionMeta, inFileID: revisionID.file, of: revisionID.share, in: self.moc)
|
||||
try self.moc.saveOrRollback()
|
||||
return obj
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delete Uploading files
|
||||
@@ -534,7 +589,7 @@ extension CloudSlot {
|
||||
self.client.postVolume(parameters: parameters) {
|
||||
switch $0 {
|
||||
case .failure(let error):
|
||||
Log.error(DriveError(error), domain: .networking)
|
||||
Log.error("CloudSlot postVolume failed", error: DriveError(error), domain: .networking)
|
||||
handler(.failure(error))
|
||||
|
||||
case .success(let newVolume):
|
||||
@@ -552,7 +607,7 @@ extension CloudSlot {
|
||||
}
|
||||
|
||||
} catch {
|
||||
Log.error(DriveError(error), domain: .encryption)
|
||||
Log.error("CloudSlot createVolume failed", error: DriveError(error), domain: .encryption)
|
||||
handler(.failure(error))
|
||||
}
|
||||
}
|
||||
@@ -591,7 +646,7 @@ extension CloudSlot {
|
||||
|
||||
// MARK: - ThumbnailCloudClient
|
||||
extension CloudSlot {
|
||||
public func downloadThumbnailURL(parameters: RevisionThumbnailParameters, completion: @escaping(Result<URL, Error>) -> Void) {
|
||||
public func downloadThumbnailURL(parameters: RevisionThumbnailParameters, completion: @escaping (Result<URL, Error>) -> Void) {
|
||||
client.getRevisionThumbnailURL(parameters: parameters, completion: completion)
|
||||
}
|
||||
}
|
||||
@@ -662,7 +717,13 @@ extension CloudSlot {
|
||||
// Client platform and version
|
||||
var photoParameter: UpdateRevisionParameters.Photo?
|
||||
if let photo = revision.photo {
|
||||
photoParameter = UpdateRevisionParameters.Photo(captureTime: photo.captureTime, mainPhotoLinkID: photo.mainPhotoLinkID, exif: nil, contentHash: photo.contentHash) // We don't upload exif until the format is aligned.
|
||||
photoParameter = UpdateRevisionParameters.Photo(
|
||||
captureTime: photo.captureTime,
|
||||
mainPhotoLinkID: photo.mainPhotoLinkID,
|
||||
exif: nil,
|
||||
contentHash: photo.contentHash,
|
||||
tags: photo.tags
|
||||
) // We don't upload exif until the format is aligned.
|
||||
}
|
||||
let parameters = UpdateRevisionParameters(
|
||||
manifestSignature: revision.manifestSignature,
|
||||
@@ -726,6 +787,7 @@ public protocol CloudUpdaterProtocol {
|
||||
public protocol CloudTrasherProtocol {
|
||||
func trash(shareID: Client.ShareID, parentID: Client.LinkID, linkIDs: [Client.LinkID]) async throws
|
||||
func trash(_ nodes: [TrashingNodeIdentifier]) async throws
|
||||
func trashVolume(nodeIDs: [AnyVolumeIdentifier]) async throws
|
||||
func delete(shareID: Client.ShareID, linkIDs: [Client.LinkID]) async throws
|
||||
func emptyTrash(shareID: Client.ShareID) async throws
|
||||
func restore(shareID: Client.ShareID, linkIDs: [Client.LinkID]) async throws -> [PartialFailure]
|
||||
@@ -778,7 +840,7 @@ extension CloudSlot {
|
||||
|
||||
share?.setValue(node, forKey: #keyPath(ShareObj.root))
|
||||
share?.setValue(volume, forKey: #keyPath(ShareObj.volume))
|
||||
share?.fulfill(from: shareMeta)
|
||||
share?.fulfillShare(with: shareMeta)
|
||||
|
||||
node?.directShares.insert(share!)
|
||||
if shareMeta.flags.contains(.main) {
|
||||
@@ -794,7 +856,7 @@ extension CloudSlot {
|
||||
|
||||
// Not part of the interface created just for tests, delete if possible
|
||||
@discardableResult
|
||||
func update(_ volumes: [VolumeMeta], in moc: NSManagedObjectContext) -> [VolumeObj] {
|
||||
public func update(_ volumes: [VolumeMeta], in moc: NSManagedObjectContext) -> [VolumeObj] {
|
||||
var result: [VolumeObj] = []
|
||||
|
||||
// switch to MOC's thread
|
||||
@@ -808,7 +870,7 @@ extension CloudSlot {
|
||||
// set up share and relationships
|
||||
result = volumes.compactMap { volumeMeta in
|
||||
let volume = uniqueVolumes.first { $0.id == volumeMeta.volumeID }
|
||||
volume?.fulfill(from: volumeMeta)
|
||||
volume?.fulfillVolume(with: volumeMeta)
|
||||
return volume
|
||||
}
|
||||
}
|
||||
@@ -818,9 +880,9 @@ extension CloudSlot {
|
||||
|
||||
// Not part of the interface created just for tests, delete if possible
|
||||
@discardableResult
|
||||
func update(_ shares: [ShareMeta], in moc: NSManagedObjectContext) -> [ShareObj] {
|
||||
public func update(_ shares: [ShareMeta], in moc: NSManagedObjectContext) -> [ShareObj] {
|
||||
let result: [ShareObj] = self.update(shares.map(ShareShortMeta.init), in: moc)
|
||||
zip(result, shares).forEach { $0.fulfill(from: $1) }
|
||||
zip(result, shares).forEach { $0.fulfillShare(with: $1) }
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -868,7 +930,10 @@ extension CloudSlot {
|
||||
|
||||
case .folder:
|
||||
affectedIds.folders.insert(link.linkID)
|
||||
@unknown default: assert(false, "Unknown node type")
|
||||
|
||||
case .album:
|
||||
Log.error("Trying to update Album by old update function", error: nil, domain: .metadata)
|
||||
assertionFailure("Shouldn't be used by iOS and Albums feature isn't supported on macOS")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -894,7 +959,7 @@ extension CloudSlot {
|
||||
photo.addToRevisions(localRevision)
|
||||
photo.photoRevision = localRevision
|
||||
photo.activeRevision = localRevision
|
||||
localRevision.fulfill(link: link, revision: revisionResponse)
|
||||
localRevision.fulfillRevision(link: link, revision: revisionResponse)
|
||||
|
||||
if revisionResponse.hasThumbnail, let thumbnails = revisionResponse.thumbnails {
|
||||
addThumbnails(thumbnails, revision: localRevision, in: moc)
|
||||
@@ -909,7 +974,7 @@ extension CloudSlot {
|
||||
{
|
||||
fileObj.addToRevisions(localRevision)
|
||||
fileObj.activeRevision = localRevision
|
||||
localRevision.fulfill(from: revisionResponse)
|
||||
localRevision.fulfillRevision(with: revisionResponse)
|
||||
|
||||
if revisionResponse.hasThumbnail, let thumbnails = revisionResponse.thumbnails {
|
||||
addThumbnails(thumbnails, revision: localRevision, in: moc)
|
||||
@@ -917,9 +982,17 @@ extension CloudSlot {
|
||||
}
|
||||
nodeObj?.setValue(parentLinkObj, forKey: #keyPath(NodeObj.parentLink))
|
||||
nodeObj?.setValue(shareID, forKey: #keyPath(NodeObj.shareID))
|
||||
(nodeObj as? FileObj)?.fulfill(from: link)
|
||||
(nodeObj as? FolderObj)?.fulfill(from: link)
|
||||
(nodeObj as? Photo)?.fulfillPhoto(from: link)
|
||||
|
||||
#if os(macOS)
|
||||
// Important for when enumerating items of a kept downloaded parent
|
||||
if let parentLinkObj, parentLinkObj.isAvailableOffline {
|
||||
nodeObj?.setValue(true, forKey: #keyPath(NodeObj.isInheritingOfflineAvailable))
|
||||
}
|
||||
#endif
|
||||
|
||||
(nodeObj as? FileObj)?.fulfillFile(with: link)
|
||||
(nodeObj as? FolderObj)?.fulfillFolder(with: link)
|
||||
(nodeObj as? Photo)?.fulfillPhoto(with: link)
|
||||
|
||||
directShares.forEach { share in
|
||||
share.setValue(nodeObj, forKey: #keyPath(ShareObj.root))
|
||||
@@ -987,7 +1060,7 @@ extension CloudSlot {
|
||||
|
||||
folderObj.setValue(parentLinkObj, forKey: #keyPath(NodeObj.parentLink))
|
||||
folderObj.setValue(shareID, forKey: #keyPath(NodeObj.shareID))
|
||||
folderObj.fulfill(from: folder)
|
||||
folderObj.fulfillFolder(with: folder)
|
||||
|
||||
result = folderObj
|
||||
}
|
||||
@@ -1033,7 +1106,7 @@ extension CloudSlot {
|
||||
moc.performAndWait {
|
||||
// set up share and relationships
|
||||
let revisionObj: RevisionObj = self.storage.unique(with: Set([revision.ID]), allowSubclasses: true, in: moc).first!
|
||||
revisionObj.fulfill(from: revision)
|
||||
revisionObj.fulfillRevision(with: revision)
|
||||
|
||||
let fileObj: File = self.storage.unique(with: Set([fileID]), allowSubclasses: true, in: moc).first!
|
||||
fileObj.setValue(shareID, forKey: #keyPath(NodeObj.shareID))
|
||||
@@ -1045,7 +1118,7 @@ extension CloudSlot {
|
||||
in: moc)
|
||||
newBlocks.forEach { block in
|
||||
let meta = revision.blocks.first { $0.URL.absoluteString == block.downloadUrl }!
|
||||
block.fulfill(from: meta)
|
||||
block.fulfillBlock(with: meta)
|
||||
block.setValue(revisionObj, forKey: #keyPath(BlockObj.revision))
|
||||
}
|
||||
|
||||
@@ -1084,7 +1157,7 @@ extension CloudSlot {
|
||||
private func update(_ newFileDetails: NewFile, file: FileObj) -> FileObj {
|
||||
let moc = file.managedObjectContext!
|
||||
moc.performAndWait {
|
||||
file.fulfill(from: newFileDetails)
|
||||
file.fulfillFile(with: newFileDetails)
|
||||
|
||||
let revision: RevisionObj = self.storage.unique(with: Set([newFileDetails.revisionID]), in: moc).first!
|
||||
file.activeRevision = revision
|
||||
@@ -1097,7 +1170,7 @@ extension CloudSlot {
|
||||
private func update(_ newFolderDetails: NewFolder, folder: FolderObj) -> FolderObj {
|
||||
let moc = folder.managedObjectContext!
|
||||
moc.performAndWait {
|
||||
folder.fulfill(from: newFolderDetails)
|
||||
folder.fulfillFolder(with: newFolderDetails)
|
||||
}
|
||||
return folder
|
||||
}
|
||||
@@ -1126,6 +1199,10 @@ extension CloudSlot {
|
||||
try await client.trash(shareID: shareID, parentID: parentID, linkIDs: linkIDs)
|
||||
}
|
||||
|
||||
public func trashVolume(nodeIDs: [AnyVolumeIdentifier]) async throws {
|
||||
fatalError("Not to be used on legacy CloudSlot")
|
||||
}
|
||||
|
||||
public func delete(shareID: Client.ShareID, linkIDs: [Client.LinkID]) async throws {
|
||||
try await client.deletePermanently(shareID: shareID, linkIDs: linkIDs)
|
||||
}
|
||||
@@ -1143,12 +1220,31 @@ extension CloudSlot {
|
||||
}
|
||||
}
|
||||
|
||||
public struct DeviceIdentifier: VolumeIdentifiable, Equatable {
|
||||
public let id: String
|
||||
public let nodeID: String
|
||||
public let shareID: String
|
||||
public let volumeID: String
|
||||
|
||||
public init(id: String, nodeID: String, shareID: String, volumeID: String) {
|
||||
self.id = id
|
||||
self.nodeID = nodeID
|
||||
self.shareID = shareID
|
||||
self.volumeID = volumeID
|
||||
}
|
||||
|
||||
public var nodeIdentifier: NodeIdentifier {
|
||||
NodeIdentifier(nodeID, shareID, volumeID)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Temporary workaround to filter non supported shares
|
||||
import Combine
|
||||
protocol SupportedSharesValidator {
|
||||
public protocol SupportedSharesValidator {
|
||||
func isValid(_ id: String) -> Bool
|
||||
}
|
||||
class iOSSupportedSharesValidator: SupportedSharesValidator {
|
||||
|
||||
public class iOSSupportedSharesValidator: SupportedSharesValidator {
|
||||
private let storage: StorageManager
|
||||
|
||||
private lazy var supportedShares: Set<String> = {
|
||||
@@ -1164,14 +1260,25 @@ class iOSSupportedSharesValidator: SupportedSharesValidator {
|
||||
return Set(shareIds)
|
||||
}()
|
||||
|
||||
init(storage: StorageManager) {
|
||||
public init(storage: StorageManager) {
|
||||
self.storage = storage
|
||||
}
|
||||
|
||||
func isValid(_ id: String) -> Bool {
|
||||
supportedShares.contains(id)
|
||||
public func isValid(_ id: String) -> Bool {
|
||||
if hasComputers {
|
||||
return true
|
||||
} else {
|
||||
return supportedShares.contains(id)
|
||||
}
|
||||
}
|
||||
|
||||
private var hasComputers: Bool {
|
||||
// We cannot use FeatureFlagsController directly anymore because the code was moved to PDCoreiOS,
|
||||
// This code should exist for a limited amount of time, until, computers become fully part of iOS
|
||||
LocalSettings.shared.driveiOSComputers && !LocalSettings.shared.driveiOSComputersDisabled
|
||||
}
|
||||
}
|
||||
|
||||
class macOSSupportedSharesValidator: SupportedSharesValidator {
|
||||
func isValid(_ id: String) -> Bool {
|
||||
true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user