Skip to content

Download destination#129

Open
Aldo10012 wants to merge 5 commits into
mainfrom
DownloadDestination
Open

Download destination#129
Aldo10012 wants to merge 5 commits into
mainfrom
DownloadDestination

Conversation

@Aldo10012

@Aldo10012 Aldo10012 commented Mar 25, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added destination configuration for downloads: save to a temporary directory, the Documents folder with a custom filename, or specify a custom location via callback function.
  • Improvements

    • Enhanced error reporting for file operations, including new failure diagnostics for file-move errors during the download process.

@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces file destination management for downloads. A new DownloadDestination enum provides three strategies for placing downloaded files: temporary directory (with UUID-based naming), Documents folder, or custom handler-based resolution. FileDownloader now accepts a destination parameter and moves downloaded files to the configured location after completion, with move failures wrapped as a new fileMoveFailed error case.

Changes

Cohort / File(s) Summary
Error handling
Sources/EZNetworking/Error/NetworkFailureReason/DownloadFailureReason.swift
Added new enum case fileMoveFailed(underlying: SendableError) with equality logic comparing underlying NSError instances via bridging.
Download destination strategy
Sources/EZNetworking/Util/Downloader/Helpers/DownloadDestination.swift
New public enum with three destination modes (temporary, documents, custom) and moveFile(from:fileManager:) implementation handling file copying/moving based on selected strategy.
Core downloader logic
Sources/EZNetworking/Util/Downloader/FileDownloader.swift
Added destination parameter to initializer; download completion now moves file to destination and wraps move errors as fileMoveFailed failures.
Error handling tests
Tests/EZNetworkingTests/Error/NetworkFailureReason/DownloadFailureReasonTests.swift
Added equality tests for fileMoveFailed case; refactored inequality assertions using shared allDistinctCases array and nested-loop comparison.
Downloader functionality tests
Tests/EZNetworkingTests/Util/Downloader/FileDownloaderCoreFunctionalityTests.swift, FileDownloaderInvalidStateTests.swift, FileDownloaderPauseResumeTests.swift
Updated test instantiations to pass explicit destination: passthroughDestination parameter using .custom { url in url } helper.
Destination behavior tests
Tests/EZNetworkingTests/Util/Downloader/Helpers/DownloadDestinationTests.swift
New comprehensive test suite validating .temporary, .documents, and .custom destination modes with file existence, extension preservation, overwrite, and error propagation assertions.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant FileDownloader
    participant DownloadTask
    participant DownloadDestination
    participant FileManager

    Caller->>FileDownloader: init(url, destination, session)
    FileDownloader->>DownloadTask: start download

    DownloadTask->>DownloadTask: receive file at temp location
    DownloadTask->>FileDownloader: notify completion(tempURL)

    FileDownloader->>DownloadDestination: moveFile(from: tempURL)
    
    alt Move Success
        DownloadDestination->>FileManager: copy/move file
        FileManager-->>DownloadDestination: finalURL
        DownloadDestination-->>FileDownloader: finalURL
        FileDownloader->>Caller: .completed(finalURL)
    else Move Failure
        DownloadDestination->>FileDownloader: throw error
        FileDownloader->>Caller: .failed(fileMoveFailed error)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Files hop from temp to their final home,
With destinations chosen, no more to roam.
Documents or custom paths we set with care,
Each download lands precisely where it should be there! 📦✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Download destination' accurately reflects the main feature added: the new DownloadDestination enum and its integration into FileDownloader to control where downloaded files are moved.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DownloadDestination

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Mar 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.14286% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../EZNetworking/Util/Downloader/FileDownloader.swift 55.55% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Aldo10012 Aldo10012 marked this pull request as ready for review March 25, 2026 03:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/EZNetworking/Util/Downloader/FileDownloader.swift (1)

182-186: ⚠️ Potential issue | 🔴 Critical

Root cause of race condition: async dispatch in event handler.

The Task { ... } wrapper causes handleDownloadInterceptorEvent to execute asynchronously, after URLSessionDownloadDelegate.didFinishDownloadingTo returns. Since URLSession deletes the temporary file immediately after the delegate method returns, the file move at lines 200-208 will likely fail because the source file no longer exists.

