first commit

a tool that automatically organize your messy folders
This commit is contained in:
2025-12-20 15:32:38 +05:30
commit 3c038e3a3c
9 changed files with 1852 additions and 0 deletions

76
src/files.rs Normal file
View File

@@ -0,0 +1,76 @@
use serde::{Deserialize, Serialize};
use std::{fs, path::Path, path::PathBuf};
#[derive(Serialize, Deserialize, Debug)]
pub struct FileCategory {
pub filename: String,
pub category: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct OrganizationPlan {
pub files: Vec<FileCategory>,
}
#[derive(Debug)]
pub struct FileBatch {
pub filenames: Vec<String>,
pub paths: Vec<PathBuf>,
}
impl FileBatch {
/// Reads a directory path and populates lists of all files inside it.
/// It skips sub-directories (does not read recursively).
pub fn from_path(root_path: PathBuf) -> Self {
let mut filenames = Vec::new();
let mut paths = Vec::new();
// Check if the path exists and is a directory
if root_path.is_dir() {
// fs::read_dir returns a Result, so we must handle it
if let Ok(read_dir) = fs::read_dir(&root_path) {
for child in read_dir {
if let Ok(child) = child {
// We only want to list FILES, not sub-folders,
// otherwise we might try to move a folder into a folder
if child.path().is_file() {
filenames.push(child.file_name().to_string_lossy().into_owned());
paths.push(child.path());
}
}
}
}
}
FileBatch { filenames, paths }
}
/// Helper to get the number of files found
pub fn count(&self) -> usize {
self.filenames.len()
}
}
pub fn execute_move(base_path: &Path, plan: OrganizationPlan) {
for item in plan.files {
let source = base_path.join(&item.filename);
let target_dir = base_path.join(&item.category);
let target = target_dir.join(&item.filename);
// 1. Create the category folder if it doesn't exist (e.g., "Downloads/Images")
if !target_dir.exists() {
fs::create_dir_all(&target_dir).expect("Failed to create folder");
println!("Created folder: {:?}", item.category);
}
// 2. Move the file
if source.exists() {
match fs::rename(&source, &target) {
Ok(_) => println!("Moved: {} -> {}/", item.filename, item.category),
Err(e) => println!("Failed to move {}: {}", item.filename, e),
}
} else {
println!("Skipping: {} (File not found)", item.filename);
}
}
}

65
src/gemini.rs Normal file
View File

@@ -0,0 +1,65 @@
use crate::files::OrganizationPlan;
use reqwest::Client;
use serde_json::json;
pub struct GeminiClient {
api_key: String,
client: Client,
base_url: String,
}
impl GeminiClient {
pub fn new(api_key: String) -> Self {
Self {
api_key,
client: Client::new(),
base_url: "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent".to_string(),
}
}
/// Takes a list of filenames and asks Gemini to categorize them
pub async fn organize_files(
&self,
filenames: Vec<String>,
) -> Result<OrganizationPlan, Box<dyn std::error::Error>> {
let url = format!("{}?key={}", self.base_url, self.api_key);
// 1. Construct the Prompt
let file_list_str = filenames.join(", ");
let prompt = format!(
"I have these files in my Downloads folder: [{}]. \
Categorize them into these folders: 'Images', 'Documents', 'Installers', 'Music', 'Archives', 'Code', 'Misc'. \
Return ONLY a JSON object with this structure: {{ 'files': [ {{ 'filename': 'name', 'category': 'folder' }} ] }}",
file_list_str
);
// 2. Build Request with JSON Mode enforced
let request_body = json!({
"contents": [{
"parts": [{ "text": prompt }]
}],
"generationConfig": {
"response_mime_type": "application/json"
}
});
// 3. Send
let res = self.client.post(&url).json(&request_body).send().await?;
// 4. Parse
if res.status().is_success() {
let resp_json: serde_json::Value = res.json().await?;
// Extract the raw JSON string from Gemini
let raw_text = resp_json["candidates"][0]["content"]["parts"][0]["text"]
.as_str()
.ok_or("Failed to get text from Gemini")?;
// Deserialize into our Rust Struct
let plan: OrganizationPlan = serde_json::from_str(raw_text)?;
Ok(plan)
} else {
let err = res.text().await?;
Err(format!("API Error: {}", err).into())
}
}
}

2
src/lib.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod files;
pub mod gemini;

42
src/main.rs Normal file
View File

@@ -0,0 +1,42 @@
use std::path::PathBuf;
use noentropy::files::FileBatch;
use noentropy::files::OrganizationPlan;
use noentropy::files::execute_move;
use noentropy::gemini::GeminiClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenv::dotenv().ok();
let api_key = std::env::var("GEMINI_API_KEY").expect("KEY not set");
let download_path_var = std::env::var("DOWNLOAD_FOLDER").expect("Set DOWNLOAD_FOLDER={path}");
// 1. Setup
let download_path: PathBuf = PathBuf::from(download_path_var.to_string());
let client: GeminiClient = GeminiClient::new(api_key);
// 2. Get Files (Using your previous FileBatch logic)
// Assuming FileBatch::from_path returns a struct with .filenames
let batch = FileBatch::from_path(download_path.clone());
if batch.filenames.is_empty() {
println!("No files found to organize!");
return Ok(());
}
println!(
"Found {} files. Asking Gemini to organize...",
batch.filenames.len()
);
// 3. Call Gemini
let plan: OrganizationPlan = client.organize_files(batch.filenames).await?;
println!("Gemini Plan received! Moving files...");
// 4. Execute
execute_move(&download_path, plan);
println!("Done!");
Ok(())
}