Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
golang.org/x/net v0.41.0
golang.org/x/sync v0.16.0
golang.org/x/sys v0.34.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -143,7 +144,6 @@ require (
golang.org/x/arch v0.18.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.9.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions backend/internal/bizerr/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ type notFoundGroup struct {
ServiceSshdNotFound BizCode `code:"40402"`
// K8sResourceNotFound: 集群中找不到指定的 Pod, Deployment 或 Namespace
K8sResourceNotFound BizCode `code:"40403"`
// StorageResourceNotFound: 存储空间中找不到指定的文件或目录
StorageResourceNotFound BizCode `code:"40404"`
}

// methodNotAllowedGroup 405xx - 方法不允许
Expand Down
170 changes: 154 additions & 16 deletions backend/internal/storage/dataset.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package storage

import (
"context"
"errors"
"fmt"
"net/http"
"os"
Expand All @@ -11,7 +12,9 @@ import (

"github.com/raids-lab/crater/dao/model"
"github.com/raids-lab/crater/dao/query"
"github.com/raids-lab/crater/internal/bizerr"
"github.com/raids-lab/crater/internal/resputil"
"github.com/raids-lab/crater/internal/util"

"github.com/gin-gonic/gin"
)
Expand All @@ -20,45 +23,180 @@ type MoveFileReq struct {
Dst string `json:"dst" binding:"required"`
}

var (
errMoveSourceNotFound = errors.New("move source does not exist")
errMoveTargetExists = errors.New("move destination exists")
errMoveNoReplaceUnsupported = errors.New("atomic no-replace move is unsupported")
)

type moveFileHandlerDeps struct {
authenticate func(*gin.Context) (util.JWTMessage, error)
permission func(string, util.JWTMessage, *gin.Context) model.FilePermission
redirect func(*gin.Context, string, util.JWTMessage) (string, error)
openTarget func(string, string, string) (*os.Root, string, error)
move func(*os.Root, string, *os.Root, string) error
storageRoot string
}

func defaultMoveFileHandlerDeps() moveFileHandlerDeps {
return moveFileHandlerDeps{
authenticate: CheckJWTToken,
permission: GetPermission,
redirect: Redirect,
openTarget: openUploadTarget,
move: moveStorageEntry,
storageRoot: storageRootDir,
}
}

func MoveFile(c *gin.Context) {
AlloweOption(c)
checkfs()
jwttoken, err := CheckJWTToken(c)
moveFileWithDeps(c, defaultMoveFileHandlerDeps())
}

//nolint:gocyclo // Keep each authorization and filesystem failure mapped to its specific public error contract.
func moveFileWithDeps(c *gin.Context, deps moveFileHandlerDeps) {
jwttoken, err := deps.authenticate(c)
if err != nil {
resputil.Error(c, err.Error(), resputil.NotSpecified)
resputil.HandleError(c, bizerr.Auth.TokenInvalid.New("invalid token"))
return
}
var moveFileReq MoveFileReq
err = c.ShouldBind(&moveFileReq)
if err := c.ShouldBindJSON(&moveFileReq); err != nil {
resputil.HandleError(c, bizerr.BadRequest.InvalidRequest.Wrap(err, "invalid move request"))
return
}

sourcePath, err := normalizeWebDAVMutationLogicalPath(c.Param("path"))
if err != nil {
resputil.BadRequestError(c, err.Error())
resputil.HandleError(c, bizerr.BadRequest.ParameterError.New("invalid source path"))
return
}
param := strings.TrimPrefix(c.Request.URL.Path, "/api/ss/move")
sourcePermission := GetPermission(param, jwttoken, c)
dstPermission := GetPermission(moveFileReq.Dst, jwttoken, c)
destinationPath, err := normalizeWebDAVMutationLogicalPath(moveFileReq.Dst)
if err != nil {
resputil.HandleError(c, bizerr.BadRequest.ParameterError.New("invalid destination path"))
return
}
if sourcePath == destinationPath || strings.HasPrefix(destinationPath, sourcePath+"/") {
resputil.HandleError(c, bizerr.BadRequest.ParameterError.New("destination must be outside the source path"))
return
}

sourcePermission := deps.permission(sourcePath, jwttoken, c)
dstPermission := deps.permission(destinationPath, jwttoken, c)
if sourcePermission != model.ReadWrite || dstPermission != model.ReadWrite {
resputil.HTTPError(c, http.StatusUnauthorized, "You have no permission to move files or move files to this location ",
resputil.NotSpecified)
resputil.HandleError(c, bizerr.Forbidden.PermissionDenied.New("write permission is required for source and destination"))
return
}
realPath, err := Redirect(c, param, jwttoken)

realSource, err := deps.redirect(c, sourcePath, jwttoken)
if err != nil {
resputil.Error(c, err.Error(), resputil.NotSpecified)
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to resolve move source"))
return
}
realDst, err := Redirect(c, moveFileReq.Dst, jwttoken)
realDestination, err := deps.redirect(c, destinationPath, jwttoken)
if err != nil {
resputil.Error(c, err.Error(), resputil.NotSpecified)
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to resolve move destination"))
return
}
err = moveFiles(c.Request.Context(), realPath, realDst, false)

sourceRoot, err := deps.redirect(c, strings.Split(sourcePath, "/")[0], jwttoken)
if err != nil {
resputil.Error(c, err.Error(), resputil.NotSpecified)
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to resolve move source"))
return
}
destinationRoot, err := deps.redirect(c, strings.Split(destinationPath, "/")[0], jwttoken)
if err != nil {
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to resolve move destination"))
return
}

sourceParent, sourceName, err := deps.openTarget(deps.storageRoot, sourceRoot, realSource)
if err != nil {
handleMoveTargetOpenError(c, err, true, "source parent directory is unavailable")
return
}
defer sourceParent.Close()
destinationParent, destinationName, err := deps.openTarget(
deps.storageRoot,
destinationRoot,
realDestination,
)
if err != nil {
handleMoveTargetOpenError(c, err, false, "destination parent directory is unavailable")
return
}
defer destinationParent.Close()

if err := deps.move(sourceParent, sourceName, destinationParent, destinationName); err != nil {
switch {
case errors.Is(err, errMoveSourceNotFound):
resputil.HandleError(c, bizerr.NotFound.StorageResourceNotFound.New("source path does not exist"))
case errors.Is(err, errMoveTargetExists):
resputil.HandleError(c, bizerr.Conflict.ResourceAlreadyExists.New("destination path already exists"))
default:
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to move storage entry"))
}
return
}

resputil.Success(c, "move files successfully")
}

func handleMoveTargetOpenError(c *gin.Context, err error, source bool, message string) {
if errors.Is(err, errUploadParentInvalid) {
if source && isUploadParentMissing(err) {
resputil.HandleError(c, bizerr.NotFound.StorageResourceNotFound.New("source path does not exist"))
return
}
if isUploadParentInfrastructureFailure(err) {
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to access storage"))
return
}
resputil.HandleError(c, bizerr.Conflict.ResourceStatusError.New(message))
return
}
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to access storage"))
}

//nolint:gocyclo // The explicit checks preserve no-clobber and source-not-found semantics around one rename.
func moveStorageEntry(
sourceParent *os.Root,
sourceName string,
destinationParent *os.Root,
destinationName string,
) error {
if sourceParent == nil || destinationParent == nil ||
sourceName == "" || sourceName == "." || sourceName == parentPathSegment ||
destinationName == "" || destinationName == "." || destinationName == parentPathSegment ||
filepath.Base(sourceName) != sourceName || filepath.Base(destinationName) != destinationName {
return errUploadParentInvalid
}
if _, err := sourceParent.Lstat(sourceName); err != nil {
if os.IsNotExist(err) {
return errMoveSourceNotFound
}
return err
}
if _, err := destinationParent.Lstat(destinationName); err == nil {
return errMoveTargetExists
} else if !os.IsNotExist(err) {
return err
}
if err := renameStorageNoReplace(sourceParent, sourceName, destinationParent, destinationName); err != nil {
if os.IsExist(err) {
return errMoveTargetExists
}
if os.IsNotExist(err) {
if _, sourceErr := sourceParent.Lstat(sourceName); os.IsNotExist(sourceErr) {
return errMoveSourceNotFound
}
}
return err
}
return nil
}

func MoveDatasetOrModel(c *gin.Context) {
AlloweOption(c)
checkfs()
Expand Down
1 change: 1 addition & 0 deletions backend/internal/storage/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,7 @@ func RegisterFile(webdavGroup *gin.RouterGroup) {
webdavGroup.GET("/admin/files", GetAllFiles)
webdavGroup.GET("/admin/files/*path", GetAllFiles)
webdavGroup.GET("/download/*path", Download)
webdavGroup.POST("/upload/*path", UploadFile)
webdavGroup.DELETE("/delete/*path", DeleteFile)
webdavGroup.GET("/userspace", GetUserSpace)
webdavGroup.GET("/queuespace", GetAccountSpace)
Expand Down
111 changes: 111 additions & 0 deletions backend/internal/storage/mkdir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package storage

import (
"errors"
"net/http"
"os"
"strings"

"github.com/gin-gonic/gin"

"github.com/raids-lab/crater/dao/model"
"github.com/raids-lab/crater/internal/bizerr"
"github.com/raids-lab/crater/internal/resputil"
"github.com/raids-lab/crater/internal/util"
)

type createDirectoryHandlerDeps struct {
authenticate func(*gin.Context) (util.JWTMessage, error)
permission func(string, util.JWTMessage, *gin.Context) model.FilePermission
redirect func(*gin.Context, string, util.JWTMessage) (string, error)
openTarget func(string, string, string) (*os.Root, string, error)
mkdir func(*os.Root, string, os.FileMode) error
storageRoot string
}

func defaultCreateDirectoryHandlerDeps() createDirectoryHandlerDeps {
return createDirectoryHandlerDeps{
authenticate: CheckJWTToken,
permission: GetPermission,
redirect: Redirect,
openTarget: openUploadTarget,
mkdir: createStorageDirectory,
storageRoot: storageRootDir,
}
}

// CreateDirectory creates exactly one directory through the existing WebDAV
// MKCOL route while returning Crater's stable error envelope on failure.
func CreateDirectory(c *gin.Context) {
AlloweOption(c)
createDirectoryWithDeps(c, defaultCreateDirectoryHandlerDeps())
}

func createDirectoryWithDeps(c *gin.Context, deps createDirectoryHandlerDeps) {
token, err := deps.authenticate(c)
if err != nil {
resputil.HandleError(c, bizerr.Auth.TokenInvalid.New("invalid token"))
return
}

logicalPath, err := normalizeWebDAVMutationLogicalPath(c.Param("path"))
if err != nil {
resputil.HandleError(c, bizerr.BadRequest.ParameterError.New("invalid directory path"))
return
}
if permission := deps.permission(logicalPath, token, c); permission != model.ReadWrite {
resputil.HandleError(c, bizerr.Forbidden.PermissionDenied.New("write permission is required"))
return
}

realPath, err := deps.redirect(c, logicalPath, token)
if err != nil {
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to resolve directory path"))
return
}
logicalRoot := strings.Split(logicalPath, "/")[0]
realRoot, err := deps.redirect(c, logicalRoot, token)
if err != nil {
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to resolve directory path"))
return
}

parent, targetName, err := deps.openTarget(deps.storageRoot, realRoot, realPath)
if err != nil {
if errors.Is(err, errUploadParentInvalid) {
if isUploadParentInfrastructureFailure(err) {
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to access storage"))
return
}
resputil.HandleError(c, bizerr.Conflict.ResourceStatusError.New("directory parent is unavailable"))
return
}
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to access storage"))
return
}
defer parent.Close()

if err := deps.mkdir(parent, targetName, model.RWXFolderPerm); err != nil {
switch {
case os.IsExist(err):
resputil.HandleError(c, bizerr.Conflict.ResourceAlreadyExists.New("directory path already exists"))
case os.IsNotExist(err):
resputil.HandleError(c, bizerr.Conflict.ResourceStatusError.New("directory parent is unavailable"))
default:
resputil.HandleError(c, bizerr.Internal.FileSystemError.Wrap(err, "failed to create directory"))
}
return
}
c.Status(http.StatusCreated)
}

func createStorageDirectory(parent *os.Root, name string, mode os.FileMode) error {
if parent == nil || name == "" || name == "." || name == parentPathSegment ||
mode.Perm() != mode {
return errUploadParentInvalid
}
if err := parent.Mkdir(name, mode); err != nil {
return err
}
return chmodCreatedStorageDirectory(parent, name, mode)
}
24 changes: 24 additions & 0 deletions backend/internal/storage/mkdir_chmod_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd

package storage

import (
"errors"
"os"
)

func chmodCreatedStorageDirectory(parent *os.Root, name string, mode os.FileMode) error {
created, err := parent.Open(name)
if err != nil {
return err
}
defer created.Close()
info, err := created.Stat()
if err != nil {
return err
}
if !info.IsDir() {
return errors.New("created storage entry is no longer a directory")
}
return created.Chmod(mode)
}
Loading
Loading