This commit is contained in:
2026-05-09 16:05:17 -05:00
commit d6591c9d0b
9 changed files with 1895 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/target
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/drawer.iml" filepath="$PROJECT_DIR$/.idea/drawer.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+86
View File
@@ -0,0 +1,86 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project
`drawer` is a Rust project (edition 2024). Entry point is `src/main.rs`.
## Dependencies
- `tar` — reading and writing tarballs in memory
- `flate2` — gzip compression, paired with `tar`
- `age` — encryption/decryption using SSH keys (RSA or Ed25519) loaded from disk
- `clap` — command line argument parsing (derive API), used for subcommands and flags
`drawer` is a command line tool for managing projects.
A drawer is a single directory with related files in for a project. Drawer files always end
in the `.drawer` extension, like `foo.drawer`. To create a drawer file from a directory, we
first tar all files in the directory, and then encrypt the resulting tarball. To open a drawer
file, we decrypt it and expand the tarball to a directory.
The `drawer` tool has two subcommands, `drawer open` and `drawer close`. Each of these commands
requires a key, the path to which can be given by either the `$DRAWER_KEY` environment variable
or a `-i` command line parameter.
### Open subcommand
The `drawer open` subcommand will decrypt and untar a drawer file into a path. It takes two arguments,
the name of a drawer file (required) and a path to expand it into (optional, defaults to a directory
under the current directory named the same as the drawer file). If the target path exists and is not
empty, print an error message, unless the `-f` flag is passed.
### Close subcommand
The `drawer close` subcommand will create a drawer file from a directory and a key. The command
takes two arguments, the name of a drawer file (required) and the path to a directory (optional,
defaults to the current directory). If the target drawer file is inside the directory, print an
error message saying so. Otherwise, create a tarball (in memory) of the directory contents, encrypt
them (again in memory), and write the drawer file to disk.
### Examples:
- `drawer close -i ~/.keys/mykey ~/Documents/blah.drawer ~/Documents/blah` (create a file, `~/Documents/blah.drawer`,
encrypted with `~/.keys/mykey`, containing the contents of `~/Documents/blah`)
- `drawer open ~/Documents/blah.drawer` (open `~/Documents/blah.drawer` into `./blah` using a key found at `$DRAWER_KEY`)
## Implementation plan
The implementation of this project will happen in several phases. Claude will do the primary coding for each phase, then
the user (me) will review the code, write tests, and possibly make changes, before proceeding with the next phase.
At to time should Claude delete or modify any tests whatsoever.
### Phase 1
Write code that uses `clap` to parse the command line arguments and create a `DrawerOperation` struct from them. The
`DrawerOperation` struct will contain:
- An `operation_type` (`OperationType`, enum of `Open` or `Close`)
- A `drawer_file` (`Path` for the drawer file to open or close)
- A `key_path` (`Path` to the key we'll use, which is either the value of the `-i` flag or the value of the `$DRAWER_KEY`
env var)
- A `target_path` (`Path` to the contents we're going to put into or take out of the drawer)
- A `force` (bool for whether or not `-f` was passed)
In order for this to be testable, it needs to be a function that returns a `Result<DrawerOperation, DrawerError>`. The
main function should call that and then do nothing with the result.
Certain things should cause the function to return `Err`:
- Not passing a drawer file
- Passing a subcommand other than `open` or `close`
- Passing any flags but `-f` or `-i`
- Not passing `-i` when the `$DRAWER_KEY` environment variable is undefined
## Commands
```bash
cargo build # compile
cargo run # build and run
cargo test # run all tests
cargo test <name> # run a single test by name
cargo clippy # lint
cargo fmt # format
```
Generated
+1617
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "drawer"
version = "0.1.0"
edition = "2024"
[dependencies]
age = { version = "0.11.3", features = ["ssh"] }
clap = { version = "4.6.1", features = ["derive"] }
flate2 = "1.1.9"
tar = "0.4.45"
+148
View File
@@ -0,0 +1,148 @@
use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum OperationType {
Open,
Close,
}
#[derive(Debug, PartialEq)]
pub struct DrawerOperation {
pub operation_type: OperationType,
pub drawer_file: PathBuf,
pub key_path: PathBuf,
pub target_path: PathBuf,
pub force: bool,
}
#[derive(Debug, PartialEq)]
pub enum DrawerError {
ParseError(String),
NoKeyPath,
}
#[derive(Parser)]
#[command(name = "drawer")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Open {
drawer_file: PathBuf,
target_path: Option<PathBuf>,
#[arg(short = 'i')]
key: Option<PathBuf>,
#[arg(short = 'f')]
force: bool,
},
Close {
drawer_file: PathBuf,
target_path: Option<PathBuf>,
#[arg(short = 'i')]
key: Option<PathBuf>,
},
}
pub fn parse_args<I, T>(args: I) -> Result<DrawerOperation, DrawerError>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let cli = Cli::try_parse_from(args)
.map_err(|e| DrawerError::ParseError(e.to_string()))?;
match cli.command {
Command::Open { drawer_file, target_path, key, force } => {
let key_path = resolve_key(key)?;
let target = target_path.unwrap_or_else(|| {
PathBuf::from(drawer_file.file_stem().unwrap_or_default())
});
Ok(DrawerOperation {
operation_type: OperationType::Open,
drawer_file,
key_path,
target_path: target,
force,
})
}
Command::Close { drawer_file, target_path, key } => {
let key_path = resolve_key(key)?;
let target = target_path.unwrap_or_else(|| PathBuf::from("."));
Ok(DrawerOperation {
operation_type: OperationType::Close,
drawer_file,
key_path,
target_path: target,
force: false,
})
}
}
}
fn resolve_key(key: Option<PathBuf>) -> Result<PathBuf, DrawerError> {
if let Some(k) = key {
return Ok(k);
}
std::env::var("DRAWER_KEY")
.map(PathBuf::from)
.map_err(|_| DrawerError::NoKeyPath)
}
fn main() {
let _ = parse_args(std::env::args_os());
}
#[cfg(test)]
mod tests {
use crate::OperationType::*;
use super::*;
#[test]
fn test_basic_close() {
let op = parse_args(vec!["drawer", "close", "blah.drawer", "-i", "foo.key", "blah"]);
assert_eq!(op, Ok(
DrawerOperation {
operation_type: Close,
drawer_file: "blah.drawer".into(),
key_path: "foo.key".into(),
target_path: "blah".into(),
force: false,
}
))
}
#[test]
fn test_basic_open() {
let op = parse_args(vec!["drawer", "open", "blah.drawer", "-i", "foo.key", "blah"]);
assert_eq!(op, Ok(
DrawerOperation {
operation_type: Open,
drawer_file: "blah.drawer".into(),
key_path: "foo.key".into(),
target_path: "blah".into(),
force: false,
}
))
}
#[test]
fn test_missing_key() {
}
#[test]
fn test_key_env_variable() {
unsafe { std::env::set_var("DRAWER_KEY", "banana.key"); }
let op = parse_args(vec!["drawer", "open", "blah.drawer", "blah"]);
assert_eq!(*op.unwrap().key_path, *"banana.key");
unsafe { std::env::remove_var("DRAWER_KEY"); }
let op = parse_args(vec!["drawer", "open", "blah.drawer", "blah"]);
assert_eq!(op, Err(DrawerError::NoKeyPath))
}
}