using afero rclone backend to provide ablitiy of mounting different kinds of service;

pull/984/head
chaoqing 4 years ago
parent 65c4367689
commit af17e661df

1
.gitignore vendored

@ -27,3 +27,4 @@ version.lock
*.ini *.ini
conf/conf.ini conf/conf.ini
/statik/ /statik/
rclone.conf

@ -5,7 +5,7 @@ go 1.13
require ( require (
github.com/DATA-DOG/go-sqlmock v1.3.3 github.com/DATA-DOG/go-sqlmock v1.3.3
github.com/aliyun/aliyun-oss-go-sdk v2.0.5+incompatible github.com/aliyun/aliyun-oss-go-sdk v2.0.5+incompatible
github.com/aws/aws-sdk-go v1.31.5 github.com/aws/aws-sdk-go v1.38.22
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/duo-labs/webauthn v0.0.0-20191119193225-4bf9a0f776d4 github.com/duo-labs/webauthn v0.0.0-20191119193225-4bf9a0f776d4
github.com/fatih/color v1.7.0 github.com/fatih/color v1.7.0
@ -16,13 +16,13 @@ require (
github.com/gin-gonic/gin v1.5.0 github.com/gin-gonic/gin v1.5.0
github.com/go-ini/ini v1.50.0 github.com/go-ini/ini v1.50.0
github.com/go-mail/mail v2.3.1+incompatible github.com/go-mail/mail v2.3.1+incompatible
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/gomodule/redigo v2.0.0+incompatible github.com/gomodule/redigo v2.0.0+incompatible
github.com/google/go-querystring v1.0.0 github.com/google/go-querystring v1.1.0
github.com/gorilla/websocket v1.4.1 github.com/gorilla/websocket v1.4.2
github.com/hashicorp/go-version v1.2.0 github.com/hashicorp/go-version v1.2.0
github.com/jinzhu/gorm v1.9.11 github.com/jinzhu/gorm v1.9.11
github.com/juju/ratelimit v1.0.1 github.com/juju/ratelimit v1.0.1
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mojocn/base64Captcha v0.0.0-20190801020520-752b1cd608b2 github.com/mojocn/base64Captcha v0.0.0-20190801020520-752b1cd608b2
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
@ -33,13 +33,16 @@ require (
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/smartystreets/goconvey v1.6.4 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/speps/go-hashids v2.0.0+incompatible github.com/speps/go-hashids v2.0.0+incompatible
github.com/stretchr/testify v1.5.1 github.com/spf13/afero v1.6.0
github.com/stretchr/testify v1.7.0
github.com/tencentcloud/tencentcloud-sdk-go v3.0.125+incompatible github.com/tencentcloud/tencentcloud-sdk-go v3.0.125+incompatible
github.com/tencentyun/cos-go-sdk-v5 v0.0.0-20200120023323-87ff3bc489ac github.com/tencentyun/cos-go-sdk-v5 v0.0.0-20200120023323-87ff3bc489ac
github.com/upyun/go-sdk v2.1.0+incompatible github.com/upyun/go-sdk v2.1.0+incompatible
golang.org/x/text v0.3.2 golang.org/x/text v0.3.6
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/go-playground/validator.v9 v9.29.1 gopkg.in/go-playground/validator.v9 v9.29.1
gopkg.in/ini.v1 v1.51.0 // indirect gopkg.in/ini.v1 v1.51.0 // indirect
gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/mail.v2 v2.3.1 // indirect
) )
replace github.com/spf13/afero => github.com/chaoqing/afero v1.6.1-0.20210815024925-e47db790683a

935
go.sum

File diff suppressed because it is too large Load Diff

@ -3,7 +3,11 @@ package conf
import ( import (
"github.com/cloudreve/Cloudreve/v3/pkg/util" "github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/go-ini/ini" "github.com/go-ini/ini"
"github.com/spf13/afero"
"github.com/spf13/afero/rclonefs"
"gopkg.in/go-playground/validator.v9" "gopkg.in/go-playground/validator.v9"
"path/filepath"
"strings"
) )
// database 数据库 // database 数据库
@ -84,6 +88,13 @@ type cors struct {
ExposeHeaders []string ExposeHeaders []string
} }
// RClone配置
// Binds /mnt/ibm:ibm
type rclone struct {
Config string
Binds []string
}
var cfg *ini.File var cfg *ini.File
const defaultConf = `[System] const defaultConf = `[System]
@ -131,6 +142,7 @@ func Init(path string) {
"Redis": RedisConfig, "Redis": RedisConfig,
"Thumbnail": ThumbConfig, "Thumbnail": ThumbConfig,
"CORS": CORSConfig, "CORS": CORSConfig,
"RClone": RCloneConfig,
"Slave": SlaveConfig, "Slave": SlaveConfig,
} }
for sectionName, sectionStruct := range sections { for sectionName, sectionStruct := range sections {
@ -147,6 +159,7 @@ func Init(path string) {
util.Log() util.Log()
} }
initRCloneBind()
} }
// mapSection 将配置文件的 Section 映射到结构体上 // mapSection 将配置文件的 Section 映射到结构体上
@ -165,3 +178,36 @@ func mapSection(section string, confStruct interface{}) error {
return nil return nil
} }
func initRCloneBind(){
if RCloneConfig.Binds[0] == "UNSET"{
return
}
if ok := util.Exists(RCloneConfig.Config); ok{
_ = rclonefs.SetConfigPath(RCloneConfig.Config)
}else{
util.Log().Warning("未找到RClone配置文件: %s", RCloneConfig.Config)
return
}
bindPoints := make(map[string]afero.Fs)
bindPoints["/"] = afero.NewOsFs()
for _, kv := range RCloneConfig.Binds{
if bind := strings.SplitN(kv,":", 2); len(bind)==2{
util.Log().Info("RClone绑定: '%s' -> '%s'", bind[1], bind[0])
if target, err := filepath.Abs(bind[0]); err==nil{
bindPoints[target] = rclonefs.NewRCloneFs(bind[1])
}else{
util.Log().Warning("绑定绝对路径出错: '%s'", bind[0])
}
}else{
util.Log().Warning("RClone绑定不符合格式: %s", kv)
return
}
}
util.OS = rclonefs.NewBindPathFs(bindPoints)
}

