初始化蓝鸟文件快递柜源码与部署文档

This commit is contained in:
lhc committed 2026-10-05 23:41:07 +08:00
commit 5643d22242
70 files changed
+18669

No files matched your search

+33
View File
@@ -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)
+15
View File
@@ -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()}
+72
View File
@@ -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'
+11
View File
@@ -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'))
+109
View File
@@ -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)
+103
View File
@@ -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)