Skip to content

Conversation

@rainxchzed
Copy link
Owner

@rainxchzed rainxchzed commented Jan 9, 2026

This commit introduces several enhancements to the download functionality:

  • File Validation: Before starting a new download, the system now checks if the file already exists. If it does, it validates the file size against the expected size. The download is skipped if the sizes match, preventing redundant downloads.
  • Post-Download Verification: After a download completes, the size of the downloaded file is verified to ensure its integrity.
  • Automatic Cleanup: The downloads directory is now automatically cleaned of files that do not belong to the current repository's release assets.
  • Code Refactoring:
    • A new DownloadedFile data class has been created.
    • The Downloader interface and its implementations (AndroidDownloader, DesktopDownloader) have been updated to support listing downloaded files, getting file sizes, and changing the download directory from the user's public downloads to a dedicated application downloads directory.
    • The download cancellation logic in DetailsViewModel's onCleared method has been simplified.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced download management with intelligent file validation; existing downloads are reused if valid instead of being re-downloaded.
    • Automatic cleanup of downloads from other repositories to free up storage space.
  • Bug Fixes

    • Improved file integrity verification to ensure downloaded assets match expected sizes.

✏️ Tip: You can customize this high-level summary in your review settings.

This commit introduces several enhancements to the download functionality:

-   **File Validation:** Before starting a new download, the system now checks if the file already exists. If it does, it validates the file size against the expected size. The download is skipped if the sizes match, preventing redundant downloads.
-   **Post-Download Verification:** After a download completes, the size of the downloaded file is verified to ensure its integrity.
-   **Automatic Cleanup:** The downloads directory is now automatically cleaned of files that do not belong to the current repository's release assets.
-   **Code Refactoring:**
    -   A new `DownloadedFile` data class has been created.
    -   The `Downloader` interface and its implementations (`AndroidDownloader`, `DesktopDownloader`) have been updated to support listing downloaded files, getting file sizes, and changing the download directory from the user's public downloads to a dedicated application downloads directory.
    -   The download cancellation logic in `DetailsViewModel`'s `onCleared` method has been simplified.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 9, 2026

Walkthrough

This PR introduces a new DownloadedFile data model and extends the Downloader interface with query methods (listDownloadedFiles, getLatestDownload, getFileSize, getLatestDownloadForAssets) across Android and Desktop platforms. It adds pre/post-download validation to detect existing valid files and skip redundant downloads, plus cleanup logic to remove downloads from other repositories.

Changes

Cohort / File(s) Summary
Download Model
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/domain/model/DownloadedFile.kt
New data class modeling downloaded files with fileName, filePath, fileSizeBytes, and downloadedAt properties.
Downloader Interface
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Downloader.kt
Extended interface with four new suspend method signatures: listDownloadedFiles(), getLatestDownload(), getFileSize(), and getLatestDownloadForAssets() for querying downloaded files.
Platform Implementations
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidDownloader.kt, composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopDownloader.kt
Both implementations add matching query methods; Desktop also migrates from userDownloadsDir() to appDownloadsDir(). Implementations include filtering, sorting, error handling, and file size retrieval.
ViewModel & Validation
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt
Introduces pre-download validation to check for existing files and skip re-downloads if size matches, post-download verification to validate downloaded file size, and IO-dispatched cleanup to remove stale downloads from other repositories. Refactors LogResult assignments and improves job cleanup in onCleared.

Sequence Diagram

sequenceDiagram
    participant DVM as DetailsViewModel
    participant DL as Downloader
    participant FS as File System
    
    DVM->>DL: Check if file exists (getLatestDownloadForAssets)
    DL->>FS: List downloaded files
    FS-->>DL: Return file metadata
    DL-->>DVM: Return DownloadedFile or null
    
    alt File exists and valid
        DVM->>DVM: Log Downloaded state, skip download
    else File missing or invalid
        DVM->>DL: Download asset
        DL->>FS: Save to app downloads dir
        FS-->>DL: File saved
        DL-->>DVM: Return download path
        DVM->>DL: Verify file size (getFileSize)
        DL->>FS: Check file length
        FS-->>DL: Return file size
        DL-->>DVM: Return file size
        DVM->>DVM: Validate size matches expected
    end
    
    DVM->>DL: Cleanup old downloads (listDownloadedFiles)
    DL->>FS: List all downloads
    FS-->>DL: Return all downloaded files
    DL-->>DVM: Return file list
    DVM->>DVM: Filter files from other repos
    DVM->>FS: Delete stale files
    FS-->>DVM: Deletion complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Downloads now remember their past,
