mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-11-13 06:00:47 +08:00
* feat(func): media library
* feat(func): media library
* feat(func): media library
* feat(func): media library build test
* feat(func): media library build test
* feat(func): media library fix remote fetch error
* feat(func): media library fix remote fetch error
* feat(func): media library fix remote fetch error
* feat(func): media library support select
* feat(func): media library support select
* feat(func): media library support select
* feat(func): media library support select
* feat(func): media library support change api url
* feat(func): media library support change api url
* feat(func): media library support change api url
* feat(media): 并发刮削(默认 5)+ 单条落库
增强 TMDB 搜索:模糊匹配 + 多候选关键词
管理页面新增导入/导出媒体库(全部 / 按扫描路径)
缩略图路径默认值改为 imgs
* feat(media): 清空刮削、删除已失效
* fix(media): fix api proxy path
* fix(media): fix lrc file display & music id3
* fix(media): fix media scan sub path error
* feat(media): support cloud codec via ffmpeg
* feat(media): support cloud codec via ffmpeg
* feat(media): support cloud codec via ffmpeg
* feat(media): support cloud codec via ffmpeg
* feat(media): 提升刮削精准度,支持模糊刮削
* fix(media): clean up imports and improve FetchFileReader logic
* feat(media): 提升刮削精准度,支持模糊刮削
* feat(media): 刮削支持自定义刮削数量,避免太占性能
* feat(media): 刮削支持自定义刮削数量,避免太占性能
* feat(media): 刮削修复写入作品名问题
* fix(media): return 404 on missing item_id and fix unscraped item query
- Validate GetMediaItemByID result before spawning goroutine in
StartMediaScrape; return 404 when item_id > 0 but item is not found
- Replace `description` check with `plot` check in GetUnscrappedItems
since scrapers populate Plot, not Description, to avoid re-selecting
already-scraped items on every run
* Apply remaining changes
* feat(media): add master switch, app keys, STRM, Emby and OpenSubsonic APIs
* feat(media): allow any logged-in user to manage their own media app keys
* feat(media): allow signing arbitrary media path for local player
* fix(media): always return an array for media status types
* fix(media): persist per-type scraper config and apply it to thumbnails
* feat(media): add local thumbnail endpoint and cover proxy with fallback gateway
* fix(media): allow guest media access, per-file local thumbnails and optional cover proxy
* feat(media): thumbnail storage modes and STRM same-directory output
* feat(media): Google Books/OpenLibrary sources and remote cover download
* chore(media): rename transcode cache dir to ctmp and log thumbnail storage dir
* feat(media): progress and favorites API shared with Emby/Subsonic
* chore: ignore runtime thumbnail directory
* feat(media): add preferred scrape language and namespace protocol endpoints under @
* fix(media): harden anonymous emby images, pagination and cover proxy SSRF
* fix(emby): allow anonymous user discovery and login before auth
* chore: drop redundant media build workflow and protocol doc
* fix(media): ID3v2.4 syncsafe frame size and preserve Hidden flag on rescan
* fix(media): address security review (path traversal, guest bypass, cover XSS, auth rate limit, SSRF, quotas, migration)
* fix(media): 清理失效条目时只删除确认不存在的记录
此前 fs.Get 返回任何错误都视为失效并硬删除,驱动故障/超时/鉴权失败时会误删有效媒体记录。现在仅 errs.IsObjectNotFound 才删除,其余错误跳过并在响应中返回 skipped 计数。
* fix(transcode): 播放凭据与 Worker 回写凭据分离,并收紧 profile 与鉴权校验
- 新增 Job.PlayToken(json 标签为 -),master/playlist/切片只认播放凭据,播放器不再拿到可上传、可结束任务的 callback_token
- Worker 上传切片时校验 profile 必须属于任务声明的档位
- TCMaster 与 WorkerJobFinish 改用恒定时间比较,避免时序侧信道
* fix(transcode): 含冒号的本地文件名可转码,补基址缺失与重新激活配额
- ValidSourceInput 不再按冒号位置判协议:本地输入只要求以 / 开头或 Windows 盘符路径,concat:/subfile: 等前缀仍被拒且由 -protocol_whitelist 兜底(修复 /mnt/x/Star Wars: IV.mkv 被误判)
- trustedInternalBaseURL 返回空串时直接 503 并给出明确原因,不再拼出 /d/xxx 让 ffmpeg 报文件不存在
- 远程输入白名单收窄为 http,https,tcp,tls(去掉 file 与 crypto)
- Reactivate 复用同一套单用户配额,避免「先让任务闲置取消、再批量激活」绕过 SubmitLimited;配额不足返回 429
- OwnerID 改为配额键:普通用户 user:<id>,访客 guest:<clientIP>,避免单个匿名访客占满全部匿名配额;补充对应单测
* fix(media): 收紧缩略图解码内存、跳过超长条目可见化,并补 CI 单测
- MaxImagePixels 从 64MP 降到 32MP,宽高改 int64 相乘防 32 位溢出,解码阶段加 12 路全局信号量
- 封面解码失败退回原始字节时限制 512KB,避免十几 MB 的 base64 写进 cover 列(豆瓣/本地书籍三处)
- 扫描进度新增 Skipped 计数,入库被跳过的超长路径会在完成消息与日志中体现
- 数据库迁移失败日志补充 OPENLIST_MEDIA_MIGRATE_WIPE=1 的处置提示
- Subsonic 认证只对「确实带了凭据却校验失败」计数,裸探测不再刷新 IP 锁定
- goexif 从 indirect 调整为直接依赖(internal/media/scraper/image.go 直接 import)
- CI 新增 go test ./internal/db/... ./internal/transcode/... ./internal/media/...
* fix(transcode): 配额/队列满时仍放行已缓存切片,并同步 worker 协议白名单
- TCSegment/TCPlaylist: Reactivate 返回 ErrUserQuota/ErrQueueFull 时不再直接 429,改为标记 quotaBlocked,缓存命中的切片与 playlist 仍可读,只有缓存缺失才返回 429
- Reactivate: 补充全局排队上限 (MaxPendingJobs) 检查,避免批量激活顶爆队列,并补单测
- cmd/worker: 远程输入协议白名单与 master 同步为 http,https,tcp,tls(去掉 file/crypto)
- scraper: decodeSem 容量 12 -> 3,避免信号量大于默认刮削并发而形同失效
* feat(emby): 完善客户端兼容层并修复认证锁定,Subsonic 复用统一密钥校验
- Emby: 用户 ID 改用 GUID 并兼容数字/旧 ID;补齐 /Users/{id}/Authenticate、Sessions 与 WebSocket、首页分区、合集/播放列表、字幕流与歌词、Series/Season 结构
- Emby: 版本号返回客户端可接受的兼容版本,/Users/Public 返回可登录用户以渲染用户选择
- Subsonic: 支持 enc: 十六进制密码编码;app key 校验与 Emby 共用 LookUpAppKeyUser
- 限流: 认证被拒时不再刷新锁定过期时间,避免客户端持续轮询把 IP 永久锁死
* fix(media): 认证失败计数增加用户名维度,防止伪造 X-Forwarded-For 绕过锁定
- media_ratelimit: 新增 mediaAuthLockedFor / mediaAuthFailedFor / mediaAuthSucceededFor,IP 与用户名双维度计数;达到上限同样锁定 5 分钟且被拒时不续期
- emby: 按用户名登录与按 id 登录都检查用户名维度;失败计入两个维度,成功清除两个维度
- subsonic: subAuth 与 429 分支改用双维度判定;成功时用真实用户名清理计数(apiKey 认证时 u 参数为空)
- 仍保留主干 5 次/5 分钟的策略,代理信任范围(SetTrustedProxies)留给项目层面单独处理
* fix(media): 匿名封面按访客身份过滤,认证日志脱敏并收紧限流计数
- embyImage: 匿名封面不再走 loadEmbyLibraryAll(全库可见),改为 loadEmbyLibrary(guest):
hidden / base_path 之外 / meta 受保护的条目都不可见,访客被禁用时直接 401
- embyUnauthorized: 只记录 Path、UA 与是否带 token,避免 query 里的 Pw/Password/api_key
和 X-Emby-Authorization 里的设备令牌写进日志
- media_ratelimit: 失败计数 Set 时带 5min TTL,避免 IP/用户名 key 无限增长(用户名来自请求参数)
- auth.loginHash: 锁定期间不再续期,失败计数同样带 TTL,与媒体侧语义一致
- /System/Endpoint: 按 RemoteAddr 判断 IsLocal/IsInNetwork;/System/Info 不再返回真实监听端口
* feat(media): PDF 批注持久化
- 新增 MediaAnnotation 模型:按 (user_id, item_id, remote_id) 唯一,anchor 原样保存查看器批注对象,不重复建模页码/坐标
- GET/POST /fs/media/annotations:读取当前用户批注、按 remote_id 增量 upsert/delete(单条 512KB、单次 8MB、最多 500 条)
- 批注为用户私有数据:未登录返回空列表,访客写入 403,并校验条目可见性与访问权限
- 条目删除 / 清空类型 / 清空扫描路径 / 批量删除时,在同一事务内清理对应批注
* fix(media): 选集签名前校验最终文件权限;转码在 ffprobe 前预检配额
- GetMediaStreamURL / GetMediaStrm: 解析选集后对最终 rawPath 再执行 CanUserAccessPath,避免「目录可见但 meta 隐藏规则禁止其中某个文件」时仍能拿到有效签名(签名之后下载链路不会重跑隐藏规则)
- TranscodePlay: ProbeSource 之前调用 Scheduler.Capacity 预检配额,已满直接 429,不再先起最长 30s 的 ffprobe
- 新增探测并发闸门(全局 8 / 每用户 3),覆盖预检与 SubmitLimited 之间的并发窗口;用 defer 释放,探测 panic 也不会占死槽位
- 补 Capacity 单测
---------
Co-authored-by: j2rong4cn <j2rong@qq.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: pikachuren <pikachuren@example.com>
410 lines
11 KiB
Go
410 lines
11 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/OpenListTeam/OpenList/v4/cmd/flags"
|
|
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/data"
|
|
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
|
"github.com/OpenListTeam/OpenList/v4/internal/db"
|
|
"github.com/OpenListTeam/OpenList/v4/internal/fs"
|
|
"github.com/OpenListTeam/OpenList/v4/internal/transcode"
|
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
|
"github.com/OpenListTeam/OpenList/v4/server"
|
|
"github.com/OpenListTeam/OpenList/v4/server/middlewares"
|
|
"github.com/OpenListTeam/sftpd-openlist"
|
|
ftpserver "github.com/fclairamb/ftpserverlib"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
"github.com/quic-go/quic-go/http3"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/net/http2"
|
|
"golang.org/x/net/http2/h2c"
|
|
)
|
|
|
|
func Init() {
|
|
InitConfig()
|
|
Log()
|
|
InitDB()
|
|
data.InitData()
|
|
InitStreamLimit()
|
|
InitIndex()
|
|
InitUpgradePatch()
|
|
}
|
|
|
|
func Release() {
|
|
db.Close()
|
|
}
|
|
|
|
var (
|
|
running bool
|
|
httpSrv *http.Server
|
|
httpRunning bool
|
|
httpsSrv *http.Server
|
|
httpsRunning bool
|
|
unixSrv *http.Server
|
|
unixRunning bool
|
|
quicSrv *http3.Server
|
|
quicRunning bool
|
|
s3Srv *http.Server
|
|
s3Running bool
|
|
ftpDriver *server.FtpMainDriver
|
|
ftpServer *ftpserver.FtpServer
|
|
ftpRunning bool
|
|
sftpDriver *server.SftpDriver
|
|
sftpServer *sftpd.SftpServer
|
|
sftpRunning bool
|
|
)
|
|
|
|
// Called by OpenList-Mobile
|
|
func IsRunning(t string) bool {
|
|
switch t {
|
|
case "http":
|
|
return httpRunning
|
|
case "https":
|
|
return httpsRunning
|
|
case "unix":
|
|
return unixRunning
|
|
case "quic":
|
|
return quicRunning
|
|
case "s3":
|
|
return s3Running
|
|
case "sftp":
|
|
return sftpRunning
|
|
case "ftp":
|
|
return ftpRunning
|
|
}
|
|
return running
|
|
}
|
|
|
|
func Start() {
|
|
if conf.Conf.DelayedStart != 0 {
|
|
utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart)
|
|
time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)
|
|
}
|
|
InitOfflineDownloadTools()
|
|
LoadStorages()
|
|
InitTaskManager()
|
|
// 初始化转码模块(总开关关时也会启动 manager,但本地 worker 仅在 enabled 时由 Manager.Start 内部按 run_mode 启动)
|
|
if transcode.IsEnabled() {
|
|
transcode.Default().Start()
|
|
}
|
|
if !flags.Debug && !flags.Dev {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
r := gin.New()
|
|
|
|
// gin log
|
|
if conf.Conf.Log.Filter.Enable {
|
|
r.Use(middlewares.FilteredLogger())
|
|
} else {
|
|
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out))
|
|
}
|
|
r.Use(gin.RecoveryWithWriter(log.StandardLogger().Out))
|
|
|
|
server.Init(r)
|
|
var httpHandler http.Handler = r
|
|
if conf.Conf.Scheme.EnableH2c {
|
|
httpHandler = h2c.NewHandler(r, &http2.Server{})
|
|
}
|
|
if conf.Conf.Scheme.HttpPort != -1 {
|
|
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
|
|
fmt.Printf("start HTTP server @ %s\n", httpBase)
|
|
utils.Log.Infof("start HTTP server @ %s", httpBase)
|
|
httpSrv = &http.Server{Addr: httpBase, Handler: httpHandler}
|
|
go func() {
|
|
httpRunning = true
|
|
err := httpSrv.ListenAndServe()
|
|
httpRunning = false
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
handleEndpointStartFailedHooks("http", err)
|
|
utils.Log.Errorf("failed to start http: %s", err.Error())
|
|
} else {
|
|
handleEndpointShutdownHooks("http")
|
|
}
|
|
}()
|
|
}
|
|
if conf.Conf.Scheme.HttpsPort != -1 {
|
|
httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)
|
|
fmt.Printf("start HTTPS server @ %s\n", httpsBase)
|
|
utils.Log.Infof("start HTTPS server @ %s", httpsBase)
|
|
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
|
|
go func() {
|
|
httpsRunning = true
|
|
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
|
httpsRunning = false
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
handleEndpointStartFailedHooks("https", err)
|
|
utils.Log.Errorf("failed to start https: %s", err.Error())
|
|
} else {
|
|
handleEndpointShutdownHooks("https")
|
|
}
|
|
}()
|
|
if conf.Conf.Scheme.EnableH3 {
|
|
fmt.Printf("start HTTP3 (quic) server @ %s\n", httpsBase)
|
|
utils.Log.Infof("start HTTP3 (quic) server @ %s", httpsBase)
|
|
r.Use(func(c *gin.Context) {
|
|
if c.Request.TLS != nil {
|
|
port := conf.Conf.Scheme.HttpsPort
|
|
c.Header("Alt-Svc", fmt.Sprintf("h3=\":%d\"; ma=86400", port))
|
|
}
|
|
c.Next()
|
|
})
|
|
quicSrv = &http3.Server{Addr: httpsBase, Handler: r}
|
|
go func() {
|
|
quicRunning = true
|
|
err := quicSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
|
quicRunning = false
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
handleEndpointStartFailedHooks("quic", err)
|
|
utils.Log.Errorf("failed to start http3 (quic): %s", err.Error())
|
|
} else {
|
|
handleEndpointShutdownHooks("quic")
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
if conf.Conf.Scheme.UnixFile != "" {
|
|
fmt.Printf("start unix server @ %s\n", conf.Conf.Scheme.UnixFile)
|
|
utils.Log.Infof("start unix server @ %s", conf.Conf.Scheme.UnixFile)
|
|
unixSrv = &http.Server{Handler: httpHandler}
|
|
go func() {
|
|
listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
|
|
if err != nil {
|
|
utils.Log.Errorf("failed to listen unix: %+v", err)
|
|
return
|
|
}
|
|
unixRunning = true
|
|
// set socket file permission
|
|
mode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)
|
|
if err != nil {
|
|
utils.Log.Errorf("failed to parse socket file permission: %+v", err)
|
|
} else {
|
|
err = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))
|
|
if err != nil {
|
|
utils.Log.Errorf("failed to chmod socket file: %+v", err)
|
|
}
|
|
}
|
|
err = unixSrv.Serve(listener)
|
|
unixRunning = false
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
handleEndpointStartFailedHooks("unix", err)
|
|
utils.Log.Errorf("failed to start unix: %s", err.Error())
|
|
} else {
|
|
handleEndpointShutdownHooks("unix")
|
|
}
|
|
}()
|
|
}
|
|
if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
|
|
s3r := gin.New()
|
|
s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
|
|
server.InitS3(s3r)
|
|
s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
|
|
fmt.Printf("start S3 server @ %s\n", s3Base)
|
|
utils.Log.Infof("start S3 server @ %s", s3Base)
|
|
go func() {
|
|
s3Running = true
|
|
var err error
|
|
if conf.Conf.S3.SSL {
|
|
s3Srv = &http.Server{Addr: s3Base, Handler: s3r}
|
|
err = s3Srv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
|
} else {
|
|
s3Srv = &http.Server{Addr: s3Base, Handler: s3r}
|
|
err = s3Srv.ListenAndServe()
|
|
}
|
|
s3Running = false
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
handleEndpointStartFailedHooks("s3", err)
|
|
utils.Log.Errorf("failed to start s3 server: %s", err.Error())
|
|
} else {
|
|
handleEndpointShutdownHooks("s3")
|
|
}
|
|
}()
|
|
}
|
|
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable {
|
|
var err error
|
|
ftpDriver, err = server.NewMainDriver()
|
|
if err != nil {
|
|
utils.Log.Errorf("failed to start ftp driver: %s", err.Error())
|
|
} else {
|
|
fmt.Printf("start ftp server on %s\n", conf.Conf.FTP.Listen)
|
|
utils.Log.Infof("start ftp server on %s", conf.Conf.FTP.Listen)
|
|
go func() {
|
|
ftpServer = ftpserver.NewFtpServer(ftpDriver)
|
|
ftpRunning = true
|
|
err = ftpServer.ListenAndServe()
|
|
ftpRunning = false
|
|
if err != nil {
|
|
handleEndpointStartFailedHooks("ftp", err)
|
|
utils.Log.Errorf("problem ftp server listening: %s", err.Error())
|
|
} else {
|
|
handleEndpointShutdownHooks("ftp")
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable {
|
|
var err error
|
|
sftpDriver, err = server.NewSftpDriver()
|
|
if err != nil {
|
|
utils.Log.Errorf("failed to start sftp driver: %s", err.Error())
|
|
} else {
|
|
fmt.Printf("start sftp server on %s", conf.Conf.SFTP.Listen)
|
|
utils.Log.Infof("start sftp server on %s", conf.Conf.SFTP.Listen)
|
|
go func() {
|
|
sftpServer = sftpd.NewSftpServer(sftpDriver)
|
|
sftpRunning = true
|
|
err = sftpServer.RunServer()
|
|
sftpRunning = false
|
|
if err != nil {
|
|
handleEndpointStartFailedHooks("sftp", err)
|
|
utils.Log.Errorf("problem sftp server listening: %s", err.Error())
|
|
} else {
|
|
handleEndpointShutdownHooks("sftp")
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
running = true
|
|
}
|
|
|
|
func Shutdown(timeout time.Duration) {
|
|
utils.Log.Println("Shutdown server...")
|
|
fs.ArchiveContentUploadTaskManager.RemoveAll()
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
var wg sync.WaitGroup
|
|
if httpSrv != nil && conf.Conf.Scheme.HttpPort != -1 {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := httpSrv.Shutdown(ctx); err != nil {
|
|
utils.Log.Error("HTTP server shutdown err: ", err)
|
|
}
|
|
httpSrv = nil
|
|
}()
|
|
}
|
|
if httpsSrv != nil && conf.Conf.Scheme.HttpsPort != -1 {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := httpsSrv.Shutdown(ctx); err != nil {
|
|
utils.Log.Error("HTTPS server shutdown err: ", err)
|
|
}
|
|
httpsSrv = nil
|
|
}()
|
|
if quicSrv != nil && conf.Conf.Scheme.EnableH3 {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := quicSrv.Shutdown(ctx); err != nil {
|
|
utils.Log.Error("HTTP3 (quic) server shutdown err: ", err)
|
|
}
|
|
quicSrv = nil
|
|
}()
|
|
}
|
|
}
|
|
if unixSrv != nil && conf.Conf.Scheme.UnixFile != "" {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := unixSrv.Shutdown(ctx); err != nil {
|
|
utils.Log.Error("Unix server shutdown err: ", err)
|
|
}
|
|
unixSrv = nil
|
|
}()
|
|
}
|
|
if s3Srv != nil && conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := s3Srv.Shutdown(ctx); err != nil {
|
|
utils.Log.Error("S3 server shutdown err: ", err)
|
|
}
|
|
s3Srv = nil
|
|
}()
|
|
}
|
|
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable && ftpServer != nil {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if ftpDriver != nil {
|
|
ftpDriver.Stop()
|
|
ftpDriver = nil
|
|
}
|
|
if err := ftpServer.Stop(); err != nil {
|
|
utils.Log.Error("FTP server shutdown err: ", err)
|
|
}
|
|
ftpServer = nil
|
|
}()
|
|
}
|
|
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable && sftpServer != nil {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := sftpServer.Close(); err != nil {
|
|
utils.Log.Error("SFTP server shutdown err: ", err)
|
|
}
|
|
sftpServer = nil
|
|
sftpDriver = nil
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
utils.Log.Println("Server exit")
|
|
running = false
|
|
}
|
|
|
|
type EndpointStartFailedHook func(string, string)
|
|
|
|
type EndpointShutdownHook func(string)
|
|
|
|
var (
|
|
endpointStartFailedHooks map[string]EndpointStartFailedHook
|
|
endpointShutdownHooks map[string]EndpointShutdownHook
|
|
)
|
|
|
|
func RegisterEndpointStartFailedHook(hook EndpointStartFailedHook) string {
|
|
id := uuid.NewString()
|
|
endpointStartFailedHooks[id] = hook
|
|
return id
|
|
}
|
|
|
|
func RemoveEndpointStartFailedHook(id string) {
|
|
delete(endpointStartFailedHooks, id)
|
|
}
|
|
|
|
func RegisterEndpointShutdownHook(hook EndpointShutdownHook) string {
|
|
id := uuid.NewString()
|
|
endpointShutdownHooks[id] = hook
|
|
return id
|
|
}
|
|
|
|
func RemoveEndpointShutdownHook(id string) {
|
|
delete(endpointShutdownHooks, id)
|
|
}
|
|
|
|
func handleEndpointStartFailedHooks(t string, err error) {
|
|
for _, hook := range endpointStartFailedHooks {
|
|
hook(t, err.Error())
|
|
}
|
|
}
|
|
|
|
func handleEndpointShutdownHooks(t string) {
|
|
for _, hook := range endpointShutdownHooks {
|
|
hook(t)
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
endpointShutdownHooks = make(map[string]EndpointShutdownHook)
|
|
endpointStartFailedHooks = make(map[string]EndpointStartFailedHook)
|
|
}
|