Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
cf08aa3668 | |||
9c84b6596f | |||
022e0ca292 | |||
88947f6676 | |||
b07ddfbc13 | |||
9a0a63d34c | |||
195c869272 | |||
bdfc1591bd | |||
82222840fe | |||
45e009a22c | |||
ac68079a76 | |||
2a17d0c2cd | |||
6f6a8e6dfc | |||
7d9ecba99c |
44
.air.toml
Normal file
44
.air.toml
Normal file
@ -0,0 +1,44 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = ["server"]
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
delay = 0
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
1
.github/workflows/build_docker.yml
vendored
1
.github/workflows/build_docker.yml
vendored
@ -71,6 +71,7 @@ jobs:
|
||||
id: docker_build_ffmpeg
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.ffmpeg
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
tags: ${{ steps.meta-ffmpeg.outputs.tags }}
|
||||
|
1
.github/workflows/release_docker.yml
vendored
1
.github/workflows/release_docker.yml
vendored
@ -62,6 +62,7 @@ jobs:
|
||||
id: docker_build_ffmpeg
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.ffmpeg
|
||||
push: true
|
||||
tags: ${{ steps.meta-ffmpeg.outputs.tags }}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,6 +24,7 @@ output/
|
||||
*.json
|
||||
/build
|
||||
/data/
|
||||
/tmp/
|
||||
/log/
|
||||
/lang/
|
||||
/daemon/
|
||||
|
@ -76,6 +76,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
||||
- [X] Cloudreve
|
||||
- [x] [Dropbox](https://www.dropbox.com/)
|
||||
- [x] [FeijiPan](https://www.feijipan.com/)
|
||||
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
||||
- [x] Easy to deploy and out-of-the-box
|
||||
- [x] File preview (PDF, markdown, code, plain text, ...)
|
||||
- [x] Image preview in gallery mode
|
||||
|
@ -75,6 +75,7 @@
|
||||
- [X] Cloudreve
|
||||
- [x] [Dropbox](https://www.dropbox.com/)
|
||||
- [x] [飞机盘](https://www.feijipan.com/)
|
||||
- [x] [多吉云](https://www.dogecloud.com/product/oss)
|
||||
- [x] 部署方便,开箱即用
|
||||
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
||||
- [x] 画廊模式下的图像预览
|
||||
|
@ -76,6 +76,7 @@
|
||||
- [X] Cloudreve
|
||||
- [x] [Dropbox](https://www.dropbox.com/)
|
||||
- [x] [FeijiPan](https://www.feijipan.com/)
|
||||
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
||||
- [x] デプロイが簡単で、すぐに使える
|
||||
- [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...)
|
||||
- [x] ギャラリーモードでの画像プレビュー
|
||||
|
@ -91,6 +91,27 @@ the address is defined in config file`,
|
||||
}
|
||||
}()
|
||||
}
|
||||
s3r := gin.New()
|
||||
s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
|
||||
server.InitS3(s3r)
|
||||
if conf.Conf.S3.Port != -1 {
|
||||
s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
|
||||
utils.Log.Infof("start S3 server @ %s", s3Base)
|
||||
go func() {
|
||||
var err error
|
||||
if conf.Conf.S3.SSL {
|
||||
httpsSrv = &http.Server{Addr: s3Base, Handler: s3r}
|
||||
err = httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||
}
|
||||
if !conf.Conf.S3.SSL {
|
||||
httpSrv = &http.Server{Addr: s3Base, Handler: s3r}
|
||||
err = httpSrv.ListenAndServe()
|
||||
}
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
utils.Log.Fatalf("failed to start s3 server: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
// Wait for interrupt signal to gracefully shutdown the server with
|
||||
// a timeout of 1 second.
|
||||
quit := make(chan os.Signal, 1)
|
||||
|
@ -8,18 +8,21 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/pkg/cron"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Yun139 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
cron *cron.Cron
|
||||
Account string
|
||||
}
|
||||
|
||||
@ -35,6 +38,13 @@ func (d *Yun139) Init(ctx context.Context) error {
|
||||
if d.Authorization == "" {
|
||||
return fmt.Errorf("authorization is empty")
|
||||
}
|
||||
d.cron = cron.NewCron(time.Hour * 24 * 7)
|
||||
d.cron.Do(func() {
|
||||
err := d.refreshToken()
|
||||
if err != nil {
|
||||
log.Errorf("%+v", err)
|
||||
}
|
||||
})
|
||||
switch d.Addition.Type {
|
||||
case MetaPersonalNew:
|
||||
if len(d.Addition.RootFolderID) == 0 {
|
||||
@ -72,6 +82,9 @@ func (d *Yun139) Init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *Yun139) Drop(ctx context.Context) error {
|
||||
if d.cron != nil {
|
||||
d.cron.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,9 @@
|
||||
package _139
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
const (
|
||||
MetaPersonal string = "personal"
|
||||
MetaFamily string = "family"
|
||||
@ -230,3 +234,12 @@ type PersonalUploadResp struct {
|
||||
UploadId string `json:"uploadId"`
|
||||
}
|
||||
}
|
||||
|
||||
type RefreshTokenResp struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Return string `xml:"return"`
|
||||
Token string `xml:"token"`
|
||||
Expiretime int32 `xml:"expiretime"`
|
||||
AccessToken string `xml:"accessToken"`
|
||||
Desc string `xml:"desc"`
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@ -52,6 +53,32 @@ func getTime(t string) time.Time {
|
||||
return stamp
|
||||
}
|
||||
|
||||
func (d *Yun139) refreshToken() error {
|
||||
url := "https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do"
|
||||
var resp RefreshTokenResp
|
||||
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decodeStr := string(decode)
|
||||
splits := strings.Split(decodeStr, ":")
|
||||
reqBody := "<root><token>" + splits[2] + "</token><account>" + splits[1] + "</account><clienttype>656</clienttype></root>"
|
||||
_, err = base.RestyClient.R().
|
||||
ForceContentType("application/xml").
|
||||
SetBody(reqBody).
|
||||
SetResult(&resp).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Return != "0" {
|
||||
return fmt.Errorf("failed to refresh token: %s", resp.Desc)
|
||||
}
|
||||
d.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + ":" + splits[1] + ":" + resp.Token))
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
url := "https://yun.139.com" + pathname
|
||||
req := base.RestyClient.R()
|
||||
|
@ -145,7 +145,10 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
u.RawQuery = query.Encode()
|
||||
realURL := u.String()
|
||||
// get the url after redirect
|
||||
res, err := base.NoRedirectClient.R().Get(realURL)
|
||||
res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{
|
||||
//"Origin": d.conf.site,
|
||||
"Referer": d.conf.site + "/",
|
||||
}).Get(realURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ type Conf struct {
|
||||
unproved string
|
||||
proved string
|
||||
devVersion string
|
||||
site string
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -45,7 +46,8 @@ func init() {
|
||||
bucket: "wpanstore-lanzou",
|
||||
unproved: "unproved",
|
||||
proved: "proved",
|
||||
devVersion: "120",
|
||||
devVersion: "122",
|
||||
site: "https://www.ilanzou.com",
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -71,6 +73,7 @@ func init() {
|
||||
unproved: "ws",
|
||||
proved: "app",
|
||||
devVersion: "121",
|
||||
site: "https://www.feijipan.com",
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -52,12 +52,16 @@ func (d *ILanZou) request(pathname, method string, callback base.ReqCallback, pr
|
||||
"devType": "6",
|
||||
"devCode": d.UUID,
|
||||
"devModel": "chrome",
|
||||
"devVersion": "120",
|
||||
"devVersion": d.conf.devVersion,
|
||||
"appVersion": "",
|
||||
"timestamp": ts,
|
||||
//"appToken": d.Token,
|
||||
"extra": "2",
|
||||
})
|
||||
req.SetHeaders(map[string]string{
|
||||
"Origin": d.conf.site,
|
||||
"Referer": d.conf.site + "/",
|
||||
})
|
||||
if proved {
|
||||
req.SetQueryParam("appToken", d.Token)
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ func (d *IPFS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
|
||||
for _, file := range dirs {
|
||||
gateurl := *d.gateURL
|
||||
gateurl.Path = "ipfs/" + file.Hash
|
||||
gateurl.RawQuery = "filename=" + file.Name
|
||||
gateurl.RawQuery = "filename=" + url.PathEscape(file.Name)
|
||||
objlist = append(objlist, &model.ObjectURL{
|
||||
Object: model.Object{ID: file.Hash, Name: file.Name, Size: int64(file.Size), IsFolder: file.Type == 1},
|
||||
Url: model.Url{Url: gateurl.String()},
|
||||
@ -73,7 +73,7 @@ func (d *IPFS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
|
||||
}
|
||||
|
||||
func (d *IPFS) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
link := d.Gateway + "/ipfs/" + file.GetID() + "/?filename=" + file.GetName()
|
||||
link := d.Gateway + "/ipfs/" + file.GetID() + "/?filename=" + url.PathEscape(file.GetName())
|
||||
return &model.Link{URL: link}, nil
|
||||
}
|
||||
|
||||
|
@ -295,7 +295,7 @@ func (d *MoPan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
|
||||
}
|
||||
|
||||
if !initUpdload.FileDataExists {
|
||||
utils.Log.Error(d.client.CloudDiskStartBusiness())
|
||||
// utils.Log.Error(d.client.CloudDiskStartBusiness())
|
||||
|
||||
threadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
|
||||
retry.Attempts(3),
|
||||
@ -323,6 +323,7 @@ func (d *MoPan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.ContentLength = byteSize
|
||||
resp, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
|
62
drivers/s3/doge.go
Normal file
62
drivers/s3/doge.go
Normal file
@ -0,0 +1,62 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TmpTokenResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data TmpTokenResponseData `json:"data,omitempty"`
|
||||
}
|
||||
type TmpTokenResponseData struct {
|
||||
Credentials Credentials `json:"Credentials"`
|
||||
}
|
||||
type Credentials struct {
|
||||
AccessKeyId string `json:"accessKeyId,omitempty"`
|
||||
SecretAccessKey string `json:"secretAccessKey,omitempty"`
|
||||
SessionToken string `json:"sessionToken,omitempty"`
|
||||
}
|
||||
|
||||
func getCredentials(AccessKey, SecretKey string) (rst Credentials, err error) {
|
||||
apiPath := "/auth/tmp_token.json"
|
||||
reqBody, err := json.Marshal(map[string]interface{}{"channel": "OSS_FULL", "scopes": []string{"*"}})
|
||||
if err != nil {
|
||||
return rst, err
|
||||
}
|
||||
|
||||
signStr := apiPath + "\n" + string(reqBody)
|
||||
hmacObj := hmac.New(sha1.New, []byte(SecretKey))
|
||||
hmacObj.Write([]byte(signStr))
|
||||
sign := hex.EncodeToString(hmacObj.Sum(nil))
|
||||
Authorization := "TOKEN " + AccessKey + ":" + sign
|
||||
|
||||
req, err := http.NewRequest("POST", "https://api.dogecloud.com"+apiPath, strings.NewReader(string(reqBody)))
|
||||
if err != nil {
|
||||
return rst, err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("Authorization", Authorization)
|
||||
client := http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return rst, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
ret, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return rst, err
|
||||
}
|
||||
var tmpTokenResp TmpTokenResponse
|
||||
err = json.Unmarshal(ret, &tmpTokenResp)
|
||||
if err != nil {
|
||||
return rst, err
|
||||
}
|
||||
return tmpTokenResp.Data.Credentials, nil
|
||||
}
|
@ -26,10 +26,12 @@ type S3 struct {
|
||||
Session *session.Session
|
||||
client *s3.S3
|
||||
linkClient *s3.S3
|
||||
|
||||
config driver.Config
|
||||
}
|
||||
|
||||
func (d *S3) Config() driver.Config {
|
||||
return config
|
||||
return d.config
|
||||
}
|
||||
|
||||
func (d *S3) GetAddition() driver.Additional {
|
||||
|
@ -22,15 +22,25 @@ type Addition struct {
|
||||
AddFilenameToDisposition bool `json:"add_filename_to_disposition" help:"Add filename to Content-Disposition header."`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &S3{
|
||||
config: driver.Config{
|
||||
Name: "S3",
|
||||
DefaultRoot: "/",
|
||||
LocalSort: true,
|
||||
CheckStatus: true,
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
})
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &S3{}
|
||||
return &S3{
|
||||
config: driver.Config{
|
||||
Name: "Doge",
|
||||
DefaultRoot: "/",
|
||||
LocalSort: true,
|
||||
CheckStatus: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -21,13 +21,21 @@ import (
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
func (d *S3) initSession() error {
|
||||
var err error
|
||||
accessKeyID, secretAccessKey, sessionToken := d.AccessKeyID, d.SecretAccessKey, d.SessionToken
|
||||
if d.config.Name == "Doge" {
|
||||
credentialsTmp, err := getCredentials(d.AccessKeyID, d.SecretAccessKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accessKeyID, secretAccessKey, sessionToken = credentialsTmp.AccessKeyId, credentialsTmp.SecretAccessKey, credentialsTmp.SessionToken
|
||||
}
|
||||
cfg := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(d.AccessKeyID, d.SecretAccessKey, d.SessionToken),
|
||||
Credentials: credentials.NewStaticCredentials(accessKeyID, secretAccessKey, sessionToken),
|
||||
Region: &d.Region,
|
||||
Endpoint: &d.Endpoint,
|
||||
S3ForcePathStyle: aws.Bool(d.ForcePathStyle),
|
||||
}
|
||||
var err error
|
||||
d.Session, err = session.NewSession(cfg)
|
||||
return err
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -19,6 +18,7 @@ type Seafile struct {
|
||||
Addition
|
||||
|
||||
authorization string
|
||||
libraryMap map[string]*LibraryInfo
|
||||
}
|
||||
|
||||
func (d *Seafile) Config() driver.Config {
|
||||
@ -31,6 +31,8 @@ func (d *Seafile) GetAddition() driver.Additional {
|
||||
|
||||
func (d *Seafile) Init(ctx context.Context) error {
|
||||
d.Address = strings.TrimSuffix(d.Address, "/")
|
||||
d.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)
|
||||
d.libraryMap = make(map[string]*LibraryInfo)
|
||||
return d.getToken()
|
||||
}
|
||||
|
||||
@ -38,10 +40,37 @@ func (d *Seafile) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) (result []model.Obj, err error) {
|
||||
path := dir.GetPath()
|
||||
if path == d.RootFolderPath {
|
||||
libraries, err := d.listLibraries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if path == "/" && d.RepoId == "" {
|
||||
return utils.SliceConvert(libraries, func(f LibraryItemResp) (model.Obj, error) {
|
||||
return &model.Object{
|
||||
Name: f.Name,
|
||||
Modified: time.Unix(f.Modified, 0),
|
||||
Size: f.Size,
|
||||
IsFolder: true,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
var repo *LibraryInfo
|
||||
repo, path, err = d.getRepoAndPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if repo.Encrypted {
|
||||
err = d.decryptLibrary(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var resp []RepoDirItemResp
|
||||
_, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
_, err = d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", repo.Id), func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetQueryParams(map[string]string{
|
||||
"p": path,
|
||||
})
|
||||
@ -63,9 +92,13 @@ func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
}
|
||||
|
||||
func (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(file.GetPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": file.GetPath(),
|
||||
"p": path,
|
||||
"reuse": "1",
|
||||
})
|
||||
})
|
||||
@ -78,9 +111,14 @@ func (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
}
|
||||
|
||||
func (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(parentDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, _ = utils.JoinBasePath(path, dirName)
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/dir/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": filepath.Join(parentDir.GetPath(), dirName),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "mkdir",
|
||||
})
|
||||
@ -89,22 +127,34 @@ func (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
|
||||
}
|
||||
|
||||
func (d *Seafile) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstRepo, dstPath, err := d.getRepoAndPath(dstDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": srcObj.GetPath(),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "move",
|
||||
"dst_repo": d.Addition.RepoId,
|
||||
"dst_dir": dstDir.GetPath(),
|
||||
"dst_repo": dstRepo.Id,
|
||||
"dst_dir": dstPath,
|
||||
})
|
||||
}, true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": srcObj.GetPath(),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "rename",
|
||||
"newname": newName,
|
||||
@ -114,31 +164,47 @@ func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string)
|
||||
}
|
||||
|
||||
func (d *Seafile) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstRepo, dstPath, err := d.getRepoAndPath(dstDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": srcObj.GetPath(),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "copy",
|
||||
"dst_repo": d.Addition.RepoId,
|
||||
"dst_dir": dstDir.GetPath(),
|
||||
"dst_repo": dstRepo.Id,
|
||||
"dst_dir": dstPath,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Seafile) Remove(ctx context.Context, obj model.Obj) error {
|
||||
_, err := d.request(http.MethodDelete, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(obj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodDelete, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": obj.GetPath(),
|
||||
"p": path,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/upload-link/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(dstDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/upload-link/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": dstDir.GetPath(),
|
||||
"p": path,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
@ -150,7 +216,7 @@ func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
||||
_, err = d.request(http.MethodPost, u, func(req *resty.Request) {
|
||||
req.SetFileReader("file", stream.GetName(), stream).
|
||||
SetFormData(map[string]string{
|
||||
"parent_dir": dstDir.GetPath(),
|
||||
"parent_dir": path,
|
||||
"replace": "1",
|
||||
})
|
||||
})
|
||||
|
@ -11,7 +11,8 @@ type Addition struct {
|
||||
Address string `json:"address" required:"true"`
|
||||
UserName string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
RepoId string `json:"repoId" required:"true"`
|
||||
RepoId string `json:"repoId" required:"false"`
|
||||
RepoPwd string `json:"repoPwd" required:"false"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -1,14 +1,44 @@
|
||||
package seafile
|
||||
|
||||
import "time"
|
||||
|
||||
type AuthTokenResp struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type RepoDirItemResp struct {
|
||||
type RepoItemResp struct {
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"` // dir, file
|
||||
Type string `json:"type"` // repo, dir, file
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Modified int64 `json:"mtime"`
|
||||
Permission string `json:"permission"`
|
||||
}
|
||||
|
||||
type LibraryItemResp struct {
|
||||
RepoItemResp
|
||||
OwnerContactEmail string `json:"owner_contact_email"`
|
||||
OwnerName string `json:"owner_name"`
|
||||
Owner string `json:"owner"`
|
||||
ModifierEmail string `json:"modifier_email"`
|
||||
ModifierContactEmail string `json:"modifier_contact_email"`
|
||||
ModifierName string `json:"modifier_name"`
|
||||
Virtual bool `json:"virtual"`
|
||||
MtimeRelative string `json:"mtime_relative"`
|
||||
Encrypted bool `json:"encrypted"`
|
||||
Version int `json:"version"`
|
||||
HeadCommitId string `json:"head_commit_id"`
|
||||
Root string `json:"root"`
|
||||
Salt string `json:"salt"`
|
||||
SizeFormatted string `json:"size_formatted"`
|
||||
}
|
||||
|
||||
type RepoDirItemResp struct {
|
||||
RepoItemResp
|
||||
}
|
||||
|
||||
type LibraryInfo struct {
|
||||
LibraryItemResp
|
||||
decryptedTime time.Time
|
||||
decryptedSuccess bool
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
package seafile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/go-resty/resty/v2"
|
||||
@ -60,3 +65,110 @@ func (d *Seafile) request(method string, pathname string, callback base.ReqCallb
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *Seafile) getRepoAndPath(fullPath string) (repo *LibraryInfo, path string, err error) {
|
||||
libraryMap := d.libraryMap
|
||||
repoId := d.Addition.RepoId
|
||||
if repoId != "" {
|
||||
if len(repoId) == 36 /* uuid */ {
|
||||
for _, library := range libraryMap {
|
||||
if library.Id == repoId {
|
||||
return library, fullPath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var repoName string
|
||||
str := fullPath[1:]
|
||||
pos := strings.IndexRune(str, '/')
|
||||
if pos == -1 {
|
||||
repoName = str
|
||||
} else {
|
||||
repoName = str[:pos]
|
||||
}
|
||||
path = utils.FixAndCleanPath(fullPath[1+len(repoName):])
|
||||
if library, ok := libraryMap[repoName]; ok {
|
||||
return library, path, nil
|
||||
}
|
||||
}
|
||||
return nil, "", errs.ObjectNotFound
|
||||
}
|
||||
|
||||
func (d *Seafile) listLibraries() (resp []LibraryItemResp, err error) {
|
||||
repoId := d.Addition.RepoId
|
||||
if repoId == "" {
|
||||
_, err = d.request(http.MethodGet, "/api2/repos/", func(req *resty.Request) {
|
||||
req.SetResult(&resp)
|
||||
})
|
||||
} else {
|
||||
var oneResp LibraryItemResp
|
||||
_, err = d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/", repoId), func(req *resty.Request) {
|
||||
req.SetResult(&oneResp)
|
||||
})
|
||||
if err == nil {
|
||||
resp = append(resp, oneResp)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
libraryMap := make(map[string]*LibraryInfo)
|
||||
var putLibraryMap func(library LibraryItemResp, index int)
|
||||
putLibraryMap = func(library LibraryItemResp, index int) {
|
||||
name := library.Name
|
||||
if index > 0 {
|
||||
name = fmt.Sprintf("%s (%d)", name, index)
|
||||
}
|
||||
if _, exist := libraryMap[name]; exist {
|
||||
putLibraryMap(library, index+1)
|
||||
} else {
|
||||
libraryInfo := LibraryInfo{}
|
||||
data, _ := utils.Json.Marshal(library)
|
||||
_ = utils.Json.Unmarshal(data, &libraryInfo)
|
||||
libraryMap[name] = &libraryInfo
|
||||
}
|
||||
}
|
||||
for _, library := range resp {
|
||||
putLibraryMap(library, 0)
|
||||
}
|
||||
d.libraryMap = libraryMap
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
var repoPwdNotConfigured = errors.New("library password not configured")
|
||||
var repoPwdIncorrect = errors.New("library password is incorrect")
|
||||
|
||||
func (d *Seafile) decryptLibrary(repo *LibraryInfo) (err error) {
|
||||
if !repo.Encrypted {
|
||||
return nil
|
||||
}
|
||||
if d.RepoPwd == "" {
|
||||
return repoPwdNotConfigured
|
||||
}
|
||||
now := time.Now()
|
||||
decryptedTime := repo.decryptedTime
|
||||
if repo.decryptedSuccess {
|
||||
if now.Sub(decryptedTime).Minutes() <= 30 {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if now.Sub(decryptedTime).Seconds() <= 10 {
|
||||
return repoPwdIncorrect
|
||||
}
|
||||
}
|
||||
var resp string
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/", repo.Id), func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetFormData(map[string]string{
|
||||
"password": d.RepoPwd,
|
||||
})
|
||||
})
|
||||
repo.decryptedTime = time.Now()
|
||||
if err != nil || !strings.Contains(resp, "success") {
|
||||
repo.decryptedSuccess = false
|
||||
return err
|
||||
}
|
||||
repo.decryptedSuccess = true
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
16
go.mod
16
go.mod
@ -49,15 +49,15 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7
|
||||
github.com/u2takey/ffmpeg-go v0.5.0
|
||||
github.com/upyun/go-sdk/v3 v3.0.4
|
||||
github.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5
|
||||
github.com/xhofe/tache v0.1.1
|
||||
golang.org/x/crypto v0.18.0
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e
|
||||
golang.org/x/crypto v0.19.0
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/net v0.20.0
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/oauth2 v0.16.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/appengine v1.6.8
|
||||
@ -204,11 +204,11 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
golang.org/x/arch v0.5.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/term v0.16.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/term v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.16.0 // indirect
|
||||
golang.org/x/tools v0.18.0 // indirect
|
||||
google.golang.org/api v0.134.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect
|
||||
google.golang.org/grpc v1.57.0 // indirect
|
||||
|
32
go.sum
32
go.sum
@ -447,8 +447,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca h1:I9rVnNXdIkij4UvMT7OmKhH9sOIvS8iXkxfPdnn9wQA=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7 h1:Jtcrb09q0AVWe3BGe8qtuuGxNSHWGkTWr43kHTJ+CpA=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA=
|
||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
|
||||
@ -505,10 +505,10 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE=
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
@ -526,16 +526,16 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -563,16 +563,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@ -596,8 +596,8 @@ golang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
|
||||
golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.134.0 h1:ktL4Goua+UBgoP1eL1/60LwZJqa1sIzkLmvoR3hR6Gw=
|
||||
|
@ -34,6 +34,9 @@ func initSettings() {
|
||||
// create or save setting
|
||||
for i := range initialSettingItems {
|
||||
item := &initialSettingItems[i]
|
||||
if item.PreDefault == "" {
|
||||
item.PreDefault = item.Value
|
||||
}
|
||||
// err
|
||||
stored, err := op.GetSettingItemByKey(item.Key)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@ -41,7 +44,7 @@ func initSettings() {
|
||||
continue
|
||||
}
|
||||
// save
|
||||
if stored != nil && item.Key != conf.VERSION && stored.Value != item.DeprecatedValue {
|
||||
if stored != nil && item.Key != conf.VERSION && stored.Value != item.PreDefault {
|
||||
item.Value = stored.Value
|
||||
}
|
||||
if stored == nil || *item != *stored {
|
||||
@ -97,7 +100,7 @@ func InitialSettings() []model.SettingItem {
|
||||
// preview settings
|
||||
{Key: conf.TextTypes, Value: "txt,htm,html,xml,java,properties,sql,js,md,json,conf,ini,vue,php,py,bat,gitignore,yml,go,sh,c,cpp,h,hpp,tsx,vtt,srt,ass,rs,lrc", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
{Key: conf.AudioTypes, Value: "mp3,flac,ogg,m4a,wav,opus,wma", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
{Key: conf.VideoTypes, Value: "mp4,mkv,avi,mov,rmvb,webm,flv", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
{Key: conf.VideoTypes, Value: "mp4,mkv,avi,mov,rmvb,webm,flv,m3u8", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
{Key: conf.ImageTypes, Value: "jpg,tiff,jpeg,png,gif,bmp,svg,ico,swf,webp", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
//{Key: conf.OfficeTypes, Value: "doc,docx,xls,xlsx,ppt,pptx", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
{Key: conf.ProxyTypes, Value: "m3u8", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
@ -128,7 +131,7 @@ func InitialSettings() []model.SettingItem {
|
||||
// global settings
|
||||
{Key: conf.HideFiles, Value: "/\\/README.md/i", Type: conf.TypeText, Group: model.GLOBAL},
|
||||
{Key: "package_download", Value: "true", Type: conf.TypeBool, Group: model.GLOBAL},
|
||||
{Key: conf.CustomizeHead, DeprecatedValue: `<script src="https://polyfill.io/v3/polyfill.min.js?features=String.prototype.replaceAll"></script>`, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
{Key: conf.CustomizeHead, PreDefault: `<script src="https://polyfill.io/v3/polyfill.min.js?features=String.prototype.replaceAll"></script>`, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
{Key: conf.CustomizeBody, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
{Key: conf.LinkExpiration, Value: "0", Type: conf.TypeNumber, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
{Key: conf.SignAll, Value: "true", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
@ -177,7 +180,6 @@ func InitialSettings() []model.SettingItem {
|
||||
{Key: conf.LdapLoginTips, Value: "login with ldap", Type: conf.TypeString, Group: model.LDAP, Flag: model.PUBLIC},
|
||||
|
||||
//s3 settings
|
||||
{Key: conf.S3Enabled, Value: "false", Type: conf.TypeBool, Group: model.S3, Flag: model.PRIVATE},
|
||||
{Key: conf.S3AccessKeyId, Value: "", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},
|
||||
{Key: conf.S3SecretAccessKey, Value: "", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},
|
||||
{Key: conf.S3Buckets, Value: "[]", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},
|
||||
|
@ -31,6 +31,7 @@ func initUser() {
|
||||
PwdHash: model.TwoHashPwd(adminPassword, salt),
|
||||
Role: model.ADMIN,
|
||||
BasePath: "/",
|
||||
Authn: "[]",
|
||||
}
|
||||
if err := op.CreateUser(admin); err != nil {
|
||||
panic(err)
|
||||
@ -53,6 +54,7 @@ func initUser() {
|
||||
BasePath: "/",
|
||||
Permission: 0,
|
||||
Disabled: true,
|
||||
Authn: "[]",
|
||||
}
|
||||
if err := db.CreateUser(guest); err != nil {
|
||||
utils.Log.Fatalf("[init user] Failed to create guest user: %v", err)
|
||||
@ -62,6 +64,7 @@ func initUser() {
|
||||
}
|
||||
}
|
||||
hashPwdForOldVersion()
|
||||
updateAuthnForOldVersion()
|
||||
}
|
||||
|
||||
func hashPwdForOldVersion() {
|
||||
@ -80,3 +83,19 @@ func hashPwdForOldVersion() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateAuthnForOldVersion() {
|
||||
users, _, err := op.GetUsers(1, -1)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("[update authn for old version] failed get users: %v", err)
|
||||
}
|
||||
for i := range users {
|
||||
user := users[i]
|
||||
if user.Authn == "" {
|
||||
user.Authn = "[]"
|
||||
if err := db.UpdateUser(&user); err != nil {
|
||||
utils.Log.Fatalf("[update authn for old version] failed update user: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/alist-org/alist/v3/cmd/flags"
|
||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
@ -63,6 +64,12 @@ type Cors struct {
|
||||
AllowHeaders []string `json:"allow_headers" env:"ALLOW_HEADERS"`
|
||||
}
|
||||
|
||||
type S3 struct {
|
||||
Enable bool `json:"enable" env:"ENABLE"`
|
||||
Port int `json:"port" env:"PORT"`
|
||||
SSL bool `json:"ssl" env:"SSL"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Force bool `json:"force" env:"FORCE"`
|
||||
SiteURL string `json:"site_url" env:"SITE_URL"`
|
||||
@ -81,6 +88,7 @@ type Config struct {
|
||||
TlsInsecureSkipVerify bool `json:"tls_insecure_skip_verify" env:"TLS_INSECURE_SKIP_VERIFY"`
|
||||
Tasks TasksConfig `json:"tasks" envPrefix:"TASKS_"`
|
||||
Cors Cors `json:"cors" envPrefix:"CORS_"`
|
||||
S3 S3 `json:"s3" envPrefix:"S3_"`
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
@ -142,5 +150,10 @@ func DefaultConfig() *Config {
|
||||
AllowMethods: []string{"*"},
|
||||
AllowHeaders: []string{"*"},
|
||||
},
|
||||
S3: S3{
|
||||
Enable: false,
|
||||
Port: 5246,
|
||||
SSL: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -85,10 +85,9 @@ const (
|
||||
LdapLoginTips = "ldap_login_tips"
|
||||
|
||||
//s3
|
||||
S3Enabled = "s3_enabled"
|
||||
S3Buckets = "s3_buckets"
|
||||
S3AccessKeyId = "s3_access_key_id"
|
||||
S3SecretAccessKey = "s3_secret_access_key"
|
||||
S3Buckets = "s3_buckets"
|
||||
|
||||
// qbittorrent
|
||||
QbittorrentUrl = "qbittorrent_url"
|
||||
|
@ -23,7 +23,7 @@ const (
|
||||
type SettingItem struct {
|
||||
Key string `json:"key" gorm:"primaryKey" binding:"required"` // unique key
|
||||
Value string `json:"value"` // value
|
||||
DeprecatedValue string `json:"deprecated_value" gorm:"-:all"` // deprecated value
|
||||
PreDefault string `json:"-" gorm:"-:all"` // deprecated value
|
||||
Help string `json:"help"` // help message
|
||||
Type string `json:"type"` // string, number, bool, select
|
||||
Options string `json:"options"` // values for select
|
||||
|
@ -41,6 +41,7 @@ func CreateUser(c *gin.Context) {
|
||||
}
|
||||
req.SetPassword(req.Password)
|
||||
req.Password = ""
|
||||
req.Authn = "[]"
|
||||
if err := op.CreateUser(&req); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
} else {
|
||||
|
@ -171,3 +171,8 @@ func Cors(r *gin.Engine) {
|
||||
config.AllowMethods = conf.Conf.Cors.AllowMethods
|
||||
r.Use(cors.New(config))
|
||||
}
|
||||
|
||||
func InitS3(e *gin.Engine) {
|
||||
Cors(e)
|
||||
S3Server(e.Group("/"))
|
||||
}
|
||||
|
22
server/s3.go
22
server/s3.go
@ -6,20 +6,25 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/alist-org/alist/v3/server/s3"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func S3(g *gin.RouterGroup) {
|
||||
if !setting.GetBool(conf.S3Enabled) {
|
||||
if !conf.Conf.S3.Enable {
|
||||
g.Any("/*path", func(c *gin.Context) {
|
||||
common.ErrorStrResp(c, "S3 server is not enabled", 403)
|
||||
})
|
||||
return
|
||||
}
|
||||
h, _ := s3.NewServer(context.Background(), []string{setting.GetStr(conf.S3AccessKeyId) + "," + setting.GetStr(conf.S3SecretAccessKey)})
|
||||
if conf.Conf.S3.Port != -1 {
|
||||
g.Any("/*path", func(c *gin.Context) {
|
||||
common.ErrorStrResp(c, "S3 server bound to single port", 403)
|
||||
})
|
||||
return
|
||||
}
|
||||
h, _ := s3.NewServer(context.Background())
|
||||
|
||||
g.Any("/*path", func(c *gin.Context) {
|
||||
adjustedPath := strings.TrimPrefix(c.Request.URL.Path, path.Join(conf.URL.Path, "/s3"))
|
||||
@ -27,3 +32,14 @@ func S3(g *gin.RouterGroup) {
|
||||
gin.WrapH(h)(c)
|
||||
})
|
||||
}
|
||||
|
||||
func S3Server(g *gin.RouterGroup) {
|
||||
if !conf.Conf.S3.Enable {
|
||||
g.Any("/*path", func(c *gin.Context) {
|
||||
common.ErrorStrResp(c, "S3 server is not enabled", 403)
|
||||
})
|
||||
return
|
||||
}
|
||||
h, _ := s3.NewServer(context.Background())
|
||||
g.Any("/*path", gin.WrapH(h))
|
||||
}
|
||||
|
@ -299,7 +299,7 @@ func (b *s3Backend) PutObject(
|
||||
Mimetype: meta["Content-Type"],
|
||||
}
|
||||
|
||||
err = fs.PutDirectly(ctx, path.Dir(reqPath), stream)
|
||||
err = fs.PutDirectly(ctx, reqPath, stream)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// Make a new S3 Server to serve the remote
|
||||
func NewServer(ctx context.Context, authpair []string) (h http.Handler, err error) {
|
||||
func NewServer(ctx context.Context) (h http.Handler, err error) {
|
||||
var newLogger logger
|
||||
faker := gofakes3.New(
|
||||
newBackend(),
|
||||
@ -19,7 +19,7 @@ func NewServer(ctx context.Context, authpair []string) (h http.Handler, err erro
|
||||
gofakes3.WithLogger(newLogger),
|
||||
gofakes3.WithRequestID(rand.Uint64()),
|
||||
gofakes3.WithoutVersioning(),
|
||||
gofakes3.WithV4Auth(authlistResolver(authpair)),
|
||||
gofakes3.WithV4Auth(authlistResolver()),
|
||||
gofakes3.WithIntegrityCheck(true), // Check Content-MD5 if supplied
|
||||
)
|
||||
|
||||
|
@ -5,7 +5,6 @@ package s3
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Mikubill/gofakes3"
|
||||
@ -15,7 +14,6 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
type Bucket struct {
|
||||
@ -150,15 +148,13 @@ func prefixParser(p *gofakes3.Prefix) (path, remaining string) {
|
||||
// }
|
||||
// }
|
||||
|
||||
func authlistResolver(list []string) map[string]string {
|
||||
func authlistResolver() map[string]string {
|
||||
s3accesskeyid := setting.GetStr(conf.S3AccessKeyId)
|
||||
s3secretaccesskey := setting.GetStr(conf.S3SecretAccessKey)
|
||||
if s3accesskeyid == "" && s3secretaccesskey == "" {
|
||||
return nil
|
||||
}
|
||||
authList := make(map[string]string)
|
||||
for _, v := range list {
|
||||
parts := strings.Split(v, ",")
|
||||
if len(parts) != 2 {
|
||||
utils.Log.Infof(fmt.Sprintf("Ignored: invalid auth pair %s", v))
|
||||
continue
|
||||
}
|
||||
authList[parts[0]] = parts[1]
|
||||
}
|
||||
authList[s3accesskeyid] = s3secretaccesskey
|
||||
return authList
|
||||
}
|
||||
|
Reference in New Issue
Block a user