With files tracked and validated fast,
No re-downloads when paths are near,
Old artifacts cleaned without fear,
Kotlin's grace makes storage clear! 📦

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% 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 'feat(download): Add file validation and cleanup' accurately summarizes the main changes: introducing file validation before/after downloads and automatic cleanup of unrelated downloaded files.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link
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: 1

Caution

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

⚠️ Outside diff range comments (1)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (1)

455-472: Missing file validation in OpenInAppManager flow.

The installAsset function now includes pre-download and post-download size validation, but the OpenInAppManager action (lines 455-472) directly downloads without these checks. This could result in opening corrupted or incomplete files in AppManager.

Consider extracting the validation logic into a shared helper or applying the same pattern here.

🤖 Fix all issues with AI agents
In
@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt:
- Around line 196-215: The final log incorrectly reports all files as removed;
modify the cleanup loop in DetailsViewModel (the block using
filesToDelete.forEach and downloader.cancelDownload) to track an actual success
counter (e.g., successCount = 0), increment it only when cancelDownload returns
true, and then log "Cleanup complete - X files removed" using that successCount
instead of filesToDelete.size; keep existing per-file success/failure/error logs
unchanged so failures are still reported.
🧹 Nitpick comments (5)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/domain/model/DownloadedFile.kt (1)

1-8: Looks good; clarify downloadedAt unit (epoch millis vs seconds)

Since downloadedAt is a raw Long, consider renaming to downloadedAtEpochMillis (or add a short KDoc) to prevent unit mixups across platforms.

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Downloader.kt (1)

16-22: Tighten API contract for getFileSize(filePath) + drop inline “new method” comment

  • The // Add this new method comment (Line 20) reads like a temporary note—can be removed.
  • Consider constraining getFileSize to the app downloads domain (e.g., accept fileName or document that filePath must be within appDownloadsDir()), to avoid callers probing arbitrary filesystem paths and to keep semantics consistent with listDownloadedFiles().
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidDownloader.kt (1)

179-223: Avoid redundant withContext(IO) nesting; consider scoping getFileSize to app downloads dir

  • getLatestDownload()/getLatestDownloadForAssets() wrap withContext(IO) but immediately call listDownloadedFiles() which already does withContext(IO)—can be simplified.
  • getFileSize(filePath) currently allows arbitrary paths; if callers can pass untrusted values, validate that the canonical path is within files.appDownloadsDir().
Proposed simplification (no behavior change)
-    override suspend fun getLatestDownload(): DownloadedFile? = withContext(Dispatchers.IO) {
-        listDownloadedFiles().firstOrNull()
-    }
+    override suspend fun getLatestDownload(): DownloadedFile? =
+        listDownloadedFiles().firstOrNull()

-    override suspend fun getLatestDownloadForAssets(assetNames: List<String>): DownloadedFile? =
-        withContext(Dispatchers.IO) {
-            listDownloadedFiles()
-                .firstOrNull { downloadedFile ->
-                    assetNames.any { assetName ->
-                        downloadedFile.fileName == assetName
-                    }
-                }
-        }
+    override suspend fun getLatestDownloadForAssets(assetNames: List<String>): DownloadedFile? =
+        listDownloadedFiles()
+            .firstOrNull { downloadedFile ->
+                assetNames.any { assetName -> downloadedFile.fileName == assetName }
+            }
composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopDownloader.kt (1)

132-177: Same follow-ups as Android: de-nest withContext(IO) and scope getFileSize

  • getLatestDownload() and getLatestDownloadForAssets() can drop their own withContext(IO) since listDownloadedFiles() already handles IO.
  • Consider validating filePath (canonicalized) is under files.appDownloadsDir() (or change the API to accept fileName) to prevent arbitrary path probing.
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (1)

