mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
feat: clarify deployment update strategies
This commit is contained in:
14
.env.example
14
.env.example
@@ -57,10 +57,18 @@ ADMIN_USERNAME=admin123456
|
||||
# docker compose 下 app 启动前自动执行 pending migration/backfill(默认 true)
|
||||
# AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
||||
|
||||
# 管理后台一键更新(正式发布版默认可用)
|
||||
# 正式发布版会下载 GitHub Release 包,校验 SHA256 后切换 /opt/aether/current 并重启。
|
||||
# 源码/本地构建不支持后台在线更新。
|
||||
# 管理后台更新策略:
|
||||
# - systemd/二进制部署使用 self:下载 GitHub Release 包,校验 SHA256 后切换 current 并重启。
|
||||
# - Docker Compose 使用 docker:后台只提示版本,实际更新请在 compose 目录执行 ./update.sh。
|
||||
# - 源码/本地构建使用 manual:手动拉取源码或下载 release。
|
||||
# Compose 默认把持久化文件放在 ./datas/{postgres,mysql,sqlite,redis},日志放在 ./logs。
|
||||
# 分布式/多节点部署不要使用 ./datas 作为共享数据目录;应使用外部共享 Postgres/MySQL 和 Redis。
|
||||
# 多节点不要从管理后台一键更新单个节点,应使用镜像滚动更新、systemd 分批发布或外部编排。
|
||||
# AETHER_BASE_DIR=/opt/aether
|
||||
# AETHER_UPDATE_STRATEGY=docker
|
||||
# AETHER_DOCKER_UPDATE_COMMAND=./update.sh
|
||||
# AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY=single-node
|
||||
# AETHER_GATEWAY_NODE_ROLE=all
|
||||
# Docker Compose 默认把应用日志输出到 stdout/stderr。
|
||||
# 如需文件日志,可改成 file 或 both,并把可写目录挂载到 /opt/aether/logs。
|
||||
# AETHER_LOG_DESTINATION=stdout
|
||||
|
||||
@@ -31,6 +31,7 @@ WORKDIR /opt/aether
|
||||
|
||||
ENV RUST_LOG=aether_gateway=info \
|
||||
APP_PORT=8084 \
|
||||
AETHER_UPDATE_STRATEGY=docker \
|
||||
AETHER_GATEWAY_STATIC_DIR=/opt/aether/current/frontend
|
||||
|
||||
EXPOSE 8084
|
||||
|
||||
@@ -136,6 +136,7 @@ ENV LANG=C.UTF-8 \
|
||||
LC_ALL=C.UTF-8 \
|
||||
RUST_LOG=aether_gateway=info \
|
||||
APP_PORT=8084 \
|
||||
AETHER_UPDATE_STRATEGY=manual \
|
||||
AETHER_GATEWAY_STATIC_DIR=/srv/frontend
|
||||
|
||||
EXPOSE 8084
|
||||
|
||||
@@ -139,6 +139,7 @@ ENV LANG=C.UTF-8 \
|
||||
RUST_LOG=aether_gateway=info \
|
||||
APP_PORT=8084 \
|
||||
AETHER_BASE_DIR=/opt/aether \
|
||||
AETHER_UPDATE_STRATEGY=self \
|
||||
AETHER_GATEWAY_STATIC_DIR=/opt/aether/current/frontend
|
||||
|
||||
EXPOSE 8084
|
||||
|
||||
@@ -19,9 +19,9 @@ use crate::handlers::admin::system::shared::settings::{
|
||||
};
|
||||
use crate::handlers::admin::system::shared::smtp::build_admin_smtp_test_payload;
|
||||
use crate::handlers::admin::system::shared::update::{
|
||||
build_admin_system_update_capability_payload, prepare_admin_system_update_task,
|
||||
read_update_history, read_update_task_status, start_admin_system_rollback_task,
|
||||
start_admin_system_update_task,
|
||||
build_admin_system_update_capability_payload, current_self_update_blocker,
|
||||
prepare_admin_system_update_task, read_update_history, read_update_task_status,
|
||||
self_update_supported, start_admin_system_rollback_task, start_admin_system_update_task,
|
||||
};
|
||||
use crate::important_notification::build_important_notification_test_payload;
|
||||
use crate::maintenance::{ManualUsageCleanupMode, ManualUsageCleanupOptions};
|
||||
@@ -99,6 +99,16 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
&& request_method == http::Method::POST
|
||||
&& request_path == "/api/admin/system/prepare-update"
|
||||
{
|
||||
if !self_update_supported() {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::PRECONDITION_REQUIRED,
|
||||
Json(json!({ "detail": current_self_update_blocker() })),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
|
||||
let target_version = request_body
|
||||
.filter(|b| !b.is_empty())
|
||||
.and_then(|body| serde_json::from_slice::<serde_json::Value>(body).ok())
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::build_admin_usage_counter_health_payload;
|
||||
use crate::handlers::admin::system::shared::update::{
|
||||
current_self_update_blocker, self_update_supported,
|
||||
};
|
||||
use crate::handlers::admin::system::shared::update_client::{
|
||||
build_direct_update_http_client, build_update_http_client, has_explicit_update_proxy_env,
|
||||
update_github_token_from_env,
|
||||
@@ -52,7 +55,7 @@ pub(crate) fn build_admin_system_check_update_payload_from_release(
|
||||
latest_release,
|
||||
error,
|
||||
);
|
||||
apply_source_build_check_update_override(&mut payload, current_build_is_release());
|
||||
apply_self_update_check_update_override(&mut payload, self_update_supported());
|
||||
payload
|
||||
}
|
||||
|
||||
@@ -62,7 +65,7 @@ pub(crate) fn build_admin_system_releases_list_payload(
|
||||
) -> serde_json::Value {
|
||||
let mut payload =
|
||||
build_admin_system_releases_payload(current_aether_version(), releases, error);
|
||||
apply_source_build_releases_override(&mut payload, current_build_is_release());
|
||||
apply_self_update_releases_override(&mut payload, self_update_supported());
|
||||
payload
|
||||
}
|
||||
|
||||
@@ -70,8 +73,20 @@ fn current_build_is_release() -> bool {
|
||||
option_env!("AETHER_BUILD_TYPE").unwrap_or("source") == "release"
|
||||
}
|
||||
|
||||
fn apply_source_build_check_update_override(payload: &mut Value, release_build: bool) {
|
||||
if release_build {
|
||||
fn apply_self_update_check_update_override(payload: &mut Value, supported: bool) {
|
||||
apply_self_update_check_update_override_with_blocker(
|
||||
payload,
|
||||
supported,
|
||||
current_self_update_blocker(),
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_self_update_check_update_override_with_blocker(
|
||||
payload: &mut Value,
|
||||
supported: bool,
|
||||
blocker: &str,
|
||||
) {
|
||||
if supported {
|
||||
return;
|
||||
}
|
||||
if payload.get("has_update").and_then(Value::as_bool) != Some(true) {
|
||||
@@ -79,11 +94,23 @@ fn apply_source_build_check_update_override(payload: &mut Value, release_build:
|
||||
}
|
||||
|
||||
payload["updatable"] = json!(false);
|
||||
payload["update_blocker"] = json!(SOURCE_BUILD_UPDATE_BLOCKER);
|
||||
payload["update_blocker"] = json!(blocker);
|
||||
}
|
||||
|
||||
fn apply_source_build_releases_override(payload: &mut Value, release_build: bool) {
|
||||
if release_build {
|
||||
fn apply_self_update_releases_override(payload: &mut Value, supported: bool) {
|
||||
apply_self_update_releases_override_with_blocker(
|
||||
payload,
|
||||
supported,
|
||||
current_self_update_release_blocker(),
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_self_update_releases_override_with_blocker(
|
||||
payload: &mut Value,
|
||||
supported: bool,
|
||||
blocker: &str,
|
||||
) {
|
||||
if supported {
|
||||
return;
|
||||
}
|
||||
let Some(releases) = payload.get_mut("releases").and_then(Value::as_array_mut) else {
|
||||
@@ -96,11 +123,23 @@ fn apply_source_build_releases_override(payload: &mut Value, release_build: bool
|
||||
}
|
||||
release["updatable"] = json!(false);
|
||||
if release.get("update_blocker").is_none() || release["update_blocker"].is_null() {
|
||||
release["update_blocker"] = json!(SOURCE_BUILD_RELEASE_BLOCKER);
|
||||
release["update_blocker"] = json!(blocker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_self_update_release_blocker() -> &'static str {
|
||||
if !current_build_is_release() {
|
||||
return SOURCE_BUILD_RELEASE_BLOCKER;
|
||||
}
|
||||
|
||||
if self_update_supported() {
|
||||
""
|
||||
} else {
|
||||
current_self_update_blocker()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
struct CachedReleases {
|
||||
all: Vec<AdminSystemUpdateRelease>,
|
||||
@@ -719,21 +758,25 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_build_check_update_override_marks_latest_release_non_updatable() {
|
||||
fn self_update_check_update_override_marks_latest_release_non_updatable() {
|
||||
let mut payload = json!({
|
||||
"has_update": true,
|
||||
"updatable": true,
|
||||
"update_blocker": serde_json::Value::Null
|
||||
});
|
||||
|
||||
apply_source_build_check_update_override(&mut payload, false);
|
||||
apply_self_update_check_update_override_with_blocker(
|
||||
&mut payload,
|
||||
false,
|
||||
SOURCE_BUILD_UPDATE_BLOCKER,
|
||||
);
|
||||
|
||||
assert_eq!(payload["updatable"], false);
|
||||
assert_eq!(payload["update_blocker"], SOURCE_BUILD_UPDATE_BLOCKER);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_build_releases_override_marks_non_current_entries_non_updatable() {
|
||||
fn self_update_releases_override_marks_non_current_entries_non_updatable() {
|
||||
let mut payload = json!({
|
||||
"releases": [
|
||||
{
|
||||
@@ -751,7 +794,11 @@ mod tests {
|
||||
]
|
||||
});
|
||||
|
||||
apply_source_build_releases_override(&mut payload, false);
|
||||
apply_self_update_releases_override_with_blocker(
|
||||
&mut payload,
|
||||
false,
|
||||
SOURCE_BUILD_RELEASE_BLOCKER,
|
||||
);
|
||||
|
||||
assert_eq!(payload["releases"][0]["updatable"], false);
|
||||
assert_eq!(
|
||||
|
||||
@@ -113,6 +113,74 @@ const MAX_SHA256SUMS_DOWNLOAD_BYTES: u64 = 1024 * 1024;
|
||||
const MAX_EXTRACTED_RELEASE_BYTES: u64 = 1024 * 1024 * 1024;
|
||||
const DEFAULT_UPDATE_DOWNLOAD_TIMEOUT_SECS: u64 = 600;
|
||||
const DEFAULT_UPDATE_DOWNLOAD_IDLE_TIMEOUT_SECS: u64 = 30;
|
||||
const SOURCE_BUILD_UPDATE_BLOCKER: &str = "当前为源码构建,请使用 git pull 后重新编译。";
|
||||
const DOCKER_UPDATE_BLOCKER: &str =
|
||||
"Docker 部署请使用镜像更新:进入 docker-compose.yml 所在目录执行 ./update.sh。";
|
||||
const MANUAL_UPDATE_BLOCKER: &str =
|
||||
"当前部署策略不支持在线自更新,请手动下载 Release 或使用安装脚本更新。";
|
||||
const MULTI_NODE_UPDATE_BLOCKER: &str =
|
||||
"多节点部署不支持在管理后台更新单个节点,请使用镜像滚动更新或外部发布编排。";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum UpdateStrategy {
|
||||
SelfManaged,
|
||||
Docker,
|
||||
Manual,
|
||||
}
|
||||
|
||||
impl UpdateStrategy {
|
||||
fn from_env_value(value: Option<&str>, release_build: bool) -> Self {
|
||||
let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return if release_build {
|
||||
Self::SelfManaged
|
||||
} else {
|
||||
Self::Manual
|
||||
};
|
||||
};
|
||||
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"self" | "self-managed" | "binary" | "systemd" | "launchd" => Self::SelfManaged,
|
||||
"docker" | "compose" | "docker-compose" | "container" => Self::Docker,
|
||||
"manual" | "source" | "none" | "off" | "disabled" => Self::Manual,
|
||||
_ => Self::Manual,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::SelfManaged => "self",
|
||||
Self::Docker => "docker",
|
||||
Self::Manual => "manual",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum DeploymentTopology {
|
||||
SingleNode,
|
||||
MultiNode,
|
||||
}
|
||||
|
||||
impl DeploymentTopology {
|
||||
fn from_env_value(value: Option<&str>) -> Self {
|
||||
match value
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.as_deref()
|
||||
{
|
||||
Some("multi-node" | "multi" | "cluster") => Self::MultiNode,
|
||||
_ => Self::SingleNode,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::SingleNode => "single-node",
|
||||
Self::MultiNode => "multi-node",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct UpdateHistoryEntry {
|
||||
@@ -249,12 +317,84 @@ fn is_release_build() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn current_update_strategy() -> UpdateStrategy {
|
||||
UpdateStrategy::from_env_value(
|
||||
std::env::var("AETHER_UPDATE_STRATEGY").ok().as_deref(),
|
||||
is_release_build(),
|
||||
)
|
||||
}
|
||||
|
||||
fn current_deployment_topology() -> DeploymentTopology {
|
||||
DeploymentTopology::from_env_value(
|
||||
std::env::var("AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY")
|
||||
.ok()
|
||||
.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
fn self_update_supported_for(
|
||||
release_build: bool,
|
||||
update_strategy: UpdateStrategy,
|
||||
deployment_topology: DeploymentTopology,
|
||||
) -> bool {
|
||||
release_build
|
||||
&& update_strategy == UpdateStrategy::SelfManaged
|
||||
&& deployment_topology == DeploymentTopology::SingleNode
|
||||
}
|
||||
|
||||
pub(crate) fn self_update_supported() -> bool {
|
||||
self_update_supported_for(
|
||||
is_release_build(),
|
||||
current_update_strategy(),
|
||||
current_deployment_topology(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn current_self_update_blocker() -> &'static str {
|
||||
if !is_release_build() {
|
||||
return SOURCE_BUILD_UPDATE_BLOCKER;
|
||||
}
|
||||
if current_deployment_topology() == DeploymentTopology::MultiNode {
|
||||
return MULTI_NODE_UPDATE_BLOCKER;
|
||||
}
|
||||
|
||||
match current_update_strategy() {
|
||||
UpdateStrategy::SelfManaged => "一键更新可用",
|
||||
UpdateStrategy::Docker => DOCKER_UPDATE_BLOCKER,
|
||||
UpdateStrategy::Manual => MANUAL_UPDATE_BLOCKER,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_logs_dir() -> PathBuf {
|
||||
std::env::var("AETHER_LOG_DIR")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| aether_base_dir().join("logs"))
|
||||
}
|
||||
|
||||
fn docker_update_command() -> String {
|
||||
std::env::var("AETHER_DOCKER_UPDATE_COMMAND")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "./update.sh".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_system_update_capability_payload() -> serde_json::Value {
|
||||
let supported = is_release_build();
|
||||
let build_type = current_build_type();
|
||||
let rollback_available = find_rollback_target().is_some();
|
||||
let update_strategy = current_update_strategy();
|
||||
let deployment_topology = current_deployment_topology();
|
||||
let supported =
|
||||
self_update_supported_for(is_release_build(), update_strategy, deployment_topology);
|
||||
let rollback_available = supported && find_rollback_target().is_some();
|
||||
let task_status = read_update_task_status();
|
||||
let base_dir = aether_base_dir();
|
||||
let docker_command = if update_strategy == UpdateStrategy::Docker {
|
||||
Some(docker_update_command())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let data_dir = base_dir.join("data");
|
||||
json!({
|
||||
"supported": supported,
|
||||
"enabled": supported,
|
||||
@@ -262,11 +402,19 @@ pub(crate) fn build_admin_system_update_capability_payload() -> serde_json::Valu
|
||||
"task_status": task_status.phase,
|
||||
"task_error": task_status.error,
|
||||
"build_type": build_type,
|
||||
"install_root": base_dir,
|
||||
"update_strategy": update_strategy.as_str(),
|
||||
"strategy": update_strategy.as_str(),
|
||||
"deployment_topology": deployment_topology.as_str(),
|
||||
"topology": deployment_topology.as_str(),
|
||||
"install_root": base_dir.clone(),
|
||||
"base_dir": base_dir,
|
||||
"data_dir": data_dir,
|
||||
"logs_dir": update_logs_dir(),
|
||||
"docker_update_command": docker_command,
|
||||
"message": if supported {
|
||||
"一键更新可用"
|
||||
} else {
|
||||
"源码构建不支持在线更新"
|
||||
current_self_update_blocker()
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -291,8 +439,8 @@ pub(crate) async fn prepare_admin_system_update_task(
|
||||
tarball_url: String,
|
||||
sha256sums_url: Option<String>,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
if !is_release_build() {
|
||||
return Ok(Err(source_build_rejection_response()));
|
||||
if !self_update_supported() {
|
||||
return Ok(Err(self_update_rejection_response()));
|
||||
}
|
||||
let Some(sha256sums_url) = sha256sums_url.filter(|url| !url.trim().is_empty()) else {
|
||||
return Ok(Err((
|
||||
@@ -692,8 +840,8 @@ fn remove_path_if_exists(path: &Path) -> std::io::Result<()> {
|
||||
pub(crate) async fn start_admin_system_update_task(
|
||||
version: Option<String>,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
if !is_release_build() {
|
||||
return Ok(Err(source_build_rejection_response()));
|
||||
if !self_update_supported() {
|
||||
return Ok(Err(self_update_rejection_response()));
|
||||
}
|
||||
|
||||
let version = match version.or_else(get_prepared_version) {
|
||||
@@ -800,8 +948,8 @@ fn switch_current_symlink(version: &str) -> Result<(), String> {
|
||||
|
||||
pub(crate) async fn start_admin_system_rollback_task(
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
if !is_release_build() {
|
||||
return Ok(Err(source_build_rejection_response()));
|
||||
if !self_update_supported() {
|
||||
return Ok(Err(self_update_rejection_response()));
|
||||
}
|
||||
|
||||
let Some(previous) = find_rollback_target() else {
|
||||
@@ -861,10 +1009,10 @@ fn update_already_running_response() -> (http::StatusCode, serde_json::Value) {
|
||||
)
|
||||
}
|
||||
|
||||
fn source_build_rejection_response() -> (http::StatusCode, serde_json::Value) {
|
||||
fn self_update_rejection_response() -> (http::StatusCode, serde_json::Value) {
|
||||
(
|
||||
http::StatusCode::PRECONDITION_REQUIRED,
|
||||
json!({ "detail": "\u{6e90}\u{7801}\u{6784}\u{5efa}\u{4e0d}\u{652f}\u{6301}\u{5728}\u{7ebf}\u{66f4}\u{65b0}\u{ff0c}\u{8bf7}\u{4f7f}\u{7528}\u{6b63}\u{5f0f}\u{53d1}\u{5e03}\u{7248}" }),
|
||||
json!({ "detail": current_self_update_blocker() }),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -893,6 +1041,60 @@ mod tests {
|
||||
.expect("frontend index should be written");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_strategy_defaults_to_self_only_for_release_builds() {
|
||||
assert_eq!(
|
||||
UpdateStrategy::from_env_value(None, true),
|
||||
UpdateStrategy::SelfManaged
|
||||
);
|
||||
assert_eq!(
|
||||
UpdateStrategy::from_env_value(None, false),
|
||||
UpdateStrategy::Manual
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_strategy_parses_docker_as_non_self_update() {
|
||||
assert_eq!(
|
||||
UpdateStrategy::from_env_value(Some("docker"), true),
|
||||
UpdateStrategy::Docker
|
||||
);
|
||||
assert_eq!(
|
||||
UpdateStrategy::from_env_value(Some("compose"), true),
|
||||
UpdateStrategy::Docker
|
||||
);
|
||||
assert_eq!(
|
||||
UpdateStrategy::from_env_value(Some("unknown"), true),
|
||||
UpdateStrategy::Manual
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deployment_topology_defaults_to_single_node() {
|
||||
assert_eq!(
|
||||
DeploymentTopology::from_env_value(None),
|
||||
DeploymentTopology::SingleNode
|
||||
);
|
||||
assert_eq!(
|
||||
DeploymentTopology::from_env_value(Some("multi-node")),
|
||||
DeploymentTopology::MultiNode
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_node_topology_disables_self_update() {
|
||||
assert!(self_update_supported_for(
|
||||
true,
|
||||
UpdateStrategy::SelfManaged,
|
||||
DeploymentTopology::SingleNode,
|
||||
));
|
||||
assert!(!self_update_supported_for(
|
||||
true,
|
||||
UpdateStrategy::SelfManaged,
|
||||
DeploymentTopology::MultiNode,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_finds_nested_release_payload_dir() {
|
||||
let staging = temp_test_dir("nested");
|
||||
|
||||
@@ -38,6 +38,7 @@ services:
|
||||
AETHER_UPDATE_PROXY_URL: ${AETHER_UPDATE_PROXY_URL:-}
|
||||
UPDATE_PROXY_URL: ${UPDATE_PROXY_URL:-}
|
||||
AETHER_BASE_DIR: /opt/aether
|
||||
AETHER_UPDATE_STRATEGY: self
|
||||
AETHER_GATEWAY_AUTO_PREPARE_DATABASE: ${AETHER_GATEWAY_AUTO_PREPARE_DATABASE:-true}
|
||||
ports:
|
||||
- "${AETHER_RELEASE_LOCAL_PORT:-18085}:${AETHER_RELEASE_LOCAL_PORT:-18085}"
|
||||
|
||||
@@ -7,6 +7,8 @@ services:
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
AETHER_BASE_DIR: /opt/aether
|
||||
AETHER_UPDATE_STRATEGY: docker
|
||||
AETHER_DOCKER_UPDATE_COMMAND: ${AETHER_DOCKER_UPDATE_COMMAND:-./update.sh}
|
||||
AETHER_DATABASE_DRIVER: sqlite
|
||||
AETHER_DATABASE_URL: sqlite:///opt/aether/data/aether.db
|
||||
AETHER_RUNTIME_BACKEND: memory
|
||||
@@ -23,5 +25,6 @@ services:
|
||||
ports:
|
||||
- "${APP_PORT:-8084}:${APP_PORT:-8084}"
|
||||
volumes:
|
||||
- ./data:/opt/aether/data
|
||||
- ./datas/sqlite:/opt/aether/data
|
||||
- ./logs:/opt/aether/logs
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./datas/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "127.0.0.1:${DB_PORT:-5432}:5432"
|
||||
command: >
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
container_name: aether-redis
|
||||
command: redis-server --appendonly yes --appendfsync everysec --save 60 1000 --requirepass ${REDIS_PASSWORD} --maxclients ${REDIS_MAXCLIENTS:-10000}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
- ./datas/redis:/data
|
||||
ports:
|
||||
- "127.0.0.1:${REDIS_PORT:-6379}:6379"
|
||||
healthcheck:
|
||||
@@ -61,7 +61,7 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-aether_root}
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./datas/mysql:/var/lib/mysql
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
@@ -83,6 +83,8 @@ services:
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
|
||||
TZ: Asia/Shanghai
|
||||
AETHER_BASE_DIR: /opt/aether
|
||||
AETHER_UPDATE_STRATEGY: docker
|
||||
AETHER_DOCKER_UPDATE_COMMAND: ${AETHER_DOCKER_UPDATE_COMMAND:-./update.sh}
|
||||
AETHER_LOG_DESTINATION: ${AETHER_LOG_DESTINATION:-stdout}
|
||||
AETHER_LOG_FORMAT: ${AETHER_LOG_FORMAT:-pretty}
|
||||
AETHER_LOG_DIR: ${AETHER_LOG_DIR:-/opt/aether/logs}
|
||||
@@ -98,9 +100,6 @@ services:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "${APP_PORT:-8084}:${APP_PORT:-8084}"
|
||||
volumes:
|
||||
- ./logs:/opt/aether/logs
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
mysql_data:
|
||||
redis_data:
|
||||
|
||||
@@ -382,11 +382,19 @@ export interface CheckUpdateResponse {
|
||||
export interface SystemUpdateCapabilityResponse {
|
||||
supported: boolean
|
||||
build_type: string
|
||||
update_strategy?: 'self' | 'docker' | 'manual' | string
|
||||
strategy?: 'self' | 'docker' | 'manual' | string
|
||||
deployment_topology?: 'single-node' | 'multi-node' | string
|
||||
topology?: 'single-node' | 'multi-node' | string
|
||||
enabled: boolean
|
||||
rollback_available: boolean
|
||||
task_status: string
|
||||
task_error: string | null
|
||||
install_root?: string
|
||||
base_dir?: string
|
||||
data_dir?: string
|
||||
logs_dir?: string
|
||||
docker_update_command?: string | null
|
||||
message: string
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,17 @@
|
||||
>
|
||||
{{ updateBlockerText }}
|
||||
</p>
|
||||
<div
|
||||
v-if="isDockerUpdate && dockerUpdateCommand"
|
||||
class="mt-3 w-full max-w-sm rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-left"
|
||||
>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
在 docker-compose.yml 所在目录执行
|
||||
</p>
|
||||
<code class="mt-1 block break-all rounded bg-background/70 px-2 py-1.5 font-mono text-xs text-foreground">
|
||||
{{ dockerUpdateCommand }}
|
||||
</code>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -199,8 +210,10 @@ const props = defineProps<{
|
||||
updatePhase?: 'download' | 'restart' | 'reconnecting'
|
||||
updating?: boolean
|
||||
updateSupported?: boolean
|
||||
updateStrategy?: string
|
||||
updatable?: boolean
|
||||
updateBlocker?: string | null
|
||||
dockerUpdateCommand?: string | null
|
||||
reconnectMessage?: string
|
||||
rollbackAvailable?: boolean
|
||||
rollingBack?: boolean
|
||||
@@ -220,6 +233,9 @@ const updatePhase = computed(() => props.updatePhase ?? 'download')
|
||||
const updateSupported = computed(() => props.updateSupported ?? true)
|
||||
const updatable = computed(() => props.updatable ?? true)
|
||||
const canApplyUpdate = computed(() => updateSupported.value && updatable.value)
|
||||
const updateStrategy = computed(() => props.updateStrategy ?? 'manual')
|
||||
const isDockerUpdate = computed(() => updateStrategy.value === 'docker' && !canApplyUpdate.value)
|
||||
const dockerUpdateCommand = computed(() => props.dockerUpdateCommand || '')
|
||||
const updateBlockerText = computed(() => {
|
||||
if (!updateSupported.value) return props.updateBlocker || SOURCE_BUILD_UPDATE_HINT
|
||||
return props.updateBlocker || '当前版本暂不支持在线更新'
|
||||
|
||||
@@ -413,6 +413,8 @@
|
||||
:update-supported="updateSupported"
|
||||
:updatable="updateInfo.updatable"
|
||||
:update-blocker="updateInfo.update_blocker"
|
||||
:update-strategy="updateStrategy"
|
||||
:docker-update-command="dockerUpdateCommand"
|
||||
:reconnect-message="reconnectMessage"
|
||||
:rollback-available="rollbackAvailable"
|
||||
:rolling-back="rollingBack"
|
||||
@@ -434,7 +436,7 @@ import { useDarkMode } from '@/composables/useDarkMode'
|
||||
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { isDemoMode } from '@/config/demo'
|
||||
import { adminApi, type CheckUpdateResponse, type ReleaseEntry, type UpdateTaskStatusResponse } from '@/api/admin'
|
||||
import { adminApi, type CheckUpdateResponse, type ReleaseEntry, type SystemUpdateCapabilityResponse, type UpdateTaskStatusResponse } from '@/api/admin'
|
||||
import { announcementApi, type Announcement } from '@/api/announcements'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
@@ -518,6 +520,9 @@ const versionStatus = ref<CheckUpdateResponse | null>(null)
|
||||
const loadingVersionStatus = ref(false)
|
||||
const applyingSystemUpdate = ref(false)
|
||||
const updateSupported = ref(true)
|
||||
const updateStrategy = ref('manual')
|
||||
const updateCapabilityMessage = ref<string | null>(null)
|
||||
const dockerUpdateCommand = ref<string | null>(null)
|
||||
const reconnectMessage = ref('等待服务恢复...')
|
||||
const rollbackAvailable = ref(false)
|
||||
const rollingBack = ref(false)
|
||||
@@ -529,6 +534,7 @@ const preparedUpdateVersion = ref<string | null>(
|
||||
)
|
||||
const SOURCE_BUILD_UPDATE_HINT = '当前为源码构建,请使用 git pull 后重新编译。'
|
||||
const SOURCE_BUILD_RELEASE_HINT = '当前为源码构建,请手动切换到对应标签后重新编译。'
|
||||
const MANUAL_UPDATE_HINT = '当前部署策略不支持在线自更新,请手动下载 Release 或使用安装脚本更新。'
|
||||
let versionStatusLoadPromise: Promise<CheckUpdateResponse | null> | null = null
|
||||
let updateStatusPollTimer: number | null = null
|
||||
const updateProgressPercent = computed(() => updateTaskStatus.value?.progress_percent ?? null)
|
||||
@@ -700,14 +706,13 @@ async function loadVersionStatus(force = false) {
|
||||
adminApi.getSystemUpdateCapability().catch(() => null),
|
||||
])
|
||||
if (capability) {
|
||||
rollbackAvailable.value = capability.rollback_available
|
||||
updateSupported.value = capability.supported
|
||||
applyUpdateCapability(capability)
|
||||
}
|
||||
versionStatus.value = capability?.supported === false && status.has_update
|
||||
versionStatus.value = updateSupported.value === false && status.has_update
|
||||
? {
|
||||
...status,
|
||||
updatable: false,
|
||||
update_blocker: SOURCE_BUILD_UPDATE_HINT,
|
||||
update_blocker: updateUnsupportedMessage(SOURCE_BUILD_UPDATE_HINT),
|
||||
}
|
||||
: status
|
||||
syncSystemUpdatePhase(versionStatus.value)
|
||||
@@ -724,6 +729,18 @@ async function loadVersionStatus(force = false) {
|
||||
return versionStatusLoadPromise
|
||||
}
|
||||
|
||||
function applyUpdateCapability(capability: SystemUpdateCapabilityResponse) {
|
||||
updateSupported.value = capability.supported
|
||||
rollbackAvailable.value = capability.supported && capability.rollback_available
|
||||
updateStrategy.value = capability.update_strategy || capability.strategy || 'manual'
|
||||
updateCapabilityMessage.value = capability.message || null
|
||||
dockerUpdateCommand.value = capability.docker_update_command || null
|
||||
}
|
||||
|
||||
function updateUnsupportedMessage(fallback = MANUAL_UPDATE_HINT): string {
|
||||
return updateCapabilityMessage.value || fallback
|
||||
}
|
||||
|
||||
function syncSystemUpdatePhase(status: CheckUpdateResponse | null) {
|
||||
if (systemUpdatePhase.value === 'reconnecting') return
|
||||
if (systemUpdatePhase.value === 'restart') {
|
||||
@@ -754,16 +771,16 @@ function buildUpdateInfoFromRelease(release: ReleaseEntry): CheckUpdateResponse
|
||||
updateInfo.value?.current_version ||
|
||||
__APP_VERSION__ ||
|
||||
''
|
||||
const sourceBuild = !updateSupported.value
|
||||
const canSelfUpdate = updateSupported.value
|
||||
return {
|
||||
current_version: currentVersion,
|
||||
latest_version: release.version,
|
||||
has_update: !release.is_current,
|
||||
updatable: !sourceBuild && !release.is_current && release.updatable,
|
||||
updatable: canSelfUpdate && !release.is_current && release.updatable,
|
||||
update_blocker: release.is_current
|
||||
? '当前已是这个版本'
|
||||
: sourceBuild
|
||||
? SOURCE_BUILD_RELEASE_HINT
|
||||
: !canSelfUpdate
|
||||
? updateUnsupportedMessage(SOURCE_BUILD_RELEASE_HINT)
|
||||
: release.update_blocker,
|
||||
release_url: release.release_url,
|
||||
release_notes: release.release_notes,
|
||||
@@ -787,16 +804,14 @@ async function handleApplySystemUpdate() {
|
||||
applyingSystemUpdate.value = true
|
||||
try {
|
||||
const capability = await adminApi.getSystemUpdateCapability()
|
||||
rollbackAvailable.value = capability.rollback_available
|
||||
applyUpdateCapability(capability)
|
||||
if (!capability.supported) {
|
||||
updateSupported.value = false
|
||||
showError(
|
||||
SOURCE_BUILD_UPDATE_HINT,
|
||||
updateUnsupportedMessage('不支持在线自更新'),
|
||||
'不支持在线更新'
|
||||
)
|
||||
return
|
||||
}
|
||||
updateSupported.value = true
|
||||
|
||||
if (systemUpdatePhase.value === 'download') {
|
||||
const targetStatus = updateInfo.value || versionStatus.value
|
||||
|
||||
26
install.sh
26
install.sh
@@ -1697,6 +1697,8 @@ AETHER_LOG_RETENTION_DAYS=7
|
||||
AETHER_LOG_MAX_FILES=30
|
||||
|
||||
APP_PORT=${APP_PORT:-8084}
|
||||
AETHER_BASE_DIR=${INSTALL_ROOT}
|
||||
AETHER_UPDATE_STRATEGY=self
|
||||
AETHER_GATEWAY_STATIC_DIR=${INSTALL_ROOT}/current/frontend
|
||||
AETHER_GATEWAY_VIDEO_TASK_TRUTH_SOURCE_MODE=rust-authoritative
|
||||
AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
||||
@@ -1736,6 +1738,8 @@ AETHER_LOG_RETENTION_DAYS=7
|
||||
AETHER_LOG_MAX_FILES=30
|
||||
|
||||
APP_PORT=${APP_PORT:-8084}
|
||||
AETHER_BASE_DIR=${INSTALL_ROOT}
|
||||
AETHER_UPDATE_STRATEGY=manual
|
||||
AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY=multi-node
|
||||
AETHER_GATEWAY_NODE_ROLE=${role}
|
||||
AETHER_GATEWAY_STATIC_DIR=${INSTALL_ROOT}/current/frontend
|
||||
@@ -1824,6 +1828,8 @@ generate_compose_env() {
|
||||
replace_or_append_env "${output}" "ADMIN_EMAIL" "${ADMIN_EMAIL:-admin@example.local}"
|
||||
replace_or_append_env "${output}" "ADMIN_USERNAME" "${ADMIN_USERNAME:-admin}"
|
||||
replace_or_append_env "${output}" "ADMIN_PASSWORD" "${ADMIN_PASSWORD}"
|
||||
replace_or_append_env "${output}" "AETHER_UPDATE_STRATEGY" "docker"
|
||||
replace_or_append_env "${output}" "AETHER_DOCKER_UPDATE_COMMAND" "./update.sh"
|
||||
append_compose_log_env_defaults "${output}"
|
||||
replace_or_append_env "${output}" "AETHER_GATEWAY_AUTO_PREPARE_DATABASE" "true"
|
||||
}
|
||||
@@ -1843,6 +1849,8 @@ $(compose_log_env_block)
|
||||
|
||||
APP_IMAGE=$(compose_image)
|
||||
APP_PORT=$(compose_app_port)
|
||||
AETHER_UPDATE_STRATEGY=docker
|
||||
AETHER_DOCKER_UPDATE_COMMAND=./update.sh
|
||||
AETHER_GATEWAY_STATIC_DIR=${COMPOSE_RELEASE_FRONTEND_DIR}
|
||||
AETHER_GATEWAY_VIDEO_TASK_TRUTH_SOURCE_MODE=rust-authoritative
|
||||
AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
||||
@@ -2223,6 +2231,12 @@ install_compose_mode() {
|
||||
resolve_compose_dir
|
||||
info "preparing Docker Compose deployment in ${COMPOSE_DIR}"
|
||||
ensure_directory "${COMPOSE_DIR}"
|
||||
ensure_directory "${COMPOSE_DIR}/logs"
|
||||
ensure_directory "${COMPOSE_DIR}/datas"
|
||||
ensure_directory "${COMPOSE_DIR}/datas/postgres"
|
||||
ensure_directory "${COMPOSE_DIR}/datas/redis"
|
||||
ensure_directory "${COMPOSE_DIR}/datas/mysql"
|
||||
ensure_directory "${COMPOSE_DIR}/datas/sqlite"
|
||||
install_project_file "docker-compose.yml" "${COMPOSE_DIR}/docker-compose.yml" "0644"
|
||||
install_project_file ".env.example" "${COMPOSE_DIR}/.env.example" "0644"
|
||||
install_project_file "update.sh" "${COMPOSE_DIR}/update.sh" "0755"
|
||||
@@ -2244,6 +2258,11 @@ Docker Compose files are ready:
|
||||
${COMPOSE_DIR}/.env.example
|
||||
${COMPOSE_DIR}/update.sh
|
||||
${COMPOSE_DIR}/generate_keys.sh
|
||||
${COMPOSE_DIR}/logs
|
||||
${COMPOSE_DIR}/datas/postgres
|
||||
${COMPOSE_DIR}/datas/redis
|
||||
${COMPOSE_DIR}/datas/mysql
|
||||
${COMPOSE_DIR}/datas/sqlite
|
||||
EOF
|
||||
|
||||
if [[ "${SKIP_START}" == "true" ]]; then
|
||||
@@ -2260,7 +2279,9 @@ install_compose_single_node_mode() {
|
||||
resolve_compose_dir
|
||||
info "preparing Docker Compose single-node deployment in ${COMPOSE_DIR}"
|
||||
ensure_directory "${COMPOSE_DIR}"
|
||||
ensure_directory "${COMPOSE_DIR}/data"
|
||||
ensure_directory "${COMPOSE_DIR}/datas"
|
||||
ensure_directory "${COMPOSE_DIR}/datas/sqlite"
|
||||
ensure_directory "${COMPOSE_DIR}/logs"
|
||||
|
||||
install_project_file "docker-compose.single-node.yml" "${COMPOSE_DIR}/docker-compose.yml" "0644"
|
||||
install_project_file ".env.example" "${COMPOSE_DIR}/.env.example" "0644"
|
||||
@@ -2283,7 +2304,8 @@ Docker Compose single-node files are ready:
|
||||
${COMPOSE_DIR}/.env.example
|
||||
${COMPOSE_DIR}/update.sh
|
||||
${COMPOSE_DIR}/generate_keys.sh
|
||||
${COMPOSE_DIR}/data
|
||||
${COMPOSE_DIR}/datas/sqlite
|
||||
${COMPOSE_DIR}/logs
|
||||
EOF
|
||||
|
||||
if [[ "${SKIP_START}" == "true" ]]; then
|
||||
|
||||
@@ -267,7 +267,7 @@ validate_env_line_for_copy() {
|
||||
|
||||
should_skip_single_node_env_key() {
|
||||
case "$1" in
|
||||
APP_IMAGE|LOCAL_APP_IMAGE|APP_PORT|DB_HOST|DB_PORT|DB_USER|DB_NAME|DB_PASSWORD|POSTGRES_*|MYSQL_*|REDIS_HOST|REDIS_PORT|REDIS_PASSWORD|REDIS_URL|AETHER_GATEWAY_DATA_REDIS_URL|AETHER_GATEWAY_DATA_REDIS_KEY_PREFIX|DATABASE_URL|AETHER_DATABASE_URL|AETHER_DATABASE_DRIVER|AETHER_GATEWAY_DATA_POSTGRES_URL|AETHER_RUNTIME_BACKEND|AETHER_RUNTIME_REDIS_URL|AETHER_RUNTIME_REDIS_KEY_PREFIX|AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY|AETHER_GATEWAY_NODE_ROLE|AETHER_GATEWAY_STATIC_DIR|AETHER_LOG_DIR|AETHER_GATEWAY_AUTO_PREPARE_DATABASE)
|
||||
APP_IMAGE|LOCAL_APP_IMAGE|APP_PORT|DB_HOST|DB_PORT|DB_USER|DB_NAME|DB_PASSWORD|POSTGRES_*|MYSQL_*|REDIS_HOST|REDIS_PORT|REDIS_PASSWORD|REDIS_URL|AETHER_GATEWAY_DATA_REDIS_URL|AETHER_GATEWAY_DATA_REDIS_KEY_PREFIX|DATABASE_URL|AETHER_DATABASE_URL|AETHER_DATABASE_DRIVER|AETHER_GATEWAY_DATA_POSTGRES_URL|AETHER_RUNTIME_BACKEND|AETHER_RUNTIME_REDIS_URL|AETHER_RUNTIME_REDIS_KEY_PREFIX|AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY|AETHER_GATEWAY_NODE_ROLE|AETHER_GATEWAY_STATIC_DIR|AETHER_UPDATE_STRATEGY|AETHER_DOCKER_UPDATE_COMMAND|AETHER_LOG_DIR|AETHER_GATEWAY_AUTO_PREPARE_DATABASE)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
@@ -316,12 +316,14 @@ write_single_node_env() {
|
||||
printf 'APP_IMAGE=%s\n' "$app_image"
|
||||
printf 'APP_PORT=%s\n' "$app_port"
|
||||
printf 'AETHER_GATEWAY_STATIC_DIR=/opt/aether/current/frontend\n'
|
||||
printf 'AETHER_LOG_DESTINATION=both\n'
|
||||
printf 'AETHER_UPDATE_STRATEGY=docker\n'
|
||||
printf 'AETHER_DOCKER_UPDATE_COMMAND=./update.sh\n'
|
||||
printf 'AETHER_LOG_DESTINATION=stdout\n'
|
||||
printf 'AETHER_LOG_FORMAT=pretty\n'
|
||||
printf 'AETHER_LOG_DIR=/app/logs\n'
|
||||
printf 'AETHER_LOG_DIR=/opt/aether/logs\n'
|
||||
printf 'AETHER_DATABASE_DRIVER=sqlite\n'
|
||||
printf 'AETHER_DATABASE_URL=sqlite://./data/aether.db\n'
|
||||
printf 'DATABASE_URL=sqlite://./data/aether.db\n'
|
||||
printf 'AETHER_DATABASE_URL=sqlite:///opt/aether/data/aether.db\n'
|
||||
printf 'DATABASE_URL=sqlite:///opt/aether/data/aether.db\n'
|
||||
printf 'AETHER_RUNTIME_BACKEND=memory\n'
|
||||
printf 'AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY=single-node\n'
|
||||
printf 'AETHER_GATEWAY_NODE_ROLE=all\n'
|
||||
@@ -728,13 +730,15 @@ preflight() {
|
||||
|
||||
NOW="$(date +%Y%m%d%H%M%S)"
|
||||
if [[ -z "$WORK_DIR" ]]; then
|
||||
WORK_DIR="${SOURCE_COMPOSE_DIR}/data/pg-compose-to-single-node-${NOW}"
|
||||
WORK_DIR="${SOURCE_COMPOSE_DIR}/datas/sqlite/pg-compose-to-single-node-${NOW}"
|
||||
fi
|
||||
WORK_DIR="$(absolute_path_maybe_missing "$WORK_DIR")"
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
prepare_target_compose
|
||||
TARGET_DB="${TARGET_DB:-${TARGET_COMPOSE_DIR}/data/aether.db}"
|
||||
mkdir -p "${TARGET_COMPOSE_DIR}/logs"
|
||||
mkdir -p "${TARGET_COMPOSE_DIR}/datas/sqlite"
|
||||
TARGET_DB="${TARGET_DB:-${TARGET_COMPOSE_DIR}/datas/sqlite/aether.db}"
|
||||
TARGET_DB="$(absolute_path_maybe_missing "$TARGET_DB")"
|
||||
|
||||
DB_USER="$(env_file_get "$SOURCE_ENV" "DB_USER")"
|
||||
|
||||
Reference in New Issue
Block a user