Files
SuyunjingandLunelleEverdawn 25da9901f5
Beta Release builds / Beta Release Changelog (push) Failing after 56s
Beta Release builds / Beta Release (, , md5, !(*musl*|*windows-arm64*|*windows7-*|*android*|*freebsd*)) (push) Skipped
Beta Release builds / Beta Release (, , md5-android, false, android-*) (push) Skipped
Beta Release builds / Beta Release (, , md5-freebsd, false, freebsd-*) (push) Skipped
Beta Release builds / Beta Release (, , md5-windows-arm64, false, windows-arm64) (push) Skipped
Beta Release builds / Beta Release (, -tags=sqlite_cgo_compat, md5-windows7, false, windows7-*) (push) Skipped
Beta Release builds / Beta Release (-ldflags=-linkmode external -extldflags '-static -fpic', , md5-linux-musl, true, linux-!(arm*|mips|mips64|mipsle|mips64le|loong64)-musl*) (push) Skipped
Beta Release builds / Beta Release (-ldflags=-linkmode external -extldflags '-static -fpic', , md5-linux-musl-arm, true, linux-arm*-musl*) (push) Skipped
Beta Release builds / Beta Release (-ldflags=-linkmode external -extldflags '-static -fpic', , md5-linux-musl-mips, true, linux-(mips|mips64|mipsle|mips64le|loong64)-musl*) (push) Skipped
Sync to Gitee / Sync GitHub to Gitee (push) Failing after 36s
Beta Release (Docker) / Build Binaries for Docker Release (Beta) (push) Failing after 57m4s
Beta Release (Docker) / Release Docker image (Beta) (aio, INSTALL_FFMPEG=true INSTALL_ARIA2=true , aio, suffix=-aio,onlatest=true) (push) Skipped
Beta Release (Docker) / Release Docker image (Beta) (aria2, INSTALL_ARIA2=true, aria2, suffix=-aria2,onlatest=true) (push) Skipped
Beta Release (Docker) / Release Docker image (Beta) (base, , latest, ) (push) Skipped
Beta Release (Docker) / Release Docker image (Beta) (ffmpeg, INSTALL_FFMPEG=true, ffmpeg, suffix=-ffmpeg,onlatest=true) (push) Skipped
fix(drivers/local): handle cross-device recycle bin deletes (#3304)
- Fall back from cross-device rename to copy-then-remove while preserving
  relative symlinks, read-only directory support, file and directory
  modification times, and existing permissions.
- Keep the source and incomplete recycle-bin copy when a named-pipe copy
  does not create a destination entry.
- Preserve the complete recycle-bin copy when source removal fails.
- Skip the recycle bin when transfer tasks remove sources after a successful
  cross-storage move, avoiding duplicate move and recycle-bin copies.
- Add targeted coverage for relative symlinks, read-only directories,
  modification times, and permanent removal with a recycle bin configured.

Fixes #1856

Co-authored-by: LunelleEverdawn <lunelle+suyunmeng@oplist.org>
2026-11-08 13:22:06 +08:00

123 lines
3.9 KiB
Go

package task_group
import (
"context"
"fmt"
"path"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/setting"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"golang.org/x/time/rate"
)
type SrcPathToRemove string
// ActualPath
type DstPathToHook string
func HookAndRemove(ctx context.Context, dstPath string, payloads ...any) {
dstStorage, dstActualPath, err := op.GetStorageAndActualPath(dstPath)
if err != nil {
log.Error(errors.WithMessage(err, "failed get dst storage"))
return
}
dstNeedHandleHook := setting.GetBool(conf.HandleHookAfterWriting)
dstHandleHookLimit := setting.GetFloat(conf.HandleHookRateLimit, .0)
var listLimiter *rate.Limiter
if dstNeedHandleHook && dstHandleHookLimit > .0 {
listLimiter = rate.NewLimiter(rate.Limit(dstHandleHookLimit), 1)
}
hookedPaths := make(map[string]struct{})
handleHook := func(actualPath string) {
if _, ok := hookedPaths[actualPath]; ok {
return
}
if listLimiter != nil {
_ = listLimiter.Wait(ctx)
}
files, e := op.List(ctx, dstStorage, actualPath, model.ListArgs{SkipHook: true})
if e != nil {
log.Errorf("failed handle objs update hook: %v", e)
} else {
op.HandleObjsUpdateHook(ctx, utils.GetFullPath(dstStorage.GetStorage().MountPath, actualPath), files)
hookedPaths[actualPath] = struct{}{}
}
}
if dstNeedHandleHook {
handleHook(dstActualPath)
}
for _, payload := range payloads {
switch p := payload.(type) {
case DstPathToHook:
if dstNeedHandleHook {
handleHook(string(p))
}
case SrcPathToRemove:
srcStorage, srcActualPath, err := op.GetStorageAndActualPath(string(p))
if err != nil {
log.Error(errors.WithMessage(err, "failed get src storage"))
continue
}
// The destination already holds the moved objects, so skip recycle bins.
removeCtx := context.WithValue(ctx, conf.RemovePermanentlyKey, struct{}{})
err = verifyAndRemove(removeCtx, srcStorage, dstStorage, srcActualPath, dstActualPath)
if err != nil {
log.Error(err)
}
}
}
}
func verifyAndRemove(ctx context.Context, srcStorage, dstStorage driver.Driver, srcPath, dstPath string) error {
srcObj, err := op.GetUnwrap(ctx, srcStorage, srcPath)
if err != nil {
return errors.WithMessagef(err, "failed get src [%s] file", path.Join(srcStorage.GetStorage().MountPath, srcPath))
}
dstObjPath := path.Join(dstPath, srcObj.GetName())
dstObj, err := op.GetUnwrap(ctx, dstStorage, dstObjPath)
if err != nil {
return errors.WithMessagef(err, "failed get dst [%s] file", path.Join(dstStorage.GetStorage().MountPath, dstObjPath))
}
if !dstObj.IsDir() {
err = op.Remove(ctx, srcStorage, srcPath)
if err != nil {
return fmt.Errorf("failed remove %s: %+v", path.Join(srcStorage.GetStorage().MountPath, srcPath), err)
}
return nil
}
// Verify directory
srcObjs, err := op.List(ctx, srcStorage, srcPath, model.ListArgs{})
if err != nil {
return errors.WithMessagef(err, "failed list src [%s] objs", path.Join(srcStorage.GetStorage().MountPath, srcPath))
}
hasErr := false
for _, obj := range srcObjs {
srcSubPath := path.Join(srcPath, obj.GetName())
err := verifyAndRemove(ctx, srcStorage, dstStorage, srcSubPath, dstObjPath)
if err != nil {
log.Error(err)
hasErr = true
}
}
if hasErr {
return errors.Errorf("some subitems of [%s] failed to verify and remove", path.Join(srcStorage.GetStorage().MountPath, srcPath))
}
err = op.Remove(ctx, srcStorage, srcPath)
if err != nil {
return fmt.Errorf("failed remove %s: %+v", path.Join(srcStorage.GetStorage().MountPath, srcPath), err)
}
return nil
}
var TransferCoordinator *TaskGroupCoordinator = NewTaskGroupCoordinator("HookAndRemove", HookAndRemove)