Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
cf58ab3a78 | |||
33ba7f1521 | |||
201e25c17f | |||
ecefa5e0eb | |||
650b03aeb1 | |||
7341846499 | |||
a3908fd9a6 | |||
2a035302b2 | |||
016e169c41 | |||
088120df82 | |||
aa45a82914 | |||
5084d98398 | |||
fa15c576f0 | |||
2d3605c684 | |||
492b49d77a |
27
.github/workflows/beta_release.yml
vendored
27
.github/workflows/beta_release.yml
vendored
@ -111,14 +111,23 @@ jobs:
|
||||
name: Beta Release Desktop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peter-evans/create-or-update-comment@v4
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
issue-number: 69
|
||||
body: |
|
||||
/release-beta
|
||||
- triggered by @${{ github.actor }}
|
||||
- commit sha: ${{ github.sha }}
|
||||
- view files: https://github.com/alist-org/alist/tree/${{ github.sha }}
|
||||
reactions: 'rocket'
|
||||
token: ${{ secrets.MY_TOKEN }}
|
||||
repository: alist-org/desktop-release
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Commit
|
||||
run: |
|
||||
git config --local user.email "bot@nn.ci"
|
||||
git config --local user.name "IlaBot"
|
||||
git commit --allow-empty -m "Trigger build for ${{ github.sha }}"
|
||||
|
||||
- name: Push commit
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.MY_TOKEN }}
|
||||
branch: main
|
||||
repository: alist-org/desktop-release
|
@ -58,7 +58,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
||||
- [x] WebDav(Support OneDrive/SharePoint without API)
|
||||
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
||||
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
|
||||
- [x] [YandexDisk](https://disk.yandex.com/)
|
||||
- [x] [BaiduNetdisk](http://pan.baidu.com/)
|
||||
- [x] [Terabox](https://www.terabox.com/main)
|
||||
@ -98,7 +98,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
||||
|
||||
## Document
|
||||
|
||||
<https://alist.nn.ci/>
|
||||
<https://alistgo.com/>
|
||||
|
||||
## Demo
|
||||
|
||||
@ -138,4 +138,4 @@ The `AList` is open-source software licensed under the AGPL-3.0 license.
|
||||
|
||||
---
|
||||
|
||||
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||
> [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||
|
@ -58,7 +58,7 @@
|
||||
- [x] WebDav(支持无API的OneDrive/SharePoint)
|
||||
- [x] Teambition([中国](https://www.teambition.com/ ),[国际](https://us.teambition.com/ ))
|
||||
- [x] [分秒帧](https://www.mediatrack.cn/)
|
||||
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云)
|
||||
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云,共享群组)
|
||||
- [x] [Yandex.Disk](https://disk.yandex.com/)
|
||||
- [x] [百度网盘](http://pan.baidu.com/)
|
||||
- [x] [UC网盘](https://drive.uc.cn)
|
||||
|
@ -58,7 +58,7 @@
|
||||
- [x] WebDav(Support OneDrive/SharePoint without API)
|
||||
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
||||
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
|
||||
- [x] [YandexDisk](https://disk.yandex.com/)
|
||||
- [x] [BaiduNetdisk](http://pan.baidu.com/)
|
||||
- [x] [Terabox](https://www.terabox.com/main)
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
func Init() {
|
||||
bootstrap.InitConfig()
|
||||
bootstrap.Log()
|
||||
bootstrap.InitHostKey()
|
||||
bootstrap.InitDB()
|
||||
data.InitData()
|
||||
bootstrap.InitIndex()
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||
"github.com/KirCute/sftpd-alist"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -112,6 +114,42 @@ the address is defined in config file`,
|
||||
}
|
||||
}()
|
||||
}
|
||||
var ftpDriver *server.FtpMainDriver
|
||||
var ftpServer *ftpserver.FtpServer
|
||||
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable {
|
||||
var err error
|
||||
ftpDriver, err = server.NewMainDriver()
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to start ftp driver: %s", err.Error())
|
||||
} else {
|
||||
utils.Log.Infof("start ftp server on %s", conf.Conf.FTP.Listen)
|
||||
go func() {
|
||||
ftpServer = ftpserver.NewFtpServer(ftpDriver)
|
||||
err = ftpServer.ListenAndServe()
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("problem ftp server listening: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
var sftpDriver *server.SftpDriver
|
||||
var sftpServer *sftpd.SftpServer
|
||||
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable {
|
||||
var err error
|
||||
sftpDriver, err = server.NewSftpDriver()
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to start sftp driver: %s", err.Error())
|
||||
} else {
|
||||
utils.Log.Infof("start sftp server on %s", conf.Conf.SFTP.Listen)
|
||||
go func() {
|
||||
sftpServer = sftpd.NewSftpServer(sftpDriver)
|
||||
err = sftpServer.RunServer()
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("problem sftp server listening: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
// Wait for interrupt signal to gracefully shutdown the server with
|
||||
// a timeout of 1 second.
|
||||
quit := make(chan os.Signal, 1)
|
||||
@ -152,6 +190,25 @@ the address is defined in config file`,
|
||||
}
|
||||
}()
|
||||
}
|
||||
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable && ftpServer != nil && ftpDriver != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ftpDriver.Stop()
|
||||
if err := ftpServer.Stop(); err != nil {
|
||||
utils.Log.Fatal("FTP server shutdown err: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable && sftpServer != nil && sftpDriver != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := sftpServer.Close(); err != nil {
|
||||
utils.Log.Fatal("SFTP server shutdown err: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
utils.Log.Println("Server exit")
|
||||
},
|
||||
|
@ -27,8 +27,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
//var UserAgent = driver115.UA115Browser
|
||||
|
||||
// var UserAgent = driver115.UA115Browser
|
||||
func (d *Pan115) login() error {
|
||||
var err error
|
||||
opts := []driver115.Option{
|
||||
@ -46,7 +45,7 @@ func (d *Pan115) login() error {
|
||||
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
|
||||
return errors.Wrap(err, "failed to login by qrcode")
|
||||
}
|
||||
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
|
||||
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
|
||||
d.QRCodeToken = ""
|
||||
} else if d.Cookie != "" {
|
||||
if err = cr.FromCookie(d.Cookie); err != nil {
|
||||
|
@ -96,7 +96,7 @@ func (d *Pan115Share) login() error {
|
||||
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
|
||||
return errors.Wrap(err, "failed to login by qrcode")
|
||||
}
|
||||
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
|
||||
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
|
||||
d.QRCodeToken = ""
|
||||
} else if d.Cookie != "" {
|
||||
if err = cr.FromCookie(d.Cookie); err != nil {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -14,8 +15,9 @@ import (
|
||||
"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"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -56,6 +58,11 @@ func (d *Yun139) Init(ctx context.Context) error {
|
||||
d.RootFolderID = "root"
|
||||
}
|
||||
fallthrough
|
||||
case MetaGroup:
|
||||
if len(d.Addition.RootFolderID) == 0 {
|
||||
d.RootFolderID = d.CloudID
|
||||
}
|
||||
fallthrough
|
||||
case MetaFamily:
|
||||
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
|
||||
if err != nil {
|
||||
@ -96,6 +103,8 @@ func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
|
||||
return d.getFiles(dir.GetID())
|
||||
case MetaFamily:
|
||||
return d.familyGetFiles(dir.GetID())
|
||||
case MetaGroup:
|
||||
return d.groupGetFiles(dir.GetID())
|
||||
default:
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
@ -108,9 +117,11 @@ func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
case MetaPersonalNew:
|
||||
url, err = d.personalGetLink(file.GetID())
|
||||
case MetaPersonal:
|
||||
fallthrough
|
||||
case MetaFamily:
|
||||
url, err = d.getLink(file.GetID())
|
||||
case MetaFamily:
|
||||
url, err = d.familyGetLink(file.GetID(), file.GetPath())
|
||||
case MetaGroup:
|
||||
url, err = d.groupGetLink(file.GetID(), file.GetPath())
|
||||
default:
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
@ -154,8 +165,22 @@ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
|
||||
"accountType": 1,
|
||||
},
|
||||
"docLibName": dirName,
|
||||
"path": path.Join(parentDir.GetPath(), parentDir.GetID()),
|
||||
}
|
||||
pathname := "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc"
|
||||
pathname := "/orchestration/familyCloud-rebuild/cloudCatalog/v1.0/createCloudDoc"
|
||||
_, err = d.post(pathname, data, nil)
|
||||
case MetaGroup:
|
||||
data := base.Json{
|
||||
"catalogName": dirName,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
"groupID": d.CloudID,
|
||||
"parentFileId": parentDir.GetID(),
|
||||
"path": path.Join(parentDir.GetPath(), parentDir.GetID()),
|
||||
}
|
||||
pathname := "/orchestration/group-rebuild/catalog/v1.0/createGroupCatalog"
|
||||
_, err = d.post(pathname, data, nil)
|
||||
default:
|
||||
err = errs.NotImplement
|
||||
@ -176,6 +201,34 @@ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj,
|
||||
return nil, err
|
||||
}
|
||||
return srcObj, nil
|
||||
case MetaGroup:
|
||||
var contentList []string
|
||||
var catalogList []string
|
||||
if srcObj.IsDir() {
|
||||
catalogList = append(catalogList, srcObj.GetID())
|
||||
} else {
|
||||
contentList = append(contentList, srcObj.GetID())
|
||||
}
|
||||
data := base.Json{
|
||||
"taskType": 3,
|
||||
"srcType": 2,
|
||||
"srcGroupID": d.CloudID,
|
||||
"destType": 2,
|
||||
"destGroupID": d.CloudID,
|
||||
"destPath": dstDir.GetPath(),
|
||||
"contentList": contentList,
|
||||
"catalogList": catalogList,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/group-rebuild/task/v1.0/createBatchOprTask"
|
||||
_, err := d.post(pathname, data, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return srcObj, nil
|
||||
case MetaPersonal:
|
||||
var contentInfoList []string
|
||||
var catalogInfoList []string
|
||||
@ -246,6 +299,65 @@ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) e
|
||||
pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo"
|
||||
}
|
||||
_, err = d.post(pathname, data, nil)
|
||||
case MetaGroup:
|
||||
var data base.Json
|
||||
var pathname string
|
||||
if srcObj.IsDir() {
|
||||
data = base.Json{
|
||||
"groupID": d.CloudID,
|
||||
"modifyCatalogID": srcObj.GetID(),
|
||||
"modifyCatalogName": newName,
|
||||
"path": srcObj.GetPath(),
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname = "/orchestration/group-rebuild/catalog/v1.0/modifyGroupCatalog"
|
||||
} else {
|
||||
data = base.Json{
|
||||
"groupID": d.CloudID,
|
||||
"contentID": srcObj.GetID(),
|
||||
"contentName": newName,
|
||||
"path": srcObj.GetPath(),
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname = "/orchestration/group-rebuild/content/v1.0/modifyGroupContent"
|
||||
}
|
||||
_, err = d.post(pathname, data, nil)
|
||||
case MetaFamily:
|
||||
var data base.Json
|
||||
var pathname string
|
||||
if srcObj.IsDir() {
|
||||
// 网页接口不支持重命名家庭云文件夹
|
||||
// data = base.Json{
|
||||
// "catalogType": 3,
|
||||
// "catalogID": srcObj.GetID(),
|
||||
// "catalogName": newName,
|
||||
// "commonAccountInfo": base.Json{
|
||||
// "account": d.Account,
|
||||
// "accountType": 1,
|
||||
// },
|
||||
// "path": srcObj.GetPath(),
|
||||
// }
|
||||
// pathname = "/orchestration/familyCloud-rebuild/photoContent/v1.0/modifyCatalogInfo"
|
||||
return errs.NotImplement
|
||||
} else {
|
||||
data = base.Json{
|
||||
"contentID": srcObj.GetID(),
|
||||
"contentName": newName,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
"path": srcObj.GetPath(),
|
||||
}
|
||||
pathname = "/orchestration/familyCloud-rebuild/photoContent/v1.0/modifyContentInfo"
|
||||
}
|
||||
_, err = d.post(pathname, data, nil)
|
||||
default:
|
||||
err = errs.NotImplement
|
||||
}
|
||||
@ -303,6 +415,28 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
|
||||
pathname := "/hcy/recyclebin/batchTrash"
|
||||
_, err := d.personalPost(pathname, data, nil)
|
||||
return err
|
||||
case MetaGroup:
|
||||
var contentList []string
|
||||
var catalogList []string
|
||||
// 必须使用完整路径删除
|
||||
if obj.IsDir() {
|
||||
catalogList = append(catalogList, obj.GetPath())
|
||||
} else {
|
||||
contentList = append(contentList, path.Join(obj.GetPath(), obj.GetID()))
|
||||
}
|
||||
data := base.Json{
|
||||
"taskType": 2,
|
||||
"srcGroupID": d.CloudID,
|
||||
"contentList": contentList,
|
||||
"catalogList": catalogList,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/group-rebuild/task/v1.0/createBatchOprTask"
|
||||
_, err := d.post(pathname, data, nil)
|
||||
return err
|
||||
case MetaPersonal:
|
||||
fallthrough
|
||||
case MetaFamily:
|
||||
@ -337,10 +471,12 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
"sourceCloudID": d.CloudID,
|
||||
"sourceCatalogType": 1002,
|
||||
"taskType": 2,
|
||||
"path": obj.GetPath(),
|
||||
}
|
||||
pathname = "/orchestration/familyCloud/batchOprTask/v1.0/createBatchOprTask"
|
||||
pathname = "/orchestration/familyCloud-rebuild/batchOprTask/v1.0/createBatchOprTask"
|
||||
}
|
||||
_, err := d.post(pathname, data, nil)
|
||||
return err
|
||||
@ -357,7 +493,10 @@ const (
|
||||
TB
|
||||
)
|
||||
|
||||
func getPartSize(size int64) int64 {
|
||||
func (d *Yun139) getPartSize(size int64) int64 {
|
||||
if d.CustomUploadPartSize != 0 {
|
||||
return d.CustomUploadPartSize
|
||||
}
|
||||
// 网盘对于分片数量存在上限
|
||||
if size/GB > 30 {
|
||||
return 512 * MB
|
||||
@ -380,19 +519,46 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
return err
|
||||
}
|
||||
}
|
||||
// return errs.NotImplement
|
||||
|
||||
partInfos := []PartInfo{}
|
||||
var partSize = d.getPartSize(stream.GetSize())
|
||||
part := (stream.GetSize() + partSize - 1) / partSize
|
||||
if part == 0 {
|
||||
part = 1
|
||||
}
|
||||
for i := int64(0); i < part; i++ {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
start := i * partSize
|
||||
byteSize := stream.GetSize() - start
|
||||
if byteSize > partSize {
|
||||
byteSize = partSize
|
||||
}
|
||||
partNumber := i + 1
|
||||
partInfo := PartInfo{
|
||||
PartNumber: partNumber,
|
||||
PartSize: byteSize,
|
||||
ParallelHashCtx: ParallelHashCtx{
|
||||
PartOffset: start,
|
||||
},
|
||||
}
|
||||
partInfos = append(partInfos, partInfo)
|
||||
}
|
||||
|
||||
// 筛选出前 100 个 partInfos
|
||||
firstPartInfos := partInfos
|
||||
if len(firstPartInfos) > 100 {
|
||||
firstPartInfos = firstPartInfos[:100]
|
||||
}
|
||||
|
||||
// 获取上传信息和前100个分片的上传地址
|
||||
data := base.Json{
|
||||
"contentHash": fullHash,
|
||||
"contentHashAlgorithm": "SHA256",
|
||||
"contentType": "application/octet-stream",
|
||||
"parallelUpload": false,
|
||||
"partInfos": []base.Json{{
|
||||
"parallelHashCtx": base.Json{
|
||||
"partOffset": 0,
|
||||
},
|
||||
"partNumber": 1,
|
||||
"partSize": stream.GetSize(),
|
||||
}},
|
||||
"partInfos": firstPartInfos,
|
||||
"size": stream.GetSize(),
|
||||
"parentFileId": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
@ -410,33 +576,68 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
return nil
|
||||
}
|
||||
|
||||
uploadPartInfos := resp.Data.PartInfos
|
||||
|
||||
// 获取后续分片的上传地址
|
||||
for i := 101; i < len(partInfos); i += 100 {
|
||||
end := i + 100
|
||||
if end > len(partInfos) {
|
||||
end = len(partInfos)
|
||||
}
|
||||
batchPartInfos := partInfos[i:end]
|
||||
|
||||
moredata := base.Json{
|
||||
"fileId": resp.Data.FileId,
|
||||
"uploadId": resp.Data.UploadId,
|
||||
"partInfos": batchPartInfos,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname := "/hcy/file/getUploadUrl"
|
||||
var moreresp PersonalUploadUrlResp
|
||||
_, err = d.personalPost(pathname, moredata, &moreresp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadPartInfos = append(uploadPartInfos, moreresp.Data.PartInfos...)
|
||||
}
|
||||
|
||||
// Progress
|
||||
p := driver.NewProgress(stream.GetSize(), up)
|
||||
|
||||
// Update Progress
|
||||
r := io.TeeReader(stream, p)
|
||||
// 上传所有分片
|
||||
for _, uploadPartInfo := range uploadPartInfos {
|
||||
index := uploadPartInfo.PartNumber - 1
|
||||
partSize := partInfos[index].PartSize
|
||||
log.Debugf("[139] uploading part %+v/%+v", index, len(uploadPartInfos))
|
||||
limitReader := io.LimitReader(stream, partSize)
|
||||
|
||||
req, err := http.NewRequest("PUT", resp.Data.PartInfos[0].UploadUrl, r)
|
||||
// Update Progress
|
||||
r := io.TeeReader(limitReader, p)
|
||||
|
||||
req, err := http.NewRequest("PUT", uploadPartInfo.UploadUrl, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Length", fmt.Sprint(stream.GetSize()))
|
||||
req.Header.Set("Content-Length", fmt.Sprint(partSize))
|
||||
req.Header.Set("Origin", "https://yun.139.com")
|
||||
req.Header.Set("Referer", "https://yun.139.com/")
|
||||
req.ContentLength = stream.GetSize()
|
||||
req.ContentLength = partSize
|
||||
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = res.Body.Close()
|
||||
log.Debugf("%+v", res)
|
||||
log.Debugf("[139] uploaded: %+v", res)
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
data = base.Json{
|
||||
"contentHash": fullHash,
|
||||
@ -471,21 +672,20 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
|
||||
if d.isFamily() {
|
||||
// data = d.newJson(base.Json{
|
||||
// "fileCount": 1,
|
||||
// "manualRename": 2,
|
||||
// "operation": 0,
|
||||
// "path": "",
|
||||
// "seqNo": "",
|
||||
// "totalSize": 0,
|
||||
// "uploadContentList": []base.Json{{
|
||||
// "contentName": stream.GetName(),
|
||||
// "contentSize": 0,
|
||||
// // "digest": "5a3231986ce7a6b46e408612d385bafa"
|
||||
// }},
|
||||
// })
|
||||
// pathname = "/orchestration/familyCloud/content/v1.0/getFileUploadURL"
|
||||
return errs.NotImplement
|
||||
data = d.newJson(base.Json{
|
||||
"fileCount": 1,
|
||||
"manualRename": 2,
|
||||
"operation": 0,
|
||||
"path": path.Join(dstDir.GetPath(), dstDir.GetID()),
|
||||
"seqNo": random.String(32), //序列号不能为空
|
||||
"totalSize": 0,
|
||||
"uploadContentList": []base.Json{{
|
||||
"contentName": stream.GetName(),
|
||||
"contentSize": 0,
|
||||
// "digest": "5a3231986ce7a6b46e408612d385bafa"
|
||||
}},
|
||||
})
|
||||
pathname = "/orchestration/familyCloud-rebuild/content/v1.0/getFileUploadURL"
|
||||
}
|
||||
var resp UploadResp
|
||||
_, err := d.post(pathname, data, &resp)
|
||||
@ -496,7 +696,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
// Progress
|
||||
p := driver.NewProgress(stream.GetSize(), up)
|
||||
|
||||
var partSize = getPartSize(stream.GetSize())
|
||||
var partSize = d.getPartSize(stream.GetSize())
|
||||
part := (stream.GetSize() + partSize - 1) / partSize
|
||||
if part == 0 {
|
||||
part = 1
|
||||
|
@ -11,6 +11,7 @@ type Addition struct {
|
||||
driver.RootID
|
||||
Type string `json:"type" type:"select" options:"personal,family,personal_new" default:"personal"`
|
||||
CloudID string `json:"cloud_id"`
|
||||
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
const (
|
||||
MetaPersonal string = "personal"
|
||||
MetaFamily string = "family"
|
||||
MetaGroup string = "group"
|
||||
MetaPersonalNew string = "personal_new"
|
||||
)
|
||||
|
||||
@ -54,6 +55,7 @@ type Content struct {
|
||||
//ContentDesc string `json:"contentDesc"`
|
||||
//ContentType int `json:"contentType"`
|
||||
//ContentOrigin int `json:"contentOrigin"`
|
||||
CreateTime string `json:"createTime"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
//CommentCount int `json:"commentCount"`
|
||||
ThumbnailURL string `json:"thumbnailURL"`
|
||||
@ -196,6 +198,37 @@ type QueryContentListResp struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type QueryGroupContentListResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
Result struct {
|
||||
ResultCode string `json:"resultCode"`
|
||||
ResultDesc string `json:"resultDesc"`
|
||||
} `json:"result"`
|
||||
GetGroupContentResult struct {
|
||||
ParentCatalogID string `json:"parentCatalogID"` // 根目录是"0"
|
||||
CatalogList []struct {
|
||||
Catalog
|
||||
Path string `json:"path"`
|
||||
} `json:"catalogList"`
|
||||
ContentList []Content `json:"contentList"`
|
||||
NodeCount int `json:"nodeCount"` // 文件+文件夹数量
|
||||
CtlgCnt int `json:"ctlgCnt"` // 文件夹数量
|
||||
ContCnt int `json:"contCnt"` // 文件数量
|
||||
} `json:"getGroupContentResult"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ParallelHashCtx struct {
|
||||
PartOffset int64 `json:"partOffset"`
|
||||
}
|
||||
|
||||
type PartInfo struct {
|
||||
PartNumber int64 `json:"partNumber"`
|
||||
PartSize int64 `json:"partSize"`
|
||||
ParallelHashCtx ParallelHashCtx `json:"parallelHashCtx"`
|
||||
}
|
||||
|
||||
type PersonalThumbnail struct {
|
||||
Style string `json:"style"`
|
||||
Url string `json:"url"`
|
||||
@ -235,6 +268,15 @@ type PersonalUploadResp struct {
|
||||
}
|
||||
}
|
||||
|
||||
type PersonalUploadUrlResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
FileId string `json:"fileId"`
|
||||
UploadId string `json:"uploadId"`
|
||||
PartInfos []PersonalPartInfo `json:"partInfos"`
|
||||
}
|
||||
}
|
||||
|
||||
type RefreshTokenResp struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Return string `xml:"return"`
|
||||
|
@ -13,9 +13,9 @@ import (
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"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"
|
||||
@ -220,10 +220,11 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
||||
"sortDirection": 1,
|
||||
})
|
||||
var resp QueryContentListResp
|
||||
_, err := d.post("/orchestration/familyCloud/content/v1.0/queryContentList", data, &resp)
|
||||
_, err := d.post("/orchestration/familyCloud-rebuild/content/v1.2/queryContentList", data, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := resp.Data.Path
|
||||
for _, catalog := range resp.Data.CloudCatalogList {
|
||||
f := model.Object{
|
||||
ID: catalog.CatalogID,
|
||||
@ -232,6 +233,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
||||
IsFolder: true,
|
||||
Modified: getTime(catalog.LastUpdateTime),
|
||||
Ctime: getTime(catalog.CreateTime),
|
||||
Path: path, // 文件夹上一级的Path
|
||||
}
|
||||
files = append(files, &f)
|
||||
}
|
||||
@ -243,6 +245,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
||||
Size: content.ContentSize,
|
||||
Modified: getTime(content.LastUpdateTime),
|
||||
Ctime: getTime(content.CreateTime),
|
||||
Path: path, // 文件所在目录的Path
|
||||
},
|
||||
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
|
||||
//Thumbnail: content.BigthumbnailURL,
|
||||
@ -257,6 +260,61 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *Yun139) groupGetFiles(catalogID string) ([]model.Obj, error) {
|
||||
pageNum := 1
|
||||
files := make([]model.Obj, 0)
|
||||
for {
|
||||
data := d.newJson(base.Json{
|
||||
"groupID": d.CloudID,
|
||||
"catalogID": catalogID,
|
||||
"contentSortType": 0,
|
||||
"sortDirection": 1,
|
||||
"startNumber": pageNum,
|
||||
"endNumber": pageNum + 99,
|
||||
"path": catalogID,
|
||||
})
|
||||
|
||||
var resp QueryGroupContentListResp
|
||||
_, err := d.post("/orchestration/group-rebuild/content/v1.0/queryGroupContentList", data, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := resp.Data.GetGroupContentResult.ParentCatalogID
|
||||
for _, catalog := range resp.Data.GetGroupContentResult.CatalogList {
|
||||
f := model.Object{
|
||||
ID: catalog.CatalogID,
|
||||
Name: catalog.CatalogName,
|
||||
Size: 0,
|
||||
IsFolder: true,
|
||||
Modified: getTime(catalog.UpdateTime),
|
||||
Ctime: getTime(catalog.CreateTime),
|
||||
Path: catalog.Path, // 文件夹的真实Path, root:/开头
|
||||
}
|
||||
files = append(files, &f)
|
||||
}
|
||||
for _, content := range resp.Data.GetGroupContentResult.ContentList {
|
||||
f := model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: content.ContentID,
|
||||
Name: content.ContentName,
|
||||
Size: content.ContentSize,
|
||||
Modified: getTime(content.UpdateTime),
|
||||
Ctime: getTime(content.CreateTime),
|
||||
Path: path, // 文件所在目录的Path
|
||||
},
|
||||
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
|
||||
//Thumbnail: content.BigthumbnailURL,
|
||||
}
|
||||
files = append(files, &f)
|
||||
}
|
||||
if pageNum > resp.Data.GetGroupContentResult.NodeCount {
|
||||
break
|
||||
}
|
||||
pageNum = pageNum + 100
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *Yun139) getLink(contentId string) (string, error) {
|
||||
data := base.Json{
|
||||
"appName": "",
|
||||
@ -273,6 +331,32 @@ func (d *Yun139) getLink(contentId string) (string, error) {
|
||||
}
|
||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||
}
|
||||
func (d *Yun139) familyGetLink(contentId string, path string) (string, error) {
|
||||
data := d.newJson(base.Json{
|
||||
"contentID": contentId,
|
||||
"path": path,
|
||||
})
|
||||
res, err := d.post("/orchestration/familyCloud-rebuild/content/v1.0/getFileDownLoadURL",
|
||||
data, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||
}
|
||||
|
||||
func (d *Yun139) groupGetLink(contentId string, path string) (string, error) {
|
||||
data := d.newJson(base.Json{
|
||||
"contentID": contentId,
|
||||
"groupID": d.CloudID,
|
||||
"path": path,
|
||||
})
|
||||
res, err := d.post("/orchestration/group-rebuild/groupManage/v1.0/getGroupFileDownLoadURL",
|
||||
data, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||
}
|
||||
|
||||
func unicode(str string) string {
|
||||
textQuoted := strconv.QuoteToASCII(str)
|
||||
|
@ -27,7 +27,7 @@ type BaiduPhoto struct {
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
AccessToken string
|
||||
// AccessToken string
|
||||
Uk int64
|
||||
root model.Obj
|
||||
|
||||
@ -48,9 +48,9 @@ func (d *BaiduPhoto) Init(ctx context.Context) error {
|
||||
d.uploadThread, d.UploadThread = 3, "3"
|
||||
}
|
||||
|
||||
if err := d.refreshToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
// if err := d.refreshToken(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// root
|
||||
if d.AlbumID != "" {
|
||||
@ -82,7 +82,7 @@ func (d *BaiduPhoto) GetRoot(ctx context.Context) (model.Obj, error) {
|
||||
}
|
||||
|
||||
func (d *BaiduPhoto) Drop(ctx context.Context) error {
|
||||
d.AccessToken = ""
|
||||
// d.AccessToken = ""
|
||||
d.Uk = 0
|
||||
d.root = nil
|
||||
return nil
|
||||
@ -140,14 +140,13 @@ func (d *BaiduPhoto) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
||||
// 处理共享相册
|
||||
if d.Uk != file.Uk {
|
||||
// 有概率无法获取到链接
|
||||
return d.linkAlbum(ctx, file, args)
|
||||
// return d.linkAlbum(ctx, file, args)
|
||||
|
||||
// 接口被限制,只能使用cookie
|
||||
// f, err := d.CopyAlbumFile(ctx, file)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return d.linkFile(ctx, f, args)
|
||||
f, err := d.CopyAlbumFile(ctx, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.linkFile(ctx, f, args)
|
||||
}
|
||||
return d.linkFile(ctx, &file.File, args)
|
||||
}
|
||||
@ -292,7 +291,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
|
||||
}
|
||||
|
||||
// 尝试获取之前的进度
|
||||
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, d.AccessToken, contentMd5)
|
||||
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, strconv.FormatInt(d.Uk, 10), contentMd5)
|
||||
if !ok {
|
||||
_, err = d.Post(FILE_API_URL_V1+"/precreate", func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
@ -343,7 +342,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
|
||||
if err = threadG.Wait(); err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
precreateResp.BlockList = utils.SliceFilter(precreateResp.BlockList, func(s int) bool { return s >= 0 })
|
||||
base.SaveUploadProgress(d, precreateResp, d.AccessToken, contentMd5)
|
||||
base.SaveUploadProgress(d, strconv.FormatInt(d.Uk, 10), contentMd5)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
@ -6,13 +6,14 @@ import (
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
// RefreshToken string `json:"refresh_token" required:"true"`
|
||||
Cookie string `json:"cookie" required:"true"`
|
||||
ShowType string `json:"show_type" type:"select" options:"root,root_only_album,root_only_file" default:"root"`
|
||||
AlbumID string `json:"album_id"`
|
||||
//AlbumPassword string `json:"album_password"`
|
||||
DeleteOrigin bool `json:"delete_origin"`
|
||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
||||
// ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
||||
// ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,7 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
@ -27,7 +25,8 @@ const (
|
||||
|
||||
func (d *BaiduPhoto) Request(client *resty.Client, furl string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
||||
req := client.R().
|
||||
SetQueryParam("access_token", d.AccessToken)
|
||||
// SetQueryParam("access_token", d.AccessToken)
|
||||
SetHeader("Cookie", d.Cookie)
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
@ -49,10 +48,10 @@ func (d *BaiduPhoto) Request(client *resty.Client, furl string, method string, c
|
||||
return nil, fmt.Errorf("no shared albums found")
|
||||
case 50100:
|
||||
return nil, fmt.Errorf("illegal title, only supports 50 characters")
|
||||
case -6:
|
||||
if err = d.refreshToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// case -6:
|
||||
// if err = d.refreshToken(); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
default:
|
||||
return nil, fmt.Errorf("errno: %d, refer to https://photo.baidu.com/union/doc", erron)
|
||||
}
|
||||
@ -67,29 +66,29 @@ func (d *BaiduPhoto) Request(client *resty.Client, furl string, method string, c
|
||||
// return res.Body(), nil
|
||||
//}
|
||||
|
||||
func (d *BaiduPhoto) refreshToken() error {
|
||||
u := "https://openapi.baidu.com/oauth/2.0/token"
|
||||
var resp base.TokenResp
|
||||
var e TokenErrResp
|
||||
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": d.RefreshToken,
|
||||
"client_id": d.ClientID,
|
||||
"client_secret": d.ClientSecret,
|
||||
}).Get(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.ErrorMsg != "" {
|
||||
return &e
|
||||
}
|
||||
if resp.RefreshToken == "" {
|
||||
return errs.EmptyToken
|
||||
}
|
||||
d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
// func (d *BaiduPhoto) refreshToken() error {
|
||||
// u := "https://openapi.baidu.com/oauth/2.0/token"
|
||||
// var resp base.TokenResp
|
||||
// var e TokenErrResp
|
||||
// _, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
|
||||
// "grant_type": "refresh_token",
|
||||
// "refresh_token": d.RefreshToken,
|
||||
// "client_id": d.ClientID,
|
||||
// "client_secret": d.ClientSecret,
|
||||
// }).Get(u)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if e.ErrorMsg != "" {
|
||||
// return &e
|
||||
// }
|
||||
// if resp.RefreshToken == "" {
|
||||
// return errs.EmptyToken
|
||||
// }
|
||||
// d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken
|
||||
// op.MustSaveDriverStorage(d)
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (d *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
||||
return d.Request(base.RestyClient, furl, http.MethodGet, callback, resp)
|
||||
@ -363,10 +362,6 @@ func (d *BaiduPhoto) linkAlbum(ctx context.Context, file *AlbumFile, args model.
|
||||
|
||||
location := resp.Header().Get("Location")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
link := &model.Link{
|
||||
URL: location,
|
||||
Header: http.Header{
|
||||
@ -388,36 +383,36 @@ func (d *BaiduPhoto) linkFile(ctx context.Context, file *File, args model.LinkAr
|
||||
headers["X-Forwarded-For"] = args.IP
|
||||
}
|
||||
|
||||
// var downloadUrl struct {
|
||||
// Dlink string `json:"dlink"`
|
||||
// }
|
||||
// _, err := d.Get(FILE_API_URL_V1+"/download", func(r *resty.Request) {
|
||||
// r.SetContext(ctx)
|
||||
// r.SetHeaders(headers)
|
||||
// r.SetQueryParams(map[string]string{
|
||||
// "fsid": fmt.Sprint(file.Fsid),
|
||||
// })
|
||||
// }, &downloadUrl)
|
||||
|
||||
resp, err := d.Request(base.NoRedirectClient, FILE_API_URL_V1+"/download", http.MethodHead, func(r *resty.Request) {
|
||||
var downloadUrl struct {
|
||||
Dlink string `json:"dlink"`
|
||||
}
|
||||
_, err := d.Get(FILE_API_URL_V2+"/download", func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetHeaders(headers)
|
||||
r.SetQueryParams(map[string]string{
|
||||
"fsid": fmt.Sprint(file.Fsid),
|
||||
})
|
||||
}, nil)
|
||||
}, &downloadUrl)
|
||||
|
||||
// resp, err := d.Request(base.NoRedirectClient, FILE_API_URL_V1+"/download", http.MethodHead, func(r *resty.Request) {
|
||||
// r.SetContext(ctx)
|
||||
// r.SetHeaders(headers)
|
||||
// r.SetQueryParams(map[string]string{
|
||||
// "fsid": fmt.Sprint(file.Fsid),
|
||||
// })
|
||||
// }, nil)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != 302 {
|
||||
return nil, fmt.Errorf("not found 302 redirect")
|
||||
}
|
||||
// if resp.StatusCode() != 302 {
|
||||
// return nil, fmt.Errorf("not found 302 redirect")
|
||||
// }
|
||||
|
||||
location := resp.Header().Get("Location")
|
||||
// location := resp.Header().Get("Location")
|
||||
link := &model.Link{
|
||||
URL: location,
|
||||
URL: downloadUrl.Dlink,
|
||||
Header: http.Header{
|
||||
"User-Agent": []string{headers["User-Agent"]},
|
||||
"Referer": []string{"https://photo.baidu.com/"},
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
|
||||
"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/go-resty/resty/v2"
|
||||
@ -134,6 +135,8 @@ func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
||||
if io.ReadCloser(stream) == http.NoBody {
|
||||
return d.create(ctx, dstDir, stream)
|
||||
}
|
||||
|
||||
// 获取存储策略
|
||||
var r DirectoryResp
|
||||
err := d.request(http.MethodGet, "/directory"+dstDir.GetPath(), nil, &r)
|
||||
if err != nil {
|
||||
@ -146,6 +149,8 @@ func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
||||
"policy_id": r.Policy.Id,
|
||||
"last_modified": stream.ModTime().Unix(),
|
||||
}
|
||||
|
||||
// 获取上传会话信息
|
||||
var u UploadInfo
|
||||
err = d.request(http.MethodPut, "/file/upload", func(req *resty.Request) {
|
||||
req.SetBody(uploadBody)
|
||||
@ -153,6 +158,14 @@ func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 根据存储方式选择分片上传的方法
|
||||
switch r.Policy.Type {
|
||||
case "onedrive":
|
||||
err = d.upOneDrive(ctx, stream, u, up)
|
||||
case "remote": // 从机存储
|
||||
err = d.upRemote(ctx, stream, u, up)
|
||||
case "local": // 本机存储
|
||||
var chunkSize = u.ChunkSize
|
||||
var buf []byte
|
||||
var chunk int
|
||||
@ -166,7 +179,6 @@ func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
@ -180,9 +192,16 @@ func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
||||
break
|
||||
}
|
||||
chunk++
|
||||
|
||||
}
|
||||
default:
|
||||
err = errs.NotImplement
|
||||
}
|
||||
if err != nil {
|
||||
// 删除失败的会话
|
||||
err = d.request(http.MethodDelete, "/file/upload/"+u.SessionID, nil, nil)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Cloudreve) create(ctx context.Context, dir model.Obj, file model.Obj) error {
|
||||
|
@ -24,6 +24,8 @@ type UploadInfo struct {
|
||||
SessionID string `json:"sessionID"`
|
||||
ChunkSize int `json:"chunkSize"`
|
||||
Expires int `json:"expires"`
|
||||
UploadURLs []string `json:"uploadURLs"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
}
|
||||
|
||||
type DirectoryResp struct {
|
||||
|
@ -1,16 +1,23 @@
|
||||
package cloudreve
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/cookie"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
json "github.com/json-iterator/go"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
@ -172,3 +179,95 @@ func (d *Cloudreve) GetThumb(file Object) (model.Thumbnail, error) {
|
||||
Thumbnail: resp.Header().Get("Location"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Cloudreve) upRemote(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {
|
||||
uploadUrl := u.UploadURLs[0]
|
||||
credential := u.Credential
|
||||
var finish int64 = 0
|
||||
var chunk int = 0
|
||||
DEFAULT := int64(u.ChunkSize)
|
||||
for finish < stream.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
utils.Log.Debugf("[Cloudreve-Remote] upload: %d", finish)
|
||||
var byteSize = DEFAULT
|
||||
left := stream.GetSize() - finish
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(stream, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("POST", uploadUrl+"?chunk="+strconv.Itoa(chunk), bytes.NewBuffer(byteData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
||||
req.Header.Set("Authorization", fmt.Sprint(credential))
|
||||
finish += byteSize
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res.Body.Close()
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
chunk++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Cloudreve) upOneDrive(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {
|
||||
uploadUrl := u.UploadURLs[0]
|
||||
var finish int64 = 0
|
||||
DEFAULT := int64(u.ChunkSize)
|
||||
for finish < stream.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
utils.Log.Debugf("[Cloudreve-OneDrive] upload: %d", finish)
|
||||
var byteSize = DEFAULT
|
||||
left := stream.GetSize() - finish
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(stream, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("PUT", uploadUrl, bytes.NewBuffer(byteData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
||||
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
|
||||
finish += byteSize
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
|
||||
if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 {
|
||||
data, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(string(data))
|
||||
}
|
||||
res.Body.Close()
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
}
|
||||
// 上传成功发送回调请求
|
||||
err := d.request(http.MethodPost, "/callback/onedrive/finish/"+u.SessionID, func(req *resty.Request) {
|
||||
req.SetBody("{}")
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ func (d *Onedrive) Request(url string, method string, callback base.ReqCallback,
|
||||
|
||||
func (d *Onedrive) getFiles(path string) ([]File, error) {
|
||||
var res []File
|
||||
nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,fileSystemInfo,content.downloadUrl,file,parentReference"
|
||||
nextLink := d.GetMetaUrl(false, path) + "/children?$top=1000&$expand=thumbnails($select=medium)&$select=id,name,size,fileSystemInfo,content.downloadUrl,file,parentReference"
|
||||
for nextLink != "" {
|
||||
var files Files
|
||||
_, err := d.Request(nextLink, http.MethodGet, nil, &files)
|
||||
|
@ -118,7 +118,7 @@ func (d *OnedriveAPP) Request(url string, method string, callback base.ReqCallba
|
||||
|
||||
func (d *OnedriveAPP) getFiles(path string) ([]File, error) {
|
||||
var res []File
|
||||
nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,lastModifiedDateTime,content.downloadUrl,file,parentReference"
|
||||
nextLink := d.GetMetaUrl(false, path) + "/children?$top=1000&$expand=thumbnails($select=medium)&$select=id,name,size,lastModifiedDateTime,content.downloadUrl,file,parentReference"
|
||||
for nextLink != "" {
|
||||
var files Files
|
||||
_, err := d.Request(nextLink, http.MethodGet, nil, &files)
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -24,7 +23,6 @@ type PikPak struct {
|
||||
*Common
|
||||
RefreshToken string
|
||||
AccessToken string
|
||||
oauth2Token oauth2.TokenSource
|
||||
}
|
||||
|
||||
func (d *PikPak) Config() driver.Config {
|
||||
@ -84,41 +82,16 @@ func (d *PikPak) Init(ctx context.Context) (err error) {
|
||||
d.Addition.DeviceID = d.Common.DeviceID
|
||||
op.MustSaveDriverStorage(d)
|
||||
}
|
||||
// 初始化 oauth2Config
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: d.ClientID,
|
||||
ClientSecret: d.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://user.mypikpak.net/v1/auth/signin",
|
||||
TokenURL: "https://user.mypikpak.net/v1/auth/token",
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
},
|
||||
}
|
||||
|
||||
// 如果已经有RefreshToken,直接获取AccessToken
|
||||
if d.Addition.RefreshToken != "" {
|
||||
if d.RefreshTokenMethod == "oauth2" {
|
||||
// 使用 oauth2 刷新令牌
|
||||
// 初始化 oauth2Token
|
||||
d.initializeOAuth2Token(ctx, oauth2Config, d.Addition.RefreshToken)
|
||||
if err := d.refreshTokenByOAuth2(); err != nil {
|
||||
if err = d.refreshToken(d.Addition.RefreshToken); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := d.refreshToken(d.Addition.RefreshToken); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// 如果没有填写RefreshToken,尝试登录 获取 refreshToken
|
||||
if err := d.login(); err != nil {
|
||||
if err = d.login(); err != nil {
|
||||
return err
|
||||
}
|
||||
if d.RefreshTokenMethod == "oauth2" {
|
||||
d.initializeOAuth2Token(ctx, oauth2Config, d.RefreshToken)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 获取CaptchaToken
|
||||
|
@ -11,7 +11,6 @@ type Addition struct {
|
||||
Password string `json:"password" required:"true"`
|
||||
Platform string `json:"platform" required:"true" default:"web" type:"select" options:"android,web,pc"`
|
||||
RefreshToken string `json:"refresh_token" required:"true" default:""`
|
||||
RefreshTokenMethod string `json:"refresh_token_method" required:"true" type:"select" options:"oauth2,http"`
|
||||
CaptchaToken string `json:"captcha_token" default:""`
|
||||
DeviceID string `json:"device_id" required:"false" default:""`
|
||||
DisableMediaLink bool `json:"disable_media_link" default:"true"`
|
||||
|
@ -2,7 +2,6 @@ package pikpak
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
@ -14,7 +13,6 @@ import (
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/oauth2"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
@ -27,8 +25,6 @@ import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
var AndroidAlgorithms = []string{
|
||||
"7xOq4Z8s",
|
||||
"QE9/9+IQco",
|
||||
@ -171,30 +167,6 @@ func (d *PikPak) refreshToken(refreshToken string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) initializeOAuth2Token(ctx context.Context, oauth2Config *oauth2.Config, refreshToken string) {
|
||||
d.oauth2Token = oauth2.ReuseTokenSource(nil, utils.TokenSource(func() (*oauth2.Token, error) {
|
||||
return oauth2Config.TokenSource(ctx, &oauth2.Token{
|
||||
RefreshToken: refreshToken,
|
||||
}).Token()
|
||||
}))
|
||||
}
|
||||
|
||||
func (d *PikPak) refreshTokenByOAuth2() error {
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Status = "work"
|
||||
d.RefreshToken = token.RefreshToken
|
||||
d.AccessToken = token.AccessToken
|
||||
// 获取用户ID
|
||||
userID := token.Extra("sub").(string)
|
||||
d.Common.SetUserID(userID)
|
||||
d.Addition.RefreshToken = d.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
@ -203,14 +175,7 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
|
||||
"X-Device-ID": d.GetDeviceID(),
|
||||
"X-Captcha-Token": d.GetCaptchaToken(),
|
||||
})
|
||||
if d.RefreshTokenMethod == "oauth2" && d.oauth2Token != nil {
|
||||
// 使用oauth2 获取 access_token
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
|
||||
} else if d.AccessToken != "" {
|
||||
if d.AccessToken != "" {
|
||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||
}
|
||||
|
||||
@ -232,16 +197,9 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
|
||||
return res.Body(), nil
|
||||
case 4122, 4121, 16:
|
||||
// access_token 过期
|
||||
if d.RefreshTokenMethod == "oauth2" {
|
||||
if err1 := d.refreshTokenByOAuth2(); err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
} else {
|
||||
if err1 := d.refreshToken(d.RefreshToken); err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
}
|
||||
|
||||
return d.request(url, method, callback, resp)
|
||||
case 9: // 验证码token过期
|
||||
if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.GetUserID()); err != nil {
|
||||
|
21
go.mod
21
go.mod
@ -3,7 +3,9 @@ module github.com/alist-org/alist/v3
|
||||
go 1.22.4
|
||||
|
||||
require (
|
||||
github.com/SheltonZhu/115driver v1.0.29
|
||||
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0
|
||||
github.com/KirCute/sftpd-alist v0.0.11
|
||||
github.com/SheltonZhu/115driver v1.0.32
|
||||
github.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21
|
||||
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
|
||||
github.com/alist-org/gofakes3 v0.0.7
|
||||
@ -50,8 +52,9 @@ require (
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/rclone/rclone v1.67.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
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
|
||||
@ -59,12 +62,12 @@ require (
|
||||
github.com/xhofe/tache v0.1.3
|
||||
github.com/xhofe/wopan-sdk-go v0.1.3
|
||||
github.com/zzzhr1990/go-common-entity v0.0.0-20221216044934-fd1c571e3a22
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/crypto v0.30.0
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e
|
||||
golang.org/x/image v0.19.0
|
||||
golang.org/x/net v0.28.0
|
||||
golang.org/x/oauth2 v0.22.0
|
||||
golang.org/x/time v0.6.0
|
||||
golang.org/x/time v0.8.0
|
||||
google.golang.org/appengine v1.6.8
|
||||
gopkg.in/ldap.v3 v3.1.0
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
@ -83,10 +86,12 @@ require (
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fclairamb/go-log v0.5.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hekmon/cunits/v2 v2.1.0 // indirect
|
||||
github.com/ipfs/boxo v0.12.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@ -220,10 +225,10 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.8 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/term v0.24.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/api v0.169.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
|
||||
|
51
go.sum
51
go.sum
@ -1,14 +1,19 @@
|
||||
cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
|
||||
cloud.google.com/go/compute v1.23.4 h1:EBT9Nw4q3zyE7G45Wvv3MzolIrCJEuHys5muLY0wvAw=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0 h1:ikwCzeqoqN6wvBHOB9OI6dde/jbV7EoTMpUcxtYl5Po=
|
||||
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0/go.mod h1:v0NgMtKDDi/6CM6r4P+daCljCW3eO9yS+Z+pZDTKo1E=
|
||||
github.com/KirCute/sftpd-alist v0.0.11 h1:BGInXmmLBI+v6S9WZCwvY0DRK1vDprGNcTv/57p2GSo=
|
||||
github.com/KirCute/sftpd-alist v0.0.11/go.mod h1:pPFzr6GrKqXvFXLr46ZpoqmtSpwH8DKTYloSp/ybzKQ=
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE=
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=
|
||||
github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM=
|
||||
github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/SheltonZhu/115driver v1.0.29 h1:yFBqFDYJyADo3eG2RjJgSovnFd1OrpGHmsHBi6j0+r4=
|
||||
github.com/SheltonZhu/115driver v1.0.29/go.mod h1:e3fPOBANbH/FsTya8FquJwOR3ErhCQgEab3q6CVY2k4=
|
||||
github.com/SheltonZhu/115driver v1.0.32 h1:Taw1bnfcPJZW0xTdhDvEbBS1tccif7J7DslRp2NkDyQ=
|
||||
github.com/SheltonZhu/115driver v1.0.32/go.mod h1:XXFi23pyhAgzUE8dUEKdGvIdUQKi3wv6zR7C1Do40D8=
|
||||
github.com/Unknwon/goconfig v1.0.0 h1:9IAu/BYbSLQi8puFjUQApZTxIHqSwrj5d8vpP8vTq4A=
|
||||
github.com/Unknwon/goconfig v1.0.0/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw=
|
||||
github.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21 h1:h6q5E9aMBhhdqouW81LozVPI1I+Pu6IxL2EKpfm5OjY=
|
||||
@ -144,6 +149,8 @@ github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJL
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/fclairamb/go-log v0.5.0 h1:Gz9wSamEaA6lta4IU2cjJc2xSq5sV5VYSB5w/SUHhVc=
|
||||
github.com/fclairamb/go-log v0.5.0/go.mod h1:XoRO1dYezpsGmLLkZE9I+sHqpqY65p8JA+Vqblb7k40=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.6 h1:6J37oI4wMZLj8EPgSCcSTTIbnI5D6RCNW/srX8vQd1Y=
|
||||
@ -168,6 +175,10 @@ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
|
||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@ -393,6 +404,8 @@ github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831 h1:K3T3eu
|
||||
github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831/go.mod h1:lSHD4lC4zlMl+zcoysdJcd5KFzsWwOD8BJbyg1Ws9Ng=
|
||||
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
||||
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
||||
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||
github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
@ -439,6 +452,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df h1:S77Pf5fIGMa7oSwp8SQPp7Hb4ZiI38K3RNBKD2LLeEM=
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
||||
@ -457,6 +472,8 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:s
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
@ -477,10 +494,13 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 h1:6Y51mutOvRGRx6KqyMNo//xk8B8o6zW9/RVmy1VamOs=
|
||||
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543/go.mod h1:jpwqYA8KUVEvSUJHkCXsnBRJCSKP1BMa81QZ6kvRpow=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
|
||||
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
||||
@ -512,8 +532,6 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25 h1:eDfebW/yfq9DtG9RO3KP7BT2dot2CvJGIvrB0NEoDXI=
|
||||
github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25/go.mod h1:fH4oNm5F9NfI5dLi0oIMtsLNKQOirUDbEMCIBb/7SU0=
|
||||
github.com/xhofe/tache v0.1.2 h1:pHrXlrWcbTb4G7hVUDW7Rc+YTUnLJvnLBrdktVE1Fqg=
|
||||
github.com/xhofe/tache v0.1.2/go.mod h1:iKumPFvywf30FRpAHHCt64G0JHLMzT0K+wyGedHsmTQ=
|
||||
github.com/xhofe/tache v0.1.3 h1:MipxzlljYX29E1YI/SLC7hVomVF+51iP1OUzlsuq1wE=
|
||||
github.com/xhofe/tache v0.1.3/go.mod h1:iKumPFvywf30FRpAHHCt64G0JHLMzT0K+wyGedHsmTQ=
|
||||
github.com/xhofe/wopan-sdk-go v0.1.3 h1:J58X6v+n25ewBZjb05pKOr7AWGohb+Rdll4CThGh6+A=
|
||||
@ -556,8 +574,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
|
||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
@ -599,8 +617,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.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=
|
||||
@ -632,8 +650,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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=
|
||||
@ -644,8 +662,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
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=
|
||||
@ -659,12 +677,13 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
|
@ -164,6 +164,7 @@ func InitialSettings() []model.SettingItem {
|
||||
{Key: conf.SSOApplicationName, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSOEndpointName, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSOJwtPublicKey, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSOExtraScopes, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSOAutoRegister, Value: "false", Type: conf.TypeBool, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSODefaultDir, Value: "/", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSODefaultPermission, Value: "0", Type: conf.TypeNumber, Group: model.SSO, Flag: model.PRIVATE},
|
||||
@ -184,6 +185,16 @@ func InitialSettings() []model.SettingItem {
|
||||
{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},
|
||||
|
||||
//ftp settings
|
||||
{Key: conf.FTPPublicHost, Value: "127.0.0.1", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPPasvPortMap, Value: "", Type: conf.TypeText, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPProxyUserAgent, Value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"Chrome/87.0.4280.88 Safari/537.36", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPMandatoryTLS, Value: "false", Type: conf.TypeBool, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPImplicitTLS, Value: "false", Type: conf.TypeBool, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPTLSPrivateKeyPath, Value: "", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPTLSPublicCertPath, Value: "", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
|
||||
}
|
||||
initialSettingItems = append(initialSettingItems, tool.Tools.Items()...)
|
||||
if flags.Dev {
|
||||
|
101
internal/bootstrap/ssh.go
Normal file
101
internal/bootstrap/ssh.go
Normal file
@ -0,0 +1,101 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/cmd/flags"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func InitHostKey() {
|
||||
sshPath := filepath.Join(flags.DataDir, "ssh")
|
||||
if !utils.Exists(sshPath) {
|
||||
err := utils.CreateNestedDirectory(sshPath)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to create ssh directory: %+v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
conf.SSHSigners = make([]ssh.Signer, 0, 4)
|
||||
if rsaKey, ok := LoadOrGenerateRSAHostKey(sshPath); ok {
|
||||
conf.SSHSigners = append(conf.SSHSigners, rsaKey)
|
||||
}
|
||||
// TODO Add keys for other encryption algorithms
|
||||
}
|
||||
|
||||
func LoadOrGenerateRSAHostKey(parentDir string) (ssh.Signer, bool) {
|
||||
privateKeyPath := filepath.Join(parentDir, "ssh_host_rsa_key")
|
||||
publicKeyPath := filepath.Join(parentDir, "ssh_host_rsa_key.pub")
|
||||
privateKeyBytes, err := os.ReadFile(privateKeyPath)
|
||||
if err == nil {
|
||||
var privateKey *rsa.PrivateKey
|
||||
privateKey, err = rsaDecodePrivateKey(privateKeyBytes)
|
||||
if err == nil {
|
||||
var ret ssh.Signer
|
||||
ret, err = ssh.NewSignerFromKey(privateKey)
|
||||
if err == nil {
|
||||
return ret, true
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = os.Remove(privateKeyPath)
|
||||
_ = os.Remove(publicKeyPath)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to generate RSA private key: %+v", err)
|
||||
return nil, false
|
||||
}
|
||||
publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to generate RSA public key: %+v", err)
|
||||
return nil, false
|
||||
}
|
||||
ret, err := ssh.NewSignerFromKey(privateKey)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to generate RSA signer: %+v", err)
|
||||
return nil, false
|
||||
}
|
||||
privateBytes := rsaEncodePrivateKey(privateKey)
|
||||
publicBytes := ssh.MarshalAuthorizedKey(publicKey)
|
||||
err = os.WriteFile(privateKeyPath, privateBytes, 0600)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to write RSA private key to file: %+v", err)
|
||||
return nil, false
|
||||
}
|
||||
err = os.WriteFile(publicKeyPath, publicBytes, 0644)
|
||||
if err != nil {
|
||||
_ = os.Remove(privateKeyPath)
|
||||
utils.Log.Fatalf("failed to write RSA public key to file: %+v", err)
|
||||
return nil, false
|
||||
}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
func rsaEncodePrivateKey(privateKey *rsa.PrivateKey) []byte {
|
||||
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
privateBlock := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Headers: nil,
|
||||
Bytes: privateKeyBytes,
|
||||
}
|
||||
return pem.EncodeToMemory(privateBlock)
|
||||
}
|
||||
|
||||
func rsaDecodePrivateKey(bytes []byte) (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode(bytes)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to parse PEM block containing the key")
|
||||
}
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privateKey, nil
|
||||
}
|
@ -71,6 +71,24 @@ type S3 struct {
|
||||
SSL bool `json:"ssl" env:"SSL"`
|
||||
}
|
||||
|
||||
type FTP struct {
|
||||
Enable bool `json:"enable" env:"ENABLE"`
|
||||
Listen string `json:"listen" env:"LISTEN"`
|
||||
FindPasvPortAttempts int `json:"find_pasv_port_attempts" env:"FIND_PASV_PORT_ATTEMPTS"`
|
||||
ActiveTransferPortNon20 bool `json:"active_transfer_port_non_20" env:"ACTIVE_TRANSFER_PORT_NON_20"`
|
||||
IdleTimeout int `json:"idle_timeout" env:"IDLE_TIMEOUT"`
|
||||
ConnectionTimeout int `json:"connection_timeout" env:"CONNECTION_TIMEOUT"`
|
||||
DisableActiveMode bool `json:"disable_active_mode" env:"DISABLE_ACTIVE_MODE"`
|
||||
DefaultTransferBinary bool `json:"default_transfer_binary" env:"DEFAULT_TRANSFER_BINARY"`
|
||||
EnableActiveConnIPCheck bool `json:"enable_active_conn_ip_check" env:"ENABLE_ACTIVE_CONN_IP_CHECK"`
|
||||
EnablePasvConnIPCheck bool `json:"enable_pasv_conn_ip_check" env:"ENABLE_PASV_CONN_IP_CHECK"`
|
||||
}
|
||||
|
||||
type SFTP struct {
|
||||
Enable bool `json:"enable" env:"ENABLE"`
|
||||
Listen string `json:"listen" env:"LISTEN"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Force bool `json:"force" env:"FORCE"`
|
||||
SiteURL string `json:"site_url" env:"SITE_URL"`
|
||||
@ -90,6 +108,8 @@ type Config struct {
|
||||
Tasks TasksConfig `json:"tasks" envPrefix:"TASKS_"`
|
||||
Cors Cors `json:"cors" envPrefix:"CORS_"`
|
||||
S3 S3 `json:"s3" envPrefix:"S3_"`
|
||||
FTP FTP `json:"ftp" envPrefix:"FTP_"`
|
||||
SFTP SFTP `json:"sftp" envPrefix:"SFTP_"`
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
@ -159,5 +179,21 @@ func DefaultConfig() *Config {
|
||||
Port: 5246,
|
||||
SSL: false,
|
||||
},
|
||||
FTP: FTP{
|
||||
Enable: false,
|
||||
Listen: ":5221",
|
||||
FindPasvPortAttempts: 50,
|
||||
ActiveTransferPortNon20: false,
|
||||
IdleTimeout: 900,
|
||||
ConnectionTimeout: 30,
|
||||
DisableActiveMode: false,
|
||||
DefaultTransferBinary: false,
|
||||
EnableActiveConnIPCheck: true,
|
||||
EnablePasvConnIPCheck: true,
|
||||
},
|
||||
SFTP: SFTP{
|
||||
Enable: false,
|
||||
Listen: ":5222",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,7 @@ const (
|
||||
SSOApplicationName = "sso_application_name"
|
||||
SSOEndpointName = "sso_endpoint_name"
|
||||
SSOJwtPublicKey = "sso_jwt_public_key"
|
||||
SSOExtraScopes = "sso_extra_scopes"
|
||||
SSOAutoRegister = "sso_auto_register"
|
||||
SSODefaultDir = "sso_default_dir"
|
||||
SSODefaultPermission = "sso_default_permission"
|
||||
@ -96,6 +97,15 @@ const (
|
||||
// qbittorrent
|
||||
QbittorrentUrl = "qbittorrent_url"
|
||||
QbittorrentSeedtime = "qbittorrent_seedtime"
|
||||
|
||||
// ftp
|
||||
FTPPublicHost = "ftp_public_host"
|
||||
FTPPasvPortMap = "ftp_pasv_port_map"
|
||||
FTPProxyUserAgent = "ftp_proxy_user_agent"
|
||||
FTPMandatoryTLS = "ftp_mandatory_tls"
|
||||
FTPImplicitTLS = "ftp_implicit_tls"
|
||||
FTPTLSPrivateKeyPath = "ftp_tls_private_key_path"
|
||||
FTPTLSPublicCertPath = "ftp_tls_public_cert_path"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -1,6 +1,7 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
"net/url"
|
||||
"regexp"
|
||||
)
|
||||
@ -32,3 +33,5 @@ var (
|
||||
ManageHtml string
|
||||
IndexHtml string
|
||||
)
|
||||
|
||||
var SSHSigners []ssh.Signer
|
||||
|
@ -11,6 +11,7 @@ const (
|
||||
SSO
|
||||
LDAP
|
||||
S3
|
||||
FTP
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -117,6 +117,14 @@ func (u *User) CanWebdavManage() bool {
|
||||
return u.IsAdmin() || (u.Permission>>9)&1 == 1
|
||||
}
|
||||
|
||||
func (u *User) CanFTPAccess() bool {
|
||||
return (u.Permission>>10)&1 == 1
|
||||
}
|
||||
|
||||
func (u *User) CanFTPManage() bool {
|
||||
return (u.Permission>>11)&1 == 1
|
||||
}
|
||||
|
||||
func (u *User) JoinPath(reqPath string) (string, error) {
|
||||
return utils.JoinBasePath(u.BasePath, reqPath)
|
||||
}
|
||||
|
288
server/ftp.go
Normal file
288
server/ftp.go
Normal file
@ -0,0 +1,288 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"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"
|
||||
"github.com/alist-org/alist/v3/server/ftp"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type FtpMainDriver struct {
|
||||
settings *ftpserver.Settings
|
||||
proxyHeader *http.Header
|
||||
clients map[uint32]ftpserver.ClientContext
|
||||
shutdownLock sync.RWMutex
|
||||
isShutdown bool
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
func NewMainDriver() (*FtpMainDriver, error) {
|
||||
header := &http.Header{}
|
||||
header.Add("User-Agent", setting.GetStr(conf.FTPProxyUserAgent))
|
||||
transferType := ftpserver.TransferTypeASCII
|
||||
if conf.Conf.FTP.DefaultTransferBinary {
|
||||
transferType = ftpserver.TransferTypeBinary
|
||||
}
|
||||
activeConnCheck := ftpserver.IPMatchDisabled
|
||||
if conf.Conf.FTP.EnableActiveConnIPCheck {
|
||||
activeConnCheck = ftpserver.IPMatchRequired
|
||||
}
|
||||
pasvConnCheck := ftpserver.IPMatchDisabled
|
||||
if conf.Conf.FTP.EnablePasvConnIPCheck {
|
||||
pasvConnCheck = ftpserver.IPMatchRequired
|
||||
}
|
||||
tlsRequired := ftpserver.ClearOrEncrypted
|
||||
if setting.GetBool(conf.FTPImplicitTLS) {
|
||||
tlsRequired = ftpserver.ImplicitEncryption
|
||||
} else if setting.GetBool(conf.FTPMandatoryTLS) {
|
||||
tlsRequired = ftpserver.MandatoryEncryption
|
||||
}
|
||||
tlsConf, err := getTlsConf(setting.GetStr(conf.FTPTLSPrivateKeyPath), setting.GetStr(conf.FTPTLSPublicCertPath))
|
||||
if err != nil && tlsRequired != ftpserver.ClearOrEncrypted {
|
||||
return nil, fmt.Errorf("FTP mandatory TLS has been enabled, but the certificate failed to load: %w", err)
|
||||
}
|
||||
return &FtpMainDriver{
|
||||
settings: &ftpserver.Settings{
|
||||
ListenAddr: conf.Conf.FTP.Listen,
|
||||
PublicHost: lookupIP(setting.GetStr(conf.FTPPublicHost)),
|
||||
PassiveTransferPortGetter: newPortMapper(setting.GetStr(conf.FTPPasvPortMap)),
|
||||
FindPasvPortAttempts: conf.Conf.FTP.FindPasvPortAttempts,
|
||||
ActiveTransferPortNon20: conf.Conf.FTP.ActiveTransferPortNon20,
|
||||
IdleTimeout: conf.Conf.FTP.IdleTimeout,
|
||||
ConnectionTimeout: conf.Conf.FTP.ConnectionTimeout,
|
||||
DisableMLSD: false,
|
||||
DisableMLST: false,
|
||||
DisableMFMT: true,
|
||||
Banner: setting.GetStr(conf.Announcement),
|
||||
TLSRequired: tlsRequired,
|
||||
DisableLISTArgs: false,
|
||||
DisableSite: false,
|
||||
DisableActiveMode: conf.Conf.FTP.DisableActiveMode,
|
||||
EnableHASH: false,
|
||||
DisableSTAT: false,
|
||||
DisableSYST: false,
|
||||
EnableCOMB: false,
|
||||
DefaultTransferType: transferType,
|
||||
ActiveConnectionsCheck: activeConnCheck,
|
||||
PasvConnectionsCheck: pasvConnCheck,
|
||||
SiteHandlers: map[string]ftpserver.SiteHandler{
|
||||
"SIZE": ftp.HandleSIZE,
|
||||
},
|
||||
},
|
||||
proxyHeader: header,
|
||||
clients: make(map[uint32]ftpserver.ClientContext),
|
||||
shutdownLock: sync.RWMutex{},
|
||||
isShutdown: false,
|
||||
tlsConfig: tlsConf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *FtpMainDriver) GetSettings() (*ftpserver.Settings, error) {
|
||||
return d.settings, nil
|
||||
}
|
||||
|
||||
func (d *FtpMainDriver) ClientConnected(cc ftpserver.ClientContext) (string, error) {
|
||||
if d.isShutdown || !d.shutdownLock.TryRLock() {
|
||||
return "", errors.New("server has shutdown")
|
||||
}
|
||||
defer d.shutdownLock.RUnlock()
|
||||
d.clients[cc.ID()] = cc
|
||||
return "AList FTP Endpoint", nil
|
||||
}
|
||||
|
||||
func (d *FtpMainDriver) ClientDisconnected(cc ftpserver.ClientContext) {
|
||||
err := cc.Close()
|
||||
if err != nil {
|
||||
utils.Log.Errorf("failed to close client: %v", err)
|
||||
}
|
||||
delete(d.clients, cc.ID())
|
||||
}
|
||||
|
||||
func (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string) (ftpserver.ClientDriver, error) {
|
||||
var userObj *model.User
|
||||
var err error
|
||||
if user == "anonymous" || user == "guest" {
|
||||
userObj, err = op.GetGuest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
userObj, err = op.GetUserByName(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passHash := model.StaticHash(pass)
|
||||
if err = userObj.ValidatePwdStaticHash(passHash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if userObj.Disabled || !userObj.CanFTPAccess() {
|
||||
return nil, errors.New("user is not allowed to access via FTP")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "user", userObj)
|
||||
if user == "anonymous" || user == "guest" {
|
||||
ctx = context.WithValue(ctx, "meta_pass", pass)
|
||||
} else {
|
||||
ctx = context.WithValue(ctx, "meta_pass", "")
|
||||
}
|
||||
ctx = context.WithValue(ctx, "client_ip", cc.RemoteAddr().String())
|
||||
ctx = context.WithValue(ctx, "proxy_header", d.proxyHeader)
|
||||
return ftp.NewAferoAdapter(ctx), nil
|
||||
}
|
||||
|
||||
func (d *FtpMainDriver) GetTLSConfig() (*tls.Config, error) {
|
||||
if d.tlsConfig == nil {
|
||||
return nil, errors.New("TLS config not provided")
|
||||
}
|
||||
return d.tlsConfig, nil
|
||||
}
|
||||
|
||||
func (d *FtpMainDriver) Stop() {
|
||||
d.isShutdown = true
|
||||
d.shutdownLock.Lock()
|
||||
defer d.shutdownLock.Unlock()
|
||||
for _, value := range d.clients {
|
||||
_ = value.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func lookupIP(host string) string {
|
||||
if host == "" || net.ParseIP(host) != nil {
|
||||
return host
|
||||
}
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil || len(ips) == 0 {
|
||||
utils.Log.Fatalf("given FTP public host is invalid, and the default value will be used: %v", err)
|
||||
return ""
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
return ip.String()
|
||||
}
|
||||
}
|
||||
v6 := ips[0].String()
|
||||
utils.Log.Warnf("no IPv4 record looked up, %s will be used as public host, and it might do not work.", v6)
|
||||
return v6
|
||||
}
|
||||
|
||||
func newPortMapper(str string) ftpserver.PasvPortGetter {
|
||||
if str == "" {
|
||||
return nil
|
||||
}
|
||||
pasvPortMappers := strings.Split(strings.Replace(str, "\n", ",", -1), ",")
|
||||
type group struct {
|
||||
ExposedStart int
|
||||
ListenedStart int
|
||||
Length int
|
||||
}
|
||||
groups := make([]group, len(pasvPortMappers))
|
||||
totalLength := 0
|
||||
convertToPorts := func(str string) (int, int, error) {
|
||||
start, end, multi := strings.Cut(str, "-")
|
||||
if multi {
|
||||
si, err := strconv.Atoi(start)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
ei, err := strconv.Atoi(end)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if ei < si || ei < 1024 || si < 1024 || ei > 65535 || si > 65535 {
|
||||
return 0, 0, errors.New("invalid port")
|
||||
}
|
||||
return si, ei - si + 1, nil
|
||||
} else {
|
||||
ret, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
} else {
|
||||
return ret, 1, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, mapper := range pasvPortMappers {
|
||||
var err error
|
||||
exposed, listened, mapped := strings.Cut(mapper, ":")
|
||||
for {
|
||||
if mapped {
|
||||
var es, ls, el, ll int
|
||||
es, el, err = convertToPorts(exposed)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
ls, ll, err = convertToPorts(listened)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if el != ll {
|
||||
err = errors.New("the number of exposed ports and listened ports does not match")
|
||||
break
|
||||
}
|
||||
groups[i].ExposedStart = es
|
||||
groups[i].ListenedStart = ls
|
||||
groups[i].Length = el
|
||||
totalLength += el
|
||||
} else {
|
||||
var start, length int
|
||||
start, length, err = convertToPorts(mapper)
|
||||
groups[i].ExposedStart = start
|
||||
groups[i].ListenedStart = start
|
||||
groups[i].Length = length
|
||||
totalLength += length
|
||||
}
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to convert FTP PASV port mapper %s: %v, the port mapper will be ignored.", mapper, err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return func() (int, int, bool) {
|
||||
idxPort := rand.Intn(totalLength)
|
||||
for _, g := range groups {
|
||||
if idxPort >= g.Length {
|
||||
idxPort -= g.Length
|
||||
} else {
|
||||
return g.ExposedStart + idxPort, g.ListenedStart + idxPort, true
|
||||
}
|
||||
}
|
||||
// unreachable
|
||||
return 0, 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func getTlsConf(keyPath, certPath string) (*tls.Config, error) {
|
||||
if keyPath == "" || certPath == "" {
|
||||
return nil, errors.New("private key or certificate is not provided")
|
||||
}
|
||||
cert, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsCert, err := tls.X509KeyPair(cert, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tls.Config{Certificates: []tls.Certificate{tlsCert}}, nil
|
||||
}
|
115
server/ftp/afero.go
Normal file
115
server/ftp/afero.go
Normal file
@ -0,0 +1,115 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/spf13/afero"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AferoAdapter struct {
|
||||
ctx context.Context
|
||||
nextFileSize int64
|
||||
}
|
||||
|
||||
func NewAferoAdapter(ctx context.Context) *AferoAdapter {
|
||||
return &AferoAdapter{ctx: ctx}
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) Create(_ string) (afero.File, error) {
|
||||
// See also GetHandle
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) Mkdir(name string, _ os.FileMode) error {
|
||||
return Mkdir(a.ctx, name)
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) MkdirAll(path string, perm os.FileMode) error {
|
||||
return a.Mkdir(path, perm)
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) Open(_ string) (afero.File, error) {
|
||||
// See also GetHandle and ReadDir
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) OpenFile(_ string, _ int, _ os.FileMode) (afero.File, error) {
|
||||
// See also GetHandle
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) Remove(name string) error {
|
||||
return Remove(a.ctx, name)
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) RemoveAll(path string) error {
|
||||
return a.Remove(path)
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) Rename(oldName, newName string) error {
|
||||
return Rename(a.ctx, oldName, newName)
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) Stat(name string) (os.FileInfo, error) {
|
||||
return Stat(a.ctx, name)
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) Name() string {
|
||||
return "AList FTP Endpoint"
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) Chmod(_ string, _ os.FileMode) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) Chown(_ string, _, _ int) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) Chtimes(_ string, _ time.Time, _ time.Time) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) ReadDir(name string) ([]os.FileInfo, error) {
|
||||
return List(a.ctx, name)
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) GetHandle(name string, flags int, offset int64) (ftpserver.FileTransfer, error) {
|
||||
fileSize := a.nextFileSize
|
||||
a.nextFileSize = 0
|
||||
if offset != 0 {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
if (flags & os.O_SYNC) != 0 {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
if (flags & os.O_APPEND) != 0 {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
_, err := fs.Get(a.ctx, name, &fs.GetArgs{})
|
||||
exists := err == nil
|
||||
if (flags&os.O_CREATE) == 0 && !exists {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
if (flags&os.O_EXCL) != 0 && exists {
|
||||
return nil, errors.New("file already exists")
|
||||
}
|
||||
if (flags & os.O_WRONLY) != 0 {
|
||||
trunc := (flags & os.O_TRUNC) != 0
|
||||
if fileSize > 0 {
|
||||
return OpenUploadWithLength(a.ctx, name, trunc, fileSize)
|
||||
} else {
|
||||
return OpenUpload(a.ctx, name, trunc)
|
||||
}
|
||||
}
|
||||
return OpenDownload(a.ctx, name)
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) SetNextFileSize(size int64) {
|
||||
a.nextFileSize = size
|
||||
}
|
11
server/ftp/const.go
Normal file
11
server/ftp/const.go
Normal file
@ -0,0 +1,11 @@
|
||||
package ftp
|
||||
|
||||
// From leffss/sftpd
|
||||
const (
|
||||
SSH_FXF_READ = 0x00000001
|
||||
SSH_FXF_WRITE = 0x00000002
|
||||
SSH_FXF_APPEND = 0x00000004
|
||||
SSH_FXF_CREAT = 0x00000008
|
||||
SSH_FXF_TRUNC = 0x00000010
|
||||
SSH_FXF_EXCL = 0x00000020
|
||||
)
|
75
server/ftp/fsmanage.go
Normal file
75
server/ftp/fsmanage.go
Normal file
@ -0,0 +1,75 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/pkg/errors"
|
||||
stdpath "path"
|
||||
)
|
||||
|
||||
func Mkdir(ctx context.Context, path string) error {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
reqPath, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !user.CanWrite() || !user.CanFTPManage() {
|
||||
meta, err := op.GetNearestMeta(stdpath.Dir(reqPath))
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !common.CanWrite(meta, reqPath) {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
}
|
||||
return fs.MakeDir(ctx, reqPath)
|
||||
}
|
||||
|
||||
func Remove(ctx context.Context, path string) error {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
if !user.CanRemove() || !user.CanFTPManage() {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
reqPath, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.Remove(ctx, reqPath)
|
||||
}
|
||||
|
||||
func Rename(ctx context.Context, oldPath, newPath string) error {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
srcPath, err := user.JoinPath(oldPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstPath, err := user.JoinPath(newPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcDir, srcBase := stdpath.Split(srcPath)
|
||||
dstDir, dstBase := stdpath.Split(dstPath)
|
||||
if srcDir == dstDir {
|
||||
if !user.CanRename() || !user.CanFTPManage() {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
return fs.Rename(ctx, srcPath, dstBase)
|
||||
} else {
|
||||
if !user.CanFTPManage() || !user.CanMove() || (srcBase != dstBase && !user.CanRename()) {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
if err := fs.Move(ctx, srcPath, dstDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if srcBase != dstBase {
|
||||
return fs.Rename(ctx, stdpath.Join(dstDir, srcBase), dstBase)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
188
server/ftp/fsread.go
Normal file
188
server/ftp/fsread.go
Normal file
@ -0,0 +1,188 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"context"
|
||||
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/net"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
fs2 "io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FileDownloadProxy struct {
|
||||
ftpserver.FileTransfer
|
||||
reader io.ReadCloser
|
||||
closers *utils.Closers
|
||||
}
|
||||
|
||||
func OpenDownload(ctx context.Context, path string) (*FileDownloadProxy, error) {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
reqPath, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta, err := op.GetNearestMeta(reqPath)
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, "meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
|
||||
return nil, errs.PermissionDenied
|
||||
}
|
||||
|
||||
// directly use proxy
|
||||
header := *(ctx.Value("proxy_header").(*http.Header))
|
||||
link, obj, err := fs.Link(ctx, reqPath, model.LinkArgs{
|
||||
IP: ctx.Value("client_ip").(string),
|
||||
Header: header,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if storage.GetStorage().ProxyRange {
|
||||
common.ProxyRange(link, obj.GetSize())
|
||||
}
|
||||
reader, closers, err := proxy(link)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FileDownloadProxy{reader: reader, closers: closers}, nil
|
||||
}
|
||||
|
||||
func proxy(link *model.Link) (io.ReadCloser, *utils.Closers, error) {
|
||||
if link.MFile != nil {
|
||||
return link.MFile, nil, nil
|
||||
} else if link.RangeReadCloser != nil {
|
||||
rc, err := link.RangeReadCloser.RangeRead(context.Background(), http_range.Range{Length: -1})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
closers := link.RangeReadCloser.GetClosers()
|
||||
return rc, &closers, nil
|
||||
} else {
|
||||
res, err := net.RequestHttp(context.Background(), http.MethodGet, link.Header, link.URL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return res.Body, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileDownloadProxy) Read(p []byte) (n int, err error) {
|
||||
return f.reader.Read(p)
|
||||
}
|
||||
|
||||
func (f *FileDownloadProxy) Write(p []byte) (n int, err error) {
|
||||
return 0, errs.NotSupport
|
||||
}
|
||||
|
||||
func (f *FileDownloadProxy) Seek(offset int64, whence int) (int64, error) {
|
||||
return 0, errs.NotSupport
|
||||
}
|
||||
|
||||
func (f *FileDownloadProxy) Close() error {
|
||||
defer func() {
|
||||
if f.closers != nil {
|
||||
_ = f.closers.Close()
|
||||
}
|
||||
}()
|
||||
return f.reader.Close()
|
||||
}
|
||||
|
||||
type OsFileInfoAdapter struct {
|
||||
obj model.Obj
|
||||
}
|
||||
|
||||
func (o *OsFileInfoAdapter) Name() string {
|
||||
return o.obj.GetName()
|
||||
}
|
||||
|
||||
func (o *OsFileInfoAdapter) Size() int64 {
|
||||
return o.obj.GetSize()
|
||||
}
|
||||
|
||||
func (o *OsFileInfoAdapter) Mode() fs2.FileMode {
|
||||
var mode fs2.FileMode = 0755
|
||||
if o.IsDir() {
|
||||
mode |= fs2.ModeDir
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
func (o *OsFileInfoAdapter) ModTime() time.Time {
|
||||
return o.obj.ModTime()
|
||||
}
|
||||
|
||||
func (o *OsFileInfoAdapter) IsDir() bool {
|
||||
return o.obj.IsDir()
|
||||
}
|
||||
|
||||
func (o *OsFileInfoAdapter) Sys() any {
|
||||
return o.obj
|
||||
}
|
||||
|
||||
func Stat(ctx context.Context, path string) (os.FileInfo, error) {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
reqPath, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta, err := op.GetNearestMeta(reqPath)
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, "meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
|
||||
return nil, errs.PermissionDenied
|
||||
}
|
||||
obj, err := fs.Get(ctx, reqPath, &fs.GetArgs{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &OsFileInfoAdapter{obj: obj}, nil
|
||||
}
|
||||
|
||||
func List(ctx context.Context, path string) ([]os.FileInfo, error) {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
reqPath, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta, err := op.GetNearestMeta(reqPath)
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, "meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
|
||||
return nil, errs.PermissionDenied
|
||||
}
|
||||
objs, err := fs.List(ctx, reqPath, &fs.ListArgs{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := make([]os.FileInfo, len(objs))
|
||||
for i, obj := range objs {
|
||||
ret[i] = &OsFileInfoAdapter{obj: obj}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
209
server/ftp/fsup.go
Normal file
209
server/ftp/fsup.go
Normal file
@ -0,0 +1,209 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/internal/stream"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
stdpath "path"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FileUploadProxy struct {
|
||||
ftpserver.FileTransfer
|
||||
buffer *os.File
|
||||
path string
|
||||
ctx context.Context
|
||||
trunc bool
|
||||
}
|
||||
|
||||
func uploadAuth(ctx context.Context, path string) error {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
path, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
meta, err := op.GetNearestMeta(stdpath.Dir(path))
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !(common.CanAccess(user, meta, path, ctx.Value("meta_pass").(string)) &&
|
||||
((user.CanFTPManage() && user.CanWrite()) || common.CanWrite(meta, stdpath.Dir(path)))) {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func OpenUpload(ctx context.Context, path string, trunc bool) (*FileUploadProxy, error) {
|
||||
err := uploadAuth(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmpFile, err := os.CreateTemp(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FileUploadProxy{buffer: tmpFile, path: path, ctx: ctx, trunc: trunc}, nil
|
||||
}
|
||||
|
||||
func (f *FileUploadProxy) Read(p []byte) (n int, err error) {
|
||||
return 0, errs.NotSupport
|
||||
}
|
||||
|
||||
func (f *FileUploadProxy) Write(p []byte) (n int, err error) {
|
||||
return f.buffer.Write(p)
|
||||
}
|
||||
|
||||
func (f *FileUploadProxy) Seek(offset int64, whence int) (int64, error) {
|
||||
return 0, errs.NotSupport
|
||||
}
|
||||
|
||||
func (f *FileUploadProxy) Close() error {
|
||||
dir, name := stdpath.Split(f.path)
|
||||
size, err := f.buffer.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.buffer.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
arr := make([]byte, 512)
|
||||
if _, err := f.buffer.Read(arr); err != nil {
|
||||
return err
|
||||
}
|
||||
contentType := http.DetectContentType(arr)
|
||||
if _, err := f.buffer.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
if f.trunc {
|
||||
_ = fs.Remove(f.ctx, f.path)
|
||||
}
|
||||
s := &stream.FileStream{
|
||||
Obj: &model.Object{
|
||||
Name: name,
|
||||
Size: size,
|
||||
Modified: time.Now(),
|
||||
},
|
||||
Mimetype: contentType,
|
||||
WebPutAsTask: true,
|
||||
}
|
||||
s.SetTmpFile(f.buffer)
|
||||
s.Closers.Add(f.buffer)
|
||||
_, err = fs.PutAsTask(f.ctx, dir, s)
|
||||
return err
|
||||
}
|
||||
|
||||
type FileUploadWithLengthProxy struct {
|
||||
ftpserver.FileTransfer
|
||||
ctx context.Context
|
||||
path string
|
||||
length int64
|
||||
first512Bytes [512]byte
|
||||
pFirst int
|
||||
pipeWriter io.WriteCloser
|
||||
errChan chan error
|
||||
}
|
||||
|
||||
func OpenUploadWithLength(ctx context.Context, path string, trunc bool, length int64) (*FileUploadWithLengthProxy, error) {
|
||||
err := uploadAuth(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if trunc {
|
||||
_ = fs.Remove(ctx, path)
|
||||
}
|
||||
return &FileUploadWithLengthProxy{ctx: ctx, path: path, length: length}, nil
|
||||
}
|
||||
|
||||
func (f *FileUploadWithLengthProxy) Read(p []byte) (n int, err error) {
|
||||
return 0, errs.NotSupport
|
||||
}
|
||||
|
||||
func (f *FileUploadWithLengthProxy) Write(p []byte) (n int, err error) {
|
||||
if f.pipeWriter != nil {
|
||||
select {
|
||||
case e := <-f.errChan:
|
||||
return 0, e
|
||||
default:
|
||||
return f.pipeWriter.Write(p)
|
||||
}
|
||||
} else if len(p) < 512-f.pFirst {
|
||||
copy(f.first512Bytes[f.pFirst:], p)
|
||||
f.pFirst += len(p)
|
||||
return len(p), nil
|
||||
} else {
|
||||
copy(f.first512Bytes[f.pFirst:], p[:512-f.pFirst])
|
||||
contentType := http.DetectContentType(f.first512Bytes[:])
|
||||
dir, name := stdpath.Split(f.path)
|
||||
reader, writer := io.Pipe()
|
||||
f.errChan = make(chan error, 1)
|
||||
s := &stream.FileStream{
|
||||
Obj: &model.Object{
|
||||
Name: name,
|
||||
Size: f.length,
|
||||
Modified: time.Now(),
|
||||
},
|
||||
Mimetype: contentType,
|
||||
WebPutAsTask: false,
|
||||
Reader: reader,
|
||||
}
|
||||
go func() {
|
||||
e := fs.PutDirectly(f.ctx, dir, s, true)
|
||||
f.errChan <- e
|
||||
close(f.errChan)
|
||||
}()
|
||||
f.pipeWriter = writer
|
||||
n, err = writer.Write(f.first512Bytes[:])
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
n1, err := writer.Write(p[512-f.pFirst:])
|
||||
if err != nil {
|
||||
return n1 + 512 - f.pFirst, err
|
||||
}
|
||||
f.pFirst = 512
|
||||
return len(p), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileUploadWithLengthProxy) Seek(offset int64, whence int) (int64, error) {
|
||||
return 0, errs.NotSupport
|
||||
}
|
||||
|
||||
func (f *FileUploadWithLengthProxy) Close() error {
|
||||
if f.pipeWriter != nil {
|
||||
err := f.pipeWriter.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = <-f.errChan
|
||||
return err
|
||||
} else {
|
||||
data := f.first512Bytes[:f.pFirst]
|
||||
contentType := http.DetectContentType(data)
|
||||
dir, name := stdpath.Split(f.path)
|
||||
s := &stream.FileStream{
|
||||
Obj: &model.Object{
|
||||
Name: name,
|
||||
Size: int64(f.pFirst),
|
||||
Modified: time.Now(),
|
||||
},
|
||||
Mimetype: contentType,
|
||||
WebPutAsTask: false,
|
||||
Reader: bytes.NewReader(data),
|
||||
}
|
||||
return fs.PutDirectly(f.ctx, dir, s, true)
|
||||
}
|
||||
}
|
122
server/ftp/sftp.go
Normal file
122
server/ftp/sftp.go
Normal file
@ -0,0 +1,122 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"github.com/KirCute/sftpd-alist"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"os"
|
||||
)
|
||||
|
||||
type SftpDriverAdapter struct {
|
||||
FtpDriver *AferoAdapter
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) OpenFile(_ string, _ uint32, _ *sftpd.Attr) (sftpd.File, error) {
|
||||
// See also GetHandle
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) OpenDir(_ string) (sftpd.Dir, error) {
|
||||
// See also GetHandle
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) Remove(name string) error {
|
||||
return s.FtpDriver.Remove(name)
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) Rename(old, new string, _ uint32) error {
|
||||
return s.FtpDriver.Rename(old, new)
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) Mkdir(name string, attr *sftpd.Attr) error {
|
||||
return s.FtpDriver.Mkdir(name, attr.Mode)
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) Rmdir(name string) error {
|
||||
return s.Remove(name)
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) Stat(name string, _ bool) (*sftpd.Attr, error) {
|
||||
stat, err := s.FtpDriver.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileInfoToSftpAttr(stat), nil
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) SetStat(_ string, _ *sftpd.Attr) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) ReadLink(_ string) (string, error) {
|
||||
return "", errs.NotSupport
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) CreateLink(_, _ string, _ uint32) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) RealPath(path string) (string, error) {
|
||||
return utils.FixAndCleanPath(path), nil
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) GetHandle(name string, flags uint32, _ *sftpd.Attr, offset uint64) (sftpd.FileTransfer, error) {
|
||||
return s.FtpDriver.GetHandle(name, sftpFlagToOpenMode(flags), int64(offset))
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) ReadDir(name string) ([]sftpd.NamedAttr, error) {
|
||||
dir, err := s.FtpDriver.ReadDir(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := make([]sftpd.NamedAttr, len(dir))
|
||||
for i, d := range dir {
|
||||
ret[i] = *fileInfoToSftpNamedAttr(d)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// From leffss/sftpd
|
||||
func sftpFlagToOpenMode(flags uint32) int {
|
||||
mode := 0
|
||||
if (flags & SSH_FXF_READ) != 0 {
|
||||
mode |= os.O_RDONLY
|
||||
}
|
||||
if (flags & SSH_FXF_WRITE) != 0 {
|
||||
mode |= os.O_WRONLY
|
||||
}
|
||||
if (flags & SSH_FXF_APPEND) != 0 {
|
||||
mode |= os.O_APPEND
|
||||
}
|
||||
if (flags & SSH_FXF_CREAT) != 0 {
|
||||
mode |= os.O_CREATE
|
||||
}
|
||||
if (flags & SSH_FXF_TRUNC) != 0 {
|
||||
mode |= os.O_TRUNC
|
||||
}
|
||||
if (flags & SSH_FXF_EXCL) != 0 {
|
||||
mode |= os.O_EXCL
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
func fileInfoToSftpAttr(stat os.FileInfo) *sftpd.Attr {
|
||||
ret := &sftpd.Attr{}
|
||||
ret.Flags |= sftpd.ATTR_SIZE
|
||||
ret.Size = uint64(stat.Size())
|
||||
ret.Flags |= sftpd.ATTR_MODE
|
||||
ret.Mode = stat.Mode()
|
||||
ret.Flags |= sftpd.ATTR_TIME
|
||||
ret.ATime = stat.Sys().(model.Obj).CreateTime()
|
||||
ret.MTime = stat.ModTime()
|
||||
return ret
|
||||
}
|
||||
|
||||
func fileInfoToSftpNamedAttr(stat os.FileInfo) *sftpd.NamedAttr {
|
||||
return &sftpd.NamedAttr{
|
||||
Name: stat.Name(),
|
||||
Attr: *fileInfoToSftpAttr(stat),
|
||||
}
|
||||
}
|
21
server/ftp/site.go
Normal file
21
server/ftp/site.go
Normal file
@ -0,0 +1,21 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func HandleSIZE(param string, client ftpserver.ClientDriver) (int, string) {
|
||||
fs, ok := client.(*AferoAdapter)
|
||||
if !ok {
|
||||
return ftpserver.StatusNotLoggedIn, "Unexpected exception (driver is nil)"
|
||||
}
|
||||
size, err := strconv.ParseInt(param, 10, 64)
|
||||
if err != nil {
|
||||
return ftpserver.StatusSyntaxErrorParameters, fmt.Sprintf(
|
||||
"Couldn't parse file size, given: %s, err: %v", param, err)
|
||||
}
|
||||
fs.SetNextFileSize(size)
|
||||
return ftpserver.StatusOK, "Accepted next file size"
|
||||
}
|
@ -4,13 +4,14 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Xhofe/go-cache"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/go-cache"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
@ -123,6 +124,10 @@ func GetOIDCClient(c *gin.Context, useCompatibility bool, redirectUri, method st
|
||||
}
|
||||
clientId := setting.GetStr(conf.SSOClientId)
|
||||
clientSecret := setting.GetStr(conf.SSOClientSecret)
|
||||
extraScopes := []string{}
|
||||
if setting.GetStr(conf.SSOExtraScopes) != "" {
|
||||
extraScopes = strings.Split(setting.GetStr(conf.SSOExtraScopes), " ")
|
||||
}
|
||||
return &oauth2.Config{
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
@ -132,7 +137,7 @@ func GetOIDCClient(c *gin.Context, useCompatibility bool, redirectUri, method st
|
||||
Endpoint: provider.Endpoint(),
|
||||
|
||||
// "openid" is a required scope for OpenID Connect flows.
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile"},
|
||||
Scopes: append([]string{oidc.ScopeOpenID, "profile"}, extraScopes...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -90,6 +90,31 @@ func getTargetedHandler[T task.TaskInfoWithCreator](manager *tache.Manager[T], c
|
||||
}
|
||||
}
|
||||
|
||||
func getBatchHandler[T task.TaskInfoWithCreator](manager *tache.Manager[T], callback func(task T)) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
isAdmin, uid, ok := getUserInfo(c)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
var tids []string
|
||||
if err := c.ShouldBind(&tids); err != nil {
|
||||
common.ErrorStrResp(c, "invalid request format", 400)
|
||||
return
|
||||
}
|
||||
retErrs := make(map[string]string)
|
||||
for _, tid := range tids {
|
||||
t, ok := manager.GetByID(tid)
|
||||
if !ok || (!isAdmin && uid != t.GetCreator().ID) {
|
||||
retErrs[tid] = "task not found"
|
||||
continue
|
||||
}
|
||||
callback(t)
|
||||
}
|
||||
common.SuccessResp(c, retErrs)
|
||||
}
|
||||
}
|
||||
|
||||
func taskRoute[T task.TaskInfoWithCreator](g *gin.RouterGroup, manager *tache.Manager[T]) {
|
||||
g.GET("/undone", func(c *gin.Context) {
|
||||
isAdmin, uid, ok := getUserInfo(c)
|
||||
@ -132,6 +157,15 @@ func taskRoute[T task.TaskInfoWithCreator](g *gin.RouterGroup, manager *tache.Ma
|
||||
manager.Retry(task.GetID())
|
||||
common.SuccessResp(c)
|
||||
}))
|
||||
g.POST("/cancel_some", getBatchHandler(manager, func(task T) {
|
||||
manager.Cancel(task.GetID())
|
||||
}))
|
||||
g.POST("/delete_some", getBatchHandler(manager, func(task T) {
|
||||
manager.Remove(task.GetID())
|
||||
}))
|
||||
g.POST("/retry_some", getBatchHandler(manager, func(task T) {
|
||||
manager.Retry(task.GetID())
|
||||
}))
|
||||
g.POST("/clear_done", func(c *gin.Context) {
|
||||
isAdmin, uid, ok := getUserInfo(c)
|
||||
if !ok {
|
||||
|
109
server/sftp.go
Normal file
109
server/sftp.go
Normal file
@ -0,0 +1,109 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/KirCute/sftpd-alist"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"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"
|
||||
"github.com/alist-org/alist/v3/server/ftp"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type SftpDriver struct {
|
||||
proxyHeader *http.Header
|
||||
config *sftpd.Config
|
||||
}
|
||||
|
||||
func NewSftpDriver() (*SftpDriver, error) {
|
||||
header := &http.Header{}
|
||||
header.Add("User-Agent", setting.GetStr(conf.FTPProxyUserAgent))
|
||||
return &SftpDriver{
|
||||
proxyHeader: header,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *SftpDriver) GetConfig() *sftpd.Config {
|
||||
if d.config != nil {
|
||||
return d.config
|
||||
}
|
||||
serverConfig := ssh.ServerConfig{
|
||||
NoClientAuth: true,
|
||||
NoClientAuthCallback: d.NoClientAuth,
|
||||
PasswordCallback: d.PasswordAuth,
|
||||
AuthLogCallback: d.AuthLogCallback,
|
||||
BannerCallback: d.GetBanner,
|
||||
}
|
||||
for _, k := range conf.SSHSigners {
|
||||
serverConfig.AddHostKey(k)
|
||||
}
|
||||
d.config = &sftpd.Config{
|
||||
ServerConfig: serverConfig,
|
||||
HostPort: conf.Conf.SFTP.Listen,
|
||||
ErrorLogFunc: utils.Log.Error,
|
||||
//DebugLogFunc: utils.Log.Debugf,
|
||||
}
|
||||
return d.config
|
||||
}
|
||||
|
||||
func (d *SftpDriver) GetFileSystem(sc *ssh.ServerConn) (sftpd.FileSystem, error) {
|
||||
userObj, err := op.GetUserByName(sc.User())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "user", userObj)
|
||||
ctx = context.WithValue(ctx, "meta_pass", "")
|
||||
ctx = context.WithValue(ctx, "client_ip", sc.RemoteAddr().String())
|
||||
ctx = context.WithValue(ctx, "proxy_header", d.proxyHeader)
|
||||
return &ftp.SftpDriverAdapter{FtpDriver: ftp.NewAferoAdapter(ctx)}, nil
|
||||
}
|
||||
|
||||
func (d *SftpDriver) Close() {
|
||||
}
|
||||
|
||||
func (d *SftpDriver) NoClientAuth(conn ssh.ConnMetadata) (*ssh.Permissions, error) {
|
||||
if conn.User() != "guest" {
|
||||
return nil, errors.New("only guest is allowed to login without authorization")
|
||||
}
|
||||
guest, err := op.GetGuest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if guest.Disabled || !guest.CanFTPAccess() {
|
||||
return nil, errors.New("user is not allowed to access via SFTP")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *SftpDriver) PasswordAuth(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
userObj, err := op.GetUserByName(conn.User())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passHash := model.StaticHash(string(password))
|
||||
if err = userObj.ValidatePwdStaticHash(passHash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userObj.Disabled || !userObj.CanFTPAccess() {
|
||||
return nil, errors.New("user is not allowed to access via SFTP")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *SftpDriver) AuthLogCallback(conn ssh.ConnMetadata, method string, err error) {
|
||||
ip := conn.RemoteAddr().String()
|
||||
if err == nil {
|
||||
utils.Log.Infof("[SFTP] %s(%s) logged in via %s", conn.User(), ip, method)
|
||||
} else if method != "none" {
|
||||
utils.Log.Infof("[SFTP] %s(%s) tries logging in via %s but with error: %s", conn.User(), ip, method, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *SftpDriver) GetBanner(_ ssh.ConnMetadata) string {
|
||||
return setting.GetStr(conf.Announcement)
|
||||
}
|
Reference in New Issue
Block a user