Skip to content

refactor: update download directory to include app subfolder in deskt…#298

Merged
rainxchzed merged 2 commits into
mainfrom
desktop-download-impr
Mar 6, 2026
Merged

refactor: update download directory to include app subfolder in deskt…#298
rainxchzed merged 2 commits into
mainfrom
desktop-download-impr

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented Mar 6, 2026

…op provider

Summary by CodeRabbit

  • Improvements
    • Desktop downloads now go into a dedicated "GitHub Store Downloads" subfolder for easier file organization.
    • More robust download handling with streaming writes, explicit error detection, and stronger cancellation support.
    • Enhanced progress reporting with regular updates and guaranteed final 100% emission.
    • Partial files are cleaned up when downloads are interrupted or canceled.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 6, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 91af9bff-59ff-4bfc-96d7-8bdc0565b6ad

📥 Commits

Reviewing files that changed from the base of the PR and between 44a0da8 and 784bdc5.

📒 Files selected for processing (1)
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt

Walkthrough

Updates adjust an Android constructor type reference, refactor the JVM desktop downloader to use a streaming callbackFlow with progress reporting and robust cancellation/error handling, and change desktop download paths to use a "GitHub Store Downloads" subdirectory across platforms.

Changes

Cohort / File(s) Summary
Android type change
core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt
Constructor parameter type switched from fully-qualified zed.rainxch.core.data.services.FileLocationsProvider to unqualified FileLocationsProvider.
Desktop downloader refactor
core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt
Replaced previous flow with callbackFlow streaming download: HTTP status checking, streaming response body to file on IO dispatcher, atomic byte counting, periodic progress emissions, explicit cancellation cleanup (partial file deletion), and updated saveToFile / getDownloadedFilePath. Added related coroutine/channel imports.
Downloads directory change
core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopFileLocationsProvider.kt
userDownloadsDir() now appends a "GitHub Store Downloads" subdirectory to platform-specific downloads paths (Windows, macOS, Linux, default).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant DesktopDownloader
  participant HTTP as "HTTP Client"
  participant FS as "Filesystem"
  participant Progress as "ProgressEmitter"

  Client->>DesktopDownloader: request download(URL)
  DesktopDownloader->>HTTP: open request stream
  HTTP-->>DesktopDownloader: response stream + status
  DesktopDownloader->>FS: open FileOutputStream (IO)
  loop read chunks
    DesktopDownloader->>HTTP: read bytes
    HTTP-->>DesktopDownloader: bytes chunk
    DesktopDownloader->>FS: write chunk
    DesktopDownloader->>Progress: update bytes/count
    Progress-->>Client: trySend(progress)
  end
  DesktopDownloader->>FS: close file
  DesktopDownloader-->>Client: emit completion (file path)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰
