Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

swift5: upload in background #164

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions config/swift5-uploader.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ changelog:
- Fix large upload request time out
- Fix callback on wrong token

library: alamofire
library: urlsession
generateAliasAsModel: true
removeMigrationProjectNameClass: true
swiftPackagePath: "Sources"
Expand All @@ -46,17 +46,12 @@ additionalProperties:
podHomepage: https://docs.api.video
podSocialMediaUrl: https://twitter.com/api_video
podLicense: "{ :type => 'MIT' }"
backgroundSupport: true
defaultChunkSize: 50 * 1024 * 1024
minChunkSize: 5 * 1024 * 1024
maxChunkSize: 128 * 1024 * 1024

files:
Auth/ApiVideoCredential.mustache:
folder: Sources/Auth
destinationFilename: ApiVideoCredential.swift
Auth/ApiVideoAuthenticator.mustache:
folder: Sources/Auth
destinationFilename: ApiVideoAuthenticator.swift
Environment.mustache:
folder: Sources/Models
destinationFilename: Environment.swift
Expand Down
9 changes: 2 additions & 7 deletions config/swift5.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ changelog:
- 0.1.0 (2021-12-06):
- Initial release

library: alamofire
library: urlsession
generateAliasAsModel: true
removeMigrationProjectNameClass: true
swiftPackagePath: "Sources"
Expand All @@ -51,17 +51,12 @@ additionalProperties:
podHomepage: https://docs.api.video
podSocialMediaUrl: https://twitter.com/api_video
podLicense: "{ :type => 'MIT' }"
backgroundSupport: true
defaultChunkSize: 50 * 1024 * 1024
minChunkSize: 5 * 1024 * 1024
maxChunkSize: 128 * 1024 * 1024

files:
Auth/ApiVideoCredential.mustache:
folder: Sources/Auth
destinationFilename: ApiVideoCredential.swift
Auth/ApiVideoAuthenticator.mustache:
folder: Sources/Auth
destinationFilename: ApiVideoAuthenticator.swift
Environment.mustache:
folder: Sources/Models
destinationFilename: Environment.swift
Expand Down
52 changes: 37 additions & 15 deletions templates/swift5/APIs.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,38 @@ import Vapor
{{/removeMigrationProjectNameClass}}

