Add user friendly message about permissions and hint, add message about AVX lack of support
This commit is contained in:
parent
36fc004a78
commit
1cdb1d7eb0
3 changed files with 122 additions and 7 deletions
47
src/error.rs
47
src/error.rs
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// User-facing hint for Docker permission errors. Used in error messages and startup checks.
|
||||||
|
pub const PERMISSION_HINT: &str = "\
|
||||||
|
Make sure that the user running the Docker container has access to the config and output folders:\n\
|
||||||
|
- In docker-compose.yml add `user: 1000:1000`\n\
|
||||||
|
- Then run `chown -R 1000:1000 path/to/output path/to/config`";
|
||||||
|
|
||||||
/// Main error type for the application.
|
/// Main error type for the application.
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|
@ -48,5 +54,46 @@ pub enum Error {
|
||||||
Cancelled,
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
/// This is for messages shown in the UI. Keep `self.to_string()` for logs.
|
||||||
|
pub fn user_message(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Error::Io(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||||
|
format!("Permission denied: {}\n\n{}", e, PERMISSION_HINT)
|
||||||
|
}
|
||||||
|
Error::Http(e) => {
|
||||||
|
let msg = e.to_string();
|
||||||
|
if e.is_connect() {
|
||||||
|
if msg.contains("dns error") || msg.contains("resolve") {
|
||||||
|
format!(
|
||||||
|
"Could not resolve hostname. \
|
||||||
|
If running in Docker, use the container name \
|
||||||
|
(e.g. `http://immich-server:2283`) instead of `localhost`.\n\n\
|
||||||
|
Raw error: {}",
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Connection refused. Is Immich running at this address?\n\n\
|
||||||
|
Raw error: {}",
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if e.is_timeout() {
|
||||||
|
format!(
|
||||||
|
"Connection timed out. \
|
||||||
|
Check that the URL is reachable from this container.\n\n\
|
||||||
|
Raw error: {}",
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => other.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convenience Result type.
|
/// Convenience Result type.
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
|
||||||
|
|
@ -166,12 +166,13 @@ pub async fn run_job(state: AppState, params: JobParams, cancel_token: Cancellat
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Job failed: {}", e);
|
tracing::error!("Job failed: {}", e);
|
||||||
|
let friendly = e.user_message();
|
||||||
state
|
state
|
||||||
.update_progress(Progress {
|
.update_progress(Progress {
|
||||||
status: JobStatus::Error(e.to_string()),
|
status: JobStatus::Error(friendly.clone()),
|
||||||
completed: final_progress.completed,
|
completed: final_progress.completed,
|
||||||
total: final_progress.total,
|
total: final_progress.total,
|
||||||
message: Some(e.to_string()),
|
message: Some(friendly),
|
||||||
skip_stats: final_progress.skip_stats,
|
skip_stats: final_progress.skip_stats,
|
||||||
person_id: final_progress.person_id,
|
person_id: final_progress.person_id,
|
||||||
person_name: final_progress.person_name,
|
person_name: final_progress.person_name,
|
||||||
|
|
@ -201,7 +202,9 @@ async fn run_job_inner(
|
||||||
|
|
||||||
// Create output directories
|
// Create output directories
|
||||||
let images_dir = person_dir.join("images");
|
let images_dir = person_dir.join("images");
|
||||||
tokio::fs::create_dir_all(&images_dir).await?;
|
tokio::fs::create_dir_all(&images_dir)
|
||||||
|
.await
|
||||||
|
.map_err(|e| permission_aware_io_error(e, &images_dir))?;
|
||||||
|
|
||||||
// Create debug directories if enabled
|
// Create debug directories if enabled
|
||||||
let debug = if config.processing.output.keep_intermediates {
|
let debug = if config.processing.output.keep_intermediates {
|
||||||
|
|
@ -516,6 +519,19 @@ async fn run_job_inner(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a permission-denied I/O error into a Config error with Docker fix instructions.
|
||||||
|
fn permission_aware_io_error(e: std::io::Error, path: &std::path::Path) -> Error {
|
||||||
|
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||||
|
Error::Config(format!(
|
||||||
|
"Permission denied for '{}'. {}",
|
||||||
|
path.display(),
|
||||||
|
crate::error::PERMISSION_HINT
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Error::Io(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Find the face data for a specific person in an asset.
|
/// Find the face data for a specific person in an asset.
|
||||||
fn find_face_for_person(asset: &Asset, person_id: &str) -> Option<FaceData> {
|
fn find_face_for_person(asset: &Asset, person_id: &str) -> Option<FaceData> {
|
||||||
let people = asset.people.as_ref()?;
|
let people = asset.people.as_ref()?;
|
||||||
|
|
|
||||||
60
src/main.rs
60
src/main.rs
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use immich_timelapse::{
|
use immich_timelapse::{
|
||||||
config::{Config, CONFIG_PATH},
|
config::{Config, CONFIG_PATH},
|
||||||
|
error::PERMISSION_HINT,
|
||||||
models::DlibLandmarks,
|
models::DlibLandmarks,
|
||||||
web,
|
web,
|
||||||
};
|
};
|
||||||
|
|
@ -33,6 +34,21 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let config = load_config()?;
|
let config = load_config()?;
|
||||||
tracing::info!("Configuration loaded");
|
tracing::info!("Configuration loaded");
|
||||||
|
|
||||||
|
// Check AVX support (required by ONNX models, x86_64 only)
|
||||||
|
#[cfg(target_arch = "x86_64")]
|
||||||
|
{
|
||||||
|
if !std::arch::is_x86_feature_detected!("avx") {
|
||||||
|
tracing::error!(
|
||||||
|
"This CPU does not support AVX instructions, which are required by the ML models \
|
||||||
|
(dlib landmarks, DMHead pose estimation). The application cannot run on this hardware."
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check output directory writability
|
||||||
|
check_output_dir(&config);
|
||||||
|
|
||||||
// Check ffmpeg availability
|
// Check ffmpeg availability
|
||||||
match immich_timelapse::video::check_ffmpeg().await {
|
match immich_timelapse::video::check_ffmpeg().await {
|
||||||
Ok(version) => tracing::info!("FFmpeg available: {}", version),
|
Ok(version) => tracing::info!("FFmpeg available: {}", version),
|
||||||
|
|
@ -64,6 +80,43 @@ async fn main() -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_output_dir(config: &Config) {
|
||||||
|
let dir = &config.output_dir;
|
||||||
|
if let Err(e) = std::fs::create_dir_all(dir) {
|
||||||
|
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||||
|
tracing::warn!(
|
||||||
|
"Cannot create output directory '{}': permission denied. {}",
|
||||||
|
dir.display(),
|
||||||
|
PERMISSION_HINT
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::warn!("Cannot create output directory '{}': {}", dir.display(), e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let test_file = dir.join(".writetest");
|
||||||
|
match std::fs::write(&test_file, b"ok") {
|
||||||
|
Ok(()) => {
|
||||||
|
let _ = std::fs::remove_file(&test_file);
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||||
|
tracing::warn!(
|
||||||
|
"Output directory '{}' is not writable: permission denied. {}",
|
||||||
|
dir.display(),
|
||||||
|
PERMISSION_HINT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"Output directory '{}' is not writable: {}",
|
||||||
|
dir.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn load_config() -> anyhow::Result<Config> {
|
fn load_config() -> anyhow::Result<Config> {
|
||||||
let config_path = std::path::Path::new(CONFIG_PATH);
|
let config_path = std::path::Path::new(CONFIG_PATH);
|
||||||
|
|
||||||
|
|
@ -82,10 +135,9 @@ fn load_config() -> anyhow::Result<Config> {
|
||||||
if msg.contains("permission denied") || msg.contains("Permission denied") {
|
if msg.contains("permission denied") || msg.contains("Permission denied") {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Could not write default config to {}: permission denied. \
|
"Could not write default config to {}: permission denied. \
|
||||||
If running in Docker, ensure the config directory is writable \
|
{} Continuing with in-memory defaults.",
|
||||||
(e.g. mount a volume with the correct permissions: \
|
CONFIG_PATH,
|
||||||
-v /host/config:/app/config). Continuing with in-memory defaults.",
|
PERMISSION_HINT
|
||||||
CONFIG_PATH
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("Could not write default config to {}: {}", CONFIG_PATH, e);
|
tracing::warn!("Could not write default config to {}: {}", CONFIG_PATH, e);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue