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

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

+171
View File
@@ -0,0 +1,171 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"bluebird/internal/app"
"bluebird/internal/web"
"golang.org/x/term"
)
type accessURLFlags []string
func (v *accessURLFlags) String() string { return strings.Join(*v, ",") }
func (v *accessURLFlags) Set(raw string) error { *v = append(*v, raw); return nil }
func main() {
if e := run(); e != nil {
log.Print(e)
os.Exit(1)
}
}
func run() error {
if len(os.Args) < 2 {
return errors.New("用法: bluebird init|serve|validate-config|configure-access|check|cleanup|backup|restore|reset-password --config config.toml")
}
command := os.Args[1]
flags := flag.NewFlagSet(command, flag.ContinueOnError)
config := flags.String("config", "config.toml", "配置文件")
username := flags.String("username", "admin", "管理员名称")
output := flags.String("output", "", "备份输出路径")
input := flags.String("input", "", "备份输入路径")
var accessURLs accessURLFlags
flags.Var(&accessURLs, "access-url", "增加访问入口,可重复")
listen := flags.String("listen", "", "访问监听地址")
lanURL := flags.String("lan-url", "", "内网直连入口")
allowLAN := flags.Bool("allow-lan-http", false, "允许明确配置的内网HTTP入口")
if e := flags.Parse(os.Args[2:]); e != nil {
return e
}
if command == "configure-access" {
if *lanURL != "" {
address, base, err := app.LANListen(*lanURL)
if err != nil {
return err
}
if *listen == "" {
*listen = address
}
accessURLs = append(accessURLs, base)
*allowLAN = true
}
if e := app.UpdateAccessConfig(*config, *listen, accessURLs, *allowLAN); e != nil {
return e
}
fmt.Println("访问入口配置已更新")
return nil
}
c, e := app.LoadConfig(*config)
if e != nil {
return e
}
if command == "validate-config" {
fmt.Println("配置检查通过")
return nil
}
if command == "restore" {
if *input == "" {
return errors.New("需指定 --input")
}
return app.Restore(c, *input)
}
a, e := app.Open(c)
if e != nil {
return e
}
defer a.Close()
switch command {
case "init", "reset-password":
if command == "init" && a.Initialized() {
return errors.New("已初始化;重置请使用 reset-password")
}
password := os.Getenv("BLUEBIRD_ADMIN_PASSWORD")
if password == "" {
if !term.IsTerminal(int(os.Stdin.Fd())) {
return errors.New("需使用交互终端或 BLUEBIRD_ADMIN_PASSWORD 环境变量")
}
fmt.Print("管理员密码(至少8字节): ")
b, e := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if e != nil {
return e
}
password = string(b)
fmt.Print("再次输入: ")
again, e := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if e != nil {
return e
}
if password != string(again) {
return errors.New("两次密码不同")
}
}
if e = a.SetAdmin(*username, password); e != nil {
return e
}
fmt.Println("管理员配置完成")
return nil
case "check":
if e = a.Check(); e != nil {
return e
}
fmt.Println("数据库与实体内容完整性检查通过")
return nil
case "cleanup":
return a.Cleanup()
case "backup":
if *output == "" {
return errors.New("需指定 --output")
}
return a.Backup(*config, *output)
case "serve":
if !a.Initialized() {
return errors.New("请先执行 init")
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()
maintenanceStop := make(chan struct{})
maintenanceDone := make(chan struct{})
go func() { defer close(maintenanceDone); a.RunMaintenance(maintenanceStop) }()
server := &http.Server{Addr: c.Listen, Handler: a.Handler(web.Handler()), ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 32 << 10}
result := make(chan error, 1)
go func() {
if c.TLSCert != "" {
result <- server.ListenAndServeTLS(c.TLSCert, c.TLSKey)
} else {
result <- server.ListenAndServe()
}
}()
log.Printf("蓝鸟正在监听 %s (%s)", c.Listen, strings.TrimRight(c.PublicURL, "/"))
select {
case e = <-result:
case <-ctx.Done():
shutdown, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
e = server.Shutdown(shutdown)
if e != nil {
_ = server.Close()
}
}
close(maintenanceStop)
<-maintenanceDone
a.WaitRequests()
if errors.Is(e, http.ErrServerClosed) {
return nil
}
return e
default:
return errors.New("未知命令")
}
}
+38
View File
@@ -0,0 +1,38 @@
package main
import (
"os"
"path/filepath"
"testing"
"bluebird/internal/app"
"github.com/pelletier/go-toml/v2"
)
func TestValidateConfigDoesNotCreateRuntimeData(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.toml")
args := os.Args
t.Cleanup(func() { os.Args = args })
os.Args = []string{"bluebird", "validate-config", "--config", path}
cfg := app.DefaultConfig()
for _, invalid := range []bool{false, true} {
if invalid {
cfg.PublicURL = "https://files.example.com"
}
encoded, err := toml.Marshal(cfg)
if err != nil {
t.Fatal(err)
}
if err = os.WriteFile(path, encoded, 0600); err != nil {
t.Fatal(err)
}
err = run()
if (err != nil) != invalid {
t.Fatalf("validation error=%v invalid=%v", err, invalid)
}
if _, err = os.Stat(filepath.Join(dir, "var")); !os.IsNotExist(err) {
t.Fatal("configuration validation initialized runtime data")
}
}
}