Skip to content

Add Downloads Monitor with opt-in auto scanning - #29

Open
andrei-chernikov wants to merge 6 commits into
Jerry23011:mainfrom
andrei-chernikov:feat/downloads-monitor-standalone
Open

andrei-chernikov wants to merge 6 commits into
Jerry23011:mainfrom
andrei-chernikov:feat/downloads-monitor-standalone

Conversation

@andrei-chernikov

Copy link
Copy Markdown
Contributor

Depends on the scan core PR #28 — this branch is built on it. Please merge that one first.

Adds a Downloads Monitor that watches a folder the user picks and scans new files as they arrive.

  • Folder access goes through a security-scoped bookmark, so the choice survives a relaunch without re-prompting. The sandbox now asks for read-only access instead of read-write, since nothing is written back.
  • Auto Scan is off by default and cannot be enabled without an explicit confirmation, because an unknown file gets uploaded to VirusTotal and spends API quota. Scanning files already in the folder is a separate, separately confirmed action.
  • Watched file categories are configurable; archives and applications are watched by default.
  • Partial downloads (.download, .crdownload, .part) are skipped until the download finishes.
  • Background Monitoring keeps the app running from the menu bar with no Dock icon, so monitoring survives closing the window.
  • Notifications carry the scanned file, so clicking one selects it.

15 tests. Czech and Simplified Chinese strings are machine translations and are marked as such in the catalog.

Scanning a large file blocked the main actor while it was hashed, could not
be stopped once started, and .app bundles could not be picked at all because
the file importers were limited to UTType.data.

- FileHasher reads the file in 4 MB chunks off the main actor and honours
  Task cancellation, so the UI stays responsive on large files instead of
  loading the whole file into memory with Data(contentsOf:).
- FilePreparation zips .app bundles into a temporary archive before upload
  and cleans the archive up afterwards.
- FileAnalysisCancellationToken carries an Alamofire request through upload
  and polling, so "cancel" actually stops the in-flight request instead of
  leaving it running in the background.
- ScanPolicy holds the upload limits, endpoint choice, and polling budget
  that were previously duplicated across the view models.
- Error.isCancellation distinguishes a user cancellation from a failure, and
  Error.displayMessageWithCode surfaces the domain and code so a failed scan
  can actually be diagnosed.
- The file importers accept UTType.item, so .app bundles can be selected.

Adds a VirusTotalTests unit test target covering the hasher, the file
preparation, the scan policy, and batch cancellation.

Czech and Simplified Chinese strings are machine translations and are marked
as such in the catalog.
@andrei-chernikov

Copy link
Copy Markdown
Contributor Author

This branch does not contain #25 or #26 — it was rebuilt on top of #28 alone, with the language-picker helper replaced by plain NSLocalizedString. Merge or drop those two in any order.

It does touch some of the same files, so whichever merges second needs a small conflict fix — both sides add adjacent lines, so keep both:

Only hard requirement is #28 first. Ping me when something conflicts and I'll rebase same day.

andrei-chernikov and others added 5 commits August 21, 2026 13:49
A file VirusTotal had never seen could be uploaded and then never reported
on. After the upload the app slept for exactly 20 seconds and asked for
/files/{sha256} once. The report does not exist until the queued analysis
finishes, so that single request answered 404, the status fell back to
"upload", and nothing scheduled another attempt. The scan sat silent while
VirusTotal was still working, and only the website showed any progress.

- uploadFile now keeps the analysis id VirusTotal returns for the upload.
- The upload path polls /analyses/{id}, the endpoint the API documents for
  this, until the status is "completed", instead of guessing a delay.
- ScanPolicy.analysisPollDecision decides finished / keep waiting / timed
  out, reusing the existing 28-attempt budget. An unknown or missing status
  counts as still running.
- A 404 from /files/{sha256} after a successful upload now enters the retry
  loop rather than ending the scan, so the report cannot be lost even if the
  analysis outlives the polling budget.

Four tests cover the decision function, including the timeout boundary.
Adds a Downloads Monitor service that watches a folder the user picks and
scans new files as they arrive.

- Folder access goes through a security-scoped bookmark, so the choice
  survives a relaunch without re-prompting. The sandbox now asks for
  read-only access to user-selected files and to Downloads instead of
  read-write, since nothing is written back.
- Auto Scan is off by default and cannot be turned on without an explicit
  confirmation, because an unknown file gets uploaded to VirusTotal and
  spends API quota. Scanning the files already in the folder is a separate,
  separately confirmed action.
- Which file categories are watched is configurable; archives and
  applications are watched by default.
- Partial downloads (.download, .crdownload, .part, ...) are skipped until
  the download finishes.
- Background Monitoring keeps the app running from the menu bar with no Dock
  icon, so monitoring survives closing the window.
- Notifications carry the scanned file, so clicking one selects that file in
  the monitor.

Czech and Simplified Chinese strings are machine translations and are marked
as such in the catalog.

Built on the scan-core branch, which supplies the hashing, file preparation
and cancellation types this monitor reuses.
The status item's first entry opens the existing main window, or restores
it if it was closed, so "New Window" described an action the app does not
perform. It now reads "Open VirusTotal" in every language.
Four pieces of Downloads Monitor UI were written as English literals, so the
catalog entries that already carried translations for them were never looked
up and the text stayed English in every language: the Scan Existing
confirmation button and its message, the empty-state description, and the
"Preparing" status.

- The three existing keys are now used at the call sites.
- Adds downloadsmonitor.status.preparing, which had no key at all.
- The file-extension examples are Text(verbatim:), since punctuation around
  a value has nothing to translate.
Hiding the window title through AppKit patched a layout problem that belongs
to the navigation structure, not to the Downloads Monitor: the sidebar and
the detail column both drive navigation, so the title is squeezed into the
sidebar titlebar area and clipped. Papering over it here made this branch
carry an unrelated fix and hid the real issue.

The title is visible again, exactly as it is on main. The underlying layout
problem is reported separately.
@andrei-chernikov
andrei-chernikov force-pushed the feat/downloads-monitor-standalone branch from 2264155 to deed662 Compare August 21, 2026 16:29
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