-2

I have an API that generates signed download links that expire after a short amount of time. I'd like to add the ability to resume downloads, but the URLSession APIs don't provide the native ability to resume downloads if the URL for the asset changes.

My attempt at solving this was to track the bytes downloaded at the time of pausing, store the data blob that was downloaded, fetch a new signed download url, resume downloading using Range headers, and then concatenate all the data blobs together when the download is completed.

Here's the code used to start the download:

let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
let task = session.downloadTask(with: signedURL)
self.sessionDownloadRequest = task

The problem that I am facing is that the resume data var doesn't appear to actually contain the data that was downloaded.

self.sessionDownloadRequest.cancel(byProducingResumeData: { (data) in 
    print(data.count) //This surprisingly always returns the same count
}

It appears that the size of that data blob is always the same regardless of how long I let the download continue for before pausing. Where/How can I access the chunk of data that was downloaded?

Thanks!

neilb
  • 120
  • 1
  • 2
  • 11
  • You haven't shown any of the relevant code, so who knows what you're doing, right or wrong? – matt Sep 17 '17 at 21:56
  • @matt, I added the initial request code. Just to clarify, I'm not having trouble getting a new signed URL, resuming the download with a specific range, or with the concatenation of data blobs so I didn't include those bits of code to keep the question concise. Let me know if there's anything else you'd like to see though. – neilb Sep 18 '17 at 17:56

1 Answers1

1

The resume data that is returned by:

- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler;

is actually a plist that includes:

  • NSURLSessionDownloadURL
  • NSURLSessionResumeBytesReceived
  • NSURLSessionResumeCurrentRequest
  • NSURLSessionResumeEntityTag
  • NSURLSessionResumeInfoTempFileName
  • NSURLSessionResumeInfoVersion
  • NSURLSessionResumeOriginalRequest
  • NSURLSessionResumeServerDownloadDate

You can access the plist with the following code:

if let resumeDictionary = try? PropertyListSerialization.propertyList(from: self, options: PropertyListSerialization.MutabilityOptions.mutableContainersAndLeaves, format: nil), let plist = resumeDictionary as? [String: Any] {
        print(plist)
}

You don't actually need to store and concatenate the data blobs as you initially suggested. You can replace the current request stored in the plist (NSURLSessionResumeCurrentRequest) with a new one with your updated signed URL. After this, create a new resumeData instance to use instead of the original.

 guard let bytesReceived = plist["NSURLSessionResumeBytesReceived"] as? Int
        else {
            return nil
 }
 let headers = ["Range":"bytes=\(bytesReceived)"]
 let newReq = try! URLRequest(url: signedURL, method: .get, headers: headers)             
 let archivedData = NSKeyedArchiver.archivedData(withRootObject: newReq)

 if let updatedResumeData = try? PropertyListSerialization.data(fromPropertyList: plist, format: PropertyListSerialization.PropertyListFormat.binary, options: 0) {
            return updatedResumeData
 }   

From there you can manipulate the plist and actually create a new one to pass it thru to the instance method:

- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;

NOTE: If you are working with iOS 10 and macOS10.12.*, there is a bug that prevents the resume ability to work as the plist is corrupted. Check this article out for a fix. You may need to fix the plist before accessing certain properties on it. Resume NSUrlSession on iOS10

neilb
  • 120
  • 1
  • 2
  • 11