Add user friendly message about permissions and hint, add message about AVX lack of support

This commit is contained in:
Arnaud_Cayrol 2026-02-15 10:53:32 +01:00
parent 36fc004a78
commit 1cdb1d7eb0
3 changed files with 122 additions and 7 deletions

View file

@ -2,6 +2,12 @@
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.
#[derive(Error, Debug)]
pub enum Error {
@ -48,5 +54,46 @@ pub enum Error {
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.
pub type Result<T> = std::result::Result<T, Error>;

View file

@ -166,12 +166,13 @@ pub async fn run_job(state: AppState, params: JobParams, cancel_token: Cancellat
}
Err(e) => {
tracing::error!("Job failed: {}", e);
let friendly = e.user_message();
state
.update_progress(Progress {
status: JobStatus::Error(e.to_string()),
status: JobStatus::Error(friendly.clone()),
completed: final_progress.completed,
total: final_progress.total,
message: Some(e.to_string()),
message: Some(friendly),
skip_stats: final_progress.skip_stats,
person_id: final_progress.person_id,
person_name: final_progress.person_name,
@ -201,7 +202,9 @@ async fn run_job_inner(
// Create output directories
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
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.
fn find_face_for_person(asset: &Asset, person_id: &str) -> Option<FaceData> {
let people = asset.people.as_ref()?;

View file

@ -4,6 +4,7 @@
use immich_timelapse::{
config::{Config, CONFIG_PATH},
error::PERMISSION_HINT,
models::DlibLandmarks,
web,
};
@ -33,6 +34,21 @@ async fn main() -> anyhow::Result<()> {
let config = load_config()?;
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
match immich_timelapse::video::check_ffmpeg().await {
Ok(version) => tracing::info!("FFmpeg available: {}", version),
@ -64,6 +80,43 @@ async fn main() -> anyhow::Result<()> {
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> {
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") {
tracing::warn!(
"Could not write default config to {}: permission denied. \
If running in Docker, ensure the config directory is writable \
(e.g. mount a volume with the correct permissions: \
-v /host/config:/app/config). Continuing with in-memory defaults.",
CONFIG_PATH
{} Continuing with in-memory defaults.",
CONFIG_PATH,
PERMISSION_HINT
);
} else {
tracing::warn!("Could not write default config to {}: {}", CONFIG_PATH, e);