Welcome to my YOLO series, where I'll be showcasing simple tools and projects that I've built—sometimes for fun, sometimes to solve specific problems, and other times just out of pure curiosity. The goal here isn't just to present a tool; I'll also dive into something interesting related to the process, whether it's a technical insight or a lesson learned while crafting these little experiments.
Nobody asked for it, and nobody want it—but here it is anyway. Meet rrm, a tool that solves a problem only I seem to have (but hey, it might be a Layer 8 issue—or, more likely, a skill issue!).
rrm adds a layer of safety to your command-line experience by moving files to a trash bin instead of permanently deleting them. With a customizable grace period, you get the chance to realize, "Oops, I actually needed that!" before it’s too late.
What’s more, rrm doesn’t rely on external configuration files or tracking systems to manage deleted files. Instead, it leverages your filesystem’s extended attributes to store essential metadata—like the original file path and deletion time—directly within the trashed item.
You might be wondering, "Why am I building this tool when there are similar, possibly better tools out there?" Well, the answer is simple:
Fun note: While working with std::Path, I found an example in the Rust standard library that uses a folder named laputa
. I know it's a reference to Castle in the Sky, but for Spanish speakers, it’s also a curse word, which made it a funny moment for me!<script> // Detect dark theme var iframe = document.getElementById('tweet-1844834987184410735-190'); if (document.body.className.includes('dark-theme')) { iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1844834987184410735&theme=dark" } </script>When I started building rrm, I needed a way to track the original path of deleted files and the time when they should be permanently removed. I didn’t want to use a JSON file or implement a weird naming format that includes this information—especially if I wanted to store more data later. A database felt like overkill for such a small task.
That’s when I discovered extended attributes.
Now, I don’t know about you, but I didn’t realize there was a built-in mechanism that lets you add custom metadata to files, which is supported by most Linux filesystems and Unix-like systems such as macOS. This feature is called Extended File Attributes. Different systems have their own limitations—like how much data can be added or the specific namespaces used—but they do allow you to store user-defined metadata.
Extended attributes are essentially name:value pairs that are permanently associated with files and directories. As I mentioned earlier, systems differ in how they handle this. For example, in Linux, the name starts with a namespace identifier. There are four such namespaces: security, system, trusted, and user. In Linux, the name starts with one of these, followed by a dot (".") and then a null-terminated string. On macOS, things are a bit different. macOS doesn't require namespaces at all, thanks to its Unified Metadata Approach, which treats extended attributes as additional metadata directly tied to files without needing to be categorized.
In this tiny CLI, I’m using the crate xattr, which supports both Linux and macOS. Regarding the namespaces I mentioned earlier for Linux, we'll be focusing on the user namespace since these attributes are meant to be used by the user. So, in the code, you’ll see something like this:
/// Namespace for extended attributes (xattrs) on macOS and other operating systems. /// On macOS, this is an empty string, while on other operating systems, it is "user.". #[cfg(target_os = "macos")] const XATTR_NAMESPACE: &str = ""; #[cfg(not(target_os = "macos"))] const XATTR_NAMESPACE: &str = "user."; ... fn set_attr(&self, path: &Path, attr: &str, value: &str) -> Result<()> { let attr_name = format!("{}{}", XATTR_NAMESPACE, attr); ... }
The #[cfg(target_os = "macos")] attribute in Rust is used to conditionally compile code based on the target operating system. In this case, it ensures that the code block is only included when compiling for macOS. This is relevant because, as mentioned earlier, macOS doesn’t require a namespace for extended attributes, so the XATTR_NAMESPACE is set to an empty string. For other operating systems, the namespace is set to "user.". This conditional compilation allows the code to adapt seamlessly across different platforms, making the CLI cross-compatible with both Linux and macOS.
One thing I found pretty cool about extended attributes is that they don’t modify the file itself. The metadata lives in a separate disk space, referenced by the inode. This means the file's actual contents remain unchanged. For example, if we create a simple file and use shasum to get its checksum:
The inode (index node) is a data structure in a Unix-style file system that describes a file-system object such as a file or a directory. Link
/// Namespace for extended attributes (xattrs) on macOS and other operating systems. /// On macOS, this is an empty string, while on other operating systems, it is "user.". #[cfg(target_os = "macos")] const XATTR_NAMESPACE: &str = ""; #[cfg(not(target_os = "macos"))] const XATTR_NAMESPACE: &str = "user."; ... fn set_attr(&self, path: &Path, attr: &str, value: &str) -> Result<()> { let attr_name = format!("{}{}", XATTR_NAMESPACE, attr); ... }
After using rrm to delete the file, we can list the deleted files and see that the file has been moved to the trash bin with its metadata intact:
$ cat a.txt https://www.kungfudev.com/ $ shasum a.txt e4c51607d5e7494143ffa5a20b73aedd4bc5ceb5 a.txt
As you can see, the file name is changed to a UUID. This is done to avoid name collisions when deleting files with the same name. By assigning a unique identifier to each file, rrm ensures that every deleted file, even if they have identical names, can be tracked and recovered without any issues.
We can navigate to the trash folder and inspect the file to confirm that its contents remain unchanged:
$ rrm rm a.txt $ rrm list ╭──────────────────────────────────────────────────────┬──────────────────────────────────────┬──────┬─────────────────────╮ │ Original Path ┆ ID ┆ Kind ┆ Deletion Date │ ╞══════════════════════════════════════════════════════╪══════════════════════════════════════╪══════╪═════════════════════╡ │ /Users/douglasmakey/workdir/personal/kungfudev/a.txt ┆ 3f566788-75dc-4674-b069-0faeaa86aa55 ┆ File ┆ 2024-10-27 04:10:19 │ ╰──────────────────────────────────────────────────────┴──────────────────────────────────────┴──────┴─────────────────────╯
Additionally, by using xattr on macOS, we can verify that the file has its metadata, such as the deletion date and original path:
$ shasum 3f566788-75dc-4674-b069-0faeaa86aa55 e4c51607d5e7494143ffa5a20b73aedd4bc5ceb5 3f566788-75dc-4674-b069-0faeaa86aa55
You can imagine the range of potential use cases for simple validations or actions using this metadata. Since extended attributes work without modifying the file itself, they allow you to check file integrity or perform other operations without affecting the original content.
This is just a small introduction to extended attributes and how they’re used in this project. It’s not meant to be an in-depth explanation, but if you’re interested in learning more, there are plenty of detailed resources out there. Here are a couple of links to the most useful and well-described resources on the topic:
I’ve spent a few years working with Go, and I’ve become fond of certain patterns—mocking being one of them. In Go, I typically implement things myself if it avoids unnecessary imports or gives me more flexibility. I’m so used to this approach that when I started writing tests in Rust, I found myself preferring to manually mock certain things, like creating mock implementations of traits.
For example, in this tiny CLI, I created a trait to decouple the trash manager from the way it interacts with the extended attributes. The trait, named ExtendedAttributes, was initially intended for testing purposes, but also because I wasn’t sure whether I would use xattr or another implementation. So, I defined the following trait:
$ xattr -l 3f566788-75dc-4674-b069-0faeaa86aa55 deletion_date: 2024-10-27T04:10:19.875614+00:00 original_path: /Users/douglasmakey/workdir/personal/kungfudev/a.txt
In Go, I would create something like the following, which provides a simple implementation of the previously mentioned interface. The code below is straightforward and generated without much consideration, just for the sake of example:
/// Namespace for extended attributes (xattrs) on macOS and other operating systems. /// On macOS, this is an empty string, while on other operating systems, it is "user.". #[cfg(target_os = "macos")] const XATTR_NAMESPACE: &str = ""; #[cfg(not(target_os = "macos"))] const XATTR_NAMESPACE: &str = "user."; ... fn set_attr(&self, path: &Path, attr: &str, value: &str) -> Result<()> { let attr_name = format!("{}{}", XATTR_NAMESPACE, attr); ... }
Then, I would use my mock and inject the specific behavior needed for each test. Again, this is simple code just for the sake of the example:
$ cat a.txt https://www.kungfudev.com/ $ shasum a.txt e4c51607d5e7494143ffa5a20b73aedd4bc5ceb5 a.txt
I've gotten used to this pattern in Go, and I plan to keep using it. But I’ve also been doing something similar in Rust. For this project, I decided to try the mockall crate, and I found it really useful.
First, I used the mock! macro to manually mock my structure. I know mockall has an automock feature, but I prefer to define the mock struct directly in my tests where it will be used. Let me know if this is something common or if the community has a different standard for this.
$ rrm rm a.txt $ rrm list ╭──────────────────────────────────────────────────────┬──────────────────────────────────────┬──────┬─────────────────────╮ │ Original Path ┆ ID ┆ Kind ┆ Deletion Date │ ╞══════════════════════════════════════════════════════╪══════════════════════════════════════╪══════╪═════════════════════╡ │ /Users/douglasmakey/workdir/personal/kungfudev/a.txt ┆ 3f566788-75dc-4674-b069-0faeaa86aa55 ┆ File ┆ 2024-10-27 04:10:19 │ ╰──────────────────────────────────────────────────────┴──────────────────────────────────────┴──────┴─────────────────────╯
I found mockall really useful, allowing me to inject specific behaviors into my tests without the verbosity of my old pattern.
$ shasum 3f566788-75dc-4674-b069-0faeaa86aa55 e4c51607d5e7494143ffa5a20b73aedd4bc5ceb5 3f566788-75dc-4674-b069-0faeaa86aa55
As we can see, mockall gives us the capability to define specific behaviors for our tests using its mock methods:
Some of you might find this super basic or not that interesting, but as I mentioned, in this YOLO series, I’m sharing things that I find interesting or just want to talk about. I wasn’t a big fan of using this kind of library in Go, partly due to Go’s constraints, but in Rust, I found mockall really useful. It even reminded me of my old days with Python.
Again, this section wasn’t meant to explain mocking in Rust or mockall. I’m sure there are plenty of great resources that cover it in detail. I just wanted to mention it briefly.
In this post, I’ve shared some of the reasoning behind building rrm and the tools I used along the way. From using extended attributes to simplify metadata handling to experimenting with the mockall crate for testing in Rust, these were just things that piqued my interest.
The goal of this YOLO series is to highlight the fun and learning that comes with building even simple tools. I hope you found something useful here, and I look forward to sharing more projects and insights in future posts. As always, feedback is welcome!
Happy coding!
The above is the detailed content of Playing with Rust: Building a Safer rm and Having Fun Along the Way. For more information, please follow other related articles on the PHP Chinese website!