Embed UI templates and static assets into binary; simplify deployment to single file; Update script files

This commit is contained in:
2026-04-27 13:35:50 +08:00
parent 02fa9d612c
commit 05a30d2227
11 changed files with 435 additions and 529 deletions

84
Cargo.lock generated
View File

@@ -1460,6 +1460,27 @@ dependencies = [
"crypto-common 0.2.1",
]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -2733,6 +2754,7 @@ dependencies = [
"regex",
"reqwest",
"roxmltree",
"rust-embed",
"serde",
"serde_json",
"serde_urlencoded",
@@ -2872,6 +2894,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "outref"
version = "0.5.2"
@@ -3325,6 +3353,17 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.17",
"libredox",
"thiserror",
]
[[package]]
name = "regex"
version = "1.12.3"
@@ -3471,6 +3510,42 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rust-embed"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"shellexpand",
"syn 2.0.117",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
dependencies = [
"globset",
"sha2 0.10.9",
"walkdir",
]
[[package]]
name = "rust_decimal"
version = "1.41.0"
@@ -3801,6 +3876,15 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shellexpand"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8"
dependencies = [
"dirs",
]
[[package]]
name = "shlex"
version = "1.3.0"

View File

@@ -48,6 +48,7 @@ aws-smithy-types = { workspace = true }
async-trait = { workspace = true }
rand = "0.8"
tera = { workspace = true }
rust-embed = { version = "8", features = ["debug-embed", "include-exclude", "interpolate-folder-path"] }
cookie = { workspace = true }
subtle = { workspace = true }
clap = { workspace = true }

View File

@@ -0,0 +1,25 @@
use rust_embed::{EmbeddedFile, RustEmbed};
#[derive(RustEmbed)]
#[folder = "$CARGO_MANIFEST_DIR/templates"]
#[include = "*.html"]
pub struct EmbeddedTemplates;
#[derive(RustEmbed)]
#[folder = "$CARGO_MANIFEST_DIR/static"]
pub struct EmbeddedStatic;
pub fn template_names() -> Vec<String> {
EmbeddedTemplates::iter()
.map(|c: std::borrow::Cow<'static, str>| c.into_owned())
.collect()
}
pub fn template_contents(name: &str) -> Option<String> {
let file = EmbeddedTemplates::get(name)?;
String::from_utf8(file.data.into_owned()).ok()
}
pub fn static_file(path: &str) -> Option<EmbeddedFile> {
EmbeddedStatic::get(path)
}

View File

@@ -3,6 +3,7 @@ mod chunked;
mod config;
pub mod kms;
mod select;
pub mod static_assets;
pub mod ui;
pub mod ui_api;
pub mod ui_pages;

View File

@@ -0,0 +1,56 @@
use axum::{
body::Body,
extract::{Path, State},
http::{header, HeaderValue, StatusCode},
response::{IntoResponse, Response},
};
use crate::embedded;
use crate::state::AppState;
pub async fn serve(State(state): State<AppState>, Path(path): Path<String>) -> Response {
let normalized = path.trim_start_matches('/').to_string();
if normalized.is_empty() || normalized.contains("..") {
return StatusCode::NOT_FOUND.into_response();
}
let use_disk = std::env::var("STATIC_DIR").is_ok() && state.config.static_dir.is_dir();
if use_disk {
let candidate = state.config.static_dir.join(&normalized);
if let Ok(canonical) = candidate.canonicalize() {
if canonical.starts_with(
state
.config
.static_dir
.canonicalize()
.unwrap_or_else(|_| state.config.static_dir.clone()),
) {
if let Ok(bytes) = tokio::fs::read(&canonical).await {
let mime = mime_guess::from_path(&canonical).first_or_octet_stream();
return build_response(&normalized, bytes, mime.as_ref());
}
}
}
return StatusCode::NOT_FOUND.into_response();
}
match embedded::static_file(&normalized) {
Some(file) => {
let mime = mime_guess::from_path(&normalized).first_or_octet_stream();
build_response(&normalized, file.data.into_owned(), mime.as_ref())
}
None => StatusCode::NOT_FOUND.into_response(),
}
}
fn build_response(_path: &str, bytes: Vec<u8>, mime: &str) -> Response {
let mut response = Response::new(Body::from(bytes));
if let Ok(value) = HeaderValue::from_str(mime) {
response.headers_mut().insert(header::CONTENT_TYPE, value);
}
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("no-cache"),
);
response
}

View File

@@ -1,4 +1,5 @@
pub mod config;
pub mod embedded;
pub mod handlers;
pub mod middleware;
pub mod services;
@@ -321,14 +322,12 @@ pub fn create_ui_router(state: state::AppState) -> Router {
secure: false,
};
let static_service = tower::ServiceBuilder::new()
.layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
axum::http::header::CACHE_CONTROL,
axum::http::HeaderValue::from_static("no-cache"),
))
.service(tower_http::services::ServeDir::new(
&state.config.static_dir,
));
let static_router = Router::new()
.route(
"/static/{*path}",
axum::routing::get(handlers::static_assets::serve),
)
.with_state(state.clone());
protected
.merge(public)
@@ -346,7 +345,7 @@ pub fn create_ui_router(state: state::AppState) -> Router {
middleware::ui_metrics_layer,
))
.with_state(state)
.nest_service("/static", static_service)
.merge(static_router)
.layer(axum::middleware::from_fn(middleware::server_header))
.layer(tower_http::compression::CompressionLayer::new())
}

