mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(update): reuse the served asset directory (#111)
Co-authored-by: Zyy <zyycn@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -28,7 +28,11 @@ impl LoadableConfig for GatewayConfig {
|
||||
if self.schema_version != CONFIG_SCHEMA_VERSION {
|
||||
return Err(ConfigError::InvalidField("schema_version"));
|
||||
}
|
||||
self.host.resolve_and_validate(source_dir)?;
|
||||
self.api
|
||||
.resolve_and_validate(source_dir)
|
||||
.map_err(|_| ConfigError::InvalidField("api"))?;
|
||||
self.host
|
||||
.resolve_and_validate(source_dir, &self.api.asset_directory)?;
|
||||
let runtime_data_dir = self.host.runtime_data_dir().to_path_buf();
|
||||
self.store
|
||||
.resolve_and_validate(&runtime_data_dir)
|
||||
@@ -39,9 +43,6 @@ impl LoadableConfig for GatewayConfig {
|
||||
self.client
|
||||
.resolve_and_validate(source_dir)
|
||||
.map_err(|_| ConfigError::InvalidField("client"))?;
|
||||
self.api
|
||||
.resolve_and_validate(source_dir)
|
||||
.map_err(|_| ConfigError::InvalidField("api"))?;
|
||||
self.openai
|
||||
.resolve_and_validate(&runtime_data_dir)
|
||||
.map_err(|_| ConfigError::InvalidField("openai"))?;
|
||||
|
||||
@@ -168,6 +168,50 @@ fn config_loader_should_resolve_paths_relative_to_config_file() {
|
||||
assert!(debug.contains("frontend/dist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_loader_should_share_resolved_assets_with_system_update() {
|
||||
const CHILD_ENV: &str = "CPR_TEST_UPDATE_ASSETS_CHILD";
|
||||
let Ok(case) = std::env::var(CHILD_ENV) else {
|
||||
// 使用子进程覆盖无环境变量、Docker 路径和相对路径,避免污染并行测试。
|
||||
for (case, web_dist) in [
|
||||
("binary", None),
|
||||
("docker", Some("/app/web/dist")),
|
||||
("relative", Some("../custom/dist")),
|
||||
] {
|
||||
let mut child = Command::new(std::env::current_exe().expect("test executable"));
|
||||
child
|
||||
.args([
|
||||
"--exact",
|
||||
"bootstrap::config_loader_should_share_resolved_assets_with_system_update",
|
||||
])
|
||||
.env(CHILD_ENV, case)
|
||||
.env_remove("CPR_WEB_DIST_DIR");
|
||||
if let Some(web_dist) = web_dist {
|
||||
child.env("CPR_WEB_DIST_DIR", web_dist);
|
||||
}
|
||||
let output = child.output().expect("isolated configuration test");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{case}: {}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
return;
|
||||
};
|
||||
let mut document = valid_config_document();
|
||||
document["api"]["asset_directory"] = serde_json::json!("../web/dist");
|
||||
let (config, directory) = parse_config(&document.to_string()).expect("binary configuration");
|
||||
let assets = match case.as_str() {
|
||||
"docker" => std::path::PathBuf::from("/app/web/dist"),
|
||||
"relative" => directory.path().join("deploy/../custom/dist"),
|
||||
_ => directory.path().join("deploy/../web/dist"),
|
||||
};
|
||||
let debug = format!("{config:?}");
|
||||
assert!(debug.contains(&format!("asset_directory: {assets:?}")));
|
||||
assert!(debug.contains(&format!("web_dist_dir: Some({assets:?})")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_loader_should_reject_missing_runtime_data_dir() {
|
||||
let config = valid_config().replace(" runtime_data_dir: '../.runtime/data'\n", "");
|
||||
|
||||
@@ -62,12 +62,17 @@ impl ApiConfig {
|
||||
return Err(ApiConfigError::InvalidAssetDirectory);
|
||||
}
|
||||
}
|
||||
if self.asset_directory.as_os_str().is_empty() {
|
||||
return Err(ApiConfigError::InvalidAssetDirectory);
|
||||
}
|
||||
self.validate()?;
|
||||
if self.asset_directory.is_relative() {
|
||||
self.asset_directory = source_dir.join(&self.asset_directory);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate(&mut self) -> Result<(), ApiConfigError> {
|
||||
if self.asset_directory.as_os_str().is_empty() {
|
||||
return Err(ApiConfigError::InvalidAssetDirectory);
|
||||
}
|
||||
if self.request_timeout_seconds == Some(0) {
|
||||
return Err(ApiConfigError::InvalidRequestTimeout);
|
||||
}
|
||||
@@ -124,9 +129,8 @@ pub fn initialize(
|
||||
worker_health: Arc<dyn WorkerHealthSource>,
|
||||
lifecycle: Arc<dyn ConnectionLifecycle>,
|
||||
) -> Result<ApiBundle, ApiError> {
|
||||
config
|
||||
.resolve_and_validate(Path::new("."))
|
||||
.map_err(ApiError::Config)?;
|
||||
// 配置加载已解析环境变量与相对路径,初始化只校验,避免覆盖最终目录。
|
||||
config.validate().map_err(ApiError::Config)?;
|
||||
let request_id_header = HeaderName::from_str(&config.request_id_header)
|
||||
.map_err(|_| ApiError::Config(ApiConfigError::InvalidRequestIdHeader))?;
|
||||
let state = ApiState {
|
||||
|
||||
@@ -5,3 +5,55 @@ mod health;
|
||||
mod key_usage;
|
||||
mod openai;
|
||||
mod support;
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_should_serve_resolved_assets_with_relative_environment_override() {
|
||||
use axum::{body::Body, http::Request};
|
||||
use std::{fs, process::Command};
|
||||
use tower::ServiceExt as _;
|
||||
|
||||
const CHILD_ENV: &str = "CPR_TEST_ASSET_DIRECTORY";
|
||||
let Ok(directory) = std::env::var(CHILD_ENV) else {
|
||||
let directory = std::env::temp_dir().join(format!("cpr-assets-{}", uuid::Uuid::now_v7()));
|
||||
fs::create_dir_all(directory.join("deploy")).expect("config directory");
|
||||
fs::create_dir_all(directory.join("web/dist")).expect("asset directory");
|
||||
fs::write(directory.join("web/dist/index.html"), "resolved-web-assets").expect("index");
|
||||
let output = Command::new(std::env::current_exe().expect("test executable"))
|
||||
.args([
|
||||
"--exact",
|
||||
"api_should_serve_resolved_assets_with_relative_environment_override",
|
||||
])
|
||||
.current_dir(&directory)
|
||||
.env(CHILD_ENV, &directory)
|
||||
.env("CPR_WEB_DIST_DIR", "../web/dist")
|
||||
.output()
|
||||
.expect("isolated asset directory test");
|
||||
fs::remove_dir_all(directory).expect("remove fixture");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
return;
|
||||
};
|
||||
let mut config = gateway_api::ApiConfig {
|
||||
asset_directory: "unused".into(),
|
||||
cors_allowed_origins: Vec::new(),
|
||||
request_timeout_seconds: None,
|
||||
request_id_header: "x-request-id".to_owned(),
|
||||
};
|
||||
config
|
||||
.resolve_and_validate(&std::path::Path::new(&directory).join("deploy"))
|
||||
.expect("resolved API config");
|
||||
let admin = admin::AdminTestFixture::new().await;
|
||||
let response = openai::api_router_with_config(admin.services, config)
|
||||
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
|
||||
.await
|
||||
.expect("static response");
|
||||
assert_eq!(response.status(), axum::http::StatusCode::OK);
|
||||
let body = axum::body::to_bytes(response.into_body(), 1024)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(body.as_ref(), b"resolved-web-assets");
|
||||
}
|
||||
|
||||
@@ -50,13 +50,23 @@ pub(super) async fn api_router(execution: Arc<dyn ExecutionService>) -> axum::Ro
|
||||
}
|
||||
|
||||
pub(super) fn api_router_with_admin(admin: gateway_admin::AdminServices) -> axum::Router {
|
||||
gateway_api::initialize(
|
||||
api_router_with_config(
|
||||
admin,
|
||||
gateway_api::ApiConfig {
|
||||
asset_directory: std::env::temp_dir(),
|
||||
cors_allowed_origins: Vec::new(),
|
||||
request_timeout_seconds: None,
|
||||
request_id_header: "x-request-id".to_owned(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn api_router_with_config(
|
||||
admin: gateway_admin::AdminServices,
|
||||
config: gateway_api::ApiConfig,
|
||||
) -> axum::Router {
|
||||
gateway_api::initialize(
|
||||
config,
|
||||
Arc::new(DefaultExecutionService::new(
|
||||
RuntimeSnapshotHandle::new(snapshot("unused-client-route-key", "openai")),
|
||||
Arc::new(UnusedExecutionStore),
|
||||
|
||||
@@ -52,7 +52,12 @@ pub struct HostConfig {
|
||||
}
|
||||
|
||||
impl HostConfig {
|
||||
pub fn resolve_and_validate(&mut self, source_dir: &Path) -> Result<(), ConfigError> {
|
||||
/// 解析 Host 配置;在线更新默认沿用已解析的 API 静态资源目录。
|
||||
pub fn resolve_and_validate(
|
||||
&mut self,
|
||||
source_dir: &Path,
|
||||
asset_directory: &Path,
|
||||
) -> Result<(), ConfigError> {
|
||||
if let Some(host) = optional_environment_value(SERVER_HOST_ENV)? {
|
||||
self.listen.host = host;
|
||||
}
|
||||
@@ -82,8 +87,11 @@ impl HostConfig {
|
||||
}
|
||||
resolve_relative_path(source_dir, &mut self.runtime_data_dir);
|
||||
self.logging.resolve_and_validate(source_dir)?;
|
||||
self.system_update
|
||||
.resolve_and_validate(source_dir, &self.runtime_data_dir)?;
|
||||
self.system_update.resolve_and_validate(
|
||||
source_dir,
|
||||
&self.runtime_data_dir,
|
||||
asset_directory,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ use self::swap::{replace_release_files, rollback_release};
|
||||
pub use self::release::validate_download_url;
|
||||
|
||||
const APP_BINARY_NAME: &str = "codex-proxy-rs";
|
||||
const DEFAULT_WEB_DIST_DIR: &str = "/app/web/dist";
|
||||
const DEFAULT_GITHUB_API_BASE: &str = "https://api.github.com/repos";
|
||||
const DEFAULT_UPDATE_REPOSITORY: &str = "zyycn/codex-proxy-rs";
|
||||
|
||||
@@ -62,7 +61,8 @@ pub struct SystemUpdateConfig {
|
||||
pub update_repository: Option<String>,
|
||||
pub github_api_base: String,
|
||||
pub executable_path: Option<PathBuf>,
|
||||
pub web_dist_dir: PathBuf,
|
||||
/// 未显式指定时,由组合根传入 API 实际使用的静态资源目录。
|
||||
pub web_dist_dir: Option<PathBuf>,
|
||||
pub update_state_file: PathBuf,
|
||||
pub update_lock_file: PathBuf,
|
||||
pub update_temp_dir: PathBuf,
|
||||
@@ -107,9 +107,7 @@ impl Default for SystemUpdateConfig {
|
||||
github_api_base: environment_value("CPR_GITHUB_API_BASE")
|
||||
.unwrap_or_else(|| DEFAULT_GITHUB_API_BASE.to_owned()),
|
||||
executable_path,
|
||||
web_dist_dir: environment_value("CPR_WEB_DIST_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from(DEFAULT_WEB_DIST_DIR)),
|
||||
web_dist_dir: None,
|
||||
update_state_file,
|
||||
update_lock_file,
|
||||
update_temp_dir,
|
||||
@@ -124,8 +122,15 @@ impl SystemUpdateConfig {
|
||||
&mut self,
|
||||
source_dir: &Path,
|
||||
runtime_data_dir: &Path,
|
||||
asset_directory: &Path,
|
||||
) -> Result<(), ConfigError> {
|
||||
for path in [self.executable_path.as_mut(), Some(&mut self.web_dist_dir)]
|
||||
let web_dist_dir = self
|
||||
.web_dist_dir
|
||||
.get_or_insert_with(|| asset_directory.to_path_buf());
|
||||
if web_dist_dir.as_os_str().is_empty() {
|
||||
return Err(ConfigError::InvalidField("host.system_update.web_dist_dir"));
|
||||
}
|
||||
for path in [self.executable_path.as_mut(), self.web_dist_dir.as_mut()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
@@ -174,6 +179,9 @@ impl SystemUpdateConfig {
|
||||
if let Err(error) = release::validate_api_base(&self.github_api_base) {
|
||||
return Some(error);
|
||||
}
|
||||
if let Err(error) = self.web_dist_dir() {
|
||||
return Some(error.to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -197,6 +205,13 @@ impl SystemUpdateConfig {
|
||||
.and_then(fs::canonicalize)
|
||||
.map_err(|error| internal(format!("failed to resolve executable: {error}")))
|
||||
}
|
||||
|
||||
pub(crate) fn web_dist_dir(&self) -> Result<&Path, OperationError> {
|
||||
self.web_dist_dir
|
||||
.as_deref()
|
||||
.filter(|path| !path.as_os_str().is_empty())
|
||||
.ok_or_else(|| conflict("web assets directory is not configured"))
|
||||
}
|
||||
}
|
||||
|
||||
/// gateway-admin 消费的真实进程/文件系统操作实现。
|
||||
@@ -395,7 +410,7 @@ impl ProcessSystemOperations {
|
||||
.info(Some(operation_id), Some("replace"), "正在替换应用文件");
|
||||
replace_release_files(
|
||||
&self.config.executable_path()?,
|
||||
&self.config.web_dist_dir,
|
||||
self.config.web_dist_dir()?,
|
||||
extracted,
|
||||
)?;
|
||||
self.events
|
||||
|
||||
@@ -81,6 +81,7 @@ pub(crate) fn replace_release_files(
|
||||
}
|
||||
|
||||
pub(crate) fn rollback_release(config: &SystemUpdateConfig) -> Result<(), OperationError> {
|
||||
let web_dist = config.web_dist_dir()?;
|
||||
let executable = config.executable_path()?;
|
||||
let binary_backup = backup_path_for(&executable);
|
||||
if !binary_backup.exists() {
|
||||
@@ -89,9 +90,9 @@ pub(crate) fn rollback_release(config: &SystemUpdateConfig) -> Result<(), Operat
|
||||
swap_file(&executable, &binary_backup)
|
||||
.map_err(|error| internal(format!("binary rollback failed: {error}")))?;
|
||||
|
||||
let web_backup = backup_path_for(&config.web_dist_dir);
|
||||
let web_backup = backup_path_for(web_dist);
|
||||
if web_backup.exists()
|
||||
&& let Err(error) = swap_dir(&config.web_dist_dir, &web_backup)
|
||||
&& let Err(error) = swap_dir(web_dist, &web_backup)
|
||||
{
|
||||
let mut rollback = Vec::new();
|
||||
collect_rollback_error(
|
||||
|
||||
@@ -21,11 +21,61 @@ fn system_update_defaults_should_use_host_build_metadata_and_official_repository
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_config_should_default_update_assets_to_the_resolved_api_directory() {
|
||||
let mut config = valid_config();
|
||||
config.system_update.web_dist_dir = None;
|
||||
let assets = std::path::Path::new("/srv/gateway/web/dist");
|
||||
|
||||
config
|
||||
.resolve_and_validate(std::path::Path::new("/srv/gateway/deploy"), assets)
|
||||
.expect("host config");
|
||||
|
||||
assert_eq!(config.system_update.web_dist_dir.as_deref(), Some(assets));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_config_should_preserve_explicit_update_asset_directory() {
|
||||
let mut config = valid_config();
|
||||
config.system_update.web_dist_dir = Some(PathBuf::from("../custom/dist"));
|
||||
|
||||
config
|
||||
.resolve_and_validate(
|
||||
std::path::Path::new("/srv/gateway/deploy"),
|
||||
std::path::Path::new("/srv/gateway/web/dist"),
|
||||
)
|
||||
.expect("host config");
|
||||
|
||||
assert_eq!(
|
||||
config.system_update.web_dist_dir,
|
||||
Some(PathBuf::from("/srv/gateway/deploy/../custom/dist"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_config_should_reject_empty_update_asset_directory() {
|
||||
let mut config = valid_config();
|
||||
config.system_update.web_dist_dir = Some(PathBuf::new());
|
||||
|
||||
assert!(matches!(
|
||||
config.resolve_and_validate(
|
||||
std::path::Path::new("/srv/gateway/deploy"),
|
||||
std::path::Path::new("/srv/gateway/web/dist"),
|
||||
),
|
||||
Err(gateway_host::ConfigError::InvalidField(
|
||||
"host.system_update.web_dist_dir"
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_config_resolves_only_host_owned_relative_paths() {
|
||||
let mut config = valid_config();
|
||||
config
|
||||
.resolve_and_validate(std::path::Path::new("/srv/gateway"))
|
||||
.resolve_and_validate(
|
||||
std::path::Path::new("/srv/gateway"),
|
||||
std::path::Path::new("/srv/gateway/web/dist"),
|
||||
)
|
||||
.expect("valid host config");
|
||||
|
||||
assert_eq!(
|
||||
@@ -38,7 +88,10 @@ fn host_config_resolves_only_host_owned_relative_paths() {
|
||||
fn host_config_resolves_runtime_data_dir_relative_to_configuration() {
|
||||
let mut config = valid_config();
|
||||
config
|
||||
.resolve_and_validate(std::path::Path::new("/srv/gateway"))
|
||||
.resolve_and_validate(
|
||||
std::path::Path::new("/srv/gateway"),
|
||||
std::path::Path::new("/srv/gateway/web/dist"),
|
||||
)
|
||||
.expect("valid host config");
|
||||
|
||||
assert_eq!(
|
||||
@@ -51,7 +104,10 @@ fn host_config_resolves_runtime_data_dir_relative_to_configuration() {
|
||||
fn host_config_derives_update_paths_from_runtime_data_dir() {
|
||||
let mut config = valid_config();
|
||||
config
|
||||
.resolve_and_validate(std::path::Path::new("/srv/gateway"))
|
||||
.resolve_and_validate(
|
||||
std::path::Path::new("/srv/gateway"),
|
||||
std::path::Path::new("/srv/gateway/web/dist"),
|
||||
)
|
||||
.expect("valid host config");
|
||||
|
||||
assert_eq!(
|
||||
@@ -75,7 +131,10 @@ fn host_config_rejects_zero_drain_window() {
|
||||
|
||||
assert!(
|
||||
config
|
||||
.resolve_and_validate(std::path::Path::new("/srv/gateway"))
|
||||
.resolve_and_validate(
|
||||
std::path::Path::new("/srv/gateway"),
|
||||
std::path::Path::new("/srv/gateway/web/dist"),
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
@@ -91,7 +150,10 @@ fn logging_rejects_zero_retention_windows() {
|
||||
}
|
||||
assert!(
|
||||
config
|
||||
.resolve_and_validate(std::path::Path::new("/srv/gateway"))
|
||||
.resolve_and_validate(
|
||||
std::path::Path::new("/srv/gateway"),
|
||||
std::path::Path::new("/srv/gateway/web/dist"),
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
@@ -120,7 +182,10 @@ fn dedicated_file_channels_can_be_enabled_without_application_sinks() {
|
||||
config.logging.oauth_recovery = oauth_recovery;
|
||||
config.logging.request_dump = !oauth_recovery;
|
||||
config
|
||||
.resolve_and_validate(std::path::Path::new("/srv/gateway"))
|
||||
.resolve_and_validate(
|
||||
std::path::Path::new("/srv/gateway"),
|
||||
std::path::Path::new("/srv/gateway/web/dist"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,10 @@ fn logging_requires_at_least_one_sink() {
|
||||
|
||||
assert!(
|
||||
config
|
||||
.resolve_and_validate(std::path::Path::new("/tmp"))
|
||||
.resolve_and_validate(
|
||||
std::path::Path::new("/tmp"),
|
||||
std::path::Path::new("/tmp/web/dist"),
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -550,7 +550,7 @@ async fn update_should_remove_stale_file_lock_and_continue() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_should_replace_local_release_files_with_latest_asset() {
|
||||
async fn update_and_rollback_should_use_the_default_api_asset_directory() {
|
||||
let server = MockServer::start().await;
|
||||
let fixture = Fixture::new();
|
||||
fixture
|
||||
@@ -561,8 +561,21 @@ async fn update_should_replace_local_release_files_with_latest_asset() {
|
||||
ChecksumKind::Valid,
|
||||
)
|
||||
.await;
|
||||
fixture
|
||||
.service(&server)
|
||||
let mut host: gateway_host::HostConfig = serde_json::from_value(serde_json::json!({
|
||||
"listen": { "host": "127.0.0.1", "port": 8080 },
|
||||
"runtime_data_dir": "../.runtime/data",
|
||||
"logging": {
|
||||
"level": "info", "stdout": true,
|
||||
"file": { "enabled": false, "directory": "../.runtime/logs", "max_file_size_mb": 20 }
|
||||
}
|
||||
}))
|
||||
.expect("binary host config");
|
||||
host.system_update = fixture.config(&format!("{}/repos", server.uri()));
|
||||
host.system_update.web_dist_dir = None;
|
||||
host.resolve_and_validate(&fixture.root.path().join("deploy"), &fixture.web())
|
||||
.expect("inherit API asset directory");
|
||||
let service = ProcessSystemOperations::new(CancellationToken::new(), host.system_update);
|
||||
service
|
||||
.perform_update(Some(TARGET_VERSION.to_owned()))
|
||||
.await
|
||||
.expect("update");
|
||||
@@ -575,6 +588,16 @@ async fn update_should_replace_local_release_files_with_latest_asset() {
|
||||
fs::read(fixture.web().join("index.html")).expect("web"),
|
||||
b"new-web"
|
||||
);
|
||||
|
||||
service.rollback().await.expect("rollback");
|
||||
assert_eq!(
|
||||
fs::read(fixture.executable()).expect("binary"),
|
||||
b"old-binary"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(fixture.web().join("index.html")).expect("web"),
|
||||
b"old-web"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -593,17 +616,18 @@ async fn update_should_replace_web_assets_across_filesystems() {
|
||||
)
|
||||
.await;
|
||||
let mut config = fixture.config(&format!("{}/repos", server.uri()));
|
||||
config.web_dist_dir = external.path().join("dist");
|
||||
fs::create_dir_all(&config.web_dist_dir).expect("web dir");
|
||||
fs::write(config.web_dist_dir.join("index.html"), "old-web").expect("web");
|
||||
let service = ProcessSystemOperations::new(CancellationToken::new(), config.clone());
|
||||
let web_dist = external.path().join("dist");
|
||||
config.web_dist_dir = Some(web_dist.clone());
|
||||
fs::create_dir_all(&web_dist).expect("web dir");
|
||||
fs::write(web_dist.join("index.html"), "old-web").expect("web");
|
||||
let service = ProcessSystemOperations::new(CancellationToken::new(), config);
|
||||
service
|
||||
.perform_update(Some(TARGET_VERSION.to_owned()))
|
||||
.await
|
||||
.expect("update");
|
||||
|
||||
assert_eq!(
|
||||
fs::read(config.web_dist_dir.join("index.html")).expect("web"),
|
||||
fs::read(web_dist.join("index.html")).expect("web"),
|
||||
b"new-web"
|
||||
);
|
||||
}
|
||||
@@ -726,7 +750,7 @@ impl Fixture {
|
||||
update_repository: Some("owner/repository".to_owned()),
|
||||
github_api_base: api_base.to_owned(),
|
||||
executable_path: Some(self.executable()),
|
||||
web_dist_dir: self.web(),
|
||||
web_dist_dir: Some(self.web()),
|
||||
update_state_file: self.state(),
|
||||
update_lock_file: self.lock(),
|
||||
update_temp_dir: self.root.path().join("tmp"),
|
||||
|
||||
+3
-1
@@ -337,6 +337,7 @@ OpenAI 主动额度重置卡及其消费结果由上游持有,不写入 Postgr
|
||||
每个 Release 独立提供 `config.example.yaml`、默认镜像固定到该版本的 `compose.yaml` 和校验和;
|
||||
各平台归档也包含 `deploy/config.example.yaml`。配置模板来自构建该版本的同一提交。
|
||||
使用二进制归档手动部署时,将模板中的 `api.asset_directory` 改为 `../web/dist`,指向归档内的静态资源。
|
||||
在线更新默认使用同一目录;如显式设置 `host.system_update.web_dist_dir`,应确保它指向实际提供页面的目录。
|
||||
升级时先阅读目标版本说明,下载同一 Release 的部署附件,对比模板并合并必要配置,保留已有凭据
|
||||
和 Compose 自定义项。不要用模板覆盖 `config.yaml`,也不要从 `main` 下载模板搭配旧镜像。
|
||||
|
||||
@@ -375,7 +376,8 @@ Compose 已显式装配正式发布构建所需的运行参数:
|
||||
- `CPR_UPDATE_REPOSITORY`:只接受 `owner/repository`;默认 `zyycn/codex-proxy-rs`。
|
||||
- `CPR_GITHUB_API_BASE`:正式环境必须为 `https://api.github.com/repos`。
|
||||
- `CPR_UPDATE_CHANNEL`:`stable` 会拒绝 prerelease。
|
||||
- `CPR_UPDATE_EXE_PATH`、`CPR_WEB_DIST_DIR`:分别指向容器内二进制和前端静态目录。
|
||||
- `CPR_UPDATE_EXE_PATH`、`CPR_WEB_DIST_DIR`:分别指向容器内二进制和前端静态目录;
|
||||
`CPR_WEB_DIST_DIR` 同时供页面服务与更新器使用,相对路径以 `deploy/config.yaml` 所在目录为基准。
|
||||
- 更新临时目录、状态文件和锁文件默认由 `host.runtime_data_dir` 派生;
|
||||
`CPR_UPDATE_TEMP_DIR`、`CPR_UPDATE_STATE_FILE`、`CPR_UPDATE_LOCK_FILE` 仅用于显式覆盖。
|
||||
- `CPR_ENABLE_SELF_RESTART=true`:更新或回滚完成后允许管理端请求重启;Docker 进程退出后由
|
||||
|
||||
Reference in New Issue
Block a user