[Planned] New BaseDirs.jl version or Trash.jl for system-trashing

I’ve recently started work on a cross-platform approach to sending (and retrieving) files from the system trash. Linux support is roughly complete, and I’ll be tackling MacOS next. This could either take the form of a feature update to BaseDirs.jl, or I could make a new packages say Trash.jl for this.

  • Bundle with BaseDirs.jl
  • Make a new Trash.jl package
0 voters

I’m opening this thread to let people know this is happening, and give an opportunity for early feedback/comment/questions :slightly_smiling_face:

1 Like

Given that XDG does attempt to have a $XDG_DATA_HOME/Trash, BaseDirs seems reasonable.

But dealing with partition-local trashes seems tricky.

Indeed, there’s a bit too it and I’m going to need to pull in Dates (and BaseDirs if done as a separate package) as a dep for this (BaseDirs currently only depends on Base).

Thankfully the logic is well-described in the Freedesktop Trash spec, and I know the author of trash-d which has been helpful.

The most annoying part so far has been getting a pure-julia replacement for du -B1 which has culminated in finding a stat doc innacuracy.


MacOS is going to be much more of a pain than it was with locating directories, as it looks like I’ll need to hook into the Objective-C runtime that apple ships.

Alternatively, I could try blending a Swift script like GitHub - sindresorhus/macos-trash: Move files and folders to the trash with the unofficial @_cdecl attribute (ref), and then either have that built/distributed with Yggdrissil or as a Pkg Artifact.


Windows seems like it will be a similar deal to earlier, calling the Win32 API with an expected amount of suffering and “why microsoft?” involved in the development.

3 Likes

BaseDirs.jl currently has a clear and clean scope, to provide information about (and optionally create) the specific base directories mentioned. Functionality to send/retrieve files in the system trash, while related, feels like it distorts that scope and makes it less clean.

On the other hand, a new package just for this seems excessive (I assume that contributes to the decision of people who have voted against it). Instead - at the risk of even more cross-platform finagling work - perhaps there’s a more general package you could create for such operations, including things like:

  • creating and managing desktop shortcuts
  • hiding/unhiding files
  • file associations (default “Open with” programs)
  • etc.

Of course, none of these need to be present initially in the package, it can just start off with the system trash handling features. These could just be things to help decide on a package name.

FilesystemExtras.jl was the first name that popped up in my head, but these aren’t strictly filesystem operations afaict, they live on the borderlands of filesystem/OS/desktop manager. Since people often do these operations via their system file manager, perhaps FileManager.jl? (Sounds a bit too general without that context though.)

1 Like

Mmm, something weighing on my mind is that I really want BaseDirs to be small enough that it can be added on a whim to packages without worry — and hopefully before people do something worse like hardcode a particular OS’s approach or shoving everything under the julia depot.

I think it currently does that rather well, with 1.10 I see:

julia> @time using BaseDirs
  0.000083 seconds (208 allocations: 19.539 KiB)

Testing the current trash code, it “blows up” using BaseDirs to ~0.02 seconds and ~20k allocations. While this is far from large, it is markedly less lightweight.

This does push me more towards making Trash.jl. At the same time, I’m wary of scattering such functionality too thin.

TBH I half feel that it would make sense for system base directory and trashing functionality to come with Base.Filesystems, but this is probably a controversial view.

4 Likes

It occurs to me that perhaps a best of both worlds approach could be to both create this menagerie of small packages, and then perhaps a meta-package called something like OsIntegrations that simply loads them all under a single API.

I’d be keen to hear more thoughts on this (and the wider topic of how system integration should be approached by Julia. It’s nice to have our own managed .julia but I think there’s a point where it makes sense to be a “good citizen” on the OS and recognise that the computer isn’t just a Julia machine[1] :wink:).

[1] I know we have Emacs users in our community (like me :grin:) but we don’t need our own take on GitHub - a-schaefers/systemE: 🤣 A lightweight systemd replacement written in Emacs lisp 🤣 and GitHub - ch11ng/exwm: Emacs X Window Manager

