feat(交付): 客户侧两台服务器离线部署包
内网机/外网机各起一套栈,两台之间不开任何端口,文件只经光闸在 gate/out 与对端 gate/in 之间单向摆渡。一份 compose + 一份 config + 一份 nginx 模板,全部差异收在 .env 里。 - docker-compose.yml:5 服务只发布前端端口;app 容器 IP 锁死并由 compose 注入 -Dcode.snowflake.workid.<IP>=<WORK_ID>,绕开兜底 99 超雪花 5 位上限导致启动即崩的问题 - .env.iw / .env.ow:出厂配置与随机口令已填好,仅 MESHSAFE_SECRET 故意留占位(以 MeshSafe 侧白名单为准);按 maintainer 决定原样入库 - install.sh iw|ow:按角色建 .env -> 自检 -> sha256 校验 tar -> 架构 核对 -> docker load 后校验 tag -> 建目录/属主/SELinux 标签 -> 起栈 并逐个等健康。修掉 pipefail 下 `grep | head -1` 静默中断、架构字段 误读 manifest.json(实际在 Config blob)、die() 未定义先被调用 - export-images.sh:就地刷新 images/*.tar、sql、.env 的 tag 与 SHA256SUMS,再打 dist 交付盘;tar 与 SHA256SUMS 由 .gitignore 排除 - sql/01+02:从 backend/db 的 PG 全量与初始数据覆盖拷贝。三个 upgrade-*.sql 的 PG 段(x_upload_config、x_file_security)已在 init-table-postgres-full.sql 内,空库跑 01->02 即完整现状 - smoke_test.py:18 条断言的现场验收,凭证全部走环境变量 - 部署说明.md:拓扑、交付清单、出厂口令表、iw/ow 差异、验收清单、 13 条已知坑 镜像 tar 不进 git,装机包由 export-images.sh 出盘。
This commit is contained in:
89
deploy/offline/.env.example
Normal file
89
deploy/offline/.env.example
Normal file
@@ -0,0 +1,89 @@
|
||||
# =============================================================================
|
||||
# s-file 客户侧离线部署 —— 唯一需要现场修改的文件
|
||||
#
|
||||
# 用法:cp .env.example .env 然后按本机角色填写,两台服务器各填一份
|
||||
# 内网机:SFILE_ROLE=iw SFILE_TRANSFER_LOCATION=1 SFILE_SITE_ROOT=/usr/share/nginx/inside
|
||||
# 外网机:SFILE_ROLE=ow SFILE_TRANSFER_LOCATION=2 SFILE_SITE_ROOT=/usr/share/nginx/outside
|
||||
#
|
||||
# 凡是写着「必须改」的值,不改起不来或者起来即安全隐患
|
||||
# =============================================================================
|
||||
|
||||
# ---------- 本机角色(必须改) ----------
|
||||
# iw=内网机 / ow=外网机,只影响容器名前缀与 MQ 交换机命名,两侧不能填同一个
|
||||
SFILE_ROLE=iw
|
||||
# 传输方向标记:内网机 1,外网机 2。写反了跨网摆渡直接不工作
|
||||
SFILE_TRANSFER_LOCATION=1
|
||||
# 前端站点根:内网机 inside,外网机 outside(同一个 web 镜像内含两套前端包)
|
||||
SFILE_SITE_ROOT=/usr/share/nginx/inside
|
||||
|
||||
# ---------- 镜像(来自交付 tar,现场 docker load,不会联网拉取) ----------
|
||||
APP_IMAGE=sfile-app:1.0.0
|
||||
WEB_IMAGE=sfile-web:1.0.0
|
||||
PG_IMAGE=postgres:18
|
||||
REDIS_IMAGE=redis:8
|
||||
MQ_IMAGE=rabbitmq:3.13-management-alpine
|
||||
|
||||
# postgres 数据目录挂载点:18 及以后是 /var/lib/postgresql,15 及以前是 /var/lib/postgresql/data
|
||||
# 用交付的 PG_IMAGE 就不用动;自行换库版本时按上面规则改这一行
|
||||
PG_DATA_DIR=/var/lib/postgresql
|
||||
|
||||
# ---------- 对外访问 ----------
|
||||
# 只暴露前端一个端口;后端/数据库/Redis/MQ 全部只在容器网络内,不映射宿主机
|
||||
WEB_HOST=0.0.0.0
|
||||
WEB_PORT=80
|
||||
|
||||
# ---------- 口令与密钥(必须改) ----------
|
||||
POSTGRES_DB=s_file
|
||||
POSTGRES_USER=sfile
|
||||
POSTGRES_PASSWORD=CHANGE_ME_pg_password
|
||||
MQ_USER=sfile
|
||||
MQ_PASSWORD=CHANGE_ME_mq_password
|
||||
# JWT 签名密钥,至少 32 字符,两侧不要相同,丢失即所有登录态失效
|
||||
JWT_SECRET=CHANGE_ME_jwt_secret_at_least_32_chars
|
||||
# 新建用户的初始密码
|
||||
USER_DEFAULT_PASSWORD=CHANGE_ME_user_init_pwd
|
||||
|
||||
# ---------- 发号器(workerId)与容器网段 ----------
|
||||
# 后端雪花 ID 的 workerId 按「app 容器 IP」取键;取不到会回退 99,而 99 超过 5 位上限,
|
||||
# 应用会直接启动失败(worker Id can't be greater than 31)。所以这里必须锁 IP。
|
||||
# 下面三个值必须同一网段,一般不用改;只有和客户机器上已有 docker 网段冲突时才整段换掉
|
||||
APP_SUBNET=172.30.88.0/24
|
||||
APP_GATEWAY=172.30.88.1
|
||||
APP_NODE_IP=172.30.88.10
|
||||
# 发号器机器号,0-31。两台独立服务器各跑一套,取同一个值也没问题;
|
||||
# 若同一台机器上并跑内网+外网两套栈,必须给不同值,否则生成的 ID 会撞
|
||||
APP_WORK_ID=1
|
||||
|
||||
# ---------- 主机数据目录 ----------
|
||||
# 应用上传文件、日志、数据库/Redis/MQ 持久化的根目录,需独立数据盘且预留足够空间
|
||||
SFILE_DATA_ROOT=/data/sfile
|
||||
# 光闸摆渡目录:本机的「发出去」目录,由光闸设备单向搬运到对端
|
||||
GATE_SEND_DIR=/data/sfile/gate/out
|
||||
# 光闸摆渡目录:对端经光闸投递进来的目录
|
||||
GATE_RECV_DIR=/data/sfile/gate/in
|
||||
|
||||
# ---------- MeshSafe 文件安全检测 ----------
|
||||
# 没有部署 MeshSafe 时置 false,前端自动隐藏「安全检测」列,跨网摆渡不受影响
|
||||
MESHSAFE_ENABLED=true
|
||||
# 同机部署:http://host.docker.internal:44380(需 Docker >= 20.10)
|
||||
# 独立服务器/集群:http://<MeshSafe地址>:<端口>
|
||||
MESHSAFE_BASE_URL=http://host.docker.internal:44380
|
||||
# 必须与 MeshSafe 侧 MESHSAFE_API_CLIENTS 白名单里的凭证一致(必须改)
|
||||
MESHSAFE_CLIENT_ID=CHANGE_ME_client_id
|
||||
MESHSAFE_SECRET=CHANGE_ME_client_secret
|
||||
# 送检来源标识,两侧区分开便于审计追溯
|
||||
MESHSAFE_CHANNEL=sfile-iw
|
||||
# 检测服务不可用时:true=放行并记录,false=阻断(等保从严建议 false)
|
||||
MESHSAFE_FAIL_OPEN=true
|
||||
MESHSAFE_TIMEOUT_SECONDS=120
|
||||
MESHSAFE_POLL_TIMEOUT_MINUTES=60
|
||||
# 拉取检测结论的轮询周期,默认每 10 秒
|
||||
MESHSAFE_POLL_CRON=0/10 * * * * ?
|
||||
|
||||
# ---------- 资源与运行时 ----------
|
||||
TZ=Asia/Shanghai
|
||||
# 按机器内存调整,堆外还需预留 1G 左右
|
||||
APP_JAVA_OPTS=-Xms1g -Xmx3g -Dfile.encoding=UTF-8
|
||||
REDIS_MAXMEMORY=512mb
|
||||
# 跨网文件监听任务周期,默认每分钟扫一次摆渡目录
|
||||
FILE_TRANSFER_MONITOR_CRON=0 */1 * * * ?
|
||||
62
deploy/offline/.env.iw
Normal file
62
deploy/offline/.env.iw
Normal file
@@ -0,0 +1,62 @@
|
||||
# =============================================================================
|
||||
# s-file 交付配置 —— 内网机(拷到内网服务器后 mv 成 .env)
|
||||
#
|
||||
# 装机后必须立刻改的两处:admin 登录口令、新用户初始密码 USER_DEFAULT_PASSWORD
|
||||
# 下面的库/MQ 口令是出厂默认值,客户内网可用,但交付单要写清并建议现场轮换
|
||||
# =============================================================================
|
||||
|
||||
# ---------- 本机角色 ----------
|
||||
SFILE_ROLE=iw
|
||||
SFILE_TRANSFER_LOCATION=1
|
||||
SFILE_SITE_ROOT=/usr/share/nginx/inside
|
||||
|
||||
# ---------- 镜像(images/ 里的 tar docker load 后就是这些 tag) ----------
|
||||
APP_IMAGE=sfile-app:1.0.0
|
||||
WEB_IMAGE=sfile-web:1.0.0
|
||||
PG_IMAGE=postgres:18
|
||||
REDIS_IMAGE=redis:8
|
||||
MQ_IMAGE=rabbitmq:3.13-management-alpine
|
||||
PG_DATA_DIR=/var/lib/postgresql
|
||||
|
||||
# ---------- 对外访问:只暴露前端 ----------
|
||||
WEB_HOST=0.0.0.0
|
||||
WEB_PORT=80
|
||||
|
||||
# ---------- 口令与密钥 ----------
|
||||
POSTGRES_DB=s_file
|
||||
POSTGRES_USER=sfile
|
||||
POSTGRES_PASSWORD=Sfile-iw-Pg-N1tOz1MxgdaS
|
||||
MQ_USER=sfile
|
||||
MQ_PASSWORD=Sfile-iw-Mq-UTqVSnCDZ0LB
|
||||
JWT_SECRET=86a6b015f3292eb3fdab298d3eced941da4276d47e24e924b82ab8adee397d70
|
||||
USER_DEFAULT_PASSWORD=Sfile-iw-Usr-FBLS-0fA
|
||||
|
||||
# ---------- 发号器与容器网段(与外网机各用一段,同机并跑时才需要错开) ----------
|
||||
APP_SUBNET=172.30.88.0/24
|
||||
APP_GATEWAY=172.30.88.1
|
||||
APP_NODE_IP=172.30.88.10
|
||||
APP_WORK_ID=1
|
||||
|
||||
# ---------- 主机数据目录 ----------
|
||||
SFILE_DATA_ROOT=/data/sfile
|
||||
GATE_SEND_DIR=/data/sfile/gate/out
|
||||
GATE_RECV_DIR=/data/sfile/gate/in
|
||||
|
||||
# ---------- MeshSafe 文件安全检测 ----------
|
||||
# 凭证必须与 MeshSafe 侧 MESHSAFE_API_CLIENTS 白名单一致,现场按 MeshSafe 交付单核对
|
||||
MESHSAFE_ENABLED=true
|
||||
MESHSAFE_BASE_URL=http://host.docker.internal:44380
|
||||
MESHSAFE_CLIENT_ID=sfile-iw
|
||||
MESHSAFE_SECRET=CHANGE_ME_meshsafe_secret
|
||||
MESHSAFE_CHANNEL=sfile-iw
|
||||
# 等保从严建议 false:检测服务不可用时阻断而不是放行
|
||||
MESHSAFE_FAIL_OPEN=true
|
||||
MESHSAFE_TIMEOUT_SECONDS=120
|
||||
MESHSAFE_POLL_TIMEOUT_MINUTES=60
|
||||
MESHSAFE_POLL_CRON=0/10 * * * * ?
|
||||
|
||||
# ---------- 资源与运行时 ----------
|
||||
TZ=Asia/Shanghai
|
||||
APP_JAVA_OPTS=-Xms1g -Xmx3g -Dfile.encoding=UTF-8
|
||||
REDIS_MAXMEMORY=512mb
|
||||
FILE_TRANSFER_MONITOR_CRON=0 */1 * * * ?
|
||||
61
deploy/offline/.env.ow
Normal file
61
deploy/offline/.env.ow
Normal file
@@ -0,0 +1,61 @@
|
||||
# =============================================================================
|
||||
# s-file 交付配置 —— 外网机(拷到外网服务器后 mv 成 .env)
|
||||
#
|
||||
# 与内网机只差:角色/传输方向/站点根/网段/机器号/目录名/口令/JWT
|
||||
# 装机后必须立刻改的两处:admin 登录口令、新用户初始密码 USER_DEFAULT_PASSWORD
|
||||
# =============================================================================
|
||||
|
||||
# ---------- 本机角色 ----------
|
||||
SFILE_ROLE=ow
|
||||
SFILE_TRANSFER_LOCATION=2
|
||||
SFILE_SITE_ROOT=/usr/share/nginx/outside
|
||||
|
||||
# ---------- 镜像(images/ 里的 tar docker load 后就是这些 tag) ----------
|
||||
APP_IMAGE=sfile-app:1.0.0
|
||||
WEB_IMAGE=sfile-web:1.0.0
|
||||
PG_IMAGE=postgres:18
|
||||
REDIS_IMAGE=redis:8
|
||||
MQ_IMAGE=rabbitmq:3.13-management-alpine
|
||||
PG_DATA_DIR=/var/lib/postgresql
|
||||
|
||||
# ---------- 对外访问:只暴露前端 ----------
|
||||
WEB_HOST=0.0.0.0
|
||||
WEB_PORT=80
|
||||
|
||||
# ---------- 口令与密钥(与内网机不同) ----------
|
||||
POSTGRES_DB=s_file
|
||||
POSTGRES_USER=sfile
|
||||
POSTGRES_PASSWORD=Sfile-ow-Pg-pT-673--i1rk
|
||||
MQ_USER=sfile
|
||||
MQ_PASSWORD=Sfile-ow-Mq-sfW5Cv-Nk-cA
|
||||
JWT_SECRET=9eeeea031052a156bb278607f89dac3e45282f8a934fa0ba01c24facacde6988
|
||||
USER_DEFAULT_PASSWORD=Sfile-ow-Usr-WqDJ7PDS
|
||||
|
||||
# ---------- 发号器与容器网段 ----------
|
||||
APP_SUBNET=172.30.98.0/24
|
||||
APP_GATEWAY=172.30.98.1
|
||||
APP_NODE_IP=172.30.98.10
|
||||
APP_WORK_ID=2
|
||||
|
||||
# ---------- 主机数据目录 ----------
|
||||
SFILE_DATA_ROOT=/data/sfile
|
||||
GATE_SEND_DIR=/data/sfile/gate/out
|
||||
GATE_RECV_DIR=/data/sfile/gate/in
|
||||
|
||||
# ---------- MeshSafe 文件安全检测 ----------
|
||||
# 外网机一般也走一次检测(接收解包后送检);该侧不部署 MeshSafe 时置 MESHSAFE_ENABLED=false
|
||||
MESHSAFE_ENABLED=true
|
||||
MESHSAFE_BASE_URL=http://host.docker.internal:44380
|
||||
MESHSAFE_CLIENT_ID=sfile-ow
|
||||
MESHSAFE_SECRET=CHANGE_ME_meshsafe_secret
|
||||
MESHSAFE_CHANNEL=sfile-ow
|
||||
MESHSAFE_FAIL_OPEN=true
|
||||
MESHSAFE_TIMEOUT_SECONDS=120
|
||||
MESHSAFE_POLL_TIMEOUT_MINUTES=60
|
||||
MESHSAFE_POLL_CRON=0/10 * * * * ?
|
||||
|
||||
# ---------- 资源与运行时 ----------
|
||||
TZ=Asia/Shanghai
|
||||
APP_JAVA_OPTS=-Xms1g -Xmx3g -Dfile.encoding=UTF-8
|
||||
REDIS_MAXMEMORY=512mb
|
||||
FILE_TRANSFER_MONITOR_CRON=0 */1 * * * ?
|
||||
7
deploy/offline/.gitignore
vendored
Normal file
7
deploy/offline/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# 交付包里的镜像 tar 是出盘产物(约 650MB),不进 git
|
||||
images/*.tar
|
||||
images/SHA256SUMS.txt
|
||||
dist/
|
||||
|
||||
# 现场改出来的配置不进 git(.env.iw / .env.ow 是出厂默认,随包交付)
|
||||
.env
|
||||
22
deploy/offline/VERSION.txt
Normal file
22
deploy/offline/VERSION.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
s-file 离线交付包 1.0.0
|
||||
构建平台: linux/amd64
|
||||
导出时间: 2026-11-02 13:58:00
|
||||
|
||||
镜像清单(sha256 见 images/SHA256SUMS.txt):
|
||||
postgres.tar 161M "RepoTags":["postgres:18"]
|
||||
rabbitmq.tar 84M "RepoTags":["rabbitmq:3.13-management-alpine"]
|
||||
redis.tar 53M "RepoTags":["redis:8"]
|
||||
sfile-app.tar 313M "RepoTags":["sfile-app:1.0.0"]
|
||||
sfile-web.tar 39M "RepoTags":["sfile-web:1.0.0"]
|
||||
|
||||
SQL: sql/01-init-table.sql (93974 B) sql/02-init-data.sql (2791 B)
|
||||
|
||||
本包所含修复: 弹窗内容被长文件名撑破右边框(DialogContent grid 隐式轨道 min-content 撑开),
|
||||
传输/分享确认框的被拦截文件清单由截断改为换行完整显示。
|
||||
|
||||
现场步骤(两台服务器各执行一次):
|
||||
内网机 tar -xzf sfile-offline-1.0.0-linux-amd64.tar.gz && cd sfile-offline-1.0.0-linux-amd64
|
||||
vi .env.iw # 至少改 MESHSAFE_SECRET,按需改口令/网段/目录
|
||||
./scripts/install.sh iw
|
||||
外网机 同上,改成 ./scripts/install.sh ow
|
||||
验证 python3 scripts/smoke_test.py
|
||||
97
deploy/offline/config/application.yml
Normal file
97
deploy/offline/config/application.yml
Normal file
@@ -0,0 +1,97 @@
|
||||
# =============================================================================
|
||||
# s-file 客户侧静态默认配置(两台机器共用同一份,现场一般不用改)
|
||||
#
|
||||
# 随角色/环境变化的取值一律由 docker-compose 的环境变量注入,本文件不放:
|
||||
# 数据库/Redis/MQ 地址与口令、JWT 密钥、用户初始密码、传输方向与摆渡目录、
|
||||
# MeshSafe 开关与凭证、MQ 交换机命名 —— 全部在 .env 里填
|
||||
# 这样交付物里不存在任何写死的口令或密钥
|
||||
# =============================================================================
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: s-file
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
druid:
|
||||
initial-size: 5
|
||||
min-idle: 5
|
||||
max-active: 50
|
||||
validation-query: SELECT 1
|
||||
test-while-idle: true
|
||||
# 摆渡解包/送检会在多任务里并发取连接,压太低会出现等待
|
||||
max-wait: 10000
|
||||
redis:
|
||||
timeout: 5000
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 16
|
||||
max-idle: 8
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 500MB
|
||||
max-request-size: 500MB
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:com/senmesh/**/mapper/*.xml
|
||||
|
||||
# 平台编码,需与 sql/02-init-data.sql 里 uc_platform.platform_code 一致
|
||||
system:
|
||||
code: 1
|
||||
|
||||
security:
|
||||
jwt:
|
||||
enable: true
|
||||
# secret 由环境变量 SECURITY_JWT_SECRET 注入,勿在此填写
|
||||
expiration: 86400000
|
||||
header: Authorization
|
||||
ignoring: /error,/favicon.ico
|
||||
login-page: /login
|
||||
superior-only: false
|
||||
# 单机自闭环部署:权限走本地库,不接外部用户中心
|
||||
use-local: true
|
||||
allow-if-all-abstain-decisions: true
|
||||
oauth2:
|
||||
enable: false
|
||||
not-logs: ""
|
||||
anonymous: ""
|
||||
|
||||
# 新用户初始密码由环境变量 USER_DEFAULT_PASSWORD 注入
|
||||
user:
|
||||
default:
|
||||
password: ${USER_DEFAULT_PASSWORD:}
|
||||
|
||||
file:
|
||||
upload-dir: /data/upload
|
||||
storage:
|
||||
type: local
|
||||
transfer:
|
||||
# location / send-dir / receive-dir / monitor.cron 均由环境变量注入
|
||||
# 解包时是否再做一次内容风险识别(依赖下面的 dify,未部署 dify 时保持 false)
|
||||
unpack-risk-check: false
|
||||
|
||||
# 内容风险识别(Dify)。未部署 Dify 时留空并保持 file.transfer.unpack-risk-check=false
|
||||
dify:
|
||||
risk:
|
||||
rule:
|
||||
api-key: ""
|
||||
scan:
|
||||
api-key: ""
|
||||
|
||||
# MeshSafe 全段由环境变量注入(MESHSAFE_*),未部署时 MESHSAFE_ENABLED=false 即可,
|
||||
# 前端会自动隐藏「安全检测」列,跨网摆渡不受影响
|
||||
meshsafe:
|
||||
review-sync-to-meshsafe: true
|
||||
|
||||
# 接口文档在生产关闭
|
||||
swagger:
|
||||
enable: false
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.senmesh: info
|
||||
189
deploy/offline/docker-compose.yml
Normal file
189
deploy/offline/docker-compose.yml
Normal file
@@ -0,0 +1,189 @@
|
||||
# =============================================================================
|
||||
# s-file 信创文件安全传输系统 —— 客户侧单台服务器部署(内网机 / 外网机通用同一份)
|
||||
#
|
||||
# 一台服务器一套本栈;两台机器靠光闸摆渡目录互通,彼此之间不开任何网络端口。
|
||||
# 所有随角色变化的取值都在同目录 .env 里,本文件现场不用改。
|
||||
#
|
||||
# ./scripts/install.sh iw # 内网机(第一次会把 .env.iw 拷成 .env)
|
||||
# ./scripts/install.sh ow # 外网机
|
||||
#
|
||||
# 手动起也行:cp .env.iw .env && vi .env -> docker load -i images/*.tar -> docker compose up -d
|
||||
#
|
||||
# 只发布前端端口;后端 / PostgreSQL / Redis / RabbitMQ 仅在容器网络内可达。
|
||||
# =============================================================================
|
||||
|
||||
name: sfile-${SFILE_ROLE:?缺少 SFILE_ROLE(内网机 iw / 外网机 ow)}
|
||||
|
||||
services:
|
||||
web:
|
||||
image: ${WEB_IMAGE:?缺少 WEB_IMAGE}
|
||||
container_name: sfile-${SFILE_ROLE}-web
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${WEB_HOST:-0.0.0.0}:${WEB_PORT:-80}:80"
|
||||
environment:
|
||||
TZ: ${TZ:-Asia/Shanghai}
|
||||
# nginx 官方镜像的 /etc/nginx/templates/*.template 会按 NGINX_ENVSUBST_FILTER
|
||||
# 过滤后 envsubst 生成 conf;限定只替换 SFILE_ 开头的变量,避免动到 $uri/$http_host
|
||||
NGINX_ENVSUBST_FILTER: ^SFILE_
|
||||
SFILE_SITE_ROOT: ${SFILE_SITE_ROOT:?缺少 SFILE_SITE_ROOT}
|
||||
SFILE_APP_UPSTREAM: app
|
||||
volumes:
|
||||
- ./nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf -o /dev/null http://127.0.0.1:80/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
networks: [sfile]
|
||||
|
||||
app:
|
||||
image: ${APP_IMAGE:?缺少 APP_IMAGE}
|
||||
container_name: sfile-${SFILE_ROLE}-app
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 30s
|
||||
environment:
|
||||
TZ: ${TZ:-Asia/Shanghai}
|
||||
# 发号器 workerId 按「容器 IP」取键,取不到会回退 99 而 99 > 31 直接启动失败,
|
||||
# 因此这里锁死 app 的容器 IP,并把同一个键写进 JVM 参数,两处永远一致
|
||||
JAVA_OPTS: "${APP_JAVA_OPTS:--Xms1g -Xmx3g -Dfile.encoding=UTF-8} -Dcode.snowflake.workid.${APP_NODE_IP:?缺少 APP_NODE_IP}=${APP_WORK_ID:?缺少 APP_WORK_ID}"
|
||||
SPRING_CONFIG_ADDITIONAL_LOCATION: file:/config/application.yml
|
||||
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://pg:5432/${POSTGRES_DB:?缺少 POSTGRES_DB}
|
||||
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:?缺少 POSTGRES_USER}
|
||||
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:?缺少 POSTGRES_PASSWORD}
|
||||
|
||||
SPRING_REDIS_HOST: redis
|
||||
SPRING_REDIS_PORT: 6379
|
||||
|
||||
SPRING_RABBITMQ_HOST: mq
|
||||
SPRING_RABBITMQ_PORT: 5672
|
||||
SPRING_RABBITMQ_USERNAME: ${MQ_USER:?缺少 MQ_USER}
|
||||
SPRING_RABBITMQ_PASSWORD: ${MQ_PASSWORD:?缺少 MQ_PASSWORD}
|
||||
# 两侧交换机/广播 ID 必须不同,防止有人把两套栈并到一台机器时串消息
|
||||
RABBITMQ_CUSTOM_TOPIC_EXCHANGE_NAME: s-file-${SFILE_ROLE}-topic
|
||||
RABBITMQ_CUSTOM_DELAYED_EXCHANGE_NAME: s-file-${SFILE_ROLE}-delayed
|
||||
RABBITMQ_CUSTOM_BROADCAST_ID: s-file-${SFILE_ROLE}-broadcast
|
||||
|
||||
SECURITY_JWT_SECRET: ${JWT_SECRET:?缺少 JWT_SECRET}
|
||||
USER_DEFAULT_PASSWORD: ${USER_DEFAULT_PASSWORD:?缺少 USER_DEFAULT_PASSWORD}
|
||||
|
||||
# 内网机 1 / 外网机 2,写反了摆渡方向就反了
|
||||
FILE_TRANSFER_LOCATION: "${SFILE_TRANSFER_LOCATION:?缺少 SFILE_TRANSFER_LOCATION}"
|
||||
FILE_TRANSFER_SEND_DIR: /data/transfer/send
|
||||
FILE_TRANSFER_RECEIVE_DIR: /data/transfer/receive
|
||||
FILE_TRANSFER_MONITOR_CRON: "${FILE_TRANSFER_MONITOR_CRON:-0 */1 * * * ?}"
|
||||
|
||||
MESHSAFE_ENABLED: ${MESHSAFE_ENABLED:-true}
|
||||
MESHSAFE_BASE_URL: ${MESHSAFE_BASE_URL:-}
|
||||
MESHSAFE_CLIENT_ID: ${MESHSAFE_CLIENT_ID:-}
|
||||
MESHSAFE_SECRET: ${MESHSAFE_SECRET:-}
|
||||
MESHSAFE_CHANNEL: ${MESHSAFE_CHANNEL:-sfile-${SFILE_ROLE}}
|
||||
MESHSAFE_FAIL_OPEN: ${MESHSAFE_FAIL_OPEN:-true}
|
||||
MESHSAFE_TIMEOUT_SECONDS: ${MESHSAFE_TIMEOUT_SECONDS:-120}
|
||||
MESHSAFE_POLL_TIMEOUT_MINUTES: ${MESHSAFE_POLL_TIMEOUT_MINUTES:-60}
|
||||
MESHSAFE_POLL_CRON: ${MESHSAFE_POLL_CRON:-0/10 * * * * ?}
|
||||
volumes:
|
||||
- ./config/application.yml:/config/application.yml:ro
|
||||
- ${SFILE_DATA_ROOT:?缺少 SFILE_DATA_ROOT}/upload:/data/upload
|
||||
- ${SFILE_DATA_ROOT}/logs:/app/logs
|
||||
# 摆渡目录:容器内固定路径,宿主机侧由光闸设备负责单向搬运
|
||||
- ${GATE_SEND_DIR:?缺少 GATE_SEND_DIR}:/data/transfer/send
|
||||
- ${GATE_RECV_DIR:?缺少 GATE_RECV_DIR}:/data/transfer/receive
|
||||
# MeshSafe 与应用同机部署时靠它从容器内访问宿主机端口;
|
||||
# Docker < 20.10 不支持 host-gateway,或 MeshSafe 在独立服务器时,删掉这两行即可
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
depends_on:
|
||||
pg:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
mq:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -s --max-time 5 -o /dev/null http://127.0.0.1:8080/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 8s
|
||||
retries: 5
|
||||
# 首次建库要跑全量 DDL,冷启动给足时间
|
||||
start_period: 180s
|
||||
networks:
|
||||
sfile:
|
||||
ipv4_address: ${APP_NODE_IP}
|
||||
|
||||
pg:
|
||||
image: ${PG_IMAGE:?缺少 PG_IMAGE}
|
||||
container_name: sfile-${SFILE_ROLE}-pg
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 60s
|
||||
environment:
|
||||
TZ: ${TZ:-Asia/Shanghai}
|
||||
POSTGRES_DB: ${POSTGRES_DB:?缺少 POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER:?缺少 POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?缺少 POSTGRES_PASSWORD}
|
||||
# 空库首次启动才执行 initdb.d,已有数据卷时会被忽略
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"
|
||||
volumes:
|
||||
# postgres 18+ 数据目录挂载点是 /var/lib/postgresql,15- 是 .../data,见 .env 的 PG_DATA_DIR
|
||||
- ${SFILE_DATA_ROOT}/pgdata:${PG_DATA_DIR:-/var/lib/postgresql}
|
||||
- ./sql/01-init-table.sql:/docker-entrypoint-initdb.d/01-init-table.sql:ro
|
||||
- ./sql/02-init-data.sql:/docker-entrypoint-initdb.d/02-init-data.sql:ro
|
||||
healthcheck:
|
||||
# 必须强制走 TCP(-h 127.0.0.1):首次 initdb 期间镜像会临时起一个只听 unix socket
|
||||
# 的实例,此时用默认 socket 探测也会返回成功,导致 app 抢跑连库失败
|
||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 60s
|
||||
networks: [sfile]
|
||||
|
||||
redis:
|
||||
image: ${REDIS_IMAGE:?缺少 REDIS_IMAGE}
|
||||
container_name: sfile-${SFILE_ROLE}-redis
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--appendonly", "yes", "--maxmemory", "${REDIS_MAXMEMORY:-512mb}", "--maxmemory-policy", "allkeys-lru"]
|
||||
environment:
|
||||
TZ: ${TZ:-Asia/Shanghai}
|
||||
volumes:
|
||||
- ${SFILE_DATA_ROOT}/redis:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
networks: [sfile]
|
||||
|
||||
mq:
|
||||
image: ${MQ_IMAGE:?缺少 MQ_IMAGE}
|
||||
container_name: sfile-${SFILE_ROLE}-mq
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 30s
|
||||
environment:
|
||||
TZ: ${TZ:-Asia/Shanghai}
|
||||
RABBITMQ_DEFAULT_USER: ${MQ_USER:?缺少 MQ_USER}
|
||||
RABBITMQ_DEFAULT_PASS: ${MQ_PASSWORD:?缺少 MQ_PASSWORD}
|
||||
volumes:
|
||||
- ${SFILE_DATA_ROOT}/mq:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "rabbitmq-diagnostics -q ping"]
|
||||
interval: 20s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
start_period: 60s
|
||||
networks: [sfile]
|
||||
|
||||
networks:
|
||||
sfile:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
# 锁 app 容器 IP 是为了发号器 workerId 取键;subnet/网关/app IP 三个值必须同网段。
|
||||
# 若与客户机器上已有 docker 网段冲突,整段换掉(如 172.31.88.0/24)即可
|
||||
- subnet: ${APP_SUBNET:?缺少 APP_SUBNET}
|
||||
gateway: ${APP_GATEWAY:?缺少 APP_GATEWAY}
|
||||
39
deploy/offline/nginx/default.conf.template
Normal file
39
deploy/offline/nginx/default.conf.template
Normal file
@@ -0,0 +1,39 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
charset utf-8;
|
||||
|
||||
# 内网机 = /usr/share/nginx/inside,外网机 = /usr/share/nginx/outside(见 .env)
|
||||
root ${SFILE_SITE_ROOT};
|
||||
index index.html;
|
||||
|
||||
# 与后端 multipart 上限(500MB)保持一致,改这里要同步改 application.yml
|
||||
client_max_body_size 500m;
|
||||
|
||||
# /api/xxx -> 后端 /xxx(去掉 /api 前缀,与前端构建时的 VITE_API_URL=/api 对应)
|
||||
location /api/ {
|
||||
proxy_pass http://${SFILE_APP_UPSTREAM}:8080/;
|
||||
proxy_http_version 1.1;
|
||||
# 必须保留端口($http_host):后端 getRootDomain 对「无端口的 IP/localhost」会抛
|
||||
# IllegalStateException,导致登录成功回调 500。改成 $host 就会复现这个坑
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
# 大文件上传不在 nginx 侧缓冲,直接流给后端
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
# SPA:hash 路由,静态资源找不到时回落 index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(js|mjs|css|woff2?|ttf|png|jpe?g|gif|svg|ico)$ {
|
||||
expires 7d;
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
110
deploy/offline/scripts/export-images.sh
Normal file
110
deploy/offline/scripts/export-images.sh
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 出交付盘:在我方构建机上执行,把 deploy/offline/ 刷新成一个可直接带走的完整交付包
|
||||
#
|
||||
# ./scripts/export-images.sh 1.0.0 # 默认 linux/amd64
|
||||
# TARGET_PLATFORM=linux/arm64 ./scripts/export-images.sh 1.0.0 # 鲲鹏/飞腾
|
||||
#
|
||||
# 刷新后的 deploy/offline/(现场照它装,不再依赖本仓库):
|
||||
# ├── images/*.tar 五个镜像 + SHA256SUMS.txt
|
||||
# ├── sql/01-init-table.sql 建表 02-init-data.sql 初始数据
|
||||
# ├── .env.iw / .env.ow 内网机 / 外网机出厂配置
|
||||
# └── docker-compose.yml config/ nginx/ scripts/ VERSION.txt
|
||||
# 另外产出 deploy/offline/dist/sfile-offline-<版本>-<平台>.tar.gz(拷盘/刻录用)
|
||||
#
|
||||
# 前置:backend/target/s-file.jar 与 frontend/ 已构建好(见仓库根 README)
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
VER="${1:?用法:export-images.sh <版本号>,如 1.0.0}"
|
||||
PLATFORM="${TARGET_PLATFORM:-linux/amd64}"
|
||||
PLATFORM_TAG="$(echo "$PLATFORM" | tr -d '/:')"
|
||||
|
||||
HERE="$(cd "$(dirname "$0")/.." && pwd)" # deploy/offline
|
||||
ROOT="$(cd "$HERE/../.." && pwd)" # 仓库根
|
||||
OUT="$HERE/dist/sfile-offline-$VER-$PLATFORM_TAG"
|
||||
|
||||
APP_IMAGE="sfile-app:$VER"
|
||||
WEB_IMAGE="sfile-web:$VER"
|
||||
PG_IMAGE="postgres:18" # 验证过的是 18.3,同大版本即可
|
||||
REDIS_IMAGE="redis:8" # 验证过的是 8.6.3
|
||||
MQ_IMAGE="rabbitmq:3.13-management-alpine" # 验证过的是 3.13.7
|
||||
|
||||
say() { printf '\033[1;36m==> %s\033[0m\n' "$1"; }
|
||||
|
||||
say "构建目标平台:$PLATFORM"
|
||||
[ "$PLATFORM" = "linux/amd64" ] || say "非 amd64 需要 buildx/QEMU 模拟器,慢属正常"
|
||||
|
||||
# ---------- 1. 构建业务镜像 ----------
|
||||
say "构建 $APP_IMAGE"
|
||||
docker build --platform "$PLATFORM" -f "$ROOT/deploy/docker/Dockerfile.app" -t "$APP_IMAGE" "$ROOT/backend/target"
|
||||
|
||||
say "构建 $WEB_IMAGE(内含内网+外网两套前端,耗时较长)"
|
||||
docker build --platform "$PLATFORM" -f "$ROOT/deploy/docker/Dockerfile.web" -t "$WEB_IMAGE" "$ROOT/frontend"
|
||||
|
||||
# ---------- 2. 基础镜像就位(本机没有才拉,避免交付时缺包) ----------
|
||||
for img in "$PG_IMAGE" "$REDIS_IMAGE" "$MQ_IMAGE"; do
|
||||
docker image inspect "$img" >/dev/null 2>&1 || {
|
||||
say "本机没有 $img,拉取 $PLATFORM 版本"
|
||||
docker pull --platform "$PLATFORM" "$img"
|
||||
}
|
||||
done
|
||||
|
||||
# ---------- 3. 镜像 tar 就地导出 ----------
|
||||
say "docker save 导出镜像到 $HERE/images/(约 700M,耐心等)"
|
||||
mkdir -p "$HERE/images"
|
||||
rm -f "$HERE"/images/*.tar "$HERE/images/SHA256SUMS.txt"
|
||||
docker save -o "$HERE/images/sfile-app.tar" "$APP_IMAGE"
|
||||
docker save -o "$HERE/images/sfile-web.tar" "$WEB_IMAGE"
|
||||
docker save -o "$HERE/images/postgres.tar" "$PG_IMAGE"
|
||||
docker save -o "$HERE/images/redis.tar" "$REDIS_IMAGE"
|
||||
docker save -o "$HERE/images/rabbitmq.tar" "$MQ_IMAGE"
|
||||
( cd "$HERE/images" && sha256sum *.tar > SHA256SUMS.txt )
|
||||
|
||||
# ---------- 4. SQL 与 .env 就地刷新 ----------
|
||||
# 建表脚本必须先于灌数据脚本执行,靠文件名前缀定序
|
||||
say "刷新 sql/(来自 backend/db/)"
|
||||
mkdir -p "$HERE/sql"
|
||||
cp "$ROOT/backend/db/init-table-postgres-full.sql" "$HERE/sql/01-init-table.sql"
|
||||
cp "$ROOT/backend/db/init-data.sql" "$HERE/sql/02-init-data.sql"
|
||||
|
||||
say "刷新 .env.iw / .env.ow 的镜像 tag 为 $VER"
|
||||
for f in .env.iw .env.ow; do
|
||||
[ -f "$HERE/$f" ] || { say "缺少 $HERE/$f,跳过"; continue; }
|
||||
sed -i -E "s/^APP_IMAGE=.*/APP_IMAGE=sfile-app:$VER/; s/^WEB_IMAGE=.*/WEB_IMAGE=sfile-web:$VER/" "$HERE/$f"
|
||||
done
|
||||
|
||||
# ---------- 5. 组交付目录并打包 ----------
|
||||
say "组装 $OUT"
|
||||
rm -rf "$OUT"
|
||||
mkdir -p "$OUT/sql"
|
||||
cp "$HERE/docker-compose.yml" "$HERE/.env.example" "$HERE/.env.iw" "$HERE/.env.ow" "$OUT/"
|
||||
cp -r "$HERE/config" "$HERE/nginx" "$HERE/scripts" "$HERE/sql" "$HERE/images" "$OUT/"
|
||||
cp "$HERE/.gitignore" "$OUT/" 2>/dev/null || true
|
||||
|
||||
{
|
||||
echo "s-file 离线交付包 $VER"
|
||||
echo "构建平台: $PLATFORM"
|
||||
echo "导出时间: $(date '+%F %T')"
|
||||
echo
|
||||
echo "镜像清单(sha256 见 images/SHA256SUMS.txt):"
|
||||
for t in "$OUT"/images/*.tar; do
|
||||
TAGS="$(tar -xOf "$t" manifest.json | tr -d '\n' | { grep -oE '"RepoTags":\[[^]]*\]' || true; } | awk 'NR==1{print}')"
|
||||
printf ' %-18s %-8s %s\n' "$(basename "$t")" "$(du -h "$t" | cut -f1)" "${TAGS:-?}"
|
||||
done
|
||||
echo
|
||||
echo "SQL: sql/01-init-table.sql sql/02-init-data.sql"
|
||||
echo
|
||||
echo "现场步骤(两台服务器各执行一次):"
|
||||
echo " 内网机 tar -xzf sfile-offline-$VER-$PLATFORM_TAG.tar.gz && cd sfile-offline-$VER-$PLATFORM_TAG"
|
||||
echo " vi .env.iw # 至少改 MESHSAFE_SECRET,按需改口令/网段/目录"
|
||||
echo " ./scripts/install.sh iw"
|
||||
echo " 外网机 同上,改成 ./scripts/install.sh ow"
|
||||
echo " 验证 python3 scripts/smoke_test.py"
|
||||
} > "$OUT/VERSION.txt"
|
||||
|
||||
say "打包交付盘"
|
||||
mkdir -p "$HERE/dist"
|
||||
tar -czf "$OUT.tar.gz" -C "$(dirname "$OUT")" "$(basename "$OUT")"
|
||||
du -h "$OUT.tar.gz" | awk '{print " 交付盘: "$1" "$2}'
|
||||
echo " 两台服务器各自解压同一份盘,装完只差 install.sh 的参数(iw / ow)"
|
||||
205
deploy/offline/scripts/install.sh
Normal file
205
deploy/offline/scripts/install.sh
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# s-file 客户侧一键安装(在每台服务器上各执行一次)
|
||||
#
|
||||
# cd /opt/sfile && ./scripts/install.sh iw # 内网机
|
||||
# cd /opt/sfile && ./scripts/install.sh ow # 外网机
|
||||
#
|
||||
# 首次执行会按角色把交付包里的 .env.iw / .env.ow 拷成 .env;之后改 .env 即可,
|
||||
# 想回到交付默认值就删掉 .env 重跑。
|
||||
#
|
||||
# 做五件事:按角色选配置 -> 环境自检 -> 载入镜像 tar -> 建数据目录 -> 起栈并等到健康
|
||||
# 幂等:重复执行只重建有差异的容器,不动已初始化好的数据库
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$HERE"
|
||||
|
||||
ROLE_ARG="${1:-}"
|
||||
|
||||
envget() { grep -E "^[[:space:]]*$1=" .env | tail -1 | cut -d= -f2- | tr -d '"' | sed 's/[[:space:]]*$//'; }
|
||||
|
||||
step() { printf '\n\033[1;36m==> %s\033[0m\n' "$1"; }
|
||||
die() { printf '\033[1;31m!!! %s\033[0m\n' "$1" >&2; exit 1; }
|
||||
|
||||
case "$ROLE_ARG" in
|
||||
""|iw|ow) ;;
|
||||
*) die "未知角色「$ROLE_ARG」,只支持 iw(内网机)或 ow(外网机)" ;;
|
||||
esac
|
||||
|
||||
# ---------- 0. 配置就位 ----------
|
||||
if [ ! -f .env ]; then
|
||||
case "$ROLE_ARG" in
|
||||
iw|ow)
|
||||
[ -f ".env.$ROLE_ARG" ] || die "缺少 .env.$ROLE_ARG(交付包不完整)"
|
||||
step "按角色初始化配置:cp .env.$ROLE_ARG .env"
|
||||
cp ".env.$ROLE_ARG" .env
|
||||
;;
|
||||
"") die "用法:./scripts/install.sh iw|ow(iw=内网机,ow=外网机)" ;;
|
||||
esac
|
||||
elif [ -n "$ROLE_ARG" ]; then
|
||||
# 已有 .env 时不让角色参数悄悄覆盖现场改动
|
||||
CUR_ROLE="$(envget SFILE_ROLE)"
|
||||
[ "$CUR_ROLE" = "$ROLE_ARG" ] \
|
||||
|| die ".env 已存在且角色是 $CUR_ROLE,与参数 $ROLE_ARG 不符;确认要换角色请先 rm .env"
|
||||
fi
|
||||
|
||||
# ---------- 0. 前置自检 ----------
|
||||
step "检查 docker 与 compose"
|
||||
command -v docker >/dev/null 2>&1 || die "未安装 docker,请先装 Docker Engine(信创系统用厂商源,版本号 >= 20.10)"
|
||||
docker info >/dev/null 2>&1 || die "docker 守护进程不可用,检查服务是否启动、当前用户是否在 docker 组"
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
DC="docker compose"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
DC="docker-compose"
|
||||
else
|
||||
die "缺少 compose:安装 docker compose v2 插件,或用 docker-compose 命令"
|
||||
fi
|
||||
echo " 使用:$DC"
|
||||
DOCKER_VER="$(docker version --format '{{.Server.Version}}')"
|
||||
echo " docker server:$DOCKER_VER"
|
||||
|
||||
[ -f .env ] || die "缺少 .env —— 先执行:cp .env.example .env && vi .env"
|
||||
|
||||
if grep -q "CHANGE_ME" .env; then
|
||||
# MeshSafe 的 secret 只有真的启用检测时才必须填,否则客户没接 MeshSafe 也装不了机
|
||||
if [ "$(envget MESHSAFE_ENABLED)" = "true" ] && grep -qE "^MESHSAFE_SECRET=.*CHANGE_ME" .env; then
|
||||
die "启用了 MeshSafe 但 MESHSAFE_SECRET 还是占位值:按 MeshSafe 交付单改成白名单里的 secret,或把 MESHSAFE_ENABLED 改成 false"
|
||||
fi
|
||||
rest="$(grep -n CHANGE_ME .env | grep -v '^.*MESHSAFE_SECRET=' | cut -d: -f2 | tr '\n' ' ')"
|
||||
[ -z "$rest" ] || die ".env 里还有 CHANGE_ME 未替换:$rest"
|
||||
fi
|
||||
|
||||
step "检查交付物"
|
||||
[ -d images ] || die "缺少 images/ 目录(镜像 tar 应与本脚本同级目录的 images/ 下)"
|
||||
tar_count=$(find images -maxdepth 1 -name '*.tar' | wc -l)
|
||||
[ "$tar_count" -eq 5 ] || die "images/ 下应有 5 个 .tar(app/web/pg/redis/mq),实际 $tar_count 个:$(ls images)"
|
||||
for f in sql/01-init-table.sql sql/02-init-data.sql; do
|
||||
[ -s "$f" ] || die "缺少 $f,参见 sql/README.md"
|
||||
done
|
||||
echo " 镜像 tar:5 个,SQL 脚本就位"
|
||||
|
||||
# ---------- 1. 校验交付盘 + 镜像架构 ----------
|
||||
# 650M 的 tar 经 U 盘/内网 scp 拷坏是最常见的现场故障,坏包 load 到一半才报错更难查
|
||||
if [ -f images/SHA256SUMS.txt ] && [ "${SKIP_CHECKSUM:-0}" != "1" ]; then
|
||||
step "校验镜像 tar 完整性(sha256,约半分钟)"
|
||||
( cd images && sha256sum -c SHA256SUMS.txt ) \
|
||||
|| die "有 tar 与 SHA256SUMS.txt 不一致:交付盘拷坏了,重新拷一份;确认无碍可 SKIP_CHECKSUM=1 跳过"
|
||||
fi
|
||||
|
||||
step "核对镜像架构"
|
||||
HOST_ARCH="$(uname -m)"
|
||||
case "$HOST_ARCH" in
|
||||
x86_64) EXPECT="amd64" ;;
|
||||
aarch64|arm64) EXPECT="arm64" ;;
|
||||
*) EXPECT="unknown" ;;
|
||||
esac
|
||||
echo " 本机:$HOST_ARCH (期望镜像架构 ${EXPECT})"
|
||||
|
||||
# 架构在镜像的 config blob 里,不在 manifest.json —— manifest.json 只给 Config 的路径
|
||||
tar_arch() {
|
||||
local t="$1" cfg
|
||||
cfg="$(tar -xOf "$t" manifest.json 2>/dev/null | tr -d '\n' \
|
||||
| { grep -oE '"Config":"[^"]+"' || true; } | awk -F'"' 'NR==1{print $4}')"
|
||||
[ -n "$cfg" ] || { echo ""; return 0; }
|
||||
tar -xOf "$t" "$cfg" 2>/dev/null | tr -d '\n' \
|
||||
| { grep -oE '"architecture":"[a-z0-9-]+"' || true; } | awk -F'"' 'NR==1{print $4}'
|
||||
}
|
||||
|
||||
if [ "$EXPECT" != "unknown" ]; then
|
||||
# app 和 pg 各查一个:一个是自己构建的,一个是基础镜像,架构错了必是出盘时选错平台
|
||||
for t in images/sfile-app.tar images/postgres.tar; do
|
||||
[ -f "$t" ] || continue
|
||||
# 不能用 `| head -1`:pipefail 下 head 提前关管道会让上游 grep 收到 SIGPIPE(141),脚本静默中断
|
||||
IMG_ARCH="$(tar_arch "$t")"
|
||||
if [ -n "${IMG_ARCH:-}" ] && [ "$IMG_ARCH" != "$EXPECT" ]; then
|
||||
die "$(basename "$t") 是 $IMG_ARCH,本机是 $EXPECT —— 别硬装,回来重出对应架构的包"
|
||||
fi
|
||||
printf ' %-18s %s\n' "$(basename "$t")" "${IMG_ARCH:-未识别,继续}"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------- 2. 载入镜像 ----------
|
||||
step "docker load 载入离线镜像"
|
||||
for t in images/*.tar; do
|
||||
echo " <- $t"
|
||||
docker load -i "$t"
|
||||
done
|
||||
|
||||
# compose 用的是 .env 里的 tag,缺一个就会去联网拉,离线机上直接卡死
|
||||
for img in "$(envget APP_IMAGE)" "$(envget WEB_IMAGE)" "$(envget PG_IMAGE)" \
|
||||
"$(envget REDIS_IMAGE)" "$(envget MQ_IMAGE)"; do
|
||||
docker image inspect "$img" >/dev/null 2>&1 \
|
||||
|| die "载入后仍看不到镜像 $img —— tar 与 .env 的 tag 不一致,别继续,回来看 images/ 和 .env"
|
||||
echo " ✓ $img"
|
||||
done
|
||||
|
||||
# ---------- 3. 目录 ----------
|
||||
step "创建数据与摆渡目录"
|
||||
DATA_ROOT="$(envget SFILE_DATA_ROOT)"
|
||||
GATE_SEND="$(envget GATE_SEND_DIR)"
|
||||
GATE_RECV="$(envget GATE_RECV_DIR)"
|
||||
[ -n "$DATA_ROOT" ] || die ".env 里 SFILE_DATA_ROOT 为空"
|
||||
|
||||
mkdir -p "$DATA_ROOT"/{upload,logs,pgdata,redis,mq}
|
||||
mkdir -p "$GATE_SEND" "$GATE_RECV"
|
||||
|
||||
# 数据盘挂载点没起来时直接建目录会落到根分区,先确认一下
|
||||
if ! mountpoint -q "$DATA_ROOT" 2>/dev/null; then
|
||||
printf ' \033[1;33m提示:%s 不是独立挂载点,确认容量够(上传+库+日志)\033[0m\n' "$DATA_ROOT"
|
||||
fi
|
||||
|
||||
# pgdata 提前给到容器内 postgres 的 uid=999,避免镜像入口脚本 chown 失败;
|
||||
# redis/mq 的官方入口脚本自己会改属主,这里只保证目录存在且可写
|
||||
chown -R 999:999 "$DATA_ROOT/pgdata" 2>/dev/null || true
|
||||
chmod -R 750 "$DATA_ROOT"
|
||||
|
||||
# SELinux(麒麟/统信多为 enforcing):无 :z 标记的绑定挂载需要放开标签,否则库起不来
|
||||
if command -v chcon >/dev/null 2>&1; then
|
||||
chcon -Rt svirt_sandbox_file_t "$DATA_ROOT" "$GATE_SEND" "$GATE_RECV" 2>/dev/null \
|
||||
|| echo " chcon 失败(未启用 SELinux 可忽略)"
|
||||
fi
|
||||
echo " $DATA_ROOT / $GATE_SEND / $GATE_RECV 就位"
|
||||
|
||||
# ---------- 4. 起栈 ----------
|
||||
step "启动服务(首次建库约 1-3 分钟)"
|
||||
$DC up -d
|
||||
|
||||
step "等待健康检查"
|
||||
ROLE="$(envget SFILE_ROLE)"
|
||||
for svc in pg redis mq app web; do
|
||||
name="sfile-${ROLE}-${svc}"
|
||||
printf ' %-24s ' "$name"
|
||||
for _ in $(seq 1 60); do
|
||||
st="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$name" 2>/dev/null || echo missing)"
|
||||
case "$st" in
|
||||
healthy|running) printf '\033[32m%s\033[0m\n' "$st"; break ;;
|
||||
missing) sleep 2 ;;
|
||||
*) sleep 5 ;;
|
||||
esac
|
||||
done
|
||||
[ "${st:-}" = "healthy" ] || [ "${st:-}" = "running" ] || {
|
||||
printf '\033[31m超时(最后状态:%s)\033[0m\n' "${st:-unknown}"
|
||||
$DC ps
|
||||
echo " 看日志:docker logs --tail 200 ${name}"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
# ---------- 5. 出口信息 ----------
|
||||
IP="$(hostname -I 2>/dev/null | tr ' ' '\n' | { grep -E '^[0-9]+\.' || true; } | awk 'NR==1{print}')"
|
||||
PORT="$(envget WEB_PORT)"
|
||||
step "完成"
|
||||
cat <<EOF
|
||||
访问地址: http://${IP:-<本机IP>}:${PORT:-80}
|
||||
角色: ${ROLE}(传输方向 location=$(envget SFILE_TRANSFER_LOCATION),站点 $(envget SFILE_SITE_ROOT))
|
||||
摆渡: 发出 $(envget GATE_SEND_DIR) / 收到 $(envget GATE_RECV_DIR)
|
||||
MeshSafe: $(envget MESHSAFE_ENABLED) -> $(envget MESHSAFE_BASE_URL)
|
||||
|
||||
常用命令:
|
||||
$DC ps 看状态
|
||||
docker logs -f sfile-${ROLE}-app 看后端日志
|
||||
$DC restart app 改完 .env/config 后重启后端
|
||||
EOF
|
||||
echo " 首次登录后立刻改掉 admin 的默认口令。"
|
||||
270
deploy/offline/scripts/smoke_test.py
Normal file
270
deploy/offline/scripts/smoke_test.py
Normal file
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""s-file 交付验收冒烟测试:登录 -> 上传送检 -> 不通过拦截 -> 跨网摆渡 -> 外网解包再送检 -> 人工复核。
|
||||
|
||||
在能同时访问内网机与外网机前端的机器上运行(需要 python3 + requests)。
|
||||
全部取值走环境变量,脚本内不含任何口令:
|
||||
|
||||
SFILE_IW_BASE=http://10.0.0.11/api SFILE_OW_BASE=http://10.0.0.12/api \\
|
||||
SFILE_ADMIN_USER=admin SFILE_ADMIN_PWD='<现场口令>' python3 smoke_test.py
|
||||
|
||||
退出码:0 全通过,1 有失败项,2 参数缺失。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("缺少 requests:pip3 install requests")
|
||||
sys.exit(2)
|
||||
|
||||
IW = os.environ.get("SFILE_IW_BASE", "http://127.0.0.1:28180/api").rstrip("/")
|
||||
OW = os.environ.get("SFILE_OW_BASE", "http://127.0.0.1:28280/api").rstrip("/")
|
||||
USER = os.environ.get("SFILE_ADMIN_USER", "admin")
|
||||
PWD = os.environ.get("SFILE_ADMIN_PWD", "")
|
||||
# 光闸发出目录(内网机侧本地路径)。跨机验收时留空,只断言接口成功
|
||||
GATE_IW2OW = os.environ.get("SFILE_GATE_SEND_DIR", "")
|
||||
|
||||
# EICAR 标准测试串;后端的 MeshSafe 结构引擎会识别「内嵌 EICAR」,
|
||||
# 因此允许在尾部追加唯一标记(见 main 里的载荷说明)
|
||||
EICAR = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
|
||||
STATUS = {"1": "检测中", "2": "检测通过", "3": "检测不通过", "4": "检测失败"}
|
||||
|
||||
results = []
|
||||
|
||||
|
||||
def rec(name, ok, detail=""):
|
||||
results.append((name, bool(ok), detail))
|
||||
print((" [OK] " if ok else " [FAIL] ") + name + (" " + detail if detail else ""), flush=True)
|
||||
|
||||
|
||||
class Side:
|
||||
def __init__(self, base, label):
|
||||
self.base = base
|
||||
self.label = label
|
||||
self.s = requests.Session()
|
||||
self.token = None
|
||||
|
||||
def _hdr(self, csrf=False):
|
||||
# 后端 MyLoginSuccessHandler 靠 x-requested-with 决定是否回 JSON,否则 302 跳转
|
||||
h = {"x-requested-with": "XMLHttpRequest"}
|
||||
if self.token:
|
||||
h["Authorization"] = "Bearer " + self.token
|
||||
if csrf:
|
||||
t = self.s.cookies.get("XSRF-TOKEN")
|
||||
if t:
|
||||
h["X-XSRF-TOKEN"] = t
|
||||
return h
|
||||
|
||||
def login(self, user, pwd):
|
||||
r = self.s.post(self.base + "/login",
|
||||
data={"username": user, "password": pwd, "userType": 1},
|
||||
headers=self._hdr(), timeout=30)
|
||||
j = self._json(r)
|
||||
if str(j.get("code")) != "200":
|
||||
raise RuntimeError("登录失败 HTTP %s:%s" % (r.status_code, json.dumps(j, ensure_ascii=False)[:300]))
|
||||
self.token = j["data"]["token"]
|
||||
# 触发 XSRF-TOKEN cookie(POST/PUT 需要带 X-XSRF-TOKEN)
|
||||
self.s.get(self.base + "/user/info", headers=self._hdr(), timeout=30)
|
||||
return j
|
||||
|
||||
def get(self, path, params=None):
|
||||
r = self.s.get(self.base + path, params=params, headers=self._hdr(), timeout=60)
|
||||
return r.status_code, self._json(r)
|
||||
|
||||
def post(self, path, payload=None, files=None):
|
||||
kw = {"files": files} if files is not None else {"json": payload if payload is not None else {}}
|
||||
r = self.s.post(self.base + path, headers=self._hdr(csrf=True), timeout=180, **kw)
|
||||
return r.status_code, self._json(r)
|
||||
|
||||
@staticmethod
|
||||
def _json(r):
|
||||
try:
|
||||
return r.json()
|
||||
except Exception:
|
||||
return {"_status": r.status_code, "_raw": r.text[:400]}
|
||||
|
||||
|
||||
def ok(j):
|
||||
return str(j.get("code")) == "200"
|
||||
|
||||
|
||||
def msg(j):
|
||||
return j.get("msg") or j.get("_raw") or json.dumps(j, ensure_ascii=False)[:200]
|
||||
|
||||
|
||||
def page_find(side, name, root_type, parent_id=0):
|
||||
"""按文件名在指定列表里找记录。root_type:1 我的文件 / 2 内穿外 / 3 外传内"""
|
||||
_, j = side.get("/user_file/page",
|
||||
{"parentId": parent_id, "pageNo": 1, "pageSize": 50, "rootType": root_type})
|
||||
if not ok(j):
|
||||
return None
|
||||
for e in (j.get("data") or {}).get("entityList", []):
|
||||
if e.get("fileName") == name:
|
||||
return e
|
||||
return None
|
||||
|
||||
|
||||
def wait_scan(side, file_code, timeout=240):
|
||||
"""轮询安全检测结论;检测中(1)继续等,其余状态返回"""
|
||||
deadline = time.time() + timeout
|
||||
detail = None
|
||||
while time.time() < deadline:
|
||||
_, j = side.get("/file_security/detail", {"fileCode": file_code})
|
||||
if ok(j):
|
||||
detail = j.get("data")
|
||||
st = str((detail or {}).get("scanStatus") or "")
|
||||
if st and st != "1":
|
||||
return detail
|
||||
time.sleep(3)
|
||||
return detail
|
||||
|
||||
|
||||
def main():
|
||||
if not PWD:
|
||||
print("必须设置 SFILE_ADMIN_PWD")
|
||||
return 2
|
||||
tag = uuid.uuid4().hex[:8]
|
||||
clean_name = "sfile-smoke-clean-%s.txt" % tag
|
||||
bad_name = "sfile-smoke-eicar-%s.txt" % tag
|
||||
|
||||
iw, ow = Side(IW, "内网"), Side(OW, "外网")
|
||||
print("\n目标:内网 %s / 外网 %s" % (IW, OW))
|
||||
|
||||
print("\n== 1. 登录两侧 ==")
|
||||
for side in (iw, ow):
|
||||
try:
|
||||
side.login(USER, PWD)
|
||||
rec("%s 登录" % side.label, True, "token %d 字符" % len(side.token))
|
||||
except Exception as e:
|
||||
rec("%s 登录" % side.label, False, str(e))
|
||||
return 1
|
||||
_, j = iw.get("/file_security/enabled")
|
||||
ms_on = ok(j) and j.get("data") is True
|
||||
rec("MeshSafe 开关", True, "enabled=%s(false 时跳过 3/4/7 段)" % ms_on)
|
||||
|
||||
print("\n== 2. 上传 + 建档 ==")
|
||||
# fileCode 按内容寻址:同一份字节会复用上一条检测结论(含人工复核),
|
||||
# 载荷必须每轮唯一,否则「不通过拦截」这类断言会被上一次的放行结论污染
|
||||
uploaded = {}
|
||||
for name, content in ((clean_name, ("s-file smoke clean payload %s\n" % tag) * 80),
|
||||
(bad_name, EICAR + "\n# smoke-tag %s\n" % tag)):
|
||||
_, j = iw.post("/file/upload", files={"file": (name, content.encode(), "text/plain")})
|
||||
if not ok(j):
|
||||
rec("上传 %s" % name, False, msg(j))
|
||||
return 1
|
||||
f = j["data"]
|
||||
_, j2 = iw.post("/normal_user_file/create",
|
||||
{"fileCode": f.get("fileCode"), "fileName": name, "fileType": "1", "parentId": 0})
|
||||
rec("上传并建档 %s" % name, ok(j2), msg(j2) if not ok(j2) else "fileCode=%s" % f.get("fileCode"))
|
||||
if not ok(j2):
|
||||
return 1
|
||||
uploaded[name] = f.get("fileCode")
|
||||
|
||||
print("\n== 3. 检测结论 ==")
|
||||
ids = {}
|
||||
verdicts = {}
|
||||
for name, code in uploaded.items():
|
||||
found = page_find(iw, name, "1")
|
||||
if found:
|
||||
ids[name] = found["id"]
|
||||
d = wait_scan(iw, code)
|
||||
st = str((d or {}).get("scanStatus") or "")
|
||||
verdicts[name] = st
|
||||
print(" %s -> %s | action=%s reason=%s"
|
||||
% (name, STATUS.get(st, "未知"), (d or {}).get("action"), ((d or {}).get("reason") or "")[:80]))
|
||||
if ms_on:
|
||||
rec("干净文件检测通过", verdicts.get(clean_name) == "2", "状态=%s" % STATUS.get(verdicts.get(clean_name)))
|
||||
rec("EICAR 检测不通过", verdicts.get(bad_name) == "3", "状态=%s" % STATUS.get(verdicts.get(bad_name)))
|
||||
|
||||
print("\n== 4. 不通过文件禁止传输 ==")
|
||||
if len(ids) != len(uploaded):
|
||||
rec("内网「我的文件」可见上传文件", False, "只找到 %s" % list(ids))
|
||||
return 1
|
||||
if ms_on:
|
||||
_, j = iw.get("/file_security/share_check", {"userFileId": ids[bad_name]})
|
||||
d = j.get("data") or {}
|
||||
rec("share_check 命中不通过", ok(j) and d.get("blockedCount", 0) >= 1,
|
||||
"blocked=%s scanning=%s" % (d.get("blockedCount"), d.get("scanningCount")))
|
||||
_, j = iw.post("/transfer_user_file/create", {"userFileId": ids[bad_name]})
|
||||
rec("不通过文件传输被拦截", not ok(j), msg(j)[:160])
|
||||
|
||||
print("\n== 5. 干净文件跨网摆渡 ==")
|
||||
before = set(os.listdir(GATE_IW2OW)) if GATE_IW2OW and os.path.isdir(GATE_IW2OW) else None
|
||||
_, j = iw.post("/transfer_user_file/create", {"userFileId": ids[clean_name]})
|
||||
rec("内网发起传输", ok(j), msg(j) if not ok(j) else "外发文件夹ID=%s" % j.get("data"))
|
||||
if not ok(j):
|
||||
return 1
|
||||
if before is None:
|
||||
print(" 未设 SFILE_GATE_SEND_DIR(跨机验收常见),跳过摆渡包落盘检查")
|
||||
else:
|
||||
new_zip = None
|
||||
for _ in range(30):
|
||||
delta = set(os.listdir(GATE_IW2OW)) - before
|
||||
if delta:
|
||||
new_zip = sorted(delta)[0]
|
||||
break
|
||||
time.sleep(2)
|
||||
rec("光闸发出目录出现摆渡包", new_zip is not None, new_zip or GATE_IW2OW)
|
||||
|
||||
print("\n== 6. 外网解包 + 再送检(监听任务默认每分钟一次) ==")
|
||||
# 接收侧落在「内穿外」列表(root_type=2),目录名带时间戳后缀,需进目录取子文件
|
||||
folder = None
|
||||
deadline = time.time() + 300
|
||||
while time.time() < deadline and folder is None:
|
||||
_, j = ow.get("/user_file/page", {"parentId": 0, "pageNo": 1, "pageSize": 50, "rootType": "2"})
|
||||
for e in (j.get("data") or {}).get("entityList", []):
|
||||
if (e.get("fileName") or "").startswith(clean_name.rsplit(".", 1)[0]):
|
||||
folder = e
|
||||
break
|
||||
if folder is None:
|
||||
time.sleep(10)
|
||||
rec("外网「内穿外」出现接收目录", folder is not None,
|
||||
str(folder.get("fileName")) if folder else "超时未出现(检查光闸与 FILE_TRANSFER_LOCATION)")
|
||||
if folder:
|
||||
child = None
|
||||
_, j = ow.get("/user_file/page",
|
||||
{"parentId": folder["id"], "pageNo": 1, "pageSize": 50, "rootType": "2"})
|
||||
for e in (j.get("data") or {}).get("entityList", []):
|
||||
if e.get("fileName") == clean_name:
|
||||
child = e
|
||||
break
|
||||
rec("接收目录内可见文件", child is not None, str(child.get("fileCode")) if child else "目录为空")
|
||||
if child:
|
||||
d = wait_scan(ow, child["fileCode"], timeout=180)
|
||||
st = str((d or {}).get("scanStatus") or "")
|
||||
rec("外网解包后自动送检", (not ms_on) or st == "2",
|
||||
"状态=%s reason=%s" % (STATUS.get(st, "未知"), ((d or {}).get("reason") or "")[:60]))
|
||||
|
||||
print("\n== 7. 人工复核改判 ==")
|
||||
if not ms_on:
|
||||
print(" MeshSafe 未启用,跳过")
|
||||
else:
|
||||
_, j = iw.post("/file_security/review",
|
||||
{"fileCode": uploaded[bad_name], "decision": "pass",
|
||||
"comment": "交付验收复核放行 %s" % tag})
|
||||
rec("复核改判接口", ok(j), msg(j) if not ok(j) else "")
|
||||
d = j.get("data") or {}
|
||||
rec("复核后状态=通过", str(d.get("scanStatus")) == "2",
|
||||
"状态=%s action=%s" % (STATUS.get(str(d.get("scanStatus"))), d.get("action")))
|
||||
_, j = iw.get("/file_security/detail", {"fileCode": uploaded[bad_name]})
|
||||
rec("复核说明落库", ok(j) and tag in ((j.get("data") or {}).get("reviewComment") or ""),
|
||||
str((j.get("data") or {}).get("reviewComment"))[:80])
|
||||
_, j = iw.get("/file_security/share_check", {"userFileId": ids[bad_name]})
|
||||
d = j.get("data") or {}
|
||||
rec("复核后不再拦截", ok(j) and d.get("blockedCount", 0) == 0, "blocked=%s" % d.get("blockedCount"))
|
||||
|
||||
print("\n== 汇总 ==")
|
||||
passed = sum(1 for _, o, _ in results if o)
|
||||
for name, o, _ in results:
|
||||
print(" %s %s" % ("OK " if o else "FAIL", name))
|
||||
print(" %d/%d 通过" % (passed, len(results)))
|
||||
return 0 if passed == len(results) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
2082
deploy/offline/sql/01-init-table.sql
Normal file
2082
deploy/offline/sql/01-init-table.sql
Normal file
File diff suppressed because it is too large
Load Diff
46
deploy/offline/sql/02-init-data.sql
Normal file
46
deploy/offline/sql/02-init-data.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
-- 系统uc_platform
|
||||
INSERT INTO uc_platform
|
||||
(platform_code, name, full_name, short_name, business, deleted, create_time)
|
||||
VALUES(1, '文档系统', 's-file', 'file', true, FALSE, now());
|
||||
|
||||
-- 平台机构/平台机构管理员
|
||||
INSERT INTO xs_org
|
||||
(org_name, org_code, type, address_code, address_name, description, quality, enable, activated, parent_id, credit_code, trade, deleted, create_by, create_time, update_by, update_time, proxy_by, activation_code, phone, query_children, org_logo, registered_address, company_size, business_license, recruit_position_type)
|
||||
VALUES('senmesh', 'O001', '1', '440232', NULL, NULL, false, TRUE, TRUE, NULL, '', NULL, FALSE, 0, now(), 1, now(), null, NULL, null, NULL, null, '1234', '6', null, '1');
|
||||
|
||||
INSERT INTO uc_user
|
||||
(username, password, type, supeiror, security_code, nickname, avatar, phone, phone_binding, real_name, card_no, authenticated, status, email, address, education, init_user_id, enabled, account_non_expired, account_non_locked, credentials_non_expired, deleted, create_by, create_time, update_by, update_time, proxy_by, gender, birthday)
|
||||
VALUES('admin', '$2a$10$0N3iaxx2sYv0hXvYzS4.6.yriL.DfjRTjED7Y4vkqxzbxi2dNpBou', 1, TRUE, '$2a$10$mGA1jdBzq8micEr02KiJ9OiYQz1fc/PSVG0hN0EYUuxhZp8EI1M2q', '平台管理员', '', '13888888888', TRUE, '平台管理员', NULL, FALSE, 2, null, NULL, NULL, NULL, TRUE, TRUE, TRUE, TRUE, FALSE, 0, now(), 1, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO uc_org_user
|
||||
(user_id, org_id, administrator, deleted, create_by, create_time, update_by, update_time, proxy_by)
|
||||
VALUES(1, 1, TRUE, FALSE, 0, now(), NULL, NULL, NULL);
|
||||
-- 平台uc_org_relation
|
||||
INSERT INTO uc_org_relation
|
||||
(org_id, org_type, parent_id, path, platform_code, platform_name, deleted, create_by, create_time, update_by, update_time, proxy_by)
|
||||
VALUES(1, 1, 0, '1', 1, NULL, FALSE, NULL, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of x_file_category
|
||||
-- ----------------------------
|
||||
INSERT INTO x_file_category (file_category, file_suffix, deleted, create_by, create_time) VALUES
|
||||
('video', 'mp4', FALSE, 0, NOW()),
|
||||
('video', 'mkv', FALSE, 0, NOW()),
|
||||
('video', 'avi', FALSE, 0, NOW()),
|
||||
('video', 'mov', FALSE, 0, NOW()),
|
||||
('video', 'wmv', FALSE, 0, NOW()),
|
||||
('video', 'flv', FALSE, 0, NOW()),
|
||||
('audio', 'mp3', FALSE, 0, NOW()),
|
||||
('audio', 'wav', FALSE, 0, NOW()),
|
||||
('audio', 'flac', FALSE, 0, NOW()),
|
||||
('audio', 'aac', FALSE, 0, NOW()),
|
||||
('document', 'txt', FALSE, 0, NOW()),
|
||||
('document', 'docx', FALSE, 0, NOW()),
|
||||
('document', 'pdf', FALSE, 0, NOW()),
|
||||
('document', 'epub', FALSE, 0, NOW()),
|
||||
('other', 'zip', FALSE, 0, NOW()),
|
||||
('other', 'rar', FALSE, 0, NOW()),
|
||||
('other', '7z', FALSE, 0, NOW()),
|
||||
('other', 'jpg', FALSE, 0, NOW()),
|
||||
('other', 'png', FALSE, 0, NOW()),
|
||||
('other', 'gif', FALSE, 0, NOW()),
|
||||
('other', 'tiff', FALSE, 0, NOW());
|
||||
12
deploy/offline/sql/README.md
Normal file
12
deploy/offline/sql/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
建表与初始数据脚本已随包放好,文件名前缀决定执行顺序(先建表后灌数据):
|
||||
|
||||
| 文件 | 来源 | 说明 |
|
||||
|---|---|---|
|
||||
| `01-init-table.sql` | `backend/db/init-table-postgres-full.sql` | 全量 DDL,含 `x_file_security` 安全检测表 |
|
||||
| `02-init-data.sql` | `backend/db/init-data.sql` | admin 账号、机构、平台编码(`system.code=1` 要与此一致) |
|
||||
|
||||
出盘时用 `scripts/export-images.sh` 从仓库重新覆盖这两份,别手改副本;
|
||||
增量升级脚本(`backend/db/upgrade-*.sql`)不在这里,装好后按《部署说明.md》第 7 节人工执行。
|
||||
|
||||
只有 `POSTGRES_*` 指向的库为空时,PostgreSQL 官方镜像才会执行 `docker-entrypoint-initdb.d`;
|
||||
数据目录已有数据时会跳过 —— 想重建就把 `$SFILE_DATA_ROOT/pgdata` 一起清掉再 `up`。
|
||||
219
deploy/offline/部署说明.md
Normal file
219
deploy/offline/部署说明.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# s-file 客户侧离线部署(两台服务器:内网机 + 外网机)
|
||||
|
||||
> 本目录是**交付用**部署;本机联调用的两套栈同起在一台机器上,见 [../部署说明.md](../部署说明.md)。
|
||||
> 客户现场没有网络、没有光闸模拟目录,一切靠 `docker load` 的镜像 tar + 真实光闸摆渡目录。
|
||||
|
||||
---
|
||||
|
||||
## 1. 拓扑
|
||||
|
||||
```
|
||||
内网(客户办公网) 外网(外部报送网)
|
||||
┌────────────────────────┐ ┌────────────────────────┐
|
||||
│ 内网服务器 A │ │ 外网服务器 B │
|
||||
│ ┌──────────────────┐ │ 光闸/单向 │ ┌──────────────────┐ │
|
||||
│ │ sfile-iw-web :80 │◄─┼── 摆渡设备 ──►┼─ │ sfile-ow-web :80 │ │
|
||||
│ │ sfile-iw-app :8080│ │ (不开放网络) │ │ sfile-ow-app │ │
|
||||
│ │ pg/redis/mq(内部) │ │ │ │ pg/redis/mq(内部)│
|
||||
│ └──────────────────┘ │ │ └──────────────────┘ │
|
||||
│ /data/sfile/gate/out │─── 发 ────► │ /data/sfile/gate/in │
|
||||
│ /data/sfile/gate/in │◄─── 收 ──── │ /data/sfile/gate/out │
|
||||
└───────────┬────────────┘ └───────────┬────────────┘
|
||||
│ http://host.docker.internal:<MeshSafe端口>
|
||||
┌──────┴───────────┐
|
||||
│ MeshSafe 文件安全检测 │ 同机容器 / 独立服务器 / 集群均可,只要求 内网机能 HTTP 访问到
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
- 两台服务器之间**不开任何 TCP 端口**,文件只经光闸在 `gate/out` ⇄ 对端 `gate/in` 之间单向搬运。
|
||||
- 每台机器一个独立 compose 栈,同一份 `docker-compose.yml`,差别全在 `.env`。
|
||||
- 摆渡目录由光闸设备挂载/同步进宿主机,应用只认容器内固定路径 `/data/transfer/send`、`/data/transfer/receive`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 交付包内容
|
||||
|
||||
本目录本身**就是**交付包,`.env`、`sql`、`images/*.tar` 都已就位,整目录拷走即可装机(不需要现场再回仓库取任何东西)。
|
||||
|
||||
```
|
||||
deploy/offline/
|
||||
├── docker-compose.yml 一台机器的 5 个服务(web/app/pg/redis/mq),现场不改
|
||||
├── .env.iw 内网机出厂配置(已填好,含口令)← install.sh iw 会拷成 .env
|
||||
├── .env.ow 外网机出厂配置(同上,口令与内网机不同)
|
||||
├── .env.example 空白模板,只在客户环境差异大到出厂值不适用时才用
|
||||
├── VERSION.txt 本次出盘的版本/平台/时间/镜像清单
|
||||
├── config/application.yml 静态默认配置,里面不放任何口令
|
||||
├── nginx/default.conf.template 前端反代(内/外网共用,靠 SFILE_SITE_ROOT 选站点)
|
||||
├── sql/01-init-table.sql 全量建表(92K,含 x_file_security)
|
||||
├── sql/02-init-data.sql 初始数据(2.8K,admin、机构、平台编码)
|
||||
├── images/*.tar 5 个镜像,共 650M(docker save 原始大小)
|
||||
├── images/SHA256SUMS.txt 交付盘校验值,install.sh 开机先比对它
|
||||
└── scripts/
|
||||
├── install.sh 现场一键安装(要带 iw / ow 参数)
|
||||
├── smoke_test.py 现场自动化验收(18 条断言)
|
||||
└── export-images.sh 我方出盘脚本,现场用不到
|
||||
```
|
||||
|
||||
> 仓库里 `images/*.tar`、`SHA256SUMS.txt`、`dist/` 已被 `.gitignore` 排除——镜像 tar 不进气仓,只走交付盘。
|
||||
|
||||
镜像清单与已验证版本(tag 必须与 tar 里的完全一致,`install.sh` 会核对 sha256、架构与 tag 三者):
|
||||
|
||||
| 镜像 | 交付 tag | tar 大小 | 本机验证版本 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `sfile-app` | `1.0.0` | 313M | JDK8 运行时 eclipse-temurin:8-jre-jammy | 内含 s-file.jar |
|
||||
| `sfile-web` | `1.0.0` | 39M | nginx:alpine | **内网包与外网包都在同一个镜像里** |
|
||||
| `postgres` | `18` | 161M | 18.3 | |
|
||||
| `redis` | `8` | 53M | 8.6.3 | |
|
||||
| `rabbitmq` | `3.13-management-alpine` | 84M | 3.13.7 | |
|
||||
|
||||
---
|
||||
|
||||
## 3. 现场要先定的 6 件事
|
||||
|
||||
| 项 | 变量 | 建议 |
|
||||
|---|---|---|
|
||||
| 服务器 CPU 架构 | — | `uname -m`:x86_64→amd64,aarch64(鲲鹏/飞腾)→arm64。**出盘前必须确认**,架构不符装不上 |
|
||||
| 前端对外端口 | `WEB_PORT` | 出厂值 80;被占用改 8080/9080,同步改客户访问地址 |
|
||||
| 数据盘根目录 | `SFILE_DATA_ROOT` | 独立挂载点,出厂值 `/data/sfile`。上传文件+库+日志都在这,按 500MB/文件上限估:`文件平均大小 × 日均份数 × 保留天数`,一般 ≥200G |
|
||||
| 光闸摆渡目录 | `GATE_SEND_DIR` / `GATE_RECV_DIR` | 与光闸厂商确认挂载点,**发出/收到是两个不同目录**,两侧机器各自配自己的 |
|
||||
| MeshSafe 地址与密钥 | `MESHSAFE_BASE_URL` / `MESHSAFE_SECRET` | 同机:`http://host.docker.internal:44380`;独立机:`http://<IP>:<端口>`;`MESHSAFE_SECRET` 出厂是占位值,**接了检测就必须按 MeshSafe 交付单改**,没接就 `MESHSAFE_ENABLED=false` |
|
||||
| 各类口令 | `POSTGRES_PASSWORD` 等 | 出厂已给随机值可直接跑,正式交付按客户安全要求轮换(见 §5.1) |
|
||||
|
||||
机器规格建议:8 核 / 16G 起(应用堆 `-Xmx` 3G,PG + MQ + Redis 还要约 2-3G,留足页缓存),系统盘 100G + 数据盘独立。
|
||||
|
||||
---
|
||||
|
||||
## 4. 出盘(我方,需要联网一次)
|
||||
|
||||
```bash
|
||||
# 前置:backend/target/s-file.jar 与 frontend/ 已构建
|
||||
cd deploy/offline
|
||||
./scripts/export-images.sh 1.0.0 # amd64
|
||||
TARGET_PLATFORM=linux/arm64 ./scripts/export-images.sh 1.0.0 # 鲲鹏/飞腾
|
||||
```
|
||||
|
||||
脚本会把 `images/*.tar`、`sql/*.sql`、`.env.iw/.env.ow` 的镜像 tag、`SHA256SUMS.txt` 就地刷新,
|
||||
再打一份 `dist/sfile-offline-<版本>-<平台>.tar.gz` 用于拷盘/刻录。带盘就带这个 gz。
|
||||
|
||||
---
|
||||
|
||||
## 5. 装机(客户侧,两台机器各执行一次)
|
||||
|
||||
```bash
|
||||
# 1) 解包(两台机器用同一个盘,解出来的目录一模一样)
|
||||
tar -xzf sfile-offline-1.0.0-linux-amd64.tar.gz -C /opt/
|
||||
cd /opt/sfile-offline-1.0.0-linux-amd64
|
||||
|
||||
# 2) 只改必须改的:MeshSafe 密钥(不接检测就关掉开关)
|
||||
vi .env.iw # 内网机;外网机改 .env.ow
|
||||
|
||||
# 3) 一键装:按角色建 .env -> 自检 -> 校验 tar -> docker load -> 建目录 -> 起栈 -> 等健康
|
||||
./scripts/install.sh iw # 内网机
|
||||
./scripts/install.sh ow # 外网机
|
||||
```
|
||||
|
||||
`install.sh` 第一次执行会把 `.env.iw`/`.env.ow` 拷成 `.env`,之后只读 `.env`——现场改动不会被覆盖。
|
||||
想回到出厂值:`rm .env && ./scripts/install.sh iw`。
|
||||
参数与已有 `.env` 的角色不一致时直接拒绝,防止在内网机上起了外网栈。
|
||||
|
||||
装完会打印访问地址。首次登录后**立刻改掉 admin 的初始口令**。
|
||||
|
||||
### 5.1 出厂口令(交付单要抄一份,并建议现场轮换)
|
||||
|
||||
| 变量 | 内网机出厂值 | 外网机出厂值 | 现场是否必须改 |
|
||||
|---|---|---|---|
|
||||
| `POSTGRES_PASSWORD` | `Sfile-iw-Pg-N1tOz1MxgdaS` | `Sfile-ow-Pg-pT-673--i1rk` | 建议(只在本机容器网络内生效,不对网暴露) |
|
||||
| `MQ_PASSWORD` | `Sfile-iw-Mq-UTqVSnCDZ0LB` | `Sfile-ow-Mq-sfW5Cv-Nk-cA` | 建议(同上) |
|
||||
| `JWT_SECRET` | `86a6b015…7d70`(64 位) | `9eeeea03…6988`(64 位) | 必须保密;泄露等于能伪造登录态,两侧已不同 |
|
||||
| `USER_DEFAULT_PASSWORD` | `Sfile-iw-Usr-FBLS-0fA` | `Sfile-ow-Usr-WqDJ7PDS` | **必须改**:新建用户的初始密码,业务员要知道 |
|
||||
| admin 登录口令 | 由 `backend/db/init-data.sql` 灌入 | 同左 | **必须改**:装完第一次登录就改 |
|
||||
| `MESHSAFE_SECRET` | `CHANGE_ME_meshsafe_secret` | `CHANGE_ME_meshsafe_secret` | **故意留空**:以 MeshSafe 侧 `MESHSAFE_API_CLIENTS` 白名单为准,装机前替换 |
|
||||
|
||||
> `install.sh` 只对**启用了 MeshSafe 且 secret 还是占位值**的情况卡住;`MESHSAFE_ENABLED=false` 时其余占位值不拦,方便先装后接检测。
|
||||
|
||||
### 5.2 内网机 / 外网机配置差异
|
||||
|
||||
`.env.iw` 与 `.env.ow` 已按下表分别填好,这里列出来只为让客户网络/安全评审能对上:
|
||||
|
||||
| 变量 | 内网机 | 外网机 |
|
||||
|---|---|---|
|
||||
| `SFILE_ROLE` | `iw` | `ow` |
|
||||
| `SFILE_TRANSFER_LOCATION` | `1` | `2` |
|
||||
| `SFILE_SITE_ROOT` | `/usr/share/nginx/inside` | `/usr/share/nginx/outside` |
|
||||
| `GATE_SEND_DIR` | `/data/sfile/gate/out`(光闸的**内→外**发出目录) | `/data/sfile/gate/out`(光闸的**外→内**发出目录) |
|
||||
| `GATE_RECV_DIR` | `/data/sfile/gate/in`(光闸的**外→内**投递目录) | `/data/sfile/gate/in`(光闸的**内→外**投递目录) |
|
||||
| `MESHSAFE_CHANNEL` / `MESHSAFE_CLIENT_ID` | `sfile-iw` | `sfile-ow` |
|
||||
| `JWT_SECRET` / `POSTGRES_PASSWORD` / `MQ_PASSWORD` | 一套 | 另一套(已确认不同) |
|
||||
| `WEB_PORT` | 80 | 80(两台机器各占自己的 80,不冲突) |
|
||||
| `APP_SUBNET` / `APP_NODE_IP` / `APP_WORK_ID` | `172.30.88.0/24` / `.10` / `1` | `172.30.98.0/24` / `.10` / `2` |
|
||||
|
||||
> 两侧网段/机器号故意错开:客户如果先把两台合并成一台机器做演示,不改也能同起。
|
||||
> `SFILE_TRANSFER_LOCATION` 写反是本方案最容易踩的坑:值对调后,两侧都以为自己是发送端,摆渡目录里的包永远不会被解包。
|
||||
|
||||
---
|
||||
|
||||
## 6. 验收清单(现场按这个走一遍)
|
||||
|
||||
| # | 动作 | 期望 |
|
||||
|---|---|---|
|
||||
| 1 | 浏览器开 `http://<内网机IP>:<端口>` | 左上角有「内网」徽标,能登录 |
|
||||
| 2 | 开 `http://<外网机IP>:<端口>` | 无「内网」徽标(设计上只有内网侧显示标识),能登录 |
|
||||
| 3 | 内网上传一个正常文件 | 「安全检测」列几秒内从**检测中**变**检测通过** |
|
||||
| 4 | 内网上传 EICAR 测试串 | 变**检测不通过**,理由含 `Eicar-Test-Signature` 或 `内嵌 EICAR` |
|
||||
| 5 | 对该文件点分享/传输 | 被拒:`安全检测未通过,禁止分享与传输` |
|
||||
| 6 | 内网把通过的文件「传输」到外网 | 内网 `GATE_SEND_DIR` 出现 `<n>_<id>.zip` |
|
||||
| 7 | 等 1 分钟内网监听任务 | 外网「内穿外」目录出现 `<原名>_<时间戳>` 文件夹,文件在内 |
|
||||
| 8 | 外网该文件的安全检测 | 自动送检并变**检测通过** |
|
||||
| 9 | 内网对不通过文件做人工复核放行 | 状态变**检测通过**、标记**人工复核**,之后可传输 |
|
||||
| 10 | `docker compose ps` 两台机各 5 个 healthy | 无 `Restarting`/`unhealthy` |
|
||||
|
||||
第 3-9 步可用 `scripts/smoke_test.py` 自动跑(在有 python3 + requests 的机器上,指向客户 IP),现场没条件就手工点。
|
||||
|
||||
```bash
|
||||
SFILE_IW_BASE=http://<内网机IP>:<端口>/api \
|
||||
SFILE_OW_BASE=http://<外网机IP>:<端口>/api \
|
||||
SFILE_ADMIN_USER=admin SFILE_ADMIN_PWD='<现场初始口令>' \
|
||||
python3 scripts/smoke_test.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 运维
|
||||
|
||||
```bash
|
||||
docker compose ps # 状态
|
||||
docker logs -f sfile-iw-app # 后端日志(含送检/摆渡全过程)
|
||||
docker compose up -d app # 改完 .env 或 config 后重启后端
|
||||
docker compose down # 停止(不动数据)
|
||||
docker compose down -v --remove-orphans # ⚠ 数据在 bind 目录里,不会丢;但别顺手执行
|
||||
```
|
||||
|
||||
**备份只需三样**:`$SFILE_DATA_ROOT/pgdata`(库)、`$SFILE_DATA_ROOT/upload`(文件本体)、`.env`(口令)。
|
||||
恢复:换机器后原路径放回 + `docker load` + `install.sh`。
|
||||
|
||||
数据库升级:新版本的增量 SQL 放 `backend/db/upgrade-*.sql`,人工执行
|
||||
|
||||
```bash
|
||||
docker exec -i sfile-iw-pg psql -U sfile -d s_file < upgrade-20261101-meshsafe-file-security.sql
|
||||
```
|
||||
|
||||
(`sql/01/02` 只在**空库首次启动**执行,已有数据卷会被忽略。)
|
||||
|
||||
---
|
||||
|
||||
## 8. 已知坑与取舍
|
||||
|
||||
1. **占位值检查是有意的,别图省事先注释掉**:`install.sh` 只在「`MESHSAFE_ENABLED=true` 且 `MESHSAFE_SECRET` 还是占位值」时拒绝启动,其余 `CHANGE_ME` 也会拦。没接检测就老实写 `MESHSAFE_ENABLED=false`。
|
||||
2. **交付盘必须整目录拷**:`install.sh` 会核 5 个 tar 的 sha256、个数、以及 `.env` 里 5 个镜像 tag 是否都能 load 到。只拷脚本不拷 `images/`(tar 不进气仓,见 `.gitignore`)是最常见的现场翻车。
|
||||
3. **架构不符装不上**:`sfile-app`/`sfile-web` 是在构建机上 `docker build` 出来的,信创 ARM 机必须用 `TARGET_PLATFORM=linux/arm64` 重新出盘(`install.sh` 会拦你)。
|
||||
4. **PostgreSQL 18 的数据目录挂载点是 `/var/lib/postgresql`**(15 及以前是 `/var/lib/postgresql/data`)。自己换库版本时改 `.env` 的 `PG_DATA_DIR`,否则容器会报「检测到未挂载的数据」并反复重启。
|
||||
5. **nginx 反代必须透传带端口的 Host**(模板里已用 `$http_host`)。改成 `$host` 会让后端 `RequestUtils.getRootDomain` 对无端口地址抛 `IllegalStateException`,症状就是**登录 500**。
|
||||
6. **发号器 workerId 必须能取到,否则应用起不来。** 后端按 `code.snowflake.workid.<app容器IP>` 取机器号,取不到时回退默认值 `99`,而 99 超出雪花 5 位上限,启动即 `IllegalArgumentException: worker Id can't be greater than 31`。本方案的做法是把 app 的容器 IP 锁死(`APP_NODE_IP`)并由 compose 自动写进 JVM 参数,`.env` 里 `APP_SUBNET / APP_GATEWAY / APP_NODE_IP` 三个值必须同网段。**只想改代码解决的话**,把 `CodeConfiguration#snowFlakeCodeGenerator` 的兜底 `99L` 改成一个合法值即可解除这层 IP 耦合——但那样同侧多实例会撞 ID,需自行权衡。
|
||||
7. **MeshSafe 凭证**:`MESHSAFE_CLIENT_ID`/`SECRET` 必须在 MeshSafe 侧 `MESHSAFE_API_CLIENTS` 白名单里存在,否则送检全部 401。`MESHSAFE_FAIL_OPEN=true` 时检测服务挂了不阻断业务(只记失败),等保从严请置 `false`。
|
||||
8. **`host.docker.internal` 需要 Docker ≥ 20.10**;MeshSafe 在独立服务器时不需要它,可把 compose 里那两行 `extra_hosts` 删掉。
|
||||
9. **PDF 盖章依赖 spire.pdf 是桩实现**:本机因拿不到商业包做了替代 jar,运行时调用会抛 `UnsupportedOperationException`。交付前若客户要电子签章功能,必须换回 `e-iceblue:spire.pdf:3.7.4` 正式包重新打 jar。
|
||||
10. **摆渡延迟固定为监听周期**(默认 1 分钟一次)。客户嫌慢就调 `FILE_TRANSFER_MONITOR_CRON`,不要改成秒级——它会整目录扫描。
|
||||
11. **两侧数据库完全独立**,同一份 `init-data.sql` 灌出来 admin 初始口令一致,交付时两侧都要改。
|
||||
12. **装完才发现角色装反了**(内网机上跑了 `install.sh ow`):`.env` 与参数角色不一致时 `install.sh` 直接拒绝,所以真发生一定是 `rm .env` 之后重装。compose 项目名跟着角色走,旧项目还在占用 `APP_SUBNET`,会报 `invalid pool request: Pool overlaps with other one on this address space`。先把旧项目拆掉再起:
|
||||
`docker compose -p sfile-<旧角色> down` → `rm .env` → `./scripts/install.sh <正确角色>`。
|
||||
13. **检测结论按文件内容复用**:MeshSafe 的 `fileCode` 是内容寻址,同一份字节重复上传会直接沿用上次结论,不会重新扫。这是性能上的设计,但等保评审可能会问「改判是否会被复用绕过」,现场演示验收时请用**每次内容都不同**的测试文件。
|
||||
Reference in New Issue
Block a user