Correct way to delete partially downloaded streams

Hello,

I have a strange issue when deleting partially downloaded HLS streams. I'm using the HLSCatalog sample as a reference and the code is confusing.

Here's a snippet:

   /// Tells the delegate that the task finished transferring data.
  func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    let userDefaults = UserDefaults.standard

    /*
     This is the ideal place to begin downloading additional media selections
     once the asset itself has finished downloading.
     */
    guard let task = task as? AVAggregateAssetDownloadTask,
      let asset = activeDownloadsMap.removeValue(forKey: task) else { return }

    guard let downloadURL = willDownloadToUrlMap.removeValue(forKey: task) else { return }

    // Prepare the basic userInfo dictionary that will be posted as part of our notification.
    var userInfo = [String: Any]()
    userInfo[Asset.Keys.name] = asset.stream.name

    if let error = error as NSError? {
      switch (error.domain, error.code) {
      case (NSURLErrorDomain, NSURLErrorCancelled):
        /*
         This task was canceled, you should perform cleanup using the
         URL saved from AVAssetDownloadDelegate.urlSession(_:assetDownloadTask:didFinishDownloadingTo:).
         */
        guard let localFileLocation = localAssetForStream(withName: asset.stream.name)?.urlAsset.url else { return }

        do {
          try FileManager.default.removeItem(at: localFileLocation)

          userDefaults.removeObject(forKey: asset.stream.name)
        } catch {
          print("An error occured trying to delete the contents on disk for \(asset.stream.name): \(error)")
        }

        userInfo[Asset.Keys.downloadState] = Asset.DownloadState.notDownloaded.rawValue

      case (NSURLErrorDomain, NSURLErrorUnknown):
        fatalError("Downloading HLS streams is not supported in the simulator.")

      default:
        fatalError("An unexpected error occured \(error.domain)")
      }
    } else {
      do {
        let bookmark = try downloadURL.bookmarkData()

        userDefaults.set(bookmark, forKey: asset.stream.name)
      } catch {
        print("Failed to create bookmarkData for download URL.")
      }

      userInfo[Asset.Keys.downloadState] = Asset.DownloadState.downloaded.rawValue
      userInfo[Asset.Keys.downloadSelectionDisplayName] = ""
    }

    NotificationCenter.default.post(name: .AssetDownloadStateChanged, object: nil, userInfo: userInfo)
  }

If the task completes, we generate a bookmark:   let bookmark = try downloadURL.bookmarkData() and we save it.

if I was to delete the stream then, it all works fine. I can go to my device's settings and I can't see it iPhone Storage. So all good.

However.... if I try to cancel the download

     task?.cancel()

Is invoked and the didCompleteWithError function is called with a cancellation error. At this point, we haven't generated a bookmark for the file, so this will always fail

       guard let localFileLocation = localAssetForStream(withName: asset.stream.name)?.urlAsset.url else { return }

        do {
          try FileManager.default.removeItem(at: localFileLocation)

But strangely, even if I try to generate a bookmark from the url location where the asset, I will get a bookmark, FileManager won't throw any errors on delete, but the partial download still remains on the device.

       let url = try URL(resolvingBookmarkData: localFileLocation,
                  bookmarkDataIsStale: &bookmarkDataIsStale)
        try FileManager.default.removeItem(at: url)

Any ideas?

Replies

The localAssetForStream function, and the mechanism of storing a bookmark in UserDefaults to track downloads is a specific implementation detail of this sample project. As you have noted, this mechanism operates only in the case where a download has completed.

In the case of a download failure, the correct approach is to delete the partial file at the URL that you know from the urlSession(_:aggregateAssetDownloadTask:willDownloadTo:) delegate method. That requires that this URL be saved somewhere when the "willDownload" delegate method is invoked, so that it can be retrieved later.

If you look carefully at the code, you'll see that the delegate method in this project already does that, saving the download URL in the willDownloadToUrlMap dictionary, and it retrieves that URL in the "didComplete" delegate method, storing it in the downloadURL local variable before removing it from the dictionary.

So, putting this together, you can just use downloadURL as the URL you delete via FileManager, when an error occurs during the download.

Hi Polyphonic, thanks so much for the quick reply.

Yes, I do just that. I store the URL from aggregateAssetDownloadTask:willDownloadTo and try to generate a bookmark from it on cancel. The only difference is, in my project I don't use UserDefaults for storage.

My sequence is -> Store the URL, report progress to the UI. On Cancel -> Cancel Task -> Receive Delegate callback -> General cleanup which also involves deleting the asset by constructing a bookmark.

      let url = try URL(resolvingBookmarkData: localURL,
                  bookmarkDataIsStale: &bookmarkDataIsStale)
        try FileManager.default.removeItem(at: url)

I very often see the partial downloads remain on the device.

You don't need a bookmark at all, since you already have the URL that the bookmark would resolve to. The only reason the sample code uses bookmarks is because they're a better way of remembering the download file across launches of your app. There's no need to create bookmarks at all. It's just the way this particular sample decided to handle its persistent file references.

Hello Polyphonic, isenwald2020, we're currently dealing with a related problem.

We're able to delete a partially downloaded stream if the download task is cancelled manually by calling task?.cancel() (exactly as described above). However, the task can also be cancelled automatically when the app is killed.

We've tried to store the url from aggregateAssetDownloadTask:willDownloadTo in UserDefaults. But if we try to delete file at that url using FileManager after the app starts again, it throws an error saying No such file or directory and the partially downloaded stream is still visible in the iPhone Storage section. By after the app starts again I mean after assetDownloadUrlSession.getAllTasks returns the pending task and task:didCompleteWithError is called with an error.

My question is can we delete partially dowloaded streams if the download task is cancelled automatically when the app is killed?

Thanks in advance for any help.

Hello @okycelt

Have you found a solution to the issue?