Skip to content

Commit f282955

Browse files
committed
Merge branch 'develop'
2 parents 386c577 + 5354d9a commit f282955

File tree

32 files changed

+1582
-28
lines changed

32 files changed

+1582
-28
lines changed

.travis.yml

-24
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// BGUploader.h
3+
// BGUploader
4+
//
5+
// Created by Ruben Nine on 20/10/21.
6+
//
7+
8+
#import <Foundation/Foundation.h>
9+
10+
//! Project version number for BGUploader.
11+
FOUNDATION_EXPORT double BGUploaderVersionNumber;
12+
13+
//! Project version string for BGUploader.
14+
FOUNDATION_EXPORT const unsigned char BGUploaderVersionString[];
15+
16+
// In this header, you should import all the public headers of your framework using statements like #import <BGUploader/PublicHeader.h>
17+
18+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// UserDefaults+Settings.swift
3+
// BGUploader
4+
//
5+
// Created by Ruben Nine on 20/10/21.
6+
//
7+
8+
import Foundation
9+
10+
extension UserDefaults {
11+
@UserDefault(key: "backgroundUploadProcess", defaultValue: BackgroundUploadProcess())
12+
static var backgroundUploadProcess: BackgroundUploadProcess
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// UserDefault.swift
3+
// Doorkeeper
4+
//
5+
// Created by Ruben Nine on 6/10/21.
6+
//
7+
8+
import Foundation
9+
10+
/// Allows to match for optionals with generics that are defined as non-optional.
11+
protocol AnyOptional {
12+
/// Returns `true` if `nil`, otherwise `false`.
13+
var isNil: Bool { get }
14+
}
15+
16+
extension Optional: AnyOptional {
17+
public var isNil: Bool { self == nil }
18+
}
19+
20+
@propertyWrapper
21+
struct UserDefault<Value: Codable> {
22+
let key: String
23+
let defaultValue: Value
24+
var container: UserDefaults = .standard
25+
26+
var wrappedValue: Value {
27+
get {
28+
if let data = container.object(forKey: key) as? Data,
29+
let user = try? JSONDecoder().decode(Value.self, from: data) {
30+
return user
31+
}
32+
33+
return defaultValue
34+
}
35+
36+
set {
37+
if let encoded = try? JSONEncoder().encode(newValue) {
38+
container.set(encoded, forKey: key)
39+
}
40+
}
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// BackgroundUploadProcess.swift
3+
// BGUploader
4+
//
5+
// Created by Ruben Nine on 20/10/21.
6+
//
7+
8+
import Foundation
9+
10+
public class BackgroundUploadProcess: Codable {
11+
/// Contains the upload tasks currently in progress.
12+
public var tasks: [Int: BackgroundUploadTaskResult] = [:]
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// BackgroundUploadTaskResult.swift
3+
// BGUploader
4+
//
5+
// Created by Ruben Nine on 20/10/21.
6+
//
7+
8+
import Foundation
9+
10+
public class BackgroundUploadTaskResult: Codable {
11+
/// The `URL` that is to be uploaded.
12+
public let url: URL
13+
14+
/// The current status for this task.
15+
public internal(set) var status: Status = .started
16+
17+
/// Default initializer.
18+
///
19+
/// - Parameter url: The `URL` that is going to be uploaded.
20+
init(url: URL) {
21+
self.url = url
22+
}
23+
}
24+
25+
extension BackgroundUploadTaskResult {
26+
public enum Status: Equatable, Codable {
27+
case started
28+
case completed(response: StoreResponse)
29+
case failed(error: BGUploadService.Error)
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// StoreResponse.swift
3+
// BGUploader
4+
//
5+
// Created by Ruben Nine on 20/10/21.
6+
//
7+
8+
import Foundation
9+
10+
/// `StoreResponse` represents the expected JSON object response when doing a POST against /api/store/S3.
11+
public struct StoreResponse: Codable, Equatable {
12+
/// Filestack Handle (derived from `url`)
13+
public var handle: String { url.lastPathComponent }
14+
15+
/// Filestack Handle URL.
16+
public let url: URL
17+
18+
/// S3 container.
19+
public let container: String
20+
21+
/// Filename (e.g. "pic1.jpg")
22+
public let filename: String
23+
24+
/// Key used in S3 storage.
25+
public let key: String
26+
27+
/// Mimetype (e.g. "image/jpeg")
28+
public let type: String
29+
30+
/// Filesize (e.g. 5520262)
31+
public let size: Int
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//
2+
// BGUploadService.swift
3+
// BGUploader
4+
//
5+
// Created by Ruben Nine on 20/10/21.
6+
//
7+
8+
import Foundation
9+
import FilestackSDK
10+
11+
public protocol BGUploadServiceDelegate: AnyObject {
12+
/// Called after an upload task completes (either successfully or failing.)
13+
///
14+
/// You may query `url` to determine the file `URL` that was uploaded and `status` to determine completion status
15+
/// on the returned `BackgroundUploadTaskResult` object.
16+
func uploadService(_ uploadService: BGUploadService, didCompleteWith result: BackgroundUploadTaskResult)
17+
}
18+
19+
public class BGUploadService: NSObject {
20+
// MARK: - Public Properties
21+
22+
public let backgroundIdentifer = "com.filestack.BGUploader"
23+
public weak var delegate: BGUploadServiceDelegate?
24+
25+
// MARK: - Private Properties
26+
27+
private let storeURL = URL(string: "https://www.filestackapi.com/api/store/S3")!
28+
private var transitorySessionData = [URLSessionTask: Data]()
29+
private let fsClient: Client
30+
31+
private lazy var session: URLSession = {
32+
let configuration: URLSessionConfiguration
33+
34+
configuration = .background(withIdentifier: backgroundIdentifer)
35+
configuration.isDiscretionary = false
36+
configuration.waitsForConnectivity = true
37+
38+
return URLSession(configuration: configuration, delegate: self, delegateQueue: .main)
39+
}()
40+
41+
// MARK: - Lifecycle
42+
43+
public init(fsClient: Client) {
44+
self.fsClient = fsClient
45+
}
46+
}
47+
48+
// MARK: - URLSessionDataDelegate Protocol Implementation
49+
50+
extension BGUploadService: URLSessionDataDelegate {
51+
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
52+
transitorySessionData[dataTask] = data
53+
}
54+
}
55+
56+
// MARK: - URLSessionTaskDelegate Protocol Implementation
57+
58+
extension BGUploadService: URLSessionTaskDelegate {
59+
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Swift.Error?) {
60+
guard let result = UserDefaults.backgroundUploadProcess.tasks[task.taskIdentifier] else { return }
61+
62+
if task.state == .completed, let responseData = transitorySessionData[task] {
63+
transitorySessionData.removeValue(forKey: task)
64+
65+
do {
66+
let storeResponse = try JSONDecoder().decode(StoreResponse.self, from: responseData)
67+
result.status = .completed(response: storeResponse)
68+
} catch {
69+
result.status = .failed(error: .undecodableJSONResponse)
70+
}
71+
} else if let error = error {
72+
result.status = .failed(error: .other(description: error.localizedDescription))
73+
} else {
74+
result.status = .failed(error: .unknown)
75+
}
76+
77+
delegate?.uploadService(self, didCompleteWith: result)
78+
removeTaskResult(with: task.taskIdentifier)
79+
}
80+
}
81+
82+
// MARK: - BGUploadService Error
83+
84+
public extension BGUploadService {
85+
enum Error: Swift.Error, Equatable, Codable {
86+
case undecodableJSONResponse
87+
case other(description: String)
88+
case unknown
89+
}
90+
}
91+
92+
// MARK: - Public Functions
93+
94+
public extension BGUploadService {
95+
/// Uploads an `URL` to Filestack using a background `URLSession`.
96+
@discardableResult
97+
func upload(url: URL) -> URLSessionUploadTask? {
98+
let task = session.uploadTask(with: storeRequest(for: url), fromFile: url)
99+
100+
addTaskResult(with: task.taskIdentifier, for: url)
101+
102+
task.resume()
103+
104+
return task
105+
}
106+
107+
/// Resumes any pending background uploads.
108+
///
109+
/// Call this function on your `AppDelegate.application(_:,handleEventsForBackgroundURLSession:,completionHandler:)`
110+
func resumePendingUploads(completionHandler: @escaping () -> Void) {
111+
session.getAllTasks { tasks in
112+
for task in tasks {
113+
task.resume()
114+
}
115+
116+
completionHandler()
117+
}
118+
}
119+
}
120+
121+
// MARK: - Private Functions
122+
123+
private extension BGUploadService {
124+
/// Returns an `URLRequest` setup for uploading a file using
125+
/// Filestack's [Basic Uploads](https://www.filestack.com/docs/uploads/uploading/#basic-uploads) API.
126+
func storeRequest(for url: URL) -> URLRequest {
127+
var components = URLComponents(url: storeURL, resolvingAgainstBaseURL: false)!
128+
var queryItems: [URLQueryItem] = []
129+
130+
queryItems = [
131+
URLQueryItem(name: "key", value: fsClient.apiKey),
132+
URLQueryItem(name: "filename", value: url.filename)
133+
]
134+
135+
if let security = fsClient.security {
136+
queryItems.append(URLQueryItem(name: "policy", value: security.encodedPolicy))
137+
queryItems.append(URLQueryItem(name: "signature", value: security.signature))
138+
}
139+
140+
components.queryItems = queryItems
141+
142+
var request = URLRequest(url: components.url!)
143+
144+
request.addValue(url.mimeType ?? "text/plain", forHTTPHeaderField: "Content-Type")
145+
request.httpMethod = "POST"
146+
147+
return request
148+
}
149+
150+
@discardableResult
151+
func addTaskResult(with taskIdentifier: Int, for url: URL) -> BackgroundUploadTaskResult {
152+
let taskResult = BackgroundUploadTaskResult(url: url)
153+
UserDefaults.backgroundUploadProcess.tasks[taskIdentifier] = taskResult
154+
return taskResult
155+
}
156+
157+
@discardableResult
158+
private func removeTaskResult(with taskIdentifier: Int) -> BackgroundUploadTaskResult? {
159+
return UserDefaults.backgroundUploadProcess.tasks.removeValue(forKey: taskIdentifier)
160+
}
161+
}

0 commit comments

Comments
 (0)