mirror of
https://github.com/cloudreve/Cloudreve.git
synced 2024-04-21 12:31:40 +00:00
Compare commits
18
Commits
3.8.0-beta1
..
3.8.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88409cc1f0 | ||
|
|
cd6eee0b60 | ||
|
|
3ffce1e356 | ||
|
|
ce832bf13d | ||
|
|
5642dd3b66 | ||
|
|
a1747073df | ||
|
|
ad6c6bcd93 | ||
|
|
f4a04ce3c3 | ||
|
|
247e31079c | ||
|
|
a26893aabc | ||
|
|
ce759c02b1 | ||
|
|
9f6f9adc89 | ||
|
|
91025b9f24 | ||
|
|
a9bee3e638 | ||
|
|
243c312066 | ||
|
|
1d52ddd93a | ||
|
|
cbc549229b | ||
|
|
173ca6cdf8 |
+1
-1
Submodule assets updated: 0feca325f4...5d4d01a797
+13
-1
@@ -1,5 +1,14 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
redis:
|
||||
container_name: redis
|
||||
image: bitnami/redis:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
volumes:
|
||||
- redis_data:/bitnami/redis/data
|
||||
|
||||
cloudreve:
|
||||
container_name: cloudreve
|
||||
image: cloudreve/cloudreve:latest
|
||||
@@ -14,6 +23,7 @@ services:
|
||||
- ./cloudreve/avatar:/cloudreve/avatar
|
||||
depends_on:
|
||||
- aria2
|
||||
|
||||
aria2:
|
||||
container_name: aria2
|
||||
image: p3terx/aria2-pro # third party image, please keep notice what you are doing
|
||||
@@ -25,9 +35,11 @@ services:
|
||||
- ./aria2/config:/config
|
||||
- temp_data:/data
|
||||
volumes:
|
||||
redis_data:
|
||||
driver: local
|
||||
temp_data:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
device: $PWD/data
|
||||
o: bind
|
||||
o: bind
|
||||
|
||||
@@ -173,3 +173,5 @@ require (
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
|
||||
)
|
||||
|
||||
replace github.com/gomodule/redigo v2.0.0+incompatible => github.com/gomodule/redigo v1.8.9
|
||||
|
||||
@@ -362,8 +362,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
|
||||
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
|
||||
@@ -116,6 +116,11 @@ func WebDAVAuth() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// 用户组已启用WebDAV代理?
|
||||
if !expectedUser.Group.OptionsSerialized.WebDAVProxy {
|
||||
webdav.UseProxy = false
|
||||
}
|
||||
|
||||
c.Set("user", &expectedUser)
|
||||
c.Set("webdav", webdav)
|
||||
c.Next()
|
||||
|
||||
+29
-7
@@ -18,7 +18,8 @@ type Folder struct {
|
||||
OwnerID uint `gorm:"index:owner_id"`
|
||||
|
||||
// 数据库忽略字段
|
||||
Position string `gorm:"-"`
|
||||
Position string `gorm:"-"`
|
||||
WebdavDstName string `gorm:"-"`
|
||||
}
|
||||
|
||||
// Create 创建目录
|
||||
@@ -169,6 +170,11 @@ 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
|
||||
}
|
||||
@@ -177,6 +183,14 @@ 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 = ?",
|
||||
@@ -184,9 +198,7 @@ func (folder *Folder) MoveOrCopyFileTo(files []uint, dstFolder *Folder, isCopy b
|
||||
folder.OwnerID,
|
||||
folder.ID,
|
||||
).
|
||||
Update(map[string]interface{}{
|
||||
"folder_id": dstFolder.ID,
|
||||
}).
|
||||
Update(updates).
|
||||
Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -221,6 +233,10 @@ 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 {
|
||||
@@ -282,15 +298,21 @@ 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(map[string]interface{}{
|
||||
"parent_id": dstFolder.ID,
|
||||
}).Error
|
||||
).Update(updates).Error
|
||||
|
||||
return err
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ 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获取用户组
|
||||
|
||||
+4
-3
@@ -12,6 +12,7 @@ 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 创建账户
|
||||
@@ -41,7 +42,7 @@ func DeleteWebDAVAccountByID(id, uid uint) {
|
||||
DB.Where("user_id = ? and id = ?", uid, id).Delete(&Webdav{})
|
||||
}
|
||||
|
||||
// UpdateWebDAVAccountReadonlyByID 根据账户ID和UID更新账户的只读性
|
||||
func UpdateWebDAVAccountReadonlyByID(id, uid uint, readonly bool) {
|
||||
DB.Model(&Webdav{Model: gorm.Model{ID: id}, UserID: uid}).UpdateColumn("readonly", readonly)
|
||||
// UpdateWebDAVAccountByID 根据账户ID和UID更新账户
|
||||
func UpdateWebDAVAccountByID(id, uid uint, updates map[string]interface{}) {
|
||||
DB.Model(&Webdav{Model: gorm.Model{ID: id}, UserID: uid}).Updates(updates)
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -21,6 +21,7 @@ func Init() {
|
||||
10,
|
||||
conf.RedisConfig.Network,
|
||||
conf.RedisConfig.Server,
|
||||
conf.RedisConfig.User,
|
||||
conf.RedisConfig.Password,
|
||||
conf.RedisConfig.DB,
|
||||
)
|
||||
|
||||
Vendored
+2
-1
@@ -44,7 +44,7 @@ func deserializer(value []byte) (interface{}, error) {
|
||||
}
|
||||
|
||||
// NewRedisStore 创建新的redis存储
|
||||
func NewRedisStore(size int, network, address, password, database string) *RedisStore {
|
||||
func NewRedisStore(size int, network, address, user, password, database string) *RedisStore {
|
||||
return &RedisStore{
|
||||
pool: &redis.Pool{
|
||||
MaxIdle: size,
|
||||
@@ -63,6 +63,7 @@ func NewRedisStore(size int, network, address, password, database string) *Redis
|
||||
network,
|
||||
address,
|
||||
redis.DialDatabase(db),
|
||||
redis.DialUsername(user),
|
||||
redis.DialPassword(password),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
Vendored
+1
-1
@@ -13,7 +13,7 @@ import (
|
||||
func TestNewRedisStore(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
|
||||
store := NewRedisStore(10, "tcp", "", "", "0")
|
||||
store := NewRedisStore(10, "tcp", "", "", "", "0")
|
||||
asserts.NotNil(store)
|
||||
|
||||
asserts.Panics(func() {
|
||||
|
||||
@@ -53,6 +53,7 @@ type slave struct {
|
||||
type redis struct {
|
||||
Network string
|
||||
Server string
|
||||
User string
|
||||
Password string
|
||||
DB string
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
package conf
|
||||
|
||||
// BackendVersion 当前后端版本号
|
||||
var BackendVersion = "3.8.0-beta1"
|
||||
var BackendVersion = "3.8.3"
|
||||
|
||||
// RequiredDBVersion 与当前版本匹配的数据库版本
|
||||
var RequiredDBVersion = "3.8.0-beta1"
|
||||
var RequiredDBVersion = "3.8.1"
|
||||
|
||||
// RequiredStaticVersion 与当前版本匹配的静态资源版本
|
||||
var RequiredStaticVersion = "3.8.0-beta1"
|
||||
var RequiredStaticVersion = "3.8.3"
|
||||
|
||||
// IsPro 是否为Pro版本
|
||||
var IsPro = "false"
|
||||
|
||||
@@ -234,9 +234,12 @@ func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.Co
|
||||
ok = false
|
||||
)
|
||||
if thumbSize, ok = ctx.Value(fsctx.ThumbSizeCtx).([2]uint); !ok {
|
||||
return nil, errors.New("无法获取缩略图尺寸设置")
|
||||
return nil, errors.New("failed to get thumbnail size")
|
||||
}
|
||||
thumbParam := fmt.Sprintf("imageMogr2/thumbnail/%dx%d", thumbSize[0], thumbSize[1])
|
||||
|
||||
thumbEncodeQuality := model.GetIntSetting("thumb_encode_quality", 85)
|
||||
|
||||
thumbParam := fmt.Sprintf("imageMogr2/thumbnail/%dx%d/quality/%d", thumbSize[0], thumbSize[1], thumbEncodeQuality)
|
||||
|
||||
source, err := handler.signSourceURL(
|
||||
ctx,
|
||||
|
||||
@@ -312,7 +312,9 @@ func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.C
|
||||
return nil, errors.New("failed to get thumbnail size")
|
||||
}
|
||||
|
||||
thumbParam := fmt.Sprintf("image/resize,m_lfit,h_%d,w_%d", thumbSize[1], thumbSize[0])
|
||||
thumbEncodeQuality := model.GetIntSetting("thumb_encode_quality", 85)
|
||||
|
||||
thumbParam := fmt.Sprintf("image/resize,m_lfit,h_%d,w_%d/quality,q_%d", thumbSize[1], thumbSize[0], thumbEncodeQuality)
|
||||
ctx = context.WithValue(ctx, fsctx.ThumbSizeCtx, thumbParam)
|
||||
thumbOption := []oss.Option{oss.Process(thumbParam)}
|
||||
thumbURL, err := handler.signSourceURL(
|
||||
|
||||
@@ -242,10 +242,12 @@ func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.C
|
||||
ok = false
|
||||
)
|
||||
if thumbSize, ok = ctx.Value(fsctx.ThumbSizeCtx).([2]uint); !ok {
|
||||
return nil, errors.New("无法获取缩略图尺寸设置")
|
||||
return nil, errors.New("failed to get thumbnail size")
|
||||
}
|
||||
|
||||
thumb := fmt.Sprintf("%s?imageView2/1/w/%d/h/%d", file.SourceName, thumbSize[0], thumbSize[1])
|
||||
thumbEncodeQuality := model.GetIntSetting("thumb_encode_quality", 85)
|
||||
|
||||
thumb := fmt.Sprintf("%s?imageView2/1/w/%d/h/%d/q/%d", file.SourceName, thumbSize[0], thumbSize[1], thumbEncodeQuality)
|
||||
return &response.ContentResponse{
|
||||
Redirect: true,
|
||||
URL: handler.signSourceURL(
|
||||
|
||||
@@ -235,7 +235,9 @@ func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.Co
|
||||
return nil, errors.New("failed to get thumbnail size")
|
||||
}
|
||||
|
||||
thumbParam := fmt.Sprintf("!/fwfh/%dx%d", thumbSize[0], thumbSize[1])
|
||||
thumbEncodeQuality := model.GetIntSetting("thumb_encode_quality", 85)
|
||||
|
||||
thumbParam := fmt.Sprintf("!/fwfh/%dx%d/quality/%d", thumbSize[0], thumbSize[1], thumbEncodeQuality)
|
||||
thumbURL, err := handler.Source(ctx, file.SourceName+thumbParam, int64(model.GetIntSetting("preview_timeout", 60)), false, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -35,4 +35,10 @@ const (
|
||||
CancelFuncCtx
|
||||
// 文件在从机节点中的路径
|
||||
SlaveSrcPath
|
||||
// Webdav目标名称
|
||||
WebdavDstName
|
||||
// WebDAVCtx WebDAV
|
||||
WebDAVCtx
|
||||
// WebDAV反代Url
|
||||
WebDAVProxyUrlCtx
|
||||
)
|
||||
|
||||
@@ -134,7 +134,7 @@ func (fs *FileSystem) generateThumbnail(ctx context.Context, file *model.File) e
|
||||
|
||||
// Provide file source path for local policy files
|
||||
src := ""
|
||||
if file.GetPolicy().Type == "local" {
|
||||
if conf.SystemConfig.Mode == "slave" || file.GetPolicy().Type == "local" {
|
||||
src = file.SourceName
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ 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)
|
||||
@@ -103,6 +108,11 @@ 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 {
|
||||
|
||||
@@ -10,7 +10,7 @@ type WopiClientMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (w *WopiClientMock) NewSession(user *model.User, file *model.File, action wopi.ActonType) (*wopi.Session, error) {
|
||||
func (w *WopiClientMock) NewSession(user uint, file *model.File, action wopi.ActonType) (*wopi.Session, error) {
|
||||
args := w.Called(user, file, action)
|
||||
return args.Get(0).(*wopi.Session), args.Error(1)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ type group struct {
|
||||
WebDAVEnabled bool `json:"webdav"`
|
||||
SourceBatchSize int `json:"sourceBatch"`
|
||||
AdvanceDelete bool `json:"advanceDelete"`
|
||||
AllowWebDAVProxy bool `json:"allowWebDAVProxy"`
|
||||
}
|
||||
|
||||
type tag struct {
|
||||
@@ -100,6 +101,7 @@ 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,
|
||||
},
|
||||
|
||||
+1
-2
@@ -67,9 +67,8 @@ 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"], "-f", inputFormat, "-i", tempInputPath,
|
||||
ffmpegOpts["thumb_ffmpeg_path"], "-ss", ffmpegOpts["thumb_ffmpeg_seek"], "-i", tempInputPath,
|
||||
"-vf", scaleOpt, "-vframes", "1", tempOutputPath)
|
||||
|
||||
// Redirect IO
|
||||
|
||||
+32
-8
@@ -9,9 +9,12 @@ 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
|
||||
@@ -23,6 +26,31 @@ 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.
|
||||
@@ -44,20 +72,17 @@ func moveFiles(ctx context.Context, fs *filesystem.FileSystem, src FileInfo, dst
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 判断是否需要移动
|
||||
if src.GetPosition() != path.Dir(dst) {
|
||||
err = fs.Move(
|
||||
ctx,
|
||||
context.WithValue(ctx, fsctx.WebdavDstName, path.Base(dst)),
|
||||
folderIDs,
|
||||
fileIDs,
|
||||
src.GetPosition(),
|
||||
path.Dir(dst),
|
||||
)
|
||||
}
|
||||
|
||||
// 判断是否需要重命名
|
||||
if err == nil && src.GetName() != path.Base(dst) {
|
||||
} else if src.GetName() != path.Base(dst) {
|
||||
// 判断是否需要重命名
|
||||
err = fs.Rename(
|
||||
ctx,
|
||||
folderIDs,
|
||||
@@ -81,7 +106,6 @@ func copyFiles(ctx context.Context, fs *filesystem.FileSystem, src FileInfo, dst
|
||||
}
|
||||
recursion++
|
||||
|
||||
|
||||
var (
|
||||
fileIDs []uint
|
||||
folderIDs []uint
|
||||
@@ -100,7 +124,7 @@ func copyFiles(ctx context.Context, fs *filesystem.FileSystem, src FileInfo, dst
|
||||
}
|
||||
|
||||
err = fs.Copy(
|
||||
ctx,
|
||||
context.WithValue(ctx, fsctx.WebdavDstName, path.Base(dst)),
|
||||
folderIDs,
|
||||
fileIDs,
|
||||
src.GetPosition(),
|
||||
|
||||
+2
-2
@@ -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).UpdateColumn("updated_at", time.Unix(modtimeUnix, 0)).Error
|
||||
err = model.DB.Model(file.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).UpdateColumn("updated_at", time.Unix(modtimeUnix, 0)).Error
|
||||
err = model.DB.Model(folder.Folder).UpdateColumn("updated_at", time.Unix(modtimeUnix, 0)).Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+55
-3
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
@@ -241,6 +242,23 @@ 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()
|
||||
@@ -279,7 +297,23 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request, fs *
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
http.Redirect(w, r, rs.URL, 301)
|
||||
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)
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
@@ -496,7 +530,16 @@ func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request, fs *fil
|
||||
return http.StatusBadRequest, errInvalidDepth
|
||||
}
|
||||
}
|
||||
return copyFiles(ctx, fs, target, dst, r.Header.Get("Overwrite") != "F", depth, 0)
|
||||
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
|
||||
}
|
||||
|
||||
// windows下,某些情况下(网盘根目录下)Office保存文件时附带的锁token只包含源文件,
|
||||
@@ -515,7 +558,16 @@ func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request, fs *fil
|
||||
return http.StatusBadRequest, errInvalidDepth
|
||||
}
|
||||
}
|
||||
return moveFiles(ctx, fs, target, dst, r.Header.Get("Overwrite") == "T")
|
||||
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
|
||||
}
|
||||
|
||||
// OK
|
||||
|
||||
+3
-3
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
type Client interface {
|
||||
// NewSession creates a new document session with access token.
|
||||
NewSession(user *model.User, file *model.File, action ActonType) (*Session, error)
|
||||
NewSession(uid uint, file *model.File, action ActonType) (*Session, error)
|
||||
// AvailableExts returns a list of file extensions that are supported by WOPI.
|
||||
AvailableExts() []string
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func NewClient(endpoint string, cache cache.Driver, http request.Client) (Client
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *client) NewSession(user *model.User, file *model.File, action ActonType) (*Session, error) {
|
||||
func (c *client) NewSession(uid uint, file *model.File, action ActonType) (*Session, error) {
|
||||
if err := c.checkDiscovery(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func (c *client) NewSession(user *model.User, file *model.File, action ActonType
|
||||
session := &SessionCache{
|
||||
AccessToken: fmt.Sprintf("%s.%s", sessionID, token),
|
||||
FileID: file.ID,
|
||||
UserID: user.ID,
|
||||
UserID: uid,
|
||||
Action: action,
|
||||
}
|
||||
err = c.cache.Set(SessionCachePrefix+sessionID.String(), *session, ttl)
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestNewSession(t *testing.T) {
|
||||
).Return(&request.Response{
|
||||
Err: expectedErr,
|
||||
})
|
||||
res, err := client.NewSession(&model.User{}, &model.File{}, ActionPreview)
|
||||
res, err := client.NewSession(0, &model.File{}, ActionPreview)
|
||||
a.Nil(res)
|
||||
a.ErrorIs(err, expectedErr)
|
||||
mockHttp.AssertExpectations(t)
|
||||
@@ -65,7 +65,7 @@ func TestNewSession(t *testing.T) {
|
||||
{
|
||||
client.discovery = &WopiDiscovery{}
|
||||
client.actions = make(map[string]map[string]Action)
|
||||
res, err := client.NewSession(&model.User{}, &model.File{}, ActionPreview)
|
||||
res, err := client.NewSession(0, &model.File{}, ActionPreview)
|
||||
a.Nil(res)
|
||||
a.ErrorIs(err, ErrActionNotSupported)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func TestNewSession(t *testing.T) {
|
||||
client.actions = map[string]map[string]Action{
|
||||
".doc": {},
|
||||
}
|
||||
res, err := client.NewSession(&model.User{}, &model.File{Name: "1.doc"}, ActionPreview)
|
||||
res, err := client.NewSession(0, &model.File{Name: "1.doc"}, ActionPreview)
|
||||
a.Nil(res)
|
||||
a.ErrorIs(err, ErrActionNotSupported)
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func TestNewSession(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
res, err := client.NewSession(&model.User{}, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
res, err := client.NewSession(0, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
a.Nil(res)
|
||||
a.ErrorContains(err, "invalid control character in URL")
|
||||
}
|
||||
@@ -106,7 +106,7 @@ func TestNewSession(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
res, err := client.NewSession(&model.User{}, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
res, err := client.NewSession(0, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
a.NotNil(res)
|
||||
a.NoError(err)
|
||||
resUrl := res.ActionURL.String()
|
||||
@@ -123,7 +123,7 @@ func TestNewSession(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
res, err := client.NewSession(&model.User{}, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
res, err := client.NewSession(0, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
a.NotNil(res)
|
||||
a.NoError(err)
|
||||
resUrl := res.ActionURL.String()
|
||||
@@ -147,7 +147,7 @@ func TestNewSession(t *testing.T) {
|
||||
},
|
||||
}
|
||||
mockCache.On("Set", testMock.Anything, testMock.Anything, testMock.Anything).Return(expectedErr)
|
||||
res, err := client.NewSession(&model.User{}, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
res, err := client.NewSession(0, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
a.Nil(res)
|
||||
a.ErrorIs(err, expectedErr)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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"
|
||||
@@ -49,6 +51,9 @@ 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)
|
||||
@@ -76,9 +81,9 @@ func DeleteWebDAVAccounts(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateWebDAVAccountsReadonly 更改WebDAV账户只读性
|
||||
func UpdateWebDAVAccountsReadonly(c *gin.Context) {
|
||||
var service setting.WebDAVAccountUpdateReadonlyService
|
||||
// UpdateWebDAVAccounts 更改WebDAV账户只读性和是否使用代理服务
|
||||
func UpdateWebDAVAccounts(c *gin.Context) {
|
||||
var service setting.WebDAVAccountUpdateService
|
||||
if err := c.ShouldBindJSON(&service); err == nil {
|
||||
res := service.Update(c, CurrentUser(c))
|
||||
c.JSON(200, res)
|
||||
|
||||
+2
-2
@@ -721,8 +721,8 @@ func InitMasterRouter() *gin.Engine {
|
||||
webdav.POST("accounts", controllers.CreateWebDAVAccounts)
|
||||
// 删除账号
|
||||
webdav.DELETE("accounts/:id", controllers.DeleteWebDAVAccounts)
|
||||
// 更新账号可读性
|
||||
webdav.PATCH("accounts", controllers.UpdateWebDAVAccountsReadonly)
|
||||
// 更新账号可读性和是否使用代理服务
|
||||
webdav.PATCH("accounts", controllers.UpdateWebDAVAccounts)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -112,8 +112,13 @@ func (service *AddUserService) Add() serializer.Response {
|
||||
user.TwoFactor = service.User.TwoFactor
|
||||
|
||||
// 检查愚蠢操作
|
||||
if user.ID == 1 && user.GroupID != 1 {
|
||||
return serializer.Err(serializer.CodeChangeGroupForDefaultUser, "", nil)
|
||||
if user.ID == 1 {
|
||||
if user.GroupID != 1 {
|
||||
return serializer.Err(serializer.CodeChangeGroupForDefaultUser, "", nil)
|
||||
}
|
||||
if user.Status != model.Active {
|
||||
return serializer.Err(serializer.CodeInvalidActionOnDefaultUser, "", nil)
|
||||
}
|
||||
}
|
||||
|
||||
if err := model.DB.Save(&user).Error; err != nil {
|
||||
|
||||
@@ -227,6 +227,16 @@ func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gi
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
|
||||
// For newer version of Cloudreve - Local Policy
|
||||
// When do not use a cdn, the downloadURL withouts hosts, like "/api/v3/file/download/xxx"
|
||||
if strings.HasPrefix(downloadURL, "/") {
|
||||
downloadURI, err := url.Parse(downloadURL)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
downloadURL = model.GetSiteURL().ResolveReference(downloadURI).String()
|
||||
}
|
||||
|
||||
var resp serializer.DocPreviewSession
|
||||
|
||||
// Use WOPI preview if available
|
||||
@@ -241,7 +251,7 @@ func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gi
|
||||
action = wopi.ActionEdit
|
||||
}
|
||||
|
||||
session, err := wopi.Default.NewSession(fs.User, &fs.FileTarget[0], action)
|
||||
session, err := wopi.Default.NewSession(fs.FileTarget[0].UserID, &fs.FileTarget[0], action)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to create WOPI session", err)
|
||||
}
|
||||
|
||||
+16
-10
@@ -22,10 +22,11 @@ type WebDAVAccountCreateService struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
// WebDAVAccountUpdateReadonlyService WebDAV 修改只读性服务
|
||||
type WebDAVAccountUpdateReadonlyService struct {
|
||||
ID uint `json:"id" binding:"required,min=1"`
|
||||
Readonly bool `json:"readonly"`
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// WebDAVMountCreateService WebDAV 挂载创建服务
|
||||
@@ -62,12 +63,17 @@ func (service *WebDAVAccountService) Delete(c *gin.Context, user *model.User) se
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}}
|
||||
// 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}
|
||||
}
|
||||
|
||||
// Accounts 列出WebDAV账号
|
||||
|
||||
Reference in New Issue
Block a user