JohnCoates/Aerial

View on GitHub
Aerial/Source/Models/Downloads/DownloadManager.swift

Summary

Maintainability
A
1 hr
Test Coverage
//
//  DownloadManager.swift
//  Aerial
//
//  Created by Guillaume Louel on 03/10/2018.
//  Copyright © 2018 John Coates. All rights reserved.

import Cocoa

/// Manager of asynchronous download `Operation` objects

final class DownloadManager: NSObject {

    /// Dictionary of operations, keyed by the `taskIdentifier` of the `URLSessionTask`

    fileprivate var operations = [Int: DownloadOperation]()

    /// Serial OperationQueue for downloads

    private let queue: OperationQueue = {
        let operationQueue = OperationQueue()
        operationQueue.name = "download"
        operationQueue.maxConcurrentOperationCount = 3
        return operationQueue
    }()

    /// Delegate-based `URLSession` for DownloadManager

    lazy var session: URLSession = {
        let configuration = URLSessionConfiguration.default
        return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
    }()

    /// Add download
    ///
    /// - parameter URL:  The URL of the file to be downloaded
    ///          folder:  The name of the subfolder where the file will be stored
    ///
    /// - returns:        The DownloadOperation of the operation that was queued

    @discardableResult
    func queueDownload(_ url: URL, folder: String) -> DownloadOperation {
        let operation = DownloadOperation(session: session, url: url, folder: folder)
        operations[operation.task.taskIdentifier] = operation
        queue.addOperation(operation)
        return operation
    }

    /// Cancel all queued operations

    func cancelAll() {
        queue.cancelAllOperations()
    }

}

// MARK: URLSessionDownloadDelegate methods

extension DownloadManager: URLSessionDownloadDelegate {

    func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didFinishDownloadingTo location: URL
    ) {
        operations[downloadTask.taskIdentifier]?.urlSession(session,
                                                            downloadTask: downloadTask,
                                                            didFinishDownloadingTo: location)
    }

    func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didWriteData bytesWritten: Int64,
        totalBytesWritten: Int64,
        totalBytesExpectedToWrite: Int64
    ) {
        operations[downloadTask.taskIdentifier]?.urlSession(session,
                                                            downloadTask: downloadTask,
                                                            didWriteData: bytesWritten,
                                                            totalBytesWritten: totalBytesWritten,
                                                            totalBytesExpectedToWrite: totalBytesExpectedToWrite)
    }
}

// MARK: URLSessionTaskDelegate methods

extension DownloadManager: URLSessionTaskDelegate {

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        let key = task.taskIdentifier
        operations[key]?.urlSession(session, task: task, didCompleteWithError: error)
        operations.removeValue(forKey: key)
    }

}

/// Asynchronous Operation subclass for downloading

final class DownloadOperation: AsynchronousOperation {
    let task: URLSessionTask
    let folder: String

    init(session: URLSession, url: URL, folder: String) {
        self.folder = folder
        task = session.downloadTask(with: url)
        super.init()
    }

    override func cancel() {
        task.cancel()
        super.cancel()
    }

    override func main() {
        task.resume()
    }
}

// MARK: NSURLSessionDownloadDelegate methods
//       Customized for our usage
extension DownloadOperation: URLSessionDownloadDelegate {
    // This is where we save the file to its location
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        do {
            // We may need to create our destination
            let destinationDirectory = Cache.supportPath.appending("/" + folder)
            FileHelpers.createDirectory(atPath: destinationDirectory)

            let manager = FileManager.default
            let supportURL = URL(fileURLWithPath: Cache.supportPath.appending("/" + folder))

            debugLog("Caching \(downloadTask.originalRequest!.url!.lastPathComponent) at \(folder)")

            // The file may exist, remove it
            try? manager.removeItem(at: supportURL.appendingPathComponent(
                                        downloadTask.originalRequest!.url!.lastPathComponent))

            // Finally move the file
            try manager.moveItem(at: location, to: supportURL.appendingPathComponent(
                                    downloadTask.originalRequest!.url!.lastPathComponent))
        } catch {
            errorLog("\(error)")
        }
    }

    func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didWriteData bytesWritten: Int64,
        totalBytesWritten: Int64,
        totalBytesExpectedToWrite: Int64
    ) {
        // let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
        // print("\(downloadTask.originalRequest!.url!.absoluteString) \(progress)")
    }
}

// MARK: URLSessionTaskDelegate methods

extension DownloadOperation: URLSessionTaskDelegate {

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        defer { finish() }

        if let error = error {
            errorLog("\(error)")
            return
        }

        let destinationDirectory = Cache.supportPath.appending("/" + folder)

        // Some manifests come in tar form, in that case untar them here
        if folder == "tvOS 13" {
            FileHelpers.unTar(file: destinationDirectory.appending("/resources-13.tar"), atPath: destinationDirectory)
        } else if folder == "tvOS 16" {
            FileHelpers.unTar(file: destinationDirectory.appending("/resources-16.tar"), atPath: destinationDirectory)
        } else if folder == "tvOS 12" {
            FileHelpers.unTar(file: destinationDirectory.appending("/resources.tar"), atPath: destinationDirectory)
        } else if folder == "macOS 14" {
            FileHelpers.unTar(file: destinationDirectory.appending("/resources-14-0-10.tar"), atPath: destinationDirectory)
        }

        debugLog("Finished downloading \(task.originalRequest!.url!.absoluteString)")
    }
}