Skip to content

Commit

Permalink
Adding Examples/BGUploaderDemo.
Browse files Browse the repository at this point in the history
  • Loading branch information
rnine committed Oct 21, 2021
1 parent 4dd64e4 commit 5354d9a
Show file tree
Hide file tree
Showing 25 changed files with 1,421 additions and 0 deletions.
18 changes: 18 additions & 0 deletions Examples/BGUploaderDemo/BGUploader/BGUploader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// BGUploader.h
// BGUploader
//
// Created by Ruben Nine on 20/10/21.
//

#import <Foundation/Foundation.h>

//! Project version number for BGUploader.
FOUNDATION_EXPORT double BGUploaderVersionNumber;

//! Project version string for BGUploader.
FOUNDATION_EXPORT const unsigned char BGUploaderVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <BGUploader/PublicHeader.h>


Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// UserDefaults+Settings.swift
// BGUploader
//
// Created by Ruben Nine on 20/10/21.
//

import Foundation

extension UserDefaults {
@UserDefault(key: "backgroundUploadProcess", defaultValue: BackgroundUploadProcess())
static var backgroundUploadProcess: BackgroundUploadProcess
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// UserDefault.swift
// Doorkeeper
//
// Created by Ruben Nine on 6/10/21.
//

import Foundation

/// Allows to match for optionals with generics that are defined as non-optional.
protocol AnyOptional {
/// Returns `true` if `nil`, otherwise `false`.
var isNil: Bool { get }
}

extension Optional: AnyOptional {
public var isNil: Bool { self == nil }
}

@propertyWrapper
struct UserDefault<Value: Codable> {
let key: String
let defaultValue: Value
var container: UserDefaults = .standard

var wrappedValue: Value {
get {
if let data = container.object(forKey: key) as? Data,
let user = try? JSONDecoder().decode(Value.self, from: data) {
return user
}

return defaultValue
}

set {
if let encoded = try? JSONEncoder().encode(newValue) {
container.set(encoded, forKey: key)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// BackgroundUploadProcess.swift
// BGUploader
//
// Created by Ruben Nine on 20/10/21.
//

import Foundation

public class BackgroundUploadProcess: Codable {
/// Contains the upload tasks currently in progress.
public var tasks: [Int: BackgroundUploadTaskResult] = [:]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// BackgroundUploadTaskResult.swift
// BGUploader
//
// Created by Ruben Nine on 20/10/21.
//

import Foundation

public class BackgroundUploadTaskResult: Codable {
/// The `URL` that is to be uploaded.
public let url: URL

/// The current status for this task.
public internal(set) var status: Status = .started

/// Default initializer.
///
/// - Parameter url: The `URL` that is going to be uploaded.
init(url: URL) {
self.url = url
}
}

extension BackgroundUploadTaskResult {
public enum Status: Equatable, Codable {
case started
case completed(response: StoreResponse)
case failed(error: BGUploadService.Error)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// StoreResponse.swift
// BGUploader
//
// Created by Ruben Nine on 20/10/21.
//

import Foundation

/// `StoreResponse` represents the expected JSON object response when doing a POST against /api/store/S3.
public struct StoreResponse: Codable, Equatable {
/// Filestack Handle (derived from `url`)
public var handle: String { url.lastPathComponent }

/// Filestack Handle URL.
public let url: URL

/// S3 container.
public let container: String

/// Filename (e.g. "pic1.jpg")
public let filename: String

/// Key used in S3 storage.
public let key: String

/// Mimetype (e.g. "image/jpeg")
public let type: String

/// Filesize (e.g. 5520262)
public let size: Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//
// BGUploadService.swift
// BGUploader
//
// Created by Ruben Nine on 20/10/21.
//

import Foundation
import FilestackSDK

public protocol BGUploadServiceDelegate: AnyObject {
/// Called after an upload task completes (either successfully or failing.)
///
/// You may query `url` to determine the file `URL` that was uploaded and `status` to determine completion status
/// on the returned `BackgroundUploadTaskResult` object.
func uploadService(_ uploadService: BGUploadService, didCompleteWith result: BackgroundUploadTaskResult)
}

public class BGUploadService: NSObject {
// MARK: - Public Properties

public let backgroundIdentifer = "com.filestack.BGUploader"
public weak var delegate: BGUploadServiceDelegate?

// MARK: - Private Properties

private let storeURL = URL(string: "https://www.filestackapi.com/api/store/S3")!
private var transitorySessionData = [URLSessionTask: Data]()
private let fsClient: Client

private lazy var session: URLSession = {
let configuration: URLSessionConfiguration

configuration = .background(withIdentifier: backgroundIdentifer)
configuration.isDiscretionary = false
configuration.waitsForConnectivity = true

return URLSession(configuration: configuration, delegate: self, delegateQueue: .main)
}()

// MARK: - Lifecycle

public init(fsClient: Client) {
self.fsClient = fsClient
}
}

// MARK: - URLSessionDataDelegate Protocol Implementation

extension BGUploadService: URLSessionDataDelegate {
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
transitorySessionData[dataTask] = data
}
}

// MARK: - URLSessionTaskDelegate Protocol Implementation

extension BGUploadService: URLSessionTaskDelegate {
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Swift.Error?) {
guard let result = UserDefaults.backgroundUploadProcess.tasks[task.taskIdentifier] else { return }

if task.state == .completed, let responseData = transitorySessionData[task] {
transitorySessionData.removeValue(forKey: task)

do {
let storeResponse = try JSONDecoder().decode(StoreResponse.self, from: responseData)
result.status = .completed(response: storeResponse)
} catch {
result.status = .failed(error: .undecodableJSONResponse)
}
} else if let error = error {
result.status = .failed(error: .other(description: error.localizedDescription))
} else {
result.status = .failed(error: .unknown)
}

delegate?.uploadService(self, didCompleteWith: result)
removeTaskResult(with: task.taskIdentifier)
}
}

// MARK: - BGUploadService Error

public extension BGUploadService {
enum Error: Swift.Error, Equatable, Codable {
case undecodableJSONResponse
case other(description: String)
case unknown
}
}

// MARK: - Public Functions

public extension BGUploadService {
/// Uploads an `URL` to Filestack using a background `URLSession`.
@discardableResult
func upload(url: URL) -> URLSessionUploadTask? {
let task = session.uploadTask(with: storeRequest(for: url), fromFile: url)

addTaskResult(with: task.taskIdentifier, for: url)

task.resume()

return task
}

/// Resumes any pending background uploads.
///
/// Call this function on your `AppDelegate.application(_:,handleEventsForBackgroundURLSession:,completionHandler:)`
func resumePendingUploads(completionHandler: @escaping () -> Void) {
session.getAllTasks { tasks in
for task in tasks {
task.resume()
}

completionHandler()
}
}
}

// MARK: - Private Functions

private extension BGUploadService {
/// Returns an `URLRequest` setup for uploading a file using
/// Filestack's [Basic Uploads](https://www.filestack.com/docs/uploads/uploading/#basic-uploads) API.
func storeRequest(for url: URL) -> URLRequest {
var components = URLComponents(url: storeURL, resolvingAgainstBaseURL: false)!
var queryItems: [URLQueryItem] = []

queryItems = [
URLQueryItem(name: "key", value: fsClient.apiKey),
URLQueryItem(name: "filename", value: url.filename)
]

if let security = fsClient.security {
queryItems.append(URLQueryItem(name: "policy", value: security.encodedPolicy))
queryItems.append(URLQueryItem(name: "signature", value: security.signature))
}

components.queryItems = queryItems

var request = URLRequest(url: components.url!)

request.addValue(url.mimeType ?? "text/plain", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"

return request
}

@discardableResult
func addTaskResult(with taskIdentifier: Int, for url: URL) -> BackgroundUploadTaskResult {
let taskResult = BackgroundUploadTaskResult(url: url)
UserDefaults.backgroundUploadProcess.tasks[taskIdentifier] = taskResult
return taskResult
}

@discardableResult
private func removeTaskResult(with taskIdentifier: Int) -> BackgroundUploadTaskResult? {
return UserDefaults.backgroundUploadProcess.tasks.removeValue(forKey: taskIdentifier)
}
}
Loading

0 comments on commit 5354d9a

Please sign in to comment.