use std::path::{Component, Path, PathBuf};
use anyhow::{bail, Context, Result};
pub const IMAGE_DIR_PARAM: &str = "image_dir";
pub fn default_image_dir() -> PathBuf {
if let Some(data) = dirs::data_local_dir() {
return data.join("netget").join("usb-images");
}
if let Some(home) = dirs::home_dir() {
return home.join(".netget-usb-images");
}
std::env::temp_dir().join("netget-usb-images")
}
#[derive(Debug, Clone)]
pub struct ImageDir {
root: PathBuf,
}
impl ImageDir {
pub fn new(configured: Option<&str>) -> Result<Self> {
let requested = match configured {
Some(raw) if !raw.trim().is_empty() => PathBuf::from(raw.trim()),
_ => default_image_dir(),
};
std::fs::create_dir_all(&requested).with_context(|| {
format!(
"could not create the {IMAGE_DIR_PARAM} directory {}",
requested.display()
)
})?;
let root = requested.canonicalize().with_context(|| {
format!(
"could not canonicalise {IMAGE_DIR_PARAM} {}",
requested.display()
)
})?;
if !root.is_dir() {
bail!("{IMAGE_DIR_PARAM} ({}) is not a directory", root.display());
}
Ok(Self { root })
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn resolve(&self, raw: &str, context: &str) -> Result<PathBuf> {
let trimmed = raw.trim();
if trimmed.is_empty() {
bail!("{context} is empty");
}
let requested = Path::new(trimmed);
let absolute = if requested.is_absolute() {
requested.to_path_buf()
} else {
self.root.join(requested)
};
let name = match absolute.components().next_back() {
Some(Component::Normal(name)) => name.to_os_string(),
_ => bail!("{context} ({trimmed}) does not end in a file name"),
};
let parent = absolute
.parent()
.with_context(|| format!("{context} ({trimmed}) has no directory"))?;
let parent = parent
.canonicalize()
.with_context(|| format!("{context} ({trimmed}): its directory does not exist"))?;
if !parent.starts_with(&self.root) {
bail!(
"{context} ({trimmed}) is in {} which is outside this server's \
{IMAGE_DIR_PARAM} ({}). Only images under that directory are served; put the \
image there, or set the `{IMAGE_DIR_PARAM}` startup parameter to a directory \
that contains it.",
parent.display(),
self.root.display()
);
}
let resolved = parent.join(name);
if let Ok(meta) = std::fs::symlink_metadata(&resolved) {
if meta.file_type().is_symlink() {
bail!("{context} ({trimmed}) is a symlink; an image must be a regular file",);
}
if !meta.is_file() {
bail!("{context} ({trimmed}) exists and is not a regular file");
}
}
Ok(resolved)
}
}