@ -49,6 +49,12 @@ var CORSConfig = &cors{
ExposeHeaders: nil, ExposeHeaders: nil,
} }
// RCloneConfig 配置
var RCloneConfig = &rclone{
Config: "rclone.conf",
Binds: []string{"UNSET"},
}
// ThumbConfig 缩略图配置 // ThumbConfig 缩略图配置
var ThumbConfig = &thumb{ var ThumbConfig = &thumb{
MaxWidth: 400, MaxWidth: 400,

@ -83,12 +83,17 @@ func (fs *FileSystem) Compress(ctx context.Context, folderIDs, fileIDs []uint, i
saveFolder, saveFolder,
fmt.Sprintf("archive_%d.zip", time.Now().UnixNano()), fmt.Sprintf("archive_%d.zip", time.Now().UnixNano()),
) )
zipFile, err := util.CreatNestedFile(zipFilePath) zipAferoFile, err := util.CreatNestedFile(zipFilePath)
if err != nil { if err != nil {
util.Log().Warning("%s", err) util.Log().Warning("%s", err)
return "", err return "", err
} }
defer zipFile.Close() defer zipAferoFile.Close()
zipFile, ok := zipAferoFile.(*os.File)
if !ok{
return "", fmt.Errorf("not implemented for non OS compression")
}
// 创建压缩文件Writer // 创建压缩文件Writer
zipWriter := zip.NewWriter(zipFile) zipWriter := zip.NewWriter(zipFile)

@ -17,6 +17,7 @@ import (
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer" "github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util" "github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/spf13/afero"
) )
// Driver 本地策略适配器 // Driver 本地策略适配器
@ -32,7 +33,7 @@ func (handler Driver) List(ctx context.Context, path string, recursive bool) ([]
root := util.RelativePath(filepath.FromSlash(path)) root := util.RelativePath(filepath.FromSlash(path))
// 开始遍历路径下的文件、目录 // 开始遍历路径下的文件、目录
err := filepath.Walk(root, err := afero.Walk(util.OS, root,
func(path string, info os.FileInfo, err error) error { func(path string, info os.FileInfo, err error) error {
// 跳过根目录 // 跳过根目录
if path == root { if path == root {
@ -73,7 +74,7 @@ func (handler Driver) List(ctx context.Context, path string, recursive bool) ([]
// Get 获取文件内容 // Get 获取文件内容
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) { func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
// 打开文件 // 打开文件
file, err := os.Open(util.RelativePath(path)) file, err := util.OS.Open(util.RelativePath(path))
if err != nil { if err != nil {
util.Log().Debug("无法打开文件:%s", err) util.Log().Debug("无法打开文件:%s", err)
return nil, err return nil, err
@ -111,7 +112,7 @@ func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, s
// 如果目标目录不存在,创建 // 如果目标目录不存在,创建
basePath := filepath.Dir(dst) basePath := filepath.Dir(dst)
if !util.Exists(basePath) { if !util.Exists(basePath) {
err := os.MkdirAll(basePath, 0744) err := util.OS.MkdirAll(basePath, 0744)
if err != nil { if err != nil {
util.Log().Warning("无法创建目录,%s", err) util.Log().Warning("无法创建目录,%s", err)
return err return err
@ -119,7 +120,7 @@ func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, s
} }
// 创建目标文件 // 创建目标文件
out, err := os.Create(dst) out, err := util.OS.Create(dst)
if err != nil { if err != nil {
util.Log().Warning("无法创建文件,%s", err) util.Log().Warning("无法创建文件,%s", err)
return err return err
@ -140,7 +141,7 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err
for _, value := range files { for _, value := range files {
filePath := util.RelativePath(filepath.FromSlash(value)) filePath := util.RelativePath(filepath.FromSlash(value))
if util.Exists(filePath) { if util.Exists(filePath) {
err := os.Remove(filePath) err := util.OS.Remove(filePath)
if err != nil { if err != nil {
util.Log().Warning("无法删除文件,%s", err) util.Log().Warning("无法删除文件,%s", err)
retErr = err retErr = err
@ -149,7 +150,7 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err
} }
// 尝试删除文件的缩略图(如果有) // 尝试删除文件的缩略图(如果有)
_ = os.Remove(util.RelativePath(value + conf.ThumbConfig.FileSuffix)) _ = util.OS.Remove(util.RelativePath(value + conf.ThumbConfig.FileSuffix))
} }
return deleteFailed, retErr return deleteFailed, retErr

@ -21,7 +21,7 @@ func TestHandler_Put(t *testing.T) {
asserts := assert.New(t) asserts := assert.New(t)
handler := Driver{} handler := Driver{}
ctx := context.WithValue(context.Background(), fsctx.DisableOverwrite, true) ctx := context.WithValue(context.Background(), fsctx.DisableOverwrite, true)
os.Remove(util.RelativePath("test/test/txt")) util.OS.Remove(util.RelativePath("test/test/txt"))
testCases := []struct { testCases := []struct {
file io.ReadCloser file io.ReadCloser
@ -62,16 +62,16 @@ func TestHandler_Delete(t *testing.T) {
ctx := context.Background() ctx := context.Background()
filePath := util.RelativePath("test.file") filePath := util.RelativePath("test.file")
file, err := os.Create(filePath) file, err := util.OS.Create(filePath)
asserts.NoError(err) asserts.NoError(err)
_ = file.Close() _ = file.Close()
list, err := handler.Delete(ctx, []string{"test.file"}) list, err := handler.Delete(ctx, []string{"test.file"})
asserts.Equal([]string{}, list) asserts.Equal([]string{}, list)
asserts.NoError(err) asserts.NoError(err)
file, err = os.Create(filePath) file, err = util.OS.Create(filePath)
_ = file.Close() _ = file.Close()
file, _ = os.OpenFile(filePath, os.O_RDWR, os.FileMode(0)) file, _ = util.OS.OpenFile(filePath, os.O_RDWR, os.FileMode(0))
asserts.NoError(err) asserts.NoError(err)
list, err = handler.Delete(ctx, []string{"test.file", "test.notexist"}) list, err = handler.Delete(ctx, []string{"test.file", "test.notexist"})
file.Close() file.Close()
@ -82,7 +82,7 @@ func TestHandler_Delete(t *testing.T) {
asserts.Equal([]string{}, list) asserts.Equal([]string{}, list)
asserts.NoError(err) asserts.NoError(err)
file, err = os.Create(filePath) file, err = util.OS.Create(filePath)
asserts.NoError(err) asserts.NoError(err)
list, err = handler.Delete(ctx, []string{"test.file"}) list, err = handler.Delete(ctx, []string{"test.file"})
_ = file.Close() _ = file.Close()
@ -97,7 +97,7 @@ func TestHandler_Get(t *testing.T) {
defer cancel() defer cancel()
// 成功 // 成功
file, err := os.Create(util.RelativePath("TestHandler_Get.txt")) file, err := util.OS.Create(util.RelativePath("TestHandler_Get.txt"))
asserts.NoError(err) asserts.NoError(err)
_ = file.Close() _ = file.Close()
@ -116,7 +116,7 @@ func TestHandler_Thumb(t *testing.T) {
asserts := assert.New(t) asserts := assert.New(t)
handler := Driver{} handler := Driver{}
ctx := context.Background() ctx := context.Background()
file, err := os.Create(util.RelativePath("TestHandler_Thumb" + conf.ThumbConfig.FileSuffix)) file, err := util.OS.Create(util.RelativePath("TestHandler_Thumb" + conf.ThumbConfig.FileSuffix))
asserts.NoError(err) asserts.NoError(err)
file.Close() file.Close()

@ -1,14 +1,19 @@
package util package util
import ( import (
"github.com/spf13/afero"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
) )
var (
OS = afero.NewOsFs()
)
// Exists reports whether the named file or directory exists. // Exists reports whether the named file or directory exists.
func Exists(name string) bool { func Exists(name string) bool {
if _, err := os.Stat(name); err != nil { if _, err := OS.Stat(name); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return false return false
} }
@ -17,22 +22,22 @@ func Exists(name string) bool {
} }
// CreatNestedFile 给定path创建文件如果目录不存在就递归创建 // CreatNestedFile 给定path创建文件如果目录不存在就递归创建
func CreatNestedFile(path string) (*os.File, error) { func CreatNestedFile(path string) (afero.File, error) {
basePath := filepath.Dir(path) basePath := filepath.Dir(path)
if !Exists(basePath) { if !Exists(basePath) {
err := os.MkdirAll(basePath, 0700) err := OS.MkdirAll(basePath, 0700)
if err != nil { if err != nil {
Log().Warning("无法创建目录,%s", err) Log().Warning("无法创建目录,%s", err)
return nil, err return nil, err
} }
} }
return os.Create(path) return OS.Create(path)
} }
// IsEmpty 返回给定目录是否为空目录 // IsEmpty 返回给定目录是否为空目录
func IsEmpty(name string) (bool, error) { func IsEmpty(name string) (bool, error) {
f, err := os.Open(name) f, err := OS.Open(name)
if err != nil { if err != nil {
return false, err return false, err
} }

Loading…
Cancel
Save