329 lines
12 KiB
Swift
329 lines
12 KiB
Swift
//
|
|
// Download.swift
|
|
// CapCollector
|
|
//
|
|
// Created by Christoph on 26.04.20.
|
|
// Copyright © 2020 CH. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
final class Download {
|
|
|
|
let serverUrl: URL
|
|
|
|
let session: URLSession
|
|
|
|
let delegate: Delegate
|
|
|
|
private var downloadingMainImages = Set<Int>()
|
|
|
|
init(server: URL) {
|
|
let delegate = Delegate()
|
|
|
|
self.serverUrl = server
|
|
self.session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
|
|
self.delegate = delegate
|
|
}
|
|
|
|
// MARK: Paths
|
|
|
|
private static func serverDatabaseUrl(server: URL) -> URL {
|
|
server.appendingPathComponent("db.sqlite3")
|
|
}
|
|
|
|
var serverDatabaseUrl: URL {
|
|
Download.serverDatabaseUrl(server: serverUrl)
|
|
}
|
|
|
|
var serverImageUrl: URL {
|
|
serverUrl.appendingPathComponent("images")
|
|
}
|
|
|
|
private func serverImageUrl(for cap: Int, version: Int = 0) -> URL {
|
|
serverImageUrl.appendingPathComponent(String(format: "%04d/%04d-%02d.jpg", cap, cap, version))
|
|
}
|
|
|
|
private func serverImageCountUrl(for cap: Int) -> URL {
|
|
serverUrl.appendingPathComponent("count/\(cap)")
|
|
}
|
|
|
|
private var serverClassifierVersionUrl: URL {
|
|
serverUrl.appendingPathComponent("classifier.version")
|
|
}
|
|
|
|
private var serverAllCountsUrl: URL {
|
|
serverUrl.appendingPathComponent("count/all")
|
|
}
|
|
|
|
var serverRecognitionModelUrl: URL {
|
|
serverUrl.appendingPathComponent("classifier.mlmodel")
|
|
}
|
|
|
|
// MARK: Delegate
|
|
|
|
final class Delegate: NSObject, URLSessionDownloadDelegate {
|
|
|
|
typealias ProgressHandler = (_ progress: Float, _ bytesWritten: Int64, _ totalBytes: Int64) -> Void
|
|
|
|
typealias CompletionHandler = (_ url: URL?) -> Void
|
|
|
|
private var progress = [URLSessionDownloadTask : Float]()
|
|
|
|
private var callbacks = [URLSessionDownloadTask : ProgressHandler]()
|
|
|
|
private var completions = [URLSessionDownloadTask : CompletionHandler]()
|
|
|
|
func registerForProgress(_ downloadTask: URLSessionDownloadTask, callback: ProgressHandler?, completion: @escaping CompletionHandler) {
|
|
progress[downloadTask] = 0
|
|
callbacks[downloadTask] = callback
|
|
completions[downloadTask] = completion
|
|
}
|
|
|
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
|
completions[downloadTask]?(location)
|
|
callbacks[downloadTask] = nil
|
|
progress[downloadTask] = nil
|
|
completions[downloadTask] = nil
|
|
}
|
|
|
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
|
let ratio = totalBytesExpectedToWrite > 0 ? Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) : 0
|
|
progress[downloadTask] = ratio
|
|
callbacks[downloadTask]?(ratio, totalBytesWritten, totalBytesExpectedToWrite)
|
|
}
|
|
|
|
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
guard let downloadTask = task as? URLSessionDownloadTask else {
|
|
return
|
|
}
|
|
completions[downloadTask]?(nil)
|
|
callbacks[downloadTask] = nil
|
|
progress[downloadTask] = nil
|
|
completions[downloadTask] = nil
|
|
}
|
|
}
|
|
|
|
// MARK: Downloading data
|
|
|
|
/**
|
|
Download an image for a cap.
|
|
- Parameter cap: The id of the cap.
|
|
- Parameter version: The image version to download.
|
|
- Parameter completion: A closure with the resulting image
|
|
- Note: The closure will be called from the main queue.
|
|
- Returns: `true`, of the file download was started, `false`, if the image is already downloading.
|
|
*/
|
|
@discardableResult
|
|
func mainImage(for cap: Int, completion: ((_ image: UIImage?) -> Void)?) -> Bool {
|
|
let url = serverImageUrl(for: cap)
|
|
let query = "Main image of cap \(cap)"
|
|
guard !downloadingMainImages.contains(cap) else {
|
|
return false
|
|
}
|
|
downloadingMainImages.insert(cap)
|
|
|
|
let task = session.downloadTask(with: url) { fileUrl, response, error in
|
|
self.downloadingMainImages.remove(cap)
|
|
guard let fileUrl = self.convertResponse(to: query, fileUrl, response, error) else {
|
|
DispatchQueue.main.async {
|
|
completion?(nil)
|
|
}
|
|
return
|
|
}
|
|
guard app.storage.saveImage(at: fileUrl, for: cap) else {
|
|
self.log("Request '\(query)' could not move downloaded file")
|
|
DispatchQueue.main.async {
|
|
completion?(nil)
|
|
}
|
|
return
|
|
}
|
|
DispatchQueue.main.async {
|
|
guard let image = app.storage.image(for: cap) else {
|
|
self.log("Request '\(query)' received an invalid image")
|
|
completion?(nil)
|
|
return
|
|
}
|
|
completion?(image)
|
|
}
|
|
}
|
|
task.resume()
|
|
return true
|
|
}
|
|
|
|
/**
|
|
Download an image for a cap.
|
|
- Parameter cap: The id of the cap.
|
|
- Parameter version: The image version to download.
|
|
- Parameter completion: A closure with the resulting image
|
|
- Note: The closure will be called from the main queue.
|
|
- Returns: `true`, of the file download was started, `false`, if the image is already downloading.
|
|
*/
|
|
@discardableResult
|
|
func image(for cap: Int, version: Int, completion: @escaping (_ image: UIImage?) -> Void) -> Bool {
|
|
let url = serverImageUrl(for: cap, version: version)
|
|
let query = "Image of cap \(cap) version \(version)"
|
|
let task = session.downloadTask(with: url) { fileUrl, response, error in
|
|
guard let fileUrl = self.convertResponse(to: query, fileUrl, response, error) else {
|
|
DispatchQueue.main.async {
|
|
completion(nil)
|
|
}
|
|
return
|
|
}
|
|
guard app.storage.saveImage(at: fileUrl, for: cap, version: version) else {
|
|
self.log("Request '\(query)' could not move downloaded file")
|
|
DispatchQueue.main.async {
|
|
completion(nil)
|
|
}
|
|
return
|
|
}
|
|
DispatchQueue.main.async {
|
|
guard let image = app.storage.image(for: cap, version: version) else {
|
|
self.log("Request '\(query)' received an invalid image")
|
|
completion(nil)
|
|
return
|
|
}
|
|
completion(image)
|
|
}
|
|
}
|
|
task.resume()
|
|
return true
|
|
}
|
|
|
|
func imageCount(for cap: Int, completion: @escaping (_ count: Int?) -> Void) {
|
|
let url = serverImageCountUrl(for: cap)
|
|
let query = "Image count for cap \(cap)"
|
|
let task = session.dataTask(with: url) { data, response, error in
|
|
let int = self.convertIntResponse(to: query, data, response, error)
|
|
completion(int)
|
|
}
|
|
task.resume()
|
|
}
|
|
|
|
func imageCounts(completion: @escaping ([(cap: Int, count: Int)]?) -> Void) {
|
|
let url = serverAllCountsUrl
|
|
let query = "Image count of all caps"
|
|
let task = session.dataTask(with: url) { data, response, error in
|
|
guard let string = self.convertStringResponse(to: query, data, response, error) else {
|
|
completion(nil)
|
|
return
|
|
}
|
|
|
|
// Convert the encoded string into (id, count) pairs
|
|
let parts = string.components(separatedBy: ";")
|
|
let array: [(cap: Int, count: Int)] = parts.compactMap { s in
|
|
let p = s.components(separatedBy: "#")
|
|
guard p.count == 2, let cap = Int(p[0]), let count = Int(p[1]) else {
|
|
return nil
|
|
}
|
|
return (cap, count)
|
|
}
|
|
completion(array)
|
|
}
|
|
task.resume()
|
|
}
|
|
|
|
func databaseSize(completion: @escaping (_ size: Int64?) -> Void) {
|
|
size(of: "database size", to: serverDatabaseUrl, completion: completion)
|
|
}
|
|
func database(progress: Delegate.ProgressHandler? = nil, completion: @escaping (URL?) -> Void) {
|
|
//let query = "Download of server database"
|
|
let task = session.downloadTask(with: serverDatabaseUrl)
|
|
delegate.registerForProgress(task, callback: progress) {url in
|
|
self.log("Database download complete")
|
|
completion(url)
|
|
}
|
|
task.resume()
|
|
}
|
|
|
|
|
|
func classifierVersion(completion: @escaping (Int?) -> Void) {
|
|
let query = "Server classifier version"
|
|
let task = session.dataTask(with: serverClassifierVersionUrl) { data, response, error in
|
|
let int = self.convertIntResponse(to: query, data, response, error)
|
|
completion(int)
|
|
}
|
|
task.resume()
|
|
}
|
|
|
|
func classifierSize(completion: @escaping (Int64?) -> Void) {
|
|
size(of: "classifier size", to: serverRecognitionModelUrl, completion: completion)
|
|
}
|
|
|
|
func classifier(progress: Delegate.ProgressHandler? = nil, completion: @escaping (URL?) -> Void) {
|
|
let task = session.downloadTask(with: serverRecognitionModelUrl)
|
|
delegate.registerForProgress(task, callback: progress) { url in
|
|
self.log("Classifier download complete")
|
|
completion(url)
|
|
}
|
|
task.resume()
|
|
}
|
|
|
|
// MARK: Requests
|
|
|
|
private func size(of query: String, to url: URL, completion: @escaping (_ size: Int64?) -> Void) {
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "HEAD"
|
|
let task = session.dataTask(with: request) { _, response, _ in
|
|
guard let r = response else {
|
|
self.log("Request '\(query)' received no response")
|
|
completion(nil)
|
|
return
|
|
}
|
|
completion(r.expectedContentLength)
|
|
}
|
|
task.resume()
|
|
}
|
|
|
|
private func convertIntResponse(to query: String, _ data: Data?, _ response: URLResponse?, _ error: Error?) -> Int? {
|
|
guard let string = self.convertStringResponse(to: query, data, response, error) else {
|
|
return nil
|
|
}
|
|
guard let int = Int(string.trimmingCharacters(in: .whitespacesAndNewlines)) else {
|
|
self.log("Request '\(query)' received an invalid value '\(string)'")
|
|
return nil
|
|
}
|
|
return int
|
|
}
|
|
|
|
private func convertStringResponse(to query: String, _ data: Data?, _ response: URLResponse?, _ error: Error?) -> String? {
|
|
guard let data = self.convertResponse(to: query, data, response, error) else {
|
|
return nil
|
|
}
|
|
guard let string = String(data: data, encoding: .utf8) else {
|
|
self.log("Request '\(query)' received invalid data (not a string)")
|
|
return nil
|
|
}
|
|
return string
|
|
}
|
|
|
|
private func convertResponse<T>(to query: String, _ result: T?, _ response: URLResponse?, _ error: Error?) -> T? {
|
|
if let error = error {
|
|
log("Request '\(query)' produced an error: \(error)")
|
|
return nil
|
|
}
|
|
guard let response = response else {
|
|
log("Request '\(query)' received no response")
|
|
return nil
|
|
}
|
|
guard let urlResponse = response as? HTTPURLResponse else {
|
|
log("Request '\(query)' received an invalid response: \(response)")
|
|
return nil
|
|
}
|
|
guard urlResponse.statusCode == 200 else {
|
|
log("Request '\(query)' failed with status code \(urlResponse.statusCode)")
|
|
return nil
|
|
}
|
|
guard let r = result else {
|
|
log("Request '\(query)' received no item")
|
|
return nil
|
|
}
|
|
return r
|
|
}
|
|
|
|
|
|
}
|
|
|
|
extension Download: Logger { }
|