272 lines
8.6 KiB
Rust
272 lines
8.6 KiB
Rust
use std::path::Path;
|
|
|
|
use anyhow::Result;
|
|
use tokio::time;
|
|
use tracing::{info, warn};
|
|
|
|
use crate::core::db::{PostgresDb, VideoRecord, VideoStatus};
|
|
|
|
pub struct WatcherConfig {
|
|
pub directories: Vec<String>,
|
|
pub poll_interval_ms: u64,
|
|
}
|
|
|
|
impl Default for WatcherConfig {
|
|
fn default() -> Self {
|
|
let default_dir = std::env::var("MOMENTRY_SFTP_ROOT")
|
|
.unwrap_or_else(|_| "/Users/accusys/momentry/var/sftpgo/data/demo/".to_string());
|
|
Self {
|
|
directories: vec![default_dir],
|
|
poll_interval_ms: 60000,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Starts the file watcher in the background.
|
|
/// Detects new files and logs them.
|
|
pub async fn run_watcher() -> Result<()> {
|
|
let config = WatcherConfig::default();
|
|
let dirs = config.directories.clone();
|
|
|
|
if dirs.is_empty() {
|
|
warn!("No directories configured for watching.");
|
|
return Err(anyhow::anyhow!("No watch directories"));
|
|
}
|
|
|
|
info!("Starting File Watcher (detection only, no auto-modification)...");
|
|
info!("Watch directories: {:?}", dirs);
|
|
|
|
tokio::spawn(async move {
|
|
let mut interval =
|
|
time::interval(std::time::Duration::from_millis(config.poll_interval_ms));
|
|
let mut known = std::collections::HashSet::new();
|
|
loop {
|
|
interval.tick().await;
|
|
report_new_files(&dirs, &mut known).await;
|
|
}
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn report_new_files(directories: &[String], known: &mut std::collections::HashSet<String>) {
|
|
for dir in directories {
|
|
let dir_path = Path::new(dir);
|
|
if !dir_path.is_dir() {
|
|
continue;
|
|
}
|
|
let entries = match std::fs::read_dir(dir_path) {
|
|
Ok(e) => e,
|
|
_ => continue,
|
|
};
|
|
|
|
let mut current_files = std::collections::HashSet::new();
|
|
for entry in entries.flatten() {
|
|
let file_path = entry.path();
|
|
if !file_path.is_file() {
|
|
continue;
|
|
}
|
|
let fname = match file_path.file_name().and_then(|n| n.to_str()) {
|
|
Some(n) if !n.starts_with('.') && !n.ends_with(".pre.json") => n.to_string(),
|
|
_ => continue,
|
|
};
|
|
current_files.insert(fname.clone());
|
|
if !known.contains(&fname) {
|
|
info!("[WATCHER] New file detected: {} in {}", fname, dir);
|
|
if crate::core::config::get_watcher_auto_register() {
|
|
let fpath = file_path.to_string_lossy().to_string();
|
|
tokio::spawn(async move {
|
|
auto_register_file(&fpath).await;
|
|
});
|
|
}
|
|
known.insert(fname);
|
|
}
|
|
}
|
|
known.retain(|k| current_files.contains(k));
|
|
}
|
|
}
|
|
|
|
async fn auto_register_file(file_path: &str) {
|
|
let file_uuid = match pre_process_file(file_path).await {
|
|
Some(u) => u,
|
|
None => return,
|
|
};
|
|
|
|
let output_dir = std::env::var("MOMENTRY_OUTPUT_DIR")
|
|
.unwrap_or_else(|_| "/Users/accusys/momentry/output_dev".to_string());
|
|
let pre_path = std::path::PathBuf::from(&output_dir).join(format!("{}.pre.json", file_uuid));
|
|
let pre_content = match std::fs::read_to_string(&pre_path) {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
warn!("[WATCHER] Failed to read pre.json: {}", e);
|
|
return;
|
|
}
|
|
};
|
|
let pre: serde_json::Value = match serde_json::from_str(&pre_content) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
warn!("[WATCHER] Failed to parse pre.json: {}", e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let file_name = pre
|
|
.get("file_name")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("unknown")
|
|
.to_string();
|
|
let probe = pre.get("probe_json").cloned().unwrap_or_default();
|
|
let file_type = pre
|
|
.get("file_type")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("unknown")
|
|
.to_string();
|
|
let canonical_path = pre
|
|
.get("file_path")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or(file_path)
|
|
.to_string();
|
|
|
|
let duration = probe
|
|
.get("format")
|
|
.and_then(|f| f.get("duration"))
|
|
.and_then(|v| v.as_f64())
|
|
.unwrap_or(0.0);
|
|
let width = probe
|
|
.get("format")
|
|
.and_then(|f| f.get("width"))
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0) as u32;
|
|
let height = probe
|
|
.get("format")
|
|
.and_then(|f| f.get("height"))
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0) as u32;
|
|
let fps_val = probe
|
|
.get("format")
|
|
.and_then(|f| f.get("fps"))
|
|
.and_then(|v| v.as_f64())
|
|
.unwrap_or(0.0);
|
|
|
|
let record = VideoRecord {
|
|
id: 0,
|
|
file_uuid,
|
|
file_path: canonical_path,
|
|
file_name,
|
|
file_type: Some(file_type),
|
|
duration,
|
|
width,
|
|
height,
|
|
fps: fps_val,
|
|
probe_json: Some(probe),
|
|
storage: Default::default(),
|
|
status: VideoStatus::Registered,
|
|
processing_status: None,
|
|
birth_registration: None,
|
|
user_id: None,
|
|
job_id: None,
|
|
created_at: String::new(),
|
|
registration_time: None,
|
|
total_frames: 0,
|
|
parent_uuid: None,
|
|
cut_done: false,
|
|
cut_count: 0,
|
|
cut_max_duration: 0.0,
|
|
scene_done: false,
|
|
audio_tracks: None,
|
|
};
|
|
|
|
let database_url = crate::core::config::DATABASE_URL.as_str();
|
|
let db = match PostgresDb::new(database_url).await {
|
|
Ok(d) => d,
|
|
Err(e) => {
|
|
warn!("[WATCHER] Failed to connect DB for auto-register: {}", e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
match db.register_video(&record).await {
|
|
Ok(id) => info!("[WATCHER] Auto-registered {} (id={})", record.file_uuid, id),
|
|
Err(e) => warn!(
|
|
"[WATCHER] Auto-register failed for {}: {}",
|
|
record.file_uuid, e
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Pre-process a single file: compute SHA256 + probe + UUID → .pre.json
|
|
pub async fn pre_process_file(file_path: &str) -> Option<String> {
|
|
let path = std::path::Path::new(file_path);
|
|
if !path.is_file() {
|
|
return None;
|
|
}
|
|
let canonical = path.canonicalize().ok()?;
|
|
let canonical_str = canonical.to_string_lossy().to_string();
|
|
let filename = path.file_name()?.to_string_lossy().to_string();
|
|
|
|
let output_dir = std::env::var("MOMENTRY_OUTPUT_DIR")
|
|
.unwrap_or_else(|_| "/Users/accusys/momentry/output_dev".to_string());
|
|
|
|
let birthday = std::fs::metadata(&path)
|
|
.ok()
|
|
.and_then(|m| m.modified().ok())
|
|
.map(|t| {
|
|
let secs = t
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs();
|
|
chrono::DateTime::from_timestamp(secs as i64, 0)
|
|
.map(|dt| dt.to_rfc3339())
|
|
.unwrap_or_else(|| chrono::Utc::now().to_rfc3339())
|
|
})
|
|
.unwrap_or_else(|| chrono::Utc::now().to_rfc3339());
|
|
|
|
let mac = crate::core::storage::uuid::get_mac_address();
|
|
let file_uuid =
|
|
crate::core::storage::uuid::compute_birth_uuid(&mac, &birthday, &canonical_str, &filename);
|
|
|
|
let pre_path = std::path::PathBuf::from(&output_dir).join(format!("{}.pre.json", file_uuid));
|
|
if pre_path.exists() {
|
|
info!("[PRE-PROCESS] Already pre-processed: {}", filename);
|
|
return Some(file_uuid);
|
|
}
|
|
|
|
info!("[PRE-PROCESS] Pre-processing: {} → {}", filename, file_uuid);
|
|
|
|
let content_hash =
|
|
crate::core::storage::content_hash::compute_sha256(&path).unwrap_or_default();
|
|
|
|
let scripts_dir = std::env::var("MOMENTRY_SCRIPTS_DIR")
|
|
.unwrap_or_else(|_| "/Users/accusys/momentry_core_0.1/scripts".to_string());
|
|
let python_path = std::env::var("MOMENTRY_PYTHON_PATH")
|
|
.unwrap_or_else(|_| "/opt/homebrew/bin/python3.11".to_string());
|
|
let probe_json =
|
|
crate::core::probe::unified::unified_probe(&path, &scripts_dir, &python_path).await;
|
|
|
|
let file_type = probe_json
|
|
.get("format")
|
|
.and_then(|f| f.get("file_type"))
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("unknown")
|
|
.to_string();
|
|
|
|
let pre_data = serde_json::json!({
|
|
"file_name": filename,
|
|
"file_path": canonical_str,
|
|
"content_hash": content_hash,
|
|
"probe_json": probe_json,
|
|
"birthday": birthday,
|
|
"file_uuid": file_uuid,
|
|
"file_size": std::fs::metadata(&path).ok().map(|m| m.len()).unwrap_or(0),
|
|
"file_type": file_type,
|
|
"pre_processed_at": chrono::Utc::now().to_rfc3339(),
|
|
});
|
|
|
|
if let Ok(content) = serde_json::to_string_pretty(&pre_data) {
|
|
if std::fs::write(&pre_path, content).is_ok() {
|
|
info!("[PRE-PROCESS] {} → {}.pre.json", filename, file_uuid);
|
|
}
|
|
}
|
|
Some(file_uuid)
|
|
}
|