Consider making the file copy/move synchronous within the delegate callback chain, or restructuring so the interceptor itself performs the copy before returning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/EZNetworking/Util/Downloader/FileDownloader.swift` around lines 182 -
186, The delegate closure currently dispatches handleDownloadInterceptorEvent
inside Task { } which defers execution until after
URLSessionDownloadDelegate.didFinishDownloadingTo returns (causing the temp file
to be removed); instead, make the interceptor invocation synchronous so file
operations happen before the delegate returns: either remove the Task wrapper
and invoke handleDownloadInterceptorEvent directly on the delegate thread (or
change the onEvent signature to be async so the delegate can await
handleDownloadInterceptorEvent inline), or move the responsibility for
copying/moving the temp file into the downloadTaskInterceptor implementation
itself so the copy happens before the interceptor returns; update the closure
around session.delegate.downloadTaskInterceptor?.onEvent and the
handleDownloadInterceptorEvent flow accordingly to ensure the file move occurs
synchronously within the delegate callback chain.
🧹 Nitpick comments (1)
Tests/EZNetworkingTests/Util/Downloader/Helpers/DownloadDestinationTests.swift (1)

137-143: Minor: Force unwraps in test helper.

The try! and ! are acceptable here since UTF-8 encoding a string literal will never fail. For improved robustness, you could use fatalError with a message, but this is fine for test code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Tests/EZNetworkingTests/Util/Downloader/Helpers/DownloadDestinationTests.swift`
around lines 137 - 143, The helper function createTempFile currently uses force
unwraps (try! and !); update it to handle failures explicitly by using do/catch
for the write operation and optional binding for content.data(using: .utf8),
calling fatalError with a clear message on failure so tests fail with useful
diagnostics; keep the same return type and behavior but replace the force
unwraps in createTempFile to provide explicit error handling and messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/EZNetworking/Util/Downloader/Helpers/DownloadDestination.swift`:
- Around line 13-39: The download temporary file is being moved asynchronously
which races with URLSession deleting it when the delegate returns; update the
flow so the file is copied/moved synchronously inside the URLSession download
delegate before returning. Concretely, in DefaultDownloadTaskInterceptor's
delegate path where it calls onEvent(.onDownloadCompleted(location)) (and where
FileDownloader currently wraps handlers in Task { ... }), perform the move/copy
using DownloadDestination.moveFile(from:) inline (or call a new synchronous
helper that copies the temp URL to a stable location) and then call onEvent with
the resulting stable URL (or pass the moved URL into the async Task) so the
system temporary file is preserved before the delegate method returns. Ensure
moveFile(from:) is invoked on the delegate thread synchronously and only after a
successful move/copy trigger further asynchronous processing.

---

Outside diff comments:
In `@Sources/EZNetworking/Util/Downloader/FileDownloader.swift`:
- Around line 182-186: The delegate closure currently dispatches
handleDownloadInterceptorEvent inside Task { } which defers execution until
after URLSessionDownloadDelegate.didFinishDownloadingTo returns (causing the
temp file to be removed); instead, make the interceptor invocation synchronous
so file operations happen before the delegate returns: either remove the Task
wrapper and invoke handleDownloadInterceptorEvent directly on the delegate
thread (or change the onEvent signature to be async so the delegate can await
handleDownloadInterceptorEvent inline), or move the responsibility for
copying/moving the temp file into the downloadTaskInterceptor implementation
itself so the copy happens before the interceptor returns; update the closure
around session.delegate.downloadTaskInterceptor?.onEvent and the
handleDownloadInterceptorEvent flow accordingly to ensure the file move occurs
synchronously within the delegate callback chain.

---

Nitpick comments:
In
`@Tests/EZNetworkingTests/Util/Downloader/Helpers/DownloadDestinationTests.swift`:
- Around line 137-143: The helper function createTempFile currently uses force
unwraps (try! and !); update it to handle failures explicitly by using do/catch
for the write operation and optional binding for content.data(using: .utf8),
calling fatalError with a clear message on failure so tests fail with useful
diagnostics; keep the same return type and behavior but replace the force
unwraps in createTempFile to provide explicit error handling and messages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a332a9cb-e99c-4c91-ae97-d20324faf270

📥 Commits

Reviewing files that changed from the base of the PR and between f099145 and 85d1b90.

📒 Files selected for processing (8)
  • Sources/EZNetworking/Error/NetworkFailureReason/DownloadFailureReason.swift
  • Sources/EZNetworking/Util/Downloader/FileDownloader.swift
  • Sources/EZNetworking/Util/Downloader/Helpers/DownloadDestination.swift
  • Tests/EZNetworkingTests/Error/NetworkFailureReason/DownloadFailureReasonTests.swift
  • Tests/EZNetworkingTests/Util/Downloader/FileDownloaderCoreFunctionalityTests.swift
  • Tests/EZNetworkingTests/Util/Downloader/FileDownloaderInvalidStateTests.swift
  • Tests/EZNetworkingTests/Util/Downloader/FileDownloaderPauseResumeTests.swift
  • Tests/EZNetworkingTests/Util/Downloader/Helpers/DownloadDestinationTests.swift

Comment on lines +13 to +39
func moveFile(from tempURL: URL, fileManager: FileManager = .default) throws -> URL {
switch self {
case .temporary:
let destination = fileManager.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(tempURL.pathExtension)
try fileManager.copyItem(at: tempURL, to: destination)
return destination

case let .documents(filename):
let documentsURL = try fileManager.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
let destination = documentsURL.appendingPathComponent(filename)
if fileManager.fileExists(atPath: destination.path) {
try fileManager.removeItem(at: destination)
}
try fileManager.moveItem(at: tempURL, to: destination)
return destination

case let .custom(handler):
return try handler(tempURL)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the async dispatch in FileDownloader's event handler setup
ast-grep --pattern $'onEvent = { $_ in
  Task { $$$
    $$$
  }
}'

Repository: Aldo10012/EZNetworking

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Check how the interceptor calls onEvent in the delegate method
rg -n -A5 'didFinishDownloadingTo.*location' --type swift

Repository: Aldo10012/EZNetworking

Length of output: 6627


🏁 Script executed:

# Find and examine the FileDownloader class
fd -t f 'FileDownloader' --exec wc -l {} \; | head -20

Repository: Aldo10012/EZNetworking

Length of output: 558


🏁 Script executed:

# Search for the setupDownloadEventHandler method mentioned in the review
rg -n 'setupDownloadEventHandler' --type swift

Repository: Aldo10012/EZNetworking

Length of output: 322


🏁 Script executed:

# Search for where onEvent is assigned/used
rg -n 'onEvent\s*=' --type swift -B2 -A5

Repository: Aldo10012/EZNetworking

Length of output: 7330


🏁 Script executed:

# Check the actual implementation of handleDownloadInterceptorEvent
rg -n 'handleDownloadInterceptorEvent' --type swift -A10

Repository: Aldo10012/EZNetworking

Length of output: 1585


🏁 Script executed:

# Get the complete handleDownloadInterceptorEvent method
rg -n 'case let .onDownloadCompleted' --type swift -A30 Sources/EZNetworking/Util/Downloader/FileDownloader.swift

Repository: Aldo10012/EZNetworking

Length of output: 1453


🏁 Script executed:

# Search for any moveFile calls in FileDownloader
rg -n 'moveFile' --type swift Sources/EZNetworking/Util/Downloader/

Repository: Aldo10012/EZNetworking

Length of output: 363


🏁 Script executed:

# Get more context around the handleDownloadInterceptorEvent implementation
sed -n '189,237p' Sources/EZNetworking/Util/Downloader/FileDownloader.swift

Repository: Aldo10012/EZNetworking

Length of output: 2005


🏁 Script executed:

# Verify URLSession's temporary file deletion behavior from Apple docs
web_search

Repository: Aldo10012/EZNetworking

Length of output: 116


🌐 Web query:

URLSessionDownloadDelegate didFinishDownloadingTo temporary file deletion behavior

💡 Result:

The temporary file provided to URLSessionDownloadDelegate's didFinishDownloadingTo(location:) method is deleted by the system after the delegate method returns if the app does not move it to a permanent location or open it for reading within the method. The official Apple documentation explicitly states: "Because the file is temporary, you must either open the file for reading or move it to a permanent location in your app's sandbox container directory before returning from this delegate method." Multiple developer reports confirm that delaying access (e.g., via async dispatch) results in "No such file or directory" errors because the file is removed promptly after the method exits.

Citations:


Critical: Race condition with URLSession's temporary file lifecycle.

The moveFile(from:) method is called asynchronously (via Task { ... } wrapping at lines 182-186 of FileDownloader.swift), but URLSession deletes the temporary file immediately when the didFinishDownloadingTo delegate method returns. According to Apple's documentation, the temporary file must be moved or accessed synchronously within the delegate method, before returning.

Since DefaultDownloadTaskInterceptor calls onEvent(.onDownloadCompleted(location)) and the event handler wraps execution in Task { @sendable [weak self] in }, the actual moveFile call (line 201) happens asynchronously after the delegate method returns and the file is deleted by the system. This will consistently fail with file-not-found errors.

Fix: Move the file synchronously within the delegate callback, or copy the temporary file to a stable location before returning from the delegate method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/EZNetworking/Util/Downloader/Helpers/DownloadDestination.swift`
around lines 13 - 39, The download temporary file is being moved asynchronously
which races with URLSession deleting it when the delegate returns; update the
flow so the file is copied/moved synchronously inside the URLSession download
delegate before returning. Concretely, in DefaultDownloadTaskInterceptor's
delegate path where it calls onEvent(.onDownloadCompleted(location)) (and where
FileDownloader currently wraps handlers in Task { ... }), perform the move/copy
using DownloadDestination.moveFile(from:) inline (or call a new synchronous
helper that copies the temp URL to a stable location) and then call onEvent with
the resulting stable URL (or pass the moved URL into the async Task) so the
system temporary file is preserved before the delegate method returns. Ensure
moveFile(from:) is invoked on the delegate thread synchronously and only after a
successful move/copy trigger further asynchronous processing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant