CH-J Toolsуправление на сървъри над SSH
Меню
Публикуван източник

CH-J Tools

Преглед на директории и файлове за конкретна версия на приложението.

Изтегляне на източник ZIP
CH-J / Server Manager 1704 Файлове
0.1.2-beta Добавено: · Построена:
local.rs Добавено: Дата, която не е регистрирана 9,4 KB · 191 линии
Код Изтегляне на файл
1//! Local disk for the File Manager panels. The UI never gets a generic file API: only listing and
2//! the few operations below, on absolute paths, never inside the application's data directory
3//! (vault, logs) and never following a symbolic link when deleting.
5use super::service::{file_error, safe_entry_name};
6use crate::core::error::CoreResult;
7use chrono::{DateTime, Utc};
8use serde_json::{json, Value};
9use std::path::{Component, Path, PathBuf};
11/// Absolute, without `.` and `..`, outside `protected`.
12pub fn checked_path(value: &str, protected: &Path) -> CoreResult<PathBuf> {
13 let path = PathBuf::from(value);
14 if !path.is_absolute() || path.components().any(|c| matches!(c, Component::ParentDir | Component::CurDir)) {
15 return Err(file_error("FILE_UNSAFE_PATH", "The local path must be absolute."));
16 }
17 // A symlink can point into the data directory, so the real path of the nearest existing parent counts too.
18 let protected_real = std::fs::canonicalize(protected).unwrap_or_else(|_| protected.to_path_buf());
19 let mut existing = path.as_path();
20 let real = loop {
21 match std::fs::canonicalize(existing) {
22 Ok(real) => break real,
23 Err(_) => match existing.parent() {
24 Some(parent) => existing = parent,
25 None => break path.clone(),
26 },
27 }
28 };
29 if path.starts_with(protected) || real.starts_with(&protected_real) {
30 return Err(file_error("FILE_PROTECTED_PATH", "The application's data directory cannot be used here."));
31 }
32 Ok(path)
35fn io_error(error: std::io::Error, path: &Path) -> crate::core::error::CoreError {
36 let code = match error.kind() {
37 std::io::ErrorKind::NotFound => "FILE_NOT_FOUND",
38 std::io::ErrorKind::PermissionDenied => "LOCAL_PERMISSION_DENIED",
39 std::io::ErrorKind::AlreadyExists => "FILE_EXISTS",
40 std::io::ErrorKind::DirectoryNotEmpty => "FILE_DIRECTORY_NOT_EMPTY",
41 _ => "LOCAL_OPERATION_FAILED",
42 };
43 file_error(code, &format!("{}: {error}", path.display()))
46/// Home directory and, on Windows, the drive roots.
47pub fn places() -> Value {
48 let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")).map(PathBuf::from).unwrap_or_else(|| PathBuf::from("/"));
49 let mut roots: Vec<String> = Vec::new();
50 if cfg!(windows) {
51 for letter in b'A'..=b'Z' {
52 let root = format!("{}:\\", letter as char);
53 if Path::new(&root).exists() {
54 roots.push(root);
55 }
56 }
57 } else {
58 roots.push("/".into());
59 }
60 json!({ "home": home.to_string_lossy(), "roots": roots, "separator": std::path::MAIN_SEPARATOR.to_string() })
63pub fn list(value: &str, protected: &Path) -> CoreResult<Value> {
64 let directory = checked_path(value, protected)?;
65 let mut entries = Vec::new();
66 for entry in std::fs::read_dir(&directory).map_err(|e| io_error(e, &directory))? {
67 let Ok(entry) = entry else { continue };
68 let path = entry.path();
69 let Ok(meta) = std::fs::symlink_metadata(&path) else { continue };
70 let link = meta.file_type().is_symlink();
71 let target = if link { std::fs::metadata(&path).ok() } else { None };
72 let kind = if link { "symlink" } else if meta.is_dir() { "directory" } else if meta.is_file() { "file" } else { "other" };
73 entries.push(json!({
74 "name": entry.file_name().to_string_lossy(),
75 "path": path.to_string_lossy(),
76 "type": kind,
77 "targetType": if !link { Value::Null } else { match &target { Some(t) if t.is_dir() => json!("directory"), Some(_) => json!("file"), None => json!("missing") } },
78 "size": meta.len(),
79 "modifiedAt": meta.modified().ok().map(|m| DateTime::<Utc>::from(m).to_rfc3339())
80 }));
81 }
82 Ok(Value::Array(entries))
85pub fn mkdir(parent: &str, name: &str, protected: &Path) -> CoreResult<Value> {
86 let path = checked_path(parent, protected)?.join(safe_entry_name(name)?);
87 std::fs::create_dir(&path).map_err(|e| io_error(e, &path))?;
88 Ok(json!({ "path": path.to_string_lossy() }))
91/// Never replaces anything: an existing name is an error.
92pub fn create_file(parent: &str, name: &str, protected: &Path) -> CoreResult<Value> {
93 let path = checked_path(parent, protected)?.join(safe_entry_name(name)?);
94 std::fs::OpenOptions::new().write(true).create_new(true).open(&path).map_err(|e| io_error(e, &path))?;
95 Ok(json!({ "path": path.to_string_lossy() }))
98pub fn rename(from: &str, name: &str, protected: &Path) -> CoreResult<Value> {
99 let source = checked_path(from, protected)?;
100 let parent = source.parent().ok_or_else(|| file_error("FILE_PROTECTED_PATH", "A root cannot be renamed."))?;
101 let target = parent.join(safe_entry_name(name)?);
102 if std::fs::symlink_metadata(&target).is_ok() {
103 return Err(file_error("FILE_EXISTS", &format!("{} already exists.", target.display())));
104 }
105 std::fs::rename(&source, &target).map_err(|e| io_error(e, &source))?;
106 Ok(json!({ "path": target.to_string_lossy() }))
109/// Roots, the home directory and its ancestors are refused. A link is removed, not what it points to.
110pub fn remove(paths: &[String], protected: &Path) -> CoreResult<Value> {
111 let home = places()["home"].as_str().map(PathBuf::from);
112 let mut checked = Vec::new();
113 for value in paths {
114 let path = checked_path(value, protected)?;
115 if path.parent().is_none() || home.as_ref().is_some_and(|h| h.starts_with(&path)) || protected.starts_with(&path) {
116 return Err(file_error("FILE_PROTECTED_PATH", &format!("{} cannot be deleted.", path.display())));
117 }
118 checked.push(path);
119 }
120 for path in &checked {
121 let meta = std::fs::symlink_metadata(path).map_err(|e| io_error(e, path))?;
122 if meta.is_dir() { std::fs::remove_dir_all(path) } else { std::fs::remove_file(path) }.map_err(|e| io_error(e, path))?;
123 }
124 Ok(json!({ "removed": checked.len() }))
127#[cfg(test)]
128mod tests {
129 use super::*;
131 #[test]
132 fn paths_are_absolute_and_stay_out_of_the_data_directory() {
133 let dir = tempfile::tempdir().unwrap();
134 let data = dir.path().join("data");
135 std::fs::create_dir(&data).unwrap();
136 std::fs::write(data.join("vault"), "secret").unwrap();
137 assert_eq!(checked_path("relative/file", &data).unwrap_err().code, "FILE_UNSAFE_PATH");
138 let dotted = format!("{}/x/../data", dir.path().display());
139 assert_eq!(checked_path(&dotted, &data).unwrap_err().code, "FILE_UNSAFE_PATH");
140 assert_eq!(checked_path(data.join("vault").to_str().unwrap(), &data).unwrap_err().code, "FILE_PROTECTED_PATH");
141 #[cfg(unix)]
142 {
143 let link = dir.path().join("link");
144 std::os::unix::fs::symlink(&data, &link).unwrap();
145 assert_eq!(checked_path(link.join("vault").to_str().unwrap(), &data).unwrap_err().code, "FILE_PROTECTED_PATH", "a link into the data directory");
146 assert_eq!(list(link.to_str().unwrap(), &data).unwrap_err().code, "FILE_PROTECTED_PATH");
147 }
148 }
150 #[test]
151 fn names_cannot_leave_their_folder() {
152 let dir = tempfile::tempdir().unwrap();
153 let (base, data) = (dir.path().join("work"), dir.path().join("data"));
154 std::fs::create_dir_all(&base).unwrap();
155 for bad in ["../evil", "a/b", "..", ""] {
156 assert!(mkdir(base.to_str().unwrap(), bad, &data).is_err(), "{bad}");
157 }
158 mkdir(base.to_str().unwrap(), "ok", &data).unwrap();
159 for good in ["a.txt", "b.htm", "c.conf", "bez-pripony", ".skryty"] {
160 create_file(base.to_str().unwrap(), good, &data).unwrap();
161 }
162 assert_eq!(create_file(base.to_str().unwrap(), "a.txt", &data).unwrap_err().code, "FILE_EXISTS", "an existing file stays");
163 assert!(create_file(base.to_str().unwrap(), "../evil.txt", &data).is_err());
164 for good in ["a.txt", "b.htm", "c.conf", "bez-pripony", ".skryty"] {
165 std::fs::remove_file(base.join(good)).unwrap();
166 }
167 std::fs::write(base.join("file"), "x").unwrap();
168 assert!(rename(base.join("file").to_str().unwrap(), "../evil", &data).is_err());
169 assert_eq!(rename(base.join("file").to_str().unwrap(), "ok", &data).unwrap_err().code, "FILE_EXISTS");
170 rename(base.join("file").to_str().unwrap(), "renamed", &data).unwrap();
171 let names: Vec<String> = list(base.to_str().unwrap(), &data).unwrap().as_array().unwrap().iter().map(|e| e["name"].as_str().unwrap().to_string()).collect();
172 assert!(names.contains(&"renamed".to_string()) && names.contains(&"ok".to_string()));
173 }
175 #[cfg(unix)]
176 #[test]
177 fn delete_removes_a_link_not_its_target_and_refuses_home_and_roots() {
178 let dir = tempfile::tempdir().unwrap();
179 let (kept, doomed, data) = (dir.path().join("kept"), dir.path().join("doomed"), dir.path().join("data"));
180 std::fs::create_dir_all(&kept).unwrap();
181 std::fs::create_dir_all(&doomed).unwrap();
182 std::fs::write(kept.join("file"), "x").unwrap();
183 std::os::unix::fs::symlink(&kept, doomed.join("link")).unwrap();
184 remove(&[doomed.to_string_lossy().to_string()], &data).unwrap();
185 assert!(!doomed.exists());
186 assert!(kept.join("file").exists(), "the link's target survives");
187 assert_eq!(remove(&["/".to_string()], &data).unwrap_err().code, "FILE_PROTECTED_PATH");
188 let home = places()["home"].as_str().unwrap().to_string();
189 assert_eq!(remove(&[home], &data).unwrap_err().code, "FILE_PROTECTED_PATH");
190 }
SHA-256d35065485c0fc1f73d8db45350b391594b41f668caa4752a6bef725e6cff837f

CH-J Proprietary Software License 1.14 · Източникът е предоставен под CH-J Proprietary Software License 1.14. Неговата наличност не променя лицензионните условия или не предоставя допълнителни права.

Архив SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Файлове