add fav + opti
This commit is contained in:
@ -5,6 +5,7 @@ use image::{ExtendedColorType, ImageEncoder};
|
||||
use rusqlite::Connection;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::io::Cursor;
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
@ -29,25 +30,44 @@ impl Database {
|
||||
PRAGMA foreign_keys=ON;",
|
||||
)?;
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL);
|
||||
INSERT OR IGNORE INTO schema_version (version)
|
||||
SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM schema_version);",
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
timestamp INTEGER NOT NULL,
|
||||
pinned INTEGER NOT NULL DEFAULT 0
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
let version: i64 = conn
|
||||
.query_row("SELECT version FROM schema_version", [], |r| r.get(0))
|
||||
.unwrap_or(1);
|
||||
|
||||
if version < 2 {
|
||||
let col_exists: bool = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('history') WHERE name='pinned'",
|
||||
[],
|
||||
|r| r.get::<_, i64>(0),
|
||||
)
|
||||
.unwrap_or(0)
|
||||
> 0;
|
||||
|
||||
if !col_exists {
|
||||
conn.execute_batch(
|
||||
"ALTER TABLE history ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;",
|
||||
)?;
|
||||
}
|
||||
conn.execute("UPDATE schema_version SET version = 2", [])?;
|
||||
println!("DB migrée → schema v2 (colonne `pinned`)");
|
||||
}
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_history_content ON history(content);
|
||||
CREATE INDEX IF NOT EXISTS idx_history_timestamp ON history(timestamp DESC);",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_history_content ON history(content);
|
||||
CREATE INDEX IF NOT EXISTS idx_history_timestamp ON history(timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_history_pinned ON history(pinned);",
|
||||
)?;
|
||||
|
||||
conn.execute_batch(
|
||||
@ -85,25 +105,25 @@ impl Database {
|
||||
.flat_map(|rgba| [rgba[0], rgba[1], rgba[2]])
|
||||
.collect();
|
||||
|
||||
let mut jpeg_buf = Vec::new();
|
||||
JpegEncoder::new_with_quality(&mut jpeg_buf, 70).write_image(
|
||||
let mut buf = Vec::new();
|
||||
JpegEncoder::new_with_quality(Cursor::new(&mut buf), 70).write_image(
|
||||
&rgb,
|
||||
img.width,
|
||||
img.height,
|
||||
ExtendedColorType::Rgb8,
|
||||
)?;
|
||||
|
||||
if jpeg_buf.len() > self.max_entry_size_bytes {
|
||||
if buf.len() > self.max_entry_size_bytes {
|
||||
eprintln!(
|
||||
"Image rejetée dans DB : JPEG {} Ko > limite {} Ko",
|
||||
jpeg_buf.len() / 1024,
|
||||
"Image rejetée : JPEG {} Ko > limite {} Ko",
|
||||
buf.len() / 1024,
|
||||
self.max_entry_size_bytes / 1024
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let path = img.file_path(&self.dir_path);
|
||||
fs::write(&path, &jpeg_buf)?;
|
||||
fs::write(&path, &buf)?;
|
||||
}
|
||||
None => return Ok(()),
|
||||
}
|
||||
@ -112,7 +132,13 @@ impl Database {
|
||||
};
|
||||
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO history (type, content, timestamp) VALUES (?1, ?2, ?3)",
|
||||
"INSERT OR REPLACE INTO history (type, content, timestamp, pinned)
|
||||
VALUES (?1, ?2, ?3,
|
||||
COALESCE(
|
||||
(SELECT pinned FROM history WHERE content = ?2),
|
||||
0
|
||||
)
|
||||
)",
|
||||
(kind, &content, ts),
|
||||
)?;
|
||||
|
||||
@ -130,10 +156,11 @@ impl Database {
|
||||
let image_files: Vec<String> = {
|
||||
let mut stmt = tx.prepare(
|
||||
"SELECT content FROM history
|
||||
WHERE type = 'image'
|
||||
AND id NOT IN (
|
||||
SELECT id FROM history ORDER BY timestamp DESC LIMIT ?1
|
||||
)",
|
||||
WHERE type = 'image' AND pinned = 0
|
||||
AND id NOT IN (
|
||||
SELECT id FROM history WHERE pinned = 0
|
||||
ORDER BY timestamp DESC LIMIT ?1
|
||||
)",
|
||||
)?;
|
||||
stmt.query_map([self.max_entries as i64], |row| row.get(0))?
|
||||
.filter_map(|r| r.ok())
|
||||
@ -141,8 +168,11 @@ impl Database {
|
||||
};
|
||||
|
||||
tx.execute(
|
||||
"DELETE FROM history WHERE id NOT IN (
|
||||
SELECT id FROM history ORDER BY timestamp DESC LIMIT ?1
|
||||
"DELETE FROM history
|
||||
WHERE pinned = 0
|
||||
AND id NOT IN (
|
||||
SELECT id FROM history WHERE pinned = 0
|
||||
ORDER BY timestamp DESC LIMIT ?1
|
||||
)",
|
||||
[self.max_entries as i64],
|
||||
)?;
|
||||
@ -163,7 +193,10 @@ impl Database {
|
||||
|
||||
pub fn read_history(&self, limit: usize) -> Result<Vec<ClipboardEntry>, Box<dyn Error>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT type, content, timestamp FROM history ORDER BY timestamp DESC LIMIT ?1",
|
||||
"SELECT type, content, timestamp, pinned
|
||||
FROM history
|
||||
ORDER BY pinned DESC, timestamp DESC
|
||||
LIMIT ?1",
|
||||
)?;
|
||||
|
||||
let rows = stmt.query_map([limit as i64], |row| {
|
||||
@ -171,12 +204,13 @@ impl Database {
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, i64>(2)?,
|
||||
row.get::<_, bool>(3)?,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for row in rows {
|
||||
let (ty, content, ts_ms) = row?;
|
||||
let (ty, content, ts_ms, pinned) = row?;
|
||||
let timestamp = UNIX_EPOCH + Duration::from_millis(ts_ms as u64);
|
||||
let data = if ty == "text" {
|
||||
ClipboardData::Text(content)
|
||||
@ -192,11 +226,23 @@ impl Database {
|
||||
entries.push(ClipboardEntry {
|
||||
content: data,
|
||||
timestamp,
|
||||
pinned,
|
||||
});
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn set_pin(&self, content: &str, pinned: bool) -> Result<(), Box<dyn Error>> {
|
||||
let rows = self.conn.execute(
|
||||
"UPDATE history SET pinned = ?1 WHERE content = ?2",
|
||||
(pinned as i32, content),
|
||||
)?;
|
||||
if rows == 0 {
|
||||
return Err(format!("Entrée introuvable pour pin : {content}").into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_entry_by_content(&self, content: &str) -> Result<(), Box<dyn Error>> {
|
||||
self.conn
|
||||
.execute("DELETE FROM history WHERE content = ?1", [content])?;
|
||||
@ -221,14 +267,19 @@ impl Database {
|
||||
let tx = self.conn.unchecked_transaction()?;
|
||||
|
||||
let image_files: Vec<String> = {
|
||||
let mut stmt =
|
||||
tx.prepare("SELECT content FROM history WHERE type = 'image' AND timestamp < ?1")?;
|
||||
let mut stmt = tx.prepare(
|
||||
"SELECT content FROM history
|
||||
WHERE type = 'image' AND pinned = 0 AND timestamp < ?1",
|
||||
)?;
|
||||
stmt.query_map([cutoff_ms], |row| row.get(0))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect()
|
||||
};
|
||||
|
||||
let count = tx.execute("DELETE FROM history WHERE timestamp < ?1", [cutoff_ms])?;
|
||||
let count = tx.execute(
|
||||
"DELETE FROM history WHERE timestamp < ?1 AND pinned = 0",
|
||||
[cutoff_ms],
|
||||
)?;
|
||||
|
||||
tx.commit()?;
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixListener;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
@ -16,6 +16,8 @@ const IPC_MAX_REQUEST_BYTES: usize = 4 * 1024 * 1024;
|
||||
pub struct HistoryItem {
|
||||
pub content: String,
|
||||
pub timestamp: i64,
|
||||
#[serde(default)]
|
||||
pub pinned: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@ -36,6 +38,10 @@ pub enum IpcRequest {
|
||||
AddEntry {
|
||||
content: String,
|
||||
},
|
||||
PinEntry {
|
||||
content: String,
|
||||
pinned: bool,
|
||||
},
|
||||
ClearHistory,
|
||||
}
|
||||
|
||||
@ -52,7 +58,12 @@ fn reply(stream: &mut std::os::unix::net::UnixStream, resp: IpcResponse) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_server(db: Arc<Mutex<Database>>, crypto: Arc<Crypto>, socket_path: &Path) {
|
||||
pub fn start_server(
|
||||
db: Arc<Mutex<Database>>,
|
||||
crypto: Arc<Crypto>,
|
||||
socket_path: &Path,
|
||||
data_dir: Arc<PathBuf>,
|
||||
) {
|
||||
if socket_path.exists() {
|
||||
let _ = fs::remove_file(socket_path);
|
||||
}
|
||||
@ -84,188 +95,194 @@ pub fn start_server(db: Arc<Mutex<Database>>, crypto: Arc<Crypto>, socket_path:
|
||||
|
||||
let db_clone = Arc::clone(&db);
|
||||
let crypto_clone = Arc::clone(&crypto);
|
||||
let data_dir_clone = Arc::clone(&data_dir);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
let mut tmp = [0u8; 4096];
|
||||
|
||||
loop {
|
||||
match stream.read(&mut tmp) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
buf.extend_from_slice(&tmp[..n]);
|
||||
if buf.len() > IPC_MAX_REQUEST_BYTES {
|
||||
eprintln!("IPC : requête trop grande, abandon");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(e)
|
||||
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||
|| e.kind() == std::io::ErrorKind::TimedOut =>
|
||||
{
|
||||
eprintln!("IPC : timeout de lecture");
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("IPC read error : {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let buf_str = match String::from_utf8(buf) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("IPC : requête non-UTF8 : {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let req = match serde_json::from_str::<IpcRequest>(&buf_str) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("IPC parse error : {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match req {
|
||||
IpcRequest::GetHistory { limit } => {
|
||||
// Limite à 1000 pour éviter les requêtes abusives
|
||||
let limit = limit.min(1000);
|
||||
let lock = db_clone.lock().unwrap();
|
||||
let history = lock.read_history(limit).unwrap_or_default();
|
||||
let items: Vec<HistoryItem> = history
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
let content = match e.content {
|
||||
ClipboardData::Text(t) => t,
|
||||
ClipboardData::Image(img) => format!("{}.jpg", img.id),
|
||||
};
|
||||
let ts = e
|
||||
.timestamp
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
as i64;
|
||||
HistoryItem {
|
||||
content,
|
||||
timestamp: ts,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
reply(&mut stream, IpcResponse::History(items));
|
||||
}
|
||||
|
||||
IpcRequest::SetClipboard { content } => {
|
||||
let actual = if Crypto::is_legacy_encrypted(&content) {
|
||||
crypto_clone.decrypt(&content).unwrap_or_else(|e| {
|
||||
eprintln!("Impossible de déchiffrer l'entrée enc: : {e}");
|
||||
content.clone()
|
||||
})
|
||||
} else if Crypto::is_password_encrypted(&content) {
|
||||
reply(
|
||||
&mut stream,
|
||||
IpcResponse::Error(
|
||||
"Entrée chiffrée par mot de passe : déchiffrez-la côté client avant de coller"
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
content
|
||||
};
|
||||
|
||||
match arboard::Clipboard::new() {
|
||||
Ok(mut cb) => {
|
||||
if actual.ends_with(".jpg") || actual.ends_with(".png") {
|
||||
if let Some(dirs) =
|
||||
directories::ProjectDirs::from("com", "zefad", "rklipd")
|
||||
{
|
||||
let path = dirs.data_dir().join("images").join(&actual);
|
||||
if let Ok(img) = image::open(&path) {
|
||||
let rgba = img.into_rgba8();
|
||||
let (w, h) =
|
||||
(rgba.width() as usize, rgba.height() as usize);
|
||||
let _ = cb.set_image(arboard::ImageData {
|
||||
width: w,
|
||||
height: h,
|
||||
bytes: std::borrow::Cow::Owned(rgba.into_raw()),
|
||||
});
|
||||
reply(&mut stream, IpcResponse::Ok);
|
||||
} else {
|
||||
reply(
|
||||
&mut stream,
|
||||
IpcResponse::Error(format!(
|
||||
"Image introuvable : {actual}"
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = cb.set_text(actual);
|
||||
reply(&mut stream, IpcResponse::Ok);
|
||||
}
|
||||
}
|
||||
Err(e) => reply(&mut stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
IpcRequest::DeleteEntry { content } => {
|
||||
{
|
||||
let lock = db_clone.lock().unwrap();
|
||||
let _ = lock.delete_entry_by_content(&content);
|
||||
}
|
||||
if !content.starts_with("enc:")
|
||||
&& !content.starts_with("enc2:")
|
||||
&& (content.ends_with(".jpg") || content.ends_with(".png"))
|
||||
{
|
||||
if let Some(dirs) =
|
||||
directories::ProjectDirs::from("com", "zefad", "rklipd")
|
||||
{
|
||||
let p = dirs.data_dir().join("images").join(&content);
|
||||
if p.exists() {
|
||||
let _ = fs::remove_file(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
reply(&mut stream, IpcResponse::Ok);
|
||||
}
|
||||
|
||||
IpcRequest::UpdateEntry {
|
||||
old_content,
|
||||
new_content,
|
||||
} => {
|
||||
let lock = db_clone.lock().unwrap();
|
||||
match lock.update_entry_content(&old_content, &new_content) {
|
||||
Ok(_) => reply(&mut stream, IpcResponse::Ok),
|
||||
Err(e) => reply(&mut stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
IpcRequest::AddEntry { content } => {
|
||||
let entry = ClipboardEntry {
|
||||
content: ClipboardData::Text(content),
|
||||
timestamp: SystemTime::now(),
|
||||
};
|
||||
let lock = db_clone.lock().unwrap();
|
||||
match lock.append(entry) {
|
||||
Ok(_) => reply(&mut stream, IpcResponse::Ok),
|
||||
Err(e) => reply(&mut stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
IpcRequest::ClearHistory => {
|
||||
let lock = db_clone.lock().unwrap();
|
||||
match lock.clear_history() {
|
||||
Ok(_) => reply(&mut stream, IpcResponse::Ok),
|
||||
Err(e) => reply(&mut stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
handle_connection(&mut stream, db_clone, crypto_clone, data_dir_clone);
|
||||
});
|
||||
}
|
||||
Err(e) => eprintln!("Erreur connexion IPC : {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_connection(
|
||||
stream: &mut std::os::unix::net::UnixStream,
|
||||
db: Arc<Mutex<Database>>,
|
||||
crypto: Arc<Crypto>,
|
||||
data_dir: Arc<PathBuf>,
|
||||
) {
|
||||
let mut buf = Vec::new();
|
||||
let mut tmp = [0u8; 4096];
|
||||
|
||||
loop {
|
||||
match stream.read(&mut tmp) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
buf.extend_from_slice(&tmp[..n]);
|
||||
if buf.len() > IPC_MAX_REQUEST_BYTES {
|
||||
eprintln!("IPC : requête trop grande, abandon");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(e)
|
||||
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||
|| e.kind() == std::io::ErrorKind::TimedOut =>
|
||||
{
|
||||
eprintln!("IPC : timeout de lecture");
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("IPC read error : {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let buf_str = match String::from_utf8(buf) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("IPC : requête non-UTF8 : {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let req = match serde_json::from_str::<IpcRequest>(&buf_str) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("IPC parse error : {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match req {
|
||||
IpcRequest::GetHistory { limit } => {
|
||||
let limit = limit.min(1000);
|
||||
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let history = lock.read_history(limit).unwrap_or_default();
|
||||
let items: Vec<HistoryItem> = history
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
let content = match e.content {
|
||||
ClipboardData::Text(t) => t,
|
||||
ClipboardData::Image(img) => format!("{}.jpg", img.id),
|
||||
};
|
||||
let ts = e
|
||||
.timestamp
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
HistoryItem {
|
||||
content,
|
||||
timestamp: ts,
|
||||
pinned: e.pinned,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
reply(stream, IpcResponse::History(items));
|
||||
}
|
||||
|
||||
IpcRequest::SetClipboard { content } => {
|
||||
let actual = if Crypto::is_legacy_encrypted(&content) {
|
||||
crypto.decrypt(&content).unwrap_or_else(|e| {
|
||||
eprintln!("Impossible de déchiffrer l'entrée enc: : {e}");
|
||||
content.clone()
|
||||
})
|
||||
} else if Crypto::is_password_encrypted(&content) {
|
||||
reply(
|
||||
stream,
|
||||
IpcResponse::Error(
|
||||
"Entrée chiffrée par mot de passe : déchiffrez côté client avant de coller"
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
content
|
||||
};
|
||||
|
||||
match arboard::Clipboard::new() {
|
||||
Ok(mut cb) => {
|
||||
if actual.ends_with(".jpg") || actual.ends_with(".png") {
|
||||
let path = data_dir.join("images").join(&actual);
|
||||
if let Ok(img) = image::open(&path) {
|
||||
let rgba = img.into_rgba8();
|
||||
let (w, h) = (rgba.width() as usize, rgba.height() as usize);
|
||||
let _ = cb.set_image(arboard::ImageData {
|
||||
width: w,
|
||||
height: h,
|
||||
bytes: std::borrow::Cow::Owned(rgba.into_raw()),
|
||||
});
|
||||
reply(stream, IpcResponse::Ok);
|
||||
} else {
|
||||
reply(
|
||||
stream,
|
||||
IpcResponse::Error(format!("Image introuvable : {actual}")),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let _ = cb.set_text(actual);
|
||||
reply(stream, IpcResponse::Ok);
|
||||
}
|
||||
}
|
||||
Err(e) => reply(stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
IpcRequest::DeleteEntry { content } => {
|
||||
{
|
||||
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let _ = lock.delete_entry_by_content(&content);
|
||||
}
|
||||
if !Crypto::is_any_encrypted(&content)
|
||||
&& (content.ends_with(".jpg") || content.ends_with(".png"))
|
||||
{
|
||||
let p = data_dir.join("images").join(&content);
|
||||
if p.exists() {
|
||||
let _ = fs::remove_file(p);
|
||||
}
|
||||
}
|
||||
reply(stream, IpcResponse::Ok);
|
||||
}
|
||||
|
||||
IpcRequest::UpdateEntry {
|
||||
old_content,
|
||||
new_content,
|
||||
} => {
|
||||
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
||||
match lock.update_entry_content(&old_content, &new_content) {
|
||||
Ok(_) => reply(stream, IpcResponse::Ok),
|
||||
Err(e) => reply(stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
IpcRequest::AddEntry { content } => {
|
||||
let entry = ClipboardEntry {
|
||||
content: ClipboardData::Text(content),
|
||||
timestamp: SystemTime::now(),
|
||||
pinned: false,
|
||||
};
|
||||
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
||||
match lock.append(entry) {
|
||||
Ok(_) => reply(stream, IpcResponse::Ok),
|
||||
Err(e) => reply(stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
IpcRequest::PinEntry { content, pinned } => {
|
||||
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
||||
match lock.set_pin(&content, pinned) {
|
||||
Ok(_) => reply(stream, IpcResponse::Ok),
|
||||
Err(e) => reply(stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
IpcRequest::ClearHistory => {
|
||||
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
||||
match lock.clear_history() {
|
||||
Ok(_) => reply(stream, IpcResponse::Ok),
|
||||
Err(e) => reply(stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,17 +31,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
let proj_dirs =
|
||||
ProjectDirs::from("com", "zefad", "rklipd").expect("Impossible d'ouvrir le répertoire");
|
||||
let dir_path = proj_dirs.data_dir();
|
||||
let dir_path = proj_dirs.data_dir().to_path_buf();
|
||||
let dir_path_str = dir_path.to_str().expect("Chemin invalide").to_string();
|
||||
|
||||
let db = Arc::new(Mutex::new(Database::init(&dir_path_str, &config)?));
|
||||
let crypto = Arc::new(Crypto::load_or_create(dir_path)?);
|
||||
let crypto = Arc::new(Crypto::load_or_create(&dir_path)?);
|
||||
|
||||
let socket_path = dir_path.join("rklip.sock");
|
||||
let db_for_ipc = Arc::clone(&db);
|
||||
let crypto_for_ipc = Arc::clone(&crypto);
|
||||
let data_dir = Arc::new(dir_path.clone());
|
||||
std::thread::spawn(move || {
|
||||
crate::ipc::start_server(db_for_ipc, crypto_for_ipc, &socket_path);
|
||||
crate::ipc::start_server(db_for_ipc, crypto_for_ipc, &socket_path, data_dir);
|
||||
});
|
||||
|
||||
if let Some(days) = config.expiry_days {
|
||||
@ -49,12 +50,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
{
|
||||
let lock = db_for_expiry.lock().unwrap();
|
||||
let lock = db_for_expiry.lock().unwrap_or_else(|p| p.into_inner());
|
||||
match lock.delete_entries_older_than(days) {
|
||||
Ok(0) => {}
|
||||
Ok(n) => {
|
||||
println!("Expiration : {n} entrée(s) supprimée(s) (> {days} jours)")
|
||||
}
|
||||
Ok(n) => println!("Expiration : {n} entrée(s) > {days} jours supprimée(s)"),
|
||||
Err(e) => eprintln!("Erreur expiration : {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ use uuid::Uuid;
|
||||
pub struct ClipboardEntry {
|
||||
pub content: ClipboardData,
|
||||
pub timestamp: SystemTime,
|
||||
pub pinned: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@ -9,13 +9,10 @@ pub fn start(db: Arc<Mutex<Database>>, clipboard: Clipboard) -> Result<(), Box<d
|
||||
|
||||
std::thread::spawn(move || {
|
||||
for entry in rx {
|
||||
let lock = match db.lock() {
|
||||
Ok(l) => l,
|
||||
Err(poisoned) => {
|
||||
eprintln!("Mutex DB empoisonné, récupération forcée");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
let lock = db.lock().unwrap_or_else(|poisoned| {
|
||||
eprintln!("Mutex DB empoisonné, récupération forcée");
|
||||
poisoned.into_inner()
|
||||
});
|
||||
if let Err(e) = lock.append(entry) {
|
||||
eprintln!("SQLite write error: {e}");
|
||||
} else {
|
||||
|
||||
@ -45,6 +45,7 @@ pub fn start(
|
||||
ClipboardEntry {
|
||||
content: ClipboardData::Text(text),
|
||||
timestamp: SystemTime::now(),
|
||||
pinned: false,
|
||||
}
|
||||
} else {
|
||||
let hash = hash_bytes(data);
|
||||
@ -83,6 +84,7 @@ pub fn start(
|
||||
id: Uuid::new_v4(),
|
||||
}),
|
||||
timestamp: SystemTime::now(),
|
||||
pinned: false,
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@ -94,6 +94,7 @@ fn handle_clipboard_event(
|
||||
.send(ClipboardEntry {
|
||||
content: ClipboardData::Text(text),
|
||||
timestamp: SystemTime::now(),
|
||||
pinned: false,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
@ -137,6 +138,7 @@ fn handle_clipboard_event(
|
||||
id: Uuid::new_v4(),
|
||||
}),
|
||||
timestamp: SystemTime::now(),
|
||||
pinned: false,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user