# Nothing is deleted until it is verified

Why a file is deleted from the drone, its SD card or its controller only after its archived copy matches by SHA-256, read again just before the delete.

- Published: 12 Oct 2026

Argus is our Mac app for drone footage. It brings in the videos, photos and flight records from the drone, its memory card and its controller. The card and the controller fill up. Before this work, the files on them were deleted by hand, with no proof that a safe copy existed. On 12 October 2026 we gave Argus a way to clean a connected source. It has one rule: a file leaves the source only when two safe copies exist, and only after its bytes are read again and match.

## The hash starts at the import

From a card, Argus copies each file with one copier. It reads the file in chunks of 4 MB and hashes each chunk with SHA-256 as it writes it. It writes into a new file that must not exist yet, and it flushes that file to the disk before it returns. The hash is the hash of the bytes it read. From the controller, a download cannot be hashed while it streams, so Argus reads the file from the device a second time for its digest. Argus records the hash in its manifest, next to the file's path in the library.

Every later check compares against that value.

## Two copies, not one

A sync alone is one copy. One copy is not enough to delete the original. A source file may go only when both of these hold:

- On the disk. The library copy is live in the manifest, it is on the destination disk, and it has the manifest's size.
- On Google Drive. The Drive copy is verified: Drive's SHA-256 equals the manifest's SHA-256 for that path.

The second rule is older. On 3 October we changed the step that frees space on the disk, so that it deletes only files whose Drive SHA-256 equals the one on the disk. A path that was imported again with other bytes after its upload no longer counts as backed up. The source cleanup uses the same rule.

There is a third condition. The history must be whole. If a line of the manifest cannot be read, or a history write still waits, every file stays.

(Figure: A confirmed file passes three checks before it is deleted. First it is planned again: its copy is on the disk and verified on Drive. Then its bytes on the card are read again and must hash to the manifest's SHA-256. Then the disk copy is checked once more. A file that fails any check stays, and the sheet says why.)

## A plan that says why

Argus makes a plan each time it scans a connected source. The plan has two lists: the files that may go, and the files that stay. Each file that stays has a reason. The reasons are plain: not a file that Argus imports, not imported, the library copy is not on the disk, another size than the library copy, a second file for the same library path, the history is incomplete, not verified on Google Drive.

The plan does not read the bytes on the source, because a card can hold 100 GB. The bytes are checked when the user deletes. Folders are never deleted.

The app shows a clean button on the source only when the plan has files to delete. The button opens a sheet that lists what goes and what stays, with the reason for each file. Nothing is deleted until the user presses delete in that sheet.

## Read again, just before the delete

The delete itself does not trust the plan. It holds the destination's sync lock, so no sync runs at the same time. It scans the source again and plans again from the history as it is now. A confirmed file must still pass, with the same library path and the same SHA-256.

Then Argus reads the file on the source again and hashes it. If the hash is not the manifest's hash, the file stays. On a card, Argus hashes the file. On the controller, it reads the file from the device and hashes it, as it does after a download.

Hashing a file over the controller's connection can take a long time. The library copy could leave the disk in that time. So after the hash, Argus checks the library copy on the disk once more. Only then does it delete the file.

If the device goes away during the run, nothing more can be checked. Every file that was not checked yet stays, with the disconnect as its reason. Any other failure keeps that one file and says that it could not be deleted.

## What each source allows

On a card, Argus deletes only files under the camera folder. A path with a `.` or `..` part is refused. The file is deleted directly, not moved to a trash folder on the card.

On the controller, Argus deletes only when the device reports that it can delete a file. A locked file is refused. When the device does not allow deletes, the sheet still shows the plan and tells the user to delete the files in the drone's own app.

The source cleanup and the step that frees space on the disk never run together. Each one refuses to start while the other runs. Otherwise a disk copy could leave while its source copy is being deleted.

The command line prints the same plan with `argus sources --clean --dry-run`. It does not delete. The command with `--clean` alone exits with an error.

## What we did not do

The disk copy is checked by its size, not hashed again. Its bytes were hashed at the import, and the step that frees space uses the same rule. The run also holds the sync lock while it hashes. A large cleanup on the controller therefore blocks a sync until it ends or the user stops it.

The tests use only a temporary card, destination and history. They check that only files on the disk and verified on Drive are offered, that a file whose bytes changed stays, that a file whose library copy leaves the disk during the run stays, and that the files not yet checked stay when the source goes away.

(A live demo runs here on the web page.)

