Menu
Public repository / d63e826eb482cb963477774b04fe8a84f23f0e45
AkurAI-Drive / client/src/util.rs
Latest change feb7ac86b5e60d55b429eacb7146cf0dc32c6628 - Add native Linux sync client (akurai-drive-sync) by olibuijr
//! Small filesystem / hashing helpers shared across the sync engine.
use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
use std::io::Read;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
/// Prefix for atomic-download temp files; the walker skips these.
pub const TMP_PREFIX: &str = ".akd-tmp-";
/// Stream a file through SHA-256 and return the lowercase hex digest.
pub fn sha256_file(path: &Path) -> Result<String> {
let mut file =
std::fs::File::open(path).with_context(|| format!("hashing {}", path.display()))?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 64 * 1024];
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
Ok(hex(&hasher.finalize()))
}
/// SHA-256 of a byte slice as lowercase hex (used for short conflict ids).
pub fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex(&hasher.finalize())
}
/// A short (6 hex char) id derived from a seed string plus the current time —
/// enough to disambiguate conflict copies without a uuid dependency.
pub fn short_id(seed: &str) -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let digest = sha256_hex(format!("{seed}:{now}").as_bytes());
digest[..6].to_string()
}
/// File modification time in whole milliseconds since the Unix epoch.
pub fn mtime_millis(path: &Path) -> Result<i64> {
let meta = std::fs::metadata(path)?;
let modified = meta.modified()?;
let millis = modified
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
Ok(millis)
}
fn hex(bytes: &[u8]) -> String {
use std::fmt::Write;
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(s, "{b:02x}");
}
s
}
/// Best-effort content type from a key's extension. Defaults to
/// `application/octet-stream`.
pub fn content_type_for(key: &str) -> &'static str {
let ext = key.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
match ext.as_str() {
"txt" | "md" | "log" => "text/plain",
"html" | "htm" => "text/html",
"css" => "text/css",
"csv" => "text/csv",
"json" => "application/json",
"js" | "mjs" => "application/javascript",
"xml" => "application/xml",
"pdf" => "application/pdf",
"zip" => "application/zip",
"gz" | "tgz" => "application/gzip",
"tar" => "application/x-tar",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"webp" => "image/webp",
"svg" => "image/svg+xml",
"mp4" => "video/mp4",
"mp3" => "audio/mpeg",
"wav" => "audio/wav",
_ => "application/octet-stream",
}
}