feat: 增加 Docker Compose 部署支持
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+8
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user