CH-J ToolsSSH によるサーバー管理
メニュー
公開済みのソース

CH-J Tools

特定のアプリケーションリリースのディレクトリとファイルを閲覧できます。

ソースの ZIP をダウンロード
CH-J / Server Manager 1704 ファイル
0.1.2-beta 追加日時: · ビルド日時:
storage.rs 追加日時: 日付の記録なし 5.9 KB · 167 行
コード ファイルをダウンロード
1//! Atomic private file writes and bounded download-cache helpers
2//! (port of `storage/atomicFile.js` and `storage/downloadCache.js`).
3//!
4//! Improvement over 0.1.0: the temporary file is fsynced before the rename and the
5//! parent directory is fsynced afterwards on POSIX, so a crash cannot leave a
6//! truncated JSON file behind.
8use crate::core::error::{CoreError, CoreResult};
9use rand::RngCore;
10use serde::Serialize;
11use std::fs::{self, File, OpenOptions};
12use std::io::Write;
13use std::path::{Path, PathBuf};
15fn random_suffix() -> String {
16 let mut bytes = [0u8; 6];
17 rand::thread_rng().fill_bytes(&mut bytes);
18 hex::encode(bytes)
21#[cfg(unix)]
22fn open_private(path: &Path, mode: u32) -> std::io::Result<File> {
23 use std::os::unix::fs::OpenOptionsExt;
24 OpenOptions::new().write(true).create_new(true).mode(mode).open(path)
27#[cfg(not(unix))]
28fn open_private(path: &Path, _mode: u32) -> std::io::Result<File> {
29 OpenOptions::new().write(true).create_new(true).open(path)
32pub fn ensure_private_dir(path: &Path) -> CoreResult<()> {
33 fs::create_dir_all(path)?;
34 #[cfg(unix)]
35 {
36 use std::os::unix::fs::PermissionsExt;
37 let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o700));
38 }
39 Ok(())
42pub fn atomic_write_file(path: &Path, content: &[u8], mode: u32) -> CoreResult<()> {
43 let parent = path.parent().ok_or_else(|| CoreError::new("EIO", "Target has no parent directory."))?;
44 fs::create_dir_all(parent)?;
45 let file_name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
46 let temp = parent.join(format!("{file_name}.tmp-{}", random_suffix()));
47 let result = (|| -> CoreResult<()> {
48 let mut file = open_private(&temp, mode)?;
49 file.write_all(content)?;
50 file.sync_all()?;
51 drop(file);
52 // `rename` replaces atomically on POSIX and uses MoveFileEx(REPLACE_EXISTING) on Windows.
53 fs::rename(&temp, path)?;
54 #[cfg(unix)]
55 if let Ok(dir) = File::open(parent) {
56 let _ = dir.sync_all();
57 }
58 Ok(())
59 })();
60 if result.is_err() {
61 let _ = fs::remove_file(&temp);
62 }
63 result
66pub fn atomic_write_json<T: Serialize + ?Sized>(path: &Path, value: &T) -> CoreResult<()> {
67 let mut text = serde_json::to_string_pretty(value)?;
68 text.push('\n');
69 atomic_write_file(path, text.as_bytes(), 0o600)
72pub fn read_json(path: &Path) -> CoreResult<serde_json::Value> {
73 let text = fs::read_to_string(path)?;
74 Ok(serde_json::from_str(&text)?)
77fn resolved_cache_root(root: &Path) -> CoreResult<PathBuf> {
78 if root.as_os_str().is_empty() {
79 return Err(CoreError::plain("Download cache root is required."));
80 }
81 let resolved = std::path::absolute(root)?;
82 if resolved.parent().is_none() {
83 return Err(CoreError::plain("Refusing to use a filesystem root as a download cache."));
84 }
85 Ok(resolved)
88pub fn clean_download_cache(root: &Path) -> CoreResult<(bool, PathBuf)> {
89 let root = resolved_cache_root(root)?;
90 let existed = root.exists();
91 if existed {
92 fs::remove_dir_all(&root)?;
93 }
94 ensure_private_dir(&root)?;
95 Ok((existed, root))
98/// True when `target` is strictly inside `root` (lexically and after symlink resolution).
99pub fn is_strictly_inside(root: &Path, target: &Path) -> bool {
100 let (Ok(root), Ok(target)) = (std::path::absolute(root), std::path::absolute(target)) else { return false };
101 if target == root || !target.starts_with(&root) {
102 return false;
103 }
104 match (fs::canonicalize(&root), fs::canonicalize(&target)) {
105 (Ok(real_root), Ok(real_target)) => real_target != real_root && real_target.starts_with(&real_root),
106 (Ok(_), Err(_)) => true, // target does not exist yet; lexical check already passed
107 _ => false,
108 }
111pub fn remove_cached_file(root: &Path, file: &Path) -> CoreResult<bool> {
112 let root = resolved_cache_root(root)?;
113 let target = std::path::absolute(file)?;
114 if !is_strictly_inside(&root, &target) {
115 return Err(CoreError::plain("Downloaded package path is outside its cache."));
116 }
117 let Ok(meta) = fs::symlink_metadata(&target) else { return Ok(false) };
118 if !meta.is_file() {
119 return Err(CoreError::plain("Downloaded package is not a regular file."));
120 }
121 fs::remove_file(&target)?;
122 let mut current = target.parent().map(Path::to_path_buf);
123 while let Some(dir) = current {
124 if dir == root || fs::remove_dir(&dir).is_err() {
125 break;
126 }
127 current = dir.parent().map(Path::to_path_buf);
128 }
129 Ok(true)
132#[cfg(test)]
133mod tests {
134 use super::*;
136 #[test]
137 fn atomic_write_replaces_and_is_private() {
138 let dir = tempfile::tempdir().unwrap();
139 let path = dir.path().join("a").join("config.json");
140 atomic_write_json(&path, &serde_json::json!({"a": 1})).unwrap();
141 atomic_write_json(&path, &serde_json::json!({"a": 2})).unwrap();
142 assert_eq!(read_json(&path).unwrap()["a"], 2);
143 #[cfg(unix)]
144 {
145 use std::os::unix::fs::PermissionsExt;
146 assert_eq!(fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
147 }
148 let leftovers: Vec<_> = fs::read_dir(path.parent().unwrap()).unwrap().collect();
149 assert_eq!(leftovers.len(), 1);
150 }
152 #[test]
153 fn cache_removal_stays_inside_root() {
154 let dir = tempfile::tempdir().unwrap();
155 let root = dir.path().join("cache");
156 clean_download_cache(&root).unwrap();
157 let outside = dir.path().join("outside.bin");
158 fs::write(&outside, b"x").unwrap();
159 assert!(remove_cached_file(&root, &outside).is_err());
160 let inside = root.join("x").join("pkg.bin");
161 fs::create_dir_all(inside.parent().unwrap()).unwrap();
162 fs::write(&inside, b"x").unwrap();
163 assert!(remove_cached_file(&root, &inside).unwrap());
164 assert!(!root.join("x").exists());
165 assert!(root.exists());
166 }
SHA-256d48b91cdf92be7c5f35ff7959990a83072be3462c970fde1563c7fed8741cc16

CH-J Proprietary Software License 1.14 · ソースは CH-J Proprietary Software License 1.14 に基づいて提供されています。公開されていても、ライセンス条項が変わったり、追加の権利が付与されたりすることはありません。

アーカイブの SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
ファイル