4 Likes

That sounds like the completely right solution to me. It allows us to load the meta-package for general usage, while allowing developers to only add the required parts as dependencies. All without duplicating the work. Perhaps a “Filesystem” github organization is in order? I am not sure if there are enough package for an org to make sense.

2 Likes

It may be reasonable, Trash.jl has also been argued as reasonable, and I find it more so. I would never have thought to look for XDG or BaseDirs.jl related to trash. Even as a Linux users, I don’t think about those. XDG is not for e.g. Windows, I see at BaseDirs.jl:

It is essentially an implementation of the XDG (Cross-Desktop Group) directory specifications, with analogues for Windows and MacOS for cross-platform. More specifically, this is a hybrid of:

The XDG base directory and the XDG user directory specifications on Linux
The Known Folder API on Windows
The Standard Directories guidelines on macOS

I’m first now hearing of “Standard Directories guidelines”, that may or may not have to do with trash.

Update: Since I had half-complete code sitting on my drive, this weekend I thought I’d finish it off and publish it. I’ve now done so.

If all is well, I think I might as well register this in the next few weeks.

5 Likes

Here’s a demo I’ve just added to the docs that might whet people’s interest :grinning_face:

julia> write("demofile", "some content")
12

julia> trash("demofile")
TrashFile("/tmp/demofile" @ 2025-04-27)

julia> write("demofile", "more content")
12

julia> trash("demofile")
TrashFile("/tmp/demofile" @ 2025-04-27)

julia> Trash.search("demofile")
2-element Vector{TrashFile}:
 TrashFile("/tmp/demofile" @ 2025-04-27)
 TrashFile("/tmp/demofile" @ 2025-04-27)

julia> untrash("demofile", pick = :oldest)
"demofile"

julia> read("demofile", String)
"some content"

julia> untrash("demofile", force = true)
"demofile"

julia> read("demofile", String)
"more content"

From what I can tell, there is no other similarly capable cross-platform Trash library (in any language).

4 Likes

On Windows, I get a `HWND` not defined in `Trash` error during precompilation. Seems like that one got missed out among the const definitions.

Full error

Precompiling project…
✗ Trash
0 dependencies successfully precompiled in 36 seconds. 214 already precompiled.
1 dependency errored.
For a report of the errors see julia> err. To retry use pkg> precompile

julia> err
PkgPrecompileError: The following 1 direct dependency failed to precompile:

Trash

Failed to precompile Trash [2177afbf-83db-417c-8225-ea8191cd3f0f] to “C:\Users\Sundar\.julia\compiled\v1.11\Trash\jl_DC03.tmp”.
ERROR: LoadError: UndefVarError: HWND not defined in Trash
Stacktrace:
[1] top-level scope
@ :0
[2] top-level scope
@ C:\Users\Sundar.julia\packages\Trash\ojl4l\src\windows.jl:475
[3] include(mod::Module, _path::String)
@ Base .\Base.jl:557
[4] include(x::String)
@ Trash C:\Users\Sundar.julia\packages\Trash\ojl4l\src\Trash.jl:4
[5] top-level scope
@ C:\Users\Sundar.julia\packages\Trash\ojl4l\src\Trash.jl:23
[6] include
@ .\Base.jl:557 [inlined]
[7] include_package_for_output(pkg::Base.PkgId, input::String, depot_path::Vector{String}, dl_load_path::Vector{String}, load_path::Vector{String},
concrete_deps::Vector{Pair{Base.PkgId, UInt128}}, source::Nothing)
@ Base .\loading.jl:2881
[8] top-level scope
@ stdin:6
in expression starting at C:\Users\Sundar.julia\packages\Trash\ojl4l\src\windows.jl:475
in expression starting at C:\Users\Sundar.julia\packages\Trash\ojl4l\src\Trash.jl:4
in expression starting at stdin:

Ah yep, thanks for flagging that. Should be fixed now.

1 Like

Yep, installation works fine now.

