Skip to content

Commit

Permalink
Merge pull request #53 from Infomaniak/kDriveShareIntegration
Browse files Browse the repository at this point in the history
On the fly JPG / HEIC conversion
  • Loading branch information
PhilippeWeidmann committed Jul 26, 2023
2 parents 0cc908c + 3a34449 commit f0ff51e
Show file tree
Hide file tree
Showing 13 changed files with 697 additions and 163 deletions.
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ let package = Package(
),
.testTarget(
name: "InfomaniakCoreTests",
dependencies: ["InfomaniakCore","ZIPFoundation"]
dependencies: ["InfomaniakCore","ZIPFoundation"],
resources: [Resource.copy("Ressources/Matterhorn_as_seen_from_Zermatt,_Wallis,_Switzerland,_2012_August,Wikimedia_Commons.heic"),
Resource.copy("Ressources/Matterhorn_as_seen_from_Zermatt,_Wallis,_Switzerland,_2012_August,Wikimedia_Commons.jpg")]
)
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -24,54 +24,78 @@ import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to load an url from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderFileRepresentation: NSObject, ProgressResultable {
/// Progress increment size
private static let progressStep: Int64 = 1

/// Number of steps to complete the task
private static let totalSteps: Int64 = 2

/// Something to transform events to a nice `async Result`
private let flowToAsync = FlowToAsyncResult<Success>()

/// Shorthand for default FileManager
private let fileManager = FileManager.default

/// Domain specific errors
public enum ErrorDomain: Error, Equatable{
public enum ErrorDomain: Error, Equatable {
case UTINotFound
case UnableToLoadFile
}

public typealias Success = URL
public typealias Failure = Error

public init(from itemProvider: NSItemProvider) throws {
/// Init method
/// - Parameters:
/// - itemProvider: The item provider we will be working with
/// - preferredImageFileFormat: Specify an output image file format. Supports HEIC and JPG. Will convert only if
/// itemProvider supports it.
public init(from itemProvider: NSItemProvider, preferredImageFileFormat: UTI? = nil) throws {
guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else {
throw ErrorDomain.UTINotFound
}

// Keep compiler happy
progress = Progress(totalUnitCount: 1)
progress = Progress(totalUnitCount: Self.totalSteps)

super.init()

// Set progress and hook completion closure to a combine pipe
progress = itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { [self] fileProviderURL, error in
// Check if requested an image conversion, and if conversion is available.
let fileIdentifierToUse = self.preferredImageFileFormat(
itemProvider: itemProvider,
typeIdentifier: typeIdentifier,
preferredImageFileFormat: preferredImageFileFormat
)

// Set progress and hook completion closure
let completionProgress = Progress(totalUnitCount: Self.progressStep)
progress.addChild(completionProgress, withPendingUnitCount: Self.progressStep)

let loadURLProgress = itemProvider.loadFileRepresentation(forTypeIdentifier: fileIdentifierToUse) { [self] fileProviderURL, error in
guard let fileProviderURL, error == nil else {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error ?? ErrorDomain.UnableToLoadFile)
return
}

do {
let UTI = UTI(rawValue: typeIdentifier as CFString)
let uti = UTI(rawValue: fileIdentifierToUse as CFString)
@InjectService var pathProvider: AppGroupPathProvidable
let temporaryURL = pathProvider.tmpDirectoryURL
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try fileManager.createDirectory(at: temporaryURL, withIntermediateDirectories: true)

let fileName = fileProviderURL.appendingPathExtension(for: UTI).lastPathComponent
let fileName = fileProviderURL.appendingPathExtension(for: uti).lastPathComponent
let temporaryFileURL = temporaryURL.appendingPathComponent(fileName)
try fileManager.copyItem(atPath: fileProviderURL.path, toPath: temporaryFileURL.path)


completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendSuccess(temporaryFileURL)
} catch {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error)
}
}
progress.addChild(loadURLProgress, withPendingUnitCount: Self.progressStep)
}

// MARK: ProgressResultable
Expand All @@ -80,7 +104,35 @@ public final class ItemProviderFileRepresentation: NSObject, ProgressResultable

public var result: Result<URL, Error> {
get async {
await self.flowToAsync.result
await flowToAsync.result
}
}

// MARK: Private

/// Check if a File conversion is possible for the provided `itemProvider` and `typeIdentifier`,
/// returns `typeIdentifier` if no conversion is possible.
///
/// - Parameters:
/// - itemProvider: The ItemProvider we work with
/// - typeIdentifier: top typeIdentifier for ItemProvider
/// - preferredImageFileFormat: The image format the user is requesting
private func preferredImageFileFormat(itemProvider: NSItemProvider,
typeIdentifier: String,
preferredImageFileFormat: UTI?) -> String {
if let preferredImageFileFormat = preferredImageFileFormat {
// Check that itemProvider supports the image types we ask of it
if itemProvider.hasItemConformingToAnyOfTypeIdentifiers([UTI.heic.identifier, UTI.jpeg.identifier]),
itemProvider.hasItemConformingToTypeIdentifier(preferredImageFileFormat.identifier) {
return preferredImageFileFormat.identifier
}
// No conversion if not possible
else {
return typeIdentifier
}
} else {
// No conversion
return typeIdentifier
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to make a raw text file from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderTextRepresentation: NSObject, ProgressResultable {
/// Progress increment size
private static let progressStep: Int64 = 1

/// Something to transform events to a nice `async Result`
private let flowToAsync = FlowToAsyncResult<Success>()

Expand All @@ -40,28 +43,22 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
public typealias Success = URL
public typealias Failure = Error

private static let progressStep: Int64 = 1

public init(from itemProvider: NSItemProvider) throws {
guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else {
throw ErrorDomain.UTINotFound
}

// Keep compiler happy
progress = Progress(totalUnitCount: 1)
progress = Progress(totalUnitCount: Self.progressStep)

super.init()

let childProgress = Progress()
progress.addChild(childProgress, withPendingUnitCount: Self.progressStep)
let completionProgress = Progress()
progress.addChild(completionProgress, withPendingUnitCount: Self.progressStep)

itemProvider.loadItem(forTypeIdentifier: typeIdentifier) { [self] coding, error in
defer {
childProgress.completedUnitCount += Self.progressStep
}

guard error == nil,
let coding else {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error ?? ErrorDomain.unknown)
return
}
Expand All @@ -74,43 +71,58 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
try fileManager.createDirectory(at: temporaryURL, withIntermediateDirectories: true)

// Is String
guard try !stringHandling(coding, temporaryURL: temporaryURL) else {
guard try !stringHandling(coding, temporaryURL: temporaryURL, completionProgress: completionProgress) else {
return
}

// Is Data
guard try !dataHandling(coding, typeIdentifier: typeIdentifier, temporaryURL: temporaryURL) else {
guard try !dataHandling(
coding,
typeIdentifier: typeIdentifier,
temporaryURL: temporaryURL,
completionProgress: completionProgress
) else {
return
}

// Not supported
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(ErrorDomain.UTINotSupported)

} catch {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error)
return
}
}
}

private func stringHandling(_ coding: NSSecureCoding, temporaryURL: URL) throws -> Bool {
private func stringHandling(_ coding: NSSecureCoding, temporaryURL: URL, completionProgress: Progress) throws -> Bool {
guard let text = coding as? String else {
// Not matching type, do nothing.
return false
}
let targetURL = temporaryURL.appendingPathComponent("\(UUID().uuidString).txt")

let targetURL = temporaryURL.appendingPathComponent("\(UUID().uuidString).txt")
try text.write(to: targetURL, atomically: true, encoding: .utf8)

completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendSuccess(targetURL)

return true
}

private func dataHandling(_ coding: NSSecureCoding, typeIdentifier: String, temporaryURL: URL) throws -> Bool {
private func dataHandling(_ coding: NSSecureCoding,
typeIdentifier: String,
temporaryURL: URL,
completionProgress: Progress) throws -> Bool {
guard let data = coding as? Data else {
// Not matching type, do nothing.
return false
}

guard let uti = UTI(typeIdentifier) else {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(ErrorDomain.UTINotFound)
return false
}
Expand All @@ -120,6 +132,8 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
.appendingPathExtension(for: uti)

try data.write(to: targetURL)

completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendSuccess(targetURL)

return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to make a webloc plist from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderWeblocRepresentation: NSObject, ProgressResultable {
/// Progress increment size
private static let progressStep: Int64 = 1

/// Number of steps to complete the task
private static let totalSteps: Int64 = 2

/// Something to transform events to a nice `async Result`
private let flowToAsync = FlowToAsyncResult<Success>()

Expand All @@ -39,14 +45,17 @@ public final class ItemProviderWeblocRepresentation: NSObject, ProgressResultabl
public typealias Failure = Error

public init(from itemProvider: NSItemProvider) throws {
// Keep compiler happy
progress = Progress(totalUnitCount: 1)
progress = Progress(totalUnitCount: Self.totalSteps)

super.init()

progress = itemProvider.loadObject(ofClass: URL.self) { [self] path, error in
let completionProgress = Progress(totalUnitCount: Self.progressStep)
progress.addChild(completionProgress, withPendingUnitCount: Self.progressStep)

let loadURLProgress = itemProvider.loadObject(ofClass: URL.self) { [self] path, error in
guard error == nil, let path: URL = path else {
let error: Error = error ?? ErrorDomain.unableToLoadURLForObject
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error)
return
}
Expand All @@ -60,17 +69,20 @@ public final class ItemProviderWeblocRepresentation: NSObject, ProgressResultabl
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try fileManager.createDirectory(at: tmpDirectoryURL, withIntermediateDirectories: true)

let fileName = path.lastPathComponent
let fileName = path.deletingPathExtension().lastPathComponent
let targetURL = tmpDirectoryURL.appendingPathComponent("\(fileName).webloc")
let encoder = PropertyListEncoder()
let data = try encoder.encode(content)
try data.write(to: targetURL)

completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendSuccess(targetURL)
} catch {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error)
}
}
progress.addChild(loadURLProgress, withPendingUnitCount: Self.progressStep)
}

// MARK: ProgressResultable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to make a zip from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderZipRepresentation: NSObject, ProgressResultable {
/// Coordinator for file operations
let coordinator = NSFileCoordinator()

/// Progress increment size
private static let progressStep: Int64 = 1

/// Number of steps to complete the task
private static let totalSteps: Int64 = 2

/// Something to transform events to a nice `async Result`
private let flowToAsync = FlowToAsyncResult<Success>()

Expand All @@ -40,23 +49,22 @@ public final class ItemProviderZipRepresentation: NSObject, ProgressResultable {
public typealias Success = URL
public typealias Failure = Error

private static let progressStep: Int64 = 1

public init(from itemProvider: NSItemProvider) throws {
// It must be a directory for the OS to zip it for us, a file returns a file
guard itemProvider.underlyingType == .isDirectory else {
throw ErrorDomain.notADirectory
}

// Keep compiler happy
progress = Progress(totalUnitCount: 1)
progress = Progress(totalUnitCount: Self.totalSteps)

super.init()

let coordinator = NSFileCoordinator()

progress = itemProvider.loadObject(ofClass: URL.self) { [self] path, error in
let completionProgress = Progress(totalUnitCount: Self.progressStep)
progress.addChild(completionProgress, withPendingUnitCount: Self.progressStep)

let loadURLProgress = itemProvider.loadObject(ofClass: URL.self) { [self] path, error in
guard error == nil, let path: URL = path else {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error ?? ErrorDomain.unableToLoadURLForObject)
return
}
Expand All @@ -66,10 +74,6 @@ public final class ItemProviderZipRepresentation: NSObject, ProgressResultable {
// > If you’d like to see such support [ie. for NSProgress] added in the future, I encourage you to file an
// enhancement request

// Minimalist progress file processing support
let childProgress = Progress()
progress.addChild(childProgress, withPendingUnitCount: Self.progressStep)

// compress content of folder and move it somewhere we can safely store it for upload
var error: NSError?
coordinator.coordinate(readingItemAt: path, options: [.forUploading], error: &error) { zipURL in
Expand All @@ -83,13 +87,16 @@ public final class ItemProviderZipRepresentation: NSObject, ProgressResultable {
let targetURL = tmpDirectoryURL.appendingPathComponent("\(fileName).zip")

try self.fileManager.moveItem(at: zipURL, to: targetURL)

completionProgress.completedUnitCount += Self.progressStep
self.flowToAsync.sendSuccess(targetURL)
} catch {
completionProgress.completedUnitCount += Self.progressStep
self.flowToAsync.sendFailure(error)
}
childProgress.completedUnitCount += Self.progressStep
}
}
progress.addChild(loadURLProgress, withPendingUnitCount: Self.progressStep)
}

// MARK: ProgressResultable
Expand Down
Loading

0 comments on commit f0ff51e

Please sign in to comment.