feat(seed): enrich capabilities and per-file source selection

Capabilities now report streamable/direct_source_available/share_available and the configured tracker list. Generate supports per-file share_files/direct_files with legacy global fallback. Add seed_default_trackers setting.
This commit is contained in:
PIKACHUIM
2026-09-08 23:13:49 +08:00
parent aa42d9e5fd
commit 8467a3abe0
3 changed files with 69 additions and 6 deletions
+1
View File
@@ -194,6 +194,7 @@ func InitialSettings() []model.SettingItem {
{Key: conf.SeedSingleDirectPreview, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
{Key: conf.SeedCASDirectAccess, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC, Help: `When opening a single-file CAS seed, immediately rapid-upload it into the same folder and preview the restored file`},
{Key: conf.SeedAutoGeneratePolicy, Value: "off", Type: conf.TypeSelect, Options: "off,on", Group: model.GLOBAL, Flag: model.PRIVATE, Help: `Global upload sidecar policy; storage-specific inheritance can be layered without changing the safe default`},
{Key: conf.SeedDefaultTrackers, Value: "", Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE, Help: `Default tracker list offered when generating torrent seeds (one tracker per line)`},
// single settings
{Key: conf.Token, Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE},
+1
View File
@@ -68,6 +68,7 @@ const (
SeedSingleDirectPreview = "seed_single_direct_preview"
SeedCASDirectAccess = "seed_cas_direct_access"
SeedAutoGeneratePolicy = "seed_auto_generate_policy"
SeedDefaultTrackers = "seed_default_trackers"
// index
SearchIndex = "search_index"
+67 -6
View File
@@ -499,6 +499,8 @@ type SeedGenerateReq struct {
SavePath string `json:"save_path"`
IncludeShare bool `json:"include_share"`
IncludeDirectSource bool `json:"include_direct_source"`
ShareFiles []string `json:"share_files"`
DirectFiles []string `json:"direct_files"`
}
// SeedCapabilityReq supports source preflight and destination import planning.
@@ -849,15 +851,33 @@ func GenerateSeedForPaths(c *gin.Context) {
seed.Comment = req.Comment
seed.Trackers = req.Trackers
seed.Channels = req.Channels
if req.IncludeShare && !user.CanShare() {
// Per-file share/direct selection. When either list is empty, fall back to the
// legacy global flags so existing callers keep working.
shareSet := make(map[string]bool, len(req.ShareFiles))
for _, p := range req.ShareFiles {
if strings.TrimSpace(p) != "" {
shareSet[p] = true
}
}
directSet := make(map[string]bool, len(req.DirectFiles))
for _, p := range req.DirectFiles {
if strings.TrimSpace(p) != "" {
directSet[p] = true
}
}
useGlobalShare := len(shareSet) == 0 && req.IncludeShare
useGlobalDirect := len(directSet) == 0 && req.IncludeDirectSource
hasShare := useGlobalShare || len(shareSet) > 0
hasDirect := useGlobalDirect || len(directSet) > 0
if hasShare && !user.CanShare() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
if req.IncludeDirectSource && setting.GetBool(conf.SignAll) && !req.IncludeShare {
if hasDirect && setting.GetBool(conf.SignAll) && !hasShare {
common.ErrorStrResp(c, "direct sources require an automatic share when global signing is enabled", 400)
return
}
if (req.IncludeShare || req.IncludeDirectSource) && strings.TrimSpace(setting.GetStr(conf.SeedSiteURL)) == "" {
if (hasShare || hasDirect) && strings.TrimSpace(setting.GetStr(conf.SeedSiteURL)) == "" {
common.ErrorStrResp(c, "seed_site_url must be configured before embedding download sources", 400)
return
}
@@ -936,7 +956,7 @@ func GenerateSeedForPaths(c *gin.Context) {
} else if comment := strings.TrimSpace(req.FileComments[obj.GetName()]); comment != "" {
seedFile.Comment = comment
}
if req.IncludeDirectSource {
if useGlobalDirect || directSet[requestedPath] {
baseURL := strings.TrimRight(setting.GetStr(conf.SeedSiteURL), "/")
seedFile.Sources = []torrent.SeedSource{{Type: "openlist-direct", URL: baseURL + utils.EncodePath("/d"+fullPath)}}
}
@@ -953,8 +973,11 @@ func GenerateSeedForPaths(c *gin.Context) {
}
}
}()
if req.IncludeShare {
if hasShare {
for index, fullPath := range fullPaths {
if !useGlobalShare && !shareSet[req.Paths[index]] {
continue
}
sharing := &model.Sharing{
SharingDB: &model.SharingDB{Remark: "Transfer seed source"},
Files: []string{fullPath}, Creator: user,
@@ -1074,6 +1097,34 @@ func encodeGeneratedSeed(seed *torrent.Seed, format string, standardPieces []byt
return t.Encode()
}
// loadSeedDefaultTrackers returns the configured default tracker list, one per line.
func loadSeedDefaultTrackers() []string {
raw := strings.TrimSpace(setting.GetStr(conf.SeedDefaultTrackers))
if raw == "" {
return nil
}
var trackers []string
for _, line := range strings.Split(raw, "\n") {
if trimmed := strings.TrimSpace(line); trimmed != "" {
trackers = append(trackers, trimmed)
}
}
return trackers
}
// directSourceAvailable reports whether an unauthenticated /d/ direct source can be embedded.
// Direct sources are only safe when the file is not encrypted and guest access is allowed.
func directSourceAvailable(meta *model.Meta, path string) bool {
if setting.GetBool(conf.SignAll) {
return false
}
if isEncrypt(meta, path) {
return false
}
guest, err := op.GetGuest()
return err == nil && !guest.Disabled
}
// SeedCapabilities reports whether files can use hashes, source URLs or require downloading.
func SeedCapabilities(c *gin.Context) {
user := c.Request.Context().Value(conf.UserKey).(*model.User)
@@ -1129,10 +1180,20 @@ func SeedCapabilities(c *gin.Context) {
fileTraffic = obj.GetSize()
estimatedTraffic += fileTraffic
}
// Probe whether the storage can be streamed for server-side hashing.
streamable := false
if link, _, linkErr := op.Link(c.Request.Context(), storage, actualPath, model.LinkArgs{}); linkErr == nil && link != nil {
if _, rrErr := stream.GetRangeReaderFromLink(obj.GetSize(), link); rrErr == nil {
streamable = true
}
_ = link.Close()
}
files = append(files, gin.H{
"path": requestedPath, "name": obj.GetName(), "size": obj.GetSize(),
"available_hashes": available, "requires_download": requiresDownload,
"requires_fetch": requiresDownload, "estimated_traffic": fileTraffic,
"streamable": streamable, "share_available": user.CanShare(),
"direct_source_available": directSourceAvailable(meta, fullPath),
})
}
existingHashes := make([]string, 0, len(existing))
@@ -1144,7 +1205,7 @@ func SeedCapabilities(c *gin.Context) {
common.SuccessResp(c, gin.H{
"formats": gin.H{"oss": true, "torrent": true, "cas": len(files) == 1},
"files": files, "existing_hashes": existingHashes, "estimated_traffic": estimatedTraffic,
"default_matrix": loadSeedDefaultMatrix(),
"default_matrix": loadSeedDefaultMatrix(), "trackers": loadSeedDefaultTrackers(),
})
return
}