Skip to content
Open
140 changes: 114 additions & 26 deletions drivers/strm/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,34 +175,15 @@ func (d *Strm) listVirtualRoots() []model.Obj {
return objs
}

// rotateAllLocal rewrites every local STRM under all aliases (used by
// RotateSignNow). It reuses the same two-phase walk as GenerateLocal, forcing
// update mode so rotated signs always overwrite existing files.
func (d *Strm) rotateAllLocal(ctx context.Context) {
for alias, roots := range d.aliases {
virtualRoot := "/"
if !d.autoFlatten {
virtualRoot = "/" + alias
}
for _, realRoot := range roots {
d.walkAndSync(ctx, virtualRoot, realRoot)
}
}
}

func (d *Strm) walkAndSync(ctx context.Context, virtualDir, realDir string) {
objs, err := fs.List(ctx, realDir, &fs.ListArgs{NoLog: true, Refresh: true, NoUpdateIndex: true})
if err != nil {
log.Warnf("strm: rotate list failed %s: %v", realDir, err)
return
}
mapped := d.mapListedObjects(ctx, realDir, objs)
d.syncLocalDirWithMode(ctx, virtualDir, mapped, SaveLocalUpdateMode)
for _, obj := range objs {
if !obj.IsDir() {
continue
}
childVirtual := stdpath.Join(virtualDir, obj.GetName())
childReal := stdpath.Join(realDir, obj.GetName())
d.walkAndSync(ctx, childVirtual, childReal)
var units []strmDirUnit
for _, s := range d.resolveStarts("/") {
d.collectUnits(ctx, s.virtualDir, s.realDir, &units)
}
d.generateUnits(ctx, units, SaveLocalUpdateMode, nil)
}

func (d *Strm) mapListedObjects(ctx context.Context, realDir string, listed []model.Obj) []model.Obj {
Expand Down Expand Up @@ -281,3 +262,110 @@ func wrapObj(path string, src model.Obj, size int64) model.Obj {
}

var _ driver.Driver = (*Strm)(nil)

type strmDirUnit struct {
virtualDir string
objs []model.Obj
}

type strmDirStart struct {
virtualDir string
realDir string
}

// resolveStarts maps a strm-internal virtual path to walk start points.
// virtualPath "/" expands to all aliases.
func (d *Strm) resolveStarts(virtualPath string) []strmDirStart {
virtualPath = cleanPath(virtualPath)
var starts []strmDirStart
if virtualPath == "/" {
for alias, roots := range d.aliases {
vroot := "/"
if !d.autoFlatten {
vroot = "/" + alias
}
for _, r := range roots {
starts = append(starts, strmDirStart{virtualDir: vroot, realDir: r})
}
}
return starts
}
root, sub := d.splitVirtualPath(virtualPath)
roots, ok := d.aliases[root]
if !ok {
return nil
}
for _, r := range roots {
starts = append(starts, strmDirStart{virtualDir: virtualPath, realDir: stdpath.Join(r, sub)})
}
return starts
}

func (d *Strm) collectUnits(ctx context.Context, virtualDir, realDir string, units *[]strmDirUnit) {
if ctx.Err() != nil {
return
}
objs, err := fs.List(ctx, realDir, &fs.ListArgs{NoLog: true, Refresh: true, NoUpdateIndex: true})
if err != nil {
log.Warnf("strm: generate list failed %s: %v", realDir, err)
return
}
mapped := d.mapListedObjects(ctx, realDir, objs)
*units = append(*units, strmDirUnit{virtualDir: virtualDir, objs: mapped})
for _, obj := range objs {
if obj.IsDir() {
d.collectUnits(ctx, stdpath.Join(virtualDir, obj.GetName()), stdpath.Join(realDir, obj.GetName()), units)
}
}
}

func (d *Strm) generateUnits(ctx context.Context, units []strmDirUnit, mode string, up func(percent float64)) {
total := 0
for _, u := range units {
for _, o := range u.objs {
if !o.IsDir() {
total++
}
}
}
if total == 0 {
if up != nil {
up(100)
}
return
}
done := 0
for _, u := range units {
if ctx.Err() != nil {
return
}
d.syncLocalDirWithMode(ctx, u.virtualDir, u.objs, mode)
for _, o := range u.objs {
if !o.IsDir() {
done++
}
}
if up != nil {
up(float64(done) / float64(total) * 100)
}
}
}

// GenerateLocal implements driver.StrmGenerator.
func (d *Strm) GenerateLocal(ctx context.Context, virtualPath string, up func(percent float64)) error {
if strings.TrimSpace(d.SaveStrmLocalPath) == "" {
return errors.New("SaveStrmLocalPath is required")
}
starts := d.resolveStarts(virtualPath)
if len(starts) == 0 {
return errs.ObjectNotFound
}
var units []strmDirUnit
for _, s := range starts {
d.collectUnits(ctx, s.virtualDir, s.realDir, &units)
}
d.generateUnits(ctx, units, d.normalizedMode, up)
return ctx.Err()
}

var _ driver.StrmGenerator = (*Strm)(nil)
55 changes: 55 additions & 0 deletions drivers/strm/generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package strm

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/alist-org/alist/v3/internal/model"
)

func TestResolveStarts(t *testing.T) {
d := &Strm{}
d.aliases = map[string][]string{"movies": {"/local/movies"}}
d.autoFlatten = true
d.singleRootKey = "movies"
starts := d.resolveStarts("/")
if len(starts) != 1 || starts[0].virtualDir != "/" || starts[0].realDir != "/local/movies" {
t.Fatalf("flatten root got %+v", starts)
}

d2 := &Strm{}
d2.aliases = map[string][]string{"a": {"/ra"}, "b": {"/rb"}}
d2.autoFlatten = false
if len(d2.resolveStarts("/")) != 2 {
t.Fatalf("non-flatten root want 2")
}
sub := d2.resolveStarts("/a/sub")
if len(sub) != 1 || sub[0].virtualDir != "/a/sub" || sub[0].realDir != "/ra/sub" {
t.Fatalf("non-flatten sub got %+v", sub)
}
}

func TestGenerateUnitsWritesAndProgress(t *testing.T) {
tmp := t.TempDir()
d := &Strm{}
d.SaveStrmLocalPath = tmp
d.EncodePath = true
d.WithoutUrl = true
d.normalizedPrefix = "/d"

units := []strmDirUnit{
{virtualDir: "/Movies", objs: []model.Obj{
&model.Object{ID: "strm", Path: "/real/Movies/m.mkv", Name: "m.strm"},
}},
}
var last float64
d.generateUnits(context.Background(), units, SaveLocalUpdateMode, func(p float64) { last = p })
if last != 100 {
t.Fatalf("progress want 100 got %v", last)
}
if b, err := os.ReadFile(filepath.Join(tmp, "Movies", "m.strm")); err != nil || len(b) == 0 {
t.Fatalf("strm not written: err=%v len=%d", err, len(b))
}
}
5 changes: 4 additions & 1 deletion drivers/strm/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,14 @@ func (d *Strm) linkRealFile(ctx context.Context, realPath string, args model.Lin
}

func (d *Strm) syncLocalDir(ctx context.Context, virtualDir string, objs []model.Obj) {
if !d.SaveStrmToLocal {
return
}
d.syncLocalDirWithMode(ctx, virtualDir, objs, d.normalizedMode)
}

func (d *Strm) syncLocalDirWithMode(ctx context.Context, virtualDir string, objs []model.Obj, mode string) {
if !d.SaveStrmToLocal || strings.TrimSpace(d.SaveStrmLocalPath) == "" {
if strings.TrimSpace(d.SaveStrmLocalPath) == "" {
return
}
baseDir := filepath.Clean(d.SaveStrmLocalPath)
Expand Down
1 change: 1 addition & 0 deletions internal/bootstrap/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func InitTaskManager() {
op.RegisterSettingChangingCallback(func() {
fs.UploadTaskManager.SetWorkersNumActive(taskFilterNegative(setting.GetInt(conf.TaskUploadThreadsNum, conf.Conf.Tasks.Upload.Workers)))
})
fs.StrmGenerateTaskManager = tache.NewManager[*fs.StrmGenerateTask](tache.WithWorks(3), tache.WithMaxRetry(0)) // strm generate, not persisted
fs.CopyTaskManager = tache.NewManager[*fs.CopyTask](tache.WithWorks(setting.GetInt(conf.TaskCopyThreadsNum, conf.Conf.Tasks.Copy.Workers)), tache.WithPersistFunction(db.GetTaskDataFunc("copy", conf.Conf.Tasks.Copy.TaskPersistant), db.UpdateTaskDataFunc("copy", conf.Conf.Tasks.Copy.TaskPersistant)), tache.WithMaxRetry(conf.Conf.Tasks.Copy.MaxRetry))
op.RegisterSettingChangingCallback(func() {
fs.CopyTaskManager.SetWorkersNumActive(taskFilterNegative(setting.GetInt(conf.TaskCopyThreadsNum, conf.Conf.Tasks.Copy.Workers)))
Expand Down
11 changes: 11 additions & 0 deletions internal/driver/strm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package driver

import "context"

// StrmGenerator is implemented by the Strm driver to (re)generate local .strm
// files for a subtree, reporting progress (0-100) via up.
type StrmGenerator interface {
// GenerateLocal walks virtualPath (relative to the storage root, e.g. "/" or
// "/Movies") and writes local files, reporting progress in percent.
GenerateLocal(ctx context.Context, virtualPath string, up func(percent float64)) error
}
43 changes: 43 additions & 0 deletions internal/fs/strm_generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package fs

import (
"fmt"
"time"

"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/task"
"github.com/pkg/errors"
"github.com/xhofe/tache"
)

type StrmGenerateTask struct {
task.TaskExtension
StorageMountPath string `json:"storage_mount_path"`
Path string `json:"path"` // actual path relative to the storage root
}

func (t *StrmGenerateTask) GetName() string {
return fmt.Sprintf("generate strm [%s](%s)", t.StorageMountPath, t.Path)
}

func (t *StrmGenerateTask) GetStatus() string {
return "generating strm"
}

func (t *StrmGenerateTask) Run() error {
t.ClearEndTime()
t.SetStartTime(time.Now())
defer func() { t.SetEndTime(time.Now()) }()
storage, err := op.GetStorageByMountPath(t.StorageMountPath)
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
gen, ok := storage.(driver.StrmGenerator)
if !ok {
return errors.New("not a strm storage")
}
return gen.GenerateLocal(t.Ctx(), t.Path, t.SetProgress)
}

var StrmGenerateTaskManager *tache.Manager[*StrmGenerateTask]
47 changes: 47 additions & 0 deletions server/handles/strm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package handles

import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/task"
"github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin"
)

type GenerateStrmReq struct {
Path string `json:"path" form:"path"`
}

// GenerateStrm enqueues a strm generation task for the given path (which must be
// inside a Strm storage). Admin only (route is under the admin group).
func GenerateStrm(c *gin.Context) {
var req GenerateStrmReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
user := c.MustGet("user").(*model.User)
reqPath, err := user.JoinPath(req.Path)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
storage, actualPath, err := op.GetStorageAndActualPath(reqPath)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
if _, ok := storage.(driver.StrmGenerator); !ok {
common.ErrorStrResp(c, "not a strm storage", 400)
return
}
t := &fs.StrmGenerateTask{
TaskExtension: task.TaskExtension{Creator: user},
StorageMountPath: storage.GetStorage().MountPath,
Path: actualPath,
}
fs.StrmGenerateTaskManager.Add(t)
common.SuccessResp(c, gin.H{"task": getTaskInfo(t)})
}
1 change: 1 addition & 0 deletions server/handles/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ func taskRoute[T task.TaskExtensionInfo](g *gin.RouterGroup, manager task.Manage

func SetupTaskRoute(g *gin.RouterGroup) {
taskRoute(g.Group("/upload"), fs.UploadTaskManager)
taskRoute(g.Group("/strm_generate"), fs.StrmGenerateTaskManager)
taskRoute(g.Group("/copy"), fs.CopyTaskManager)
taskRoute(g.Group("/offline_download"), tool.DownloadTaskManager)
taskRoute(g.Group("/offline_download_transfer"), tool.TransferTaskManager)
Expand Down
1 change: 1 addition & 0 deletions server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ func admin(g *gin.RouterGroup) {

// retain /admin/task API to ensure compatibility with legacy automation scripts
_task(g.Group("/task"))
g.POST("/strm/generate", handles.GenerateStrm)

ms := g.Group("/message")
ms.POST("/get", message.HttpInstance.GetHandle)
Expand Down
Loading