public class {{projectName}} {
public static var apiKey: String? = nil
private static var apiKey: String? = nil
public static var basePath = "{{basePath}}"
{{#useVapor}}
internal static var customHeaders: [String: String] = [:]
internal static var defaultHeaders: HTTPHeaders = ["AV-Origin-Client": "{{ originClient }}:{{ podVersion }}"]
{{/useVapor}}
{{^useVapor}}
internal static var customHeaders:[String: String] = ["AV-Origin-Client": "{{ originClient }}:{{ podVersion }}"]
internal static var defaultHeaders:[String: String] = ["AV-Origin-Client": "{{ originClient }}:{{ podVersion }}"]
internal static var credential: URLCredential?
private static var chunkSize: Int = {{defaultChunkSize}}{{#useAlamofire}}
internal static var requestBuilderFactory: RequestBuilderFactory = AlamofireRequestBuilderFactory(){{/useAlamofire}}{{#useURLSession}}
internal static var requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(){{/useURLSession}}
internal static var credential = ApiVideoCredential()
public static var apiResponseQueue: DispatchQueue = .main
public static var timeout: TimeInterval = 60
{{/useVapor}}{{#backgroundSupport}}
public static var backgroundIdentifier: String = "video.api.upload.background"
{{/backgroundSupport}}public static var timeout: TimeInterval = 60
internal static var customHeaders:[String: String] {
var headers = defaultHeaders
if let apiKey = apiKey {
headers["Authorization"] = apiKey
}
return headers
}

public static func setApiKey(_ apiKey: String?) {
if let apiKey = apiKey {
self.apiKey = "Basic " + "\(apiKey):".toBase64()
} else {
self.apiKey = nil
}
}

public static func setChunkSize(chunkSize: Int) throws {
public static func setChunkSize(_ chunkSize: Int) throws {
if (chunkSize > {{maxChunkSize}}) {
throw ParameterError.outOfRange
} else if (chunkSize < {{minChunkSize}}) {
Expand All @@ -55,25 +72,25 @@ public class {{projectName}} {
}
}

static func isValidVersion(version: String) -> Bool {
static func isValidVersion(_ version: String) -> Bool {
let pattern = #"^\d{1,3}(\.\d{1,3}(\.\d{1,3})?)?$"#
return isValid(pattern: pattern, field: version)
}

static func isValidName(name: String) -> Bool {
static func isValidName(_ name: String) -> Bool {
let pattern = #"^[\w\-]{1,50}$"#
return isValid(pattern: pattern, field: name)
}

static func setName(key: String, name: String, version: String) throws {
if(!isValidName(name: name)) {
if(!isValidName(name)) {
throw ParameterError.invalidName
}

if(!isValidVersion(version: version)) {
if(!isValidVersion(version)) {
throw ParameterError.invalidVersion
}
{{projectName}}.customHeaders[key] = name + ":" + version
{{projectName}}.defaultHeaders[key] = name + ":" + version
}

public static func setSdkName(name: String, version: String) throws {
Expand All @@ -83,11 +100,10 @@ public class {{projectName}} {
public static func setApplicationName(name: String, version: String) throws {
try setName(key: "AV-Origin-App", name: name, version: version)
}

{{/useVapor}}
}{{^useVapor}}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class RequestBuilder<T> {
var credential: URLCredential?
var headers: [String: String]
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var parameters: [String: Any]?
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} let method: String
Expand Down Expand Up @@ -126,9 +142,15 @@ public class {{projectName}} {
}
return self
}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func addCredential() -> Self {
credential = {{projectName}}.credential
return self
}
}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} protocol RequestBuilderFactory {
func getNonDecodableBuilder<T>() -> RequestBuilder<T>.Type
func getBuilder<T: Decodable>() -> RequestBuilder<T>.Type
}{{/useVapor}}
func getBuilder<T: Decodable>() -> RequestBuilder<T>.Type{{#backgroundSupport}}
func getBackgroundBuilder<T: Decodable>() -> RequestBuilder<T>.Type
{{/backgroundSupport}}}{{/useVapor}}
31 changes: 0 additions & 31 deletions templates/swift5/Auth/ApiVideoCredential.mustache

This file was deleted.

6 changes: 6 additions & 0 deletions templates/swift5/Extensions.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ extension String: CodingKey {

}

extension String {
func toBase64() -> String {
return Data(self.utf8).base64EncodedString()
}
}

extension KeyedEncodingContainerProtocol {

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} mutating func encodeArray<T>(_ values: [T], forKey key: Self.Key) throws where T: Encodable {
Expand Down
28 changes: 20 additions & 8 deletions templates/swift5/Models.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,15 @@ protocol JSONEncodable {
request = nil
}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var state: Request.State {
request?.state ?? .initialized
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var isFinished: Bool {
request?.isFinished ?? false
}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var uploadProgress: Progress? {
request?.uploadProgress
}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var downloadProgress: Progress? {
request?.downloadProgress
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var progress: Progress? {
// TODO: Add upload and download progress as subprogress
let progress = Progress(totalUnitCount: (request?.uploadProgress.totalUnitCount ?? 0) + (request?.downloadProgress.totalUnitCount ?? 0))
progress.completedUnitCount = (request?.uploadProgress.completedUnitCount ?? 0) + (request?.downloadProgress.completedUnitCount ?? 0)
return progress
}
{{/useAlamofire}}
{{^useAlamofire}}
Expand All @@ -108,5 +107,18 @@ protocol JSONEncodable {
task?.cancel()
task = nil
}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var isFinished: Bool {
guard let state = task?.state else {
return false
}

return state == URLSessionTask.State.completed
}

@available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *)
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var progress: Progress? {
return task?.progress
}
{{/useAlamofire}}
}
3 changes: 0 additions & 3 deletions templates/swift5/Upload/FileChunkInputStream.mustache
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// FileChunkInputStream.swift
//

import Foundation

public class FileChunkInputStream: InputStream {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// ProgressiveUploadSessionProtocol.swift
//

import Foundation

public protocol ProgressiveUploadSessionProtocol {
Expand Down
34 changes: 11 additions & 23 deletions templates/swift5/Upload/RequestTaskQueue.mustache
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import Foundation
import Alamofire

public class RequestTaskQueue<T>: RequestTask {
private let operationQueue: OperationQueue
private var requestBuilders: [RequestBuilder<T>] = []

private let _downloadProgress = Progress(totalUnitCount: 0)
private let _uploadProgress = Progress(totalUnitCount: 0)
private let _progress = Progress(totalUnitCount: 0)

internal init(queueLabel: String) {
operationQueue = OperationQueue()
Expand All @@ -15,34 +13,20 @@ public class RequestTaskQueue<T>: RequestTask {
super.init()
}

override public var uploadProgress: Progress {
{{#useURLSession}}@available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *)
{{/useURLSession}}override public var progress: Progress {
var completedUnitCount: Int64 = 0
var totalUnitCount: Int64 = 0
requestBuilders.forEach {
if let progress = $0.requestTask.uploadProgress {
if let progress = $0.requestTask.progress {
completedUnitCount += progress.completedUnitCount
totalUnitCount += progress.totalUnitCount
}
}

_uploadProgress.totalUnitCount = totalUnitCount
_uploadProgress.completedUnitCount = completedUnitCount
return _uploadProgress
}

override public var downloadProgress: Progress {
var completedUnitCount: Int64 = 0
var totalUnitCount: Int64 = 0
requestBuilders.forEach {
if let progress = $0.requestTask.downloadProgress {
completedUnitCount += progress.completedUnitCount
totalUnitCount += progress.totalUnitCount
}
}

_downloadProgress.totalUnitCount = totalUnitCount
_downloadProgress.completedUnitCount = completedUnitCount
return _downloadProgress
_progress.totalUnitCount = totalUnitCount
_progress.completedUnitCount = completedUnitCount
return _progress
}

internal func willExecuteRequestBuilder(requestBuilder: RequestBuilder<T>) -> Void {
Expand All @@ -62,6 +46,10 @@ public class RequestTaskQueue<T>: RequestTask {
}
operationQueue.cancelAllOperations()
}

override public var isFinished: Bool {
requestBuilders.allSatisfy { $0.requestTask.isFinished }
}
}

final class RequestOperation<T>: Operation {
Expand Down
19 changes: 8 additions & 11 deletions templates/swift5/Upload/UploadChunkRequestTaskQueue.mustache
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import Foundation
import Alamofire


public class UploadChunkRequestTaskQueue: RequestTaskQueue<Video> {
private let requestBuilders: [RequestBuilder<Video>]
Expand All @@ -10,7 +8,7 @@ public class UploadChunkRequestTaskQueue: RequestTaskQueue<Video> {
private let completion: (_ data: Video?, _ error: Error?) -> Void

private var videoId: String?
private let _uploadProgress: Progress
private let _progress: Progress

private var hasSentError = false

Expand All @@ -20,7 +18,7 @@ public class UploadChunkRequestTaskQueue: RequestTaskQueue<Video> {
onProgressReady: ((Progress) -> Void)? = nil,
apiResponseQueue: DispatchQueue = {{projectName}}.apiResponseQueue,
completion: @escaping (_ data: Video?, _ error: Error?) -> Void) {
_uploadProgress = Progress(totalUnitCount: fileSize)
_progress = Progress(totalUnitCount: fileSize)
self.requestBuilders = requestBuilders
self.onProgressReady = onProgressReady
self.apiResponseQueue = apiResponseQueue
Expand Down Expand Up @@ -76,14 +74,15 @@ public class UploadChunkRequestTaskQueue: RequestTaskQueue<Video> {
requestBuilder.onProgressReady = progressReadyHook
}

override public var uploadProgress: Progress {
_uploadProgress.completedUnitCount = min(super.uploadProgress.completedUnitCount, _uploadProgress.totalUnitCount)
return _uploadProgress
{{#useURLSession}}@available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *)
{{/useURLSession}}override public var progress: Progress {
_progress.completedUnitCount = min(super.progress.completedUnitCount, _progress.totalUnitCount)
return _progress
}

private func progressReadyHook(progress: Progress) -> Void {
if let onProgressReady = onProgressReady {
onProgressReady(uploadProgress)
onProgressReady(progress)
}
}

Expand All @@ -98,9 +97,7 @@ public class UploadChunkRequestTaskQueue: RequestTaskQueue<Video> {
if (videoId == nil) {
videoId = data?.videoId
}
if (requestBuilders.allSatisfy {
$0.requestTask.state == .finished
}) {
if (isFinished) {
completion(data, nil)
}
}
Expand Down
Loading
Loading