Repository navigation
Add "Replace the original" to presets: the original goes to the Recycle Bin / Trash (#332) - #481
Conversation
Off by default. For a finished result of the source's own format, the original goes to the Recycle Bin / Trash and the result takes its name and place. When the original can't go there (no Recycle Bin on the drive, a network share, a file open elsewhere, the command line), both are kept and the file's notice says why. Watch folders and files from a browser never replace. - Windows: SHFileOperation with undo, only on local fixed drives, and a file open in another program is refused before Windows is asked. - macOS: NSFileManager trashItemAtURL. - Linux: the freedesktop.org home Trash with a .trashinfo record, only on the same filesystem.
KnifeLemon
left a comment
There was a problem hiding this comment.
Thanks @cre8ive-marketing, this follows the design we agreed on in #332 closely, and the most important parts hold up. I merged it with current main and checked that a failed or cancelled conversion leaves the original alone and never touches the Trash, browser drops and watch folders don't replace anything, a different format is saved beside the original, and the option is off by default. dotnet format, the build, Core (509), Platform (50), CLI (27) and the App tests I ran (321) all pass, and all 14 language files have the new keys. The Linux .trashinfo record and the macOS call look right, and macOS CI ran the real-Trash test on both Macs.
There are some things to fix before it goes in, and the first one matters most:
1. Windows can still delete the original for good. With FOF_ALLOWUNDO | FOF_NOCONFIRMATION and no FOF_WANTNUKEWARNING, SHFileOperationW doesn't fail when a file can't be recycled: it deletes it for good without asking. The file is gone afterwards, so File.Exists(full) is false, Move returns null, and the queue moves the result in with no notice. The DriveType.Fixed check doesn't catch this:
- I tried a
substdrive, which reportsFixed.MoveToTrashreturned null and the file was gone, but the Recycle Bin's item count (SHQueryRecycleBin) didn't change. - The same happens on a fixed drive with its Recycle Bin turned off ("Don't move files to the Recycle Bin"), and with a file larger than the bin's maximum size.
Please make sure Windows only ever recycles. One way is IFileOperation with FOFX_RECYCLEONDELETE, run on an STA thread because it doesn't support MTA. Add an IFileOperationProgressSink whose PreDeleteItem returns an error unless dwFlags has TSF_DELETE_RECYCLE_IF_POSSIBLE. That cancels a delete that would be permanent, and we can return a reason. A Windows test with a subst drive would cover this, and it needs no admin rights.
2. "Keep dates" stops working with it. CopyDates(file.SourcePath, …) runs after the replace, so it reads the result's own dates. I tried a source dated 2020 with Keep dates on, and the replaced file got today's date. Please read the source's dates before it goes to the Trash.
3. Move the result next to the original before the original goes. Right now the original goes to the Trash first, and then File.Move brings the result over. When the preset saves to a folder on another drive, that move is a copy. If it fails (disk full, permissions), the original is already in the Trash. A safer order:
- Move the result to a temporary name in the original's folder.
- Then move the original to the Trash.
- Then rename the result within that folder.
That way the risky step happens while the original is still in place. Also, the result now ends up in the source's folder even when the preset says Save to → a folder. That's what "replace" means, but the hint could say so.
4. Same format, but not the same kind of file. The TXT format covers .txt, .log, .py, .bat, .command, .lua and .css (and .ps1 with #474). So a To TXT preset with this option on sends run.bat to the Trash and puts the UTF-8 run.txt in its place, named run.bat (I checked this with a test). Please also require the same extension (maybe allowing a format's aliases such as .jpeg / .jpg), or leave TXT out.
5. "Only if smaller" together with replace. When the result isn't smaller, the source is copied over the result. The original then still goes to the Trash, and the copy takes its place with the notice "Couldn't make it smaller, so it was copied as it is". Nothing changed, so please skip the replace in that case and leave the original where it is: no Trash entry and no copy.
6. Reasons in the user's language. The reason after the notice is in English in every language ("the file is open in another program"), and the user needs that part most. Please use keys, in all 14 files, for the reasons Filee works out itself:
- the drive has no Recycle Bin
- the file is open in another program
- the file is read-only
- the Trash is on another drive
- there is no Trash folder
- the command line doesn't replace originals
Keep the OS's own text (macOS localizedDescription, exception messages) as the detail.
Linux, smaller
7. MountOf matches by string prefix, so /home also matches /home2/.... Please compare with a trailing /, or use the device id from stat.
8. Suppose another program trashes a file with the same name at the same moment, so FileMode.CreateNew fails because that .trashinfo already exists. The catch then deletes the other program's record. Please delete only a record this call created.
9. Please create Trash, files and info with mode 0700 (Directory.CreateDirectory(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute)), as the spec expects. Also check free names with Path.Exists, because the Trash holds folders too.
You're right that no settings schema change is needed (it's a new bool that defaults to false). We'll adjust the Korean strings to the app's usual tone ourselves when merging, so please leave those as they are. Thanks for taking on the Trash part, it's what makes this option safe.
Command line
10. filee convert should replace originals too when the preset says so, the same as the app (watch folders and browser drops stay excluded, as agreed in #332). Could the CLI get the platform's MoveToTrash the same way AppHost does?
To get this into the next release, we'll push the remaining changes to your branch ourselves today (maintainer edits are on). Have a look at the commits when they land, and tell us if anything looks off.
…r, CLI) Review of KnifeLemon#481: 1. Windows: IFileOperation with FOFX_RECYCLEONDELETE on an STA thread and a progress sink whose PreDeleteItem refuses any delete Windows can't recycle, so a subst or network drive, a bin that is off or too small never deletes the original for good. Test with a subst drive. 2. "Keep dates" reads the source's dates before it goes to the Trash. 3. The result moves next to the source under a temporary name first, then the source goes to the Trash, then the result is renamed in that folder. When the Trash refuses, the result goes back to where it was saved. The hint says the result ends up in the original's folder. 4. TXT (scripts, logs) replaces only with the same extension. 5. "Only if smaller" that copied the source leaves the original alone, with no copy beside it (notice.original_unchanged). 6. Reasons are translated keys (trash.*) in all 14 languages; the system's own words stay as the detail. 7. Linux: a mount holds only its own folders (/home not /home2). 8. Linux: only a .trashinfo this call created is removed on failure. 9. Linux: Trash folders are created 0700; free names checked with Path.Exists. 10. filee convert replaces originals too, with the platform's Trash.
|
Thanks @KnifeLemon for the thorough checks, especially the
I left the Korean strings as they are, as you said.
|
…he app's tone One format can cover different kinds of file (PPT also .pps and .pot, AIFF also .aifc, HTML also .xhtml, TXT also scripts and logs), and the result takes the original's name. So only the same extension, or another spelling of it (.jpeg / .jpg, .tif / .tiff), replaces; the TXT-only rule becomes the general one. When the result can't take the original's name after the original went to the Trash, it goes back to the name it was saved under instead of keeping the temporary one. Korean: the new notices, reasons and hint in 해요체, like the rest of the app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ne.json and th.json: keep both the Replace-the-original strings and the OCR notice from KnifeLemon#429. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/ARCHITECTURE.md: kept the PDF finishing line and KnifeLemon#481's Replace the original paragraph, in the order JobQueue runs them.
Closes #332 (the "in place" part), built to the design @KnifeLemon approved in #332.
What it does
A new preset option, Replace the original (Settings → Presets → Output card). It is off by default.
When it is on, and a file's conversion is complete and the result has the same format as the source (for example a PNG trim → PNG):
When the original can't be moved to the Trash, both files are kept: the result is saved as a copy, as usual, and the file's notice says why. For example:
Cases where it never replaces:
WatchRule.Applyalways clears the option, otherwise the result would be converted again.Trash per OS (no new dependencies)
SHFileOperationWwithFOF_ALLOWUNDOFileShare.Nonebefore Windows is asked. Any aborted or failed operation.NSFileManager trashItemAtURL:resultingItemURL:error:localizedDescription)$XDG_DATA_HOME/Trash): writesinfo/<name>.trashinfofirst, then moves the file tofiles/IPlatformServices.MoveToTrash(path)returns null when it worked, or a short reason when it didn't. The default implementation returns a reason, so platforms without support keep originals.Changes
OutputRule.ReplaceOriginal: the new option.WatchRule.Apply: forces the option off.FileResult.NoticeDetail: the reason, shown in brackets after the notice, the same wayErrorDetailis shown.JobQueue:moveToTrash.ReplaceOriginal(...)runs after the final output is written and before the reserved names are released.AppHost: passesIPlatformServices.MoveToTrashto the queue.JobViewModel: shows the notice with its detail, usingFailureText.Reason.--json): shows the notice with its detail.presets.replace_original,presets.replace_original_hint,notice.original_kept,notice.original_in_trash) in all 14 language files.docs/ARCHITECTURE.md: a short note in the "Run" step.Tests
ReplaceOriginalTests(Core, 8 tests), using a fake Trash:notice.original_keptand the reason.Clone.TrashTests(Platform):.trashinforecord, percent-encoded paths, and unique names for a second file with the same name.The code that replaces originals is new, so please look closely at the macOS interop. I couldn't run it on a Mac myself; CI covers it.