初始化蓝鸟文件快递柜源码与部署文档
This commit is contained in:
commit
5643d22242
70 files changed
+18669
No files matched your search
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
project_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$project_root"
|
||||
build_mode=${1:-build}
|
||||
case "$build_mode" in build|release|web) ;; *) echo 'Usage: scripts/build.sh [build|release|web] [all|amd64|arm64]' >&2; exit 2;; esac
|
||||
release_target=${2:-all}
|
||||
case "$release_target" in all) release_arches='amd64 arm64' ;; amd64|arm64) release_arches=$release_target ;; *) echo 'Release architecture must be all, amd64 or arm64' >&2; exit 2 ;; esac
|
||||
npm --prefix web ci
|
||||
npm --prefix web run build
|
||||
touch internal/web/dist/.gitkeep
|
||||
if [ "$build_mode" = web ]; then exit 0; fi
|
||||
node scripts/notices.mjs
|
||||
mkdir -p build
|
||||
if [ "$build_mode" = build ]; then
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o build/bluebird ./cmd/bluebird
|
||||
exit 0
|
||||
fi
|
||||
for build_arch in $release_arches; do
|
||||
build_dir="build/linux-$build_arch"
|
||||
rm -rf -- "$build_dir"
|
||||
mkdir -p "$build_dir"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH="$build_arch" go build -trimpath -ldflags='-s -w' -o "$build_dir/bluebird" ./cmd/bluebird
|
||||
cp deploy/config.example.toml "$build_dir/config.toml"
|
||||
cp deploy/install.sh "$build_dir/install.sh"
|
||||
cp deploy/upgrade.sh "$build_dir/upgrade.sh"
|
||||
printf '%s\n' "$build_arch" > "$build_dir/ARCH"
|
||||
cp -R deploy docs "$build_dir/"
|
||||
cp README.md THIRD_PARTY_NOTICES.txt "$build_dir/"
|
||||
tar -czf "build/bluebird-linux-$build_arch.tar.gz" -C "$build_dir" .
|
||||
done
|
||||
if [ "$release_target" != arm64 ]; then cp build/linux-amd64/bluebird build/bluebird; fi
|
||||
(cd build && sha256sum bluebird-linux-*.tar.gz > SHA256SUMS)
|
||||
@@ -0,0 +1,15 @@
|
||||
import { chromium } from '../web/node_modules/playwright/index.mjs'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
const svg=readFileSync('web/public/favicon.svg','utf8')
|
||||
const browser=await chromium.launch({headless:true})
|
||||
try {
|
||||
const page=await browser.newPage();await page.setContent('<canvas id="icon"></canvas>')
|
||||
const images=[]
|
||||
for(const size of [16,32,48,180,192,512]){
|
||||
const data=await page.evaluate(async({svg,size})=>{const canvas=document.querySelector('canvas');canvas.width=size;canvas.height=size;const image=new Image();image.src='data:image/svg+xml;base64,'+btoa(unescape(encodeURIComponent(svg)));await image.decode();canvas.getContext('2d').drawImage(image,0,0,size,size);return canvas.toDataURL('image/png').split(',')[1]},{svg,size})
|
||||
const bytes=Buffer.from(data,'base64');if(size<=48)images.push({size,bytes});if(size===180)writeFileSync('web/public/apple-touch-icon.png',bytes);if(size===192||size===512)writeFileSync(`web/public/icon-${size}.png`,bytes)
|
||||
}
|
||||
const header=Buffer.alloc(6+16*images.length);header.writeUInt16LE(1,2);header.writeUInt16LE(images.length,4);let offset=header.length
|
||||
images.forEach(({size,bytes},i)=>{const index=6+16*i;header[index]=size;header[index+1]=size;header.writeUInt16LE(1,index+4);header.writeUInt16LE(32,index+6);header.writeUInt32LE(bytes.length,index+8);header.writeUInt32LE(offset,index+12);offset+=bytes.length})
|
||||
writeFileSync('web/public/favicon.ico',Buffer.concat([header,...images.map(image=>image.bytes)]))
|
||||
} finally{await browser.close()}
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/bin/sh
|
||||
# Only run inside a disposable container with /pkg mounted read-only.
|
||||
set -eu
|
||||
[ "${BLUEBIRD_INSTALL_SMOKE:-}" = 1 ] && [ "$(id -u)" = 0 ] && [ -f /pkg/ARCH ] || { echo 'Only for the isolated deployment test container' >&2; exit 1; }
|
||||
fixture_password=install-fixture-password
|
||||
worker_pid=
|
||||
trap 'if [ -n "$worker_pid" ]; then kill "$worker_pid" 2>/dev/null || :; wait "$worker_pid" 2>/dev/null || :; fi' EXIT
|
||||
|
||||
/pkg/bluebird validate-config --config /pkg/config.toml
|
||||
[ ! -e /pkg/var ]
|
||||
# Invalid site paths must fail before publishing configuration or credentials.
|
||||
if BLUEBIRD_ADMIN_PASSWORD=$fixture_password /pkg/install.sh --prefix /opt/rejected --url http://files.example.test/path --encrypt --no-start >/tmp/rejected.log 2>&1; then echo 'Invalid site path accepted' >&2; exit 1; fi
|
||||
[ ! -e /opt/rejected ]
|
||||
[ -z "$(find /opt -maxdepth 1 -name '.bluebird-install-*' -print)" ]
|
||||
if [ "${1:-https}" = http ]; then
|
||||
BLUEBIRD_ADMIN_PASSWORD=$fixture_password /pkg/install.sh --url http://files.example.test --encrypt --no-start
|
||||
grep -q '^listen = "127.0.0.1:8080"$' /opt/bluebird/config.toml
|
||||
grep -q '^secure_cookies = false$' /opt/bluebird/config.toml
|
||||
runuser -u bluebird -- /opt/bluebird/bluebird check --config /opt/bluebird/config.toml
|
||||
echo 'Public HTTP hostname without a port installed successfully'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${1:-https}" = lan ]; then
|
||||
if BLUEBIRD_ADMIN_PASSWORD=$fixture_password /pkg/install.sh --prefix /opt/rejected --url http://192.168.1.10 --lan-http --no-start >/tmp/rejected-no-port.log 2>&1; then echo 'Portless LAN HTTP accepted' >&2; exit 1; fi
|
||||
grep -q '内网HTTP地址须包含端口' /tmp/rejected-no-port.log
|
||||
if BLUEBIRD_ADMIN_PASSWORD=$fixture_password /pkg/install.sh --prefix /opt/rejected --url http://192.168.1.10:8080 --no-start >/tmp/rejected-lan.log 2>&1; then echo 'LAN HTTP accepted without explicit flag' >&2; exit 1; fi
|
||||
BLUEBIRD_ADMIN_PASSWORD=$fixture_password /pkg/install.sh --url http://192.168.1.10:8080 --lan-http --encrypt --no-start
|
||||
grep -q '^listen = "192.168.1.10:8080"$' /opt/bluebird/config.toml
|
||||
grep -q '^allow_lan_http = true$' /opt/bluebird/config.toml
|
||||
grep -q '^secure_cookies = false$' /opt/bluebird/config.toml
|
||||
echo 'LAN installation and explicit HTTP opt-in passed'
|
||||
exit 0
|
||||
fi
|
||||
# A failed password prompt/init leaves no final data directory or stale stage.
|
||||
if BLUEBIRD_ADMIN_PASSWORD=short /pkg/install.sh --prefix /opt/rejected --url https://files.example.com --no-start >/tmp/rejected-password.log 2>&1; then echo 'Short password accepted' >&2; exit 1; fi
|
||||
[ ! -e /opt/rejected ]
|
||||
[ -z "$(find /opt -maxdepth 1 -name '.bluebird-install-*' -print)" ]
|
||||
BLUEBIRD_ADMIN_PASSWORD=$fixture_password /pkg/install.sh --prefix /opt/bluebird-fixture --lan-url http://127.0.0.1:18995 --access-url http://files.example.com --url https://files.example.com --encrypt --no-start
|
||||
prefix=/opt/bluebird-fixture
|
||||
[ "$(stat -c '%U:%G:%a' "$prefix")" = root:bluebird:750 ]
|
||||
[ "$(stat -c '%U:%G:%a' "$prefix/bluebird")" = root:bluebird:750 ]
|
||||
[ "$(stat -c '%U:%G:%a' "$prefix/config.toml")" = root:bluebird:640 ]
|
||||
[ "$(stat -c '%U:%G:%a' "$prefix/var")" = bluebird:bluebird:700 ]
|
||||
grep -q '^public_url = .*https://files.example.com' "$prefix/config.toml"
|
||||
grep -q '^secure_cookies = true$' "$prefix/config.toml"
|
||||
grep -q 'http://files.example.com' "$prefix/config.toml"
|
||||
grep -q 'http://127.0.0.1:18995' "$prefix/config.toml"
|
||||
grep -q '^allow_lan_http = true$' "$prefix/config.toml"
|
||||
grep -q '^encryption = true$' "$prefix/config.toml"
|
||||
grep -q "^ExecStart=$prefix/bluebird serve --config $prefix/config.toml$" /etc/systemd/system/bluebird.service
|
||||
[ -f "$prefix/var/secrets/age.key" ]
|
||||
if grep -raq "$fixture_password" "$prefix"; then echo 'Plaintext bootstrap password persisted' >&2; exit 1; fi
|
||||
before=$(sha256sum "$prefix/config.toml")
|
||||
if BLUEBIRD_ADMIN_PASSWORD=another-password /pkg/install.sh --prefix "$prefix" --url https://other.example.com --no-start >/tmp/reinstall.log 2>&1; then echo 'Existing installation overwritten' >&2; exit 1; fi
|
||||
[ "$(sha256sum "$prefix/config.toml")" = "$before" ]
|
||||
runuser -u bluebird -- "$prefix/bluebird" check --config "$prefix/config.toml"
|
||||
runuser -u bluebird -- "$prefix/bluebird" serve --config "$prefix/config.toml" >/tmp/bluebird-serve.log 2>&1 &
|
||||
worker_pid=$!
|
||||
ready=false
|
||||
for attempt in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if bash -c 'exec 3<>/dev/tcp/127.0.0.1/18995; printf "GET /api/v1/health HTTP/1.1\r\nHost: files.example.com\r\nConnection: close\r\n\r\n" >&3; cat <&3' >/tmp/health.txt 2>/dev/null && grep -q '"ready":true' /tmp/health.txt; then ready=true; break; fi
|
||||
sleep .1
|
||||
done
|
||||
[ "$ready" = true ] || { cat /tmp/bluebird-serve.log; exit 1; }
|
||||
# Config validation is read-only and may run while the service holds its lock.
|
||||
runuser -u bluebird -- "$prefix/bluebird" validate-config --config "$prefix/config.toml"
|
||||
if runuser -u bluebird -- "$prefix/bluebird" check --config "$prefix/config.toml" >/tmp/locked-check.log 2>&1; then echo 'Maintenance ignored service lock' >&2; exit 1; fi
|
||||
kill "$worker_pid"
|
||||
wait "$worker_pid" || :
|
||||
worker_pid=
|
||||
echo 'Installation, ownership, protected credentials, repeat-install guard and native service passed'
|
||||
@@ -0,0 +1,11 @@
|
||||
import { readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
const notices=['蓝鸟第三方组件许可声明\nGenerated from pinned dependency sources. Some development/test dependencies are included.']
|
||||
function include(label,dir){if(!dir)return;let files;try{files=readdirSync(dir)}catch{return}for(const file of files){if(/^(LICENSE|COPYING)([.\-_]|$)/i.test(file)){try{notices.push(`\n${'='.repeat(72)}\n${label} / ${file}\n${readFileSync(join(dir,file),'utf8')}`)}catch{}}}}
|
||||
const modules=execFileSync('go',['list','-m','-json','all'],{encoding:'utf8'}).trim().split(/\n(?=\{)/).map(s=>JSON.parse(s))
|
||||
for(const module of modules){if(!module.Main)include(`${module.Path}@${module.Version}`,module.Dir)}
|
||||
include('Go runtime / standard library',execFileSync('go',['env','GOROOT'],{encoding:'utf8'}).trim())
|
||||
const lock=JSON.parse(readFileSync('web/package-lock.json','utf8'))
|
||||
for(const [path,entry] of Object.entries(lock.packages)){if(path.startsWith('node_modules/'))include(`${path.slice(13)}@${entry.version}`,join('web',path))}
|
||||
writeFileSync('THIRD_PARTY_NOTICES.txt',notices.join('\n'))
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Verify the native release in an isolated directory on the workspace filesystem."""
|
||||
import http.cookiejar
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import shutil
|
||||
import socket
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
project = Path(__file__).resolve().parent.parent
|
||||
stage = Path(tempfile.mkdtemp(prefix=".bluebird-smoke-", dir=project))
|
||||
server = None
|
||||
try:
|
||||
with tarfile.open(project / "build/bluebird-linux-amd64.tar.gz") as archive:
|
||||
archive.extractall(stage, filter="data")
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
port = sock.getsockname()[1]
|
||||
base = f"http://127.0.0.1:{port}"
|
||||
config = stage / "config.toml"
|
||||
source = (config.read_text().replace("127.0.0.1:8080", f"127.0.0.1:{port}")
|
||||
.replace("http://localhost:8080", base)
|
||||
.replace("min_free_bytes = 1073741824", "min_free_bytes = 0")
|
||||
.replace("encryption = false", "encryption = true"))
|
||||
config.write_text(source)
|
||||
binary = str(stage / "bluebird")
|
||||
password = secrets.token_urlsafe(24)
|
||||
env = os.environ.copy()
|
||||
env["BLUEBIRD_ADMIN_PASSWORD"] = password
|
||||
subprocess.run([binary, "init", "--config", str(config)], env=env, check=True,
|
||||
capture_output=True, timeout=15)
|
||||
server = subprocess.Popen([binary, "serve", "--config", str(config)],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
|
||||
|
||||
def call(method, path, body=None, raw=False):
|
||||
headers = {}
|
||||
if body is not None:
|
||||
if not raw:
|
||||
body = json.dumps(body).encode()
|
||||
headers = {"Origin": base, "Content-Type":
|
||||
"application/octet-stream" if raw else "application/json"}
|
||||
request = urllib.request.Request(base + "/api/v1" + path, data=body,
|
||||
headers=headers, method=method)
|
||||
with opener.open(request, timeout=10) as response:
|
||||
return json.loads(response.read())
|
||||
|
||||
for _ in range(50):
|
||||
try:
|
||||
assert call("GET", "/health")["ready"]
|
||||
break
|
||||
except (OSError, AssertionError):
|
||||
time.sleep(.1)
|
||||
else:
|
||||
raise AssertionError("Release server did not start")
|
||||
with opener.open(base + "/", timeout=10) as response:
|
||||
assert b'<div id="app">' in response.read()
|
||||
call("GET", "/policy")
|
||||
payload = b"bluebird-release-smoke\n" * 4096
|
||||
parcel = call("POST", "/uploads", {
|
||||
"files": [{"name": "first.dat", "size": len(payload)},
|
||||
{"name": "same.dat", "size": len(payload)}],
|
||||
"days": 7, "downloads": 2, "renew": True, "password": ""})
|
||||
for file in parcel["files"]:
|
||||
call("PUT", f"/uploads/{parcel['id']}/files/{file['id']}", payload, True)
|
||||
share = call("POST", f"/uploads/{parcel['id']}/publish", {})
|
||||
call("POST", "/pickup/resolve", {"code": share["code"]})
|
||||
request = urllib.request.Request(
|
||||
base + f"/api/v1/pickup/files/{parcel['files'][0]['id']}/download")
|
||||
with opener.open(request, timeout=10) as response:
|
||||
assert response.read() == payload
|
||||
call("POST", "/admin/login", {"username": "admin", "password": password})
|
||||
stats = call("GET", "/admin/stats")
|
||||
assert stats["objects"] == 1 and stats["files"] == 2
|
||||
server.terminate()
|
||||
server.wait(timeout=10)
|
||||
server = None
|
||||
subprocess.run([binary, "check", "--config", str(config)], check=True,
|
||||
capture_output=True, timeout=15)
|
||||
backup = stage / "backup.tar.gz"
|
||||
subprocess.run([binary, "backup", "--config", str(config), "--output", str(backup)],
|
||||
check=True, capture_output=True, timeout=15)
|
||||
restored = stage / "restore.toml"
|
||||
restored.write_text(source.replace('data_dir = "var"', 'data_dir = "restored-var"'))
|
||||
subprocess.run([binary, "restore", "--config", str(restored), "--input", str(backup)],
|
||||
check=True, capture_output=True, timeout=15)
|
||||
subprocess.run([binary, "check", "--config", str(restored)], check=True,
|
||||
capture_output=True, timeout=15)
|
||||
with sqlite3.connect(stage / "restored-var/db/bluebird.db") as db:
|
||||
assert db.execute("SELECT state,code_hash FROM parcels").fetchone() == ("restored", None)
|
||||
assert db.execute("SELECT count(*) FROM grants").fetchone()[0] == 0
|
||||
assert db.execute("SELECT count(*) FROM objects").fetchone()[0] == 1
|
||||
print("发布包验证通过:初始化、内嵌前端、匿名去重、加密下载、管理、检查、备份恢复。")
|
||||
finally:
|
||||
if server is not None:
|
||||
server.terminate()
|
||||
try:
|
||||
server.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
server.kill()
|
||||
server.wait()
|
||||
shutil.rmtree(stage)
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Exercise a real previous installation inside a disposable rootless container.
|
||||
Requires podman, the old tarball argument and the newly built amd64 release.
|
||||
"""
|
||||
import http.cookies
|
||||
import json
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
project = Path(__file__).resolve().parent.parent
|
||||
name = 'bluebird-upgrade-' + secrets.token_hex(4)
|
||||
password = 'upgrade-preserved-password'
|
||||
base = 'http://127.0.0.1:18996'
|
||||
origin = 'https://files.example.test'
|
||||
|
||||
def run(*args, **kw):
|
||||
return subprocess.run(args, check=True, capture_output=True, text=True, **kw).stdout
|
||||
|
||||
def inside(*args, **kw):
|
||||
return run('podman', 'exec', name, *args, **kw)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='bluebird-upgrade-old-') as old:
|
||||
with tarfile.open(sys.argv[1]) as archive:
|
||||
archive.extractall(old, filter='data')
|
||||
try:
|
||||
run('podman', 'run', '--rm', '-d', '--security-opt', 'label=disable', '--name', name,
|
||||
'-p', '127.0.0.1:18996:18996', '-v', old+':/oldpkg:ro',
|
||||
'-v', str(project/'build/linux-amd64')+':/pkg:ro',
|
||||
'docker.io/library/debian:bookworm-slim', 'sleep', '300')
|
||||
inside('env', 'BLUEBIRD_ADMIN_PASSWORD='+password, '/oldpkg/install.sh',
|
||||
'--url', origin, '--listen', '0.0.0.0:18996', '--encrypt', '--no-start')
|
||||
inside('sed','-i','s/min_free_bytes = 1073741824/min_free_bytes = 0/', '/opt/bluebird/config.toml')
|
||||
key_before = inside('sha256sum', '/opt/bluebird/var/secrets/age.key')
|
||||
cookies = {}
|
||||
def call(method, path, body=None):
|
||||
data = body if isinstance(body, bytes) else json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(base+'/api/v1'+path, data=data, method=method, headers={
|
||||
'Origin': origin, 'X-Bluebird-Origin': origin,
|
||||
'Cookie': '; '.join(k+'='+v for k,v in cookies.items()),
|
||||
'Content-Type': 'application/octet-stream' if isinstance(body,bytes) else 'application/json'})
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
for line in response.headers.get_all('Set-Cookie',[]):
|
||||
parsed = http.cookies.SimpleCookie(); parsed.load(line)
|
||||
cookies.update({k:v.value for k,v in parsed.items()})
|
||||
result=response.read()
|
||||
return result if path.endswith('/download') else json.loads(result)
|
||||
def start():
|
||||
subprocess.run(['podman','exec','-d',name,'runuser','-u','bluebird','--','/opt/bluebird/bluebird','serve','--config','/opt/bluebird/config.toml'],check=True)
|
||||
for _ in range(50):
|
||||
try:
|
||||
if call('GET','/health')['ready']: return
|
||||
except OSError: time.sleep(.1)
|
||||
raise AssertionError('Container service not ready')
|
||||
start()
|
||||
call('GET','/policy')
|
||||
parcel=call('POST','/uploads',{'files':[{'name':'preserved.txt','size':21}],'days':7,'downloads':5,'renew':True})
|
||||
call('PUT',f"/uploads/{parcel['id']}/files/{parcel['files'][0]['id']}", b'old encrypted payload')
|
||||
share=call('POST',f"/uploads/{parcel['id']}/publish",{})
|
||||
call('POST','/admin/login',{'username':'admin','password':password})
|
||||
before=inside('sha256sum','/opt/bluebird/config.toml','/opt/bluebird/bluebird')
|
||||
# Invalid candidates must not stop a running service or change files.
|
||||
bad=subprocess.run(['podman','exec',name,'/pkg/upgrade.sh','--access-url','http://bad.test/path','--no-start'],capture_output=True)
|
||||
assert bad.returncode != 0
|
||||
assert before == inside('sha256sum','/opt/bluebird/config.toml','/opt/bluebird/bluebird')
|
||||
assert call('GET','/health')['ready']
|
||||
# No pgrep dependency: /proc/comm identifies only the native service.
|
||||
inside('sh','-c','for p in /proc/[0-9]*; do [ "$(cat "$p/comm" 2>/dev/null)" != bluebird ] || kill -TERM "${p##*/}"; done')
|
||||
for _ in range(50):
|
||||
try: call('GET','/health')
|
||||
except OSError: break
|
||||
time.sleep(.1)
|
||||
# Simulate a new-version integrity-check failure after files were replaced.
|
||||
inside('sh','-c','mkdir /failpkg; cp /pkg/bluebird /failpkg/bluebird.real; cp /pkg/ARCH /pkg/upgrade.sh /failpkg/')
|
||||
wrapper='#!/bin/sh\nif [ "$1" = check ]; then exit 1; fi\nexec "$(dirname "$0")/bluebird.real" "$@"\n'
|
||||
inside('sh','-c','printf "%s" "$1" > /failpkg/bluebird; chmod 755 /failpkg/bluebird','wrapper',wrapper)
|
||||
failed=subprocess.run(['podman','exec',name,'/failpkg/upgrade.sh','--lan-url',base,'--listen','0.0.0.0:18996','--no-start'],capture_output=True)
|
||||
assert failed.returncode != 0 and '已恢复旧程序和配置' in failed.stderr.decode()
|
||||
assert before == inside('sha256sum','/opt/bluebird/config.toml','/opt/bluebird/bluebird')
|
||||
result=inside('/pkg/upgrade.sh','--lan-url',base,'--listen','0.0.0.0:18996',
|
||||
'--access-url','http://files.example.test','--no-start')
|
||||
assert '升级完成' in result
|
||||
assert key_before == inside('sha256sum','/opt/bluebird/var/secrets/age.key')
|
||||
assert inside('stat','-c','%U:%G:%a','/opt/bluebird/config.toml').strip() == 'root:bluebird:640'
|
||||
assert 'data.tar.gz' in inside('find','/opt/bluebird/backups','-type','f')
|
||||
start();cookies.clear()
|
||||
call('POST','/admin/login',{'username':'admin','password':password})
|
||||
assert call('GET','/admin/stats')['objects'] == 1
|
||||
policy=call('GET','/policy');assert set(policy['access_urls']) == {origin,base,'http://files.example.test'}
|
||||
for entrance in [origin,base,'http://files.example.test']:
|
||||
origin=entrance;cookies.clear()
|
||||
call('POST','/pickup/resolve',{'code':share['code']})
|
||||
assert call('GET',f"/pickup/files/{parcel['files'][0]['id']}/download") == b'old encrypted payload'
|
||||
print('升级验证通过:旧管理员密码、加密文件、密钥及取件码保留;三入口下载、权限、备份、非法配置保护及检查失败回滚通过。')
|
||||
except subprocess.CalledProcessError as error:
|
||||
print(error.stdout, error.stderr, file=sys.stderr)
|
||||
raise
|
||||
finally:
|
||||
subprocess.run(['podman','rm','-f',name],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
Reference in new issue
Block a user