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

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

+157
View File
@@ -0,0 +1,157 @@
package app
import (
"bytes"
"errors"
"fmt"
"net"
"net/url"
"os"
"path/filepath"
"github.com/pelletier/go-toml/v2"
)
type Policy struct {
AnonymousUpload bool `json:"anonymous_upload" toml:"anonymous_upload"`
AnonymousDownload bool `json:"anonymous_download" toml:"anonymous_download"`
MaxFileBytes int64 `json:"max_file_bytes" toml:"max_file_bytes"`
MaxBatchBytes int64 `json:"max_batch_bytes" toml:"max_batch_bytes"`
MaxFiles int `json:"max_files" toml:"max_files"`
DefaultDays int `json:"default_days" toml:"default_days"`
MaxInitialDays int `json:"max_initial_days" toml:"max_initial_days"`
MaxLifetimeDays int `json:"max_lifetime_days" toml:"max_lifetime_days"`
DefaultDownloads int `json:"default_downloads" toml:"default_downloads"`
MaxDownloads int `json:"max_downloads" toml:"max_downloads"`
RenewalDays int `json:"renewal_days" toml:"renewal_days"`
RenewalMode string `json:"renewal_mode" toml:"renewal_mode"`
IPDailyBytes int64 `json:"ip_daily_bytes" toml:"ip_daily_bytes"`
IPDailyUploads int `json:"ip_daily_uploads" toml:"ip_daily_uploads"`
SessionDailyUploads int `json:"session_daily_uploads" toml:"session_daily_uploads"`
RequirePassword bool `json:"require_password" toml:"require_password"`
}
type Config struct {
Listen string `toml:"listen"`
PublicURL string `toml:"public_url"`
AccessURLs []string `toml:"access_urls"`
DataDir string `toml:"data_dir"`
SecureCookies bool `toml:"secure_cookies"`
AllowLANHTTP bool `toml:"allow_lan_http"`
Encryption bool `toml:"encryption"`
Deduplication bool `toml:"deduplication"`
TrustedProxies []string `toml:"trusted_proxies"`
DataBudget int64 `toml:"data_budget"`
FileBudget int64 `toml:"file_budget"`
MinFreeBytes int64 `toml:"min_free_bytes"`
AuditRetentionDays int `toml:"audit_retention_days"`
AuditMaxRecords int `toml:"audit_max_records"`
AuditMaxBytes int64 `toml:"audit_max_bytes"`
MaxUploads int `toml:"max_uploads"`
MaxDownloads int `toml:"max_downloads"`
MaxIPUploads int `toml:"max_ip_uploads"`
MaxIPDownloads int `toml:"max_ip_downloads"`
MaxRecords int `toml:"max_records"`
TransferIdleSeconds int `toml:"transfer_idle_seconds"`
MaxTransferSeconds int `toml:"max_transfer_seconds"`
TLSCert string `toml:"tls_cert"`
TLSKey string `toml:"tls_key"`
Policy Policy `toml:"policy"`
}
func DefaultConfig() Config {
return Config{
Listen: "127.0.0.1:8080", PublicURL: "http://localhost:8080", DataDir: "var", Deduplication: true,
DataBudget: 10 << 30, FileBudget: 9 << 30, MinFreeBytes: 1 << 30, MaxUploads: 8, MaxDownloads: 16, MaxIPUploads: 2, MaxIPDownloads: 4, MaxRecords: 200000, MaxTransferSeconds: 3600, TransferIdleSeconds: 30,
AuditRetentionDays: 30, AuditMaxRecords: 100000, AuditMaxBytes: 32 << 20,
Policy: Policy{AnonymousUpload: true, AnonymousDownload: true, MaxFileBytes: 100 << 20, MaxBatchBytes: 500 << 20, MaxFiles: 10, DefaultDays: 1, MaxInitialDays: 7, MaxLifetimeDays: 30, DefaultDownloads: 5, MaxDownloads: 20, RenewalDays: 3, RenewalMode: "add", IPDailyBytes: 1 << 30, IPDailyUploads: 20, SessionDailyUploads: 20},
}
}
func (p Policy) Validate() error {
if p.MaxBatchBytes > 1<<40 || p.IPDailyBytes > 1<<50 || p.MaxFileBytes <= 0 || p.MaxBatchBytes <= 0 || p.MaxFiles < 1 || p.MaxFiles > 1000 || p.MaxFileBytes > p.MaxBatchBytes || p.DefaultDays < 1 || p.DefaultDays > p.MaxInitialDays || p.MaxInitialDays > p.MaxLifetimeDays || p.MaxLifetimeDays > 3650 || p.DefaultDownloads < 1 || p.DefaultDownloads > p.MaxDownloads || p.MaxDownloads > 100000 || p.RenewalDays < 0 || p.RenewalDays > p.MaxLifetimeDays || p.IPDailyBytes < 1 || p.IPDailyUploads < 1 || p.SessionDailyUploads < 1 {
return errors.New("策略参数范围错误")
}
if p.RenewalMode != "add" && p.RenewalMode != "sliding" {
return errors.New("renewal_mode 必须为 add 或 sliding")
}
return nil
}
func LoadConfig(path string) (Config, error) {
c := DefaultConfig()
b, e := os.ReadFile(path)
if e != nil {
return c, e
}
if e = toml.NewDecoder(bytes.NewReader(b)).DisallowUnknownFields().Decode(&c); e != nil {
return c, e
}
base, e := filepath.Abs(filepath.Dir(path))
if e != nil {
return c, e
}
if !filepath.IsAbs(c.DataDir) {
c.DataDir = filepath.Join(base, c.DataDir)
}
for _, ptr := range []*string{&c.TLSCert, &c.TLSKey} {
if *ptr != "" && !filepath.IsAbs(*ptr) {
*ptr = filepath.Join(base, *ptr)
}
}
return c, c.Validate()
}
func (c Config) Validate() error {
if e := c.InitialRuntimeSettings().Validate(); e != nil {
return e
}
if e := c.Policy.Validate(); e != nil {
return e
}
primary, e := normalizeAccessURL(c.PublicURL)
if e != nil {
return e
}
u, _ := url.Parse(primary)
if len(c.AccessURLs) > 16 {
return errors.New("额外访问地址最多16个")
}
for _, raw := range append([]string{c.PublicURL}, c.AccessURLs...) {
base, err := normalizeAccessURL(raw)
if err != nil {
return err
}
entry, _ := url.Parse(base)
ip := net.ParseIP(entry.Hostname())
if entry.Scheme == "http" && ip != nil && ip.IsPrivate() && !ip.IsLoopback() && !c.AllowLANHTTP {
return errors.New("内网HTTP入口须显式开启 allow_lan_http")
}
}
if u.Scheme == "https" && !c.SecureCookies {
return errors.New("HTTPS主入口须设置 secure_cookies = true")
}
if u.Scheme == "http" && c.SecureCookies {
return errors.New("HTTP主入口须设置 secure_cookies = false;额外HTTPS入口自动使用安全cookie")
}
listenHost, port, err := net.SplitHostPort(c.Listen)
if err != nil || port == "" {
return errors.New("listen须包含地址和端口")
}
// Preserve explicit-address protection for a single local/LAN HTTP site.
ip := net.ParseIP(u.Hostname())
if u.Scheme == "http" && (u.Hostname() == "localhost" || ip != nil && (ip.IsPrivate() || ip.IsLoopback())) {
bound := net.ParseIP(listenHost)
if listenHost != "localhost" && (bound == nil || !bound.IsPrivate() && !bound.IsLoopback()) {
return errors.New("内网HTTP须监听具体私有或回环地址")
}
}
if (c.TLSCert == "") != (c.TLSKey == "") {
return errors.New("TLS 证书和私钥需一起配置")
}
if c.FileBudget <= 0 || c.DataBudget-c.FileBudget < 64<<20 || c.MinFreeBytes < 0 || c.MaxUploads < 1 || c.MaxDownloads < 1 || c.MaxIPUploads < 1 || c.MaxIPDownloads < 1 || c.MaxRecords < 1 || c.MaxTransferSeconds < 1 || c.TransferIdleSeconds < 1 {
return fmt.Errorf("容量、并发或传输配置非法")
}
for _, p := range c.TrustedProxies {
if _, _, e := net.ParseCIDR(p); e != nil {
return fmt.Errorf("非法代理网段: %s", p)
}
}
return nil
}