View File

@@ -260,8 +260,16 @@ impl AppState {
}
fn init_templates(templates_dir: &std::path::Path) -> Option<Arc<TemplateEngine>> {
let glob = format!("{}/*.html", templates_dir.display()).replace('\\', "/");
match TemplateEngine::new(&glob) {
let use_disk = std::env::var("TEMPLATES_DIR").is_ok() && templates_dir.is_dir();
let result = if use_disk {
let glob = format!("{}/*.html", templates_dir.display()).replace('\\', "/");
tracing::info!("Loading templates from disk: {}", templates_dir.display());
TemplateEngine::new(&glob)
} else {
tracing::info!("Loading templates from embedded assets");
TemplateEngine::from_embedded()
};
match result {
Ok(engine) => {
crate::handlers::ui_pages::register_ui_endpoints(&engine);
Some(Arc::new(engine))

View File

@@ -31,6 +31,33 @@ impl TemplateEngine {
})
}
pub fn from_embedded() -> Result<Self, TeraError> {
let mut tera = Tera::default();
tera.set_escape_fn(html_escape);
register_filters(&mut tera);
let names = crate::embedded::template_names();
let mut entries: Vec<(String, String)> = Vec::with_capacity(names.len());
for name in names {
if let Some(contents) = crate::embedded::template_contents(&name) {
entries.push((name, contents));
}
}
let refs: Vec<(&str, &str)> = entries
.iter()
.map(|(n, c)| (n.as_str(), c.as_str()))
.collect();
tera.add_raw_templates(refs)?;
let endpoints: Arc<RwLock<HashMap<String, String>>> = Arc::new(RwLock::new(HashMap::new()));
register_functions(&mut tera, endpoints.clone());
Ok(Self {
tera: Arc::new(RwLock::new(tera)),
endpoints,
})
}
pub fn register_endpoint(&self, name: &str, path_template: &str) {
self.endpoints
.write()
@@ -343,6 +370,24 @@ mod tests {
);
}
#[test]
fn embedded_templates_parse() {
let engine = TemplateEngine::from_embedded().expect("Embedded Tera parse failed");
let names: Vec<String> = engine
.tera
.read()
.get_template_names()
.map(|s| s.to_string())
.collect();
assert!(
names.len() >= 10,
"expected 10+ embedded templates, got {}",
names.len()
);
assert!(names.iter().any(|n| n == "login.html"));
assert!(names.iter().any(|n| n == "404.html"));
}
#[test]
fn format_datetime_rfc3339() {
let v = format_datetime_filter(

View File

@@ -1,292 +0,0 @@
import os
import re
import sys
TEMPLATE_DIR = os.path.dirname(os.path.abspath(__file__))
TERNARY_RE = re.compile(
r"""(\{\{\s*)
(?:"([^"]*)"|'([^']*)') # literal A
\s+if\s+
([^{}]+?) # condition
\s+else\s+
(?:"([^"]*)"|'([^']*)') # literal B
(\s*\}\})""",
re.VERBOSE,
)
TERNARY_SET_RE = re.compile(
r"""(\{%\s*set\s+([A-Za-z_][A-Za-z_0-9]*)\s*=\s*)
(?:"([^"]*)"|'([^']*)')
\s+if\s+
([^{}]+?)
\s+else\s+
(?:"([^"]*)"|'([^']*)')
(\s*%\})""",
re.VERBOSE,
)
def convert_single_quoted_strings_in_expressions(text: str) -> str:
"""Inside {{...}} or {%...%}, swap ' for " around tokens that look like strings."""
def fix(m):
body = m.group(2)
body_fixed = re.sub(r"'([^'\\\n]*)'", r'"\1"', body)
return m.group(1) + body_fixed + m.group(3)
return re.sub(
r"(\{[{%])([^{}]*?)([}%]\})",
fix,
text,
flags=re.DOTALL,
)
def convert_inline_ternary(text: str) -> str:
def repl_expr(m):
a = m.group(2) if m.group(2) is not None else m.group(3)
cond = m.group(4)
b = m.group(5) if m.group(5) is not None else m.group(6)
return (
'{% if ' + cond + ' %}' + a + '{% else %}' + b + '{% endif %}'
)
def repl_set(m):
varname = m.group(2)
a = m.group(3) if m.group(3) is not None else m.group(4)
cond = m.group(5)
b = m.group(6) if m.group(6) is not None else m.group(7)
return (
'{% if ' + cond + ' %}{% set ' + varname + ' = "' + a + '" %}'
'{% else %}{% set ' + varname + ' = "' + b + '" %}{% endif %}'
)
prev = None
while prev != text:
prev = text
text = TERNARY_SET_RE.sub(repl_set, text)
text = TERNARY_RE.sub(repl_expr, text)
return text
def convert_request_args(text: str) -> str:
text = re.sub(
r'request\.args\.get\(\s*"([^"]+)"\s*,\s*"([^"]*)"\s*\)',
r'request_args.\1 | default(value="\2")',
text,
)
text = re.sub(
r'request\.args\.get\(\s*"([^"]+)"\s*\)',
r'request_args.\1',
text,
)
text = text.replace('request.endpoint', 'current_endpoint')
return text
def convert_items_keys(text: str) -> str:
text = re.sub(r'\.items\(\)', '', text)
text = re.sub(r'\.keys\(\)', '', text)
text = re.sub(r'\.values\(\)', '', text)
return text
def convert_tojson(text: str) -> str:
text = re.sub(r'\|\s*tojson\b', '| json_encode | safe', text)
return text
def convert_is_none(text: str) -> str:
text = re.sub(r'\bis\s+not\s+none\b', '!= null', text)
text = re.sub(r'\bis\s+none\b', '== null', text)
return text
def convert_namespace(text: str) -> str:
def repl(m):
body = m.group(1)
assigns = [a.strip() for a in body.split(',')]
return '{# namespace shim #}'
text = re.sub(
r'\{%\s*set\s+ns\s*=\s*namespace\(([^)]*)\)\s*%\}',
repl,
text,
)
text = re.sub(r'\bns\.([A-Za-z_][A-Za-z_0-9]*)\s*=\s*', r'{% set_global \1 = ', text)
text = re.sub(r'\bns\.([A-Za-z_][A-Za-z_0-9]*)', r'\1', text)
return text
def convert_url_for_positional(text: str) -> str:
"""url_for("x", ...) -> url_for(endpoint="x", ...)"""
def repl(m):
prefix = m.group(1)
endpoint = m.group(2)
rest = m.group(3) or ''
rest = rest.strip()
if rest.startswith(','):
rest = rest[1:].strip()
if rest:
return f'{prefix}(endpoint="{endpoint}", {rest})'
return f'{prefix}(endpoint="{endpoint}")'
pattern = re.compile(r'(url_for)\(\s*"([^"]+)"\s*((?:,[^()]*)?)\)')
prev = None
while prev != text:
prev = text
text = pattern.sub(repl, text)
return text
def convert_d_filter(text: str) -> str:
text = re.sub(r'\|\s*d\(\s*([^)]*?)\s*\)', lambda m: f'| default(value={m.group(1) or 0})', text)
return text
def convert_replace_filter(text: str) -> str:
def repl(m):
a = m.group(1)
b = m.group(2)
return f'| replace(from="{a}", to="{b}")'
text = re.sub(r'\|\s*replace\(\s*"([^"]*)"\s*,\s*"([^"]*)"\s*\)', repl, text)
return text
def convert_truncate_filter(text: str) -> str:
def repl(m):
n = m.group(1)
return f'| truncate(length={n})'
text = re.sub(r'\|\s*truncate\(\s*(\d+)\s*(?:,[^)]*)?\)', repl, text)
return text
def convert_strip_method(text: str) -> str:
text = re.sub(r'(\b[A-Za-z_][A-Za-z_0-9.\[\]"]*)\s*\.\s*strip\(\s*\)', r'\1 | trim', text)
return text
def convert_split_method(text: str) -> str:
def repl(m):
obj = m.group(1)
sep = m.group(2)
return f'{obj} | split(pat="{sep}")'
text = re.sub(r'(\b[A-Za-z_][A-Za-z_0-9.]*)\s*\.\s*split\(\s*"([^"]*)"\s*\)', repl, text)
return text
def convert_python_slice(text: str) -> str:
def repl_colon(m):
obj = m.group(1)
start = m.group(2) or '0'
end = m.group(3)
if start.startswith('-') or (end and end.startswith('-')):
return m.group(0)
if end:
return f'{obj} | slice(start={start}, end={end})'
return f'{obj} | slice(start={start})'
def repl_neg_end(m):
obj = m.group(1)
n = m.group(2)
return f'{obj} | slice(start=-{n})'
text = re.sub(
r'(\b[A-Za-z_][A-Za-z_0-9.]*)\[\s*(-?\d*)\s*:\s*(-?\d*)\s*\]',
repl_colon,
text,
)
text = re.sub(
r'(\b[A-Za-z_][A-Za-z_0-9.]*)\|\s*slice\(start=-(\d+)\s*,\s*end=\s*\)',
repl_neg_end,
text,
)
return text
def convert_inline_ternary_expr(text: str) -> str:
"""Handle arbitrary ternary inside {{ ... }}: A if COND else B -> {% if COND %}A{% else %}B{% endif %}"""
out_lines = []
for line in text.split('\n'):
out_lines.append(_convert_line_ternary(line))
return '\n'.join(out_lines)
def _convert_line_ternary(line: str) -> str:
if '{{' not in line or ' if ' not in line or ' else ' not in line:
return line
prev = None
while prev != line:
prev = line
m = re.search(r'\{\{\s*([^{}]+?)\s+if\s+([^{}]+?)\s+else\s+([^{}]+?)\s*\}\}', line)
if not m:
break
replacement = '{% if ' + m.group(2) + ' %}{{ ' + m.group(1) + ' }}{% else %}{{ ' + m.group(3) + ' }}{% endif %}'
line = line[:m.start()] + replacement + line[m.end():]
return line
def convert_dict_get(text: str) -> str:
"""Convert X.get("key", default) -> X.key | default(value=default) when simple."""
pattern = re.compile(
r'([A-Za-z_][A-Za-z_0-9]*(?:\.[A-Za-z_][A-Za-z_0-9]*)*)'
r'\.get\(\s*"([A-Za-z_][A-Za-z_0-9]*)"\s*(?:,\s*([^(){}]+?))?\s*\)'
)
def repl(m):
obj = m.group(1)
key = m.group(2)
default = (m.group(3) or '').strip()
if default:
return f'{obj}.{key} | default(value={default})'
return f'{obj}.{key}'
prev = None
while prev != text:
prev = text
text = pattern.sub(repl, text)
return text
def convert_file(path: str) -> bool:
with open(path, 'r', encoding='utf-8') as f:
original = f.read()
text = original
text = convert_single_quoted_strings_in_expressions(text)
text = convert_inline_ternary(text)
text = convert_request_args(text)
text = convert_items_keys(text)
text = convert_tojson(text)
text = convert_is_none(text)
text = convert_namespace(text)
text = convert_dict_get(text)
text = convert_url_for_positional(text)
text = convert_d_filter(text)
text = convert_replace_filter(text)
text = convert_truncate_filter(text)
text = convert_strip_method(text)
text = convert_split_method(text)
text = convert_python_slice(text)
text = convert_inline_ternary_expr(text)
if text != original:
with open(path, 'w', encoding='utf-8', newline='\n') as f:
f.write(text)
return True
return False
def main():
changed = []
for name in sorted(os.listdir(TEMPLATE_DIR)):
if not name.endswith('.html'):
continue
p = os.path.join(TEMPLATE_DIR, name)
if convert_file(p):
changed.append(name)
print('Changed:', len(changed))
for c in changed:
print(' -', c)
if __name__ == '__main__':
main()

View File

@@ -11,11 +11,12 @@
# --data-dir DIR Data directory (default: /var/lib/myfsio)
# --log-dir DIR Log directory (default: /var/log/myfsio)
# --user USER System user to run as (default: myfsio)
# --host HOST Bind host (default: 0.0.0.0)
# --port PORT API port (default: 5000)
# --ui-port PORT UI port (default: 5100)
# --api-url URL Public API URL (for presigned URLs behind proxy)
# --no-systemd Skip systemd service creation
# --binary PATH Path to myfsio binary (will download if not provided)
# --binary PATH Path to myfsio binary (default: ./myfsio)
# -y, --yes Skip confirmation prompts
#
@@ -25,6 +26,7 @@ INSTALL_DIR="/opt/myfsio"
DATA_DIR="/var/lib/myfsio"
LOG_DIR="/var/log/myfsio"
SERVICE_USER="myfsio"
BIND_HOST="0.0.0.0"
API_PORT="5000"
UI_PORT="5100"
API_URL=""
@@ -34,54 +36,19 @@ AUTO_YES=false
while [[ $# -gt 0 ]]; do
case $1 in
--install-dir)
INSTALL_DIR="$2"
shift 2
;;
--data-dir)
DATA_DIR="$2"
shift 2
;;
--log-dir)
LOG_DIR="$2"
shift 2
;;
--user)
SERVICE_USER="$2"
shift 2
;;
--port)
API_PORT="$2"
shift 2
;;
--ui-port)
UI_PORT="$2"
shift 2
;;
--api-url)
API_URL="$2"
shift 2
;;
--no-systemd)
SKIP_SYSTEMD=true
shift
;;
--binary)
BINARY_PATH="$2"
shift 2
;;
-y|--yes)
AUTO_YES=true
shift
;;
-h|--help)
head -30 "$0" | tail -25
exit 0
;;
*)
echo "Unknown option: $1"
exit 1
;;
--install-dir) INSTALL_DIR="$2"; shift 2 ;;
--data-dir) DATA_DIR="$2"; shift 2 ;;
--log-dir) LOG_DIR="$2"; shift 2 ;;
--user) SERVICE_USER="$2"; shift 2 ;;
--host) BIND_HOST="$2"; shift 2 ;;
--port) API_PORT="$2"; shift 2 ;;
--ui-port) UI_PORT="$2"; shift 2 ;;
--api-url) API_URL="$2"; shift 2 ;;
--no-systemd) SKIP_SYSTEMD=true; shift ;;
--binary) BINARY_PATH="$2"; shift 2 ;;
-y|--yes) AUTO_YES=true; shift ;;
-h|--help) head -22 "$0" | tail -17; exit 0 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
@@ -107,14 +74,11 @@ echo " Install directory: $INSTALL_DIR"
echo " Data directory: $DATA_DIR"
echo " Log directory: $LOG_DIR"
echo " Service user: $SERVICE_USER"
echo " Bind host: $BIND_HOST"
echo " API port: $API_PORT"
echo " UI port: $UI_PORT"
if [[ -n "$API_URL" ]]; then
echo " Public API URL: $API_URL"
fi
if [[ -n "$BINARY_PATH" ]]; then
echo " Binary path: $BINARY_PATH"
fi
[[ -n "$API_URL" ]] && echo " Public API URL: $API_URL"
[[ -n "$BINARY_PATH" ]] && echo " Binary: $BINARY_PATH"
echo ""
if [[ "$AUTO_YES" != true ]]; then
@@ -143,12 +107,9 @@ echo "------------------------------------------------------------"
echo "STEP 3: Creating Directories"
echo "------------------------------------------------------------"
echo ""
mkdir -p "$INSTALL_DIR"
echo " [OK] Created $INSTALL_DIR"
mkdir -p "$DATA_DIR"
echo " [OK] Created $DATA_DIR"
mkdir -p "$LOG_DIR"
echo " [OK] Created $LOG_DIR"
mkdir -p "$INSTALL_DIR" && echo " [OK] Created $INSTALL_DIR"
mkdir -p "$DATA_DIR" && echo " [OK] Created $DATA_DIR"
mkdir -p "$LOG_DIR" && echo " [OK] Created $LOG_DIR"
echo ""
echo "------------------------------------------------------------"
@@ -156,13 +117,12 @@ echo "STEP 4: Installing Binary"
echo "------------------------------------------------------------"
echo ""
if [[ -n "$BINARY_PATH" ]]; then
if [[ -f "$BINARY_PATH" ]]; then
cp "$BINARY_PATH" "$INSTALL_DIR/myfsio"
echo " [OK] Copied binary from $BINARY_PATH"
else
if [[ ! -f "$BINARY_PATH" ]]; then
echo " [ERROR] Binary not found at $BINARY_PATH"
exit 1
fi
cp "$BINARY_PATH" "$INSTALL_DIR/myfsio"
echo " [OK] Copied binary from $BINARY_PATH"
elif [[ -f "./myfsio" ]]; then
cp "./myfsio" "$INSTALL_DIR/myfsio"
echo " [OK] Copied binary from ./myfsio"
@@ -173,20 +133,53 @@ else
fi
chmod +x "$INSTALL_DIR/myfsio"
echo " [OK] Set executable permissions"
echo " [INFO] Templates and static UI assets are embedded in the binary"
echo ""
echo "------------------------------------------------------------"
echo "STEP 5: Generating Secret Key"
echo "STEP 5: Creating Configuration File"
echo "------------------------------------------------------------"
echo ""
SECRET_KEY=$(openssl rand -base64 32)
echo " [OK] Generated secure SECRET_KEY"
echo ""
echo "------------------------------------------------------------"
echo "STEP 6: Creating Configuration File"
echo "------------------------------------------------------------"
echo ""
SECRET_FILE="$DATA_DIR/.myfsio.sys/config/.secret"
mkdir -p "$(dirname "$SECRET_FILE")"
if [[ -s "$SECRET_FILE" ]]; then
echo " [OK] Existing secret found at $SECRET_FILE - preserving"
elif [[ -n "${SECRET_KEY:-}" ]]; then
printf '%s' "$SECRET_KEY" > "$SECRET_FILE"
chmod 600 "$SECRET_FILE"
echo " [OK] Wrote SECRET_KEY from environment to $SECRET_FILE"
else
if command -v openssl &>/dev/null; then
printf '%s' "$(openssl rand -base64 32)" > "$SECRET_FILE"
elif [[ -r /dev/urandom ]]; then
printf '%s' "$(head -c 32 /dev/urandom | base64)" > "$SECRET_FILE"
else
echo " [ERROR] Neither openssl nor /dev/urandom available; cannot generate secret"
exit 1
fi
chmod 600 "$SECRET_FILE"
echo " [OK] Generated secret key at $SECRET_FILE"
fi
unset SECRET_KEY
if [[ -n "$API_URL" ]]; then
EFFECTIVE_API_URL="$API_URL"
else
case "$BIND_HOST" in
0.0.0.0|::|"")
DETECTED_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
[[ -z "$DETECTED_IP" ]] && DETECTED_IP="127.0.0.1"
EFFECTIVE_API_URL="http://$DETECTED_IP:$API_PORT"
echo " [INFO] Bind host is $BIND_HOST; deriving API_BASE_URL=$EFFECTIVE_API_URL"
echo " Pass --api-url to set a public URL for presigned access."
;;
*)
EFFECTIVE_API_URL="http://$BIND_HOST:$API_PORT"
;;
esac
fi
cat > "$INSTALL_DIR/myfsio.env" << EOF
# MyFSIO Configuration
# Generated by install.sh on $(date)
@@ -196,61 +189,63 @@ cat > "$INSTALL_DIR/myfsio.env" << EOF
# STORAGE PATHS
# =============================================================================
STORAGE_ROOT=$DATA_DIR
LOG_DIR=$LOG_DIR
# =============================================================================
# NETWORK
# =============================================================================
APP_HOST=0.0.0.0
APP_PORT=$API_PORT
HOST=$BIND_HOST
PORT=$API_PORT
UI_PORT=$UI_PORT
# Public URL (set this if behind a reverse proxy for presigned URLs)
$(if [[ -n "$API_URL" ]]; then echo "API_BASE_URL=$API_URL"; else echo "# API_BASE_URL=https://s3.example.com"; fi)
# Public URL used to sign presigned URLs (override with --api-url for proxies)
API_BASE_URL=$EFFECTIVE_API_URL
# =============================================================================
# SECURITY
# =============================================================================
# Secret key for session signing (auto-generated if not set)
SECRET_KEY=$SECRET_KEY
# CORS settings - restrict in production
CORS_ORIGINS=*
# CORS_METHODS=GET,PUT,POST,DELETE,OPTIONS,HEAD
# CORS_ALLOW_HEADERS=*
# CORS_EXPOSE_HEADERS=*
# Brute-force protection
AUTH_MAX_ATTEMPTS=5
AUTH_LOCKOUT_MINUTES=15
# Reverse proxy settings (set to number of trusted proxies in front)
# Reverse proxy settings (number of trusted proxies in front)
# NUM_TRUSTED_PROXIES=1
# Allow internal admin endpoints (only enable on trusted networks)
# Allow internal/diagnostic admin endpoints (only on trusted networks)
# ALLOW_INTERNAL_ENDPOINTS=false
# Allowed hosts for redirects (comma-separated, empty = restrict all)
# Comma-separated external hosts allowed for UI login redirects
# ALLOWED_REDIRECT_HOSTS=
# UI session lifetime in days
# SESSION_LIFETIME_DAYS=1
# SigV4 timestamp tolerance (seconds)
# SIGV4_TIMESTAMP_TOLERANCE_SECONDS=900
# =============================================================================
# UI ASSET OVERRIDES (optional - assets are embedded in the binary by default)
# =============================================================================
# Set these only when developing UI changes against an unpacked source tree.
# TEMPLATES_DIR=/path/to/templates
# STATIC_DIR=/path/to/static
# =============================================================================
# LOGGING
# =============================================================================
LOG_LEVEL=INFO
LOG_TO_FILE=true
# RUST_LOG=info,myfsio_server=info
# =============================================================================
# RATE LIMITING
# =============================================================================
RATE_LIMIT_DEFAULT=200 per minute
# RATE_LIMIT_LIST_BUCKETS=60 per minute
# RATE_LIMIT_BUCKET_OPS=120 per minute
# RATE_LIMIT_OBJECT_OPS=240 per minute
# RATE_LIMIT_ADMIN=60 per minute
# =============================================================================
# SERVER TUNING (0 = auto-detect based on system resources)
# =============================================================================
# SERVER_THREADS=0
# SERVER_CONNECTION_LIMIT=0
# SERVER_BACKLOG=0
# SERVER_CHANNEL_TIMEOUT=120
RATE_LIMIT_DEFAULT=500 per minute
# RATE_LIMIT_LIST_BUCKETS=500 per minute
# RATE_LIMIT_BUCKET_OPS=500 per minute
# RATE_LIMIT_OBJECT_OPS=500 per minute
# RATE_LIMIT_HEAD_OPS=500 per minute
RATE_LIMIT_ADMIN=60 per minute
# =============================================================================
# ENCRYPTION (uncomment to enable)
@@ -259,39 +254,50 @@ RATE_LIMIT_DEFAULT=200 per minute
# KMS_ENABLED=true
# =============================================================================
# SITE SYNC / REPLICATION (for multi-site deployments)
# SITE SYNC / REPLICATION (multi-site deployments)
# =============================================================================
# SITE_ID=site-1
# SITE_ENDPOINT=https://s3-site1.example.com
# SITE_REGION=us-east-1
# SITE_PRIORITY=100
# SITE_SYNC_ENABLED=false
# SITE_SYNC_INTERVAL_SECONDS=60
# SITE_SYNC_BATCH_SIZE=100
# =============================================================================
# OPTIONAL FEATURES
# =============================================================================
# WEBSITE_HOSTING_ENABLED=false
# LIFECYCLE_ENABLED=false
# METRICS_HISTORY_ENABLED=false
# OPERATION_METRICS_ENABLED=false
# GC_ENABLED=false
# GC_INTERVAL_HOURS=6
# GC_DRY_RUN=false
# INTEGRITY_ENABLED=false
# =============================================================================
# FIRST-RUN ADMIN OVERRIDE (optional)
# =============================================================================
# ADMIN_ACCESS_KEY=
# ADMIN_SECRET_KEY=
EOF
chmod 600 "$INSTALL_DIR/myfsio.env"
echo " [OK] Created $INSTALL_DIR/myfsio.env"
echo ""
echo "------------------------------------------------------------"
echo "STEP 7: Setting Permissions"
echo "STEP 6: Setting Permissions"
echo "------------------------------------------------------------"
echo ""
chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR"
echo " [OK] Set ownership for $INSTALL_DIR"
chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR"
echo " [OK] Set ownership for $DATA_DIR"
chown -R "$SERVICE_USER:$SERVICE_USER" "$LOG_DIR"
echo " [OK] Set ownership for $LOG_DIR"
chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" && echo " [OK] Set ownership for $INSTALL_DIR"
chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR" && echo " [OK] Set ownership for $DATA_DIR"
chown -R "$SERVICE_USER:$SERVICE_USER" "$LOG_DIR" && echo " [OK] Set ownership for $LOG_DIR"
if [[ "$SKIP_SYSTEMD" != true ]]; then
echo ""
echo "------------------------------------------------------------"
echo "STEP 8: Creating Systemd Service"
echo "STEP 7: Creating Systemd Service"
echo "------------------------------------------------------------"
echo ""
cat > /etc/systemd/system/myfsio.service << EOF
@@ -306,18 +312,16 @@ User=$SERVICE_USER
Group=$SERVICE_USER
WorkingDirectory=$INSTALL_DIR
EnvironmentFile=$INSTALL_DIR/myfsio.env
ExecStart=$INSTALL_DIR/myfsio
ExecStart=$INSTALL_DIR/myfsio serve
Restart=on-failure
RestartSec=5
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=$DATA_DIR $LOG_DIR
PrivateTmp=true
# Resource limits (adjust as needed)
# LimitNOFILE=65535
# MemoryMax=2G
@@ -331,7 +335,7 @@ EOF
else
echo ""
echo "------------------------------------------------------------"
echo "STEP 8: Skipping Systemd Service (--no-systemd flag used)"
echo "STEP 7: Skipping Systemd Service (--no-systemd flag used)"
echo "------------------------------------------------------------"
fi
@@ -343,30 +347,33 @@ echo ""
if [[ "$SKIP_SYSTEMD" != true ]]; then
echo "------------------------------------------------------------"
echo "STEP 9: Start the Service"
echo "STEP 8: Start the Service"
echo "------------------------------------------------------------"
echo ""
if [[ "$AUTO_YES" != true ]]; then
read -p "Would you like to start MyFSIO now? [Y/n] " -n 1 -r
echo
START_SERVICE=true
if [[ $REPLY =~ ^[Nn]$ ]]; then
START_SERVICE=false
fi
[[ $REPLY =~ ^[Nn]$ ]] && START_SERVICE=false
else
START_SERVICE=true
fi
if [[ "$START_SERVICE" == true ]]; then
echo " Starting MyFSIO service..."
systemctl start myfsio
echo " [OK] Service started"
echo ""
read -p "Would you like to enable MyFSIO to start on boot? [Y/n] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
if [[ "$AUTO_YES" != true ]]; then
read -p "Would you like to enable MyFSIO to start on boot? [Y/n] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
systemctl enable myfsio
echo " [OK] Service enabled on boot"
fi
else
systemctl enable myfsio
echo " [OK] Service enabled on boot"
fi
@@ -383,21 +390,18 @@ if [[ "$SKIP_SYSTEMD" != true ]]; then
echo " ============================================"
echo " ADMIN CREDENTIALS (save these securely!)"
echo " ============================================"
CRED_OUTPUT=$(journalctl -u myfsio --no-pager -n 50 2>/dev/null | grep -A 5 "FIRST RUN - ADMIN CREDENTIALS")
CRED_OUTPUT=$(journalctl -u myfsio --no-pager -n 100 2>/dev/null | grep -A 5 "FIRST RUN - ADMIN CREDENTIALS")
ACCESS_KEY=$(echo "$CRED_OUTPUT" | grep "Access Key:" | head -1 | sed 's/.*Access Key: //' | awk '{print $1}')
SECRET_KEY=$(echo "$CRED_OUTPUT" | grep "Secret Key:" | head -1 | sed 's/.*Secret Key: //' | awk '{print $1}')
if [[ -n "$ACCESS_KEY" && "$ACCESS_KEY" != *"from"* && -n "$SECRET_KEY" && "$SECRET_KEY" != *"from"* ]]; then
if [[ -n "$ACCESS_KEY" && -n "$SECRET_KEY" ]]; then
echo " Access Key: $ACCESS_KEY"
echo " Secret Key: $SECRET_KEY"
else
echo " [!] Could not extract credentials from service logs."
echo " Check startup output: journalctl -u myfsio --no-pager | grep -A 5 'ADMIN CREDENTIALS'"
echo " Or reset credentials: $INSTALL_DIR/myfsio reset-cred"
echo " Check: journalctl -u myfsio --no-pager | grep -A 5 'ADMIN CREDENTIALS'"
echo " Or reset: $INSTALL_DIR/myfsio --reset-cred"
fi
echo " ============================================"
echo ""
echo " NOTE: The IAM config file is encrypted at rest."
echo " Credentials are only shown on first run or after reset."
else
echo " [WARNING] MyFSIO may not have started correctly"
echo " Check logs with: journalctl -u myfsio -f"
@@ -405,27 +409,21 @@ if [[ "$SKIP_SYSTEMD" != true ]]; then
else
echo " [SKIPPED] Service not started"
echo ""
echo " To start manually, run:"
echo " sudo systemctl start myfsio"
echo ""
echo " To enable on boot, run:"
echo " sudo systemctl enable myfsio"
echo " Start manually: sudo systemctl start myfsio"
echo " Enable on boot: sudo systemctl enable myfsio"
fi
fi
HOST_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost")
echo ""
echo "============================================================"
echo " Summary"
echo "============================================================"
echo ""
echo "Access Points:"
echo " API: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost"):$API_PORT"
echo " UI: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost"):$UI_PORT/ui"
echo ""
echo "Credentials:"
echo " Admin credentials are shown on first service start (see above)."
echo " The IAM config is encrypted at rest and cannot be read directly."
echo " To reset credentials: $INSTALL_DIR/myfsio reset-cred"
echo " S3 API: http://$HOST_IP:$API_PORT"
echo " Web UI: http://$HOST_IP:$UI_PORT/ui"
echo ""
echo "Configuration Files:"
echo " Environment: $INSTALL_DIR/myfsio.env"
@@ -433,18 +431,14 @@ echo " IAM Users: $DATA_DIR/.myfsio.sys/config/iam.json (encrypted)"
echo " Bucket Policies: $DATA_DIR/.myfsio.sys/config/bucket_policies.json"
echo " Secret Key: $DATA_DIR/.myfsio.sys/config/.secret (auto-generated)"
echo ""
echo "Security Notes:"
echo " - Rate limiting is enabled by default (200 req/min)"
echo " - Brute-force protection: 5 attempts, 15 min lockout"
echo " - Set CORS_ORIGINS to specific domains in production"
echo " - Set NUM_TRUSTED_PROXIES if behind a reverse proxy"
echo ""
echo "Useful Commands:"
echo " Check status: sudo systemctl status myfsio"
echo " View logs: sudo journalctl -u myfsio -f"
echo " Validate config: $INSTALL_DIR/myfsio --check-config"
echo " Restart: sudo systemctl restart myfsio"
echo " Stop: sudo systemctl stop myfsio"
echo " Check status: sudo systemctl status myfsio"
echo " View logs: sudo journalctl -u myfsio -f"
echo " Validate config: $INSTALL_DIR/myfsio --check-config"
echo " Show config: $INSTALL_DIR/myfsio --show-config"
echo " Reset admin: sudo -u $SERVICE_USER $INSTALL_DIR/myfsio --reset-cred"
echo " Restart: sudo systemctl restart myfsio"
echo " Stop: sudo systemctl stop myfsio"
echo ""
echo "Documentation: https://go.jzwsite.com/myfsio"
echo ""

