chore: initialiser le projet imagod-web (Rust)
- Structure du projet Cargo avec dépendances (clap, axum, reqwest, rusqlite, arboard, tray-icon) - CLI avec sous-commandes start, serve, stop, status, config, install, uninstall, logs - Module config: parsing TOML, chemins Windows/Linux - Module dedup: dédoublonnage SHA-256 avec cache SQLite - Module imago_client: client HTTP pour API Imago (upload, search, get) - Module watcher: boucle de polling stub - Module server: serveur web axum avec routes API REST stub - Platform-specific: clipboard, tray, service, window_info (Windows) - Frontend placeholder HTML - Config par défaut (imagod.toml) - README.md, LICENSE (MIT), .gitignore
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
# Build
|
||||
/target/
|
||||
.cargo/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local data
|
||||
imagod.db
|
||||
imagod.log
|
||||
*.local.toml
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
[package]
|
||||
name = "imagod-web"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Imago Daemon + Dashboard — Service de capture automatique de screenshots + Dashboard web embarqué"
|
||||
authors = ["Bruno Charest <[email protected]>"]
|
||||
license = "MIT"
|
||||
repository = "https://git.dracodev.net/Projets/imagod-web"
|
||||
keywords = ["screenshot", "clipboard", "imago", "dashboard", "ai"]
|
||||
categories = ["command-line-utilities", "web-programming"]
|
||||
|
||||
[dependencies]
|
||||
# CLI
|
||||
clap = { version = "4", features = ["derive", "env"] }
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
# HTTP Client (API Imago)
|
||||
reqwest = { version = "0.12", features = ["json", "multipart", "stream"] }
|
||||
# Serialization
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# Config
|
||||
toml = "0.8"
|
||||
# Hashing
|
||||
sha2 = "0.10"
|
||||
# SQLite
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
# Time
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
# Logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
# Error handling
|
||||
anyhow = "1"
|
||||
thiserror = "2"
|
||||
# Async trait
|
||||
async-trait = "0.1"
|
||||
# UUID for unique IDs
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
# Web server (axum + tower)
|
||||
axum = { version = "0.8", features = ["ws"] }
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors", "fs", "trace"] }
|
||||
# Frontend embedding
|
||||
rust-embed = "8"
|
||||
|
||||
# Platform-specific — Windows
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
# Clipboard access
|
||||
arboard = "3"
|
||||
# System tray
|
||||
tray-icon = "0.19"
|
||||
# Windows API (pour titre fenêtre active)
|
||||
windows-sys = { version = "0.59", features = [
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_TextServices",
|
||||
] }
|
||||
|
||||
# Platform-specific — Linux
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
# Notify pour watcher filesystem (Linux)
|
||||
notify = { version = "7", features = ["macos_kqueue"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z" # Optimize for size
|
||||
lto = true # Link-time optimization
|
||||
strip = true # Strip symbols
|
||||
codegen-units = 1 # Better optimization
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Bruno Charest
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,92 @@
|
||||
# imagod-web
|
||||
|
||||
**Imago Daemon + Dashboard** — Service de capture automatique de screenshots + Dashboard web embarqué.
|
||||
|
||||
Un binaire **Rust** unique qui remplit deux rôles :
|
||||
|
||||
| Mode | Plateforme | Rôle |
|
||||
|------|-----------|------|
|
||||
| `imagod-web start` | Windows | Watcher clipboard + Dashboard web :9090 |
|
||||
| `imagod-web serve` | Linux/Raspi | Dashboard web uniquement (sans watcher) |
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- **Capture automatique** : surveille le clipboard Windows, détecte les nouveaux screenshots
|
||||
- **Dédoublonnage** : SHA-256 pour éviter les doublons
|
||||
- **Upload Imago** : envoi vers le backend Imago pour OCR + AI Vision + Tags
|
||||
- **Dashboard web embarqué** : galerie, recherche, preview, copie/partage
|
||||
- **Cross-platform** : Windows (watcher+dashboard), Linux/Raspi (dashboard)
|
||||
- **System tray** : icône native Windows avec menu contextuel
|
||||
- **Service Windows** : installation via NSSM pour démarrage automatique
|
||||
|
||||
## Prérequis
|
||||
|
||||
- [Rust](https://rustup.rs) 1.80+
|
||||
- Un serveur [Imago](https://git.dracodev.net/Projets/Imago) fonctionnel
|
||||
- Une clé API client Imago
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Compiler depuis les sources
|
||||
git clone https://git.dracodev.net/Projets/imagod-web.git
|
||||
cd imagod-web
|
||||
cargo build --release
|
||||
|
||||
# Le binaire est dans target/release/imagod-web (ou imagod-web.exe)
|
||||
```
|
||||
|
||||
### Cross-compilation
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
cargo build --release --target x86_64-pc-windows-msvc
|
||||
|
||||
# Linux x86_64 (openclaw-1)
|
||||
cargo build --release --target x86_64-unknown-linux-gnu
|
||||
|
||||
# Linux ARM64 (Raspberry Pi)
|
||||
cargo build --release --target aarch64-unknown-linux-gnu
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
```bash
|
||||
# Windows — lancer le watcher + dashboard
|
||||
imagod-web start --port 9090 --open-browser
|
||||
|
||||
# Linux — dashboard uniquement
|
||||
imagod-web serve --port 9090 --host 0.0.0.0 --no-watcher
|
||||
|
||||
# Voir le statut
|
||||
imagod-web status
|
||||
|
||||
# Configurer
|
||||
imagod-web config show
|
||||
imagod-web config set imago.api_key "ik_..."
|
||||
|
||||
# Installer comme service Windows (admin requis)
|
||||
imagod-web install
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Fichier `imagod.toml` dans `%APPDATA%/imagod-web/` (Windows) ou `~/.config/imagod-web/` (Linux).
|
||||
|
||||
```toml
|
||||
[imago]
|
||||
url = "http://imago.dracodev.net:8000"
|
||||
api_key = "CHANGE_ME"
|
||||
|
||||
[watcher]
|
||||
capture_images = true
|
||||
poll_interval_ms = 500
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Voir [[imagod-web-architecture|Document d'architecture complet]] dans le vault Obsidian IT.
|
||||
|
||||
## Licence
|
||||
|
||||
MIT — Copyright (c) 2026 Bruno Charest
|
||||
@@ -0,0 +1,30 @@
|
||||
[imago]
|
||||
# URL du serveur Imago
|
||||
url = "http://imago.dracodev.net:8000"
|
||||
# Clé API client Imago (à configurer)
|
||||
api_key = "CHANGE_ME"
|
||||
|
||||
[watcher]
|
||||
capture_images = true
|
||||
poll_interval_ms = 500
|
||||
|
||||
[watcher.filters]
|
||||
# Applications à capturer (vide = tout)
|
||||
only_apps = []
|
||||
# Applications à exclure
|
||||
exclude_apps = ["mstsc"]
|
||||
|
||||
[dedup]
|
||||
# Méthode de dédoublonnage : sha256 | perceptual_hash
|
||||
method = "sha256"
|
||||
# Nombre de hashes en cache
|
||||
cache_size = 10000
|
||||
|
||||
[ui]
|
||||
show_notifications = true
|
||||
show_tray_icon = true
|
||||
notification_timeout_sec = 3
|
||||
|
||||
[logging]
|
||||
# Niveau de log : trace | debug | info | warn | error
|
||||
level = "info"
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
//! Parsing CLI avec clap
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "imagod-web",
|
||||
version = env!("CARGO_PKG_VERSION"),
|
||||
about = "Imago Daemon + Dashboard — Capture automatique de screenshots + Dashboard web embarqué",
|
||||
long_about = "Service de capture automatique de screenshots vers Imago avec dashboard web embarqué.\n\n\
|
||||
Modes :\n \
|
||||
Windows : imagod-web start → watcher clipboard + dashboard :9090\n \
|
||||
Linux : imagod-web serve --port 9090 --no-watcher → dashboard uniquement"
|
||||
)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// Lancer le watcher + dashboard (Windows uniquement)
|
||||
Start {
|
||||
/// Port du dashboard web
|
||||
#[arg(long, default_value = "9090")]
|
||||
port: u16,
|
||||
/// Ouvrir le dashboard dans le navigateur
|
||||
#[arg(long)]
|
||||
open_browser: bool,
|
||||
},
|
||||
/// Lancer le dashboard web uniquement (Linux/serveur)
|
||||
Serve {
|
||||
/// Port du dashboard web
|
||||
#[arg(long, default_value = "9090")]
|
||||
port: u16,
|
||||
/// Interface d'écoute
|
||||
#[arg(long, default_value = "127.0.0.1")]
|
||||
host: String,
|
||||
/// Désactiver le watcher clipboard
|
||||
#[arg(long)]
|
||||
no_watcher: bool,
|
||||
/// Ouvrir le dashboard dans le navigateur
|
||||
#[arg(long)]
|
||||
open_browser: bool,
|
||||
},
|
||||
/// Arrêter le service
|
||||
Stop,
|
||||
/// Afficher l'état du service
|
||||
Status,
|
||||
/// Gérer la configuration
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
action: ConfigAction,
|
||||
},
|
||||
/// Installer comme service Windows
|
||||
Install,
|
||||
/// Désinstaller le service Windows
|
||||
Uninstall,
|
||||
/// Afficher les derniers logs
|
||||
Logs,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ConfigAction {
|
||||
/// Afficher la configuration actuelle
|
||||
Show,
|
||||
/// Modifier un paramètre
|
||||
Set {
|
||||
/// Clé de configuration (ex: imago.url)
|
||||
key: String,
|
||||
/// Valeur
|
||||
value: String,
|
||||
},
|
||||
/// Réinitialiser la configuration par défaut
|
||||
Reset,
|
||||
}
|
||||
|
||||
pub fn parse() -> Args {
|
||||
Args::parse()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! Clipboard — Abstraction arboard (Windows)
|
||||
//!
|
||||
//! Phase 3: Lecture du clipboard Windows, détection d'images
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// Vérifie si le clipboard contient une image
|
||||
pub fn has_image() -> Result<bool> {
|
||||
// Phase 3: Implémentation arboard
|
||||
todo!("Phase 3: Clipboard detection")
|
||||
}
|
||||
|
||||
/// Récupère l'image du clipboard en bytes
|
||||
pub fn get_image_bytes() -> Result<Vec<u8>> {
|
||||
todo!("Phase 3: Clipboard image extraction")
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
//! Gestion de la configuration TOML
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Configuration complète d'imagod-web
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub imago: ImagoConfig,
|
||||
#[serde(default)]
|
||||
pub watcher: WatcherConfig,
|
||||
#[serde(default)]
|
||||
pub dedup: DedupConfig,
|
||||
#[serde(default)]
|
||||
pub ui: UiConfig,
|
||||
#[serde(default)]
|
||||
pub logging: LoggingConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ImagoConfig {
|
||||
pub url: String,
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WatcherConfig {
|
||||
pub capture_images: bool,
|
||||
pub poll_interval_ms: u64,
|
||||
pub filters: WatcherFilters,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WatcherFilters {
|
||||
pub only_apps: Vec<String>,
|
||||
pub exclude_apps: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DedupConfig {
|
||||
pub method: String,
|
||||
pub cache_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UiConfig {
|
||||
pub show_notifications: bool,
|
||||
pub show_tray_icon: bool,
|
||||
pub notification_timeout_sec: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LoggingConfig {
|
||||
pub level: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
imago: ImagoConfig {
|
||||
url: "http://imago.dracodev.net:8000".into(),
|
||||
api_key: String::new(),
|
||||
},
|
||||
watcher: WatcherConfig {
|
||||
capture_images: true,
|
||||
poll_interval_ms: 500,
|
||||
filters: WatcherFilters {
|
||||
only_apps: vec![],
|
||||
exclude_apps: vec!["mstsc".into()],
|
||||
},
|
||||
},
|
||||
dedup: DedupConfig {
|
||||
method: "sha256".into(),
|
||||
cache_size: 10000,
|
||||
},
|
||||
ui: UiConfig {
|
||||
show_notifications: true,
|
||||
show_tray_icon: true,
|
||||
notification_timeout_sec: 3,
|
||||
},
|
||||
logging: LoggingConfig {
|
||||
level: "info".into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Charge la configuration depuis `%APPDATA%/imagod-web/imagod.toml`
|
||||
pub fn load() -> Result<Self> {
|
||||
let path = Self::config_path()?;
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("Impossible de lire {}", path.display()))?;
|
||||
let config: Config = toml::from_str(&content)
|
||||
.with_context(|| "Erreur de parsing du fichier de configuration")?;
|
||||
Ok(config)
|
||||
} else {
|
||||
// Premier lancement — créer la config par défaut
|
||||
let config = Config::default();
|
||||
config.save()?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sauvegarde la configuration
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let path = Self::config_path()?;
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let content = toml::to_string_pretty(self)?;
|
||||
std::fs::write(&path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Chemin du fichier de configuration
|
||||
fn config_path() -> Result<PathBuf> {
|
||||
let base = if cfg!(target_os = "windows") {
|
||||
let appdata = std::env::var("APPDATA")
|
||||
.context("Variable APPDATA non définie")?;
|
||||
PathBuf::from(appdata)
|
||||
} else {
|
||||
let home = std::env::var("HOME")
|
||||
.context("Variable HOME non définie")?;
|
||||
PathBuf::from(home).join(".config")
|
||||
};
|
||||
Ok(base.join("imagod-web").join("imagod.toml"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//! Dédoublonnage des images par hash SHA-256
|
||||
|
||||
use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::Path;
|
||||
|
||||
/// Gestionnaire de dédoublonnage
|
||||
pub struct Dedup {
|
||||
conn: Connection,
|
||||
cache_size: usize,
|
||||
}
|
||||
|
||||
impl Dedup {
|
||||
/// Ouvre ou crée la base de dédoublonnage locale
|
||||
pub fn open(db_path: &Path, cache_size: usize) -> Result<Self> {
|
||||
let conn = Connection::open(db_path)?;
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS hashes (
|
||||
hash BLOB PRIMARY KEY,
|
||||
image_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_hashes_created ON hashes(created_at);"
|
||||
)?;
|
||||
Ok(Self { conn, cache_size })
|
||||
}
|
||||
|
||||
/// Calcule le hash SHA-256 d'un buffer
|
||||
pub fn hash_bytes(data: &[u8]) -> Vec<u8> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data);
|
||||
hasher.finalize().to_vec()
|
||||
}
|
||||
|
||||
/// Vérifie si un hash a déjà été vu
|
||||
pub fn already_seen(&self, hash: &[u8]) -> Result<bool> {
|
||||
let exists: bool = self.conn.query_row(
|
||||
"SELECT COUNT(*) > 0 FROM hashes WHERE hash = ?1",
|
||||
rusqlite::params![hash],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Marque un hash comme vu
|
||||
pub fn mark_seen(&self, hash: &[u8], image_id: &str) -> Result<()> {
|
||||
self.conn.execute(
|
||||
"INSERT OR IGNORE INTO hashes (hash, image_id) VALUES (?1, ?2)",
|
||||
rusqlite::params![hash, image_id],
|
||||
)?;
|
||||
// Nettoyage périodique si dépassement du cache
|
||||
self.conn.execute(
|
||||
"DELETE FROM hashes WHERE rowid NOT IN (
|
||||
SELECT rowid FROM hashes ORDER BY created_at DESC LIMIT ?1
|
||||
)",
|
||||
rusqlite::params![self.cache_size],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Nombre de hashes en cache
|
||||
pub fn count(&self) -> Result<usize> {
|
||||
let count: usize = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM hashes",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Imago Dashboard</title>
|
||||
<style>
|
||||
/* Phase 9: Dashboard Svelte — ici juste un placeholder */
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--fg: #c9d1d9;
|
||||
--accent: #58a6ff;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.placeholder {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 { color: var(--accent); margin-bottom: 1rem; }
|
||||
p { color: #8b949e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="placeholder">
|
||||
<h1>🖼️ Imago Dashboard</h1>
|
||||
<p>Dashboard web en construction — Phase 9</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,113 @@
|
||||
//! Client HTTP pour l'API Imago
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::{Client, multipart};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UploadResponse {
|
||||
pub id: String,
|
||||
pub filename: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ImageInfo {
|
||||
pub id: String,
|
||||
pub filename: String,
|
||||
pub tags: Vec<String>,
|
||||
pub description: Option<String>,
|
||||
pub ocr_text: Option<String>,
|
||||
pub created_at: String,
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub size_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchResponse {
|
||||
pub results: Vec<ImageInfo>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
/// Client pour l'API Imago
|
||||
pub struct ImagoClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
impl ImagoClient {
|
||||
pub fn new(base_url: String, api_key: String) -> Result<Self> {
|
||||
let client = Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()?;
|
||||
Ok(Self { client, base_url, api_key })
|
||||
}
|
||||
|
||||
/// Upload une image vers Imago
|
||||
pub async fn upload_image(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
filename: &str,
|
||||
window_title: Option<&str>,
|
||||
) -> Result<UploadResponse> {
|
||||
let part = multipart::Part::bytes(data)
|
||||
.file_name(filename.to_string())
|
||||
.mime_str("image/png")?;
|
||||
|
||||
let mut form = multipart::Form::new()
|
||||
.part("file", part);
|
||||
|
||||
if let Some(title) = window_title {
|
||||
form = form.text("window_title", title.to_string());
|
||||
}
|
||||
|
||||
let response = self.client
|
||||
.post(format!("{}/images/upload", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let upload: UploadResponse = response
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(upload)
|
||||
}
|
||||
|
||||
/// Recherche des images
|
||||
pub async fn search(&self, query: &str, limit: usize) -> Result<SearchResponse> {
|
||||
let response = self.client
|
||||
.get(format!("{}/images/search", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.query(&[("q", query), ("limit", &limit.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let result: SearchResponse = response
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Récupère les infos d'une image
|
||||
pub async fn get_image(&self, id: &str) -> Result<ImageInfo> {
|
||||
let response = self.client
|
||||
.get(format!("{}/images/{}", self.base_url, id))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let info: ImageInfo = response
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
//! imagod-web — Imago Daemon + Dashboard
|
||||
//!
|
||||
//! Point d'entrée CLI. Dispatch vers les sous-commandes :
|
||||
//! - `start` : Lance le watcher clipboard + dashboard web (Windows)
|
||||
//! - `serve` : Lance le dashboard web uniquement (Linux/serveur)
|
||||
//! - `stop` : Arrête le service
|
||||
//! - `status`: Affiche l'état
|
||||
//! - `config`: Gère la configuration
|
||||
//! - `install` / `uninstall`: Service Windows
|
||||
|
||||
mod cli;
|
||||
mod config;
|
||||
mod dedup;
|
||||
mod imago_client;
|
||||
mod watcher;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod clipboard;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod tray;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod service;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod window_info;
|
||||
|
||||
mod server;
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
fn setup_logging() {
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
|
||||
fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(true)
|
||||
.init();
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
setup_logging();
|
||||
|
||||
let args = cli::parse();
|
||||
|
||||
match args.command {
|
||||
cli::Command::Start { port, open_browser } => {
|
||||
tracing::info!("Démarrage imagod-web — watcher + dashboard");
|
||||
// Lance le watcher et le serveur web en parallèle
|
||||
// ...
|
||||
todo!("Phase 6-7: Watcher + serveur web")
|
||||
}
|
||||
cli::Command::Serve { port, host, no_watcher, open_browser } => {
|
||||
tracing::info!("Démarrage dashboard web uniquement sur {}:{}", host, port);
|
||||
// Lance uniquement le serveur web
|
||||
// ...
|
||||
todo!("Phase 9: Dashboard web")
|
||||
}
|
||||
cli::Command::Stop => {
|
||||
tracing::info!("Arrêt du service");
|
||||
todo!("Phase 7: Stop")
|
||||
}
|
||||
cli::Command::Status => {
|
||||
tracing::info!("Statut du service");
|
||||
todo!("Phase 7: Status")
|
||||
}
|
||||
cli::Command::Config { action } => {
|
||||
tracing::info!("Configuration: {:?}", action);
|
||||
todo!("Phase 2: Config")
|
||||
}
|
||||
cli::Command::Install => {
|
||||
tracing::info!("Installation comme service Windows");
|
||||
todo!("Phase 10: Service install")
|
||||
}
|
||||
cli::Command::Uninstall => {
|
||||
tracing::info!("Désinstallation du service Windows");
|
||||
todo!("Phase 10: Service uninstall")
|
||||
}
|
||||
cli::Command::Logs => {
|
||||
tracing::info!("Logs");
|
||||
todo!("Phase 7: Logs")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//! Serveur web — Dashboard embarqué (axum)
|
||||
//!
|
||||
//! Phase 9: API REST + WebSocket + frontend Svelte embarqué via rust-embed
|
||||
|
||||
pub mod routes;
|
||||
pub mod ws;
|
||||
|
||||
use axum::Router;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
/// Démarre le serveur web
|
||||
pub async fn serve(host: &str, port: u16) -> anyhow::Result<()> {
|
||||
let app = Router::new()
|
||||
.merge(routes::api_routes())
|
||||
.merge(routes::static_routes());
|
||||
|
||||
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
|
||||
tracing::info!("Dashboard web démarré sur http://{}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//! Routes API REST du dashboard
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
/// Routes de l'API REST
|
||||
pub fn api_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/api/health", get(health_check))
|
||||
.route("/api/images", get(list_images))
|
||||
.route("/api/images/search", get(search_images))
|
||||
.route("/api/stats", get(stats))
|
||||
}
|
||||
|
||||
/// Routes statiques (frontend embarqué)
|
||||
pub fn static_routes() -> Router {
|
||||
// Phase 9: Servir le frontend Svelte via rust-embed
|
||||
Router::new()
|
||||
}
|
||||
|
||||
async fn health_check() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
|
||||
async fn list_images() -> &'static str {
|
||||
// Phase 9: Liste paginée des images
|
||||
"[]"
|
||||
}
|
||||
|
||||
async fn search_images() -> &'static str {
|
||||
// Phase 9: Recherche full-text
|
||||
"[]"
|
||||
}
|
||||
|
||||
async fn stats() -> &'static str {
|
||||
// Phase 9: Statistiques
|
||||
r#"{"total_images":0,"total_storage_bytes":0}"#
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! WebSocket — Mise à jour en temps réel du dashboard
|
||||
//!
|
||||
//! Phase 9: Notifie le frontend quand un nouveau screenshot est uploadé
|
||||
|
||||
// Stub — à implémenter dans Phase 9
|
||||
@@ -0,0 +1,13 @@
|
||||
//! Service Windows (NSSM / windows-service)
|
||||
//!
|
||||
//! Phase 10: Installation comme service Windows
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn install() -> Result<()> {
|
||||
todo!("Phase 10: Service install via NSSM")
|
||||
}
|
||||
|
||||
pub fn uninstall() -> Result<()> {
|
||||
todo!("Phase 10: Service uninstall")
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
//! System Tray (Windows)
|
||||
//!
|
||||
//! Phase 8: Icône system tray avec menu contextuel
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn run() -> Result<()> {
|
||||
// Phase 8: Icône tray avec Start/Stop/Status/Quit
|
||||
todo!("Phase 8: System tray")
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//! Watcher — Boucle principale de polling du clipboard
|
||||
//!
|
||||
//! Surveille le clipboard à intervalle régulier, détecte les nouvelles images,
|
||||
//! les dédoublonne et les envoie à Imago.
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::config::Config;
|
||||
use crate::dedup::Dedup;
|
||||
use crate::imago_client::ImagoClient;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct Watcher {
|
||||
config: Config,
|
||||
dedup: Dedup,
|
||||
imago: ImagoClient,
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
pub async fn new(config: Config) -> Result<Self> {
|
||||
let db_path = PathBuf::from("imagod.db");
|
||||
let dedup = Dedup::open(&db_path, config.dedup.cache_size)?;
|
||||
let imago = ImagoClient::new(
|
||||
config.imago.url.clone(),
|
||||
config.imago.api_key.clone(),
|
||||
)?;
|
||||
|
||||
Ok(Self { config, dedup, imago })
|
||||
}
|
||||
|
||||
/// Lance la boucle de polling
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
let interval = Duration::from_millis(self.config.watcher.poll_interval_ms);
|
||||
|
||||
tracing::info!(
|
||||
"Watcher démarré — polling toutes les {}ms",
|
||||
self.config.watcher.poll_interval_ms
|
||||
);
|
||||
|
||||
// TODO: Phase 6 — Boucle principale
|
||||
// loop {
|
||||
// 1. Vérifier le clipboard (Windows: arboard, Linux: N/A)
|
||||
// 2. Si nouvelle image → hash SHA-256
|
||||
// 3. Vérifier dédoublonnage
|
||||
// 4. Si nouveau → upload vers Imago
|
||||
// 5. Marquer comme vu
|
||||
// sleep(interval)
|
||||
// }
|
||||
tracing::warn!("Watcher::run() — implémentation Phase 6 à venir");
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Window Info — Titre de la fenêtre active (Windows API)
|
||||
//!
|
||||
//! Phase 6: Capture du contexte (titre fenêtre) pour enrichir les tags
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// Récupère le titre de la fenêtre active
|
||||
pub fn get_active_window_title() -> Result<Option<String>> {
|
||||
// Phase 6: Windows API — GetForegroundWindow + GetWindowText
|
||||
todo!("Phase 6: Active window title")
|
||||
}
|
||||
Reference in New Issue
Block a user