793-804: Consider adding file validation to downloadAsset for consistency.

The downloadAsset function lacks the pre-download and post-download validation added to installAsset. While this may be intentional for non-installable assets, adding size verification would ensure download integrity across all download paths.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87b06e7 and ffad659.

📒 Files selected for processing (5)
  • composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidDownloader.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Downloader.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/domain/model/DownloadedFile.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt
  • composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopDownloader.kt
🧰 Additional context used
🧬 Code graph analysis (2)
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidDownloader.kt (2)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Downloader.kt (1)
  • listDownloadedFiles (16-16)
composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopDownloader.kt (1)
  • listDownloadedFiles (132-148)
composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopDownloader.kt (2)
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidDownloader.kt (1)
  • listDownloadedFiles (179-195)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Downloader.kt (1)
  • listDownloadedFiles (16-16)
🔇 Additional comments (4)
composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopDownloader.kt (1)

27-29: Directory change to appDownloadsDir() is consistent with the new storage model

Nice alignment across download, saveToFile, getDownloadedFilePath, and cancelDownload, and the log message now matches the new directory.

Also applies to: 86-87, 105-106, 116-123

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (3)

550-551: LGTM!

Clean ternary expressions for differentiating update vs new install log results.


855-860: LGTM!

Proper cleanup: cancelling the job and nullifying the reference prevents dangling references.


563-617: Pre/post-download validation logic looks good.

The implementation correctly:

  • Skips redundant downloads when a valid file already exists
  • Re-downloads when file size mismatches
  • Verifies downloaded file integrity before proceeding to install

getFileSize is implemented safely across all platforms (Android and Desktop): it returns Long? (nullable), never -1. On missing files or I/O errors, it returns null, which correctly fails the size validation check at line 568 and triggers re-download as intended.

Comment on lines +196 to +215
if (filesToDelete.isNotEmpty()) {
Logger.d { "Cleaning up ${filesToDelete.size} files from other repositories" }

filesToDelete.forEach { file ->
try {
val deleted = downloader.cancelDownload(file.fileName)
if (deleted) {
Logger.d { "✓ Cleaned up file from other repo: ${file.fileName}" }
} else {
Logger.w { "✗ Failed to delete file: ${file.fileName}" }
}
} catch (e: Exception) {
Logger.e { "✗ Error deleting ${file.fileName}: ${e.message}" }
}
}

Logger.d { "Cleanup complete - ${filesToDelete.size} files removed" }
} else {
Logger.d { "No files from other repos to clean up" }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Misleading log message: claims all files removed even when some deletions fail.

The log at line 212 reports "Cleanup complete - ${filesToDelete.size} files removed", but previous iterations may have failed (logged at lines 205 and 208). Consider tracking actual success count:

Suggested fix
+                            var successCount = 0
                             filesToDelete.forEach { file ->
                                 try {
                                     val deleted = downloader.cancelDownload(file.fileName)
                                     if (deleted) {
                                         Logger.d { "✓ Cleaned up file from other repo: ${file.fileName}" }
+                                        successCount++
                                     } else {
                                         Logger.w { "✗ Failed to delete file: ${file.fileName}" }
                                     }
                                 } catch (e: Exception) {
                                     Logger.e { "✗ Error deleting ${file.fileName}: ${e.message}" }
                                 }
                             }

-                            Logger.d { "Cleanup complete - ${filesToDelete.size} files removed" }
+                            Logger.d { "Cleanup complete - $successCount/${filesToDelete.size} files removed" }
🤖 Prompt for AI Agents
In
@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt
around lines 196 - 215, The final log incorrectly reports all files as removed;
modify the cleanup loop in DetailsViewModel (the block using
filesToDelete.forEach and downloader.cancelDownload) to track an actual success
counter (e.g., successCount = 0), increment it only when cancelDownload returns
true, and then log "Cleanup complete - X files removed" using that successCount
instead of filesToDelete.size; keep existing per-file success/failure/error logs
unchanged so failures are still reported.

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.

2 participants