View File

@@ -13,6 +13,7 @@
# --data-dir DIR Data directory (default: /var/lib/myfsio)
# --log-dir DIR Log directory (default: /var/log/myfsio)
# --user USER System user (default: myfsio)
# --no-systemd Skip systemd service teardown
# -y, --yes Skip confirmation prompts
#
@@ -24,46 +25,21 @@ LOG_DIR="/var/log/myfsio"
SERVICE_USER="myfsio"
KEEP_DATA=false
KEEP_LOGS=false
SKIP_SYSTEMD=false
AUTO_YES=false
while [[ $# -gt 0 ]]; do
case $1 in
--keep-data)
KEEP_DATA=true
shift
;;
--keep-logs)
KEEP_LOGS=true
shift
;;
--install-dir)
INSTALL_DIR="$2"
shift 2
;;
--data-dir)
DATA_DIR="$2"
shift 2
;;
--log-dir)
LOG_DIR="$2"
shift 2
;;
--user)
SERVICE_USER="$2"
shift 2
;;
-y|--yes)
AUTO_YES=true
shift
;;
-h|--help)
head -20 "$0" | tail -15
exit 0
;;
*)
echo "Unknown option: $1"
exit 1
;;
--keep-data) KEEP_DATA=true; shift ;;
--keep-logs) KEEP_LOGS=true; shift ;;
--install-dir) INSTALL_DIR="$2"; shift 2 ;;
--data-dir) DATA_DIR="$2"; shift 2 ;;
--log-dir) LOG_DIR="$2"; shift 2 ;;
--user) SERVICE_USER="$2"; shift 2 ;;
--no-systemd) SKIP_SYSTEMD=true; shift ;;
-y|--yes) AUTO_YES=true; shift ;;
-h|--help) head -21 "$0" | tail -16; exit 0 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
@@ -125,42 +101,51 @@ if [[ "$AUTO_YES" != true ]]; then
fi
fi
echo ""
echo "------------------------------------------------------------"
echo "STEP 2: Stopping Service"
echo "------------------------------------------------------------"
echo ""
if systemctl is-active --quiet myfsio 2>/dev/null; then
systemctl stop myfsio
echo " [OK] Stopped myfsio service"
else
echo " [SKIP] Service not running"
fi
if [[ "$SKIP_SYSTEMD" != true ]]; then
echo ""
echo "------------------------------------------------------------"
echo "STEP 2: Stopping Service"
echo "------------------------------------------------------------"
echo ""
if systemctl is-active --quiet myfsio 2>/dev/null; then
systemctl stop myfsio
echo " [OK] Stopped myfsio service"
else
echo " [SKIP] Service not running"
fi
echo ""
echo "------------------------------------------------------------"
echo "STEP 3: Disabling Service"
echo "------------------------------------------------------------"
echo ""
if systemctl is-enabled --quiet myfsio 2>/dev/null; then
systemctl disable myfsio
echo " [OK] Disabled myfsio service"
else
echo " [SKIP] Service not enabled"
fi
echo ""
echo "------------------------------------------------------------"
echo "STEP 3: Disabling Service"
echo "------------------------------------------------------------"
echo ""
if systemctl is-enabled --quiet myfsio 2>/dev/null; then
systemctl disable myfsio
echo " [OK] Disabled myfsio service"
else
echo " [SKIP] Service not enabled"
fi
echo ""
echo "------------------------------------------------------------"
echo "STEP 4: Removing Systemd Service File"
echo "------------------------------------------------------------"
echo ""
if [[ -f /etc/systemd/system/myfsio.service ]]; then
rm -f /etc/systemd/system/myfsio.service
systemctl daemon-reload
echo " [OK] Removed /etc/systemd/system/myfsio.service"
echo " [OK] Reloaded systemd daemon"
echo ""
echo "------------------------------------------------------------"
echo "STEP 4: Removing Systemd Service File"
echo "------------------------------------------------------------"
echo ""
if [[ -f /etc/systemd/system/myfsio.service ]]; then
rm -f /etc/systemd/system/myfsio.service
systemctl daemon-reload
echo " [OK] Removed /etc/systemd/system/myfsio.service"
echo " [OK] Reloaded systemd daemon"
else
echo " [SKIP] Service file not found"
fi
else
echo " [SKIP] Service file not found"
echo ""
echo "------------------------------------------------------------"
echo "STEPS 2-4: Skipping Systemd Teardown (--no-systemd)"
echo "------------------------------------------------------------"
echo ""
echo " Stop any running myfsio process manually before continuing."
fi
echo ""
@@ -235,11 +220,11 @@ if [[ "$KEEP_DATA" == true ]]; then
echo " - Secret key: $DATA_DIR/.myfsio.sys/config/.secret"
echo " - Encryption keys: $DATA_DIR/.myfsio.sys/keys/ (if encryption was enabled)"
echo ""
echo "NOTE: The IAM config is encrypted and requires the SECRET_KEY to read."
echo " Keep the .secret file intact for reinstallation."
echo "NOTE: The IAM config is encrypted at rest and is unlocked by the .secret"
echo " file in the data directory. Keep that file intact for reinstallation."
echo ""
echo "To reinstall MyFSIO with existing data:"
echo " ./install.sh --data-dir $DATA_DIR"
echo " ./install.sh --binary ./myfsio --data-dir $DATA_DIR"
echo ""
fi