Caps-iOS/CapCollector/Data/Download.swift
2020-05-16 11:21:55 +02:00

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 { }