netget/server/usb/msc/
image_dir.rs1use std::path::{Component, Path, PathBuf};
19
20use anyhow::{bail, Context, Result};
21
22pub const IMAGE_DIR_PARAM: &str = "image_dir";
24
25pub fn default_image_dir() -> PathBuf {
27 if let Some(data) = dirs::data_local_dir() {
28 return data.join("netget").join("usb-images");
29 }
30 if let Some(home) = dirs::home_dir() {
31 return home.join(".netget-usb-images");
32 }
33 std::env::temp_dir().join("netget-usb-images")
34}
35
36#[derive(Debug, Clone)]
38pub struct ImageDir {
39 root: PathBuf,
40}
41
42impl ImageDir {
43 pub fn new(configured: Option<&str>) -> Result<Self> {
46 let requested = match configured {
47 Some(raw) if !raw.trim().is_empty() => PathBuf::from(raw.trim()),
48 _ => default_image_dir(),
49 };
50 std::fs::create_dir_all(&requested).with_context(|| {
51 format!(
52 "could not create the {IMAGE_DIR_PARAM} directory {}",
53 requested.display()
54 )
55 })?;
56 let root = requested.canonicalize().with_context(|| {
57 format!(
58 "could not canonicalise {IMAGE_DIR_PARAM} {}",
59 requested.display()
60 )
61 })?;
62 if !root.is_dir() {
63 bail!("{IMAGE_DIR_PARAM} ({}) is not a directory", root.display());
64 }
65 Ok(Self { root })
66 }
67
68 pub fn root(&self) -> &Path {
70 &self.root
71 }
72
73 pub fn resolve(&self, raw: &str, context: &str) -> Result<PathBuf> {
77 let trimmed = raw.trim();
78 if trimmed.is_empty() {
79 bail!("{context} is empty");
80 }
81 let requested = Path::new(trimmed);
82 let absolute = if requested.is_absolute() {
83 requested.to_path_buf()
84 } else {
85 self.root.join(requested)
86 };
87 let name = match absolute.components().next_back() {
88 Some(Component::Normal(name)) => name.to_os_string(),
89 _ => bail!("{context} ({trimmed}) does not end in a file name"),
90 };
91 let parent = absolute
92 .parent()
93 .with_context(|| format!("{context} ({trimmed}) has no directory"))?;
94 let parent = parent
95 .canonicalize()
96 .with_context(|| format!("{context} ({trimmed}): its directory does not exist"))?;
97 if !parent.starts_with(&self.root) {
98 bail!(
99 "{context} ({trimmed}) is in {} which is outside this server's \
100 {IMAGE_DIR_PARAM} ({}). Only images under that directory are served; put the \
101 image there, or set the `{IMAGE_DIR_PARAM}` startup parameter to a directory \
102 that contains it.",
103 parent.display(),
104 self.root.display()
105 );
106 }
107 let resolved = parent.join(name);
108 if let Ok(meta) = std::fs::symlink_metadata(&resolved) {
109 if meta.file_type().is_symlink() {
110 bail!("{context} ({trimmed}) is a symlink; an image must be a regular file",);
111 }
112 if !meta.is_file() {
113 bail!("{context} ({trimmed}) exists and is not a regular file");
114 }
115 }
116 Ok(resolved)
117 }
118}