Merge pull request #12 from glitchySid/feature/custom-path-support
Feature/custom path support
This commit is contained in:
52
README.md
52
README.md
@@ -60,9 +60,18 @@ On first run, NoEntropy will guide you through an interactive setup to configure
|
|||||||
# Organize your downloads folder
|
# Organize your downloads folder
|
||||||
./noentropy
|
./noentropy
|
||||||
|
|
||||||
|
# Organize a specific directory (current directory)
|
||||||
|
./noentropy .
|
||||||
|
|
||||||
|
# Organize a specific directory (absolute path)
|
||||||
|
./noentropy /path/to/folder
|
||||||
|
|
||||||
# Preview changes without moving files
|
# Preview changes without moving files
|
||||||
./noentropy --dry-run
|
./noentropy --dry-run
|
||||||
|
|
||||||
|
# Preview organization of current directory
|
||||||
|
./noentropy . --dry-run
|
||||||
|
|
||||||
# Undo the last organization
|
# Undo the last organization
|
||||||
./noentropy --undo
|
./noentropy --undo
|
||||||
```
|
```
|
||||||
@@ -109,6 +118,48 @@ Files moved: 47, Errors: 0
|
|||||||
Done!
|
Done!
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Custom Path Support
|
||||||
|
|
||||||
|
NoEntropy now supports organizing any directory, not just your configured Downloads folder!
|
||||||
|
|
||||||
|
### Organize Any Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Organize current directory
|
||||||
|
./noentropy .
|
||||||
|
|
||||||
|
# Organize specific folder
|
||||||
|
./noentropy /path/to/folder
|
||||||
|
|
||||||
|
# Organize with relative path
|
||||||
|
./noentropy ./subfolder
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Path Validation**: Ensures the directory exists and is accessible
|
||||||
|
- **Path Normalization**: Resolves `.`, `..`, and symlinks for consistency
|
||||||
|
- **Full Compatibility**: Works with all existing options (`--dry-run`, `--recursive`, etc.)
|
||||||
|
- **Security**: Prevents path traversal attacks and invalid paths
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
- Quickly organize project directories
|
||||||
|
- Clean up specific folders without changing configuration
|
||||||
|
- Test organization on different directories
|
||||||
|
- Organize documents, downloads, or any file collection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview organization of current directory
|
||||||
|
./noentropy . --dry-run
|
||||||
|
|
||||||
|
# Organize project folder recursively
|
||||||
|
./noentropy ./my-project --recursive
|
||||||
|
|
||||||
|
# Undo organization in specific directory
|
||||||
|
./noentropy /path/to/folder --undo
|
||||||
|
```
|
||||||
|
|
||||||
## Use Cases
|
## Use Cases
|
||||||
|
|
||||||
- 📂 Organize a messy Downloads folder
|
- 📂 Organize a messy Downloads folder
|
||||||
@@ -153,6 +204,7 @@ All file moves are tracked for 30 days with full conflict detection and safety f
|
|||||||
|
|
||||||
| Option | Short | Description |
|
| Option | Short | Description |
|
||||||
|--------|-------|-------------|
|
|--------|-------|-------------|
|
||||||
|
| `[PATH]` | - | Path to organize (defaults to configured download folder) |
|
||||||
| `--dry-run` | `-d` | Preview changes without moving files |
|
| `--dry-run` | `-d` | Preview changes without moving files |
|
||||||
| `--max-concurrent` | `-m` | Maximum concurrent API requests (default: 5) |
|
| `--max-concurrent` | `-m` | Maximum concurrent API requests (default: 5) |
|
||||||
| `--recursive` | - | Recursively search files in subdirectories |
|
| `--recursive` | - | Recursively search files in subdirectories |
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ NoEntropy supports several command-line flags to customize its behavior:
|
|||||||
|
|
||||||
| Option | Short | Default | Description |
|
| Option | Short | Default | Description |
|
||||||
|--------|-------|---------|-------------|
|
|--------|-------|---------|-------------|
|
||||||
|
| `[PATH]` | - | - | Path to organize (defaults to configured download folder) |
|
||||||
| `--dry-run` | `-d` | `false` | Preview changes without moving files |
|
| `--dry-run` | `-d` | `false` | Preview changes without moving files |
|
||||||
| `--max-concurrent` | `-m` | `5` | Maximum concurrent API requests |
|
| `--max-concurrent` | `-m` | `5` | Maximum concurrent API requests |
|
||||||
| `--recursive` | - | `false` | Recursively search files in subdirectories |
|
| `--recursive` | - | `false` | Recursively search files in subdirectories |
|
||||||
@@ -32,6 +33,35 @@ NoEntropy supports several command-line flags to customize its behavior:
|
|||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
|
### Custom Path Organization
|
||||||
|
|
||||||
|
Organize any directory instead of the configured download folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./noentropy /path/to/folder
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage with current directory:**
|
||||||
|
```bash
|
||||||
|
./noentropy .
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage with relative path:**
|
||||||
|
```bash
|
||||||
|
./noentropy ./subfolder
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- Organize directories other than your Downloads folder
|
||||||
|
- Quickly organize the current working directory
|
||||||
|
- Test organization on specific folders before applying to Downloads
|
||||||
|
- Organize project directories, documents, or other file collections
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Path validation ensures the directory exists and is accessible
|
||||||
|
- Path normalization resolves `.`, `..`, and symlinks for consistency
|
||||||
|
- Works with all other options (`--dry-run`, `--recursive`, etc.)
|
||||||
|
|
||||||
### Dry-Run Mode
|
### Dry-Run Mode
|
||||||
|
|
||||||
Preview what NoEntropy would do without actually moving any files:
|
Preview what NoEntropy would do without actually moving any files:
|
||||||
@@ -102,6 +132,28 @@ You can combine multiple options:
|
|||||||
./noentropy --recursive --max-concurrent 10
|
./noentropy --recursive --max-concurrent 10
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Custom path combinations:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview organization of current directory
|
||||||
|
./noentropy . --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Organize specific folder recursively
|
||||||
|
./noentropy /path/to/folder --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Organize current directory with custom concurrency
|
||||||
|
./noentropy . --max-concurrent 10
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Undo organization in specific directory
|
||||||
|
./noentropy /path/to/folder --undo
|
||||||
|
```
|
||||||
|
|
||||||
## Undo Operations
|
## Undo Operations
|
||||||
|
|
||||||
NoEntropy tracks all file moves and allows you to undo them.
|
NoEntropy tracks all file moves and allows you to undo them.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
@@ -21,4 +22,17 @@ pub struct Args {
|
|||||||
pub undo: bool,
|
pub undo: bool,
|
||||||
#[arg(long, help = "Change api key")]
|
#[arg(long, help = "Change api key")]
|
||||||
pub change_key: bool,
|
pub change_key: bool,
|
||||||
|
|
||||||
|
/// Optional path to organize instead of the configured download folder
|
||||||
|
///
|
||||||
|
/// If provided, this path will be used instead of the download folder
|
||||||
|
/// configured in the settings. The path will be validated and normalized
|
||||||
|
/// (resolving `.`, `..`, and symlinks) before use.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// - `.` or `./` for current directory
|
||||||
|
/// - `/absolute/path/to/folder` for absolute paths
|
||||||
|
/// - `relative/path` for paths relative to current working directory
|
||||||
|
#[arg(help = "Path to organize (defaults to configured download folder)")]
|
||||||
|
pub path: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,44 @@ use crate::settings::Config;
|
|||||||
use crate::storage::{Cache, UndoLog};
|
use crate::storage::{Cache, UndoLog};
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Validates that a path exists and is a readable directory
|
||||||
|
/// Returns the canonicalized path if validation succeeds
|
||||||
|
fn validate_and_normalize_path(path: &PathBuf) -> Result<PathBuf, String> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(format!("Path '{}' does not exist", path.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !path.is_dir() {
|
||||||
|
return Err(format!("Path '{}' is not a directory", path.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can read the directory
|
||||||
|
match fs::read_dir(path) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!(
|
||||||
|
"Cannot access directory '{}': {}",
|
||||||
|
path.display(),
|
||||||
|
e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the path to resolve ., .., and symlinks
|
||||||
|
match path.canonicalize() {
|
||||||
|
Ok(canonical) => Ok(canonical),
|
||||||
|
Err(e) => Err(format!(
|
||||||
|
"Failed to normalize path '{}': {}",
|
||||||
|
path.display(),
|
||||||
|
e
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_gemini_error(error: crate::gemini::GeminiError) {
|
pub fn handle_gemini_error(error: crate::gemini::GeminiError) {
|
||||||
use colored::*;
|
use colored::*;
|
||||||
|
|
||||||
@@ -99,14 +134,28 @@ pub async fn handle_organization(
|
|||||||
let cache_path = data_dir.join(".noentropy_cache.json");
|
let cache_path = data_dir.join(".noentropy_cache.json");
|
||||||
let mut cache = Cache::load_or_create(&cache_path);
|
let mut cache = Cache::load_or_create(&cache_path);
|
||||||
|
|
||||||
cache.cleanup_old_entries(7 * 24 * 60 * 60);
|
const CACHE_RETENTION_SECONDS: u64 = 7 * 24 * 60 * 60; // 7 days
|
||||||
|
const UNDO_LOG_RETENTION_SECONDS: u64 = 30 * 24 * 60 * 60; // 30 days
|
||||||
|
|
||||||
|
cache.cleanup_old_entries(CACHE_RETENTION_SECONDS);
|
||||||
|
|
||||||
let undo_log_path = Config::get_undo_log_path()?;
|
let undo_log_path = Config::get_undo_log_path()?;
|
||||||
let mut undo_log = UndoLog::load_or_create(&undo_log_path);
|
let mut undo_log = UndoLog::load_or_create(&undo_log_path);
|
||||||
undo_log.cleanup_old_entries(30 * 24 * 60 * 60);
|
undo_log.cleanup_old_entries(UNDO_LOG_RETENTION_SECONDS);
|
||||||
|
|
||||||
let download_path = config.download_folder;
|
// Use custom path if provided, otherwise fall back to configured download folder
|
||||||
let batch = FileBatch::from_path(download_path.clone(), args.recursive);
|
let target_path = args.path.unwrap_or(config.download_folder);
|
||||||
|
|
||||||
|
// Validate and normalize the target path early
|
||||||
|
let target_path = match validate_and_normalize_path(&target_path) {
|
||||||
|
Ok(normalized) => normalized,
|
||||||
|
Err(e) => {
|
||||||
|
println!("{}", format!("ERROR: {}", e).red());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let batch = FileBatch::from_path(target_path.clone(), args.recursive);
|
||||||
|
|
||||||
if batch.filenames.is_empty() {
|
if batch.filenames.is_empty() {
|
||||||
println!("{}", "No files found to organize!".yellow());
|
println!("{}", "No files found to organize!".yellow());
|
||||||
@@ -119,7 +168,7 @@ pub async fn handle_organization(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut plan: OrganizationPlan = match client
|
let mut plan: OrganizationPlan = match client
|
||||||
.organize_files_in_batches(batch.filenames, Some(&mut cache), Some(&download_path))
|
.organize_files_in_batches(batch.filenames, Some(&mut cache), Some(&target_path))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(plan) => plan,
|
Ok(plan) => plan,
|
||||||
@@ -180,7 +229,7 @@ pub async fn handle_organization(
|
|||||||
if args.dry_run {
|
if args.dry_run {
|
||||||
println!("{} Dry run mode - skipping file moves.", "INFO:".cyan());
|
println!("{} Dry run mode - skipping file moves.", "INFO:".cyan());
|
||||||
} else {
|
} else {
|
||||||
execute_move(&download_path, plan, Some(&mut undo_log));
|
execute_move(&target_path, plan, Some(&mut undo_log));
|
||||||
}
|
}
|
||||||
println!("{}", "Done!".green().bold());
|
println!("{}", "Done!".green().bold());
|
||||||
|
|
||||||
@@ -213,10 +262,29 @@ pub async fn handle_undo(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::files::undo_moves(&download_path, &mut undo_log, args.dry_run)?;
|
// Use custom path if provided, otherwise use the configured download path
|
||||||
|
let target_path = args.path.unwrap_or(download_path);
|
||||||
|
|
||||||
|
// Validate and normalize the target path early
|
||||||
|
let target_path = match validate_and_normalize_path(&target_path) {
|
||||||
|
Ok(normalized) => normalized,
|
||||||
|
Err(e) => {
|
||||||
|
println!("{}", format!("ERROR: {}", e).red());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
crate::files::undo_moves(&target_path, &mut undo_log, args.dry_run)?;
|
||||||
|
|
||||||
if let Err(e) = undo_log.save(&undo_log_path) {
|
if let Err(e) = undo_log.save(&undo_log_path) {
|
||||||
eprintln!("Warning: Failed to save undo log: {}", e);
|
eprintln!(
|
||||||
|
"{}",
|
||||||
|
format!(
|
||||||
|
"WARNING: Failed to save undo log to '{}': {}. Your undo history may be incomplete.",
|
||||||
|
undo_log_path.display(),
|
||||||
|
e
|
||||||
|
).yellow()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user