Compare commits

..
19 changed files with 98 additions and 189 deletions
+1 -1
Submodule assets updated: b993b4283e...0feca325f4
-5
View File
@@ -116,11 +116,6 @@ func WebDAVAuth() gin.HandlerFunc {
return
}
// 用户组已启用WebDAV代理?
if !expectedUser.Group.OptionsSerialized.WebDAVProxy {
webdav.UseProxy = false
}
c.Set("user", &expectedUser)
c.Set("webdav", webdav)
c.Next()
+7 -29
View File
@@ -18,8 +18,7 @@ type Folder struct {
OwnerID uint `gorm:"index:owner_id"`
// 数据库忽略字段
Position string `gorm:"-"`
WebdavDstName string `gorm:"-"`
Position string `gorm:"-"`
}
// Create 创建目录
@@ -170,11 +169,6 @@ func (folder *Folder) MoveOrCopyFileTo(files []uint, dstFolder *Folder, isCopy b
oldFile.FolderID = dstFolder.ID
oldFile.UserID = dstFolder.OwnerID
// webdav目标名重置
if dstFolder.WebdavDstName != "" {
oldFile.Name = dstFolder.WebdavDstName
}
if err := DB.Create(&oldFile).Error; err != nil {
return copiedSize, err
}
@@ -183,14 +177,6 @@ func (folder *Folder) MoveOrCopyFileTo(files []uint, dstFolder *Folder, isCopy b
}
} else {
var updates = map[string]interface{}{
"folder_id": dstFolder.ID,
}
// webdav目标名重置
if dstFolder.WebdavDstName != "" {
updates["name"] = dstFolder.WebdavDstName
}
// 更改顶级要移动文件的父目录指向
err := DB.Model(File{}).Where(
"id in (?) and user_id = ? and folder_id = ?",
@@ -198,7 +184,9 @@ func (folder *Folder) MoveOrCopyFileTo(files []uint, dstFolder *Folder, isCopy b
folder.OwnerID,
folder.ID,
).
Update(updates).
Update(map[string]interface{}{
"folder_id": dstFolder.ID,
}).
Error
if err != nil {
return 0, err
@@ -233,10 +221,6 @@ func (folder *Folder) CopyFolderTo(folderID uint, dstFolder *Folder) (size uint6
// 顶级目录直接指向新的目的目录
if folder.ID == folderID {
newID = dstFolder.ID
// webdav目标名重置
if dstFolder.WebdavDstName != "" {
folder.Name = dstFolder.WebdavDstName
}
} else if IDCache, ok := newIDCache[*folder.ParentID]; ok {
newID = IDCache
} else {
@@ -298,21 +282,15 @@ func (folder *Folder) MoveFolderTo(dirs []uint, dstFolder *Folder) error {
return errors.New("cannot move a folder into itself")
}
var updates = map[string]interface{}{
"parent_id": dstFolder.ID,
}
// webdav目标名重置
if dstFolder.WebdavDstName != "" {
updates["name"] = dstFolder.WebdavDstName
}
// 更改顶级要移动目录的父目录指向
err := DB.Model(Folder{}).Where(
"id in (?) and owner_id = ? and parent_id = ?",
dirs,
folder.OwnerID,
folder.ID,
).Update(updates).Error
).Update(map[string]interface{}{
"parent_id": dstFolder.ID,
}).Error
return err
-1
View File
@@ -35,7 +35,6 @@ type GroupOption struct {
RedirectedSource bool `json:"redirected_source,omitempty"`
Aria2BatchSize int `json:"aria2_batch,omitempty"`
AdvanceDelete bool `json:"advance_delete,omitempty"`
WebDAVProxy bool `json:"webdav_proxy,omitempty"`
}
// GetGroupByID 用ID获取用户组
+3 -4
View File
@@ -12,7 +12,6 @@ type Webdav struct {
UserID uint `gorm:"unique_index:password_only_on"` // 用户ID
Root string `gorm:"type:text"` // 根目录
Readonly bool `gorm:"type:bool"` // 是否只读
UseProxy bool `gorm:"type:bool"` // 是否进行反代
}
// Create 创建账户
@@ -42,7 +41,7 @@ func DeleteWebDAVAccountByID(id, uid uint) {
DB.Where("user_id = ? and id = ?", uid, id).Delete(&Webdav{})
}
// UpdateWebDAVAccountByID 根据账户ID和UID更新账户
func UpdateWebDAVAccountByID(id, uid uint, updates map[string]interface{}) {
DB.Model(&Webdav{Model: gorm.Model{ID: id}, UserID: uid}).Updates(updates)
// UpdateWebDAVAccountReadonlyByID 根据账户ID和UID更新账户的只读性
func UpdateWebDAVAccountReadonlyByID(id, uid uint, readonly bool) {
DB.Model(&Webdav{Model: gorm.Model{ID: id}, UserID: uid}).UpdateColumn("readonly", readonly)
}
+2 -2
View File
@@ -1,13 +1,13 @@
package conf
// BackendVersion 当前后端版本号
var BackendVersion = "3.8.1"
var BackendVersion = "3.8.0"
// RequiredDBVersion 与当前版本匹配的数据库版本
var RequiredDBVersion = "3.8.0-beta1"
// RequiredStaticVersion 与当前版本匹配的静态资源版本
var RequiredStaticVersion = "3.8.1"
var RequiredStaticVersion = "3.8.0-beta1"
// IsPro 是否为Pro版本
var IsPro = "false"
-6
View File
@@ -35,10 +35,4 @@ const (
CancelFuncCtx
// 文件在从机节点中的路径
SlaveSrcPath
// Webdav目标名称
WebdavDstName
// WebDAVCtx WebDAV
WebDAVCtx
// WebDAV反代Url
WebDAVProxyUrlCtx
)
+24
View File
@@ -1,9 +1,12 @@
package fsctx
import (
"encoding/hex"
"errors"
"github.com/HFO4/aliyun-oss-go-sdk/oss"
"hash"
"io"
"strings"
"time"
)
@@ -121,3 +124,24 @@ func (file *FileStream) SetSize(size uint64) {
func (file *FileStream) SetModel(fileModel interface{}) {
file.Model = fileModel
}
type ChecksumFileStream struct {
Md5 hash.Hash
Sha1 hash.Hash
io.Reader
io.Closer
}
func (file *ChecksumFileStream) Hash() string {
var sb strings.Builder
sb.WriteString("MD5:")
sb.WriteString(hex.EncodeToString(file.Md5.Sum(nil)))
sb.WriteByte(' ')
sb.WriteString("SHA1:")
sb.WriteString(hex.EncodeToString(file.Sha1.Sum(nil)))
return sb.String()
}
+20 -11
View File
@@ -2,6 +2,8 @@ package filesystem
import (
"context"
"crypto/md5"
"crypto/sha1"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
@@ -9,6 +11,7 @@ import (
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"io"
"io/ioutil"
"net/http"
"strconv"
@@ -282,23 +285,29 @@ func NewWebdavAfterUploadHook(request *http.Request) func(ctx context.Context, f
modtime = time.Unix(timeUnix, 0)
}
}
checksum := request.Header.Get("OC-Checksum")
return func(ctx context.Context, fs *FileSystem, newFile fsctx.FileHeader) error {
file := newFile.Info().Model.(*model.File)
if !modtime.IsZero() {
err := model.DB.Model(file).UpdateColumn("updated_at", modtime).Error
if err != nil {
return err
}
}
if checksum != "" {
return file.UpdateMetadata(map[string]string{
model.ChecksumMetadataKey: checksum,
})
return model.DB.Model(file).UpdateColumn("updated_at", modtime).Error
}
return nil
}
}
// NewChecksumFileStreamAndAfterUploadHook 创建一个计算hash的数据流和相应的钩子函数
func NewChecksumFileStreamAndAfterUploadHook(rc io.ReadCloser) (io.ReadCloser, Hook) {
cfs := &fsctx.ChecksumFileStream{
Md5: md5.New(),
Sha1: sha1.New(),
Closer: rc,
}
cfs.Reader = io.TeeReader(rc, io.MultiWriter(cfs.Md5, cfs.Sha1))
return cfs, func(ctx context.Context, fs *FileSystem, newFile fsctx.FileHeader) error {
file := newFile.Info().Model.(*model.File)
return file.UpdateMetadata(map[string]string{
model.ChecksumMetadataKey: cfs.Hash(),
})
}
}
-10
View File
@@ -69,11 +69,6 @@ func (fs *FileSystem) Copy(ctx context.Context, dirs, files []uint, src, dst str
// 记录复制的文件的总容量
var newUsedStorage uint64
// 设置webdav目标名
if dstName, ok := ctx.Value(fsctx.WebdavDstName).(string); ok {
dstFolder.WebdavDstName = dstName
}
// 复制目录
if len(dirs) > 0 {
subFileSizes, err := srcFolder.CopyFolderTo(dirs[0], dstFolder)
@@ -108,11 +103,6 @@ func (fs *FileSystem) Move(ctx context.Context, dirs, files []uint, src, dst str
return ErrPathNotExist
}
// 设置webdav目标名
if dstName, ok := ctx.Value(fsctx.WebdavDstName).(string); ok {
dstFolder.WebdavDstName = dstName
}
// 处理目录及子文件移动
err := srcFolder.MoveFolderTo(dirs, dstFolder)
if err != nil {
-2
View File
@@ -42,7 +42,6 @@ type group struct {
WebDAVEnabled bool `json:"webdav"`
SourceBatchSize int `json:"sourceBatch"`
AdvanceDelete bool `json:"advanceDelete"`
AllowWebDAVProxy bool `json:"allowWebDAVProxy"`
}
type tag struct {
@@ -101,7 +100,6 @@ func BuildUser(user model.User) User {
ShareDownload: user.Group.OptionsSerialized.ShareDownload,
CompressEnabled: user.Group.OptionsSerialized.ArchiveTask,
WebDAVEnabled: user.Group.WebDAVEnabled,
AllowWebDAVProxy: user.Group.OptionsSerialized.WebDAVProxy,
SourceBatchSize: user.Group.OptionsSerialized.SourceBatchSize,
AdvanceDelete: user.Group.OptionsSerialized.AdvanceDelete,
},
+2 -1
View File
@@ -67,8 +67,9 @@ func (f *FfmpegGenerator) Generate(ctx context.Context, file io.Reader, src, nam
// Invoke ffmpeg
scaleOpt := fmt.Sprintf("scale=%s:%s:force_original_aspect_ratio=decrease", options["thumb_width"], options["thumb_height"])
inputFormat := filepath.Ext(name)[1:]
cmd := exec.CommandContext(ctx,
ffmpegOpts["thumb_ffmpeg_path"], "-ss", ffmpegOpts["thumb_ffmpeg_seek"], "-i", tempInputPath,
ffmpegOpts["thumb_ffmpeg_path"], "-ss", ffmpegOpts["thumb_ffmpeg_seek"], "-f", inputFormat, "-i", tempInputPath,
"-vf", scaleOpt, "-vframes", "1", tempOutputPath)
// Redirect IO
+8 -32
View File
@@ -9,12 +9,9 @@ import (
"net/http"
"path"
"path/filepath"
"strconv"
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
)
// slashClean is equivalent to but slightly more efficient than
@@ -26,31 +23,6 @@ func slashClean(name string) string {
return path.Clean(name)
}
// 更新Copy或Move后的修改时间
func updateCopyMoveModtime(req *http.Request, fs *filesystem.FileSystem, dst string) error {
var modtime time.Time
if timeVal := req.Header.Get("X-OC-Mtime"); timeVal != "" {
timeUnix, err := strconv.ParseInt(timeVal, 10, 64)
if err == nil {
modtime = time.Unix(timeUnix, 0)
}
}
if modtime.IsZero() {
return nil
}
ok, fi := isPathExist(req.Context(), fs, dst)
if !ok {
return nil
}
if fi.IsDir() {
return model.DB.Model(fi.(*model.Folder)).UpdateColumn("updated_at", modtime).Error
}
return model.DB.Model(fi.(*model.File)).UpdateColumn("updated_at", modtime).Error
}
// moveFiles moves files and/or directories from src to dst.
//
// See section 9.9.4 for when various HTTP status codes apply.
@@ -72,17 +44,20 @@ func moveFiles(ctx context.Context, fs *filesystem.FileSystem, src FileInfo, dst
}
}
// 判断是否需要移动
if src.GetPosition() != path.Dir(dst) {
err = fs.Move(
context.WithValue(ctx, fsctx.WebdavDstName, path.Base(dst)),
ctx,
folderIDs,
fileIDs,
src.GetPosition(),
path.Dir(dst),
)
} else if src.GetName() != path.Base(dst) {
// 判断是否需要重命名
}
// 判断是否需要重命名
if err == nil && src.GetName() != path.Base(dst) {
err = fs.Rename(
ctx,
folderIDs,
@@ -106,6 +81,7 @@ func copyFiles(ctx context.Context, fs *filesystem.FileSystem, src FileInfo, dst
}
recursion++
var (
fileIDs []uint
folderIDs []uint
@@ -124,7 +100,7 @@ func copyFiles(ctx context.Context, fs *filesystem.FileSystem, src FileInfo, dst
}
err = fs.Copy(
context.WithValue(ctx, fsctx.WebdavDstName, path.Base(dst)),
ctx,
folderIDs,
fileIDs,
src.GetPosition(),
+2 -2
View File
@@ -49,7 +49,7 @@ func (file *FileDeadProps) Patch(proppatches []Proppatch) ([]Propstat, error) {
var modtimeUnix int64
modtimeUnix, err = strconv.ParseInt(string(prop.InnerXML), 10, 64)
if err == nil {
err = model.DB.Model(file.File).UpdateColumn("updated_at", time.Unix(modtimeUnix, 0)).Error
err = model.DB.Model(file).UpdateColumn("updated_at", time.Unix(modtimeUnix, 0)).Error
}
}
}
@@ -78,7 +78,7 @@ func (folder *FolderDeadProps) Patch(proppatches []Proppatch) ([]Propstat, error
var modtimeUnix int64
modtimeUnix, err = strconv.ParseInt(string(prop.InnerXML), 10, 64)
if err == nil {
err = model.DB.Model(folder.Folder).UpdateColumn("updated_at", time.Unix(modtimeUnix, 0)).Error
err = model.DB.Model(folder).UpdateColumn("updated_at", time.Unix(modtimeUnix, 0)).Error
}
}
}
+9 -56
View File
@@ -10,7 +10,6 @@ import (
"errors"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"path"
"strconv"
@@ -242,23 +241,6 @@ func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request, fs *file
return 0, nil
}
var proxy = &httputil.ReverseProxy{
Director: func(request *http.Request) {
if target, ok := request.Context().Value(fsctx.WebDAVProxyUrlCtx).(*url.URL); ok {
request.URL.Scheme = target.Scheme
request.URL.Host = target.Host
request.URL.Path = target.Path
request.URL.RawPath = target.RawPath
request.URL.RawQuery = target.RawQuery
request.Host = target.Host
request.Header.Del("Authorization")
}
},
ErrorHandler: func(writer http.ResponseWriter, request *http.Request, err error) {
writer.WriteHeader(http.StatusInternalServerError)
},
}
// OK
func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem) (status int, err error) {
defer fs.Recycle()
@@ -297,23 +279,7 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request, fs *
return 0, nil
}
if application, ok := r.Context().Value(fsctx.WebDAVCtx).(*model.Webdav); ok && application.UseProxy {
target, err := url.Parse(rs.URL)
if err != nil {
return http.StatusInternalServerError, err
}
r = r.Clone(context.WithValue(r.Context(), fsctx.WebDAVProxyUrlCtx, target))
// 忽略反向代理在传输错误时报错
defer func() {
if err := recover(); err != nil && err != http.ErrAbortHandler {
panic(err)
}
}()
proxy.ServeHTTP(w, r)
} else {
http.Redirect(w, r, rs.URL, 301)
}
http.Redirect(w, r, rs.URL, 301)
return 0, nil
}
@@ -378,9 +344,13 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request, fs *filesyst
}
fileName := path.Base(reqPath)
filePath := path.Dir(reqPath)
// 计算hash
file, hook := filesystem.NewChecksumFileStreamAndAfterUploadHook(r.Body)
fileData := fsctx.FileStream{
MimeType: r.Header.Get("Content-Type"),
File: r.Body,
File: file,
Size: fileSize,
Name: fileName,
VirtualPath: filePath,
@@ -425,6 +395,7 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request, fs *filesyst
// rclone 请求
fs.Use("AfterUpload", filesystem.NewWebdavAfterUploadHook(r))
fs.Use("AfterUpload", hook)
// 执行上传
err = fs.Upload(ctx, &fileData)
@@ -530,16 +501,7 @@ func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request, fs *fil
return http.StatusBadRequest, errInvalidDepth
}
}
status, err = copyFiles(ctx, fs, target, dst, r.Header.Get("Overwrite") != "F", depth, 0)
if err != nil {
return status, err
}
err = updateCopyMoveModtime(r, fs, dst)
if err != nil {
return http.StatusInternalServerError, err
}
return status, nil
return copyFiles(ctx, fs, target, dst, r.Header.Get("Overwrite") != "F", depth, 0)
}
// windows下,某些情况下(网盘根目录下)Office保存文件时附带的锁token只包含源文件,
@@ -558,16 +520,7 @@ func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request, fs *fil
return http.StatusBadRequest, errInvalidDepth
}
}
status, err = moveFiles(ctx, fs, target, dst, r.Header.Get("Overwrite") == "T")
if err != nil {
return status, err
}
err = updateCopyMoveModtime(r, fs, dst)
if err != nil {
return http.StatusInternalServerError, err
}
return status, nil
return moveFiles(ctx, fs, target, dst, r.Header.Get("Overwrite") == "T")
}
// OK
+3 -8
View File
@@ -1,10 +1,8 @@
package controllers
import (
"context"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/cloudreve/Cloudreve/v3/pkg/webdav"
"github.com/cloudreve/Cloudreve/v3/service/setting"
@@ -51,9 +49,6 @@ func ServeWebDAV(c *gin.Context) {
return
}
}
// 更新Context
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), fsctx.WebDAVCtx, application))
}
handler.ServeHTTP(c.Writer, c.Request, fs)
@@ -81,9 +76,9 @@ func DeleteWebDAVAccounts(c *gin.Context) {
}
}
// UpdateWebDAVAccounts 更改WebDAV账户只读性和是否使用代理服务
func UpdateWebDAVAccounts(c *gin.Context) {
var service setting.WebDAVAccountUpdateService
// UpdateWebDAVAccountsReadonly 更改WebDAV账户只读性
func UpdateWebDAVAccountsReadonly(c *gin.Context) {
var service setting.WebDAVAccountUpdateReadonlyService
if err := c.ShouldBindJSON(&service); err == nil {
res := service.Update(c, CurrentUser(c))
c.JSON(200, res)
+2 -2
View File
@@ -721,8 +721,8 @@ func InitMasterRouter() *gin.Engine {
webdav.POST("accounts", controllers.CreateWebDAVAccounts)
// 删除账号
webdav.DELETE("accounts/:id", controllers.DeleteWebDAVAccounts)
// 更新账号可读性和是否使用代理服务
webdav.PATCH("accounts", controllers.UpdateWebDAVAccounts)
// 更新账号可读性
webdav.PATCH("accounts", controllers.UpdateWebDAVAccountsReadonly)
}
}
+5 -1
View File
@@ -411,9 +411,12 @@ func (service *FileIDService) PutContent(ctx context.Context, c *gin.Context) se
return serializer.ParamErr("Invalid content-length value", err)
}
// 计算hash
file, hook := filesystem.NewChecksumFileStreamAndAfterUploadHook(c.Request.Body)
fileData := fsctx.FileStream{
MimeType: c.Request.Header.Get("Content-Type"),
File: c.Request.Body,
File: file,
Size: fileSize,
Mode: fsctx.Overwrite,
}
@@ -453,6 +456,7 @@ func (service *FileIDService) PutContent(ctx context.Context, c *gin.Context) se
fs.Use("BeforeUpload", filesystem.HookValidateFile)
fs.Use("BeforeUpload", filesystem.HookValidateCapacityDiff)
fs.Use("AfterUpload", filesystem.GenericAfterUpdate)
fs.Use("AfterUpload", hook)
// 执行上传
uploadCtx = context.WithValue(uploadCtx, fsctx.FileModelCtx, originFile[0])
+10 -16
View File
@@ -22,11 +22,10 @@ type WebDAVAccountCreateService struct {
Name string `json:"name" binding:"required,min=1,max=255"`
}
// WebDAVAccountUpdateService WebDAV 修改只读性和是否使用代理服务
type WebDAVAccountUpdateService struct {
ID uint `json:"id" binding:"required,min=1"`
Readonly *bool `json:"readonly" binding:"required_without=UseProxy"`
UseProxy *bool `json:"use_proxy" binding:"required_without=Readonly"`
// WebDAVAccountUpdateReadonlyService WebDAV 修改只读性服务
type WebDAVAccountUpdateReadonlyService struct {
ID uint `json:"id" binding:"required,min=1"`
Readonly bool `json:"readonly"`
}
// WebDAVMountCreateService WebDAV 挂载创建服务
@@ -63,17 +62,12 @@ func (service *WebDAVAccountService) Delete(c *gin.Context, user *model.User) se
return serializer.Response{}
}
// Update 修改WebDAV账户只读性和是否使用代理服务
func (service *WebDAVAccountUpdateService) Update(c *gin.Context, user *model.User) serializer.Response {
var updates = make(map[string]interface{})
if service.Readonly != nil {
updates["readonly"] = *service.Readonly
}
if service.UseProxy != nil {
updates["use_proxy"] = *service.UseProxy
}
model.UpdateWebDAVAccountByID(service.ID, user.ID, updates)
return serializer.Response{Data: updates}
// Update 修改WebDAV账户只读性
func (service *WebDAVAccountUpdateReadonlyService) Update(c *gin.Context, user *model.User) serializer.Response {
model.UpdateWebDAVAccountReadonlyByID(service.ID, user.ID, service.Readonly)
return serializer.Response{Data: map[string]bool{
"readonly": service.Readonly,
}}
}
// Accounts 列出WebDAV账号