mirror of
https://github.com/cloudreve/Cloudreve.git
synced 2024-04-21 12:31:40 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
959e1f2576 |
@@ -5,9 +5,36 @@ on:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get github.com/rakyll/statik
|
||||
export PATH=$PATH:~/go/bin/
|
||||
statik -src=models -f
|
||||
|
||||
- name: Test
|
||||
run: go test -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-18.04
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get github.com/rakyll/statik
|
||||
export PATH=$PATH:~/go/bin/
|
||||
statik -src=models -f
|
||||
|
||||
- name: Test
|
||||
run: go test -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Upload binary files (linux_arm)
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: cloudreve_linux_arm
|
||||
path: release/cloudreve*linux_arm.*
|
||||
|
||||
- name: Upload binary files (linux_arm64)
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: cloudreve_linux_arm64
|
||||
path: release/cloudreve*linux_arm64.*
|
||||
@@ -27,4 +27,3 @@ version.lock
|
||||
*.ini
|
||||
conf/conf.ini
|
||||
/statik/
|
||||
.vscode/
|
||||
|
||||
+1
-1
Submodule assets updated: efe2981761...1c827ee20a
+1
-1
@@ -34,7 +34,7 @@ type GitHubRelease struct {
|
||||
|
||||
// CheckUpdate 检查更新
|
||||
func CheckUpdate() {
|
||||
client := request.NewClient()
|
||||
client := request.HTTPClient{}
|
||||
res, err := client.Request("GET", "https://api.github.com/repos/cloudreve/cloudreve/releases", nil).GetResponse()
|
||||
if err != nil {
|
||||
util.Log().Warning("更新检查失败, %s", err)
|
||||
|
||||
+9
-83
@@ -5,11 +5,9 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/crontab"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/slave"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -22,86 +20,14 @@ func Init(path string) {
|
||||
if !conf.SystemConfig.Debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
dependencies := []struct {
|
||||
mode string
|
||||
factory func()
|
||||
}{
|
||||
{
|
||||
"both",
|
||||
func() {
|
||||
cache.Init()
|
||||
},
|
||||
},
|
||||
{
|
||||
"master",
|
||||
func() {
|
||||
model.Init()
|
||||
},
|
||||
},
|
||||
{
|
||||
"both",
|
||||
func() {
|
||||
task.Init()
|
||||
},
|
||||
},
|
||||
{
|
||||
"master",
|
||||
func() {
|
||||
cluster.Init()
|
||||
},
|
||||
},
|
||||
{
|
||||
"master",
|
||||
func() {
|
||||
aria2.Init(false)
|
||||
},
|
||||
},
|
||||
{
|
||||
"master",
|
||||
func() {
|
||||
email.Init()
|
||||
},
|
||||
},
|
||||
{
|
||||
"master",
|
||||
func() {
|
||||
crontab.Init()
|
||||
},
|
||||
},
|
||||
{
|
||||
"master",
|
||||
func() {
|
||||
InitStatic()
|
||||
},
|
||||
},
|
||||
{
|
||||
"slave",
|
||||
func() {
|
||||
slave.Init()
|
||||
},
|
||||
},
|
||||
{
|
||||
"both",
|
||||
func() {
|
||||
auth.Init()
|
||||
},
|
||||
},
|
||||
cache.Init()
|
||||
if conf.SystemConfig.Mode == "master" {
|
||||
model.Init()
|
||||
task.Init()
|
||||
aria2.Init(false)
|
||||
email.Init()
|
||||
crontab.Init()
|
||||
InitStatic()
|
||||
}
|
||||
|
||||
for _, dependency := range dependencies {
|
||||
switch dependency.mode {
|
||||
case "master":
|
||||
if conf.SystemConfig.Mode == "master" {
|
||||
dependency.factory()
|
||||
}
|
||||
case "slave":
|
||||
if conf.SystemConfig.Mode == "slave" {
|
||||
dependency.factory()
|
||||
}
|
||||
default:
|
||||
dependency.factory()
|
||||
}
|
||||
}
|
||||
|
||||
auth.Init()
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ require (
|
||||
github.com/gin-gonic/gin v1.5.0
|
||||
github.com/go-ini/ini v1.50.0
|
||||
github.com/go-mail/mail v2.3.1+incompatible
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
github.com/gomodule/redigo v2.0.0+incompatible
|
||||
github.com/google/go-querystring v1.0.0
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
@@ -32,6 +31,7 @@ require (
|
||||
github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1
|
||||
github.com/rakyll/statik v0.1.7
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/speps/go-hashids v2.0.0+incompatible
|
||||
github.com/stretchr/testify v1.5.1
|
||||
|
||||
@@ -77,8 +77,6 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
@@ -210,6 +208,8 @@ github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Ung
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
|
||||
@@ -51,11 +51,12 @@ func main() {
|
||||
|
||||
// 如果启用了Unix
|
||||
if conf.UnixConfig.Listen != "" {
|
||||
util.Log().Info("开始监听 %s", conf.UnixConfig.Listen)
|
||||
if err := api.RunUnix(conf.UnixConfig.Listen); err != nil {
|
||||
util.Log().Error("无法监听[%s],%s", conf.UnixConfig.Listen, err)
|
||||
}
|
||||
return
|
||||
go func() {
|
||||
util.Log().Info("开始监听 %s", conf.UnixConfig.Listen)
|
||||
if err := api.RunUnix(conf.UnixConfig.Listen); err != nil {
|
||||
util.Log().Error("无法监听[%s],%s", conf.UnixConfig.Listen, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
util.Log().Info("开始监听 %s", conf.SystemConfig.Listen)
|
||||
|
||||
+6
-4
@@ -22,14 +22,16 @@ import (
|
||||
)
|
||||
|
||||
// SignRequired 验证请求签名
|
||||
func SignRequired(authInstance auth.Auth) gin.HandlerFunc {
|
||||
func SignRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var err error
|
||||
switch c.Request.Method {
|
||||
case "PUT", "POST", "PATCH":
|
||||
err = auth.CheckRequest(authInstance, c.Request)
|
||||
case "PUT", "POST":
|
||||
err = auth.CheckRequest(auth.General, c.Request)
|
||||
// TODO 生产环境去掉下一行
|
||||
//err = nil
|
||||
default:
|
||||
err = auth.CheckURI(authInstance, c.Request.URL)
|
||||
err = auth.CheckURI(auth.General, c.Request.URL)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -87,10 +87,11 @@ func TestAuthRequired(t *testing.T) {
|
||||
|
||||
func TestSignRequired(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
auth.General = auth.HMACAuth{SecretKey: []byte(util.RandStringRunes(256))}
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request, _ = http.NewRequest("GET", "/test", nil)
|
||||
SignRequiredFunc := SignRequired(auth.HMACAuth{SecretKey: []byte(util.RandStringRunes(256))})
|
||||
SignRequiredFunc := SignRequired()
|
||||
|
||||
// 鉴权失败
|
||||
SignRequiredFunc(c)
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/recaptcha"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
captcha "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha/v20190722"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type req struct {
|
||||
CaptchaCode string `json:"captchaCode"`
|
||||
Ticket string `json:"ticket"`
|
||||
Randstr string `json:"randstr"`
|
||||
}
|
||||
|
||||
// CaptchaRequired 验证请求签名
|
||||
func CaptchaRequired(configName string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 相关设定
|
||||
options := model.GetSettingByNames(configName,
|
||||
"captcha_type",
|
||||
"captcha_ReCaptchaSecret",
|
||||
"captcha_TCaptcha_SecretId",
|
||||
"captcha_TCaptcha_SecretKey",
|
||||
"captcha_TCaptcha_CaptchaAppId",
|
||||
"captcha_TCaptcha_AppSecretKey")
|
||||
// 检查验证码
|
||||
isCaptchaRequired := model.IsTrueVal(options[configName])
|
||||
|
||||
if isCaptchaRequired {
|
||||
var service req
|
||||
bodyCopy := new(bytes.Buffer)
|
||||
_, err := io.Copy(bodyCopy, c.Request.Body)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.ParamErr("验证码错误", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
bodyData := bodyCopy.Bytes()
|
||||
err = json.Unmarshal(bodyData, &service)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.ParamErr("验证码错误", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Request.Body = ioutil.NopCloser(bytes.NewReader(bodyData))
|
||||
switch options["captcha_type"] {
|
||||
case "normal":
|
||||
captchaID := util.GetSession(c, "captchaID")
|
||||
util.DeleteSession(c, "captchaID")
|
||||
if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) {
|
||||
c.JSON(200, serializer.ParamErr("验证码错误", nil))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
case "recaptcha":
|
||||
reCAPTCHA, err := recaptcha.NewReCAPTCHA(options["captcha_ReCaptchaSecret"], recaptcha.V2, 10*time.Second)
|
||||
if err != nil {
|
||||
util.Log().Warning("reCAPTCHA 验证错误, %s", err)
|
||||
c.Abort()
|
||||
break
|
||||
}
|
||||
|
||||
err = reCAPTCHA.Verify(service.CaptchaCode)
|
||||
if err != nil {
|
||||
util.Log().Warning("reCAPTCHA 验证错误, %s", err)
|
||||
c.JSON(200, serializer.ParamErr("验证失败,请刷新网页后再次验证", nil))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
case "tcaptcha":
|
||||
credential := common.NewCredential(
|
||||
options["captcha_TCaptcha_SecretId"],
|
||||
options["captcha_TCaptcha_SecretKey"],
|
||||
)
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "captcha.tencentcloudapi.com"
|
||||
client, _ := captcha.NewClient(credential, "", cpf)
|
||||
request := captcha.NewDescribeCaptchaResultRequest()
|
||||
request.CaptchaType = common.Uint64Ptr(9)
|
||||
appid, _ := strconv.Atoi(options["captcha_TCaptcha_CaptchaAppId"])
|
||||
request.CaptchaAppId = common.Uint64Ptr(uint64(appid))
|
||||
request.AppSecretKey = common.StringPtr(options["captcha_TCaptcha_AppSecretKey"])
|
||||
request.Ticket = common.StringPtr(service.Ticket)
|
||||
request.Randstr = common.StringPtr(service.Randstr)
|
||||
request.UserIp = common.StringPtr(c.ClientIP())
|
||||
response, err := client.DescribeCaptchaResult(request)
|
||||
if err != nil {
|
||||
util.Log().Warning("TCaptcha 验证错误, %s", err)
|
||||
c.Abort()
|
||||
break
|
||||
}
|
||||
|
||||
if *response.Response.CaptchaCode != int64(1) {
|
||||
c.JSON(200, serializer.ParamErr("验证失败,请刷新网页后再次验证", nil))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type errReader int
|
||||
|
||||
func (errReader) Read(p []byte) (n int, err error) {
|
||||
return 0, errors.New("test error")
|
||||
}
|
||||
|
||||
func TestCaptchaRequired_General(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
// 未启用验证码
|
||||
{
|
||||
cache.SetSettings(map[string]string{
|
||||
"login_captcha": "0",
|
||||
"captcha_type": "1",
|
||||
"captcha_ReCaptchaSecret": "1",
|
||||
"captcha_TCaptcha_SecretId": "1",
|
||||
"captcha_TCaptcha_SecretKey": "1",
|
||||
"captcha_TCaptcha_CaptchaAppId": "1",
|
||||
"captcha_TCaptcha_AppSecretKey": "1",
|
||||
}, "setting_")
|
||||
TestFunc := CaptchaRequired("login_captcha")
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Params = []gin.Param{}
|
||||
c.Request, _ = http.NewRequest("GET", "/", nil)
|
||||
TestFunc(c)
|
||||
asserts.False(c.IsAborted())
|
||||
}
|
||||
|
||||
// body 无法读取
|
||||
{
|
||||
cache.SetSettings(map[string]string{
|
||||
"login_captcha": "1",
|
||||
"captcha_type": "1",
|
||||
"captcha_ReCaptchaSecret": "1",
|
||||
"captcha_TCaptcha_SecretId": "1",
|
||||
"captcha_TCaptcha_SecretKey": "1",
|
||||
"captcha_TCaptcha_CaptchaAppId": "1",
|
||||
"captcha_TCaptcha_AppSecretKey": "1",
|
||||
}, "setting_")
|
||||
TestFunc := CaptchaRequired("login_captcha")
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Params = []gin.Param{}
|
||||
c.Request, _ = http.NewRequest("GET", "/", errReader(1))
|
||||
TestFunc(c)
|
||||
asserts.True(c.IsAborted())
|
||||
}
|
||||
|
||||
// body JSON 解析失败
|
||||
{
|
||||
cache.SetSettings(map[string]string{
|
||||
"login_captcha": "1",
|
||||
"captcha_type": "1",
|
||||
"captcha_ReCaptchaSecret": "1",
|
||||
"captcha_TCaptcha_SecretId": "1",
|
||||
"captcha_TCaptcha_SecretKey": "1",
|
||||
"captcha_TCaptcha_CaptchaAppId": "1",
|
||||
"captcha_TCaptcha_AppSecretKey": "1",
|
||||
}, "setting_")
|
||||
TestFunc := CaptchaRequired("login_captcha")
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Params = []gin.Param{}
|
||||
r := bytes.NewReader([]byte("123"))
|
||||
c.Request, _ = http.NewRequest("GET", "/", r)
|
||||
TestFunc(c)
|
||||
asserts.True(c.IsAborted())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptchaRequired_Normal(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
// 验证码错误
|
||||
{
|
||||
cache.SetSettings(map[string]string{
|
||||
"login_captcha": "1",
|
||||
"captcha_type": "normal",
|
||||
"captcha_ReCaptchaSecret": "1",
|
||||
"captcha_TCaptcha_SecretId": "1",
|
||||
"captcha_TCaptcha_SecretKey": "1",
|
||||
"captcha_TCaptcha_CaptchaAppId": "1",
|
||||
"captcha_TCaptcha_AppSecretKey": "1",
|
||||
}, "setting_")
|
||||
TestFunc := CaptchaRequired("login_captcha")
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Params = []gin.Param{}
|
||||
r := bytes.NewReader([]byte("{}"))
|
||||
c.Request, _ = http.NewRequest("GET", "/", r)
|
||||
Session("233")(c)
|
||||
TestFunc(c)
|
||||
asserts.True(c.IsAborted())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptchaRequired_Recaptcha(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
// 无法初始化reCaptcha实例
|
||||
{
|
||||
cache.SetSettings(map[string]string{
|
||||
"login_captcha": "1",
|
||||
"captcha_type": "recaptcha",
|
||||
"captcha_ReCaptchaSecret": "",
|
||||
"captcha_TCaptcha_SecretId": "1",
|
||||
"captcha_TCaptcha_SecretKey": "1",
|
||||
"captcha_TCaptcha_CaptchaAppId": "1",
|
||||
"captcha_TCaptcha_AppSecretKey": "1",
|
||||
}, "setting_")
|
||||
TestFunc := CaptchaRequired("login_captcha")
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Params = []gin.Param{}
|
||||
r := bytes.NewReader([]byte("{}"))
|
||||
c.Request, _ = http.NewRequest("GET", "/", r)
|
||||
TestFunc(c)
|
||||
asserts.True(c.IsAborted())
|
||||
}
|
||||
|
||||
// 验证码错误
|
||||
{
|
||||
cache.SetSettings(map[string]string{
|
||||
"login_captcha": "1",
|
||||
"captcha_type": "recaptcha",
|
||||
"captcha_ReCaptchaSecret": "233",
|
||||
"captcha_TCaptcha_SecretId": "1",
|
||||
"captcha_TCaptcha_SecretKey": "1",
|
||||
"captcha_TCaptcha_CaptchaAppId": "1",
|
||||
"captcha_TCaptcha_AppSecretKey": "1",
|
||||
}, "setting_")
|
||||
TestFunc := CaptchaRequired("login_captcha")
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Params = []gin.Param{}
|
||||
r := bytes.NewReader([]byte("{}"))
|
||||
c.Request, _ = http.NewRequest("GET", "/", r)
|
||||
TestFunc(c)
|
||||
asserts.True(c.IsAborted())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptchaRequired_Tcaptcha(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
// 验证出错
|
||||
{
|
||||
cache.SetSettings(map[string]string{
|
||||
"login_captcha": "1",
|
||||
"captcha_type": "tcaptcha",
|
||||
"captcha_ReCaptchaSecret": "",
|
||||
"captcha_TCaptcha_SecretId": "1",
|
||||
"captcha_TCaptcha_SecretKey": "1",
|
||||
"captcha_TCaptcha_CaptchaAppId": "1",
|
||||
"captcha_TCaptcha_AppSecretKey": "1",
|
||||
}, "setting_")
|
||||
TestFunc := CaptchaRequired("login_captcha")
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Params = []gin.Param{}
|
||||
r := bytes.NewReader([]byte("{}"))
|
||||
c.Request, _ = http.NewRequest("GET", "/", r)
|
||||
TestFunc(c)
|
||||
asserts.True(c.IsAborted())
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/slave"
|
||||
"github.com/gin-gonic/gin"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// MasterMetadata 解析主机节点发来请求的包含主机节点信息的元数据
|
||||
func MasterMetadata() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Set("MasterSiteID", c.GetHeader("X-Site-Id"))
|
||||
c.Set("MasterSiteURL", c.GetHeader("X-Site-Url"))
|
||||
c.Set("MasterVersion", c.GetHeader("X-Cloudreve-Version"))
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// UseSlaveAria2Instance 从机用于获取对应主机节点的Aria2实例
|
||||
func UseSlaveAria2Instance() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if siteID, exist := c.Get("MasterSiteID"); exist {
|
||||
// 获取对应主机节点的从机Aria2实例
|
||||
caller, err := slave.DefaultController.GetAria2Instance(siteID.(string))
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(serializer.CodeNotSet, "无法获取 Aria2 实例", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("MasterAria2Instance", caller)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, serializer.ParamErr("未知的主机节点ID", nil))
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
|
||||
func SlaveRPCSignRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
nodeID, err := strconv.ParseUint(c.GetHeader("X-Node-Id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.ParamErr("未知的主机节点ID", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
slaveNode := cluster.Default.GetNodeByID(uint(nodeID))
|
||||
if slaveNode == nil {
|
||||
c.JSON(200, serializer.ParamErr("未知的主机节点ID", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
SignRequired(slaveNode.MasterAuthInstance())(c)
|
||||
|
||||
}
|
||||
}
|
||||
+4
-15
@@ -18,13 +18,12 @@ type Download struct {
|
||||
DownloadedSize uint64 // 文件大小
|
||||
GID string `gorm:"size:32,index:gid"` // 任务ID
|
||||
Speed int // 下载速度
|
||||
Parent string `gorm:"type:text"` // 存储目录
|
||||
Attrs string `gorm:"size:4294967295"` // 任务状态属性
|
||||
Error string `gorm:"type:text"` // 错误描述
|
||||
Dst string `gorm:"type:text"` // 用户文件系统存储父目录路径
|
||||
Parent string `gorm:"type:text"` // 存储目录
|
||||
Attrs string `gorm:"size:65535"` // 任务状态属性
|
||||
Error string `gorm:"type:text"` // 错误描述
|
||||
Dst string `gorm:"type:text"` // 用户文件系统存储父目录路径
|
||||
UserID uint // 发起者UID
|
||||
TaskID uint // 对应的转存任务ID
|
||||
NodeID uint // 处理任务的节点ID
|
||||
|
||||
// 关联模型
|
||||
User *User `gorm:"PRELOAD:false,association_autoupdate:false"`
|
||||
@@ -115,13 +114,3 @@ func (task *Download) GetOwner() *User {
|
||||
func (download *Download) Delete() error {
|
||||
return DB.Model(download).Delete(download).Error
|
||||
}
|
||||
|
||||
// GetNodeID 返回任务所属节点ID
|
||||
func (task *Download) GetNodeID() uint {
|
||||
// 兼容3.4版本之前生成的下载记录
|
||||
if task.NodeID == 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
return task.NodeID
|
||||
}
|
||||
|
||||
+35
-2
@@ -26,6 +26,12 @@ type File struct {
|
||||
|
||||
// 数据库忽略字段
|
||||
Position string `gorm:"-"`
|
||||
|
||||
// exif 信息
|
||||
ExifModel string
|
||||
ExifDateTime time.Time
|
||||
ExifLatLong string
|
||||
ExifAddress string
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -79,6 +85,16 @@ func GetFilesByIDs(ids []uint, uid uint) ([]File, error) {
|
||||
return files, result.Error
|
||||
}
|
||||
|
||||
// GetEmptyLocationFilesByPage 分页获取所有经纬度未更新文件
|
||||
func GetEmptyLocationFilesByPage(page uint, pageSize uint) ([]File, error) {
|
||||
var files []File
|
||||
var result *gorm.DB
|
||||
var offset = int(page) * int(pageSize) - int(pageSize)
|
||||
result = DB.Where("exif_lat_long != '' AND (exif_address is null or exif_address = '')").Limit(pageSize).
|
||||
Offset(offset).Find(&files)
|
||||
return files, result.Error
|
||||
}
|
||||
|
||||
// GetFilesByKeywords 根据关键字搜索文件,
|
||||
// UID为0表示忽略用户,只根据文件ID检索
|
||||
func GetFilesByKeywords(uid uint, keywords ...interface{}) ([]File, error) {
|
||||
@@ -186,12 +202,12 @@ func (file *File) Rename(new string) error {
|
||||
|
||||
// UpdatePicInfo 更新文件的图像信息
|
||||
func (file *File) UpdatePicInfo(value string) error {
|
||||
return DB.Model(&file).Set("gorm:association_autoupdate", false).Update("pic_info", value).Error
|
||||
return DB.Model(&file).Update("pic_info", value).Error
|
||||
}
|
||||
|
||||
// UpdateSize 更新文件的大小信息
|
||||
func (file *File) UpdateSize(value uint64) error {
|
||||
return DB.Model(&file).Set("gorm:association_autoupdate", false).Update("size", value).Error
|
||||
return DB.Model(&file).Update("size", value).Error
|
||||
}
|
||||
|
||||
// UpdateSourceName 更新文件的源文件名
|
||||
@@ -199,6 +215,23 @@ func (file *File) UpdateSourceName(value string) error {
|
||||
return DB.Model(&file).Set("gorm:association_autoupdate", false).Update("source_name", value).Error
|
||||
}
|
||||
|
||||
// UpdatePicExifModel 更新文件的设备信息
|
||||
func (file *File) UpdatePicExifModel(value string) error {
|
||||
return DB.Model(&file).Update("exif_model", value).Error
|
||||
}
|
||||
// UpdatePicExifDateTime 更新图片EXIF时间
|
||||
func (file *File) UpdatePicExifDateTime(value time.Time) error {
|
||||
return DB.Model(&file).Update("exif_date_time", value).Error
|
||||
}
|
||||
// UpdatePicExifLatLong 更新图片EXIF坐标
|
||||
func (file *File) UpdatePicExifLatLong(value string) error {
|
||||
return DB.Model(&file).Update("exif_lat_long", value).Error
|
||||
}
|
||||
// UpdatePicExifAddress 更新图片EXIF位置信息
|
||||
func (file *File) UpdatePicExifAddress(value string) error {
|
||||
return DB.Model(&file).Update("exif_address", value).Error
|
||||
}
|
||||
|
||||
/*
|
||||
实现 webdav.FileInfo 接口
|
||||
*/
|
||||
|
||||
@@ -44,26 +44,6 @@ func (folder *Folder) GetChild(name string) (*Folder, error) {
|
||||
return &resFolder, err
|
||||
}
|
||||
|
||||
// TraceRoot 向上递归查找父目录
|
||||
func (folder *Folder) TraceRoot() error {
|
||||
if folder.ParentID == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var parentFolder Folder
|
||||
err := DB.
|
||||
Where("id = ? AND owner_id = ?", folder.ParentID, folder.OwnerID).
|
||||
First(&parentFolder).Error
|
||||
|
||||
if err == nil {
|
||||
err := parentFolder.TraceRoot()
|
||||
folder.Position = path.Join(parentFolder.Position, parentFolder.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetChildFolder 查找子目录
|
||||
func (folder *Folder) GetChildFolder() ([]Folder, error) {
|
||||
var folders []Folder
|
||||
|
||||
@@ -530,37 +530,3 @@ func TestFolder_FileInfoInterface(t *testing.T) {
|
||||
asserts.True(folder.IsDir())
|
||||
asserts.Equal("/test", folder.GetPosition())
|
||||
}
|
||||
|
||||
func TestTraceRoot(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
var parentId uint
|
||||
parentId = 5
|
||||
folder := Folder{
|
||||
ParentID: &parentId,
|
||||
OwnerID: 1,
|
||||
Name: "test_name",
|
||||
}
|
||||
|
||||
// 成功
|
||||
{
|
||||
mock.ExpectQuery("SELECT(.+)").WithArgs(5, 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "parent_id"}).AddRow(5, "parent", 1))
|
||||
mock.ExpectQuery("SELECT(.+)").WithArgs(1, 0).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(5, "/"))
|
||||
asserts.NoError(folder.TraceRoot())
|
||||
asserts.Equal("/parent", folder.Position)
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// 出现错误
|
||||
// 成功
|
||||
{
|
||||
mock.ExpectQuery("SELECT(.+)").WithArgs(5, 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "parent_id"}).AddRow(5, "parent", 1))
|
||||
mock.ExpectQuery("SELECT(.+)").WithArgs(1, 0).
|
||||
WillReturnError(errors.New("error"))
|
||||
asserts.Error(folder.TraceRoot())
|
||||
asserts.Equal("parent", folder.Position)
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
}
|
||||
|
||||
+6
-13
@@ -9,9 +9,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
_ "github.com/jinzhu/gorm/dialects/mssql"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
)
|
||||
|
||||
@@ -35,21 +33,16 @@ func Init() {
|
||||
case "UNSET", "sqlite", "sqlite3":
|
||||
// 未指定数据库或者明确指定为 sqlite 时,使用 SQLite3 数据库
|
||||
db, err = gorm.Open("sqlite3", util.RelativePath(conf.DatabaseConfig.DBFile))
|
||||
case "postgres":
|
||||
db, err = gorm.Open(conf.DatabaseConfig.Type, fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable",
|
||||
conf.DatabaseConfig.Host,
|
||||
conf.DatabaseConfig.User,
|
||||
conf.DatabaseConfig.Password,
|
||||
conf.DatabaseConfig.Name,
|
||||
conf.DatabaseConfig.Port))
|
||||
case "mysql", "mssql":
|
||||
db, err = gorm.Open(conf.DatabaseConfig.Type, fmt.Sprintf("%s:%s@(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
|
||||
case "mysql":
|
||||
// 当前只支持 sqlite3 与 mysql 数据库
|
||||
// TODO: import 其他 gorm 支持的主流数据库?否则直接 Open 没有任何意义。
|
||||
// TODO: 数据库连接其他参数允许用户自定义?譬如编码更换为 utf8mb4 以支持表情。
|
||||
db, err = gorm.Open("mysql", fmt.Sprintf("%s:%s@(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local",
|
||||
conf.DatabaseConfig.User,
|
||||
conf.DatabaseConfig.Password,
|
||||
conf.DatabaseConfig.Host,
|
||||
conf.DatabaseConfig.Port,
|
||||
conf.DatabaseConfig.Name,
|
||||
conf.DatabaseConfig.Charset))
|
||||
conf.DatabaseConfig.Name))
|
||||
default:
|
||||
util.Log().Panic("不支持数据库类型: %s", conf.DatabaseConfig.Type)
|
||||
}
|
||||
|
||||
+9
-38
@@ -5,7 +5,6 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/fatih/color"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
@@ -35,9 +34,8 @@ func migration() {
|
||||
if conf.DatabaseConfig.Type == "mysql" {
|
||||
DB = DB.Set("gorm:table_options", "ENGINE=InnoDB")
|
||||
}
|
||||
|
||||
DB.AutoMigrate(&User{}, &Setting{}, &Group{}, &Policy{}, &Folder{}, &File{}, &Share{},
|
||||
&Task{}, &Download{}, &Tag{}, &Webdav{}, &Node{})
|
||||
&Task{}, &Download{}, &Tag{}, &Webdav{})
|
||||
|
||||
// 创建初始存储策略
|
||||
addDefaultPolicy()
|
||||
@@ -48,9 +46,6 @@ func migration() {
|
||||
// 创建初始管理员账户
|
||||
addDefaultUser()
|
||||
|
||||
// 创建初始节点
|
||||
addDefaultNode()
|
||||
|
||||
// 向设置数据表添加初始设置
|
||||
addDefaultSettings()
|
||||
|
||||
@@ -78,8 +73,6 @@ func addDefaultPolicy() {
|
||||
}
|
||||
|
||||
func addDefaultSettings() {
|
||||
siteID, _ := uuid.NewV4()
|
||||
|
||||
defaultSettings := []Setting{
|
||||
{Name: "siteURL", Value: `http://localhost`, Type: "basic"},
|
||||
{Name: "siteName", Value: `Cloudreve`, Type: "basic"},
|
||||
@@ -90,7 +83,6 @@ func addDefaultSettings() {
|
||||
{Name: "siteDes", Value: `Cloudreve`, Type: "basic"},
|
||||
{Name: "siteTitle", Value: `平步云端`, Type: "basic"},
|
||||
{Name: "siteScript", Value: ``, Type: "basic"},
|
||||
{Name: "siteID", Value: siteID.String(), Type: "basic"},
|
||||
{Name: "fromName", Value: `Cloudreve`, Type: "mail"},
|
||||
{Name: "mail_keepalive", Value: `30`, Type: "mail"},
|
||||
{Name: "fromAdress", Value: `no-reply@acg.blue`, Type: "mail"},
|
||||
@@ -108,14 +100,10 @@ func addDefaultSettings() {
|
||||
{Name: "upload_credential_timeout", Value: `1800`, Type: "timeout"},
|
||||
{Name: "upload_session_timeout", Value: `86400`, Type: "timeout"},
|
||||
{Name: "slave_api_timeout", Value: `60`, Type: "timeout"},
|
||||
{Name: "slave_node_retry", Value: `3`, Type: "slave"},
|
||||
{Name: "slave_ping_interval", Value: `60`, Type: "slave"},
|
||||
{Name: "slave_recover_interval", Value: `120`, Type: "slave"},
|
||||
{Name: "slave_transfer_timeout", Value: `172800`, Type: "timeout"},
|
||||
{Name: "onedrive_monitor_timeout", Value: `600`, Type: "timeout"},
|
||||
{Name: "share_download_session_timeout", Value: `2073600`, Type: "timeout"},
|
||||
{Name: "onedrive_callback_check", Value: `20`, Type: "timeout"},
|
||||
{Name: "folder_props_timeout", Value: `300`, Type: "timeout"},
|
||||
{Name: "aria2_call_timeout", Value: `5`, Type: "timeout"},
|
||||
{Name: "onedrive_chunk_retries", Value: `1`, Type: "retry"},
|
||||
{Name: "onedrive_source_timeout", Value: `1800`, Type: "timeout"},
|
||||
{Name: "reset_after_upload_failed", Value: `0`, Type: "upload"},
|
||||
@@ -142,6 +130,11 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
|
||||
{Name: "gravatar_server", Value: `https://www.gravatar.com/`, Type: "avatar"},
|
||||
{Name: "defaultTheme", Value: `#3f51b5`, Type: "basic"},
|
||||
{Name: "themes", Value: `{"#3f51b5":{"palette":{"primary":{"main":"#3f51b5"},"secondary":{"main":"#f50057"}}},"#2196f3":{"palette":{"primary":{"main":"#2196f3"},"secondary":{"main":"#FFC107"}}},"#673AB7":{"palette":{"primary":{"main":"#673AB7"},"secondary":{"main":"#2196F3"}}},"#E91E63":{"palette":{"primary":{"main":"#E91E63"},"secondary":{"main":"#42A5F5","contrastText":"#fff"}}},"#FF5722":{"palette":{"primary":{"main":"#FF5722"},"secondary":{"main":"#3F51B5"}}},"#FFC107":{"palette":{"primary":{"main":"#FFC107"},"secondary":{"main":"#26C6DA"}}},"#8BC34A":{"palette":{"primary":{"main":"#8BC34A","contrastText":"#fff"},"secondary":{"main":"#FF8A65","contrastText":"#fff"}}},"#009688":{"palette":{"primary":{"main":"#009688"},"secondary":{"main":"#4DD0E1","contrastText":"#fff"}}},"#607D8B":{"palette":{"primary":{"main":"#607D8B"},"secondary":{"main":"#F06292"}}},"#795548":{"palette":{"primary":{"main":"#795548"},"secondary":{"main":"#4CAF50","contrastText":"#fff"}}}}`, Type: "basic"},
|
||||
{Name: "aria2_token", Value: ``, Type: "aria2"},
|
||||
{Name: "aria2_rpcurl", Value: ``, Type: "aria2"},
|
||||
{Name: "aria2_temp_path", Value: ``, Type: "aria2"},
|
||||
{Name: "aria2_options", Value: `{}`, Type: "aria2"},
|
||||
{Name: "aria2_interval", Value: `60`, Type: "aria2"},
|
||||
{Name: "max_worker_num", Value: `10`, Type: "task"},
|
||||
{Name: "max_parallel_transfer", Value: `4`, Type: "task"},
|
||||
{Name: "secret_key", Value: util.RandStringRunes(256), Type: "auth"},
|
||||
@@ -154,8 +147,8 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
|
||||
{Name: "home_view_method", Value: "icon", Type: "view"},
|
||||
{Name: "share_view_method", Value: "list", Type: "view"},
|
||||
{Name: "cron_garbage_collect", Value: "@hourly", Type: "cron"},
|
||||
{Name: "cron_sync_photo_lat_long_to_address", Value: "@hourly", Type: "cron"},
|
||||
{Name: "authn_enabled", Value: "0", Type: "authn"},
|
||||
{Name: "captcha_type", Value: "normal", Type: "captcha"},
|
||||
{Name: "captcha_height", Value: "60", Type: "captcha"},
|
||||
{Name: "captcha_width", Value: "240", Type: "captcha"},
|
||||
{Name: "captcha_mode", Value: "3", Type: "captcha"},
|
||||
@@ -167,12 +160,9 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
|
||||
{Name: "captcha_IsShowSlimeLine", Value: "1", Type: "captcha"},
|
||||
{Name: "captcha_IsShowSineLine", Value: "0", Type: "captcha"},
|
||||
{Name: "captcha_CaptchaLen", Value: "6", Type: "captcha"},
|
||||
{Name: "captcha_IsUseReCaptcha", Value: "0", Type: "captcha"},
|
||||
{Name: "captcha_ReCaptchaKey", Value: "defaultKey", Type: "captcha"},
|
||||
{Name: "captcha_ReCaptchaSecret", Value: "defaultSecret", Type: "captcha"},
|
||||
{Name: "captcha_TCaptcha_CaptchaAppId", Value: "", Type: "captcha"},
|
||||
{Name: "captcha_TCaptcha_AppSecretKey", Value: "", Type: "captcha"},
|
||||
{Name: "captcha_TCaptcha_SecretId", Value: "", Type: "captcha"},
|
||||
{Name: "captcha_TCaptcha_SecretKey", Value: "", Type: "captcha"},
|
||||
{Name: "thumb_width", Value: "400", Type: "thumb"},
|
||||
{Name: "thumb_height", Value: "300", Type: "thumb"},
|
||||
{Name: "pwa_small_icon", Value: "/static/img/favicon.ico", Type: "pwa"},
|
||||
@@ -271,22 +261,3 @@ func addDefaultUser() {
|
||||
util.Log().Info("初始管理员密码:" + c.Sprint(password))
|
||||
}
|
||||
}
|
||||
|
||||
func addDefaultNode() {
|
||||
_, err := GetNodeByID(1)
|
||||
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
defaultAdminGroup := Node{
|
||||
Name: "主机(本机)",
|
||||
Status: NodeActive,
|
||||
Type: MasterNodeType,
|
||||
Aria2OptionsSerialized: Aria2Option{
|
||||
Interval: 10,
|
||||
Timeout: 10,
|
||||
},
|
||||
}
|
||||
if err := DB.Create(&defaultAdminGroup).Error; err != nil {
|
||||
util.Log().Panic("无法创建初始节点记录, %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// Node 从机节点信息模型
|
||||
type Node struct {
|
||||
gorm.Model
|
||||
Status NodeStatus // 节点状态
|
||||
Name string // 节点别名
|
||||
Type ModelType // 节点状态
|
||||
Server string // 服务器地址
|
||||
SlaveKey string `gorm:"type:text"` // 主->从 通信密钥
|
||||
MasterKey string `gorm:"type:text"` // 从->主 通信密钥
|
||||
Aria2Enabled bool // 是否支持用作离线下载节点
|
||||
Aria2Options string `gorm:"type:text"` // 离线下载配置
|
||||
Rank int // 负载均衡权重
|
||||
|
||||
// 数据库忽略字段
|
||||
Aria2OptionsSerialized Aria2Option `gorm:"-"`
|
||||
}
|
||||
|
||||
// Aria2Option 非公有的Aria2配置属性
|
||||
type Aria2Option struct {
|
||||
// RPC 服务器地址
|
||||
Server string `json:"server,omitempty"`
|
||||
// RPC 密钥
|
||||
Token string `json:"token,omitempty"`
|
||||
// 临时下载目录
|
||||
TempPath string `json:"temp_path,omitempty"`
|
||||
// 附加下载配置
|
||||
Options string `json:"options,omitempty"`
|
||||
// 下载监控间隔
|
||||
Interval int `json:"interval,omitempty"`
|
||||
// RPC API 请求超时
|
||||
Timeout int `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
type NodeStatus int
|
||||
type ModelType int
|
||||
|
||||
const (
|
||||
NodeActive NodeStatus = iota
|
||||
NodeSuspend
|
||||
)
|
||||
|
||||
const (
|
||||
SlaveNodeType ModelType = iota
|
||||
MasterNodeType
|
||||
)
|
||||
|
||||
// GetNodeByID 用ID获取节点
|
||||
func GetNodeByID(ID interface{}) (Node, error) {
|
||||
var node Node
|
||||
result := DB.First(&node, ID)
|
||||
return node, result.Error
|
||||
}
|
||||
|
||||
// GetNodesByStatus 根据给定状态获取节点
|
||||
func GetNodesByStatus(status ...NodeStatus) ([]Node, error) {
|
||||
var nodes []Node
|
||||
result := DB.Where("status in (?)", status).Find(&nodes)
|
||||
return nodes, result.Error
|
||||
}
|
||||
|
||||
// AfterFind 找到节点后的钩子
|
||||
func (node *Node) AfterFind() (err error) {
|
||||
// 解析离线下载设置到 Aria2OptionsSerialized
|
||||
if node.Aria2Options != "" {
|
||||
err = json.Unmarshal([]byte(node.Aria2Options), &node.Aria2OptionsSerialized)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// BeforeSave Save策略前的钩子
|
||||
func (node *Node) BeforeSave() (err error) {
|
||||
optionsValue, err := json.Marshal(&node.Aria2OptionsSerialized)
|
||||
node.Aria2Options = string(optionsValue)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetStatus 设置节点启用状态
|
||||
func (node *Node) SetStatus(status NodeStatus) error {
|
||||
node.Status = status
|
||||
return DB.Model(node).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
}).Error
|
||||
}
|
||||
+3
-12
@@ -37,7 +37,6 @@ type Policy struct {
|
||||
|
||||
// 数据库忽略字段
|
||||
OptionsSerialized PolicyOption `gorm:"-"`
|
||||
MasterID string `gorm:"-"`
|
||||
}
|
||||
|
||||
// PolicyOption 非公有的存储策略属性
|
||||
@@ -52,8 +51,6 @@ type PolicyOption struct {
|
||||
OdRedirect string `json:"od_redirect,omitempty"`
|
||||
// OdProxy Onedrive 反代地址
|
||||
OdProxy string `json:"od_proxy,omitempty"`
|
||||
// OdDriver OneDrive 驱动器定位符
|
||||
OdDriver string `json:"od_driver,omitempty"`
|
||||
// Region 区域代码
|
||||
Region string `json:"region,omitempty"`
|
||||
// ServerSideEndpoint 服务端请求使用的 Endpoint,为空时使用 Policy.Server 字段
|
||||
@@ -271,20 +268,14 @@ func (policy *Policy) GetUploadURL() string {
|
||||
return server.ResolveReference(controller).String()
|
||||
}
|
||||
|
||||
// SaveAndClearCache 更新并清理缓存
|
||||
func (policy *Policy) SaveAndClearCache() error {
|
||||
// UpdateAccessKey 更新 AccessKey
|
||||
func (policy *Policy) UpdateAccessKey(key string) error {
|
||||
policy.AccessKey = key
|
||||
err := DB.Save(policy).Error
|
||||
policy.ClearCache()
|
||||
return err
|
||||
}
|
||||
|
||||
// SaveAndClearCache 更新并清理缓存
|
||||
func (policy *Policy) UpdateAccessKeyAndClearCache(s string) error {
|
||||
err := DB.Model(policy).UpdateColumn("access_key", s).Error
|
||||
policy.ClearCache()
|
||||
return err
|
||||
}
|
||||
|
||||
// ClearCache 清空policy缓存
|
||||
func (policy *Policy) ClearCache() {
|
||||
cache.Deletes([]string{strconv.FormatUint(uint64(policy.ID), 10)}, "policy_")
|
||||
|
||||
@@ -257,8 +257,7 @@ func TestPolicy_UpdateAccessKey(t *testing.T) {
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
policy.AccessKey = "123"
|
||||
err := policy.SaveAndClearCache()
|
||||
err := policy.UpdateAccessKey("123")
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
asserts.NoError(err)
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package scripts
|
||||
|
||||
import (
|
||||
"context"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
type ResetAdminPassword int
|
||||
|
||||
func init() {
|
||||
register("ResetAdminPassword", ResetAdminPassword(0))
|
||||
}
|
||||
|
||||
// Run 运行脚本从社区版升级至 Pro 版
|
||||
func (script ResetAdminPassword) Run(ctx context.Context) {
|
||||
// 查找用户
|
||||
user, err := model.GetUserByID(1)
|
||||
if err != nil {
|
||||
util.Log().Panic("初始管理员用户不存在, %s", err)
|
||||
}
|
||||
|
||||
// 生成密码
|
||||
password := util.RandStringRunes(8)
|
||||
|
||||
// 更改为新密码
|
||||
user.SetPassword(password)
|
||||
if err := user.Update(map[string]interface{}{"password": user.Password}); err != nil {
|
||||
util.Log().Panic("密码更改失败, %s", err)
|
||||
}
|
||||
|
||||
c := color.New(color.FgWhite).Add(color.BgBlack).Add(color.Bold)
|
||||
util.Log().Info("初始管理员密码已更改为:" + c.Sprint(password))
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package scripts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResetAdminPassword_Run(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
script := ResetAdminPassword(0)
|
||||
|
||||
// 初始用户不存在
|
||||
{
|
||||
mock.ExpectQuery("SELECT(.+)users(.+)").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "email", "storage"}))
|
||||
asserts.Panics(func() {
|
||||
script.Run(context.Background())
|
||||
})
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// 密码更新失败
|
||||
{
|
||||
mock.ExpectQuery("SELECT(.+)users(.+)").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "email", "storage"}).AddRow(1, "a@a.com", 10))
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnError(errors.New("error"))
|
||||
mock.ExpectRollback()
|
||||
asserts.Panics(func() {
|
||||
script.Run(context.Background())
|
||||
})
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// 成功
|
||||
{
|
||||
mock.ExpectQuery("SELECT(.+)users(.+)").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "email", "storage"}).AddRow(1, "a@a.com", 10))
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
asserts.NotPanics(func() {
|
||||
script.Run(context.Background())
|
||||
})
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
}
|
||||
+4
-8
@@ -30,16 +30,12 @@ func GetSettingByName(name string) string {
|
||||
if optionValue, ok := cache.Get(cacheKey); ok {
|
||||
return optionValue.(string)
|
||||
}
|
||||
|
||||
// 尝试数据库中查找
|
||||
if DB != nil {
|
||||
result := DB.Where("name = ?", name).First(&setting)
|
||||
if result.Error == nil {
|
||||
_ = cache.Set(cacheKey, setting.Value, -1)
|
||||
return setting.Value
|
||||
}
|
||||
result := DB.Where("name = ?", name).First(&setting)
|
||||
if result.Error == nil {
|
||||
_ = cache.Set(cacheKey, setting.Value, -1)
|
||||
return setting.Value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
+137
-33
@@ -1,65 +1,169 @@
|
||||
package aria2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/common"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/monitor"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/balancer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
)
|
||||
|
||||
// Instance 默认使用的Aria2处理实例
|
||||
var Instance common.Aria2 = &common.DummyAria2{}
|
||||
|
||||
// LB 获取 Aria2 节点的负载均衡器
|
||||
var LB balancer.Balancer
|
||||
var Instance Aria2 = &DummyAria2{}
|
||||
|
||||
// Lock Instance的读写锁
|
||||
var Lock sync.RWMutex
|
||||
|
||||
// GetLoadBalancer 返回供Aria2使用的负载均衡器
|
||||
func GetLoadBalancer() balancer.Balancer {
|
||||
Lock.RLock()
|
||||
defer Lock.RUnlock()
|
||||
return LB
|
||||
// EventNotifier 任务状态更新通知处理器
|
||||
var EventNotifier = &Notifier{}
|
||||
|
||||
// Aria2 离线下载处理接口
|
||||
type Aria2 interface {
|
||||
// CreateTask 创建新的任务
|
||||
CreateTask(task *model.Download, options map[string]interface{}) error
|
||||
// 返回状态信息
|
||||
Status(task *model.Download) (rpc.StatusInfo, error)
|
||||
// 取消任务
|
||||
Cancel(task *model.Download) error
|
||||
// 选择要下载的文件
|
||||
Select(task *model.Download, files []int) error
|
||||
}
|
||||
|
||||
const (
|
||||
// URLTask 从URL添加的任务
|
||||
URLTask = iota
|
||||
// TorrentTask 种子任务
|
||||
TorrentTask
|
||||
)
|
||||
|
||||
const (
|
||||
// Ready 准备就绪
|
||||
Ready = iota
|
||||
// Downloading 下载中
|
||||
Downloading
|
||||
// Paused 暂停中
|
||||
Paused
|
||||
// Error 出错
|
||||
Error
|
||||
// Complete 完成
|
||||
Complete
|
||||
// Canceled 取消/停止
|
||||
Canceled
|
||||
// Unknown 未知状态
|
||||
Unknown
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotEnabled 功能未开启错误
|
||||
ErrNotEnabled = serializer.NewError(serializer.CodeNoPermissionErr, "离线下载功能未开启", nil)
|
||||
// ErrUserNotFound 未找到下载任务创建者
|
||||
ErrUserNotFound = serializer.NewError(serializer.CodeNotFound, "无法找到任务创建者", nil)
|
||||
)
|
||||
|
||||
// DummyAria2 未开启Aria2功能时使用的默认处理器
|
||||
type DummyAria2 struct {
|
||||
}
|
||||
|
||||
// CreateTask 创建新任务,此处直接返回未开启错误
|
||||
func (instance *DummyAria2) CreateTask(model *model.Download, options map[string]interface{}) error {
|
||||
return ErrNotEnabled
|
||||
}
|
||||
|
||||
// Status 返回未开启错误
|
||||
func (instance *DummyAria2) Status(task *model.Download) (rpc.StatusInfo, error) {
|
||||
return rpc.StatusInfo{}, ErrNotEnabled
|
||||
}
|
||||
|
||||
// Cancel 返回未开启错误
|
||||
func (instance *DummyAria2) Cancel(task *model.Download) error {
|
||||
return ErrNotEnabled
|
||||
}
|
||||
|
||||
// Select 返回未开启错误
|
||||
func (instance *DummyAria2) Select(task *model.Download, files []int) error {
|
||||
return ErrNotEnabled
|
||||
}
|
||||
|
||||
// Init 初始化
|
||||
func Init(isReload bool) {
|
||||
Lock.Lock()
|
||||
LB = balancer.NewBalancer("RoundRobin")
|
||||
Lock.Unlock()
|
||||
defer Lock.Unlock()
|
||||
|
||||
// 关闭上个初始连接
|
||||
if previousClient, ok := Instance.(*RPCService); ok {
|
||||
if previousClient.Caller != nil {
|
||||
util.Log().Debug("关闭上个 aria2 连接")
|
||||
previousClient.Caller.Close()
|
||||
}
|
||||
}
|
||||
|
||||
options := model.GetSettingByNames("aria2_rpcurl", "aria2_token", "aria2_options")
|
||||
timeout := model.GetIntSetting("aria2_call_timeout", 5)
|
||||
if options["aria2_rpcurl"] == "" {
|
||||
Instance = &DummyAria2{}
|
||||
return
|
||||
}
|
||||
|
||||
util.Log().Info("初始化 aria2 RPC 服务[%s]", options["aria2_rpcurl"])
|
||||
client := &RPCService{}
|
||||
|
||||
// 解析RPC服务地址
|
||||
server, err := url.Parse(options["aria2_rpcurl"])
|
||||
if err != nil {
|
||||
util.Log().Warning("无法解析 aria2 RPC 服务地址,%s", err)
|
||||
Instance = &DummyAria2{}
|
||||
return
|
||||
}
|
||||
server.Path = "/jsonrpc"
|
||||
|
||||
// 加载自定义下载配置
|
||||
var globalOptions map[string]interface{}
|
||||
err = json.Unmarshal([]byte(options["aria2_options"]), &globalOptions)
|
||||
if err != nil {
|
||||
util.Log().Warning("无法解析 aria2 全局配置,%s", err)
|
||||
Instance = &DummyAria2{}
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.Init(server.String(), options["aria2_token"], timeout, globalOptions); err != nil {
|
||||
util.Log().Warning("初始化 aria2 RPC 服务失败,%s", err)
|
||||
Instance = &DummyAria2{}
|
||||
return
|
||||
}
|
||||
|
||||
Instance = client
|
||||
|
||||
if !isReload {
|
||||
// 从数据库中读取未完成任务,创建监控
|
||||
unfinished := model.GetDownloadsByStatus(common.Ready, common.Paused, common.Downloading)
|
||||
unfinished := model.GetDownloadsByStatus(Ready, Paused, Downloading)
|
||||
|
||||
for i := 0; i < len(unfinished); i++ {
|
||||
// 创建任务监控
|
||||
monitor.NewMonitor(&unfinished[i])
|
||||
NewMonitor(&unfinished[i])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestRPCConnection 发送测试用的 RPC 请求,测试服务连通性
|
||||
func TestRPCConnection(server, secret string, timeout int) (rpc.VersionInfo, error) {
|
||||
// 解析RPC服务地址
|
||||
rpcServer, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return rpc.VersionInfo{}, fmt.Errorf("cannot parse RPC server: %w", err)
|
||||
// getStatus 将给定的状态字符串转换为状态标识数字
|
||||
func getStatus(status string) int {
|
||||
switch status {
|
||||
case "complete":
|
||||
return Complete
|
||||
case "active":
|
||||
return Downloading
|
||||
case "waiting":
|
||||
return Ready
|
||||
case "paused":
|
||||
return Paused
|
||||
case "error":
|
||||
return Error
|
||||
case "removed":
|
||||
return Canceled
|
||||
default:
|
||||
return Unknown
|
||||
}
|
||||
|
||||
rpcServer.Path = "/jsonrpc"
|
||||
caller, err := rpc.New(context.Background(), rpcServer.String(), secret, time.Duration(timeout)*time.Second, nil)
|
||||
if err != nil {
|
||||
return rpc.VersionInfo{}, fmt.Errorf("cannot initialize rpc connection: %w", err)
|
||||
}
|
||||
|
||||
return caller.GetVersion()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/monitor"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -38,7 +37,7 @@ func TestDummyAria2(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
monitor.MAX_RETRY = 0
|
||||
MAX_RETRY = 0
|
||||
asserts := assert.New(t)
|
||||
cache.Set("setting_aria2_token", "1", 0)
|
||||
cache.Set("setting_aria2_call_timeout", "5", 0)
|
||||
@@ -82,11 +81,11 @@ func TestInit(t *testing.T) {
|
||||
|
||||
func TestGetStatus(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
asserts.Equal(4, GetStatus("complete"))
|
||||
asserts.Equal(1, GetStatus("active"))
|
||||
asserts.Equal(0, GetStatus("waiting"))
|
||||
asserts.Equal(2, GetStatus("paused"))
|
||||
asserts.Equal(3, GetStatus("error"))
|
||||
asserts.Equal(5, GetStatus("removed"))
|
||||
asserts.Equal(6, GetStatus("?"))
|
||||
asserts.Equal(4, getStatus("complete"))
|
||||
asserts.Equal(1, getStatus("active"))
|
||||
asserts.Equal(0, getStatus("waiting"))
|
||||
asserts.Equal(2, getStatus("paused"))
|
||||
asserts.Equal(3, getStatus("error"))
|
||||
asserts.Equal(5, getStatus("removed"))
|
||||
asserts.Equal(6, getStatus("?"))
|
||||
}
|
||||
|
||||
+14
-5
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mq"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
)
|
||||
|
||||
@@ -34,7 +33,7 @@ func (client *RPCService) Init(server, secret string, timeout int, options map[s
|
||||
Options: options,
|
||||
}
|
||||
caller, err := rpc.New(context.Background(), server, secret, time.Duration(timeout)*time.Second,
|
||||
mq.GlobalMQ)
|
||||
EventNotifier)
|
||||
client.Caller = caller
|
||||
return err
|
||||
}
|
||||
@@ -86,7 +85,7 @@ func (client *RPCService) Select(task *model.Download, files []int) error {
|
||||
}
|
||||
|
||||
// CreateTask 创建新任务
|
||||
func (client *RPCService) CreateTask(task *model.Download, groupOptions map[string]interface{}) (string, error) {
|
||||
func (client *RPCService) CreateTask(task *model.Download, groupOptions map[string]interface{}) error {
|
||||
// 生成存储路径
|
||||
path := filepath.Join(
|
||||
model.GetSettingByName("aria2_temp_path"),
|
||||
@@ -107,8 +106,18 @@ func (client *RPCService) CreateTask(task *model.Download, groupOptions map[stri
|
||||
|
||||
gid, err := client.Caller.AddURI(task.Source, options)
|
||||
if err != nil || gid == "" {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
return gid, nil
|
||||
// 保存到数据库
|
||||
task.GID = gid
|
||||
_, err = task.Create()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建任务监控
|
||||
NewMonitor(task)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
)
|
||||
|
||||
// Aria2 离线下载处理接口
|
||||
type Aria2 interface {
|
||||
// Init 初始化客户端连接
|
||||
Init() error
|
||||
// CreateTask 创建新的任务
|
||||
CreateTask(task *model.Download, options map[string]interface{}) (string, error)
|
||||
// 返回状态信息
|
||||
Status(task *model.Download) (rpc.StatusInfo, error)
|
||||
// 取消任务
|
||||
Cancel(task *model.Download) error
|
||||
// 选择要下载的文件
|
||||
Select(task *model.Download, files []int) error
|
||||
// 获取离线下载配置
|
||||
GetConfig() model.Aria2Option
|
||||
// 删除临时下载文件
|
||||
DeleteTempFile(*model.Download) error
|
||||
}
|
||||
|
||||
const (
|
||||
// URLTask 从URL添加的任务
|
||||
URLTask = iota
|
||||
// TorrentTask 种子任务
|
||||
TorrentTask
|
||||
)
|
||||
|
||||
const (
|
||||
// Ready 准备就绪
|
||||
Ready = iota
|
||||
// Downloading 下载中
|
||||
Downloading
|
||||
// Paused 暂停中
|
||||
Paused
|
||||
// Error 出错
|
||||
Error
|
||||
// Complete 完成
|
||||
Complete
|
||||
// Canceled 取消/停止
|
||||
Canceled
|
||||
// Unknown 未知状态
|
||||
Unknown
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotEnabled 功能未开启错误
|
||||
ErrNotEnabled = serializer.NewError(serializer.CodeNoPermissionErr, "离线下载功能未开启", nil)
|
||||
// ErrUserNotFound 未找到下载任务创建者
|
||||
ErrUserNotFound = serializer.NewError(serializer.CodeNotFound, "无法找到任务创建者", nil)
|
||||
)
|
||||
|
||||
// DummyAria2 未开启Aria2功能时使用的默认处理器
|
||||
type DummyAria2 struct {
|
||||
}
|
||||
|
||||
func (instance *DummyAria2) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateTask 创建新任务,此处直接返回未开启错误
|
||||
func (instance *DummyAria2) CreateTask(model *model.Download, options map[string]interface{}) (string, error) {
|
||||
return "", ErrNotEnabled
|
||||
}
|
||||
|
||||
// Status 返回未开启错误
|
||||
func (instance *DummyAria2) Status(task *model.Download) (rpc.StatusInfo, error) {
|
||||
return rpc.StatusInfo{}, ErrNotEnabled
|
||||
}
|
||||
|
||||
// Cancel 返回未开启错误
|
||||
func (instance *DummyAria2) Cancel(task *model.Download) error {
|
||||
return ErrNotEnabled
|
||||
}
|
||||
|
||||
// Select 返回未开启错误
|
||||
func (instance *DummyAria2) Select(task *model.Download, files []int) error {
|
||||
return ErrNotEnabled
|
||||
}
|
||||
|
||||
// GetConfig 返回空的
|
||||
func (instance *DummyAria2) GetConfig() model.Aria2Option {
|
||||
return model.Aria2Option{}
|
||||
}
|
||||
|
||||
// GetConfig 返回空的
|
||||
func (instance *DummyAria2) DeleteTempFile(src *model.Download) error {
|
||||
return ErrNotEnabled
|
||||
}
|
||||
|
||||
// GetStatus 将给定的状态字符串转换为状态标识数字
|
||||
func GetStatus(status string) int {
|
||||
switch status {
|
||||
case "complete":
|
||||
return Complete
|
||||
case "active":
|
||||
return Downloading
|
||||
case "waiting":
|
||||
return Ready
|
||||
case "paused":
|
||||
return Paused
|
||||
case "error":
|
||||
return Error
|
||||
case "removed":
|
||||
return Canceled
|
||||
default:
|
||||
return Unknown
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,19 @@
|
||||
package monitor
|
||||
package aria2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/common"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/local"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mq"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/task"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
)
|
||||
@@ -25,34 +23,32 @@ type Monitor struct {
|
||||
Task *model.Download
|
||||
Interval time.Duration
|
||||
|
||||
notifier <-chan mq.Message
|
||||
node cluster.Node
|
||||
notifier chan StatusEvent
|
||||
retried int
|
||||
}
|
||||
|
||||
// StatusEvent 状态改变事件
|
||||
type StatusEvent struct {
|
||||
GID string
|
||||
Status int
|
||||
}
|
||||
|
||||
var MAX_RETRY = 10
|
||||
|
||||
// NewMonitor 新建离线下载状态监控
|
||||
// NewMonitor 新建上传状态监控
|
||||
func NewMonitor(task *model.Download) {
|
||||
monitor := &Monitor{
|
||||
Task: task,
|
||||
notifier: make(chan mq.Message),
|
||||
node: cluster.Default.GetNodeByID(task.GetNodeID()),
|
||||
}
|
||||
|
||||
if monitor.node != nil {
|
||||
monitor.Interval = time.Duration(monitor.node.GetAria2Instance().GetConfig().Interval) * time.Second
|
||||
go monitor.Loop()
|
||||
|
||||
monitor.notifier = mq.GlobalMQ.Subscribe(monitor.Task.GID, 0)
|
||||
} else {
|
||||
monitor.setErrorStatus(errors.New("节点不可用"))
|
||||
Interval: time.Duration(model.GetIntSetting("aria2_interval", 10)) * time.Second,
|
||||
notifier: make(chan StatusEvent),
|
||||
}
|
||||
go monitor.Loop()
|
||||
EventNotifier.Subscribe(monitor.notifier, monitor.Task.GID)
|
||||
}
|
||||
|
||||
// Loop 开启监控循环
|
||||
func (monitor *Monitor) Loop() {
|
||||
defer mq.GlobalMQ.Unsubscribe(monitor.Task.GID, monitor.notifier)
|
||||
defer EventNotifier.Unsubscribe(monitor.Task.GID)
|
||||
|
||||
// 首次循环立即更新
|
||||
interval := time.Duration(0)
|
||||
@@ -74,7 +70,9 @@ func (monitor *Monitor) Loop() {
|
||||
|
||||
// Update 更新状态,返回值表示是否退出监控
|
||||
func (monitor *Monitor) Update() bool {
|
||||
status, err := monitor.node.GetAria2Instance().Status(monitor.Task)
|
||||
Lock.RLock()
|
||||
status, err := Instance.Status(monitor.Task)
|
||||
Lock.RUnlock()
|
||||
|
||||
if err != nil {
|
||||
monitor.retried++
|
||||
@@ -104,7 +102,6 @@ func (monitor *Monitor) Update() bool {
|
||||
if err := monitor.UpdateTaskInfo(status); err != nil {
|
||||
util.Log().Warning("无法更新下载任务[%s]的任务信息[%s],", monitor.Task.GID, err)
|
||||
monitor.setErrorStatus(err)
|
||||
monitor.RemoveTempFolder()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -118,7 +115,7 @@ func (monitor *Monitor) Update() bool {
|
||||
case "active", "waiting", "paused":
|
||||
return false
|
||||
case "removed":
|
||||
monitor.Task.Status = common.Canceled
|
||||
monitor.Task.Status = Canceled
|
||||
monitor.Task.Save()
|
||||
monitor.RemoveTempFolder()
|
||||
return true
|
||||
@@ -133,7 +130,7 @@ func (monitor *Monitor) UpdateTaskInfo(status rpc.StatusInfo) error {
|
||||
originSize := monitor.Task.TotalSize
|
||||
|
||||
monitor.Task.GID = status.Gid
|
||||
monitor.Task.Status = common.GetStatus(status.Status)
|
||||
monitor.Task.Status = getStatus(status.Status)
|
||||
|
||||
// 文件大小、已下载大小
|
||||
total, err := strconv.ParseUint(status.TotalLength, 10, 64)
|
||||
@@ -167,7 +164,9 @@ func (monitor *Monitor) UpdateTaskInfo(status rpc.StatusInfo) error {
|
||||
// 文件大小更新后,对文件限制等进行校验
|
||||
if err := monitor.ValidateFile(); err != nil {
|
||||
// 验证失败时取消任务
|
||||
monitor.node.GetAria2Instance().Cancel(monitor.Task)
|
||||
Lock.RLock()
|
||||
Instance.Cancel(monitor.Task)
|
||||
Lock.RUnlock()
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -180,7 +179,7 @@ func (monitor *Monitor) ValidateFile() error {
|
||||
// 找到任务创建者
|
||||
user := monitor.Task.GetOwner()
|
||||
if user == nil {
|
||||
return common.ErrUserNotFound
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
// 创建文件系统
|
||||
@@ -231,31 +230,28 @@ func (monitor *Monitor) Error(status rpc.StatusInfo) bool {
|
||||
|
||||
// RemoveTempFolder 清理下载临时目录
|
||||
func (monitor *Monitor) RemoveTempFolder() {
|
||||
monitor.node.GetAria2Instance().DeleteTempFile(monitor.Task)
|
||||
err := os.RemoveAll(monitor.Task.Parent)
|
||||
if err != nil {
|
||||
util.Log().Warning("无法删除离线下载临时目录[%s], %s", monitor.Task.Parent, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Complete 完成下载,返回是否中断监控
|
||||
func (monitor *Monitor) Complete(status rpc.StatusInfo) bool {
|
||||
// 创建中转任务
|
||||
file := make([]string, 0, len(monitor.Task.StatusInfo.Files))
|
||||
sizes := make(map[string]uint64, len(monitor.Task.StatusInfo.Files))
|
||||
for i := 0; i < len(monitor.Task.StatusInfo.Files); i++ {
|
||||
fileInfo := monitor.Task.StatusInfo.Files[i]
|
||||
if fileInfo.Selected == "true" {
|
||||
file = append(file, fileInfo.Path)
|
||||
size, _ := strconv.ParseUint(fileInfo.Length, 10, 64)
|
||||
sizes[fileInfo.Path] = size
|
||||
if monitor.Task.StatusInfo.Files[i].Selected == "true" {
|
||||
file = append(file, monitor.Task.StatusInfo.Files[i].Path)
|
||||
}
|
||||
}
|
||||
|
||||
job, err := task.NewTransferTask(
|
||||
monitor.Task.UserID,
|
||||
file,
|
||||
monitor.Task.Dst,
|
||||
monitor.Task.Parent,
|
||||
true,
|
||||
monitor.node.ID(),
|
||||
sizes,
|
||||
)
|
||||
if err != nil {
|
||||
monitor.setErrorStatus(err)
|
||||
@@ -273,7 +269,7 @@ func (monitor *Monitor) Complete(status rpc.StatusInfo) bool {
|
||||
}
|
||||
|
||||
func (monitor *Monitor) setErrorStatus(err error) {
|
||||
monitor.Task.Status = common.Error
|
||||
monitor.Task.Status = Error
|
||||
monitor.Task.Error = err.Error()
|
||||
monitor.Task.Save()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package monitor
|
||||
package aria2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/common"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
@@ -46,13 +44,13 @@ func (m InstanceMock) Select(task *model.Download, files []int) error {
|
||||
func TestNewMonitor(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
NewMonitor(&model.Download{GID: "gid"})
|
||||
_, ok := common.EventNotifier.Subscribes.Load("gid")
|
||||
_, ok := EventNotifier.Subscribes.Load("gid")
|
||||
asserts.True(ok)
|
||||
}
|
||||
|
||||
func TestMonitor_Loop(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
notifier := make(chan common.StatusEvent)
|
||||
notifier := make(chan StatusEvent)
|
||||
MAX_RETRY = 0
|
||||
monitor := &Monitor{
|
||||
Task: &model.Download{GID: "gid"},
|
||||
@@ -78,10 +76,10 @@ func TestMonitor_Update(t *testing.T) {
|
||||
{
|
||||
MAX_RETRY = 1
|
||||
testInstance := new(InstanceMock)
|
||||
testInstance.On("SlaveStatus", testMock.Anything).Return(rpc.StatusInfo{}, errors.New("error"))
|
||||
testInstance.On("Status", testMock.Anything).Return(rpc.StatusInfo{}, errors.New("error"))
|
||||
file, _ := util.CreatNestedFile("TestMonitor_Update/1")
|
||||
file.Close()
|
||||
aria2.Instance = testInstance
|
||||
Instance = testInstance
|
||||
asserts.False(monitor.Update())
|
||||
asserts.True(monitor.Update())
|
||||
testInstance.AssertExpectations(t)
|
||||
@@ -91,16 +89,16 @@ func TestMonitor_Update(t *testing.T) {
|
||||
// 磁力链下载重定向
|
||||
{
|
||||
testInstance := new(InstanceMock)
|
||||
testInstance.On("SlaveStatus", testMock.Anything).Return(rpc.StatusInfo{
|
||||
testInstance.On("Status", testMock.Anything).Return(rpc.StatusInfo{
|
||||
FollowedBy: []string{"1"},
|
||||
}, nil)
|
||||
monitor.Task.ID = 1
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
aria2.mock.ExpectCommit()
|
||||
aria2.Instance = testInstance
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
Instance = testInstance
|
||||
asserts.False(monitor.Update())
|
||||
asserts.NoError(aria2.mock.ExpectationsWereMet())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
testInstance.AssertExpectations(t)
|
||||
asserts.EqualValues("1", monitor.Task.GID)
|
||||
}
|
||||
@@ -108,82 +106,82 @@ func TestMonitor_Update(t *testing.T) {
|
||||
// 无法更新任务信息
|
||||
{
|
||||
testInstance := new(InstanceMock)
|
||||
testInstance.On("SlaveStatus", testMock.Anything).Return(rpc.StatusInfo{}, nil)
|
||||
testInstance.On("Status", testMock.Anything).Return(rpc.StatusInfo{}, nil)
|
||||
monitor.Task.ID = 1
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)").WillReturnError(errors.New("error"))
|
||||
aria2.mock.ExpectRollback()
|
||||
aria2.Instance = testInstance
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnError(errors.New("error"))
|
||||
mock.ExpectRollback()
|
||||
Instance = testInstance
|
||||
asserts.True(monitor.Update())
|
||||
asserts.NoError(aria2.mock.ExpectationsWereMet())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
testInstance.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// 返回未知状态
|
||||
{
|
||||
testInstance := new(InstanceMock)
|
||||
testInstance.On("SlaveStatus", testMock.Anything).Return(rpc.StatusInfo{Status: "?"}, nil)
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
aria2.mock.ExpectCommit()
|
||||
aria2.Instance = testInstance
|
||||
testInstance.On("Status", testMock.Anything).Return(rpc.StatusInfo{Status: "?"}, nil)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
Instance = testInstance
|
||||
asserts.True(monitor.Update())
|
||||
asserts.NoError(aria2.mock.ExpectationsWereMet())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
testInstance.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// 返回被取消状态
|
||||
{
|
||||
testInstance := new(InstanceMock)
|
||||
testInstance.On("SlaveStatus", testMock.Anything).Return(rpc.StatusInfo{Status: "removed"}, nil)
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
aria2.mock.ExpectCommit()
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
aria2.mock.ExpectCommit()
|
||||
aria2.Instance = testInstance
|
||||
testInstance.On("Status", testMock.Anything).Return(rpc.StatusInfo{Status: "removed"}, nil)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
Instance = testInstance
|
||||
asserts.True(monitor.Update())
|
||||
asserts.NoError(aria2.mock.ExpectationsWereMet())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
testInstance.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// 返回活跃状态
|
||||
{
|
||||
testInstance := new(InstanceMock)
|
||||
testInstance.On("SlaveStatus", testMock.Anything).Return(rpc.StatusInfo{Status: "active"}, nil)
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
aria2.mock.ExpectCommit()
|
||||
aria2.Instance = testInstance
|
||||
testInstance.On("Status", testMock.Anything).Return(rpc.StatusInfo{Status: "active"}, nil)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
Instance = testInstance
|
||||
asserts.False(monitor.Update())
|
||||
asserts.NoError(aria2.mock.ExpectationsWereMet())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
testInstance.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// 返回错误状态
|
||||
{
|
||||
testInstance := new(InstanceMock)
|
||||
testInstance.On("SlaveStatus", testMock.Anything).Return(rpc.StatusInfo{Status: "error"}, nil)
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
aria2.mock.ExpectCommit()
|
||||
aria2.Instance = testInstance
|
||||
testInstance.On("Status", testMock.Anything).Return(rpc.StatusInfo{Status: "error"}, nil)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
Instance = testInstance
|
||||
asserts.True(monitor.Update())
|
||||
asserts.NoError(aria2.mock.ExpectationsWereMet())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
testInstance.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// 返回完成
|
||||
{
|
||||
testInstance := new(InstanceMock)
|
||||
testInstance.On("SlaveStatus", testMock.Anything).Return(rpc.StatusInfo{Status: "complete"}, nil)
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
aria2.mock.ExpectCommit()
|
||||
aria2.Instance = testInstance
|
||||
testInstance.On("Status", testMock.Anything).Return(rpc.StatusInfo{Status: "complete"}, nil)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
Instance = testInstance
|
||||
asserts.True(monitor.Update())
|
||||
asserts.NoError(aria2.mock.ExpectationsWereMet())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
testInstance.AssertExpectations(t)
|
||||
}
|
||||
}
|
||||
@@ -200,34 +198,34 @@ func TestMonitor_UpdateTaskInfo(t *testing.T) {
|
||||
|
||||
// 失败
|
||||
{
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)").WillReturnError(errors.New("error"))
|
||||
aria2.mock.ExpectRollback()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnError(errors.New("error"))
|
||||
mock.ExpectRollback()
|
||||
err := monitor.UpdateTaskInfo(rpc.StatusInfo{})
|
||||
asserts.NoError(aria2.mock.ExpectationsWereMet())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
asserts.Error(err)
|
||||
}
|
||||
|
||||
// 更新成功,无需校验
|
||||
{
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
aria2.mock.ExpectCommit()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
err := monitor.UpdateTaskInfo(rpc.StatusInfo{})
|
||||
asserts.NoError(aria2.mock.ExpectationsWereMet())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
asserts.NoError(err)
|
||||
}
|
||||
|
||||
// 更新成功,大小改变,需要校验,校验失败
|
||||
{
|
||||
testInstance := new(InstanceMock)
|
||||
testInstance.On("SlaveCancel", testMock.Anything).Return(nil)
|
||||
aria2.Instance = testInstance
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
aria2.mock.ExpectCommit()
|
||||
testInstance.On("Cancel", testMock.Anything).Return(nil)
|
||||
Instance = testInstance
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
err := monitor.UpdateTaskInfo(rpc.StatusInfo{TotalLength: "1"})
|
||||
asserts.NoError(aria2.mock.ExpectationsWereMet())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
asserts.Error(err)
|
||||
testInstance.AssertExpectations(t)
|
||||
}
|
||||
@@ -310,17 +308,17 @@ func TestMonitor_Complete(t *testing.T) {
|
||||
}
|
||||
|
||||
cache.Set("setting_max_worker_num", "1", 0)
|
||||
aria2.mock.ExpectQuery("SELECT(.+)tasks").WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||||
mock.ExpectQuery("SELECT(.+)tasks").WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||||
task.Init()
|
||||
aria2.mock.ExpectQuery("SELECT(.+)users").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||||
aria2.mock.ExpectQuery("SELECT(.+)policies").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("INSERT(.+)tasks").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
aria2.mock.ExpectCommit()
|
||||
mock.ExpectQuery("SELECT(.+)users").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||||
mock.ExpectQuery("SELECT(.+)policies").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("INSERT(.+)tasks").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
aria2.mock.ExpectBegin()
|
||||
aria2.mock.ExpectExec("UPDATE(.+)downloads").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
aria2.mock.ExpectCommit()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)downloads").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
asserts.True(monitor.Complete(rpc.StatusInfo{}))
|
||||
asserts.NoError(aria2.mock.ExpectationsWereMet())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package aria2
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
)
|
||||
|
||||
// Notifier aria2实践通知处理
|
||||
type Notifier struct {
|
||||
Subscribes sync.Map
|
||||
}
|
||||
|
||||
// Subscribe 订阅事件通知
|
||||
func (notifier *Notifier) Subscribe(target chan StatusEvent, gid string) {
|
||||
notifier.Subscribes.Store(gid, target)
|
||||
}
|
||||
|
||||
// Unsubscribe 取消订阅事件通知
|
||||
func (notifier *Notifier) Unsubscribe(gid string) {
|
||||
notifier.Subscribes.Delete(gid)
|
||||
}
|
||||
|
||||
// Notify 发送通知
|
||||
func (notifier *Notifier) Notify(events []rpc.Event, status int) {
|
||||
for _, event := range events {
|
||||
if target, ok := notifier.Subscribes.Load(event.Gid); ok {
|
||||
target.(chan StatusEvent) <- StatusEvent{
|
||||
GID: event.Gid,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnDownloadStart 下载开始
|
||||
func (notifier *Notifier) OnDownloadStart(events []rpc.Event) {
|
||||
notifier.Notify(events, Downloading)
|
||||
}
|
||||
|
||||
// OnDownloadPause 下载暂停
|
||||
func (notifier *Notifier) OnDownloadPause(events []rpc.Event) {
|
||||
notifier.Notify(events, Paused)
|
||||
}
|
||||
|
||||
// OnDownloadStop 下载停止
|
||||
func (notifier *Notifier) OnDownloadStop(events []rpc.Event) {
|
||||
notifier.Notify(events, Canceled)
|
||||
}
|
||||
|
||||
// OnDownloadComplete 下载完成
|
||||
func (notifier *Notifier) OnDownloadComplete(events []rpc.Event) {
|
||||
notifier.Notify(events, Complete)
|
||||
}
|
||||
|
||||
// OnDownloadError 下载出错
|
||||
func (notifier *Notifier) OnDownloadError(events []rpc.Event) {
|
||||
notifier.Notify(events, Error)
|
||||
}
|
||||
|
||||
// OnBtDownloadComplete BT下载完成
|
||||
func (notifier *Notifier) OnBtDownloadComplete(events []rpc.Event) {
|
||||
notifier.Notify(events, Complete)
|
||||
}
|
||||
+10
-22
@@ -2,11 +2,9 @@ package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -32,8 +30,9 @@ type Auth interface {
|
||||
Check(body string, sign string) error
|
||||
}
|
||||
|
||||
// SignRequest 对PUT\POST等复杂HTTP请求签名,只会对URI部分、
|
||||
// 请求正文、`X-`开头的header进行签名
|
||||
// SignRequest 对PUT\POST等复杂HTTP请求签名,如果请求Header中
|
||||
// 包含 X-Policy, 则此请求会被认定为上传请求,只会对URI部分和
|
||||
// Policy部分进行签名。其他请求则会对URI和Body部分进行签名。
|
||||
func SignRequest(instance Auth, r *http.Request, expires int64) *http.Request {
|
||||
// 处理有效期
|
||||
if expires > 0 {
|
||||
@@ -62,31 +61,20 @@ func CheckRequest(instance Auth, r *http.Request) error {
|
||||
return instance.Check(getSignContent(r), sign[0])
|
||||
}
|
||||
|
||||
// getSignContent 签名请求 path、正文、以`X-`开头的 Header. 如果 Header 中包含 `X-Policy`,
|
||||
// 则不对正文签名。返回待签名/验证的字符串
|
||||
// getSignContent 根据请求Header中是否包含X-Policy判断是否为上传请求,
|
||||
// 返回待签名/验证的字符串
|
||||
func getSignContent(r *http.Request) (rawSignString string) {
|
||||
// 读取所有body正文
|
||||
var body = []byte{}
|
||||
if _, ok := r.Header["X-Policy"]; !ok {
|
||||
if policy, ok := r.Header["X-Policy"]; ok {
|
||||
rawSignString = serializer.NewRequestSignString(r.URL.Path, policy[0], "")
|
||||
} else {
|
||||
var body = []byte{}
|
||||
if r.Body != nil {
|
||||
body, _ = ioutil.ReadAll(r.Body)
|
||||
_ = r.Body.Close()
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(body))
|
||||
}
|
||||
rawSignString = serializer.NewRequestSignString(r.URL.Path, "", string(body))
|
||||
}
|
||||
|
||||
// 决定要签名的header
|
||||
var signedHeader []string
|
||||
for k, _ := range r.Header {
|
||||
if strings.HasPrefix(k, "X-") && k != "X-Filename" {
|
||||
signedHeader = append(signedHeader, fmt.Sprintf("%s=%s", k, r.Header.Get(k)))
|
||||
}
|
||||
}
|
||||
sort.Strings(signedHeader)
|
||||
|
||||
// 读取所有待签名Header
|
||||
rawSignString = serializer.NewRequestSignString(r.URL.Path, strings.Join(signedHeader, "&"), string(body))
|
||||
|
||||
return rawSignString
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package balancer
|
||||
|
||||
type Balancer interface {
|
||||
NextPeer(nodes interface{}) (error, interface{})
|
||||
}
|
||||
|
||||
// NewBalancer 根据策略标识返回新的负载均衡器
|
||||
func NewBalancer(strategy string) Balancer {
|
||||
switch strategy {
|
||||
case "RoundRobin":
|
||||
return &RoundRobin{}
|
||||
default:
|
||||
return &RoundRobin{}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package balancer
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInputNotSlice = errors.New("Input value is not silice")
|
||||
ErrNoAvaliableNode = errors.New("No nodes avaliable")
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
package balancer
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type RoundRobin struct {
|
||||
current uint64
|
||||
}
|
||||
|
||||
// NextPeer 返回轮盘的下一节点
|
||||
func (r *RoundRobin) NextPeer(nodes interface{}) (error, interface{}) {
|
||||
v := reflect.ValueOf(nodes)
|
||||
if v.Kind() != reflect.Slice {
|
||||
return ErrInputNotSlice, nil
|
||||
}
|
||||
|
||||
if v.Len() == 0 {
|
||||
return ErrNoAvaliableNode, nil
|
||||
}
|
||||
|
||||
next := r.NextIndex(v.Len())
|
||||
return nil, v.Index(next).Interface()
|
||||
}
|
||||
|
||||
// NextIndex 返回下一个节点下标
|
||||
func (r *RoundRobin) NextIndex(total int) int {
|
||||
return int(atomic.AddUint64(&r.current, uint64(1)) % uint64(total))
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package cluster
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrFeatureNotExist = errors.New("No nodes in nodepool match the feature specificed")
|
||||
ErrIlegalPath = errors.New("path out of boundary of setting temp folder")
|
||||
)
|
||||
@@ -1,265 +0,0 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/common"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mq"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/gofrs/uuid"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const deleteTempFileDuration = 60 * time.Second
|
||||
|
||||
type MasterNode struct {
|
||||
Model *model.Node
|
||||
aria2RPC rpcService
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// RPCService 通过RPC服务的Aria2任务管理器
|
||||
type rpcService struct {
|
||||
Caller rpc.Client
|
||||
Initialized bool
|
||||
|
||||
parent *MasterNode
|
||||
options *clientOptions
|
||||
}
|
||||
|
||||
type clientOptions struct {
|
||||
Options map[string]interface{} // 创建下载时额外添加的设置
|
||||
}
|
||||
|
||||
// Init 初始化节点
|
||||
func (node *MasterNode) Init(nodeModel *model.Node) {
|
||||
node.lock.Lock()
|
||||
node.Model = nodeModel
|
||||
node.aria2RPC.parent = node
|
||||
node.lock.Unlock()
|
||||
|
||||
node.lock.RLock()
|
||||
if node.Model.Aria2Enabled {
|
||||
node.lock.RUnlock()
|
||||
node.aria2RPC.Init()
|
||||
return
|
||||
}
|
||||
node.lock.RUnlock()
|
||||
}
|
||||
|
||||
func (node *MasterNode) ID() uint {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
return node.Model.ID
|
||||
}
|
||||
|
||||
func (node *MasterNode) Ping(req *serializer.NodePingReq) (*serializer.NodePingResp, error) {
|
||||
return &serializer.NodePingResp{}, nil
|
||||
}
|
||||
|
||||
// IsFeatureEnabled 查询节点的某项功能是否启用
|
||||
func (node *MasterNode) IsFeatureEnabled(feature string) bool {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
switch feature {
|
||||
case "aria2":
|
||||
return node.Model.Aria2Enabled
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (node *MasterNode) MasterAuthInstance() auth.Auth {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
return auth.HMACAuth{SecretKey: []byte(node.Model.MasterKey)}
|
||||
}
|
||||
|
||||
func (node *MasterNode) SlaveAuthInstance() auth.Auth {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
return auth.HMACAuth{SecretKey: []byte(node.Model.SlaveKey)}
|
||||
}
|
||||
|
||||
// SubscribeStatusChange 订阅节点状态更改
|
||||
func (node *MasterNode) SubscribeStatusChange(callback func(isActive bool, id uint)) {
|
||||
}
|
||||
|
||||
// IsActive 返回节点是否在线
|
||||
func (node *MasterNode) IsActive() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Kill 结束aria2请求
|
||||
func (node *MasterNode) Kill() {
|
||||
if node.aria2RPC.Caller != nil {
|
||||
node.aria2RPC.Caller.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// GetAria2Instance 获取主机Aria2实例
|
||||
func (node *MasterNode) GetAria2Instance() common.Aria2 {
|
||||
node.lock.RLock()
|
||||
|
||||
if !node.Model.Aria2Enabled {
|
||||
node.lock.RUnlock()
|
||||
return &common.DummyAria2{}
|
||||
}
|
||||
|
||||
if !node.aria2RPC.Initialized {
|
||||
node.lock.RUnlock()
|
||||
node.aria2RPC.Init()
|
||||
return &common.DummyAria2{}
|
||||
}
|
||||
|
||||
defer node.lock.RUnlock()
|
||||
return &node.aria2RPC
|
||||
}
|
||||
|
||||
func (node *MasterNode) IsMater() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (node *MasterNode) DBModel() *model.Node {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
return node.Model
|
||||
}
|
||||
|
||||
func (r *rpcService) Init() error {
|
||||
r.parent.lock.Lock()
|
||||
defer r.parent.lock.Unlock()
|
||||
r.Initialized = false
|
||||
|
||||
// 客户端已存在,则关闭先前连接
|
||||
if r.Caller != nil {
|
||||
r.Caller.Close()
|
||||
}
|
||||
|
||||
// 解析RPC服务地址
|
||||
server, err := url.Parse(r.parent.Model.Aria2OptionsSerialized.Server)
|
||||
if err != nil {
|
||||
util.Log().Warning("无法解析主机 Aria2 RPC 服务地址,%s", err)
|
||||
return err
|
||||
}
|
||||
server.Path = "/jsonrpc"
|
||||
|
||||
// 加载自定义下载配置
|
||||
var globalOptions map[string]interface{}
|
||||
if r.parent.Model.Aria2OptionsSerialized.Options != "" {
|
||||
err = json.Unmarshal([]byte(r.parent.Model.Aria2OptionsSerialized.Options), &globalOptions)
|
||||
if err != nil {
|
||||
util.Log().Warning("无法解析主机 Aria2 配置,%s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
r.options = &clientOptions{
|
||||
Options: globalOptions,
|
||||
}
|
||||
timeout := r.parent.Model.Aria2OptionsSerialized.Timeout
|
||||
caller, err := rpc.New(context.Background(), server.String(), r.parent.Model.Aria2OptionsSerialized.Token, time.Duration(timeout)*time.Second, mq.GlobalMQ)
|
||||
|
||||
r.Caller = caller
|
||||
r.Initialized = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rpcService) CreateTask(task *model.Download, groupOptions map[string]interface{}) (string, error) {
|
||||
r.parent.lock.RLock()
|
||||
// 生成存储路径
|
||||
guid, _ := uuid.NewV4()
|
||||
path := filepath.Join(
|
||||
r.parent.Model.Aria2OptionsSerialized.TempPath,
|
||||
"aria2",
|
||||
guid.String(),
|
||||
)
|
||||
r.parent.lock.RUnlock()
|
||||
|
||||
// 创建下载任务
|
||||
options := map[string]interface{}{
|
||||
"dir": path,
|
||||
}
|
||||
for k, v := range r.options.Options {
|
||||
options[k] = v
|
||||
}
|
||||
for k, v := range groupOptions {
|
||||
options[k] = v
|
||||
}
|
||||
|
||||
gid, err := r.Caller.AddURI(task.Source, options)
|
||||
if err != nil || gid == "" {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return gid, nil
|
||||
}
|
||||
|
||||
func (r *rpcService) Status(task *model.Download) (rpc.StatusInfo, error) {
|
||||
res, err := r.Caller.TellStatus(task.GID)
|
||||
if err != nil {
|
||||
// 失败后重试
|
||||
util.Log().Debug("无法获取离线下载状态,%s,10秒钟后重试", err)
|
||||
time.Sleep(time.Duration(10) * time.Second)
|
||||
res, err = r.Caller.TellStatus(task.GID)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (r *rpcService) Cancel(task *model.Download) error {
|
||||
// 取消下载任务
|
||||
_, err := r.Caller.Remove(task.GID)
|
||||
if err != nil {
|
||||
util.Log().Warning("无法取消离线下载任务[%s], %s", task.GID, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rpcService) Select(task *model.Download, files []int) error {
|
||||
var selected = make([]string, len(files))
|
||||
for i := 0; i < len(files); i++ {
|
||||
selected[i] = strconv.Itoa(files[i])
|
||||
}
|
||||
_, err := r.Caller.ChangeOption(task.GID, map[string]interface{}{"select-file": strings.Join(selected, ",")})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rpcService) GetConfig() model.Aria2Option {
|
||||
r.parent.lock.RLock()
|
||||
defer r.parent.lock.RUnlock()
|
||||
|
||||
return r.parent.Model.Aria2OptionsSerialized
|
||||
}
|
||||
|
||||
func (s *rpcService) DeleteTempFile(task *model.Download) error {
|
||||
s.parent.lock.RLock()
|
||||
defer s.parent.lock.RUnlock()
|
||||
|
||||
// 避免被aria2占用,异步执行删除
|
||||
go func(src string) {
|
||||
time.Sleep(deleteTempFileDuration)
|
||||
err := os.RemoveAll(src)
|
||||
if err != nil {
|
||||
util.Log().Warning("无法删除离线下载临时目录[%s], %s", src, err)
|
||||
}
|
||||
}(task.Parent)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/common"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
)
|
||||
|
||||
type Node interface {
|
||||
// Init a node from database model
|
||||
Init(node *model.Node)
|
||||
|
||||
// Check if given feature is enabled
|
||||
IsFeatureEnabled(feature string) bool
|
||||
|
||||
// Subscribe node status change to a callback function
|
||||
SubscribeStatusChange(callback func(isActive bool, id uint))
|
||||
|
||||
// Ping the node
|
||||
Ping(req *serializer.NodePingReq) (*serializer.NodePingResp, error)
|
||||
|
||||
// Returns if the node is active
|
||||
IsActive() bool
|
||||
|
||||
// Get instances for aria2 calls
|
||||
GetAria2Instance() common.Aria2
|
||||
|
||||
// Returns unique id of this node
|
||||
ID() uint
|
||||
|
||||
// Kill node and recycle resources
|
||||
Kill()
|
||||
|
||||
// Returns if current node is master node
|
||||
IsMater() bool
|
||||
|
||||
// Get auth instance used to check RPC call from slave to master
|
||||
MasterAuthInstance() auth.Auth
|
||||
|
||||
// Get auth instance used to check RPC call from master to slave
|
||||
SlaveAuthInstance() auth.Auth
|
||||
|
||||
// Get node DB model
|
||||
DBModel() *model.Node
|
||||
}
|
||||
|
||||
// Create new node from DB model
|
||||
func NewNodeFromDBModel(node *model.Node) Node {
|
||||
switch node.Type {
|
||||
case model.SlaveNodeType:
|
||||
slave := &SlaveNode{}
|
||||
slave.Init(node)
|
||||
return slave
|
||||
default:
|
||||
master := &MasterNode{}
|
||||
master.Init(node)
|
||||
return master
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/balancer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var Default *NodePool
|
||||
|
||||
// 需要分类的节点组
|
||||
var featureGroup = []string{"aria2"}
|
||||
|
||||
// Pool 节点池
|
||||
type Pool interface {
|
||||
// Returns active node selected by given feature and load balancer
|
||||
BalanceNodeByFeature(feature string, lb balancer.Balancer) (error, Node)
|
||||
|
||||
// Returns node by ID
|
||||
GetNodeByID(id uint) Node
|
||||
|
||||
// Add given node into pool. If node existed, refresh node.
|
||||
Add(node *model.Node)
|
||||
|
||||
// Delete and kill node from pool by given node id
|
||||
Delete(id uint)
|
||||
}
|
||||
|
||||
// NodePool 通用节点池
|
||||
type NodePool struct {
|
||||
active map[uint]Node
|
||||
inactive map[uint]Node
|
||||
|
||||
featureMap map[string][]Node
|
||||
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// Init 初始化从机节点池
|
||||
func Init() {
|
||||
Default = &NodePool{
|
||||
featureMap: make(map[string][]Node),
|
||||
}
|
||||
if err := Default.initFromDB(); err != nil {
|
||||
util.Log().Warning("节点池初始化失败, %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (pool *NodePool) buildIndexMap() {
|
||||
pool.lock.Lock()
|
||||
for _, feature := range featureGroup {
|
||||
pool.featureMap[feature] = make([]Node, 0)
|
||||
}
|
||||
|
||||
for _, v := range pool.active {
|
||||
for _, feature := range featureGroup {
|
||||
if v.IsFeatureEnabled(feature) {
|
||||
pool.featureMap[feature] = append(pool.featureMap[feature], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
pool.lock.Unlock()
|
||||
}
|
||||
|
||||
func (pool *NodePool) GetNodeByID(id uint) Node {
|
||||
pool.lock.RLock()
|
||||
defer pool.lock.RUnlock()
|
||||
|
||||
if node, ok := pool.active[id]; ok {
|
||||
return node
|
||||
}
|
||||
|
||||
return pool.inactive[id]
|
||||
}
|
||||
|
||||
func (pool *NodePool) nodeStatusChange(isActive bool, id uint) {
|
||||
util.Log().Debug("从机节点 [ID=%d] 状态变更 [Active=%t]", id, isActive)
|
||||
pool.lock.Lock()
|
||||
if isActive {
|
||||
node := pool.inactive[id]
|
||||
delete(pool.inactive, id)
|
||||
pool.active[id] = node
|
||||
} else {
|
||||
node := pool.active[id]
|
||||
delete(pool.active, id)
|
||||
pool.inactive[id] = node
|
||||
}
|
||||
pool.lock.Unlock()
|
||||
|
||||
pool.buildIndexMap()
|
||||
}
|
||||
|
||||
func (pool *NodePool) initFromDB() error {
|
||||
nodes, err := model.GetNodesByStatus(model.NodeActive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pool.lock.Lock()
|
||||
pool.active = make(map[uint]Node)
|
||||
pool.inactive = make(map[uint]Node)
|
||||
for i := 0; i < len(nodes); i++ {
|
||||
pool.add(&nodes[i])
|
||||
}
|
||||
pool.lock.Unlock()
|
||||
|
||||
pool.buildIndexMap()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pool *NodePool) add(node *model.Node) {
|
||||
newNode := NewNodeFromDBModel(node)
|
||||
if newNode.IsActive() {
|
||||
pool.active[node.ID] = newNode
|
||||
} else {
|
||||
pool.inactive[node.ID] = newNode
|
||||
}
|
||||
|
||||
// 订阅节点状态变更
|
||||
newNode.SubscribeStatusChange(func(isActive bool, id uint) {
|
||||
pool.nodeStatusChange(isActive, id)
|
||||
})
|
||||
}
|
||||
|
||||
func (pool *NodePool) Add(node *model.Node) {
|
||||
pool.lock.Lock()
|
||||
defer pool.buildIndexMap()
|
||||
defer pool.lock.Unlock()
|
||||
|
||||
var (
|
||||
old Node
|
||||
ok bool
|
||||
)
|
||||
if old, ok = pool.active[node.ID]; !ok {
|
||||
old, ok = pool.inactive[node.ID]
|
||||
}
|
||||
if old != nil {
|
||||
go old.Init(node)
|
||||
return
|
||||
}
|
||||
|
||||
pool.add(node)
|
||||
}
|
||||
|
||||
func (pool *NodePool) Delete(id uint) {
|
||||
pool.lock.Lock()
|
||||
defer pool.buildIndexMap()
|
||||
defer pool.lock.Unlock()
|
||||
|
||||
if node, ok := pool.active[id]; ok {
|
||||
node.Kill()
|
||||
delete(pool.active, id)
|
||||
return
|
||||
}
|
||||
|
||||
if node, ok := pool.inactive[id]; ok {
|
||||
node.Kill()
|
||||
delete(pool.inactive, id)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// BalanceNodeByFeature 根据 feature 和 LoadBalancer 取出节点
|
||||
func (pool *NodePool) BalanceNodeByFeature(feature string, lb balancer.Balancer) (error, Node) {
|
||||
pool.lock.RLock()
|
||||
defer pool.lock.RUnlock()
|
||||
if nodes, ok := pool.featureMap[feature]; ok {
|
||||
err, res := lb.NextPeer(nodes)
|
||||
if err == nil {
|
||||
return nil, res.(Node)
|
||||
}
|
||||
|
||||
return err, nil
|
||||
}
|
||||
|
||||
return ErrFeatureNotExist, nil
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/common"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SlaveNode struct {
|
||||
Model *model.Node
|
||||
Active bool
|
||||
|
||||
caller slaveCaller
|
||||
callback func(bool, uint)
|
||||
close chan bool
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
type slaveCaller struct {
|
||||
parent *SlaveNode
|
||||
Client request.Client
|
||||
}
|
||||
|
||||
// Init 初始化节点
|
||||
func (node *SlaveNode) Init(nodeModel *model.Node) {
|
||||
node.lock.Lock()
|
||||
node.Model = nodeModel
|
||||
|
||||
// Init http request client
|
||||
var endpoint *url.URL
|
||||
if serverURL, err := url.Parse(node.Model.Server); err == nil {
|
||||
var controller *url.URL
|
||||
controller, _ = url.Parse("/api/v3/slave")
|
||||
endpoint = serverURL.ResolveReference(controller)
|
||||
}
|
||||
|
||||
signTTL := model.GetIntSetting("slave_api_timeout", 60)
|
||||
node.caller.Client = request.NewClient(
|
||||
request.WithMasterMeta(),
|
||||
request.WithTimeout(time.Duration(signTTL)*time.Second),
|
||||
request.WithCredential(auth.HMACAuth{SecretKey: []byte(nodeModel.SlaveKey)}, int64(signTTL)),
|
||||
request.WithEndpoint(endpoint.String()),
|
||||
)
|
||||
|
||||
node.caller.parent = node
|
||||
if node.close != nil {
|
||||
node.lock.Unlock()
|
||||
node.close <- true
|
||||
go node.StartPingLoop()
|
||||
} else {
|
||||
node.Active = true
|
||||
node.lock.Unlock()
|
||||
go node.StartPingLoop()
|
||||
}
|
||||
}
|
||||
|
||||
// IsFeatureEnabled 查询节点的某项功能是否启用
|
||||
func (node *SlaveNode) IsFeatureEnabled(feature string) bool {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
switch feature {
|
||||
case "aria2":
|
||||
return node.Model.Aria2Enabled
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// SubscribeStatusChange 订阅节点状态更改
|
||||
func (node *SlaveNode) SubscribeStatusChange(callback func(bool, uint)) {
|
||||
node.lock.Lock()
|
||||
node.callback = callback
|
||||
node.lock.Unlock()
|
||||
}
|
||||
|
||||
// Ping 从机节点,返回从机负载
|
||||
func (node *SlaveNode) Ping(req *serializer.NodePingReq) (*serializer.NodePingResp, error) {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
reqBodyEncoded, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bodyReader := strings.NewReader(string(reqBodyEncoded))
|
||||
|
||||
resp, err := node.caller.Client.Request(
|
||||
"POST",
|
||||
"heartbeat",
|
||||
bodyReader,
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 处理列取结果
|
||||
if resp.Code != 0 {
|
||||
return nil, serializer.NewErrorFromResponse(resp)
|
||||
}
|
||||
|
||||
var res serializer.NodePingResp
|
||||
|
||||
if resStr, ok := resp.Data.(string); ok {
|
||||
err = json.Unmarshal([]byte(resStr), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// IsActive 返回节点是否在线
|
||||
func (node *SlaveNode) IsActive() bool {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
return node.Active
|
||||
}
|
||||
|
||||
// Kill 结束节点内相关循环
|
||||
func (node *SlaveNode) Kill() {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
if node.close != nil {
|
||||
close(node.close)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAria2Instance 获取从机Aria2实例
|
||||
func (node *SlaveNode) GetAria2Instance() common.Aria2 {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
if !node.Model.Aria2Enabled {
|
||||
return &common.DummyAria2{}
|
||||
}
|
||||
|
||||
return &node.caller
|
||||
}
|
||||
|
||||
func (node *SlaveNode) ID() uint {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
return node.Model.ID
|
||||
}
|
||||
|
||||
func (node *SlaveNode) StartPingLoop() {
|
||||
node.lock.Lock()
|
||||
node.close = make(chan bool)
|
||||
node.lock.Unlock()
|
||||
|
||||
tickDuration := time.Duration(model.GetIntSetting("slave_ping_interval", 300)) * time.Second
|
||||
recoverDuration := time.Duration(model.GetIntSetting("slave_recover_interval", 600)) * time.Second
|
||||
pingTicker := time.Duration(0)
|
||||
|
||||
util.Log().Debug("从机节点 [%s] 启动心跳循环", node.Model.Name)
|
||||
retry := 0
|
||||
recoverMode := false
|
||||
isFirstLoop := true
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-time.After(pingTicker):
|
||||
if pingTicker == 0 {
|
||||
pingTicker = tickDuration
|
||||
}
|
||||
|
||||
util.Log().Debug("从机节点 [%s] 发送Ping", node.Model.Name)
|
||||
res, err := node.Ping(node.getHeartbeatContent(isFirstLoop))
|
||||
isFirstLoop = false
|
||||
|
||||
if err != nil {
|
||||
util.Log().Debug("Ping从机节点 [%s] 时发生错误: %s", node.Model.Name, err)
|
||||
retry++
|
||||
if retry >= model.GetIntSetting("slave_node_retry", 3) {
|
||||
util.Log().Debug("从机节点 [%s] Ping 重试已达到最大限制,将从机节点标记为不可用", node.Model.Name)
|
||||
node.changeStatus(false)
|
||||
|
||||
if !recoverMode {
|
||||
// 启动恢复监控循环
|
||||
util.Log().Debug("从机节点 [%s] 进入恢复模式", node.Model.Name)
|
||||
pingTicker = recoverDuration
|
||||
recoverMode = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if recoverMode {
|
||||
util.Log().Debug("从机节点 [%s] 复活", node.Model.Name)
|
||||
pingTicker = tickDuration
|
||||
recoverMode = false
|
||||
isFirstLoop = true
|
||||
}
|
||||
|
||||
util.Log().Debug("从机节点 [%s] 状态: %s", node.Model.Name, res)
|
||||
node.changeStatus(true)
|
||||
retry = 0
|
||||
}
|
||||
|
||||
case <-node.close:
|
||||
util.Log().Debug("从机节点 [%s] 收到关闭信号", node.Model.Name)
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (node *SlaveNode) IsMater() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (node *SlaveNode) MasterAuthInstance() auth.Auth {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
return auth.HMACAuth{SecretKey: []byte(node.Model.MasterKey)}
|
||||
}
|
||||
|
||||
func (node *SlaveNode) SlaveAuthInstance() auth.Auth {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
return auth.HMACAuth{SecretKey: []byte(node.Model.SlaveKey)}
|
||||
}
|
||||
|
||||
func (node *SlaveNode) DBModel() *model.Node {
|
||||
node.lock.RLock()
|
||||
defer node.lock.RUnlock()
|
||||
|
||||
return node.Model
|
||||
}
|
||||
|
||||
// getHeartbeatContent gets serializer.NodePingReq used to send heartbeat to slave
|
||||
func (node *SlaveNode) getHeartbeatContent(isUpdate bool) *serializer.NodePingReq {
|
||||
return &serializer.NodePingReq{
|
||||
SiteURL: model.GetSiteURL().String(),
|
||||
IsUpdate: isUpdate,
|
||||
SiteID: model.GetSettingByName("siteID"),
|
||||
Node: node.Model,
|
||||
CredentialTTL: model.GetIntSetting("slave_api_timeout", 60),
|
||||
}
|
||||
}
|
||||
|
||||
func (node *SlaveNode) changeStatus(isActive bool) {
|
||||
node.lock.RLock()
|
||||
id := node.Model.ID
|
||||
if isActive != node.Active {
|
||||
node.lock.RUnlock()
|
||||
node.lock.Lock()
|
||||
node.Active = isActive
|
||||
node.lock.Unlock()
|
||||
node.callback(isActive, id)
|
||||
} else {
|
||||
node.lock.RUnlock()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *slaveCaller) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendAria2Call send remote aria2 call to slave node
|
||||
func (s *slaveCaller) SendAria2Call(body *serializer.SlaveAria2Call, scope string) (*serializer.Response, error) {
|
||||
reqReader, err := getAria2RequestBody(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.Client.Request(
|
||||
"POST",
|
||||
"aria2/"+scope,
|
||||
reqReader,
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
}
|
||||
|
||||
func (s *slaveCaller) CreateTask(task *model.Download, options map[string]interface{}) (string, error) {
|
||||
s.parent.lock.RLock()
|
||||
defer s.parent.lock.RUnlock()
|
||||
|
||||
req := &serializer.SlaveAria2Call{
|
||||
Task: task,
|
||||
GroupOptions: options,
|
||||
}
|
||||
|
||||
res, err := s.SendAria2Call(req, "task")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return "", serializer.NewErrorFromResponse(res)
|
||||
}
|
||||
|
||||
return res.Data.(string), err
|
||||
}
|
||||
|
||||
func (s *slaveCaller) Status(task *model.Download) (rpc.StatusInfo, error) {
|
||||
s.parent.lock.RLock()
|
||||
defer s.parent.lock.RUnlock()
|
||||
|
||||
req := &serializer.SlaveAria2Call{
|
||||
Task: task,
|
||||
}
|
||||
|
||||
res, err := s.SendAria2Call(req, "status")
|
||||
if err != nil {
|
||||
return rpc.StatusInfo{}, err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return rpc.StatusInfo{}, serializer.NewErrorFromResponse(res)
|
||||
}
|
||||
|
||||
var status rpc.StatusInfo
|
||||
res.GobDecode(&status)
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
func (s *slaveCaller) Cancel(task *model.Download) error {
|
||||
s.parent.lock.RLock()
|
||||
defer s.parent.lock.RUnlock()
|
||||
|
||||
req := &serializer.SlaveAria2Call{
|
||||
Task: task,
|
||||
}
|
||||
|
||||
res, err := s.SendAria2Call(req, "cancel")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return serializer.NewErrorFromResponse(res)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *slaveCaller) Select(task *model.Download, files []int) error {
|
||||
s.parent.lock.RLock()
|
||||
defer s.parent.lock.RUnlock()
|
||||
|
||||
req := &serializer.SlaveAria2Call{
|
||||
Task: task,
|
||||
Files: files,
|
||||
}
|
||||
|
||||
res, err := s.SendAria2Call(req, "select")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return serializer.NewErrorFromResponse(res)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *slaveCaller) GetConfig() model.Aria2Option {
|
||||
s.parent.lock.RLock()
|
||||
defer s.parent.lock.RUnlock()
|
||||
|
||||
return s.parent.Model.Aria2OptionsSerialized
|
||||
}
|
||||
|
||||
func (s *slaveCaller) DeleteTempFile(task *model.Download) error {
|
||||
s.parent.lock.RLock()
|
||||
defer s.parent.lock.RUnlock()
|
||||
|
||||
req := &serializer.SlaveAria2Call{
|
||||
Task: task,
|
||||
}
|
||||
|
||||
res, err := s.SendAria2Call(req, "delete")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return serializer.NewErrorFromResponse(res)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAria2RequestBody(body *serializer.SlaveAria2Call) (io.Reader, error) {
|
||||
reqBodyEncoded, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return strings.NewReader(string(reqBodyEncoded)), nil
|
||||
}
|
||||
@@ -16,7 +16,6 @@ type database struct {
|
||||
TablePrefix string
|
||||
DBFile string
|
||||
Port int
|
||||
Charset string
|
||||
}
|
||||
|
||||
// system 系统通用配置
|
||||
|
||||
@@ -12,10 +12,9 @@ var RedisConfig = &redis{
|
||||
|
||||
// DatabaseConfig 数据库配置
|
||||
var DatabaseConfig = &database{
|
||||
Type: "UNSET",
|
||||
Charset: "utf8",
|
||||
DBFile: "cloudreve.db",
|
||||
Port: 3306,
|
||||
Type: "UNSET",
|
||||
DBFile: "cloudreve.db",
|
||||
Port: 3306,
|
||||
}
|
||||
|
||||
// SystemConfig 系统公用配置
|
||||
@@ -69,5 +68,5 @@ var SSLConfig = &ssl{
|
||||
}
|
||||
|
||||
var UnixConfig = &unix{
|
||||
Listen: "",
|
||||
Listen: "",
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
package conf
|
||||
|
||||
// BackendVersion 当前后端版本号
|
||||
var BackendVersion = "3.4.0-beta1"
|
||||
var BackendVersion = "3.2.1"
|
||||
|
||||
// RequiredDBVersion 与当前版本匹配的数据库版本
|
||||
var RequiredDBVersion = "3.4.0-beta1"
|
||||
var RequiredDBVersion = "3.2.1"
|
||||
|
||||
// RequiredStaticVersion 与当前版本匹配的静态资源版本
|
||||
var RequiredStaticVersion = "3.4.0-beta1"
|
||||
var RequiredStaticVersion = "3.2.1"
|
||||
|
||||
// IsPro 是否为Pro版本
|
||||
var IsPro = "false"
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package crontab
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
)
|
||||
|
||||
type AddressJson struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
RequestID string `json:"request_id"`
|
||||
Result struct {
|
||||
Location struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
} `json:"location"`
|
||||
Address string `json:"address"`
|
||||
FormattedAddresses struct {
|
||||
Recommend string `json:"recommend"`
|
||||
Rough string `json:"rough"`
|
||||
} `json:"formatted_addresses"`
|
||||
AddressComponent struct {
|
||||
Nation string `json:"nation"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
District string `json:"district"`
|
||||
Street string `json:"street"`
|
||||
StreetNumber string `json:"street_number"`
|
||||
} `json:"address_component"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type AddressExif struct{
|
||||
Address string `json:"address"`
|
||||
Nation string `json:"nation"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
District string `json:"district"`
|
||||
Street string `json:"street"`
|
||||
StreetNumber string `json:"street_number"`
|
||||
}
|
||||
|
||||
func syncPhotoAddress() {
|
||||
// 同步照片的经纬度为文本地址
|
||||
syncPhotoLatLongToAddress()
|
||||
|
||||
util.Log().Info("定时任务 [cron_sync_photo_lat_long_to_address] 执行完毕")
|
||||
}
|
||||
|
||||
func syncPhotoLatLongToAddress() {
|
||||
page := 1
|
||||
pageSize := 10
|
||||
for true{
|
||||
files , _ := model.GetEmptyLocationFilesByPage(uint(page), uint(pageSize))
|
||||
if len(files) <= 0 {
|
||||
break
|
||||
}
|
||||
for i := 0; i < len(files); i++{
|
||||
file := files[i]
|
||||
util.Log().Debug("file name: %s",file.Name)
|
||||
util.Log().Debug("file ExifLatLong: %s",file.ExifLatLong)
|
||||
util.Log().Debug("file ExifAddress: %s",file.ExifAddress)
|
||||
|
||||
// 获取文件数据流
|
||||
url := "https://apis.map.qq.com/ws/geocoder/v1/?location="+ file.ExifLatLong +"&get_poi=1&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77"
|
||||
client := request.HTTPClient{}
|
||||
resp := client.Request(
|
||||
"GET",
|
||||
url,
|
||||
nil,
|
||||
request.WithHeader(
|
||||
http.Header{"Referer": {"https://lbs.qq.com/"}},
|
||||
),
|
||||
)
|
||||
|
||||
respString, err := resp.GetResponse()
|
||||
if err != nil{
|
||||
util.Log().Warning("response error: %s",err)
|
||||
}
|
||||
|
||||
var addressJson AddressJson
|
||||
err = json.Unmarshal([]byte(respString), &addressJson)
|
||||
if err != nil {
|
||||
util.Log().Warning("解析经纬度结果错误原始文本: %s",respString)
|
||||
util.Log().Warning("解析经纬度结果错误: %s",err)
|
||||
continue
|
||||
}
|
||||
var addressInfo AddressExif
|
||||
addressInfo.Address = addressJson.Result.Address
|
||||
addressInfo.StreetNumber = addressJson.Result.AddressComponent.StreetNumber
|
||||
addressInfo.District = addressJson.Result.AddressComponent.District
|
||||
addressInfo.City = addressJson.Result.AddressComponent.City
|
||||
addressInfo.Province = addressJson.Result.AddressComponent.Province
|
||||
addressInfo.Nation = addressJson.Result.AddressComponent.Nation
|
||||
|
||||
if addressInfoStr, err := json.Marshal(addressInfo); err == nil {
|
||||
file.UpdatePicExifAddress(string(addressInfoStr))
|
||||
util.Log().Debug("解析经纬度结果重组: %s",string(addressInfoStr))
|
||||
}
|
||||
}
|
||||
|
||||
page++
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -21,13 +21,15 @@ func Reload() {
|
||||
func Init() {
|
||||
util.Log().Info("初始化定时任务...")
|
||||
// 读取cron日程设置
|
||||
options := model.GetSettingByNames("cron_garbage_collect")
|
||||
options := model.GetSettingByNames("cron_garbage_collect","cron_sync_photo_lat_long_to_address")
|
||||
Cron := cron.New()
|
||||
for k, v := range options {
|
||||
var handler func()
|
||||
switch k {
|
||||
case "cron_garbage_collect":
|
||||
handler = garbageCollect
|
||||
case "cron_sync_photo_lat_long_to_address":
|
||||
handler = syncPhotoAddress
|
||||
default:
|
||||
util.Log().Warning("未知定时任务类型 [%s],跳过", k)
|
||||
continue
|
||||
|
||||
+1
-2
@@ -77,12 +77,11 @@ func (client *SMTP) Init() {
|
||||
d := mail.NewDialer(client.Config.Host, client.Config.Port, client.Config.User, client.Config.Password)
|
||||
d.Timeout = time.Duration(client.Config.Keepalive+5) * time.Second
|
||||
client.chOpen = true
|
||||
|
||||
// 是否启用 SSL
|
||||
d.SSL = false
|
||||
if client.Config.Encryption {
|
||||
d.SSL = true
|
||||
}
|
||||
d.StartTLSPolicy = mail.OpportunisticStartTLS
|
||||
|
||||
var s mail.SendCloser
|
||||
var err error
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"io"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Handler 存储策略适配器
|
||||
type Handler interface {
|
||||
// 上传文件, dst为文件存储路径,size 为文件大小。上下文关闭
|
||||
// 时,应取消上传并清理临时文件
|
||||
Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error
|
||||
|
||||
// 删除一个或多个给定路径的文件,返回删除失败的文件路径列表及错误
|
||||
Delete(ctx context.Context, files []string) ([]string, error)
|
||||
|
||||
// 获取文件内容
|
||||
Get(ctx context.Context, path string) (response.RSCloser, error)
|
||||
|
||||
// 获取缩略图,可直接在ContentResponse中返回文件数据流,也可指
|
||||
// 定为重定向
|
||||
Thumb(ctx context.Context, path string) (*response.ContentResponse, error)
|
||||
|
||||
// 获取外链/下载地址,
|
||||
// url - 站点本身地址,
|
||||
// isDownload - 是否直接下载
|
||||
Source(ctx context.Context, path string, url url.URL, ttl int64, isDownload bool, speed int) (string, error)
|
||||
|
||||
// Token 获取有效期为ttl的上传凭证和签名,同时回调会话有效期为sessionTTL
|
||||
Token(ctx context.Context, ttl int64, callbackKey string) (serializer.UploadCredential, error)
|
||||
|
||||
// List 递归列取远程端path路径下文件、目录,不包含path本身,
|
||||
// 返回的对象路径以path作为起始根目录.
|
||||
// recursive - 是否递归列出
|
||||
List(ctx context.Context, path string, recursive bool) ([]response.Object, error)
|
||||
}
|
||||
@@ -53,23 +53,12 @@ func (err RespError) Error() string {
|
||||
return err.APIError.Message
|
||||
}
|
||||
|
||||
func (client *Client) getRequestURL(api string, opts ...Option) string {
|
||||
options := newDefaultOption()
|
||||
for _, o := range opts {
|
||||
o.apply(options)
|
||||
}
|
||||
|
||||
func (client *Client) getRequestURL(api string) string {
|
||||
base, _ := url.Parse(client.Endpoints.EndpointURL)
|
||||
if base == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if options.useDriverResource {
|
||||
base.Path = path.Join(base.Path, client.Endpoints.DriverResource, api)
|
||||
} else {
|
||||
base.Path = path.Join(base.Path, api)
|
||||
}
|
||||
|
||||
base.Path = path.Join(base.Path, api)
|
||||
return base.String()
|
||||
}
|
||||
|
||||
@@ -78,9 +67,9 @@ func (client *Client) ListChildren(ctx context.Context, path string) ([]FileInfo
|
||||
var requestURL string
|
||||
dst := strings.TrimPrefix(path, "/")
|
||||
if dst == "" {
|
||||
requestURL = client.getRequestURL("root/children")
|
||||
requestURL = client.getRequestURL("me/drive/root/children")
|
||||
} else {
|
||||
requestURL = client.getRequestURL("root:/" + dst + ":/children")
|
||||
requestURL = client.getRequestURL("me/drive/root:/" + dst + ":/children")
|
||||
}
|
||||
|
||||
res, err := client.requestWithStr(ctx, "GET", requestURL+"?$top=999999999", "", 200)
|
||||
@@ -114,10 +103,10 @@ func (client *Client) ListChildren(ctx context.Context, path string) ([]FileInfo
|
||||
func (client *Client) Meta(ctx context.Context, id string, path string) (*FileInfo, error) {
|
||||
var requestURL string
|
||||
if id != "" {
|
||||
requestURL = client.getRequestURL("items/" + id)
|
||||
requestURL = client.getRequestURL("/me/drive/items/" + id)
|
||||
} else {
|
||||
dst := strings.TrimPrefix(path, "/")
|
||||
requestURL = client.getRequestURL("root:/" + dst)
|
||||
requestURL = client.getRequestURL("me/drive/root:/" + dst)
|
||||
}
|
||||
|
||||
res, err := client.requestWithStr(ctx, "GET", requestURL+"?expand=thumbnails", "", 200)
|
||||
@@ -140,13 +129,14 @@ func (client *Client) Meta(ctx context.Context, id string, path string) (*FileIn
|
||||
|
||||
// CreateUploadSession 创建分片上传会话
|
||||
func (client *Client) CreateUploadSession(ctx context.Context, dst string, opts ...Option) (string, error) {
|
||||
|
||||
options := newDefaultOption()
|
||||
for _, o := range opts {
|
||||
o.apply(options)
|
||||
}
|
||||
|
||||
dst = strings.TrimPrefix(dst, "/")
|
||||
requestURL := client.getRequestURL("root:/" + dst + ":/createUploadSession")
|
||||
requestURL := client.getRequestURL("me/drive/root:/" + dst + ":/createUploadSession")
|
||||
body := map[string]map[string]interface{}{
|
||||
"item": {
|
||||
"@microsoft.graph.conflictBehavior": options.conflictBehavior,
|
||||
@@ -171,33 +161,6 @@ func (client *Client) CreateUploadSession(ctx context.Context, dst string, opts
|
||||
return uploadSession.UploadURL, nil
|
||||
}
|
||||
|
||||
// GetSiteIDByURL 通过 SharePoint 站点 URL 获取站点ID
|
||||
func (client *Client) GetSiteIDByURL(ctx context.Context, siteUrl string) (string, error) {
|
||||
siteUrlParsed, err := url.Parse(siteUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hostName := siteUrlParsed.Hostname()
|
||||
relativePath := strings.Trim(siteUrlParsed.Path, "/")
|
||||
requestURL := client.getRequestURL(fmt.Sprintf("sites/%s:/%s", hostName, relativePath), WithDriverResource(false))
|
||||
res, reqErr := client.requestWithStr(ctx, "GET", requestURL, "", 200)
|
||||
if reqErr != nil {
|
||||
return "", reqErr
|
||||
}
|
||||
|
||||
var (
|
||||
decodeErr error
|
||||
siteInfo Site
|
||||
)
|
||||
decodeErr = json.Unmarshal([]byte(res), &siteInfo)
|
||||
if decodeErr != nil {
|
||||
return "", decodeErr
|
||||
}
|
||||
|
||||
return siteInfo.ID, nil
|
||||
}
|
||||
|
||||
// GetUploadSessionStatus 查询上传会话状态
|
||||
func (client *Client) GetUploadSessionStatus(ctx context.Context, uploadURL string) (*UploadSessionResponse, error) {
|
||||
res, err := client.requestWithStr(ctx, "GET", uploadURL, "", 200)
|
||||
@@ -337,7 +300,7 @@ func (client *Client) SimpleUpload(ctx context.Context, dst string, body io.Read
|
||||
}
|
||||
|
||||
dst = strings.TrimPrefix(dst, "/")
|
||||
requestURL := client.getRequestURL("root:/" + dst + ":/content")
|
||||
requestURL := client.getRequestURL("me/drive/root:/" + dst + ":/content")
|
||||
requestURL += ("?@microsoft.graph.conflictBehavior=" + options.conflictBehavior)
|
||||
|
||||
res, err := client.request(ctx, "PUT", requestURL, body, request.WithContentLength(int64(size)),
|
||||
@@ -394,8 +357,7 @@ func (client *Client) BatchDelete(ctx context.Context, dst []string) ([]string,
|
||||
// 由于API限制,最多删除20个
|
||||
func (client *Client) Delete(ctx context.Context, dst []string) ([]string, error) {
|
||||
body := client.makeBatchDeleteRequestsBody(dst)
|
||||
res, err := client.requestWithStr(ctx, "POST", client.getRequestURL("$batch",
|
||||
WithDriverResource(false)), body, 200)
|
||||
res, err := client.requestWithStr(ctx, "POST", client.getRequestURL("$batch"), body, 200)
|
||||
if err != nil {
|
||||
return dst, err
|
||||
}
|
||||
@@ -434,7 +396,7 @@ func (client *Client) makeBatchDeleteRequestsBody(files []string) string {
|
||||
}
|
||||
for i, v := range files {
|
||||
v = strings.TrimPrefix(v, "/")
|
||||
filePath, _ := url.Parse("/" + client.Endpoints.DriverResource + "/root:/")
|
||||
filePath, _ := url.Parse("/me/drive/root:/")
|
||||
filePath.Path = path.Join(filePath.Path, v)
|
||||
req.Requests[i] = BatchRequest{
|
||||
ID: v,
|
||||
@@ -450,7 +412,17 @@ func (client *Client) makeBatchDeleteRequestsBody(files []string) string {
|
||||
// GetThumbURL 获取给定尺寸的缩略图URL
|
||||
func (client *Client) GetThumbURL(ctx context.Context, dst string, w, h uint) (string, error) {
|
||||
dst = strings.TrimPrefix(dst, "/")
|
||||
requestURL := client.getRequestURL("root:/"+dst+":/thumbnails/0") + "/large"
|
||||
var (
|
||||
cropOption string
|
||||
requestURL string
|
||||
)
|
||||
if client.Endpoints.isInChina {
|
||||
cropOption = "large"
|
||||
requestURL = client.getRequestURL("me/drive/root:/"+dst+":/thumbnails/0") + "/" + cropOption
|
||||
} else {
|
||||
cropOption = fmt.Sprintf("c%dx%d_Crop", w, h)
|
||||
requestURL = client.getRequestURL("me/drive/root:/"+dst+":/thumbnails") + "?select=" + cropOption
|
||||
}
|
||||
|
||||
res, err := client.requestWithStr(ctx, "GET", requestURL, "", 200)
|
||||
if err != nil {
|
||||
@@ -471,7 +443,7 @@ func (client *Client) GetThumbURL(ctx context.Context, dst string, w, h uint) (s
|
||||
}
|
||||
|
||||
if len(thumbRes.Value) == 1 {
|
||||
if res, ok := thumbRes.Value[0]["large"]; ok {
|
||||
if res, ok := thumbRes.Value[0][cropOption]; ok {
|
||||
return res.(map[string]interface{})["url"].(string), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,82 +167,6 @@ func TestClient_GetRequestURL(t *testing.T) {
|
||||
client.Endpoints.EndpointURL = string([]byte{0x7f})
|
||||
asserts.Equal("", client.getRequestURL("123"))
|
||||
}
|
||||
|
||||
// 使用DriverResource
|
||||
{
|
||||
client.Endpoints.EndpointURL = "https://graph.microsoft.com/v1.0"
|
||||
asserts.Equal("https://graph.microsoft.com/v1.0/me/drive/123", client.getRequestURL("123"))
|
||||
}
|
||||
|
||||
// 不使用DriverResource
|
||||
{
|
||||
client.Endpoints.EndpointURL = "https://graph.microsoft.com/v1.0"
|
||||
asserts.Equal("https://graph.microsoft.com/v1.0/123", client.getRequestURL("123", WithDriverResource(false)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetSiteIDByURL(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
client, _ := NewClient(&model.Policy{})
|
||||
client.Credential.AccessToken = "AccessToken"
|
||||
|
||||
// 请求失败
|
||||
{
|
||||
client.Credential.ExpiresIn = 0
|
||||
res, err := client.GetSiteIDByURL(context.Background(), "https://cquedu.sharepoint.com")
|
||||
asserts.Error(err)
|
||||
asserts.Empty(res)
|
||||
|
||||
}
|
||||
|
||||
// 返回未知响应
|
||||
{
|
||||
client.Credential.ExpiresIn = time.Now().Add(time.Duration(100) * time.Hour).Unix()
|
||||
clientMock := ClientMock{}
|
||||
clientMock.On(
|
||||
"Request",
|
||||
"GET",
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`???`)),
|
||||
},
|
||||
})
|
||||
client.Request = clientMock
|
||||
res, err := client.GetSiteIDByURL(context.Background(), "https://cquedu.sharepoint.com")
|
||||
clientMock.AssertExpectations(t)
|
||||
asserts.Error(err)
|
||||
asserts.Empty(res)
|
||||
}
|
||||
|
||||
// 返回正常
|
||||
{
|
||||
client.Credential.ExpiresIn = time.Now().Add(time.Duration(100) * time.Hour).Unix()
|
||||
clientMock := ClientMock{}
|
||||
clientMock.On(
|
||||
"Request",
|
||||
"GET",
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"id":"123321"}`)),
|
||||
},
|
||||
})
|
||||
client.Request = clientMock
|
||||
res, err := client.GetSiteIDByURL(context.Background(), "https://cquedu.sharepoint.com")
|
||||
clientMock.AssertExpectations(t)
|
||||
asserts.NoError(err)
|
||||
asserts.NotEmpty(res)
|
||||
asserts.Equal("123321", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Meta(t *testing.T) {
|
||||
@@ -966,7 +890,7 @@ func TestClient_GetThumbURL(t *testing.T) {
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"value":[{"large":{"url":"thumb"}}]}`)),
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"value":[{"c1x1_Crop":{"url":"thumb"}}]}`)),
|
||||
},
|
||||
})
|
||||
client.Request = clientMock
|
||||
|
||||
@@ -37,16 +37,14 @@ type Endpoints struct {
|
||||
OAuthEndpoints *oauthEndpoint
|
||||
EndpointURL string // 接口请求的基URL
|
||||
isInChina bool // 是否为世纪互联
|
||||
DriverResource string // 要使用的驱动器
|
||||
}
|
||||
|
||||
// NewClient 根据存储策略获取新的client
|
||||
func NewClient(policy *model.Policy) (*Client, error) {
|
||||
client := &Client{
|
||||
Endpoints: &Endpoints{
|
||||
OAuthURL: policy.BaseURL,
|
||||
EndpointURL: policy.Server,
|
||||
DriverResource: policy.OptionsSerialized.OdDriver,
|
||||
OAuthURL: policy.BaseURL,
|
||||
EndpointURL: policy.Server,
|
||||
},
|
||||
Credential: &Credential{
|
||||
RefreshToken: policy.AccessKey,
|
||||
@@ -55,11 +53,7 @@ func NewClient(policy *model.Policy) (*Client, error) {
|
||||
ClientID: policy.BucketName,
|
||||
ClientSecret: policy.SecretKey,
|
||||
Redirect: policy.OptionsSerialized.OdRedirect,
|
||||
Request: request.NewClient(),
|
||||
}
|
||||
|
||||
if client.Endpoints.DriverResource == "" {
|
||||
client.Endpoints.DriverResource = "me/drive"
|
||||
Request: request.HTTPClient{},
|
||||
}
|
||||
|
||||
oauthBase := client.getOAuthEndpoint()
|
||||
|
||||
@@ -12,9 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
@@ -28,16 +26,6 @@ type Driver struct {
|
||||
HTTPClient request.Client
|
||||
}
|
||||
|
||||
// NewDriver 从存储策略初始化新的Driver实例
|
||||
func NewDriver(policy *model.Policy) (driver.Handler, error) {
|
||||
client, err := NewClient(policy)
|
||||
return Driver{
|
||||
Policy: policy,
|
||||
Client: client,
|
||||
HTTPClient: request.NewClient(),
|
||||
}, err
|
||||
}
|
||||
|
||||
// List 列取项目
|
||||
func (handler Driver) List(ctx context.Context, base string, recursive bool) ([]response.Object, error) {
|
||||
base = strings.TrimPrefix(base, "/")
|
||||
@@ -167,19 +155,6 @@ func (handler Driver) Source(
|
||||
cacheKey := fmt.Sprintf("onedrive_source_%d_%s", handler.Policy.ID, path)
|
||||
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
|
||||
cacheKey = fmt.Sprintf("onedrive_source_file_%d_%d", file.UpdatedAt.Unix(), file.ID)
|
||||
// 如果是永久链接,则返回签名后的中转外链
|
||||
if ttl == 0 {
|
||||
signedURI, err := auth.SignURI(
|
||||
auth.General,
|
||||
fmt.Sprintf("/api/v3/file/source/%d/%s", file.ID, file.Name),
|
||||
ttl,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return baseURL.ResolveReference(signedURI).String(), nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 尝试从缓存中查找
|
||||
|
||||
@@ -3,7 +3,6 @@ package onedrive
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -136,7 +135,7 @@ func TestDriver_Source(t *testing.T) {
|
||||
|
||||
// 失败
|
||||
{
|
||||
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 1, true, 0)
|
||||
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 0, true, 0)
|
||||
asserts.Error(err)
|
||||
asserts.Empty(res)
|
||||
}
|
||||
@@ -145,7 +144,7 @@ func TestDriver_Source(t *testing.T) {
|
||||
{
|
||||
handler.Client.Credential.ExpiresIn = time.Now().Add(time.Duration(100) * time.Hour).Unix()
|
||||
handler.Client.Credential.AccessToken = "1"
|
||||
cache.Set("onedrive_source_0_123.jpg", "res", 1)
|
||||
cache.Set("onedrive_source_0_123.jpg", "res", 0)
|
||||
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 0, true, 0)
|
||||
cache.Deletes([]string{"0_123.jpg"}, "onedrive_source_")
|
||||
asserts.NoError(err)
|
||||
@@ -161,7 +160,7 @@ func TestDriver_Source(t *testing.T) {
|
||||
handler.Client.Credential.ExpiresIn = time.Now().Add(time.Duration(100) * time.Hour).Unix()
|
||||
handler.Client.Credential.AccessToken = "1"
|
||||
cache.Set(fmt.Sprintf("onedrive_source_file_%d_1", file.UpdatedAt.Unix()), "res", 0)
|
||||
res, err := handler.Source(ctx, "123.jpg", url.URL{}, 1, true, 0)
|
||||
res, err := handler.Source(ctx, "123.jpg", url.URL{}, 0, true, 0)
|
||||
cache.Deletes([]string{"0_123.jpg"}, "onedrive_source_")
|
||||
asserts.NoError(err)
|
||||
asserts.Equal("res", res)
|
||||
@@ -186,25 +185,10 @@ func TestDriver_Source(t *testing.T) {
|
||||
})
|
||||
handler.Client.Request = clientMock
|
||||
handler.Client.Credential.AccessToken = "1"
|
||||
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 1, true, 0)
|
||||
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 0, true, 0)
|
||||
asserts.NoError(err)
|
||||
asserts.Equal("123321", res)
|
||||
}
|
||||
|
||||
// 成功 永久直链
|
||||
{
|
||||
file := model.File{}
|
||||
file.ID = 1
|
||||
file.Name = "123.jpg"
|
||||
file.UpdatedAt = time.Now()
|
||||
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, file)
|
||||
handler.Client.Credential.ExpiresIn = time.Now().Add(time.Duration(100) * time.Hour).Unix()
|
||||
auth.General = auth.HMACAuth{}
|
||||
handler.Client.Credential.AccessToken = "1"
|
||||
res, err := handler.Source(ctx, "123.jpg", url.URL{}, 0, true, 0)
|
||||
asserts.NoError(err)
|
||||
asserts.Contains(res, "/api/v3/file/source/1/123.jpg?sign")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_List(t *testing.T) {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package onedrive
|
||||
|
||||
import "sync"
|
||||
|
||||
// CredentialLock 针对存储策略凭证的锁
|
||||
type CredentialLock interface {
|
||||
Lock(uint)
|
||||
Unlock(uint)
|
||||
}
|
||||
|
||||
var GlobalMutex = mutexMap{}
|
||||
|
||||
type mutexMap struct {
|
||||
locks sync.Map
|
||||
}
|
||||
|
||||
func (m *mutexMap) Lock(id uint) {
|
||||
lock, _ := m.locks.LoadOrStore(id, &sync.Mutex{})
|
||||
lock.(*sync.Mutex).Lock()
|
||||
}
|
||||
|
||||
func (m *mutexMap) Unlock(id uint) {
|
||||
lock, _ := m.locks.LoadOrStore(id, &sync.Mutex{})
|
||||
lock.(*sync.Mutex).Unlock()
|
||||
}
|
||||
@@ -10,9 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/slave"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
)
|
||||
|
||||
@@ -126,13 +124,6 @@ func (client *Client) ObtainToken(ctx context.Context, opts ...Option) (*Credent
|
||||
|
||||
// UpdateCredential 更新凭证,并检查有效期
|
||||
func (client *Client) UpdateCredential(ctx context.Context) error {
|
||||
if conf.SystemConfig.Mode == "slave" {
|
||||
return client.fetchCredentialFromMaster(ctx)
|
||||
}
|
||||
|
||||
GlobalMutex.Lock(client.Policy.ID)
|
||||
defer GlobalMutex.Unlock(client.Policy.ID)
|
||||
|
||||
// 如果已存在凭证
|
||||
if client.Credential != nil && client.Credential.AccessToken != "" {
|
||||
// 检查已有凭证是否过期
|
||||
@@ -169,21 +160,10 @@ func (client *Client) UpdateCredential(ctx context.Context) error {
|
||||
client.Credential = credential
|
||||
|
||||
// 更新存储策略的 RefreshToken
|
||||
client.Policy.UpdateAccessKeyAndClearCache(credential.RefreshToken)
|
||||
client.Policy.UpdateAccessKey(credential.RefreshToken)
|
||||
|
||||
// 更新缓存
|
||||
cache.Set("onedrive_"+client.ClientID, *credential, int(expires))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCredential 更新凭证,并检查有效期
|
||||
func (client *Client) fetchCredentialFromMaster(ctx context.Context) error {
|
||||
res, err := slave.DefaultController.GetOneDriveToken(client.Policy.MasterID, client.Policy.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client.Credential = &Credential{AccessToken: res}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,12 +8,11 @@ type Option interface {
|
||||
}
|
||||
|
||||
type options struct {
|
||||
redirect string
|
||||
code string
|
||||
refreshToken string
|
||||
conflictBehavior string
|
||||
expires time.Time
|
||||
useDriverResource bool
|
||||
redirect string
|
||||
code string
|
||||
refreshToken string
|
||||
conflictBehavior string
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
type optionFunc func(*options)
|
||||
@@ -39,21 +38,13 @@ func WithConflictBehavior(t string) Option {
|
||||
})
|
||||
}
|
||||
|
||||
// WithConflictBehavior 设置文件重名后的处理方式
|
||||
func WithDriverResource(t bool) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
o.useDriverResource = t
|
||||
})
|
||||
}
|
||||
|
||||
func (f optionFunc) apply(o *options) {
|
||||
f(o)
|
||||
}
|
||||
|
||||
func newDefaultOption() *options {
|
||||
return &options{
|
||||
conflictBehavior: "fail",
|
||||
useDriverResource: true,
|
||||
expires: time.Now().UTC().Add(time.Duration(1) * time.Hour),
|
||||
conflictBehavior: "fail",
|
||||
expires: time.Now().UTC().Add(time.Duration(1) * time.Hour),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,15 +131,6 @@ type OAuthError struct {
|
||||
CorrelationID string `json:"correlation_id"`
|
||||
}
|
||||
|
||||
// Site SharePoint 站点信息
|
||||
type Site struct {
|
||||
Description string `json:"description"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
WebUrl string `json:"webUrl"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(Credential{})
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func GetPublicKey(r *http.Request) ([]byte, error) {
|
||||
}
|
||||
|
||||
// 获取公钥
|
||||
client := request.NewClient()
|
||||
client := request.HTTPClient{}
|
||||
body, err := client.Request("GET", string(pubURL), nil).
|
||||
CheckHTTPResponse(200).
|
||||
GetResponse()
|
||||
|
||||
@@ -292,7 +292,7 @@ func TestDriver_Get(t *testing.T) {
|
||||
BucketName: "test",
|
||||
Server: "oss-cn-shanghai.aliyuncs.com",
|
||||
},
|
||||
HTTPClient: request.NewClient(),
|
||||
HTTPClient: request.HTTPClient{},
|
||||
}
|
||||
cache.Set("setting_preview_timeout", "3600", 0)
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ func (handler Driver) List(ctx context.Context, path string, recursive bool) ([]
|
||||
handler.getAPIUrl("list"),
|
||||
bodyReader,
|
||||
request.WithCredential(handler.AuthInstance, int64(signTTL)),
|
||||
request.WithMasterMeta(),
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return res, err
|
||||
@@ -98,7 +97,7 @@ func (handler Driver) getAPIUrl(scope string, routes ...string) string {
|
||||
|
||||
// Get 获取文件内容
|
||||
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
|
||||
// 尝试获取速度限制
|
||||
// 尝试获取速度限制 TODO 是否需要在这里限制?
|
||||
speedLimit := 0
|
||||
if user, ok := ctx.Value(fsctx.UserCtx).(model.User); ok {
|
||||
speedLimit = user.Group.SpeedLimit
|
||||
@@ -117,7 +116,6 @@ func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser,
|
||||
nil,
|
||||
request.WithContext(ctx),
|
||||
request.WithTimeout(time.Duration(0)),
|
||||
request.WithMasterMeta(),
|
||||
).CheckHTTPResponse(200).GetRSCloser()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -170,15 +168,13 @@ func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, s
|
||||
handler.Policy.GetUploadURL(),
|
||||
file,
|
||||
request.WithHeader(map[string][]string{
|
||||
"X-Policy": {credential.Policy},
|
||||
"X-FileName": {fileName},
|
||||
"X-Overwrite": {overwrite},
|
||||
"Authorization": {credential.Token},
|
||||
"X-Policy": {credential.Policy},
|
||||
"X-FileName": {fileName},
|
||||
"X-Overwrite": {overwrite},
|
||||
}),
|
||||
request.WithContentLength(int64(size)),
|
||||
request.WithTimeout(time.Duration(0)),
|
||||
request.WithMasterMeta(),
|
||||
request.WithSlaveMeta(handler.Policy.AccessKey),
|
||||
request.WithCredential(handler.AuthInstance, int64(credentialTTL)),
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -210,8 +206,6 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err
|
||||
handler.getAPIUrl("delete"),
|
||||
bodyReader,
|
||||
request.WithCredential(handler.AuthInstance, int64(signTTL)),
|
||||
request.WithMasterMeta(),
|
||||
request.WithSlaveMeta(handler.Policy.AccessKey),
|
||||
).CheckHTTPResponse(200).GetResponse()
|
||||
if err != nil {
|
||||
return files, err
|
||||
|
||||
@@ -172,7 +172,7 @@ func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser,
|
||||
}
|
||||
|
||||
// 获取文件数据流
|
||||
client := request.NewClient()
|
||||
client := request.HTTPClient{}
|
||||
resp, err := client.Request(
|
||||
"GET",
|
||||
downloadURL,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package masterinslave
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotImplemented = errors.New("this method of shadowed policy is not implemented")
|
||||
)
|
||||
@@ -1,56 +0,0 @@
|
||||
package masterinslave
|
||||
|
||||
import (
|
||||
"context"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"io"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Driver 影子存储策略,用于在从机端上传文件
|
||||
type Driver struct {
|
||||
master cluster.Node
|
||||
handler driver.Handler
|
||||
policy *model.Policy
|
||||
}
|
||||
|
||||
// NewDriver 返回新的处理器
|
||||
func NewDriver(master cluster.Node, handler driver.Handler, policy *model.Policy) driver.Handler {
|
||||
return &Driver{
|
||||
master: master,
|
||||
handler: handler,
|
||||
policy: policy,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error {
|
||||
return d.handler.Put(ctx, file, dst, size)
|
||||
}
|
||||
|
||||
func (d *Driver) Delete(ctx context.Context, files []string) ([]string, error) {
|
||||
return d.handler.Delete(ctx, files)
|
||||
}
|
||||
|
||||
func (d *Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (d *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (d *Driver) Source(ctx context.Context, path string, url url.URL, ttl int64, isDownload bool, speed int) (string, error) {
|
||||
return "", ErrNotImplemented
|
||||
}
|
||||
|
||||
func (d *Driver) Token(ctx context.Context, ttl int64, callbackKey string) (serializer.UploadCredential, error) {
|
||||
return serializer.UploadCredential{}, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (d *Driver) List(ctx context.Context, path string, recursive bool) ([]response.Object, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package slaveinmaster
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotImplemented = errors.New("this method of shadowed policy is not implemented")
|
||||
ErrSlaveSrcPathNotExist = errors.New("cannot determine source file path in slave node")
|
||||
ErrWaitResultTimeout = errors.New("timeout waiting for slave transfer result")
|
||||
)
|
||||
@@ -1,121 +0,0 @@
|
||||
package slaveinmaster
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mq"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Driver 影子存储策略,将上传任务指派给从机节点处理,并等待从机通知上传结果
|
||||
type Driver struct {
|
||||
node cluster.Node
|
||||
handler driver.Handler
|
||||
policy *model.Policy
|
||||
client request.Client
|
||||
}
|
||||
|
||||
// NewDriver 返回新的从机指派处理器
|
||||
func NewDriver(node cluster.Node, handler driver.Handler, policy *model.Policy) driver.Handler {
|
||||
var endpoint *url.URL
|
||||
if serverURL, err := url.Parse(node.DBModel().Server); err == nil {
|
||||
var controller *url.URL
|
||||
controller, _ = url.Parse("/api/v3/slave")
|
||||
endpoint = serverURL.ResolveReference(controller)
|
||||
}
|
||||
|
||||
signTTL := model.GetIntSetting("slave_api_timeout", 60)
|
||||
return &Driver{
|
||||
node: node,
|
||||
handler: handler,
|
||||
policy: policy,
|
||||
client: request.NewClient(
|
||||
request.WithMasterMeta(),
|
||||
request.WithTimeout(time.Duration(signTTL)*time.Second),
|
||||
request.WithCredential(node.SlaveAuthInstance(), int64(signTTL)),
|
||||
request.WithEndpoint(endpoint.String()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Put 将ctx中指定的从机物理文件由从机上传到目标存储策略
|
||||
func (d *Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error {
|
||||
src, ok := ctx.Value(fsctx.SlaveSrcPath).(string)
|
||||
if !ok {
|
||||
return ErrSlaveSrcPathNotExist
|
||||
}
|
||||
|
||||
req := serializer.SlaveTransferReq{
|
||||
Src: src,
|
||||
Dst: dst,
|
||||
Policy: d.policy,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 订阅转存结果
|
||||
resChan := mq.GlobalMQ.Subscribe(req.Hash(model.GetSettingByName("siteID")), 0)
|
||||
defer mq.GlobalMQ.Unsubscribe(req.Hash(model.GetSettingByName("siteID")), resChan)
|
||||
|
||||
res, err := d.client.Request("PUT", "task/transfer", bytes.NewReader(body)).
|
||||
CheckHTTPResponse(200).
|
||||
DecodeResponse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return serializer.NewErrorFromResponse(res)
|
||||
}
|
||||
|
||||
// 等待转存结果或者超时
|
||||
waitTimeout := model.GetIntSetting("slave_transfer_timeout", 172800)
|
||||
select {
|
||||
case <-time.After(time.Duration(waitTimeout) * time.Second):
|
||||
return ErrWaitResultTimeout
|
||||
case msg := <-resChan:
|
||||
if msg.Event != serializer.SlaveTransferSuccess {
|
||||
return errors.New(msg.Content.(serializer.SlaveTransferResult).Error)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) Delete(ctx context.Context, files []string) ([]string, error) {
|
||||
return d.handler.Delete(ctx, files)
|
||||
}
|
||||
|
||||
func (d *Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (d *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (d *Driver) Source(ctx context.Context, path string, url url.URL, ttl int64, isDownload bool, speed int) (string, error) {
|
||||
return "", ErrNotImplemented
|
||||
}
|
||||
|
||||
func (d *Driver) Token(ctx context.Context, ttl int64, callbackKey string) (serializer.UploadCredential, error) {
|
||||
return serializer.UploadCredential{}, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (d *Driver) List(ctx context.Context, path string, recursive bool) ([]response.Object, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
@@ -105,6 +105,9 @@ func (handler Driver) List(ctx context.Context, base string, recursive bool) ([]
|
||||
|
||||
// Get 获取文件
|
||||
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
|
||||
// 给文件名加上随机参数以强制拉取
|
||||
path = fmt.Sprintf("%s?v=%d", path, time.Now().UnixNano())
|
||||
|
||||
// 获取文件源地址
|
||||
downloadURL, err := handler.Source(
|
||||
ctx,
|
||||
|
||||
@@ -126,7 +126,7 @@ func (fs *FileSystem) Preview(ctx context.Context, id uint, isText bool) (*respo
|
||||
|
||||
// 否则重定向到签名的预览URL
|
||||
ttl := model.GetIntSetting("preview_timeout", 60)
|
||||
previewURL, err := fs.SignURL(ctx, &fs.FileTarget[0], int64(ttl), false)
|
||||
previewURL, err := fs.signURL(ctx, &fs.FileTarget[0], int64(ttl), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -234,7 +234,7 @@ func (fs *FileSystem) GetDownloadURL(ctx context.Context, id uint, timeout strin
|
||||
|
||||
// 生成下載地址
|
||||
ttl := model.GetIntSetting(timeout, 60)
|
||||
source, err := fs.SignURL(
|
||||
source, err := fs.signURL(
|
||||
ctx,
|
||||
fileTarget,
|
||||
int64(ttl),
|
||||
@@ -264,7 +264,7 @@ func (fs *FileSystem) GetSource(ctx context.Context, fileID uint) (string, error
|
||||
)
|
||||
}
|
||||
|
||||
source, err := fs.SignURL(ctx, &fs.FileTarget[0], 0, false)
|
||||
source, err := fs.signURL(ctx, &fs.FileTarget[0], 0, false)
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeNotSet, "无法获取外链", err)
|
||||
}
|
||||
@@ -272,8 +272,7 @@ func (fs *FileSystem) GetSource(ctx context.Context, fileID uint) (string, error
|
||||
return source, nil
|
||||
}
|
||||
|
||||
// SignURL 签名文件原始 URL
|
||||
func (fs *FileSystem) SignURL(ctx context.Context, file *model.File, ttl int64, isDownload bool) (string, error) {
|
||||
func (fs *FileSystem) signURL(ctx context.Context, file *model.File, ttl int64, isDownload bool) (string, error) {
|
||||
fs.FileTarget = []model.File{*file}
|
||||
ctx = context.WithValue(ctx, fsctx.FileModelCtx, *file)
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/shadow/masterinslave"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/shadow/slaveinmaster"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -23,6 +19,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/remote"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/s3"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/upyun"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -46,6 +43,36 @@ type FileHeader interface {
|
||||
GetVirtualPath() string
|
||||
}
|
||||
|
||||
// Handler 存储策略适配器
|
||||
type Handler interface {
|
||||
// 上传文件, dst为文件存储路径,size 为文件大小。上下文关闭
|
||||
// 时,应取消上传并清理临时文件
|
||||
Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error
|
||||
|
||||
// 删除一个或多个给定路径的文件,返回删除失败的文件路径列表及错误
|
||||
Delete(ctx context.Context, files []string) ([]string, error)
|
||||
|
||||
// 获取文件内容
|
||||
Get(ctx context.Context, path string) (response.RSCloser, error)
|
||||
|
||||
// 获取缩略图,可直接在ContentResponse中返回文件数据流,也可指
|
||||
// 定为重定向
|
||||
Thumb(ctx context.Context, path string) (*response.ContentResponse, error)
|
||||
|
||||
// 获取外链/下载地址,
|
||||
// url - 站点本身地址,
|
||||
// isDownload - 是否直接下载
|
||||
Source(ctx context.Context, path string, url url.URL, ttl int64, isDownload bool, speed int) (string, error)
|
||||
|
||||
// Token 获取有效期为ttl的上传凭证和签名,同时回调会话有效期为sessionTTL
|
||||
Token(ctx context.Context, ttl int64, callbackKey string) (serializer.UploadCredential, error)
|
||||
|
||||
// List 递归列取远程端path路径下文件、目录,不包含path本身,
|
||||
// 返回的对象路径以path作为起始根目录.
|
||||
// recursive - 是否递归列出
|
||||
List(ctx context.Context, path string, recursive bool) ([]response.Object, error)
|
||||
}
|
||||
|
||||
// FileSystem 管理文件的文件系统
|
||||
type FileSystem struct {
|
||||
// 文件系统所有者
|
||||
@@ -69,7 +96,7 @@ type FileSystem struct {
|
||||
/*
|
||||
文件系统处理适配器
|
||||
*/
|
||||
Handler driver.Handler
|
||||
Handler Handler
|
||||
|
||||
// 回收锁
|
||||
recycleLock sync.Mutex
|
||||
@@ -107,6 +134,7 @@ func NewFileSystem(user *model.User) (*FileSystem, error) {
|
||||
// 分配存储策略适配器
|
||||
err := fs.DispatchHandler()
|
||||
|
||||
// TODO 分配默认钩子
|
||||
return fs, err
|
||||
}
|
||||
|
||||
@@ -131,6 +159,7 @@ func NewAnonymousFileSystem() (*FileSystem, error) {
|
||||
}
|
||||
|
||||
// DispatchHandler 根据存储策略分配文件适配器
|
||||
// TODO 完善测试
|
||||
func (fs *FileSystem) DispatchHandler() error {
|
||||
var policyType string
|
||||
var currentPolicy *model.Policy
|
||||
@@ -155,7 +184,7 @@ func (fs *FileSystem) DispatchHandler() error {
|
||||
case "remote":
|
||||
fs.Handler = remote.Driver{
|
||||
Policy: currentPolicy,
|
||||
Client: request.NewClient(),
|
||||
Client: request.HTTPClient{},
|
||||
AuthInstance: auth.HMACAuth{[]byte(currentPolicy.SecretKey)},
|
||||
}
|
||||
return nil
|
||||
@@ -167,7 +196,7 @@ func (fs *FileSystem) DispatchHandler() error {
|
||||
case "oss":
|
||||
fs.Handler = oss.Driver{
|
||||
Policy: currentPolicy,
|
||||
HTTPClient: request.NewClient(),
|
||||
HTTPClient: request.HTTPClient{},
|
||||
}
|
||||
return nil
|
||||
case "upyun":
|
||||
@@ -176,9 +205,13 @@ func (fs *FileSystem) DispatchHandler() error {
|
||||
}
|
||||
return nil
|
||||
case "onedrive":
|
||||
var odErr error
|
||||
fs.Handler, odErr = onedrive.NewDriver(currentPolicy)
|
||||
return odErr
|
||||
client, err := onedrive.NewClient(currentPolicy)
|
||||
fs.Handler = onedrive.Driver{
|
||||
Policy: currentPolicy,
|
||||
Client: client,
|
||||
HTTPClient: request.HTTPClient{},
|
||||
}
|
||||
return err
|
||||
case "cos":
|
||||
u, _ := url.Parse(currentPolicy.Server)
|
||||
b := &cossdk.BaseURL{BucketURL: u}
|
||||
@@ -190,7 +223,7 @@ func (fs *FileSystem) DispatchHandler() error {
|
||||
SecretKey: currentPolicy.SecretKey,
|
||||
},
|
||||
}),
|
||||
HTTPClient: request.NewClient(),
|
||||
HTTPClient: request.HTTPClient{},
|
||||
}
|
||||
return nil
|
||||
case "s3":
|
||||
@@ -239,30 +272,6 @@ func NewFileSystemFromCallback(c *gin.Context) (*FileSystem, error) {
|
||||
return fs, err
|
||||
}
|
||||
|
||||
// SwitchToSlaveHandler 将负责上传的 Handler 切换为从机节点
|
||||
func (fs *FileSystem) SwitchToSlaveHandler(node cluster.Node) {
|
||||
fs.Handler = slaveinmaster.NewDriver(node, fs.Handler, &fs.User.Policy)
|
||||
}
|
||||
|
||||
// SwitchToShadowHandler 将负责上传的 Handler 切换为从机节点转存使用的影子处理器
|
||||
func (fs *FileSystem) SwitchToShadowHandler(master cluster.Node, masterURL, masterID string) {
|
||||
switch fs.Policy.Type {
|
||||
case "remote":
|
||||
fs.Policy.Type = "local"
|
||||
fs.DispatchHandler()
|
||||
case "local":
|
||||
fs.Policy.Type = "remote"
|
||||
fs.Policy.Server = masterURL
|
||||
fs.Policy.AccessKey = fmt.Sprintf("%d", master.ID())
|
||||
fs.Policy.SecretKey = master.DBModel().MasterKey
|
||||
fs.DispatchHandler()
|
||||
case "onedrive":
|
||||
fs.Policy.MasterID = masterID
|
||||
}
|
||||
|
||||
fs.Handler = masterinslave.NewDriver(master, fs.Handler, fs.Policy)
|
||||
}
|
||||
|
||||
// SetTargetFile 设置当前处理的目标文件
|
||||
func (fs *FileSystem) SetTargetFile(files *[]model.File) {
|
||||
if len(fs.FileTarget) == 0 {
|
||||
|
||||
@@ -41,6 +41,4 @@ const (
|
||||
ValidateCapacityOnceCtx
|
||||
// 禁止上传时同名覆盖操作
|
||||
DisableOverwrite
|
||||
// 文件在从机节点中的路径
|
||||
SlaveSrcPath
|
||||
)
|
||||
|
||||
@@ -321,6 +321,7 @@ func GenericAfterUpload(ctx context.Context, fs *FileSystem) error {
|
||||
fs.recycleLock.Lock()
|
||||
go func() {
|
||||
defer fs.recycleLock.Unlock()
|
||||
file.Name = strings.TrimRight(file.Name, ".tacitpart")
|
||||
fs.GenerateThumbnail(ctx, file)
|
||||
}()
|
||||
}
|
||||
|
||||
+38
-6
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/thumb"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
)
|
||||
|
||||
/* ================
|
||||
@@ -39,8 +41,8 @@ func (fs *FileSystem) GetThumb(ctx context.Context, id uint) (*response.ContentR
|
||||
res.MaxAge = model.GetIntSetting("preview_timeout", 60)
|
||||
}
|
||||
|
||||
// 本地存储策略出错时重新生成缩略图
|
||||
if err != nil && fs.Policy.Type == "local" {
|
||||
// 出错时重新生成缩略图
|
||||
if err != nil {
|
||||
fs.GenerateThumbnail(ctx, &fs.FileTarget[0])
|
||||
}
|
||||
|
||||
@@ -54,7 +56,6 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) {
|
||||
if !IsInExtensionList(HandledExtension, file.Name) {
|
||||
return
|
||||
}
|
||||
|
||||
// 新建上下文
|
||||
newCtx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -65,7 +66,6 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) {
|
||||
return
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
image, err := thumb.NewThumbFromFile(source, file.Name)
|
||||
if err != nil {
|
||||
util.Log().Warning("生成缩略图时无法解析 [%s] 图像数据:%s", file.SourceName, err)
|
||||
@@ -74,7 +74,6 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) {
|
||||
|
||||
// 获取原始图像尺寸
|
||||
w, h := image.GetSize()
|
||||
|
||||
// 生成缩略图
|
||||
image.GetThumb(fs.GenerateThumbnailSize(w, h))
|
||||
// 保存到文件
|
||||
@@ -83,7 +82,6 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) {
|
||||
util.Log().Warning("无法保存缩略图:%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新文件的图像信息
|
||||
if file.Model.ID > 0 {
|
||||
err = file.UpdatePicInfo(fmt.Sprintf("%d,%d", w, h))
|
||||
@@ -91,6 +89,40 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) {
|
||||
file.PicInfo = fmt.Sprintf("%d,%d", w, h)
|
||||
}
|
||||
|
||||
// 更新文件的图像信息
|
||||
// 记录文件句柄位置并还原, 获取 exif 信息
|
||||
currentPosition, err := source.Seek(0, 1)
|
||||
source.Seek(0,0)
|
||||
x, err := exif.Decode(source)
|
||||
source.Seek(currentPosition, 0)
|
||||
if err != nil {
|
||||
util.Log().Warning("照片解析EXIF失败:%s", err)
|
||||
}else{
|
||||
ExifCamModel, _ := x.Get(exif.Model)
|
||||
file.ExifModel,_ = ExifCamModel.StringVal()
|
||||
|
||||
ExifDateTime, _ := x.DateTime()
|
||||
if !ExifDateTime.IsZero() {
|
||||
file.ExifDateTime = ExifDateTime
|
||||
}
|
||||
|
||||
lat, long, _ := x.LatLong()
|
||||
if lat > 0 && long > 0 {
|
||||
file.ExifLatLong = fmt.Sprintf("%f,%f", lat, long)
|
||||
}
|
||||
util.Log().Debug("照片的经纬度:%f,%f", lat,long)
|
||||
if file.Model.ID > 0 {
|
||||
file.UpdatePicExifModel(file.ExifModel)
|
||||
if !ExifDateTime.IsZero() {
|
||||
file.UpdatePicExifDateTime(file.ExifDateTime)
|
||||
}
|
||||
if lat > 0 && long > 0 {
|
||||
file.UpdatePicExifLatLong(file.ExifLatLong)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 失败时删除缩略图文件
|
||||
if err != nil {
|
||||
_, _ = fs.Handler.Delete(newCtx, []string{file.SourceName + conf.ThumbConfig.FileSuffix})
|
||||
|
||||
+26
-12
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
@@ -21,14 +20,18 @@ import (
|
||||
|
||||
// Object 文件或者目录
|
||||
type Object struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Pic string `json:"pic"`
|
||||
Size uint64 `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Date time.Time `json:"date"`
|
||||
Key string `json:"key,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Pic string `json:"pic"`
|
||||
Size uint64 `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Date string `json:"date"`
|
||||
Key string `json:"key,omitempty"`
|
||||
ExifModel string `json:"exif_model"`
|
||||
ExifDateTime string `json:"exif_date_time"`
|
||||
ExifLatLong string `json:"exif_lat_long"`
|
||||
ExifAddress string `json:"exit_address"`
|
||||
}
|
||||
|
||||
// Rename 重命名对象
|
||||
@@ -350,7 +353,11 @@ func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []mo
|
||||
Pic: "",
|
||||
Size: 0,
|
||||
Type: "dir",
|
||||
Date: subFolder.CreatedAt,
|
||||
Date: subFolder.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
ExifModel: "",
|
||||
ExifDateTime: "",
|
||||
ExifLatLong: "",
|
||||
ExifAddress: "",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -362,7 +369,10 @@ func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []mo
|
||||
processedPath = parent
|
||||
}
|
||||
}
|
||||
|
||||
ExifDateTimeText := ""
|
||||
if !file.ExifDateTime.IsZero(){
|
||||
ExifDateTimeText = file.ExifDateTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
newFile := Object{
|
||||
ID: hashid.HashID(file.ID, hashid.FileID),
|
||||
Name: file.Name,
|
||||
@@ -370,7 +380,11 @@ func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []mo
|
||||
Pic: file.PicInfo,
|
||||
Size: file.Size,
|
||||
Type: "file",
|
||||
Date: file.CreatedAt,
|
||||
Date: file.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
ExifModel: file.ExifModel,
|
||||
ExifDateTime: ExifDateTimeText,
|
||||
ExifLatLong: file.ExifLatLong,
|
||||
ExifAddress: file.ExifAddress,
|
||||
}
|
||||
if shareKey != "" {
|
||||
newFile.Key = shareKey
|
||||
|
||||
@@ -228,14 +228,12 @@ func (fs *FileSystem) UploadFromStream(ctx context.Context, src io.ReadCloser, d
|
||||
}
|
||||
|
||||
// UploadFromPath 将本机已有文件上传到用户的文件系统
|
||||
func (fs *FileSystem) UploadFromPath(ctx context.Context, src, dst string, resetPolicy bool) error {
|
||||
func (fs *FileSystem) UploadFromPath(ctx context.Context, src, dst string) error {
|
||||
// 重设存储策略
|
||||
if resetPolicy {
|
||||
fs.Policy = &fs.User.Policy
|
||||
err := fs.DispatchHandler()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fs.Policy = &fs.User.Policy
|
||||
err := fs.DispatchHandler()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Open(util.RelativePath(src))
|
||||
|
||||
@@ -226,13 +226,13 @@ func TestFileSystem_UploadFromPath(t *testing.T) {
|
||||
|
||||
// 文件不存在
|
||||
{
|
||||
err := fs.UploadFromPath(ctx, "test/not_exist", "/", true)
|
||||
err := fs.UploadFromPath(ctx, "test/not_exist", "/")
|
||||
asserts.Error(err)
|
||||
}
|
||||
|
||||
// 文存在,上传失败
|
||||
{
|
||||
err := fs.UploadFromPath(ctx, "tests/test.zip", "/", true)
|
||||
err := fs.UploadFromPath(ctx, "tests/test.zip", "/")
|
||||
asserts.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/common"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Message 消息事件正文
|
||||
type Message struct {
|
||||
// 消息触发者
|
||||
TriggeredBy string
|
||||
|
||||
// 事件标识
|
||||
Event string
|
||||
|
||||
// 消息正文
|
||||
Content interface{}
|
||||
}
|
||||
|
||||
type CallbackFunc func(Message)
|
||||
|
||||
// MQ 消息队列
|
||||
type MQ interface {
|
||||
rpc.Notifier
|
||||
|
||||
// 发布一个消息
|
||||
Publish(string, Message)
|
||||
|
||||
// 订阅一个消息主题
|
||||
Subscribe(string, int) <-chan Message
|
||||
|
||||
// 订阅一个消息主题,注册触发回调函数
|
||||
SubscribeCallback(string, CallbackFunc)
|
||||
|
||||
// 取消订阅一个消息主题
|
||||
Unsubscribe(string, <-chan Message)
|
||||
}
|
||||
|
||||
var GlobalMQ = NewMQ()
|
||||
|
||||
func NewMQ() MQ {
|
||||
return &inMemoryMQ{
|
||||
topics: make(map[string][]chan Message),
|
||||
callbacks: make(map[string][]CallbackFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(Message{})
|
||||
gob.Register([]rpc.Event{})
|
||||
}
|
||||
|
||||
type inMemoryMQ struct {
|
||||
topics map[string][]chan Message
|
||||
callbacks map[string][]CallbackFunc
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (i *inMemoryMQ) Publish(topic string, message Message) {
|
||||
i.RLock()
|
||||
subscribersChan, okChan := i.topics[topic]
|
||||
subscribersCallback, okCallback := i.callbacks[topic]
|
||||
i.RUnlock()
|
||||
|
||||
if okChan {
|
||||
go func(subscribersChan []chan Message) {
|
||||
for i := 0; i < len(subscribersChan); i++ {
|
||||
select {
|
||||
case subscribersChan[i] <- message:
|
||||
case <-time.After(time.Millisecond * 500):
|
||||
}
|
||||
}
|
||||
}(subscribersChan)
|
||||
|
||||
}
|
||||
|
||||
if okCallback {
|
||||
for i := 0; i < len(subscribersCallback); i++ {
|
||||
go subscribersCallback[i](message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *inMemoryMQ) Subscribe(topic string, buffer int) <-chan Message {
|
||||
ch := make(chan Message, buffer)
|
||||
i.Lock()
|
||||
i.topics[topic] = append(i.topics[topic], ch)
|
||||
i.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (i *inMemoryMQ) SubscribeCallback(topic string, callbackFunc CallbackFunc) {
|
||||
i.Lock()
|
||||
i.callbacks[topic] = append(i.callbacks[topic], callbackFunc)
|
||||
i.Unlock()
|
||||
}
|
||||
|
||||
func (i *inMemoryMQ) Unsubscribe(topic string, sub <-chan Message) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
subscribers, ok := i.topics[topic]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var newSubs []chan Message
|
||||
for _, subscriber := range subscribers {
|
||||
if subscriber == sub {
|
||||
continue
|
||||
}
|
||||
newSubs = append(newSubs, subscriber)
|
||||
}
|
||||
|
||||
i.topics[topic] = newSubs
|
||||
}
|
||||
|
||||
func (i *inMemoryMQ) Aria2Notify(events []rpc.Event, status int) {
|
||||
for _, event := range events {
|
||||
i.Publish(event.Gid, Message{
|
||||
TriggeredBy: event.Gid,
|
||||
Event: strconv.FormatInt(int64(status), 10),
|
||||
Content: events,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// OnDownloadStart 下载开始
|
||||
func (i *inMemoryMQ) OnDownloadStart(events []rpc.Event) {
|
||||
i.Aria2Notify(events, common.Downloading)
|
||||
}
|
||||
|
||||
// OnDownloadPause 下载暂停
|
||||
func (i *inMemoryMQ) OnDownloadPause(events []rpc.Event) {
|
||||
i.Aria2Notify(events, common.Paused)
|
||||
}
|
||||
|
||||
// OnDownloadStop 下载停止
|
||||
func (i *inMemoryMQ) OnDownloadStop(events []rpc.Event) {
|
||||
i.Aria2Notify(events, common.Canceled)
|
||||
}
|
||||
|
||||
// OnDownloadComplete 下载完成
|
||||
func (i *inMemoryMQ) OnDownloadComplete(events []rpc.Event) {
|
||||
i.Aria2Notify(events, common.Complete)
|
||||
}
|
||||
|
||||
// OnDownloadError 下载出错
|
||||
func (i *inMemoryMQ) OnDownloadError(events []rpc.Event) {
|
||||
i.Aria2Notify(events, common.Error)
|
||||
}
|
||||
|
||||
// OnBtDownloadComplete BT下载完成
|
||||
func (i *inMemoryMQ) OnBtDownloadComplete(events []rpc.Event) {
|
||||
i.Aria2Notify(events, common.Complete)
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPublishAndSubscribe(t *testing.T) {
|
||||
t.Parallel()
|
||||
asserts := assert.New(t)
|
||||
mq := NewMQ()
|
||||
|
||||
// No subscriber
|
||||
{
|
||||
asserts.NotPanics(func() {
|
||||
mq.Publish("No subscriber", Message{})
|
||||
})
|
||||
}
|
||||
|
||||
// One channel subscriber
|
||||
{
|
||||
topic := "One channel subscriber"
|
||||
msg := Message{TriggeredBy: "Tester"}
|
||||
notifier := mq.Subscribe(topic, 0)
|
||||
mq.Publish(topic, msg)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
wg.Done()
|
||||
msgRecv := <-notifier
|
||||
asserts.Equal(msg, msgRecv)
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// two channel subscriber
|
||||
{
|
||||
topic := "two channel subscriber"
|
||||
msg := Message{TriggeredBy: "Tester"}
|
||||
notifier := mq.Subscribe(topic, 0)
|
||||
notifier2 := mq.Subscribe(topic, 0)
|
||||
mq.Publish(topic, msg)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
wg.Done()
|
||||
msgRecv := <-notifier
|
||||
asserts.Equal(msg, msgRecv)
|
||||
}()
|
||||
go func() {
|
||||
wg.Done()
|
||||
msgRecv := <-notifier2
|
||||
asserts.Equal(msg, msgRecv)
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// two channel subscriber, one timeout
|
||||
{
|
||||
topic := "two channel subscriber, one timeout"
|
||||
msg := Message{TriggeredBy: "Tester"}
|
||||
mq.Subscribe(topic, 0)
|
||||
notifier2 := mq.Subscribe(topic, 0)
|
||||
mq.Publish(topic, msg)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
wg.Done()
|
||||
msgRecv := <-notifier2
|
||||
asserts.Equal(msg, msgRecv)
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// two channel subscriber, one unsubscribe
|
||||
{
|
||||
topic := "two channel subscriber, one unsubscribe"
|
||||
msg := Message{TriggeredBy: "Tester"}
|
||||
mq.Subscribe(topic, 0)
|
||||
notifier2 := mq.Subscribe(topic, 0)
|
||||
notifier := mq.Subscribe(topic, 0)
|
||||
mq.Unsubscribe(topic, notifier)
|
||||
mq.Publish(topic, msg)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
wg.Done()
|
||||
msgRecv := <-notifier2
|
||||
asserts.Equal(msg, msgRecv)
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case <-notifier:
|
||||
t.Error()
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAria2Interface(t *testing.T) {
|
||||
t.Parallel()
|
||||
asserts := assert.New(t)
|
||||
mq := NewMQ()
|
||||
var (
|
||||
OnDownloadStart int
|
||||
OnDownloadPause int
|
||||
OnDownloadStop int
|
||||
OnDownloadComplete int
|
||||
OnDownloadError int
|
||||
)
|
||||
l := sync.Mutex{}
|
||||
|
||||
mq.SubscribeCallback("TestAria2Interface", func(message Message) {
|
||||
asserts.Equal("TestAria2Interface", message.TriggeredBy)
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
switch message.Event {
|
||||
case "1":
|
||||
OnDownloadStart++
|
||||
case "2":
|
||||
OnDownloadPause++
|
||||
case "5":
|
||||
OnDownloadStop++
|
||||
case "4":
|
||||
OnDownloadComplete++
|
||||
case "3":
|
||||
OnDownloadError++
|
||||
}
|
||||
})
|
||||
|
||||
mq.OnDownloadStart([]rpc.Event{{"TestAria2Interface"}, {"TestAria2Interface"}})
|
||||
mq.OnDownloadPause([]rpc.Event{{"TestAria2Interface"}, {"TestAria2Interface"}})
|
||||
mq.OnDownloadStop([]rpc.Event{{"TestAria2Interface"}, {"TestAria2Interface"}})
|
||||
mq.OnDownloadComplete([]rpc.Event{{"TestAria2Interface"}, {"TestAria2Interface"}})
|
||||
mq.OnDownloadError([]rpc.Event{{"TestAria2Interface"}, {"TestAria2Interface"}})
|
||||
mq.OnBtDownloadComplete([]rpc.Event{{"TestAria2Interface"}, {"TestAria2Interface"}})
|
||||
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
|
||||
asserts.Equal(2, OnDownloadStart)
|
||||
asserts.Equal(2, OnDownloadPause)
|
||||
asserts.Equal(2, OnDownloadStop)
|
||||
asserts.Equal(4, OnDownloadComplete)
|
||||
asserts.Equal(2, OnDownloadError)
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Option 发送请求的额外设置
|
||||
type Option interface {
|
||||
apply(*options)
|
||||
}
|
||||
|
||||
type options struct {
|
||||
timeout time.Duration
|
||||
header http.Header
|
||||
sign auth.Auth
|
||||
signTTL int64
|
||||
ctx context.Context
|
||||
contentLength int64
|
||||
masterMeta bool
|
||||
endpoint *url.URL
|
||||
slaveNodeID string
|
||||
}
|
||||
|
||||
type optionFunc func(*options)
|
||||
|
||||
func (f optionFunc) apply(o *options) {
|
||||
f(o)
|
||||
}
|
||||
|
||||
func newDefaultOption() *options {
|
||||
return &options{
|
||||
header: http.Header{},
|
||||
timeout: time.Duration(30) * time.Second,
|
||||
contentLength: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// WithTimeout 设置请求超时
|
||||
func WithTimeout(t time.Duration) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
o.timeout = t
|
||||
})
|
||||
}
|
||||
|
||||
// WithContext 设置请求上下文
|
||||
func WithContext(c context.Context) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
o.ctx = c
|
||||
})
|
||||
}
|
||||
|
||||
// WithCredential 对请求进行签名
|
||||
func WithCredential(instance auth.Auth, ttl int64) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
o.sign = instance
|
||||
o.signTTL = ttl
|
||||
})
|
||||
}
|
||||
|
||||
// WithHeader 设置请求Header
|
||||
func WithHeader(header http.Header) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
for k, v := range header {
|
||||
o.header[k] = v
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithoutHeader 设置清除请求Header
|
||||
func WithoutHeader(header []string) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
for _, v := range header {
|
||||
delete(o.header, v)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// WithContentLength 设置请求大小
|
||||
func WithContentLength(s int64) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
o.contentLength = s
|
||||
})
|
||||
}
|
||||
|
||||
// WithMasterMeta 请求时携带主机信息
|
||||
func WithMasterMeta() Option {
|
||||
return optionFunc(func(o *options) {
|
||||
o.masterMeta = true
|
||||
})
|
||||
}
|
||||
|
||||
// WithSlaveMeta 请求时携带从机信息
|
||||
func WithSlaveMeta(s string) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
o.slaveNodeID = s
|
||||
})
|
||||
}
|
||||
|
||||
// Endpoint 使用同一的请求Endpoint
|
||||
func WithEndpoint(endpoint string) Option {
|
||||
endpointURL, _ := url.Parse(endpoint)
|
||||
return optionFunc(func(o *options) {
|
||||
o.endpoint = endpointURL
|
||||
})
|
||||
}
|
||||
+79
-50
@@ -1,25 +1,23 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
)
|
||||
|
||||
// GeneralClient 通用 HTTP Client
|
||||
var GeneralClient Client = NewClient()
|
||||
var GeneralClient Client = HTTPClient{}
|
||||
|
||||
// Response 请求的响应或错误信息
|
||||
type Response struct {
|
||||
@@ -34,30 +32,90 @@ type Client interface {
|
||||
|
||||
// HTTPClient 实现 Client 接口
|
||||
type HTTPClient struct {
|
||||
mu sync.Mutex
|
||||
options *options
|
||||
}
|
||||
|
||||
func NewClient(opts ...Option) Client {
|
||||
client := &HTTPClient{
|
||||
options: newDefaultOption(),
|
||||
}
|
||||
// Option 发送请求的额外设置
|
||||
type Option interface {
|
||||
apply(*options)
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o.apply(client.options)
|
||||
}
|
||||
type options struct {
|
||||
timeout time.Duration
|
||||
header http.Header
|
||||
sign auth.Auth
|
||||
signTTL int64
|
||||
ctx context.Context
|
||||
contentLength int64
|
||||
}
|
||||
|
||||
return client
|
||||
type optionFunc func(*options)
|
||||
|
||||
func (f optionFunc) apply(o *options) {
|
||||
f(o)
|
||||
}
|
||||
|
||||
func newDefaultOption() *options {
|
||||
return &options{
|
||||
header: http.Header{},
|
||||
timeout: time.Duration(30) * time.Second,
|
||||
contentLength: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// WithTimeout 设置请求超时
|
||||
func WithTimeout(t time.Duration) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
o.timeout = t
|
||||
})
|
||||
}
|
||||
|
||||
// WithContext 设置请求上下文
|
||||
func WithContext(c context.Context) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
o.ctx = c
|
||||
})
|
||||
}
|
||||
|
||||
// WithCredential 对请求进行签名
|
||||
func WithCredential(instance auth.Auth, ttl int64) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
o.sign = instance
|
||||
o.signTTL = ttl
|
||||
})
|
||||
}
|
||||
|
||||
// WithHeader 设置请求Header
|
||||
func WithHeader(header http.Header) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
for k, v := range header {
|
||||
o.header[k] = v
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithoutHeader 设置清除请求Header
|
||||
func WithoutHeader(header []string) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
for _, v := range header {
|
||||
delete(o.header, v)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// WithContentLength 设置请求大小
|
||||
func WithContentLength(s int64) Option {
|
||||
return optionFunc(func(o *options) {
|
||||
o.contentLength = s
|
||||
})
|
||||
}
|
||||
|
||||
// Request 发送HTTP请求
|
||||
func (c HTTPClient) Request(method, target string, body io.Reader, opts ...Option) *Response {
|
||||
// 应用额外设置
|
||||
c.mu.Lock()
|
||||
options := *c.options
|
||||
c.mu.Unlock()
|
||||
options := newDefaultOption()
|
||||
for _, o := range opts {
|
||||
o.apply(&options)
|
||||
o.apply(options)
|
||||
}
|
||||
|
||||
// 创建请求客户端
|
||||
@@ -68,13 +126,6 @@ func (c HTTPClient) Request(method, target string, body io.Reader, opts ...Optio
|
||||
body = nil
|
||||
}
|
||||
|
||||
// 确定请求URL
|
||||
if options.endpoint != nil {
|
||||
targetURL := *options.endpoint
|
||||
targetURL.Path = path.Join(targetURL.Path, target)
|
||||
target = targetURL.String()
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
var (
|
||||
req *http.Request
|
||||
@@ -90,36 +141,14 @@ func (c HTTPClient) Request(method, target string, body io.Reader, opts ...Optio
|
||||
}
|
||||
|
||||
// 添加请求相关设置
|
||||
if options.header != nil {
|
||||
for k, v := range options.header {
|
||||
req.Header.Add(k, strings.Join(v, " "))
|
||||
}
|
||||
}
|
||||
|
||||
if options.masterMeta && conf.SystemConfig.Mode == "master" {
|
||||
req.Header.Add("X-Site-Url", model.GetSiteURL().String())
|
||||
req.Header.Add("X-Site-Id", model.GetSettingByName("siteID"))
|
||||
req.Header.Add("X-Cloudreve-Version", conf.BackendVersion)
|
||||
}
|
||||
|
||||
if options.slaveNodeID != "" && conf.SystemConfig.Mode == "slave" {
|
||||
req.Header.Add("X-Node-Id", options.slaveNodeID)
|
||||
}
|
||||
|
||||
req.Header = options.header
|
||||
if options.contentLength != -1 {
|
||||
req.ContentLength = options.contentLength
|
||||
}
|
||||
|
||||
// 签名请求
|
||||
if options.sign != nil {
|
||||
switch method {
|
||||
case "PUT", "POST", "PATCH":
|
||||
auth.SignRequest(options.sign, req, options.signTTL)
|
||||
default:
|
||||
if resURL, err := auth.SignURI(options.sign, req.URL.String(), options.signTTL); err == nil {
|
||||
req.URL = resURL
|
||||
}
|
||||
}
|
||||
auth.SignRequest(options.sign, req, options.signTTL)
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
)
|
||||
|
||||
// TODO: move to slave pkg
|
||||
// RemoteCallback 发送远程存储策略上传回调请求
|
||||
func RemoteCallback(url string, body serializer.UploadCallback) error {
|
||||
callbackBody, err := json.Marshal(struct {
|
||||
|
||||
@@ -2,7 +2,6 @@ package serializer
|
||||
|
||||
import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
@@ -10,7 +9,7 @@ import (
|
||||
|
||||
// DownloadListResponse 下载列表响应条目
|
||||
type DownloadListResponse struct {
|
||||
UpdateTime time.Time `json:"update"`
|
||||
UpdateTime int64 `json:"update"`
|
||||
UpdateInterval int `json:"interval"`
|
||||
Name string `json:"name"`
|
||||
Status int `json:"status"`
|
||||
@@ -32,8 +31,8 @@ type FinishedListResponse struct {
|
||||
Files []rpc.FileInfo `json:"files"`
|
||||
TaskStatus int `json:"task_status"`
|
||||
TaskError string `json:"task_error"`
|
||||
CreateTime time.Time `json:"create"`
|
||||
UpdateTime time.Time `json:"update"`
|
||||
CreateTime string `json:"create"`
|
||||
UpdateTime string `json:"update"`
|
||||
}
|
||||
|
||||
// BuildFinishedListResponse 构建已完成任务条目
|
||||
@@ -60,8 +59,8 @@ func BuildFinishedListResponse(tasks []model.Download) Response {
|
||||
Total: tasks[i].TotalSize,
|
||||
Files: tasks[i].StatusInfo.Files,
|
||||
TaskStatus: -1,
|
||||
UpdateTime: tasks[i].UpdatedAt,
|
||||
CreateTime: tasks[i].CreatedAt,
|
||||
UpdateTime: tasks[i].UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
CreateTime: tasks[i].CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
if tasks[i].Task != nil {
|
||||
@@ -93,7 +92,7 @@ func BuildDownloadingResponse(tasks []model.Download) Response {
|
||||
}
|
||||
|
||||
resp = append(resp, DownloadListResponse{
|
||||
UpdateTime: tasks[i].UpdatedAt,
|
||||
UpdateTime: tasks[i].UpdatedAt.Unix(),
|
||||
UpdateInterval: interval,
|
||||
Name: fileName,
|
||||
Status: tasks[i].Status,
|
||||
|
||||
@@ -5,15 +5,16 @@ import "encoding/json"
|
||||
// RequestRawSign 待签名的HTTP请求
|
||||
type RequestRawSign struct {
|
||||
Path string
|
||||
Header string
|
||||
Policy string
|
||||
Body string
|
||||
}
|
||||
|
||||
// NewRequestSignString 返回JSON格式的待签名字符串
|
||||
func NewRequestSignString(path, header, body string) string {
|
||||
// TODO 测试
|
||||
func NewRequestSignString(path, policy, body string) string {
|
||||
req := RequestRawSign{
|
||||
Path: path,
|
||||
Header: header,
|
||||
Policy: policy,
|
||||
Body: body,
|
||||
}
|
||||
res, _ := json.Marshal(req)
|
||||
|
||||
+10
-16
@@ -1,9 +1,14 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// Response 基础序列化器
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Msg string `json:"msg"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// AppError 应用错误,实现了error接口
|
||||
type AppError struct {
|
||||
@@ -12,7 +17,7 @@ type AppError struct {
|
||||
RawError error
|
||||
}
|
||||
|
||||
// NewError 返回新的错误对象
|
||||
// NewError 返回新的错误对象 todo:测试 还有下面的
|
||||
func NewError(code int, msg string, err error) AppError {
|
||||
return AppError{
|
||||
Code: code,
|
||||
@@ -21,15 +26,6 @@ func NewError(code int, msg string, err error) AppError {
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorFromResponse 从 serializer.Response 构建错误
|
||||
func NewErrorFromResponse(resp *Response) AppError {
|
||||
return AppError{
|
||||
Code: resp.Code,
|
||||
Msg: resp.Msg,
|
||||
RawError: errors.New(resp.Error),
|
||||
}
|
||||
}
|
||||
|
||||
// WithError 将应用error携带标准库中的error
|
||||
func (err *AppError) WithError(raw error) AppError {
|
||||
err.RawError = raw
|
||||
@@ -70,8 +66,6 @@ const (
|
||||
CodeGroupNotAllowed = 40007
|
||||
// CodeAdminRequired 非管理用户组
|
||||
CodeAdminRequired = 40008
|
||||
// CodeMasterNotFound 主机节点未注册
|
||||
CodeMasterNotFound = 40009
|
||||
// CodeDBError 数据库操作失败
|
||||
CodeDBError = 50001
|
||||
// CodeEncryptError 加密失败
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(ObjectProps{})
|
||||
}
|
||||
|
||||
// ObjectProps 文件、目录对象的详细属性信息
|
||||
type ObjectProps struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Policy string `json:"policy"`
|
||||
Size uint64 `json:"size"`
|
||||
ChildFolderNum int `json:"child_folder_num"`
|
||||
ChildFileNum int `json:"child_file_num"`
|
||||
Path string `json:"path"`
|
||||
|
||||
QueryDate time.Time `json:"query_date"`
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
)
|
||||
|
||||
// Response 基础序列化器
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Msg string `json:"msg"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// NewResponseWithGobData 返回Data字段使用gob编码的Response
|
||||
func NewResponseWithGobData(data interface{}) Response {
|
||||
var w bytes.Buffer
|
||||
encoder := gob.NewEncoder(&w)
|
||||
if err := encoder.Encode(data); err != nil {
|
||||
return Err(CodeInternalSetting, "无法编码返回结果", err)
|
||||
}
|
||||
|
||||
return Response{Data: w.Bytes()}
|
||||
}
|
||||
|
||||
// GobDecode 将 Response 正文解码至目标指针
|
||||
func (r *Response) GobDecode(target interface{}) {
|
||||
src := r.Data.(string)
|
||||
raw := make([]byte, len(src)*len(src)/base64.StdEncoding.DecodedLen(len(src)))
|
||||
base64.StdEncoding.Decode(raw, []byte(src))
|
||||
decoder := gob.NewDecoder(bytes.NewBuffer(raw))
|
||||
decoder.Decode(target)
|
||||
}
|
||||
+35
-42
@@ -1,36 +1,31 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"time"
|
||||
)
|
||||
import model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
|
||||
// SiteConfig 站点全局设置序列
|
||||
type SiteConfig struct {
|
||||
SiteName string `json:"title"`
|
||||
SiteICPId string `json:"siteICPId"`
|
||||
LoginCaptcha bool `json:"loginCaptcha"`
|
||||
RegCaptcha bool `json:"regCaptcha"`
|
||||
ForgetCaptcha bool `json:"forgetCaptcha"`
|
||||
EmailActive bool `json:"emailActive"`
|
||||
Themes string `json:"themes"`
|
||||
DefaultTheme string `json:"defaultTheme"`
|
||||
HomepageViewMethod string `json:"home_view_method"`
|
||||
ShareViewMethod string `json:"share_view_method"`
|
||||
Authn bool `json:"authn"`
|
||||
User User `json:"user"`
|
||||
ReCaptchaKey string `json:"captcha_ReCaptchaKey"`
|
||||
CaptchaType string `json:"captcha_type"`
|
||||
TCaptchaCaptchaAppId string `json:"tcaptcha_captcha_app_id"`
|
||||
RegisterEnabled bool `json:"registerEnabled"`
|
||||
SiteName string `json:"title"`
|
||||
SiteICPId string `json:"siteICPId"`
|
||||
LoginCaptcha bool `json:"loginCaptcha"`
|
||||
RegCaptcha bool `json:"regCaptcha"`
|
||||
ForgetCaptcha bool `json:"forgetCaptcha"`
|
||||
EmailActive bool `json:"emailActive"`
|
||||
Themes string `json:"themes"`
|
||||
DefaultTheme string `json:"defaultTheme"`
|
||||
HomepageViewMethod string `json:"home_view_method"`
|
||||
ShareViewMethod string `json:"share_view_method"`
|
||||
Authn bool `json:"authn"`
|
||||
User User `json:"user"`
|
||||
UseReCaptcha bool `json:"captcha_IsUseReCaptcha"`
|
||||
ReCaptchaKey string `json:"captcha_ReCaptchaKey"`
|
||||
}
|
||||
|
||||
type task struct {
|
||||
Status int `json:"status"`
|
||||
Type int `json:"type"`
|
||||
CreateDate time.Time `json:"create_date"`
|
||||
Progress int `json:"progress"`
|
||||
Error string `json:"error"`
|
||||
Status int `json:"status"`
|
||||
Type int `json:"type"`
|
||||
CreateDate string `json:"create_date"`
|
||||
Progress int `json:"progress"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// BuildTaskList 构建任务列表响应
|
||||
@@ -40,7 +35,7 @@ func BuildTaskList(tasks []model.Task, total int) Response {
|
||||
res = append(res, task{
|
||||
Status: t.Status,
|
||||
Type: t.Type,
|
||||
CreateDate: t.CreatedAt,
|
||||
CreateDate: t.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
Progress: t.Progress,
|
||||
Error: t.Error,
|
||||
})
|
||||
@@ -69,22 +64,20 @@ func BuildSiteConfig(settings map[string]string, user *model.User) Response {
|
||||
}
|
||||
res := Response{
|
||||
Data: SiteConfig{
|
||||
SiteName: checkSettingValue(settings, "siteName"),
|
||||
SiteICPId: checkSettingValue(settings, "siteICPId"),
|
||||
LoginCaptcha: model.IsTrueVal(checkSettingValue(settings, "login_captcha")),
|
||||
RegCaptcha: model.IsTrueVal(checkSettingValue(settings, "reg_captcha")),
|
||||
ForgetCaptcha: model.IsTrueVal(checkSettingValue(settings, "forget_captcha")),
|
||||
EmailActive: model.IsTrueVal(checkSettingValue(settings, "email_active")),
|
||||
Themes: checkSettingValue(settings, "themes"),
|
||||
DefaultTheme: checkSettingValue(settings, "defaultTheme"),
|
||||
HomepageViewMethod: checkSettingValue(settings, "home_view_method"),
|
||||
ShareViewMethod: checkSettingValue(settings, "share_view_method"),
|
||||
Authn: model.IsTrueVal(checkSettingValue(settings, "authn_enabled")),
|
||||
User: userRes,
|
||||
ReCaptchaKey: checkSettingValue(settings, "captcha_ReCaptchaKey"),
|
||||
CaptchaType: checkSettingValue(settings, "captcha_type"),
|
||||
TCaptchaCaptchaAppId: checkSettingValue(settings, "captcha_TCaptcha_CaptchaAppId"),
|
||||
RegisterEnabled: model.IsTrueVal(checkSettingValue(settings, "register_enabled")),
|
||||
SiteName: checkSettingValue(settings, "siteName"),
|
||||
SiteICPId: checkSettingValue(settings, "siteICPId"),
|
||||
LoginCaptcha: model.IsTrueVal(checkSettingValue(settings, "login_captcha")),
|
||||
RegCaptcha: model.IsTrueVal(checkSettingValue(settings, "reg_captcha")),
|
||||
ForgetCaptcha: model.IsTrueVal(checkSettingValue(settings, "forget_captcha")),
|
||||
EmailActive: model.IsTrueVal(checkSettingValue(settings, "email_active")),
|
||||
Themes: checkSettingValue(settings, "themes"),
|
||||
DefaultTheme: checkSettingValue(settings, "defaultTheme"),
|
||||
HomepageViewMethod: checkSettingValue(settings, "home_view_method"),
|
||||
ShareViewMethod: checkSettingValue(settings, "share_view_method"),
|
||||
Authn: model.IsTrueVal(checkSettingValue(settings, "authn_enabled")),
|
||||
User: userRes,
|
||||
UseReCaptcha: model.IsTrueVal(checkSettingValue(settings, "captcha_IsUseReCaptcha")),
|
||||
ReCaptchaKey: checkSettingValue(settings, "captcha_ReCaptchaKey"),
|
||||
}}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Share struct {
|
||||
Key string `json:"key"`
|
||||
Locked bool `json:"locked"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
CreateDate time.Time `json:"create_date,omitempty"`
|
||||
CreateDate string `json:"create_date,omitempty"`
|
||||
Downloads int `json:"downloads"`
|
||||
Views int `json:"views"`
|
||||
Expire int64 `json:"expire"`
|
||||
@@ -37,7 +37,7 @@ type myShareItem struct {
|
||||
Key string `json:"key"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Password string `json:"password"`
|
||||
CreateDate time.Time `json:"create_date,omitempty"`
|
||||
CreateDate string `json:"create_date,omitempty"`
|
||||
Downloads int `json:"downloads"`
|
||||
RemainDownloads int `json:"remain_downloads"`
|
||||
Views int `json:"views"`
|
||||
@@ -55,7 +55,7 @@ func BuildShareList(shares []model.Share, total int) Response {
|
||||
Key: hashid.HashID(shares[i].ID, hashid.ShareID),
|
||||
IsDir: shares[i].IsDir,
|
||||
Password: shares[i].Password,
|
||||
CreateDate: shares[i].CreatedAt,
|
||||
CreateDate: shares[i].CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
Downloads: shares[i].Downloads,
|
||||
Views: shares[i].Views,
|
||||
Preview: shares[i].PreviewEnabled,
|
||||
@@ -99,7 +99,7 @@ func BuildShareResponse(share *model.Share, unlocked bool) Share {
|
||||
Nick: creator.Nick,
|
||||
GroupName: creator.Group.Name,
|
||||
},
|
||||
CreateDate: share.CreatedAt,
|
||||
CreateDate: share.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
// 未解锁时只返回基本信息
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
)
|
||||
|
||||
// RemoteDeleteRequest 远程策略删除接口请求正文
|
||||
type RemoteDeleteRequest struct {
|
||||
Files []string `json:"files"`
|
||||
@@ -17,51 +10,3 @@ type ListRequest struct {
|
||||
Path string `json:"path"`
|
||||
Recursive bool `json:"recursive"`
|
||||
}
|
||||
|
||||
// NodePingReq 从机节点Ping请求
|
||||
type NodePingReq struct {
|
||||
SiteURL string `json:"site_url"`
|
||||
SiteID string `json:"site_id"`
|
||||
IsUpdate bool `json:"is_update"`
|
||||
CredentialTTL int `json:"credential_ttl"`
|
||||
Node *model.Node `json:"node"`
|
||||
}
|
||||
|
||||
// NodePingResp 从机节点Ping响应
|
||||
type NodePingResp struct {
|
||||
}
|
||||
|
||||
// SlaveAria2Call 从机有关Aria2的请求正文
|
||||
type SlaveAria2Call struct {
|
||||
Task *model.Download `json:"task"`
|
||||
GroupOptions map[string]interface{} `json:"group_options"`
|
||||
Files []int `json:"files"`
|
||||
}
|
||||
|
||||
// SlaveTransferReq 从机中转任务创建请求
|
||||
type SlaveTransferReq struct {
|
||||
Src string `json:"src"`
|
||||
Dst string `json:"dst"`
|
||||
Policy *model.Policy `json:"policy"`
|
||||
}
|
||||
|
||||
// Hash 返回创建请求的唯一标识,保持创建请求幂等
|
||||
func (s *SlaveTransferReq) Hash(id string) string {
|
||||
h := sha1.New()
|
||||
h.Write([]byte(fmt.Sprintf("transfer-%s-%s-%s-%d", id, s.Src, s.Dst, s.Policy.ID)))
|
||||
bs := h.Sum(nil)
|
||||
return fmt.Sprintf("%x", bs)
|
||||
}
|
||||
|
||||
const (
|
||||
SlaveTransferSuccess = "success"
|
||||
SlaveTransferFailed = "failed"
|
||||
)
|
||||
|
||||
type SlaveTransferResult struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(SlaveTransferResult{})
|
||||
}
|
||||
|
||||
+12
-13
@@ -6,7 +6,6 @@ import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CheckLogin 检查登录
|
||||
@@ -19,17 +18,17 @@ func CheckLogin() Response {
|
||||
|
||||
// User 用户序列化器
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"user_name"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status int `json:"status"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PreferredTheme string `json:"preferred_theme"`
|
||||
Anonymous bool `json:"anonymous"`
|
||||
Policy policy `json:"policy"`
|
||||
Group group `json:"group"`
|
||||
Tags []tag `json:"tags"`
|
||||
ID string `json:"id"`
|
||||
Email string `json:"user_name"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status int `json:"status"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
PreferredTheme string `json:"preferred_theme"`
|
||||
Anonymous bool `json:"anonymous"`
|
||||
Policy policy `json:"policy"`
|
||||
Group group `json:"group"`
|
||||
Tags []tag `json:"tags"`
|
||||
}
|
||||
|
||||
type policy struct {
|
||||
@@ -95,7 +94,7 @@ func BuildUser(user model.User) User {
|
||||
Nickname: user.Nick,
|
||||
Status: user.Status,
|
||||
Avatar: user.Avatar,
|
||||
CreatedAt: user.CreatedAt,
|
||||
CreatedAt: user.CreatedAt.Unix(),
|
||||
PreferredTheme: user.OptionsSerialized.PreferredTheme,
|
||||
Anonymous: user.IsAnonymous(),
|
||||
Policy: policy{
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package slave
|
||||
|
||||
import "github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
|
||||
var (
|
||||
ErrMasterNotFound = serializer.NewError(serializer.CodeMasterNotFound, "未知的主机节点", nil)
|
||||
)
|
||||
@@ -1,209 +0,0 @@
|
||||
package slave
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/common"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mq"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/jinzhu/gorm"
|
||||
"net/url"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var DefaultController Controller
|
||||
|
||||
// Controller controls communications between master and slave
|
||||
type Controller interface {
|
||||
// Handle heartbeat sent from master
|
||||
HandleHeartBeat(*serializer.NodePingReq) (serializer.NodePingResp, error)
|
||||
|
||||
// Get Aria2 Instance by master node ID
|
||||
GetAria2Instance(string) (common.Aria2, error)
|
||||
|
||||
// Send event change message to master node
|
||||
SendNotification(string, string, mq.Message) error
|
||||
|
||||
// Submit async task into task pool
|
||||
SubmitTask(string, interface{}, string, func(interface{})) error
|
||||
|
||||
// Get master node info
|
||||
GetMasterInfo(string) (*MasterInfo, error)
|
||||
|
||||
// Get master OneDrive policy credential
|
||||
GetOneDriveToken(string, uint) (string, error)
|
||||
}
|
||||
|
||||
type slaveController struct {
|
||||
masters map[string]MasterInfo
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// info of master node
|
||||
type MasterInfo struct {
|
||||
ID string
|
||||
TTL int
|
||||
URL *url.URL
|
||||
// used to invoke aria2 rpc calls
|
||||
Instance cluster.Node
|
||||
Client request.Client
|
||||
|
||||
jobTracker map[string]bool
|
||||
}
|
||||
|
||||
func Init() {
|
||||
DefaultController = &slaveController{
|
||||
masters: make(map[string]MasterInfo),
|
||||
}
|
||||
gob.Register(rpc.StatusInfo{})
|
||||
}
|
||||
|
||||
func (c *slaveController) HandleHeartBeat(req *serializer.NodePingReq) (serializer.NodePingResp, error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
req.Node.AfterFind()
|
||||
|
||||
// close old node if exist
|
||||
origin, ok := c.masters[req.SiteID]
|
||||
|
||||
if (ok && req.IsUpdate) || !ok {
|
||||
if ok {
|
||||
origin.Instance.Kill()
|
||||
}
|
||||
|
||||
masterUrl, err := url.Parse(req.SiteURL)
|
||||
if err != nil {
|
||||
return serializer.NodePingResp{}, err
|
||||
}
|
||||
|
||||
c.masters[req.SiteID] = MasterInfo{
|
||||
ID: req.SiteID,
|
||||
URL: masterUrl,
|
||||
TTL: req.CredentialTTL,
|
||||
Client: request.NewClient(
|
||||
request.WithEndpoint(masterUrl.String()),
|
||||
request.WithSlaveMeta(fmt.Sprintf("%d", req.Node.ID)),
|
||||
request.WithCredential(auth.HMACAuth{
|
||||
SecretKey: []byte(req.Node.MasterKey),
|
||||
}, int64(req.CredentialTTL)),
|
||||
),
|
||||
jobTracker: make(map[string]bool),
|
||||
Instance: cluster.NewNodeFromDBModel(&model.Node{
|
||||
Model: gorm.Model{ID: req.Node.ID},
|
||||
MasterKey: req.Node.MasterKey,
|
||||
Type: model.MasterNodeType,
|
||||
Aria2Enabled: req.Node.Aria2Enabled,
|
||||
Aria2OptionsSerialized: req.Node.Aria2OptionsSerialized,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
return serializer.NodePingResp{}, nil
|
||||
}
|
||||
|
||||
func (c *slaveController) GetAria2Instance(id string) (common.Aria2, error) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
if node, ok := c.masters[id]; ok {
|
||||
return node.Instance.GetAria2Instance(), nil
|
||||
}
|
||||
|
||||
return nil, ErrMasterNotFound
|
||||
}
|
||||
|
||||
func (c *slaveController) SendNotification(id, subject string, msg mq.Message) error {
|
||||
c.lock.RLock()
|
||||
|
||||
if node, ok := c.masters[id]; ok {
|
||||
c.lock.RUnlock()
|
||||
|
||||
body := bytes.Buffer{}
|
||||
enc := gob.NewEncoder(&body)
|
||||
if err := enc.Encode(&msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := node.Client.Request(
|
||||
"PUT",
|
||||
fmt.Sprintf("/api/v3/slave/notification/%s", subject),
|
||||
&body,
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return serializer.NewErrorFromResponse(res)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
c.lock.RUnlock()
|
||||
return ErrMasterNotFound
|
||||
}
|
||||
|
||||
// SubmitTask 提交异步任务
|
||||
func (c *slaveController) SubmitTask(id string, job interface{}, hash string, submitter func(interface{})) error {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
if node, ok := c.masters[id]; ok {
|
||||
if _, ok := node.jobTracker[hash]; ok {
|
||||
// 任务已存在,直接返回
|
||||
return nil
|
||||
}
|
||||
|
||||
submitter(job)
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrMasterNotFound
|
||||
}
|
||||
|
||||
// GetMasterInfo 获取主机节点信息
|
||||
func (c *slaveController) GetMasterInfo(id string) (*MasterInfo, error) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
if node, ok := c.masters[id]; ok {
|
||||
return &node, nil
|
||||
}
|
||||
|
||||
return nil, ErrMasterNotFound
|
||||
}
|
||||
|
||||
// GetOneDriveToken 获取主机OneDrive凭证
|
||||
func (c *slaveController) GetOneDriveToken(id string, policyID uint) (string, error) {
|
||||
c.lock.RLock()
|
||||
|
||||
if node, ok := c.masters[id]; ok {
|
||||
c.lock.RUnlock()
|
||||
|
||||
res, err := node.Client.Request(
|
||||
"GET",
|
||||
fmt.Sprintf("/api/v3/slave/credential/onedrive/%d", policyID),
|
||||
nil,
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return "", serializer.NewErrorFromResponse(res)
|
||||
}
|
||||
|
||||
return res.Data.(string), nil
|
||||
}
|
||||
|
||||
c.lock.RUnlock()
|
||||
return "", ErrMasterNotFound
|
||||
}
|
||||
@@ -106,7 +106,7 @@ func (job *CompressTask) Do() {
|
||||
job.TaskModel.SetProgress(TransferringProgress)
|
||||
|
||||
// 上传文件
|
||||
err = fs.UploadFromPath(ctx, zipFile, job.TaskProps.Dst, true)
|
||||
err = fs.UploadFromPath(ctx, zipFile, job.TaskProps.Dst)
|
||||
if err != nil {
|
||||
job.SetErrorMsg(err.Error())
|
||||
return
|
||||
|
||||
+1
-3
@@ -96,9 +96,7 @@ func Resume() {
|
||||
continue
|
||||
}
|
||||
|
||||
if job != nil {
|
||||
TaskPoll.Submit(job)
|
||||
}
|
||||
TaskPoll.Submit(job)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -2,7 +2,6 @@ package task
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
)
|
||||
|
||||
@@ -57,7 +56,5 @@ func Init() {
|
||||
TaskPoll.Add(maxWorker)
|
||||
util.Log().Info("初始化任务队列,WorkerNum = %d", maxWorker)
|
||||
|
||||
if conf.SystemConfig.Mode == "master" {
|
||||
Resume()
|
||||
}
|
||||
Resume()
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
package slavetask
|
||||
|
||||
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/mq"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/slave"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/task"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// TransferTask 文件中转任务
|
||||
type TransferTask struct {
|
||||
Err *task.JobError
|
||||
Req *serializer.SlaveTransferReq
|
||||
MasterID string
|
||||
}
|
||||
|
||||
// Props 获取任务属性
|
||||
func (job *TransferTask) Props() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Type 获取任务类型
|
||||
func (job *TransferTask) Type() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Creator 获取创建者ID
|
||||
func (job *TransferTask) Creator() uint {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Model 获取任务的数据库模型
|
||||
func (job *TransferTask) Model() *model.Task {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetStatus 设定状态
|
||||
func (job *TransferTask) SetStatus(status int) {
|
||||
}
|
||||
|
||||
// SetError 设定任务失败信息
|
||||
func (job *TransferTask) SetError(err *task.JobError) {
|
||||
job.Err = err
|
||||
|
||||
}
|
||||
|
||||
// SetErrorMsg 设定任务失败信息
|
||||
func (job *TransferTask) SetErrorMsg(msg string, err error) {
|
||||
jobErr := &task.JobError{Msg: msg}
|
||||
if err != nil {
|
||||
jobErr.Error = err.Error()
|
||||
}
|
||||
|
||||
job.SetError(jobErr)
|
||||
|
||||
notifyMsg := mq.Message{
|
||||
TriggeredBy: job.MasterID,
|
||||
Event: serializer.SlaveTransferFailed,
|
||||
Content: serializer.SlaveTransferResult{
|
||||
Error: err.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
if err := slave.DefaultController.SendNotification(job.MasterID, job.Req.Hash(job.MasterID), notifyMsg); err != nil {
|
||||
util.Log().Warning("无法发送转存失败通知到从机, ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetError 返回任务失败信息
|
||||
func (job *TransferTask) GetError() *task.JobError {
|
||||
return job.Err
|
||||
}
|
||||
|
||||
// Do 开始执行任务
|
||||
func (job *TransferTask) Do() {
|
||||
defer job.Recycle()
|
||||
|
||||
fs, err := filesystem.NewAnonymousFileSystem()
|
||||
if err != nil {
|
||||
job.SetErrorMsg("无法初始化匿名文件系统", err)
|
||||
return
|
||||
}
|
||||
|
||||
fs.Policy = job.Req.Policy
|
||||
if err := fs.DispatchHandler(); err != nil {
|
||||
job.SetErrorMsg("无法分发存储策略", err)
|
||||
return
|
||||
}
|
||||
|
||||
master, err := slave.DefaultController.GetMasterInfo(job.MasterID)
|
||||
if err != nil {
|
||||
job.SetErrorMsg("找不到主机节点", err)
|
||||
return
|
||||
}
|
||||
|
||||
fs.SwitchToShadowHandler(master.Instance, master.URL.String(), master.ID)
|
||||
ctx := context.WithValue(context.Background(), fsctx.DisableOverwrite, true)
|
||||
file, err := os.Open(util.RelativePath(job.Req.Src))
|
||||
if err != nil {
|
||||
job.SetErrorMsg("无法读取源文件", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
// 获取源文件大小
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
job.SetErrorMsg("无法获取源文件大小", err)
|
||||
return
|
||||
}
|
||||
|
||||
size := fi.Size()
|
||||
|
||||
err = fs.Handler.Put(ctx, file, job.Req.Dst, uint64(size))
|
||||
if err != nil {
|
||||
job.SetErrorMsg("文件上传失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
msg := mq.Message{
|
||||
TriggeredBy: job.MasterID,
|
||||
Event: serializer.SlaveTransferSuccess,
|
||||
Content: serializer.SlaveTransferResult{},
|
||||
}
|
||||
|
||||
if err := slave.DefaultController.SendNotification(job.MasterID, job.Req.Hash(job.MasterID), msg); err != nil {
|
||||
util.Log().Warning("无法发送转存成功通知到从机, ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Recycle 回收临时文件
|
||||
func (job *TransferTask) Recycle() {
|
||||
err := os.RemoveAll(filepath.Dir(job.Req.Src))
|
||||
if err != nil {
|
||||
util.Log().Warning("无法删除中转临时目录[%s], %s", job.Req.Src, err)
|
||||
}
|
||||
}
|
||||
+9
-33
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
@@ -27,14 +26,11 @@ type TransferTask struct {
|
||||
|
||||
// TransferProps 中转任务属性
|
||||
type TransferProps struct {
|
||||
Src []string `json:"src"` // 原始文件
|
||||
SrcSizes map[string]uint64 `json:"src_size"` // 原始文件的大小信息,从机转存时使用
|
||||
Parent string `json:"parent"` // 父目录
|
||||
Dst string `json:"dst"` // 目的目录ID
|
||||
Src []string `json:"src"` // 原始文件
|
||||
Parent string `json:"parent"` // 父目录
|
||||
Dst string `json:"dst"` // 目的目录ID
|
||||
// 将会保留原始文件的目录结构,Src 除去 Parent 开头作为最终路径
|
||||
TrimPath bool `json:"trim_path"`
|
||||
// 负责处理中专任务的节点ID
|
||||
NodeID uint `json:"node_id"`
|
||||
}
|
||||
|
||||
// Props 获取任务属性
|
||||
@@ -108,24 +104,7 @@ func (job *TransferTask) Do() {
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), fsctx.DisableOverwrite, true)
|
||||
ctx = context.WithValue(ctx, fsctx.SlaveSrcPath, file)
|
||||
if job.TaskProps.NodeID > 1 {
|
||||
// 指定为从机中转
|
||||
|
||||
// 获取从机节点
|
||||
node := cluster.Default.GetNodeByID(job.TaskProps.NodeID)
|
||||
if node == nil {
|
||||
job.SetErrorMsg("从机节点不可用", nil)
|
||||
}
|
||||
|
||||
// 切换为从机节点处理上传
|
||||
fs.SwitchToSlaveHandler(node)
|
||||
err = fs.UploadFromStream(ctx, nil, dst, job.TaskProps.SrcSizes[file])
|
||||
} else {
|
||||
// 主机节点中转
|
||||
err = fs.UploadFromPath(ctx, file, dst, true)
|
||||
}
|
||||
|
||||
err = fs.UploadFromPath(ctx, file, dst)
|
||||
if err != nil {
|
||||
job.SetErrorMsg("文件转存失败", err)
|
||||
}
|
||||
@@ -135,16 +114,15 @@ func (job *TransferTask) Do() {
|
||||
|
||||
// Recycle 回收临时文件
|
||||
func (job *TransferTask) Recycle() {
|
||||
if job.TaskProps.NodeID == 1 {
|
||||
err := os.RemoveAll(job.TaskProps.Parent)
|
||||
if err != nil {
|
||||
util.Log().Warning("无法删除中转临时目录[%s], %s", job.TaskProps.Parent, err)
|
||||
}
|
||||
err := os.RemoveAll(job.TaskProps.Parent)
|
||||
if err != nil {
|
||||
util.Log().Warning("无法删除中转临时目录[%s], %s", job.TaskProps.Parent, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// NewTransferTask 新建中转任务
|
||||
func NewTransferTask(user uint, src []string, dst, parent string, trim bool, node uint, sizes map[string]uint64) (Job, error) {
|
||||
func NewTransferTask(user uint, src []string, dst, parent string, trim bool) (Job, error) {
|
||||
creator, err := model.GetActiveUserByID(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -157,8 +135,6 @@ func NewTransferTask(user uint, src []string, dst, parent string, trim bool, nod
|
||||
Parent: parent,
|
||||
Dst: dst,
|
||||
TrimPath: trim,
|
||||
NodeID: node,
|
||||
SrcSizes: sizes,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -188,7 +188,7 @@ func props(ctx context.Context, fs *filesystem.FileSystem, ls LockSystem, fi Fil
|
||||
}
|
||||
// Otherwise, it must either be a live property or we don't know it.
|
||||
if prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !isDir) {
|
||||
innerXML, err := prop.findFn(ctx, fs, ls, fi.GetName(), fi)
|
||||
innerXML, err := prop.findFn(ctx, fs, ls, "", fi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -371,11 +371,11 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request, fs *filesyst
|
||||
fs.Use("AfterValidateFailed", filesystem.HookDeleteTempFile)
|
||||
fs.Use("AfterValidateFailed", filesystem.HookGiveBackCapacity)
|
||||
fs.Use("AfterUploadFailed", filesystem.HookGiveBackCapacity)
|
||||
|
||||
// 禁止覆盖
|
||||
ctx = context.WithValue(ctx, fsctx.DisableOverwrite, true)
|
||||
}
|
||||
|
||||
// 禁止覆盖
|
||||
ctx = context.WithValue(ctx, fsctx.DisableOverwrite, true)
|
||||
|
||||
// 执行上传
|
||||
err = fs.Upload(ctx, fileData)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,7 +3,6 @@ package controllers
|
||||
import (
|
||||
"io"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
@@ -25,8 +24,8 @@ func AdminSummary(c *gin.Context) {
|
||||
|
||||
// AdminNews 获取社区新闻
|
||||
func AdminNews(c *gin.Context) {
|
||||
r := request.NewClient()
|
||||
res := r.Request("GET", "https://forum.cloudreve.org/api/discussions?include=startUser%2ClastUser%2CstartPost%2Ctags&filter%5Bq%5D=%20tag%3Anotice&sort=-startTime&page%5Blimit%5D=10", nil)
|
||||
r := request.HTTPClient{}
|
||||
res := r.Request("GET", "https://forum.cloudreve.org/api/discussions?include=startUser%2ClastUser%2CstartPost%2Ctags&filter%5Bq%5D=%20tag%3Anotice&sort=-startTime&", nil)
|
||||
if res.Err == nil {
|
||||
io.Copy(c.Writer, res.Response.Body)
|
||||
}
|
||||
@@ -93,13 +92,7 @@ func AdminSendTestMail(c *gin.Context) {
|
||||
func AdminTestAria2(c *gin.Context) {
|
||||
var service admin.Aria2TestService
|
||||
if err := c.ShouldBindJSON(&service); err == nil {
|
||||
var res serializer.Response
|
||||
if service.Type == model.MasterNodeType {
|
||||
res = service.TestMaster()
|
||||
} else {
|
||||
res = service.TestSlave()
|
||||
}
|
||||
|
||||
res := service.Test()
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
@@ -432,58 +425,3 @@ func AdminListFolders(c *gin.Context) {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
// AdminListNodes 列出从机节点
|
||||
func AdminListNodes(c *gin.Context) {
|
||||
var service admin.AdminListService
|
||||
if err := c.ShouldBindJSON(&service); err == nil {
|
||||
res := service.Nodes()
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
// AdminAddNode 新建节点
|
||||
func AdminAddNode(c *gin.Context) {
|
||||
var service admin.AddNodeService
|
||||
if err := c.ShouldBindJSON(&service); err == nil {
|
||||
res := service.Add()
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
// AdminToggleNode 启用/暂停节点
|
||||
func AdminToggleNode(c *gin.Context) {
|
||||
var service admin.ToggleNodeService
|
||||
if err := c.ShouldBindUri(&service); err == nil {
|
||||
res := service.Toggle()
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
// AdminDeleteGroup 删除用户组
|
||||
func AdminDeleteNode(c *gin.Context) {
|
||||
var service admin.NodeService
|
||||
if err := c.ShouldBindUri(&service); err == nil {
|
||||
res := service.Delete()
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
// AdminGetNode 获取节点详情
|
||||
func AdminGetNode(c *gin.Context) {
|
||||
var service admin.NodeService
|
||||
if err := c.ShouldBindUri(&service); err == nil {
|
||||
res := service.Get()
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package controllers
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/common"
|
||||
ariaCall "github.com/cloudreve/Cloudreve/v3/pkg/aria2"
|
||||
"github.com/cloudreve/Cloudreve/v3/service/aria2"
|
||||
"github.com/cloudreve/Cloudreve/v3/service/explorer"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func AddAria2URL(c *gin.Context) {
|
||||
var addService aria2.AddURLService
|
||||
if err := c.ShouldBindJSON(&addService); err == nil {
|
||||
res := addService.Add(c, common.URLTask)
|
||||
res := addService.Add(c, ariaCall.URLTask)
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
@@ -52,7 +52,7 @@ func AddAria2Torrent(c *gin.Context) {
|
||||
|
||||
if err := c.ShouldBindJSON(&addService); err == nil {
|
||||
addService.URL = res.Data.(string)
|
||||
res := addService.Add(c, common.URLTask)
|
||||
res := addService.Add(c, ariaCall.URLTask)
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user