From 151ba27088a03505a0d548f1b780373fd9c1ea02 Mon Sep 17 00:00:00 2001 From: Strom Date: Sat, 15 Aug 2026 00:54:47 +0800 Subject: [PATCH 1/2] feat(strm): add local save permission mode - add private and shared permission modes for local STRM files - repair shared-mode directory and file permissions during generation - add permission handling tests Co-authored-by: Codex <267193182+codex@users.noreply.github.com> --- drivers/strm/driver.go | 6 ++ drivers/strm/hook.go | 144 ++++++++++++++++++++++++++------------ drivers/strm/hook_test.go | 74 ++++++++++++++++++++ drivers/strm/meta.go | 9 ++- 4 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 drivers/strm/hook_test.go diff --git a/drivers/strm/driver.go b/drivers/strm/driver.go index 3d243c157..e655286e7 100644 --- a/drivers/strm/driver.go +++ b/drivers/strm/driver.go @@ -45,6 +45,12 @@ func (d *Strm) Init(ctx context.Context) error { if d.SaveStrmToLocal && len(d.SaveStrmLocalPath) <= 0 { return errors.New("SaveStrmLocalPath is required") } + if len(d.SaveLocalPermMode) == 0 { + d.SaveLocalPermMode = SaveLocalPrivatePermMode + } + if d.SaveLocalPermMode != SaveLocalPrivatePermMode && d.SaveLocalPermMode != SaveLocalSharedPermMode { + return fmt.Errorf("invalid SaveLocalPermMode: %s", d.SaveLocalPermMode) + } d.pathMap = make(map[string][]string) for path := range strings.SplitSeq(d.Paths, "\n") { path = strings.TrimSpace(path) diff --git a/drivers/strm/hook.go b/drivers/strm/hook.go index 78ec1e7e6..b8c7bfc17 100644 --- a/drivers/strm/hook.go +++ b/drivers/strm/hook.go @@ -5,9 +5,11 @@ import ( "context" "crypto/sha256" "errors" + "fmt" "io" "os" stdpath "path" + "path/filepath" "strings" "github.com/OpenListTeam/OpenList/v4/internal/model" @@ -26,6 +28,11 @@ func UpdateLocalStrm(ctx context.Context, path string, objs []model.Obj) { updateLocal := func(driver *Strm, basePath string, objs []model.Obj) { relParent := strings.TrimPrefix(basePath, utils.GetActualMountPath(driver.MountPath)) localParentPath := stdpath.Join(driver.SaveStrmLocalPath, relParent) + if driver.SaveLocalPermMode == SaveLocalSharedPermMode { + if err := ensureLocalDirectories(localParentPath, driver.SaveStrmLocalPath, 0o755); err != nil { + log.Warnf("failed to set local strm directory permissions for %s: %v", localParentPath, err) + } + } for _, obj := range objs { localPath := stdpath.Join(localParentPath, obj.GetName()) generateStrm(ctx, driver, obj, localPath) @@ -92,55 +99,104 @@ func RemoveStrm(dstPath string, d *Strm) { } func generateStrm(ctx context.Context, driver *Strm, obj model.Obj, localPath string) { - if !obj.IsDir() { - if utils.Exists(localPath) && driver.SaveLocalMode == SaveLocalInsertMode { - return - } - link, err := driver.Link(ctx, obj, model.LinkArgs{}) - if err != nil { - log.Warnf("failed to generate strm of obj %s: failed to link: %v", localPath, err) - return - } - defer link.Close() - size := link.ContentLength - if size <= 0 { - size = obj.GetSize() - } - rrf, err := stream.GetRangeReaderFromLink(size, link) - if err != nil { - log.Warnf("failed to generate strm of obj %s: failed to get range reader: %v", localPath, err) - return - } - rc, err := rrf.RangeRead(ctx, http_range.Range{Length: -1}) - if err != nil { - log.Warnf("failed to generate strm of obj %s: failed to read range: %v", localPath, err) - return - } - defer rc.Close() - same, err := isSameContent(localPath, size, rc) - if err != nil { - log.Warnf("failed to compare content of obj %s: %v", localPath, err) - return - } - if same { - return + if obj.IsDir() { + if driver.SaveLocalPermMode == SaveLocalSharedPermMode { + if err := ensureLocalDirectories(localPath, driver.SaveStrmLocalPath, 0o755); err != nil { + log.Warnf("failed to set local strm directory permissions for %s: %v", localPath, err) + } } - rc, err = rrf.RangeRead(ctx, http_range.Range{Length: -1}) - if err != nil { - log.Warnf("failed to generate strm of obj %s: failed to reread range: %v", localPath, err) - return + return + } + + if utils.Exists(localPath) && driver.SaveLocalMode == SaveLocalInsertMode { + setLocalStrmFilePermissions(driver, localPath) + return + } + link, err := driver.Link(ctx, obj, model.LinkArgs{}) + if err != nil { + log.Warnf("failed to generate strm of obj %s: failed to link: %v", localPath, err) + return + } + defer link.Close() + size := link.ContentLength + if size <= 0 { + size = obj.GetSize() + } + rrf, err := stream.GetRangeReaderFromLink(size, link) + if err != nil { + log.Warnf("failed to generate strm of obj %s: failed to get range reader: %v", localPath, err) + return + } + rc, err := rrf.RangeRead(ctx, http_range.Range{Length: -1}) + if err != nil { + log.Warnf("failed to generate strm of obj %s: failed to read range: %v", localPath, err) + return + } + defer rc.Close() + same, err := isSameContent(localPath, size, rc) + if err != nil { + log.Warnf("failed to compare content of obj %s: %v", localPath, err) + return + } + if same { + setLocalStrmFilePermissions(driver, localPath) + return + } + rc, err = rrf.RangeRead(ctx, http_range.Range{Length: -1}) + if err != nil { + log.Warnf("failed to generate strm of obj %s: failed to reread range: %v", localPath, err) + return + } + defer rc.Close() + file, err := utils.CreateNestedFile(localPath) + if err != nil { + log.Warnf("failed to generate strm of obj %s: failed to create local file: %v", localPath, err) + return + } + defer file.Close() + if _, err := utils.CopyWithBuffer(file, rc); err != nil { + log.Warnf("failed to generate strm of obj %s: copy failed: %v", localPath, err) + } + setLocalStrmFilePermissions(driver, localPath) +} + +func ensureLocalDirectories(path, basePath string, mode os.FileMode) error { + path = filepath.Clean(path) + basePath = filepath.Clean(basePath) + rel, err := filepath.Rel(basePath, path) + if err != nil { + return err + } + if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return fmt.Errorf("local path %q is outside save path %q", path, basePath) + } + + dirs := []string{basePath} + current := basePath + if rel != "." { + for _, part := range strings.Split(rel, string(filepath.Separator)) { + current = filepath.Join(current, part) + dirs = append(dirs, current) } - defer rc.Close() - file, err := utils.CreateNestedFile(localPath) - if err != nil { - log.Warnf("failed to generate strm of obj %s: failed to create local file: %v", localPath, err) - return + } + for _, dir := range dirs { + if err := os.MkdirAll(dir, mode); err != nil { + return err } - defer file.Close() - if _, err := utils.CopyWithBuffer(file, rc); err != nil { - log.Warnf("failed to generate strm of obj %s: copy failed: %v", localPath, err) + if err := os.Chmod(dir, mode); err != nil { + return err } } + return nil +} + +func setLocalStrmFilePermissions(driver *Strm, path string) { + if driver.SaveLocalPermMode != SaveLocalSharedPermMode { + return + } + if err := os.Chmod(path, 0o644); err != nil { + log.Warnf("failed to set local strm file permissions for %s: %v", path, err) + } } func isSameContent(localPath string, size int64, rc io.Reader) (bool, error) { diff --git a/drivers/strm/hook_test.go b/drivers/strm/hook_test.go new file mode 100644 index 000000000..30c4e2ca5 --- /dev/null +++ b/drivers/strm/hook_test.go @@ -0,0 +1,74 @@ +package strm + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +func TestEnsureLocalDirectories(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("directory permission bits are not supported on Windows") + } + + root := t.TempDir() + nested := filepath.Join(root, "library", "movie") + if err := ensureLocalDirectories(nested, root, 0o755); err != nil { + t.Fatalf("ensureLocalDirectories() error = %v", err) + } + + for _, path := range []string{root, filepath.Join(root, "library"), nested} { + info, err := os.Stat(path) + if err != nil { + t.Fatalf("stat %q: %v", path, err) + } + if got := info.Mode().Perm(); got != 0o755 { + t.Errorf("permissions for %q = %#o, want %#o", path, got, 0o755) + } + } + + if err := os.Chmod(filepath.Join(root, "library"), 0o700); err != nil { + t.Fatalf("chmod existing directory: %v", err) + } + if err := ensureLocalDirectories(nested, root, 0o755); err != nil { + t.Fatalf("ensureLocalDirectories() repairing existing directory error = %v", err) + } + info, err := os.Stat(filepath.Join(root, "library")) + if err != nil { + t.Fatalf("stat repaired directory: %v", err) + } + if got := info.Mode().Perm(); got != 0o755 { + t.Errorf("repaired directory permissions = %#o, want %#o", got, 0o755) + } +} + +func TestEnsureLocalDirectoriesRejectsPathOutsideBase(t *testing.T) { + root := t.TempDir() + outside := filepath.Join(filepath.Dir(root), "outside") + if err := ensureLocalDirectories(outside, root, 0o755); err == nil { + t.Fatal("ensureLocalDirectories() error = nil, want path validation error") + } +} + +func TestSetLocalStrmFilePermissions(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("file permission bits are not supported on Windows") + } + + path := filepath.Join(t.TempDir(), "movie.strm") + if err := os.WriteFile(path, []byte("https://example.com/movie"), 0o600); err != nil { + t.Fatalf("write test file: %v", err) + } + + driver := &Strm{Addition: Addition{SaveLocalPermMode: SaveLocalSharedPermMode}} + setLocalStrmFilePermissions(driver, path) + + info, err := os.Stat(path) + if err != nil { + t.Fatalf("stat test file: %v", err) + } + if got := info.Mode().Perm(); got != 0o644 { + t.Errorf("file permissions = %#o, want %#o", got, 0o644) + } +} diff --git a/drivers/strm/meta.go b/drivers/strm/meta.go index 2e2e8a08e..55a767ba9 100644 --- a/drivers/strm/meta.go +++ b/drivers/strm/meta.go @@ -6,9 +6,11 @@ import ( ) const ( - SaveLocalInsertMode = "insert" - SaveLocalUpdateMode = "update" - SaveLocalSyncMode = "sync" + SaveLocalInsertMode = "insert" + SaveLocalUpdateMode = "update" + SaveLocalSyncMode = "sync" + SaveLocalPrivatePermMode = "private" + SaveLocalSharedPermMode = "shared" ) type Addition struct { @@ -24,6 +26,7 @@ type Addition struct { SaveStrmToLocal bool `json:"SaveStrmToLocal" default:"false" help:"save strm file locally"` SaveStrmLocalPath string `json:"SaveStrmLocalPath" type:"text" help:"save strm file local path"` SaveLocalMode string `json:"SaveLocalMode" type:"select" help:"save strm file locally mode" options:"insert,update,sync" default:"insert"` + SaveLocalPermMode string `json:"SaveLocalPermMode" type:"select" help:"permission mode for locally saved strm files; shared uses 0755 directories and 0644 files" options:"private,shared" default:"private"` Version int } From 98fbb99066b61cede00d5093287296be8c192802 Mon Sep 17 00:00:00 2001 From: Strom Date: Sat, 15 Aug 2026 09:46:01 +0800 Subject: [PATCH 2/2] fix(strm): respect deployment umask for local directories - remove application-level permission modes and chmod operations - create local STRM directories with umask-controlled permissions - preserve existing permissions and test the behavior Co-authored-by: Codex <267193182+codex@users.noreply.github.com> --- drivers/strm/driver.go | 6 --- drivers/strm/hook.go | 62 +++++-------------------- drivers/strm/hook_test.go | 67 ++++++++-------------------- drivers/strm/hook_umask_unix_test.go | 30 +++++++++++++ drivers/strm/meta.go | 9 ++-- 5 files changed, 63 insertions(+), 111 deletions(-) create mode 100644 drivers/strm/hook_umask_unix_test.go diff --git a/drivers/strm/driver.go b/drivers/strm/driver.go index e655286e7..3d243c157 100644 --- a/drivers/strm/driver.go +++ b/drivers/strm/driver.go @@ -45,12 +45,6 @@ func (d *Strm) Init(ctx context.Context) error { if d.SaveStrmToLocal && len(d.SaveStrmLocalPath) <= 0 { return errors.New("SaveStrmLocalPath is required") } - if len(d.SaveLocalPermMode) == 0 { - d.SaveLocalPermMode = SaveLocalPrivatePermMode - } - if d.SaveLocalPermMode != SaveLocalPrivatePermMode && d.SaveLocalPermMode != SaveLocalSharedPermMode { - return fmt.Errorf("invalid SaveLocalPermMode: %s", d.SaveLocalPermMode) - } d.pathMap = make(map[string][]string) for path := range strings.SplitSeq(d.Paths, "\n") { path = strings.TrimSpace(path) diff --git a/drivers/strm/hook.go b/drivers/strm/hook.go index b8c7bfc17..24b31ee3e 100644 --- a/drivers/strm/hook.go +++ b/drivers/strm/hook.go @@ -5,7 +5,6 @@ import ( "context" "crypto/sha256" "errors" - "fmt" "io" "os" stdpath "path" @@ -28,10 +27,9 @@ func UpdateLocalStrm(ctx context.Context, path string, objs []model.Obj) { updateLocal := func(driver *Strm, basePath string, objs []model.Obj) { relParent := strings.TrimPrefix(basePath, utils.GetActualMountPath(driver.MountPath)) localParentPath := stdpath.Join(driver.SaveStrmLocalPath, relParent) - if driver.SaveLocalPermMode == SaveLocalSharedPermMode { - if err := ensureLocalDirectories(localParentPath, driver.SaveStrmLocalPath, 0o755); err != nil { - log.Warnf("failed to set local strm directory permissions for %s: %v", localParentPath, err) - } + if err := createLocalDirectory(localParentPath); err != nil { + log.Warnf("failed to create local strm directory %s: %v", localParentPath, err) + return } for _, obj := range objs { localPath := stdpath.Join(localParentPath, obj.GetName()) @@ -100,16 +98,13 @@ func RemoveStrm(dstPath string, d *Strm) { func generateStrm(ctx context.Context, driver *Strm, obj model.Obj, localPath string) { if obj.IsDir() { - if driver.SaveLocalPermMode == SaveLocalSharedPermMode { - if err := ensureLocalDirectories(localPath, driver.SaveStrmLocalPath, 0o755); err != nil { - log.Warnf("failed to set local strm directory permissions for %s: %v", localPath, err) - } + if err := createLocalDirectory(localPath); err != nil { + log.Warnf("failed to create local strm directory %s: %v", localPath, err) } return } if utils.Exists(localPath) && driver.SaveLocalMode == SaveLocalInsertMode { - setLocalStrmFilePermissions(driver, localPath) return } link, err := driver.Link(ctx, obj, model.LinkArgs{}) @@ -139,7 +134,6 @@ func generateStrm(ctx context.Context, driver *Strm, obj model.Obj, localPath st return } if same { - setLocalStrmFilePermissions(driver, localPath) return } rc, err = rrf.RangeRead(ctx, http_range.Range{Length: -1}) @@ -148,7 +142,11 @@ func generateStrm(ctx context.Context, driver *Strm, obj model.Obj, localPath st return } defer rc.Close() - file, err := utils.CreateNestedFile(localPath) + if err := createLocalDirectory(filepath.Dir(localPath)); err != nil { + log.Warnf("failed to generate strm of obj %s: failed to create parent directory: %v", localPath, err) + return + } + file, err := os.Create(localPath) if err != nil { log.Warnf("failed to generate strm of obj %s: failed to create local file: %v", localPath, err) return @@ -157,46 +155,10 @@ func generateStrm(ctx context.Context, driver *Strm, obj model.Obj, localPath st if _, err := utils.CopyWithBuffer(file, rc); err != nil { log.Warnf("failed to generate strm of obj %s: copy failed: %v", localPath, err) } - setLocalStrmFilePermissions(driver, localPath) -} - -func ensureLocalDirectories(path, basePath string, mode os.FileMode) error { - path = filepath.Clean(path) - basePath = filepath.Clean(basePath) - rel, err := filepath.Rel(basePath, path) - if err != nil { - return err - } - if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { - return fmt.Errorf("local path %q is outside save path %q", path, basePath) - } - - dirs := []string{basePath} - current := basePath - if rel != "." { - for _, part := range strings.Split(rel, string(filepath.Separator)) { - current = filepath.Join(current, part) - dirs = append(dirs, current) - } - } - for _, dir := range dirs { - if err := os.MkdirAll(dir, mode); err != nil { - return err - } - if err := os.Chmod(dir, mode); err != nil { - return err - } - } - return nil } -func setLocalStrmFilePermissions(driver *Strm, path string) { - if driver.SaveLocalPermMode != SaveLocalSharedPermMode { - return - } - if err := os.Chmod(path, 0o644); err != nil { - log.Warnf("failed to set local strm file permissions for %s: %v", path, err) - } +func createLocalDirectory(path string) error { + return os.MkdirAll(path, 0o777) } func isSameContent(localPath string, size int64, rc io.Reader) (bool, error) { diff --git a/drivers/strm/hook_test.go b/drivers/strm/hook_test.go index 30c4e2ca5..eccdf209b 100644 --- a/drivers/strm/hook_test.go +++ b/drivers/strm/hook_test.go @@ -7,68 +7,37 @@ import ( "testing" ) -func TestEnsureLocalDirectories(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("directory permission bits are not supported on Windows") - } - +func TestCreateLocalDirectory(t *testing.T) { root := t.TempDir() nested := filepath.Join(root, "library", "movie") - if err := ensureLocalDirectories(nested, root, 0o755); err != nil { - t.Fatalf("ensureLocalDirectories() error = %v", err) - } - - for _, path := range []string{root, filepath.Join(root, "library"), nested} { - info, err := os.Stat(path) - if err != nil { - t.Fatalf("stat %q: %v", path, err) - } - if got := info.Mode().Perm(); got != 0o755 { - t.Errorf("permissions for %q = %#o, want %#o", path, got, 0o755) - } + if err := createLocalDirectory(nested); err != nil { + t.Fatalf("createLocalDirectory() error = %v", err) } - if err := os.Chmod(filepath.Join(root, "library"), 0o700); err != nil { - t.Fatalf("chmod existing directory: %v", err) - } - if err := ensureLocalDirectories(nested, root, 0o755); err != nil { - t.Fatalf("ensureLocalDirectories() repairing existing directory error = %v", err) - } - info, err := os.Stat(filepath.Join(root, "library")) - if err != nil { - t.Fatalf("stat repaired directory: %v", err) - } - if got := info.Mode().Perm(); got != 0o755 { - t.Errorf("repaired directory permissions = %#o, want %#o", got, 0o755) + if _, err := os.Stat(nested); err != nil { + t.Fatalf("stat created directory: %v", err) } } -func TestEnsureLocalDirectoriesRejectsPathOutsideBase(t *testing.T) { - root := t.TempDir() - outside := filepath.Join(filepath.Dir(root), "outside") - if err := ensureLocalDirectories(outside, root, 0o755); err == nil { - t.Fatal("ensureLocalDirectories() error = nil, want path validation error") - } -} - -func TestSetLocalStrmFilePermissions(t *testing.T) { +func TestCreateLocalDirectoryPreservesExistingPermissions(t *testing.T) { if runtime.GOOS == "windows" { - t.Skip("file permission bits are not supported on Windows") + t.Skip("directory permission bits are not supported on Windows") } - path := filepath.Join(t.TempDir(), "movie.strm") - if err := os.WriteFile(path, []byte("https://example.com/movie"), 0o600); err != nil { - t.Fatalf("write test file: %v", err) + root := t.TempDir() + nested := filepath.Join(root, "library", "movie") + if err := os.MkdirAll(nested, 0o700); err != nil { + t.Fatalf("create existing directory: %v", err) + } + if err := createLocalDirectory(nested); err != nil { + t.Fatalf("createLocalDirectory() error = %v", err) } - driver := &Strm{Addition: Addition{SaveLocalPermMode: SaveLocalSharedPermMode}} - setLocalStrmFilePermissions(driver, path) - - info, err := os.Stat(path) + info, err := os.Stat(nested) if err != nil { - t.Fatalf("stat test file: %v", err) + t.Fatalf("stat existing directory: %v", err) } - if got := info.Mode().Perm(); got != 0o644 { - t.Errorf("file permissions = %#o, want %#o", got, 0o644) + if got := info.Mode().Perm(); got != 0o700 { + t.Errorf("existing directory permissions = %#o, want %#o", got, 0o700) } } diff --git a/drivers/strm/hook_umask_unix_test.go b/drivers/strm/hook_umask_unix_test.go new file mode 100644 index 000000000..b693f9f18 --- /dev/null +++ b/drivers/strm/hook_umask_unix_test.go @@ -0,0 +1,30 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris + +package strm + +import ( + "os" + "path/filepath" + "syscall" + "testing" +) + +func TestCreateLocalDirectoryRespectsUmask(t *testing.T) { + root := t.TempDir() + nested := filepath.Join(root, "library", "movie") + + originalUmask := syscall.Umask(0o027) + defer syscall.Umask(originalUmask) + + if err := createLocalDirectory(nested); err != nil { + t.Fatalf("createLocalDirectory() error = %v", err) + } + + info, err := os.Stat(nested) + if err != nil { + t.Fatalf("stat created directory: %v", err) + } + if got := info.Mode().Perm(); got != 0o750 { + t.Errorf("created directory permissions = %#o, want %#o", got, 0o750) + } +} diff --git a/drivers/strm/meta.go b/drivers/strm/meta.go index 55a767ba9..2e2e8a08e 100644 --- a/drivers/strm/meta.go +++ b/drivers/strm/meta.go @@ -6,11 +6,9 @@ import ( ) const ( - SaveLocalInsertMode = "insert" - SaveLocalUpdateMode = "update" - SaveLocalSyncMode = "sync" - SaveLocalPrivatePermMode = "private" - SaveLocalSharedPermMode = "shared" + SaveLocalInsertMode = "insert" + SaveLocalUpdateMode = "update" + SaveLocalSyncMode = "sync" ) type Addition struct { @@ -26,7 +24,6 @@ type Addition struct { SaveStrmToLocal bool `json:"SaveStrmToLocal" default:"false" help:"save strm file locally"` SaveStrmLocalPath string `json:"SaveStrmLocalPath" type:"text" help:"save strm file local path"` SaveLocalMode string `json:"SaveLocalMode" type:"select" help:"save strm file locally mode" options:"insert,update,sync" default:"insert"` - SaveLocalPermMode string `json:"SaveLocalPermMode" type:"select" help:"permission mode for locally saved strm files; shared uses 0755 directories and 0644 files" options:"private,shared" default:"private"` Version int }