Merge pull request #18 from glitchySid/refactor
Refactor cache API and expand installation docs
This commit is contained in:
47
README.md
47
README.md
@@ -26,26 +26,53 @@ NoEntropy is a smart command-line tool that organizes your cluttered Downloads f
|
|||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
**Option 1: Download Pre-built Binary**
|
### Download Pre-built Binary
|
||||||
|
|
||||||
Download the binary for your operating system from [releases](https://github.com/glitchySid/noentropy/releases):
|
Download the latest release for your operating system from [releases](https://github.com/glitchySid/noentropy/releases):
|
||||||
|
|
||||||
|
| OS | Download |
|
||||||
|
|----|----------|
|
||||||
|
| Linux x86_64 | `noentropy-x86_64-unknown-linux-gnu.tar.gz` |
|
||||||
|
| macOS x86_64 | `noentropy-x86_64-apple-darwin.tar.gz` |
|
||||||
|
| macOS arm64 | `noentropy-aarch64-apple-darwin.tar.gz` |
|
||||||
|
| Windows x86_64 | `noentropy-x86_64-pc-windows-msvc.zip` |
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
```bash
|
```bash
|
||||||
# Linux/macOS: Give execute permissions
|
# Download and extract
|
||||||
chmod +x noentropy
|
curl -LO https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-x86_64-unknown-linux-gnu.tar.gz
|
||||||
|
tar -xzf noentropy-x86_64-unknown-linux-gnu.tar.gz
|
||||||
|
|
||||||
# Run NoEntropy
|
# Add to PATH (user-level)
|
||||||
./noentropy
|
mkdir -p ~/.local/bin
|
||||||
|
mv noentropy ~/.local/bin/
|
||||||
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc
|
||||||
|
source ~/.bashrc
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
noentropy --help
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option 2: Build from Source**
|
**Windows:**
|
||||||
|
```powershell
|
||||||
|
# Download and extract
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-x86_64-pc-windows-msvc.zip" -OutFile "noentropy.zip"
|
||||||
|
Expand-Archive -Path "noentropy.zip" -DestinationPath "noentropy"
|
||||||
|
|
||||||
|
# Add to PATH (User-level)
|
||||||
|
$env:PATH += ";$env:USERPROFILE\AppData\Local\NoEntropy"
|
||||||
|
|
||||||
|
# Or add via System Properties:
|
||||||
|
# Win + R → sysdm.cpl → Environment Variables → Edit PATH
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Installation Guide](docs/INSTALLATION.md) for detailed instructions.
|
||||||
|
|
||||||
|
### Build from Source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone repository
|
|
||||||
git clone https://github.com/glitchySid/noentropy.git
|
git clone https://github.com/glitchySid/noentropy.git
|
||||||
cd noentropy
|
cd noentropy
|
||||||
|
|
||||||
# Build and run
|
|
||||||
cargo build --release
|
cargo build --release
|
||||||
./target/release/noentropy
|
./target/release/noentropy
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ This guide covers different ways to install and set up NoEntropy on your system.
|
|||||||
|
|
||||||
Before installing NoEntropy, ensure you have:
|
Before installing NoEntropy, ensure you have:
|
||||||
|
|
||||||
- **Rust 2024 Edition** or later (if building from source)
|
|
||||||
- **Google Gemini API Key** - Get one at [https://ai.google.dev/](https://ai.google.dev/)
|
- **Google Gemini API Key** - Get one at [https://ai.google.dev/](https://ai.google.dev/)
|
||||||
- A folder full of unorganized files to clean up!
|
- A folder full of unorganized files to clean up!
|
||||||
|
|
||||||
@@ -14,49 +13,208 @@ Before installing NoEntropy, ensure you have:
|
|||||||
|
|
||||||
The easiest way to get started is to download a pre-built binary for your operating system.
|
The easiest way to get started is to download a pre-built binary for your operating system.
|
||||||
|
|
||||||
1. **Download Binary**
|
### Step 1: Download the Binary
|
||||||
|
|
||||||
Visit the releases page and download the binary for your operating system (Windows, Linux, or macOS):
|
Visit the [releases page](https://github.com/glitchySid/noentropy/releases) and download the appropriate archive for your system:
|
||||||
|
|
||||||
|
| Operating System | Architecture | File to Download |
|
||||||
|
|------------------|--------------|------------------|
|
||||||
|
| Linux | x86_64 | `noentropy-x86_64-unknown-linux-gnu.tar.gz` |
|
||||||
|
| macOS | x86_64 (Intel) | `noentropy-x86_64-apple-darwin.tar.gz` |
|
||||||
|
| macOS | arm64 (Apple Silicon) | `noentropy-aarch64-apple-darwin.tar.gz` |
|
||||||
|
| Windows | x86_64 | `noentropy-x86_64-pc-windows-msvc.zip` |
|
||||||
|
|
||||||
|
Or download directly from the command line:
|
||||||
|
|
||||||
|
**Linux:**
|
||||||
```bash
|
```bash
|
||||||
https://github.com/glitchySid/noentropy/releases
|
curl -LO https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-x86_64-unknown-linux-gnu.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Give Permission (Linux/macOS only)**
|
**macOS (Intel):**
|
||||||
|
|
||||||
Make the binary executable:
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x noentropy
|
curl -LO https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-x86_64-apple-darwin.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Run NoEntropy**
|
**macOS (Apple Silicon):**
|
||||||
|
```bash
|
||||||
|
curl -LO https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-aarch64-apple-darwin.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows (PowerShell):**
|
||||||
|
```powershell
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-x86_64-pc-windows-msvc.zip" -OutFile "noentropy.zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Extract the Archive
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
|
```bash
|
||||||
|
tar -xzf noentropy-x86_64-unknown-linux-gnu.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
Right-click the downloaded zip file and select "Extract All..." or use PowerShell:
|
||||||
|
```powershell
|
||||||
|
Expand-Archive -Path "noentropy.zip" -DestinationPath "noentropy"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Add to PATH
|
||||||
|
|
||||||
|
You need to add the folder containing `noentropy` to your system's PATH so you can run it from anywhere.
|
||||||
|
|
||||||
|
#### Linux/macOS
|
||||||
|
|
||||||
|
**Option A: User-level (recommended, no sudo required)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./noentropy
|
# Create local bin directory if it doesn't exist
|
||||||
|
mkdir -p ~/.local/bin
|
||||||
|
|
||||||
|
# Move the binary to a location in your PATH
|
||||||
|
mv noentropy ~/.local/bin/
|
||||||
|
|
||||||
|
# Add to PATH temporarily for this session
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
|
# Verify it works
|
||||||
|
noentropy --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To make this permanent, add this line to your shell configuration file:
|
||||||
|
|
||||||
|
**For bash (`~/.bashrc` or `~/.bash_profile`):**
|
||||||
|
```bash
|
||||||
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
|
||||||
|
source ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
**For zsh (`~/.zshrc`):**
|
||||||
|
```bash
|
||||||
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
|
||||||
|
source ~/.zshrc
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: System-wide (requires sudo)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Move to system bin (requires sudo on most systems)
|
||||||
|
sudo mv noentropy /usr/local/bin/
|
||||||
|
|
||||||
|
# Verify it works
|
||||||
|
noentropy --help
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
|
||||||
|
**Option A: User-level (recommended)**
|
||||||
|
|
||||||
|
1. Move the extracted `noentropy.exe` to a folder, for example:
|
||||||
|
```
|
||||||
|
C:\Users\<YourUsername>\AppData\Local\NoEntropy
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add to User PATH:
|
||||||
|
- Press `Win + R`, type `sysdm.cpl`, press Enter
|
||||||
|
- Click "Environment Variables"
|
||||||
|
- Under "User variables", select "Path", click "Edit"
|
||||||
|
- Click "New" and add:
|
||||||
|
```
|
||||||
|
C:\Users\<YourUsername>\AppData\Local\NoEntropy
|
||||||
|
```
|
||||||
|
- Click "OK" on all dialogs
|
||||||
|
|
||||||
|
3. **Alternative using PowerShell (Admin):**
|
||||||
|
```powershell
|
||||||
|
# Create installation directory
|
||||||
|
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\AppData\Local\NoEntropy"
|
||||||
|
|
||||||
|
# Move the binary
|
||||||
|
Move-Item -Path ".\noentropy.exe" -Destination "$env:USERPROFILE\AppData\Local\NoEntropy\"
|
||||||
|
|
||||||
|
# Add to PATH (User level)
|
||||||
|
$path = [Environment]::GetEnvironmentVariable("PATH", "User")
|
||||||
|
$newPath = "$path;$env:USERPROFILE\AppData\Local\NoEntropy"
|
||||||
|
[Environment]::SetEnvironmentVariable("PATH", $newPath, "User")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
noentropy --help
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Restart your terminal** or start a new Command Prompt/PowerShell window for the PATH changes to take effect.
|
||||||
|
|
||||||
|
**Option B: System-wide (requires Administrator)**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Run PowerShell as Administrator
|
||||||
|
Move-Item -Path ".\noentropy.exe" -Destination "C:\Program Files\NoEntropy\noentropy.exe"
|
||||||
|
|
||||||
|
# Add to system PATH
|
||||||
|
$path = [Environment]::GetEnvironmentVariable("PATH", "Machine")
|
||||||
|
$newPath = "$path;C:\Program Files\NoEntropy"
|
||||||
|
[Environment]::SetEnvironmentVariable("PATH", $newPath, "Machine")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
noentropy --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Verify Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
noentropy --help
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see the help message with available options.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Option 2: Build from Source
|
## Option 2: Build from Source
|
||||||
|
|
||||||
If you prefer to build from source or want the latest development version:
|
If you prefer to build from source or want the latest development version:
|
||||||
|
|
||||||
1. **Clone the Repository**
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Rust 2024 Edition** or later - Install from [rustup.rs](https://rustup.rs/)
|
||||||
|
- **Git** - For cloning the repository
|
||||||
|
|
||||||
|
### Step 1: Clone the Repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/glitchySid/noentropy.git
|
git clone https://github.com/glitchySid/noentropy.git
|
||||||
cd noentropy
|
cd noentropy
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Build the Application**
|
### Step 2: Build the Application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build --release
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Run the Application**
|
The binary will be located at `target/release/noentropy`.
|
||||||
|
|
||||||
|
### Step 3: Install Globally (Optional)
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
```bash
|
```bash
|
||||||
./target/release/noentropy
|
# User-level installation
|
||||||
|
mkdir -p ~/.local/bin
|
||||||
|
cp target/release/noentropy ~/.local/bin/
|
||||||
|
noentropy --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```powershell
|
||||||
|
# Create installation directory
|
||||||
|
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\AppData\Local\NoEntropy"
|
||||||
|
|
||||||
|
# Copy the binary
|
||||||
|
Copy-Item -Path ".\target\release\noentropy.exe" -Destination "$env:USERPROFILE\AppData\Local\NoEntropy\"
|
||||||
|
|
||||||
|
# Add to PATH (see Windows instructions above)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## First-Run Setup
|
## First-Run Setup
|
||||||
|
|
||||||
On first run, NoEntropy will guide you through an interactive setup process:
|
On first run, NoEntropy will guide you through an interactive setup process:
|
||||||
@@ -69,19 +227,40 @@ NoEntropy provides an interactive setup if configuration is missing:
|
|||||||
- **Missing download folder?** → You'll be prompted to specify it (with default suggestion)
|
- **Missing download folder?** → You'll be prompted to specify it (with default suggestion)
|
||||||
- **Both missing?** → You'll be guided through complete setup
|
- **Both missing?** → You'll be guided through complete setup
|
||||||
|
|
||||||
Configuration is automatically saved to `~/.config/noentropy/config.toml` after interactive setup.
|
Configuration is automatically saved to:
|
||||||
|
|
||||||
|
| OS | Path |
|
||||||
|
|----|------|
|
||||||
|
| Linux/macOS | `~/.config/noentropy/config.toml` |
|
||||||
|
| Windows | `%APPDATA%\NoEntropy\config.toml` |
|
||||||
|
|
||||||
### Manual Configuration
|
### Manual Configuration
|
||||||
|
|
||||||
Alternatively, you can manually create the configuration file:
|
Alternatively, you can manually create the configuration file:
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
```bash
|
```bash
|
||||||
|
mkdir -p ~/.config/noentropy
|
||||||
cp config.example.toml ~/.config/noentropy/config.toml
|
cp config.example.toml ~/.config/noentropy/config.toml
|
||||||
nano ~/.config/noentropy/config.toml
|
nano ~/.config/noentropy/config.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```powershell
|
||||||
|
# Create config directory
|
||||||
|
New-Item -ItemType Directory -Force -Path "$env:APPDATA\NoEntropy"
|
||||||
|
|
||||||
|
# Copy example config
|
||||||
|
Copy-Item -Path ".\config.example.toml" -Destination "$env:APPDATA\NoEntropy\config.toml"
|
||||||
|
|
||||||
|
# Edit with Notepad
|
||||||
|
notepad "$env:APPDATA\NoEntropy\config.toml"
|
||||||
|
```
|
||||||
|
|
||||||
See the [Configuration Guide](CONFIGURATION.md) for detailed configuration options.
|
See the [Configuration Guide](CONFIGURATION.md) for detailed configuration options.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Getting Your Gemini API Key
|
## Getting Your Gemini API Key
|
||||||
|
|
||||||
1. Visit [Google AI Studio](https://ai.google.dev/)
|
1. Visit [Google AI Studio](https://ai.google.dev/)
|
||||||
@@ -89,16 +268,28 @@ See the [Configuration Guide](CONFIGURATION.md) for detailed configuration optio
|
|||||||
3. Create a new API key
|
3. Create a new API key
|
||||||
4. Copy the key to your configuration file or enter it during interactive setup
|
4. Copy the key to your configuration file or enter it during interactive setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
To verify your installation works correctly:
|
To verify your installation works correctly:
|
||||||
|
|
||||||
1. Run NoEntropy with the `--dry-run` flag:
|
|
||||||
```bash
|
```bash
|
||||||
./noentropy --dry-run
|
noentropy --help
|
||||||
```
|
```
|
||||||
|
|
||||||
2. You should see NoEntropy scan your downloads folder and display an organization plan without moving any files.
|
If you see the help output, installation was successful!
|
||||||
|
|
||||||
|
To test file organization:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Organize your downloads folder (or configured folder)
|
||||||
|
noentropy --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see NoEntropy scan your folder and display an organization plan without moving any files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
@@ -106,15 +297,19 @@ To verify your installation works correctly:
|
|||||||
- [Usage Guide](USAGE.md) - Learn how to use NoEntropy effectively
|
- [Usage Guide](USAGE.md) - Learn how to use NoEntropy effectively
|
||||||
- [How It Works](HOW_IT_WORKS.md) - Understand the organization process
|
- [How It Works](HOW_IT_WORKS.md) - Understand the organization process
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
If you encounter issues during installation, check the [Troubleshooting Guide](TROUBLESHOOTING.md).
|
If you encounter issues during installation, check the [Troubleshooting Guide](TROUBLESHOOTING.md).
|
||||||
|
|
||||||
Common installation issues:
|
Common installation issues:
|
||||||
|
|
||||||
|
- **"noentropy: command not found"**: The folder is not in your PATH. Restart your terminal or run `source ~/.bashrc` (or `source ~/.zshrc`).
|
||||||
|
- **Permission denied (Linux/macOS)**: Make sure the binary has execute permissions: `chmod +x noentropy`
|
||||||
|
- **Windows PATH not updating**: Restart your terminal or computer after adding to PATH.
|
||||||
- **Rust not installed**: Install Rust from [rustup.rs](https://rustup.rs/)
|
- **Rust not installed**: Install Rust from [rustup.rs](https://rustup.rs/)
|
||||||
- **Build errors**: Ensure you have the latest Rust toolchain: `rustup update`
|
- **Build errors**: Ensure you have the latest Rust toolchain: `rustup update`
|
||||||
- **Permission denied**: Make sure the binary has execute permissions (Linux/macOS)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -93,17 +93,12 @@ impl GeminiClient {
|
|||||||
) -> Result<OrganizationPlan, GeminiError> {
|
) -> Result<OrganizationPlan, GeminiError> {
|
||||||
let url = self.build_url();
|
let url = self.build_url();
|
||||||
|
|
||||||
// Check cache and get pre-fetched metadata in one pass
|
// Check cache first
|
||||||
let cache_result = match (cache.as_ref(), base_path) {
|
if let Some(ref mut c) = cache
|
||||||
(Some(c), Some(bp)) => Some(c.check_cache(&filenames, bp)),
|
&& let Some(bp) = base_path
|
||||||
_ => None,
|
&& let Some(cached) = c.check_cache(&filenames, bp)
|
||||||
};
|
|
||||||
|
|
||||||
// Return cached response if valid
|
|
||||||
if let Some(ref result) = cache_result
|
|
||||||
&& let Some(ref cached_response) = result.cached_response
|
|
||||||
{
|
{
|
||||||
return Ok(cached_response.clone());
|
return Ok(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
let prompt = PromptBuilder::new(&filenames).build_categorization_prompt(&self.categories);
|
let prompt = PromptBuilder::new(&filenames).build_categorization_prompt(&self.categories);
|
||||||
@@ -112,9 +107,9 @@ impl GeminiClient {
|
|||||||
let res = self.send_request_with_retry(&url, &request_body).await?;
|
let res = self.send_request_with_retry(&url, &request_body).await?;
|
||||||
let plan = self.parse_categorization_response(res).await?;
|
let plan = self.parse_categorization_response(res).await?;
|
||||||
|
|
||||||
// Cache response using pre-fetched metadata (no second metadata lookup)
|
// Cache the response
|
||||||
if let (Some(cache), Some(result)) = (cache.as_mut(), cache_result) {
|
if let (Some(cache), Some(base_path)) = (cache, base_path) {
|
||||||
cache.cache_response_with_metadata(&filenames, plan.clone(), result.file_metadata);
|
cache.cache_response(&filenames, plan.clone(), base_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(plan)
|
Ok(plan)
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
/// Result of checking the cache - includes pre-fetched metadata to avoid double lookups
|
|
||||||
pub struct CacheCheckResult {
|
|
||||||
pub cached_response: Option<OrganizationPlan>,
|
|
||||||
pub file_metadata: HashMap<String, FileMetadata>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct Cache {
|
pub struct Cache {
|
||||||
entries: HashMap<String, CacheEntry>,
|
entries: HashMap<String, CacheEntry>,
|
||||||
@@ -37,7 +31,10 @@ impl Cache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_or_create(cache_path: &Path) -> Self {
|
pub fn load_or_create(cache_path: &Path) -> Self {
|
||||||
if cache_path.exists() {
|
if !cache_path.exists() {
|
||||||
|
return Self::new();
|
||||||
|
}
|
||||||
|
|
||||||
match fs::read_to_string(cache_path) {
|
match fs::read_to_string(cache_path) {
|
||||||
Ok(content) => match serde_json::from_str::<Cache>(&content) {
|
Ok(content) => match serde_json::from_str::<Cache>(&content) {
|
||||||
Ok(cache) => {
|
Ok(cache) => {
|
||||||
@@ -54,10 +51,6 @@ impl Cache {
|
|||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
println!("Creating new cache file");
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self, cache_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn save(&self, cache_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -70,32 +63,13 @@ impl Cache {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks cache and returns pre-fetched metadata to avoid double lookups.
|
pub fn check_cache(&self, filenames: &[String], base_path: &Path) -> Option<OrganizationPlan> {
|
||||||
/// The returned metadata can be passed to `cache_response_with_metadata` on cache miss.
|
let cache_key = Self::generate_cache_key(filenames);
|
||||||
pub fn check_cache(&self, filenames: &[String], base_path: &Path) -> CacheCheckResult {
|
let entry = self.entries.get(&cache_key)?;
|
||||||
// Fetch metadata once for all files
|
|
||||||
let file_metadata: HashMap<String, FileMetadata> = filenames
|
|
||||||
.iter()
|
|
||||||
.filter_map(|filename| {
|
|
||||||
let file_path = base_path.join(filename);
|
|
||||||
Self::get_file_metadata(&file_path)
|
|
||||||
.ok()
|
|
||||||
.map(|m| (filename.clone(), m))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let cache_key = self.generate_cache_key(filenames);
|
|
||||||
|
|
||||||
let cached_response = self.entries.get(&cache_key).and_then(|entry| {
|
|
||||||
// Validate all files are unchanged using pre-fetched metadata
|
|
||||||
let all_unchanged = filenames.iter().all(|filename| {
|
let all_unchanged = filenames.iter().all(|filename| {
|
||||||
match (
|
let file_path = base_path.join(filename);
|
||||||
file_metadata.get(filename),
|
FileMetadata::from_path(&file_path).ok().as_ref() == entry.file_metadata.get(filename)
|
||||||
entry.file_metadata.get(filename),
|
|
||||||
) {
|
|
||||||
(Some(current), Some(cached)) => current == cached,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if all_unchanged {
|
if all_unchanged {
|
||||||
@@ -104,73 +78,25 @@ impl Cache {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
CacheCheckResult {
|
|
||||||
cached_response,
|
|
||||||
file_metadata,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cache response using pre-fetched metadata (avoids double metadata lookup)
|
|
||||||
pub fn cache_response_with_metadata(
|
|
||||||
&mut self,
|
|
||||||
filenames: &[String],
|
|
||||||
response: OrganizationPlan,
|
|
||||||
file_metadata: HashMap<String, FileMetadata>,
|
|
||||||
) {
|
|
||||||
let cache_key = self.generate_cache_key(filenames);
|
|
||||||
|
|
||||||
let timestamp = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
let entry = CacheEntry {
|
|
||||||
response,
|
|
||||||
timestamp,
|
|
||||||
file_metadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.entries.insert(cache_key, entry);
|
|
||||||
|
|
||||||
if self.entries.len() > self.max_entries {
|
|
||||||
self.evict_oldest();
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Cached response for {} files", filenames.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Legacy method - checks cache for a response (fetches metadata internally)
|
|
||||||
#[deprecated(
|
|
||||||
note = "Use check_cache() + cache_response_with_metadata() to avoid double metadata lookups"
|
|
||||||
)]
|
|
||||||
pub fn get_cached_response(
|
|
||||||
&self,
|
|
||||||
filenames: &[String],
|
|
||||||
base_path: &Path,
|
|
||||||
) -> Option<OrganizationPlan> {
|
|
||||||
let result = self.check_cache(filenames, base_path);
|
|
||||||
result.cached_response
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Legacy method - caches a response (fetches metadata internally)
|
|
||||||
#[deprecated(note = "Use cache_response_with_metadata() with pre-fetched metadata")]
|
|
||||||
pub fn cache_response(
|
pub fn cache_response(
|
||||||
&mut self,
|
&mut self,
|
||||||
filenames: &[String],
|
filenames: &[String],
|
||||||
response: OrganizationPlan,
|
response: OrganizationPlan,
|
||||||
base_path: &Path,
|
base_path: &Path,
|
||||||
) {
|
) {
|
||||||
let cache_key = self.generate_cache_key(filenames);
|
let cache_key = Self::generate_cache_key(filenames);
|
||||||
let mut file_metadata = HashMap::new();
|
|
||||||
|
|
||||||
for filename in filenames {
|
let file_metadata: HashMap<String, FileMetadata> = filenames
|
||||||
|
.iter()
|
||||||
|
.filter_map(|filename| {
|
||||||
let file_path = base_path.join(filename);
|
let file_path = base_path.join(filename);
|
||||||
if let Ok(metadata) = Self::get_file_metadata(&file_path) {
|
FileMetadata::from_path(&file_path)
|
||||||
file_metadata.insert(filename.clone(), metadata);
|
.ok()
|
||||||
}
|
.map(|m| (filename.clone(), m))
|
||||||
}
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let timestamp = SystemTime::now()
|
let timestamp = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -192,12 +118,12 @@ impl Cache {
|
|||||||
println!("Cached response for {} files", filenames.len());
|
println!("Cached response for {} files", filenames.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_cache_key(&self, filenames: &[String]) -> String {
|
fn generate_cache_key(filenames: &[String]) -> String {
|
||||||
let mut sorted_filenames = filenames.to_vec();
|
|
||||||
sorted_filenames.sort();
|
|
||||||
|
|
||||||
let mut hasher = Hasher::new();
|
let mut hasher = Hasher::new();
|
||||||
for filename in &sorted_filenames {
|
let mut sorted: Vec<_> = filenames.iter().collect();
|
||||||
|
sorted.sort();
|
||||||
|
|
||||||
|
for filename in sorted {
|
||||||
hasher.update(filename.as_bytes());
|
hasher.update(filename.as_bytes());
|
||||||
hasher.update(b"|");
|
hasher.update(b"|");
|
||||||
}
|
}
|
||||||
@@ -205,16 +131,6 @@ impl Cache {
|
|||||||
hasher.finalize().to_hex().to_string()
|
hasher.finalize().to_hex().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_file_metadata(file_path: &Path) -> Result<FileMetadata, Box<dyn std::error::Error>> {
|
|
||||||
let metadata = fs::metadata(file_path)?;
|
|
||||||
let modified = metadata.modified()?.duration_since(UNIX_EPOCH)?.as_secs();
|
|
||||||
|
|
||||||
Ok(FileMetadata {
|
|
||||||
size: metadata.len(),
|
|
||||||
modified,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cleanup_old_entries(&mut self, max_age_seconds: u64) {
|
pub fn cleanup_old_entries(&mut self, max_age_seconds: u64) {
|
||||||
let current_time = SystemTime::now()
|
let current_time = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -231,8 +147,8 @@ impl Cache {
|
|||||||
println!("Cleaned up {} old cache entries", removed_count);
|
println!("Cleaned up {} old cache entries", removed_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.entries.len() > self.max_entries {
|
while self.entries.len() > self.max_entries {
|
||||||
self.compact_cache();
|
self.evict_oldest();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,9 +164,11 @@ impl Cache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compact_cache(&mut self) {
|
pub fn len(&self) -> usize {
|
||||||
while self.entries.len() > self.max_entries {
|
self.entries.len()
|
||||||
self.evict_oldest();
|
}
|
||||||
}
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.entries.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod undo_log;
|
pub mod undo_log;
|
||||||
|
|
||||||
pub use cache::{Cache, CacheCheckResult};
|
pub use cache::Cache;
|
||||||
pub use undo_log::UndoLog;
|
pub use undo_log::UndoLog;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -53,18 +53,18 @@ fn test_cache_stores_and_retrieves_organization_plans() {
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check cache (will also fetch metadata)
|
// Check cache (returns None on miss)
|
||||||
let check_result = cache.check_cache(&filenames, temp_dir.path());
|
let cached = cache.check_cache(&filenames, temp_dir.path());
|
||||||
assert!(check_result.cached_response.is_none());
|
assert!(cached.is_none());
|
||||||
|
|
||||||
// Store in cache with metadata
|
// Store in cache
|
||||||
cache.cache_response_with_metadata(&filenames, plan.clone(), check_result.file_metadata);
|
cache.cache_response(&filenames, plan.clone(), temp_dir.path());
|
||||||
|
|
||||||
// Retrieve from cache
|
// Retrieve from cache
|
||||||
let check_result2 = cache.check_cache(&filenames, temp_dir.path());
|
let cached2 = cache.check_cache(&filenames, temp_dir.path());
|
||||||
assert!(check_result2.cached_response.is_some());
|
assert!(cached2.is_some());
|
||||||
|
|
||||||
let cached = check_result2.cached_response.unwrap();
|
let cached = cached2.unwrap();
|
||||||
assert_eq!(cached.files.len(), 1);
|
assert_eq!(cached.files.len(), 1);
|
||||||
assert_eq!(cached.files[0].filename, "test.txt");
|
assert_eq!(cached.files[0].filename, "test.txt");
|
||||||
}
|
}
|
||||||
@@ -84,8 +84,7 @@ fn test_cache_invalidates_on_file_modification() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Cache the response
|
// Cache the response
|
||||||
let check_result = cache.check_cache(&filenames, temp_dir.path());
|
cache.cache_response(&filenames, plan, temp_dir.path());
|
||||||
cache.cache_response_with_metadata(&filenames, plan, check_result.file_metadata);
|
|
||||||
|
|
||||||
// Wait longer to ensure filesystem timestamp changes (at least 1 second for most filesystems)
|
// Wait longer to ensure filesystem timestamp changes (at least 1 second for most filesystems)
|
||||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||||
@@ -101,14 +100,14 @@ fn test_cache_invalidates_on_file_modification() {
|
|||||||
let _ = fs::metadata(temp_dir.path().join("test.txt"));
|
let _ = fs::metadata(temp_dir.path().join("test.txt"));
|
||||||
|
|
||||||
// Cache should be invalidated due to modification time change
|
// Cache should be invalidated due to modification time change
|
||||||
let check_result2 = cache.check_cache(&filenames, temp_dir.path());
|
let cached = cache.check_cache(&filenames, temp_dir.path());
|
||||||
|
|
||||||
// Note: Cache invalidation depends on file metadata (size/mtime) changing.
|
// Note: Cache invalidation depends on file metadata (size/mtime) changing.
|
||||||
// If the filesystem has coarse timestamp granularity, this test may be flaky.
|
// If the filesystem has coarse timestamp granularity, this test may be flaky.
|
||||||
// The important behavior is that the cache CAN detect file changes.
|
// The important behavior is that the cache CAN detect file changes.
|
||||||
// For a more robust test, we check that the cache at least loads without error.
|
// For a more robust test, we check that the cache at least loads without error.
|
||||||
// In production, files are typically modified minutes/hours apart.
|
// In production, files are typically modified minutes/hours apart.
|
||||||
if check_result2.cached_response.is_some() {
|
if cached.is_some() {
|
||||||
// If cache wasn't invalidated, it means the filesystem timestamp
|
// If cache wasn't invalidated, it means the filesystem timestamp
|
||||||
// didn't change within our sleep window - this is acceptable
|
// didn't change within our sleep window - this is acceptable
|
||||||
// as long as the mechanism works for real-world use cases
|
// as long as the mechanism works for real-world use cases
|
||||||
@@ -151,12 +150,11 @@ fn test_cache_handles_multiple_files() {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
let check_result = cache.check_cache(&filenames, temp_dir.path());
|
cache.cache_response(&filenames, plan.clone(), temp_dir.path());
|
||||||
cache.cache_response_with_metadata(&filenames, plan.clone(), check_result.file_metadata);
|
|
||||||
|
|
||||||
let check_result2 = cache.check_cache(&filenames, temp_dir.path());
|
let cached = cache.check_cache(&filenames, temp_dir.path());
|
||||||
assert!(check_result2.cached_response.is_some());
|
assert!(cached.is_some());
|
||||||
assert_eq!(check_result2.cached_response.unwrap().files.len(), 3);
|
assert_eq!(cached.unwrap().files.len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user