list() always returns only files that have been trashed from the C: drive, whereas the system “Recycle Bin” lists all files from all drives. Doing a Trash.list(Trash.trashdir("G:/")) does return trashed files from that drive. But changing Julia’s current dir with cd("G:\\") seems to have no impact, list() still returns C:'s trashed files only. Not necessarily an issue, but at least worth documenting.

untrash’s docs do mention path (and not filename) repeatedly for the first arg, so this is mostly my bad, but it would be nice if the error messages from untrash showed the absolute path it actually looks for, instead of just the argument as is. I was giving it the filename as I saw it in the Recycle Bin, and getting confused why it was saying it’s “not present in the trash” (seems like bare filenames are treated as relative paths relative to current dir).

Looking at the code, untrash carefully selects the entry as per the user’s pick and then just seems to ignore it - I’d guess the first(candidates) in the last line is a remnant of old code, and it’s supposed to be entry there?

Yes, currently list() defaults to telling you about the trash that your homedir would use. I’m not sure if this is better/worse than the current directory. Regardless, I agree the docs could benefit from clarification.

Oh yes? That’s interesting. I imagine there’s probably an API for listing all drives… Hmm, I’m not sure what a good cross-platform-generalisable behaviour here would be.

Done :slightly_smiling_face:

Ooops! You’re right, I’ve just pushed a fix.

1 Like

@digital_carver do let me know if you happen to have any thoughts around this :slight_smile:

On Windows, it’s pretty unusual to think of the Recycle Bin as being per-drive, so that would seem like a weird default on the face of it. It’s not a dealbreaker or anything, just a factor to consider, that this will probably be the user expectation.

On the other side, even the system Recycle Bin can sometimes be pretty slow to populate the trashed files from all the drives. So one could argue that making the default be the fast “only the user homedir’s drive” version and having the user opt into the potentially much slower “all drives” version makes sense.

It looks like the default on both the freedesktop and darwin versions of the code is also to list only the homedir’s trash. That tips the balance towards that remaining as the default for me. The current behaviour seems fine, except list could maybe have an all argument for convenience? That’s false by default, but when set to true lists the trashed files from all drives/mounted devices/trash locations?

1 Like

Thanks for the thought-through reply @digital_carver! I’ve also had a bit of a look, and I’m tempted by the idea of making all system’s list() read all trashes, but I’m worried about whether this might be a bad idea given network drives etc. If there’s a way I can just check for local drives accessible to the current user, maybe that could be a good balance?

That will be the “least surprise” thing to do, I can get behind that.

On Windows, it seems like what you’re thinking of is pretty much what the Recycle Bin does, but it’s not exposed via any syscall afaict. GetLogicalDrives (and GetLogicalDriveStringsW) seems to return all drives, not just local ones. So it looks like that list has to be filtered with calls to GetDriveTypeW, keeping only specific types (DRIVE_REMOVABLE and DRIVE_FIXED?).

I’m not sure how costly GetDriveTypeW calls are, but it seems sensible to cache the results of that and not do it for every list invocation. Perhaps query this during installation and cache the results, and use the cache unless the return value of GetLogicalDrives ever changes? That seems a good heuristic - assume the device types are the same unless there has been a change in the drive letters. It’s not foolproof - someone could remove a USB drive and create a network drive at the same letter - but that should be pretty rare, and maybe there can be a keyword argument use_drive_cache that can be set to false for case like that (or just an update_drive_cache() function to call before list()).

Or maybe all of this is overthinking it, maybe GetDriveType isn’t that slow even for network drives. Perhaps worth some test code and a Slack thread asking for Windows users who have network drives attached?

As of Change list() to scan all accessible volumes · 5c440c1568 - tec/Trash.jl - Code by TEC, this should be implemented :slightly_smiling_face:

(as seems to be par for this course, it took more effort than expected)

Minor update: I’ve fixed some bugs and done a few more quality-control checks. It feels ready for registration at this stage to me :slight_smile:

If I could interest anyone in re-confirming that it works as expected on Windows/Mac, that would be brilliant!