I hop on streams and nibble bytes,
Progress beeps through quiet nights,
Folders tidy, names align,
Types fixed up — the code feels fine,
A carrot-cheer for builds that shine!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 accurately describes the main change: updating the download directory to include an app subfolder in the desktop provider, which aligns with the primary modification in DesktopFileLocationsProvider.kt.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch desktop-download-impr

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt`:
- Around line 41-44: The suggestedFileName is used raw to build File objects
(safeName -> outFile) in DesktopDownloader, allowing path traversal or absolute
paths; sanitize by extracting only the base filename (remove any directory
components like "../" or "\" and reject/strip drive letters or leading '/' or
'\' characters), disallow null/blank after stripping, and fall back to the
existing UUID-based name; then use that sanitized filename to construct
File(dir, safeName). Apply the same sanitization logic to the other occurrence
around the code handling lines 135-137 so both file creations use a safe,
basename-only filename.
- Around line 77-79: The write to the FileChannel
(fc.write(ByteBuffer.wrap(...))) may be partial, but the code treats bytesRead
as fully written and calls downloaded.addAndGet(bytesRead.toLong()); change this
by looping until the ByteBuffer has no remaining bytes (use while
(byteBuffer.hasRemaining()) { val written = fc.write(byteBuffer); /* handle 0 or
negative? */ downloaded.addAndGet(written.toLong()) }) so that you only
increment downloaded by the actual bytes written; update the logic around
byteBuffer, bytesRead, fc.write and downloaded.addAndGet to reflect the actual
written count.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fd8c1541-7b11-49c9-83d0-0793d5adc2b8

📥 Commits

Reviewing files that changed from the base of the PR and between 6bb3759 and 44a0da8.

📒 Files selected for processing (3)
  • core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopFileLocationsProvider.kt

Comment thread core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt Outdated
Comment on lines +77 to +79
val byteBuffer = ByteBuffer.wrap(buffer, 0, bytesRead)
fc.write(byteBuffer)
downloaded.addAndGet(bytesRead.toLong())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the DesktopDownloader.kt file to see the actual code around lines 77-79
cat -n core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt | head -100 | tail -40

Repository: OpenHub-Store/GitHub-Store

Length of output: 2139


🏁 Script executed:

# Also get more context - check the full method to understand the usage
wc -l core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt

Repository: OpenHub-Store/GitHub-Store

Length of output: 154


🏁 Script executed:

# Search for FileChannel write usage patterns in the file
rg -nP -C6 '\bfc\.write\s*\(' core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt

Repository: OpenHub-Store/GitHub-Store

Length of output: 847


🌐 Web query:

FileChannel.write(ByteBuffer) partial writes Java documentation

💡 Result:

FileChannel.write(ByteBuffer) follows the contract of WritableByteChannel.write(ByteBuffer).

From the WritableByteChannel.write(ByteBuffer) Javadoc:

  • It attempts to write up to r = src.remaining() bytes. The actual number written is n, where 0 ≤ n ≤ r, and on return the buffer position is advanced by n. The method returns “the number of bytes written, possibly zero.” [1]
  • “Unless otherwise specified, a write operation will return only after writing all of the r requested bytes.” It also notes that some channel types (e.g., non-blocking socket channels) may write only some or none of the bytes. [1]

From the FileChannel Javadoc for the relative write:

  • Bytes are written at the channel’s current file position (or end-of-file in append mode), the file grows if needed, and the file position is updated with “the number of bytes actually written.”
  • “Otherwise this method behaves exactly as specified by the WritableByteChannel interface.” [2]

Practical implication: even though blocking FileChannel writes are generally expected to complete the whole buffer per the “unless otherwise specified” rule, the API still allows partial writes (since n can be < r and the return value can be 0). So, if you need to be correct for all implementations/platforms, write in a loop:

while (buf.hasRemaining()) fc.write(buf);

Sources:
[1] WritableByteChannel.write(ByteBuffer) Javadoc (Oracle Java SE 11) (docs.oracle.com)
[2] FileChannel.write(ByteBuffer) Javadoc (Oracle Java SE 22) (docs.oracle.com)

Citations:


Handle partial FileChannel.write results to prevent data truncation and progress misreporting.

FileChannel.write(ByteBuffer) can write fewer bytes than requested. Although Java documentation states that blocking FileChannel writes generally complete fully, the API contract explicitly permits partial writes. The current code at lines 77-79 doesn't handle this case: it increments the downloaded progress counter by the full bytesRead amount even if fc.write() returns fewer bytes, causing inaccurate progress reporting and risking silent data truncation.

Use a while (byteBuffer.hasRemaining()) loop to ensure all bytes are written:

🛠️ Proposed fix
-                                    val byteBuffer = ByteBuffer.wrap(buffer, 0, bytesRead)
-                                    fc.write(byteBuffer)
+                                    val byteBuffer = ByteBuffer.wrap(buffer, 0, bytesRead)
+                                    while (byteBuffer.hasRemaining()) {
+                                        fc.write(byteBuffer)
+                                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt`
around lines 77 - 79, The write to the FileChannel
(fc.write(ByteBuffer.wrap(...))) may be partial, but the code treats bytesRead
as fully written and calls downloaded.addAndGet(bytesRead.toLong()); change this
by looping until the ByteBuffer has no remaining bytes (use while
(byteBuffer.hasRemaining()) { val written = fc.write(byteBuffer); /* handle 0 or
negative? */ downloaded.addAndGet(written.toLong()) }) so that you only
increment downloaded by the actual bytes written; update the logic around
byteBuffer, bytesRead, fc.write and downloaded.addAndGet to reflect the actual
written count.

@rainxchzed rainxchzed merged commit 9a4f7dc into main Mar 6, 2026
1 check was pending
@rainxchzed rainxchzed deleted the desktop-download-impr branch March 6, 2026 03:49
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