phase 1
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+8
@@ -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
|
||||
Generated
+11
@@ -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>
|
||||
Generated
+8
@@ -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
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+10
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user