feat: 增加 Docker Compose 部署支持

This commit is contained in:
chuan
2026-08-10 17:15:41 +08:00
parent d0e51bed4e
commit 0f9072565d
8 changed files with 347 additions and 11 deletions
+71
View File
@@ -6,6 +6,7 @@ mod service;
mod store;
use std::{
net::SocketAddr,
path::{Path, PathBuf},
sync::Arc,
};
@@ -30,6 +31,14 @@ pub(crate) struct Cli {
#[arg(long)]
data_dir: Option<PathBuf>,
#[arg(long)]
http_listen: Option<SocketAddr>,
#[arg(long)]
web_dir: Option<PathBuf>,
#[arg(long)]
console_logging: bool,
#[arg(long)]
no_file_logging: bool,
#[arg(long)]
run_duration_secs: Option<u64>,
#[arg(long)]
restore_checkpoint: Option<PathBuf>,
@@ -54,6 +63,22 @@ impl Cli {
dto.data_dir = data_dir;
command_line_overrides.push("data_dir".to_owned());
}
if let Some(http_listen) = self.http_listen {
dto.http.listen = http_listen;
command_line_overrides.push("http.listen".to_owned());
}
if let Some(web_dir) = self.web_dir {
dto.http.web_dir = web_dir;
command_line_overrides.push("http.web_dir".to_owned());
}
if self.console_logging {
dto.logging.console_enabled = true;
command_line_overrides.push("logging.console_enabled".to_owned());
}
if self.no_file_logging {
dto.logging.file_enabled = false;
command_line_overrides.push("logging.file_enabled".to_owned());
}
if self.run_duration_secs.is_some() {
dto.run_duration_secs = self.run_duration_secs;
command_line_overrides.push("run_duration_secs".to_owned());
@@ -139,6 +164,10 @@ mod tests {
let startup = Cli {
config: config_path,
data_dir: None,
http_listen: None,
web_dir: None,
console_logging: false,
no_file_logging: false,
run_duration_secs: None,
restore_checkpoint: None,
}
@@ -166,6 +195,10 @@ mod tests {
let error = Cli {
config: config_path,
data_dir: None,
http_listen: None,
web_dir: None,
console_logging: false,
no_file_logging: false,
run_duration_secs: None,
restore_checkpoint: None,
}
@@ -174,6 +207,44 @@ mod tests {
assert!(matches!(error, AppError::Toml(_)));
}
#[test]
fn command_line_can_override_container_runtime_settings() {
let directory = TempDir::new().unwrap();
let config_path = directory.path().join("service.toml");
fs::write(
&config_path,
"[http]\nlisten = '127.0.0.1:8080'\nweb_dir = 'src/web/dist'",
)
.unwrap();
let listen = "0.0.0.0:8080".parse().unwrap();
let startup = Cli {
config: config_path,
data_dir: None,
http_listen: Some(listen),
web_dir: Some(PathBuf::from("/dht-search/web")),
console_logging: true,
no_file_logging: true,
run_duration_secs: None,
restore_checkpoint: None,
}
.load()
.unwrap();
assert_eq!(startup.app.http.listen, listen);
assert!(startup.app.http.web_dir.ends_with("dht-search/web"));
assert_eq!(
startup.config_service.snapshot().command_line_overrides,
[
"http.listen",
"http.web_dir",
"logging.console_enabled",
"logging.file_enabled"
]
);
assert!(startup.app.logging.console_enabled);
assert!(!startup.app.logging.file_enabled);
}
#[test]
fn zero_metadata_limit_is_rejected() {
let mut dto = AppConfigDto::default();
+39 -2
View File
@@ -1,4 +1,4 @@
// 负责从持久化介质读取并原子保存用户配置 DTO
// 负责从持久化介质读取保存用户配置 DTO 并兼容 Docker 单文件挂载
use std::{
fs::{self, OpenOptions},
@@ -55,7 +55,14 @@ impl ConfigStore for TomlConfigStore {
file.write_all(contents.as_bytes())?;
file.sync_all()?;
drop(file);
replace_file(&temporary, &self.path)?;
match replace_file(&temporary, &self.path) {
Ok(()) => {}
Err(error) if is_bind_mount_replace_error(&error) => {
write_in_place(&self.path, contents.as_bytes())?;
fs::remove_file(&temporary)?;
}
Err(error) => return Err(error),
}
sync_parent(parent)?;
Ok::<(), std::io::Error>(())
})();
@@ -70,6 +77,25 @@ impl ConfigStore for TomlConfigStore {
}
}
fn write_in_place(destination: &Path, contents: &[u8]) -> std::io::Result<()> {
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(destination)?;
file.write_all(contents)?;
file.sync_all()
}
#[cfg(target_os = "linux")]
fn is_bind_mount_replace_error(error: &std::io::Error) -> bool {
error.raw_os_error() == Some(16)
}
#[cfg(not(target_os = "linux"))]
fn is_bind_mount_replace_error(_error: &std::io::Error) -> bool {
false
}
fn temporary_path(destination: &Path) -> PathBuf {
let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let file_name = destination
@@ -152,4 +178,15 @@ mod tests {
0
);
}
#[test]
fn in_place_fallback_truncates_previous_contents() {
let directory = TempDir::new().unwrap();
let path = directory.path().join("service.toml");
fs::write(&path, "old contents that are longer").unwrap();
write_in_place(&path, b"new").unwrap();
assert_eq!(fs::read_to_string(path).unwrap(), "new");
}
}
+8
View File
@@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>
export default component
}