mirror of
https://github.com/AlistGo/alist.git
synced 2025-11-25 19:37:41 +08:00
Compare commits
44 Commits
fix-aliyun
...
v3.53.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1800f18e4 | ||
|
|
16cce37947 | ||
|
|
6e7c7d1dd0 | ||
|
|
28a8428559 | ||
|
|
d0026030cb | ||
|
|
fcbc79cb24 | ||
|
|
930f9f6096 | ||
|
|
23107483a1 | ||
|
|
4b288a08ef | ||
|
|
63391a2091 | ||
|
|
a11e4cfb31 | ||
|
|
9a7c82a71e | ||
|
|
8623da5361 | ||
|
|
84adba3acc | ||
|
|
3bf0af1e68 | ||
|
|
de09ba08b6 | ||
|
|
c64f899a63 | ||
|
|
3319f6ea6a | ||
|
|
d7723c378f | ||
|
|
a9fcd51bc4 | ||
|
|
74e384175b | ||
|
|
eca500861a | ||
|
|
97d4f79b96 | ||
|
|
fcfb3369d1 | ||
|
|
aea3ba1499 | ||
|
|
6b2d81eede | ||
|
|
85fe4e5bb3 | ||
|
|
52da07e8a7 | ||
|
|
46de9e9ebb | ||
|
|
ae90fb579b | ||
|
|
394a18cbd9 | ||
|
|
280960ce3e | ||
|
|
74332e91fb | ||
|
|
540d6c7064 | ||
|
|
55b2bb6b80 | ||
|
|
d5df6fa4cf | ||
|
|
3353055482 | ||
|
|
4d7c2a09ce | ||
|
|
5b8c26510b | ||
|
|
91cc7529a0 | ||
|
|
f61d13d433 | ||
|
|
00120cba27 | ||
|
|
5e15a360b7 | ||
|
|
2bdc5bef9e |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -25,6 +25,8 @@ jobs:
|
||||
- android-arm64
|
||||
name: Build
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org,direct
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:edge
|
||||
FROM alpine:3.20.7
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG INSTALL_FFMPEG=false
|
||||
|
||||
@@ -57,6 +57,7 @@ English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing
|
||||
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
|
||||
- [x] WebDav(Support OneDrive/SharePoint without API)
|
||||
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
||||
- [x] [MediaFire](https://www.mediafire.com)
|
||||
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
|
||||
- [x] [YandexDisk](https://disk.yandex.com/)
|
||||
@@ -101,6 +102,10 @@ English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing
|
||||
|
||||
<https://alistgo.com/>
|
||||
|
||||
## API Documentation (via Apifox):
|
||||
|
||||
<https://alist-public.apifox.cn/>
|
||||
|
||||
## Demo
|
||||
|
||||
<https://al.nn.ci>
|
||||
@@ -117,8 +122,6 @@ https://alistgo.com/guide/sponsor.html
|
||||
### Special sponsors
|
||||
|
||||
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
|
||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
||||
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
- [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)
|
||||
- [x] WebDav(支持无API的OneDrive/SharePoint)
|
||||
- [x] Teambition([中国](https://www.teambition.com/ ),[国际](https://us.teambition.com/ ))
|
||||
- [x] [MediaFire](https://www.mediafire.com)
|
||||
- [x] [分秒帧](https://www.mediatrack.cn/)
|
||||
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云,共享群组)
|
||||
- [x] [Yandex.Disk](https://disk.yandex.com/)
|
||||
@@ -99,6 +100,10 @@
|
||||
|
||||
<https://alistgo.com/zh/>
|
||||
|
||||
## API 文档(通过 Apifox 提供)
|
||||
|
||||
<https://alist-public.apifox.cn/>
|
||||
|
||||
## Demo
|
||||
|
||||
<https://al.nn.ci>
|
||||
@@ -114,8 +119,6 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
|
||||
### 特别赞助
|
||||
|
||||
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器,iPhone,iPad,Mac,Apple TV全平台支持。
|
||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
|
||||
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||
|
||||
## 贡献者
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
|
||||
- [x] WebDav(Support OneDrive/SharePoint without API)
|
||||
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
||||
- [x] [MediaFire](https://www.mediafire.com)
|
||||
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
|
||||
- [x] [YandexDisk](https://disk.yandex.com/)
|
||||
@@ -100,6 +101,10 @@
|
||||
|
||||
<https://alistgo.com/>
|
||||
|
||||
## APIドキュメント(Apifox 提供)
|
||||
|
||||
<https://alist-public.apifox.cn/>
|
||||
|
||||
## デモ
|
||||
|
||||
<https://al.nn.ci>
|
||||
@@ -116,8 +121,6 @@ https://alistgo.com/guide/sponsor.html
|
||||
### スペシャルスポンサー
|
||||
|
||||
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
|
||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
||||
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||
|
||||
## コントリビューター
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_46_0"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -16,6 +17,12 @@ func Init() {
|
||||
bootstrap.InitConfig()
|
||||
bootstrap.Log()
|
||||
bootstrap.InitDB()
|
||||
|
||||
if v3_46_0.IsLegacyRoleDetected() {
|
||||
utils.Log.Warnf("Detected legacy role format, executing ConvertLegacyRoles patch early...")
|
||||
v3_46_0.ConvertLegacyRoles()
|
||||
}
|
||||
|
||||
data.InitData()
|
||||
bootstrap.InitStreamLimit()
|
||||
bootstrap.InitIndex()
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -29,6 +31,7 @@ type Pan123 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
apiRateLimit sync.Map
|
||||
safeBoxUnlocked sync.Map
|
||||
}
|
||||
|
||||
func (d *Pan123) Config() driver.Config {
|
||||
@@ -52,10 +55,27 @@ func (d *Pan123) Drop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
if f, ok := dir.(File); ok && f.IsLock {
|
||||
if err := d.unlockSafeBox(f.FileId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
files, err := d.getFiles(ctx, dir.GetID(), dir.GetName())
|
||||
if err != nil {
|
||||
msg := strings.ToLower(err.Error())
|
||||
if strings.Contains(msg, "safe box") || strings.Contains(err.Error(), "保险箱") {
|
||||
if id, e := strconv.ParseInt(dir.GetID(), 10, 64); e == nil {
|
||||
if e = d.unlockSafeBox(id); e == nil {
|
||||
files, err = d.getFiles(ctx, dir.GetID(), dir.GetName())
|
||||
} else {
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
||||
return src, nil
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
type Addition struct {
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
SafePassword string `json:"safe_password"`
|
||||
driver.RootID
|
||||
//OrderBy string `json:"order_by" type:"select" options:"file_id,file_name,size,update_at" default:"file_name"`
|
||||
//OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
|
||||
@@ -20,6 +20,7 @@ type File struct {
|
||||
Etag string `json:"Etag"`
|
||||
S3KeyFlag string `json:"S3KeyFlag"`
|
||||
DownloadUrl string `json:"DownloadUrl"`
|
||||
IsLock bool `json:"IsLock"`
|
||||
}
|
||||
|
||||
func (f File) CreateTime() time.Time {
|
||||
|
||||
@@ -43,6 +43,7 @@ const (
|
||||
S3Auth = MainApi + "/file/s3_upload_object/auth"
|
||||
UploadCompleteV2 = MainApi + "/file/upload_complete/v2"
|
||||
S3Complete = MainApi + "/file/s3_complete_multipart_upload"
|
||||
SafeBoxUnlock = MainApi + "/restful/goapi/v1/file/safe_box/auth/unlockbox"
|
||||
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
|
||||
)
|
||||
|
||||
@@ -238,6 +239,22 @@ do:
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (d *Pan123) unlockSafeBox(fileId int64) error {
|
||||
if _, ok := d.safeBoxUnlocked.Load(fileId); ok {
|
||||
return nil
|
||||
}
|
||||
data := base.Json{"password": d.SafePassword}
|
||||
url := fmt.Sprintf("%s?fileId=%d", SafeBoxUnlock, fileId)
|
||||
_, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.safeBoxUnlocked.Store(fileId, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]File, error) {
|
||||
page := 1
|
||||
total := 0
|
||||
@@ -267,6 +284,15 @@ func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]
|
||||
req.SetQueryParams(query)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
msg := strings.ToLower(err.Error())
|
||||
if strings.Contains(msg, "safe box") || strings.Contains(err.Error(), "保险箱") {
|
||||
if fid, e := strconv.ParseInt(parentId, 10, 64); e == nil {
|
||||
if e = d.unlockSafeBox(fid); e == nil {
|
||||
return d.getFiles(ctx, parentId, name)
|
||||
}
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(string(_res))
|
||||
|
||||
191
drivers/123_open/api.go
Normal file
191
drivers/123_open/api.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package _123Open
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
// baseurl
|
||||
ApiBaseURL = "https://open-api.123pan.com"
|
||||
|
||||
// auth
|
||||
ApiToken = "/api/v1/access_token"
|
||||
|
||||
// file list
|
||||
ApiFileList = "/api/v2/file/list"
|
||||
|
||||
// direct link
|
||||
ApiGetDirectLink = "/api/v1/direct-link/url"
|
||||
|
||||
// mkdir
|
||||
ApiMakeDir = "/upload/v1/file/mkdir"
|
||||
|
||||
// remove
|
||||
ApiRemove = "/api/v1/file/trash"
|
||||
|
||||
// upload
|
||||
ApiUploadDomainURL = "/upload/v2/file/domain"
|
||||
ApiSingleUploadURL = "/upload/v2/file/single/create"
|
||||
ApiCreateUploadURL = "/upload/v2/file/create"
|
||||
ApiUploadSliceURL = "/upload/v2/file/slice"
|
||||
ApiUploadCompleteURL = "/upload/v2/file/upload_complete"
|
||||
|
||||
// move
|
||||
ApiMove = "/api/v1/file/move"
|
||||
|
||||
// rename
|
||||
ApiRename = "/api/v1/file/name"
|
||||
)
|
||||
|
||||
type Response[T any] struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
type TokenResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data TokenData `json:"data"`
|
||||
}
|
||||
|
||||
type TokenData struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
ExpiredAt string `json:"expiredAt"`
|
||||
}
|
||||
|
||||
type FileListResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data FileListData `json:"data"`
|
||||
}
|
||||
|
||||
type FileListData struct {
|
||||
LastFileId int64 `json:"lastFileId"`
|
||||
FileList []File `json:"fileList"`
|
||||
}
|
||||
|
||||
type DirectLinkResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data DirectLinkData `json:"data"`
|
||||
}
|
||||
|
||||
type DirectLinkData struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type MakeDirRequest struct {
|
||||
Name string `json:"name"`
|
||||
ParentID int64 `json:"parentID"`
|
||||
}
|
||||
|
||||
type MakeDirResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data MakeDirData `json:"data"`
|
||||
}
|
||||
|
||||
type MakeDirData struct {
|
||||
DirID int64 `json:"dirID"`
|
||||
}
|
||||
|
||||
type RemoveRequest struct {
|
||||
FileIDs []int64 `json:"fileIDs"`
|
||||
}
|
||||
|
||||
type UploadCreateResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data UploadCreateData `json:"data"`
|
||||
}
|
||||
|
||||
type UploadCreateData struct {
|
||||
FileID int64 `json:"fileId"`
|
||||
Reuse bool `json:"reuse"`
|
||||
PreuploadID string `json:"preuploadId"`
|
||||
SliceSize int64 `json:"sliceSize"`
|
||||
Servers []string `json:"servers"`
|
||||
}
|
||||
|
||||
type UploadUrlResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data UploadUrlData `json:"data"`
|
||||
}
|
||||
|
||||
type UploadUrlData struct {
|
||||
PresignedURL string `json:"presignedUrl"`
|
||||
}
|
||||
|
||||
type UploadCompleteResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data UploadCompleteData `json:"data"`
|
||||
}
|
||||
|
||||
type UploadCompleteData struct {
|
||||
FileID int `json:"fileID"`
|
||||
Completed bool `json:"completed"`
|
||||
}
|
||||
|
||||
func (d *Open123) Request(endpoint string, method string, setup func(*resty.Request), result any) (*resty.Response, error) {
|
||||
client := resty.New()
|
||||
token, err := d.tm.getToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := client.R().
|
||||
SetHeader("Authorization", "Bearer "+token).
|
||||
SetHeader("Platform", "open_platform").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetResult(result)
|
||||
|
||||
if setup != nil {
|
||||
setup(req)
|
||||
}
|
||||
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
return req.Get(ApiBaseURL + endpoint)
|
||||
case http.MethodPost:
|
||||
return req.Post(ApiBaseURL + endpoint)
|
||||
case http.MethodPut:
|
||||
return req.Put(ApiBaseURL + endpoint)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Open123) RequestTo(fullURL string, method string, setup func(*resty.Request), result any) (*resty.Response, error) {
|
||||
client := resty.New()
|
||||
|
||||
token, err := d.tm.getToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := client.R().
|
||||
SetHeader("Authorization", "Bearer "+token).
|
||||
SetHeader("Platform", "open_platform").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetResult(result)
|
||||
|
||||
if setup != nil {
|
||||
setup(req)
|
||||
}
|
||||
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
return req.Get(fullURL)
|
||||
case http.MethodPost:
|
||||
return req.Post(fullURL)
|
||||
case http.MethodPut:
|
||||
return req.Put(fullURL)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
}
|
||||
277
drivers/123_open/driver.go
Normal file
277
drivers/123_open/driver.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package _123Open
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/internal/stream"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Open123 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
UploadThread int
|
||||
tm *tokenManager
|
||||
}
|
||||
|
||||
func (d *Open123) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *Open123) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *Open123) Init(ctx context.Context) error {
|
||||
d.tm = newTokenManager(d.ClientID, d.ClientSecret)
|
||||
|
||||
if _, err := d.tm.getToken(); err != nil {
|
||||
return fmt.Errorf("token 初始化失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open123) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
parentFileId, err := strconv.ParseInt(dir.GetID(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileLastId := int64(0)
|
||||
var results []File
|
||||
|
||||
for fileLastId != -1 {
|
||||
files, err := d.getFiles(parentFileId, 100, fileLastId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range files.Data.FileList {
|
||||
if f.Trashed == 0 {
|
||||
results = append(results, f)
|
||||
}
|
||||
}
|
||||
fileLastId = files.Data.LastFileId
|
||||
}
|
||||
|
||||
objs := make([]model.Obj, 0, len(results))
|
||||
for _, f := range results {
|
||||
objs = append(objs, f)
|
||||
}
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func (d *Open123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
if file.IsDir() {
|
||||
return nil, errs.LinkIsDir
|
||||
}
|
||||
|
||||
fileID := file.GetID()
|
||||
|
||||
var result DirectLinkResp
|
||||
url := fmt.Sprintf("%s?fileID=%s", ApiGetDirectLink, fileID)
|
||||
_, err := d.Request(url, http.MethodGet, nil, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("get link failed: %s", result.Message)
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: result.Data.URL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Open123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
parentID, err := strconv.ParseInt(parentDir.GetID(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid parent ID: %w", err)
|
||||
}
|
||||
|
||||
var result MakeDirResp
|
||||
reqBody := MakeDirRequest{
|
||||
Name: dirName,
|
||||
ParentID: parentID,
|
||||
}
|
||||
|
||||
_, err = d.Request(ApiMakeDir, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetBody(reqBody)
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("mkdir failed: %s", result.Message)
|
||||
}
|
||||
|
||||
newDir := File{
|
||||
FileId: result.Data.DirID,
|
||||
FileName: dirName,
|
||||
Type: 1,
|
||||
ParentFileId: int(parentID),
|
||||
Size: 0,
|
||||
Trashed: 0,
|
||||
}
|
||||
return newDir, nil
|
||||
}
|
||||
|
||||
func (d *Open123) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
srcID, err := strconv.ParseInt(srcObj.GetID(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid src file ID: %w", err)
|
||||
}
|
||||
dstID, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid dest dir ID: %w", err)
|
||||
}
|
||||
|
||||
var result Response[any]
|
||||
reqBody := map[string]interface{}{
|
||||
"fileIDs": []int64{srcID},
|
||||
"toParentFileID": dstID,
|
||||
}
|
||||
|
||||
_, err = d.Request(ApiMove, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetBody(reqBody)
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("move failed: %s", result.Message)
|
||||
}
|
||||
|
||||
files, err := d.getFiles(dstID, 100, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("move succeed but failed to get target dir: %w", err)
|
||||
}
|
||||
for _, f := range files.Data.FileList {
|
||||
if f.FileId == srcID {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("move succeed but file not found in target dir")
|
||||
}
|
||||
|
||||
func (d *Open123) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
srcID, err := strconv.ParseInt(srcObj.GetID(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid file ID: %w", err)
|
||||
}
|
||||
|
||||
var result Response[any]
|
||||
reqBody := map[string]interface{}{
|
||||
"fileId": srcID,
|
||||
"fileName": newName,
|
||||
}
|
||||
|
||||
_, err = d.Request(ApiRename, http.MethodPut, func(r *resty.Request) {
|
||||
r.SetBody(reqBody)
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("rename failed: %s", result.Message)
|
||||
}
|
||||
|
||||
parentID := 0
|
||||
if file, ok := srcObj.(File); ok {
|
||||
parentID = file.ParentFileId
|
||||
}
|
||||
files, err := d.getFiles(int64(parentID), 100, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rename succeed but failed to get parent dir: %w", err)
|
||||
}
|
||||
for _, f := range files.Data.FileList {
|
||||
if f.FileId == srcID {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("rename succeed but file not found in parent dir")
|
||||
}
|
||||
|
||||
func (d *Open123) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *Open123) Remove(ctx context.Context, obj model.Obj) error {
|
||||
idStr := obj.GetID()
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid file ID: %w", err)
|
||||
}
|
||||
|
||||
var result Response[any]
|
||||
reqBody := RemoveRequest{
|
||||
FileIDs: []int64{id},
|
||||
}
|
||||
|
||||
_, err = d.Request(ApiRemove, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetBody(reqBody)
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return fmt.Errorf("remove failed: %s", result.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
|
||||
parentFileId, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
|
||||
etag := file.GetHash().GetHash(utils.MD5)
|
||||
|
||||
if len(etag) < utils.MD5.Width {
|
||||
up = model.UpdateProgressWithRange(up, 50, 100)
|
||||
_, etag, err = stream.CacheFullInTempFileAndHash(file, utils.MD5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
createResp, err := d.create(parentFileId, file.GetName(), etag, file.GetSize(), 2, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if createResp.Data.Reuse {
|
||||
return nil
|
||||
}
|
||||
|
||||
return d.Upload(ctx, file, parentFileId, createResp, up)
|
||||
}
|
||||
|
||||
func (d *Open123) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *Open123) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *Open123) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *Open123) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
//func (d *Open123) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
// return nil, errs.NotSupport
|
||||
//}
|
||||
|
||||
var _ driver.Driver = (*Open123)(nil)
|
||||
33
drivers/123_open/meta.go
Normal file
33
drivers/123_open/meta.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package _123Open
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
|
||||
ClientID string `json:"client_id" required:"true" label:"Client ID"`
|
||||
ClientSecret string `json:"client_secret" required:"true" label:"Client Secret"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "123 Open",
|
||||
LocalSort: false,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: false,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "0",
|
||||
CheckStatus: false,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Open123{}
|
||||
})
|
||||
}
|
||||
85
drivers/123_open/token.go
Normal file
85
drivers/123_open/token.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package _123Open
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const tokenURL = ApiBaseURL + ApiToken
|
||||
|
||||
type tokenManager struct {
|
||||
clientID string
|
||||
clientSecret string
|
||||
|
||||
mu sync.Mutex
|
||||
accessToken string
|
||||
expireTime time.Time
|
||||
}
|
||||
|
||||
func newTokenManager(clientID, clientSecret string) *tokenManager {
|
||||
return &tokenManager{
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *tokenManager) getToken() (string, error) {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
if tm.accessToken != "" && time.Now().Before(tm.expireTime.Add(-5*time.Minute)) {
|
||||
return tm.accessToken, nil
|
||||
}
|
||||
|
||||
reqBody := map[string]string{
|
||||
"clientID": tm.clientID,
|
||||
"clientSecret": tm.clientSecret,
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req, err := http.NewRequest("POST", tokenURL, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Platform", "open_platform")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result TokenResp
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.Code != 0 {
|
||||
return "", fmt.Errorf("get token failed: %s", result.Message)
|
||||
}
|
||||
|
||||
tm.accessToken = result.Data.AccessToken
|
||||
expireAt, err := time.Parse(time.RFC3339, result.Data.ExpiredAt)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse expire time failed: %w", err)
|
||||
}
|
||||
tm.expireTime = expireAt
|
||||
|
||||
return tm.accessToken, nil
|
||||
}
|
||||
|
||||
func (tm *tokenManager) buildHeaders() (http.Header, error) {
|
||||
token, err := tm.getToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header := http.Header{}
|
||||
header.Set("Authorization", "Bearer "+token)
|
||||
header.Set("Platform", "open_platform")
|
||||
header.Set("Content-Type", "application/json")
|
||||
return header, nil
|
||||
}
|
||||
70
drivers/123_open/types.go
Normal file
70
drivers/123_open/types.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package _123Open
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
FileName string `json:"filename"`
|
||||
Size int64 `json:"size"`
|
||||
CreateAt string `json:"createAt"`
|
||||
UpdateAt string `json:"updateAt"`
|
||||
FileId int64 `json:"fileId"`
|
||||
Type int `json:"type"`
|
||||
Etag string `json:"etag"`
|
||||
S3KeyFlag string `json:"s3KeyFlag"`
|
||||
ParentFileId int `json:"parentFileId"`
|
||||
Category int `json:"category"`
|
||||
Status int `json:"status"`
|
||||
Trashed int `json:"trashed"`
|
||||
}
|
||||
|
||||
func (f File) GetID() string {
|
||||
return fmt.Sprint(f.FileId)
|
||||
}
|
||||
|
||||
func (f File) GetName() string {
|
||||
return f.FileName
|
||||
}
|
||||
|
||||
func (f File) GetSize() int64 {
|
||||
return f.Size
|
||||
}
|
||||
|
||||
func (f File) IsDir() bool {
|
||||
return f.Type == 1
|
||||
}
|
||||
|
||||
func (f File) GetModified() string {
|
||||
return f.UpdateAt
|
||||
}
|
||||
|
||||
func (f File) GetThumb() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f File) ModTime() time.Time {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", f.UpdateAt)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (f File) CreateTime() time.Time {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", f.CreateAt)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (f File) GetHash() utils.HashInfo {
|
||||
return utils.NewHashInfo(utils.MD5, f.Etag)
|
||||
}
|
||||
|
||||
func (f File) GetPath() string {
|
||||
return ""
|
||||
}
|
||||
282
drivers/123_open/upload.go
Normal file
282
drivers/123_open/upload.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package _123Open
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/stream"
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (d *Open123) create(parentFileID int64, filename, etag string, size int64, duplicate int, containDir bool) (*UploadCreateResp, error) {
|
||||
var resp UploadCreateResp
|
||||
|
||||
_, err := d.Request(ApiCreateUploadURL, http.MethodPost, func(req *resty.Request) {
|
||||
body := base.Json{
|
||||
"parentFileID": parentFileID,
|
||||
"filename": filename,
|
||||
"etag": etag,
|
||||
"size": size,
|
||||
}
|
||||
if duplicate > 0 {
|
||||
body["duplicate"] = duplicate
|
||||
}
|
||||
if containDir {
|
||||
body["containDir"] = true
|
||||
}
|
||||
req.SetBody(body)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (d *Open123) GetUploadDomains() ([]string, error) {
|
||||
var resp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
|
||||
_, err := d.Request(ApiUploadDomainURL, http.MethodGet, nil, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
return nil, fmt.Errorf("get upload domain failed: %s", resp.Message)
|
||||
}
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
func (d *Open123) UploadSingle(ctx context.Context, createResp *UploadCreateResp, file model.FileStreamer, parentID int64) error {
|
||||
domain := createResp.Data.Servers[0]
|
||||
|
||||
etag := file.GetHash().GetHash(utils.MD5)
|
||||
if len(etag) < utils.MD5.Width {
|
||||
_, _, err := stream.CacheFullInTempFileAndHash(file, utils.MD5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
reader, err := file.RangeRead(http_range.Range{Start: 0, Length: file.GetSize()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader = driver.NewLimitedUploadStream(ctx, reader)
|
||||
|
||||
var b bytes.Buffer
|
||||
mw := multipart.NewWriter(&b)
|
||||
mw.WriteField("parentFileID", fmt.Sprint(parentID))
|
||||
mw.WriteField("filename", file.GetName())
|
||||
mw.WriteField("etag", etag)
|
||||
mw.WriteField("size", fmt.Sprint(file.GetSize()))
|
||||
fw, _ := mw.CreateFormFile("file", file.GetName())
|
||||
_, err = io.Copy(fw, reader)
|
||||
mw.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", domain+ApiSingleUploadURL, &b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+d.tm.accessToken)
|
||||
req.Header.Set("Platform", "open_platform")
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
FileID int64 `json:"fileID"`
|
||||
Completed bool `json:"completed"`
|
||||
} `json:"data"`
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return fmt.Errorf("unmarshal response error: %v, body: %s", err, string(body))
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return fmt.Errorf("upload failed: %s", result.Message)
|
||||
}
|
||||
if !result.Data.Completed || result.Data.FileID == 0 {
|
||||
return fmt.Errorf("upload incomplete or missing fileID")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, parentID int64, createResp *UploadCreateResp, up driver.UpdateProgress) error {
|
||||
if cacher, ok := file.(interface{ CacheFullInTempFile() (model.File, error) }); ok {
|
||||
if _, err := cacher.CacheFullInTempFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
size := file.GetSize()
|
||||
chunkSize := createResp.Data.SliceSize
|
||||
uploadNums := (size + chunkSize - 1) / chunkSize
|
||||
uploadDomain := createResp.Data.Servers[0]
|
||||
|
||||
if d.UploadThread <= 0 {
|
||||
cpuCores := runtime.NumCPU()
|
||||
threads := cpuCores * 2
|
||||
if threads < 4 {
|
||||
threads = 4
|
||||
}
|
||||
if threads > 16 {
|
||||
threads = 16
|
||||
}
|
||||
d.UploadThread = threads
|
||||
fmt.Printf("[Upload] Auto set upload concurrency: %d (CPU cores=%d)\n", d.UploadThread, cpuCores)
|
||||
}
|
||||
|
||||
fmt.Printf("[Upload] File size: %d bytes, chunk size: %d bytes, total slices: %d, concurrency: %d\n",
|
||||
size, chunkSize, uploadNums, d.UploadThread)
|
||||
|
||||
if size <= 1<<30 {
|
||||
return d.UploadSingle(ctx, createResp, file, parentID)
|
||||
}
|
||||
|
||||
if createResp.Data.Reuse {
|
||||
up(100)
|
||||
return nil
|
||||
}
|
||||
|
||||
client := resty.New()
|
||||
semaphore := make(chan struct{}, d.UploadThread)
|
||||
threadG, _ := errgroup.WithContext(ctx)
|
||||
|
||||
var progressArr = make([]int64, uploadNums)
|
||||
|
||||
for partIndex := int64(0); partIndex < uploadNums; partIndex++ {
|
||||
partIndex := partIndex
|
||||
semaphore <- struct{}{}
|
||||
|
||||
threadG.Go(func() error {
|
||||
defer func() { <-semaphore }()
|
||||
offset := partIndex * chunkSize
|
||||
length := min(chunkSize, size-offset)
|
||||
partNumber := partIndex + 1
|
||||
|
||||
fmt.Printf("[Slice %d] Starting read from offset %d, length %d\n", partNumber, offset, length)
|
||||
reader, err := file.RangeRead(http_range.Range{Start: offset, Length: length})
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Slice %d] RangeRead error: %v", partNumber, err)
|
||||
}
|
||||
|
||||
buf := make([]byte, length)
|
||||
n, err := io.ReadFull(reader, buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("[Slice %d] Read error: %v", partNumber, err)
|
||||
}
|
||||
buf = buf[:n]
|
||||
hash := md5.Sum(buf)
|
||||
sliceMD5Str := hex.EncodeToString(hash[:])
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
writer.WriteField("preuploadID", createResp.Data.PreuploadID)
|
||||
writer.WriteField("sliceNo", strconv.FormatInt(partNumber, 10))
|
||||
writer.WriteField("sliceMD5", sliceMD5Str)
|
||||
partName := fmt.Sprintf("%s.part%d", file.GetName(), partNumber)
|
||||
fw, _ := writer.CreateFormFile("slice", partName)
|
||||
fw.Write(buf)
|
||||
writer.Close()
|
||||
|
||||
resp, err := client.R().
|
||||
SetHeader("Authorization", "Bearer "+d.tm.accessToken).
|
||||
SetHeader("Platform", "open_platform").
|
||||
SetHeader("Content-Type", writer.FormDataContentType()).
|
||||
SetBody(body.Bytes()).
|
||||
Post(uploadDomain + ApiUploadSliceURL)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Slice %d] Upload HTTP error: %v", partNumber, err)
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return fmt.Errorf("[Slice %d] Upload failed with status: %s, resp: %s", partNumber, resp.Status(), resp.String())
|
||||
}
|
||||
|
||||
progressArr[partIndex] = length
|
||||
var totalUploaded int64 = 0
|
||||
for _, v := range progressArr {
|
||||
totalUploaded += v
|
||||
}
|
||||
if up != nil {
|
||||
percent := float64(totalUploaded) / float64(size) * 100
|
||||
up(percent)
|
||||
}
|
||||
|
||||
fmt.Printf("[Slice %d] MD5: %s\n", partNumber, sliceMD5Str)
|
||||
fmt.Printf("[Slice %d] Upload finished\n", partNumber)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := threadG.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var completeResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Completed bool `json:"completed"`
|
||||
FileID int64 `json:"fileID"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
for {
|
||||
reqBody := fmt.Sprintf(`{"preuploadID":"%s"}`, createResp.Data.PreuploadID)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", uploadDomain+ApiUploadCompleteURL, bytes.NewBufferString(reqBody))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+d.tm.accessToken)
|
||||
req.Header.Set("Platform", "open_platform")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if err := json.Unmarshal(body, &completeResp); err != nil {
|
||||
return fmt.Errorf("completion response unmarshal error: %v, body: %s", err, string(body))
|
||||
}
|
||||
if completeResp.Code != 0 {
|
||||
return fmt.Errorf("completion API returned error code %d: %s", completeResp.Code, completeResp.Message)
|
||||
}
|
||||
if completeResp.Data.Completed && completeResp.Data.FileID != 0 {
|
||||
fmt.Printf("[Upload] Upload completed successfully. FileID: %d\n", completeResp.Data.FileID)
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
up(100)
|
||||
return nil
|
||||
}
|
||||
20
drivers/123_open/util.go
Normal file
20
drivers/123_open/util.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package _123Open
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*FileListResp, error) {
|
||||
var result FileListResp
|
||||
url := fmt.Sprintf("%s?parentFileId=%d&limit=%d&lastFileId=%d", ApiFileList, parentFileId, limit, lastFileId)
|
||||
|
||||
_, err := d.Request(url, http.MethodGet, nil, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("list error: %s", result.Message)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func (d *AListV3) Init(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Data.Role == model.GUEST {
|
||||
if utils.SliceContains(resp.Data.Role, model.GUEST) {
|
||||
u := d.Address + "/api/public/settings"
|
||||
res, err := base.RestyClient.R().Get(u)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package alist_v3
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
@@ -76,7 +77,7 @@ type MeResp struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
BasePath string `json:"base_path"`
|
||||
Role int `json:"role"`
|
||||
Role IntSlice `json:"role"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Permission int `json:"permission"`
|
||||
SsoId string `json:"sso_id"`
|
||||
@@ -168,3 +169,17 @@ type DecompressReq struct {
|
||||
PutIntoNewDir bool `json:"put_into_new_dir"`
|
||||
SrcDir string `json:"src_dir"`
|
||||
}
|
||||
|
||||
type IntSlice []int
|
||||
|
||||
func (s *IntSlice) UnmarshalJSON(data []byte) error {
|
||||
if len(data) > 0 && data[0] == '[' {
|
||||
return json.Unmarshal(data, (*[]int)(s))
|
||||
}
|
||||
var single int
|
||||
if err := json.Unmarshal(data, &single); err != nil {
|
||||
return err
|
||||
}
|
||||
*s = []int{single}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type Addition struct {
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
|
||||
OauthTokenURL string `json:"oauth_token_url" default:"https://api.nn.ci/alist/ali_open/token"`
|
||||
OauthTokenURL string `json:"oauth_token_url" default:"https://api.alistgo.com/alist/ali_open/token"`
|
||||
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
|
||||
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
|
||||
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/115_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123_link"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123_open"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/139"
|
||||
_ "github.com/alist-org/alist/v3/drivers/189"
|
||||
@@ -31,6 +32,7 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
||||
_ "github.com/alist-org/alist/v3/drivers/github"
|
||||
_ "github.com/alist-org/alist/v3/drivers/github_releases"
|
||||
_ "github.com/alist-org/alist/v3/drivers/gofile"
|
||||
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
||||
_ "github.com/alist-org/alist/v3/drivers/google_photo"
|
||||
_ "github.com/alist-org/alist/v3/drivers/halalcloud"
|
||||
@@ -40,6 +42,7 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/lanzou"
|
||||
_ "github.com/alist-org/alist/v3/drivers/lenovonas_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/local"
|
||||
_ "github.com/alist-org/alist/v3/drivers/mediafire"
|
||||
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
||||
_ "github.com/alist-org/alist/v3/drivers/mega"
|
||||
_ "github.com/alist-org/alist/v3/drivers/misskey"
|
||||
|
||||
@@ -11,8 +11,8 @@ type Addition struct {
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
DownloadAPI string `json:"download_api" type:"select" options:"official,crack,crack_video" default:"official"`
|
||||
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:"hq9yQ9w9kR4YHj1kyYafLygVocobh7Sf"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"YH2VpZcFJHYNnV6vLfHQXDBhcE7ZChyE"`
|
||||
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
||||
AccessToken string
|
||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
@@ -36,37 +37,35 @@ func (d *GithubReleases) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files := make([]File, 0)
|
||||
path := fmt.Sprintf("/%s", strings.Trim(dir.GetPath(), "/"))
|
||||
|
||||
for i := range d.points {
|
||||
point := &d.points[i]
|
||||
// processPoint 处理单个挂载点的文件列表
|
||||
func (d *GithubReleases) processPoint(point *MountPoint, path string, args model.ListArgs) []File {
|
||||
var pointFiles []File
|
||||
|
||||
if !d.Addition.ShowAllVersion { // latest
|
||||
point.RequestRelease(d.GetRequest, args.Refresh)
|
||||
point.RequestLatestRelease(d.GetRequest, args.Refresh)
|
||||
pointFiles = d.processLatestVersion(point, path)
|
||||
} else { // all version
|
||||
point.RequestReleases(d.GetRequest, args.Refresh)
|
||||
pointFiles = d.processAllVersions(point, path)
|
||||
}
|
||||
|
||||
return pointFiles
|
||||
}
|
||||
|
||||
// processLatestVersion 处理最新版本的逻辑
|
||||
func (d *GithubReleases) processLatestVersion(point *MountPoint, path string) []File {
|
||||
var pointFiles []File
|
||||
|
||||
if point.Point == path { // 与仓库路径相同
|
||||
files = append(files, point.GetLatestRelease()...)
|
||||
pointFiles = append(pointFiles, point.GetLatestRelease()...)
|
||||
if d.Addition.ShowReadme {
|
||||
files = append(files, point.GetOtherFile(d.GetRequest, args.Refresh)...)
|
||||
files := point.GetOtherFile(d.GetRequest, false)
|
||||
pointFiles = append(pointFiles, files...)
|
||||
}
|
||||
} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录
|
||||
nextDir := GetNextDir(point.Point, path)
|
||||
if nextDir == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
hasSameDir := false
|
||||
for index := range files {
|
||||
if files[index].GetName() == nextDir {
|
||||
hasSameDir = true
|
||||
files[index].Size += point.GetLatestSize()
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSameDir {
|
||||
files = append(files, File{
|
||||
if nextDir != "" {
|
||||
dirFile := File{
|
||||
Path: path + "/" + nextDir,
|
||||
FileName: nextDir,
|
||||
Size: point.GetLatestSize(),
|
||||
@@ -74,33 +73,28 @@ func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.Lis
|
||||
CreateAt: point.Release.CreatedAt,
|
||||
Type: "dir",
|
||||
Url: "",
|
||||
})
|
||||
}
|
||||
pointFiles = append(pointFiles, dirFile)
|
||||
}
|
||||
}
|
||||
} else { // all version
|
||||
point.RequestReleases(d.GetRequest, args.Refresh)
|
||||
|
||||
return pointFiles
|
||||
}
|
||||
|
||||
// processAllVersions 处理所有版本的逻辑
|
||||
func (d *GithubReleases) processAllVersions(point *MountPoint, path string) []File {
|
||||
var pointFiles []File
|
||||
|
||||
if point.Point == path { // 与仓库路径相同
|
||||
files = append(files, point.GetAllVersion()...)
|
||||
pointFiles = append(pointFiles, point.GetAllVersion()...)
|
||||
if d.Addition.ShowReadme {
|
||||
files = append(files, point.GetOtherFile(d.GetRequest, args.Refresh)...)
|
||||
files := point.GetOtherFile(d.GetRequest, false)
|
||||
pointFiles = append(pointFiles, files...)
|
||||
}
|
||||
} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录
|
||||
nextDir := GetNextDir(point.Point, path)
|
||||
if nextDir == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
hasSameDir := false
|
||||
for index := range files {
|
||||
if files[index].GetName() == nextDir {
|
||||
hasSameDir = true
|
||||
files[index].Size += point.GetAllVersionSize()
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSameDir {
|
||||
files = append(files, File{
|
||||
if nextDir != "" {
|
||||
dirFile := File{
|
||||
FileName: nextDir,
|
||||
Path: path + "/" + nextDir,
|
||||
Size: point.GetAllVersionSize(),
|
||||
@@ -108,16 +102,65 @@ func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.Lis
|
||||
CreateAt: (*point.Releases)[0].CreatedAt,
|
||||
Type: "dir",
|
||||
Url: "",
|
||||
})
|
||||
}
|
||||
pointFiles = append(pointFiles, dirFile)
|
||||
}
|
||||
} else if strings.HasPrefix(path, point.Point) { // 仓库目录的子目录
|
||||
tagName := GetNextDir(path, point.Point)
|
||||
if tagName == "" {
|
||||
continue
|
||||
if tagName != "" {
|
||||
pointFiles = append(pointFiles, point.GetReleaseByTagName(tagName)...)
|
||||
}
|
||||
}
|
||||
|
||||
files = append(files, point.GetReleaseByTagName(tagName)...)
|
||||
return pointFiles
|
||||
}
|
||||
|
||||
// mergeFiles 合并文件列表,处理重复目录
|
||||
func (d *GithubReleases) mergeFiles(files *[]File, newFiles []File) {
|
||||
for _, newFile := range newFiles {
|
||||
if newFile.Type == "dir" {
|
||||
hasSameDir := false
|
||||
for index := range *files {
|
||||
if (*files)[index].GetName() == newFile.GetName() && (*files)[index].Type == "dir" {
|
||||
hasSameDir = true
|
||||
(*files)[index].Size += newFile.Size
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSameDir {
|
||||
*files = append(*files, newFile)
|
||||
}
|
||||
} else {
|
||||
*files = append(*files, newFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files := make([]File, 0)
|
||||
path := fmt.Sprintf("/%s", strings.Trim(dir.GetPath(), "/"))
|
||||
|
||||
if d.Addition.ConcurrentRequests && d.Addition.Token != "" { // 并发处理
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range d.points {
|
||||
wg.Add(1)
|
||||
go func(point *MountPoint) {
|
||||
defer wg.Done()
|
||||
pointFiles := d.processPoint(point, path, args)
|
||||
|
||||
mu.Lock()
|
||||
d.mergeFiles(&files, pointFiles)
|
||||
mu.Unlock()
|
||||
}(&d.points[i])
|
||||
}
|
||||
wg.Wait()
|
||||
} else { // 串行处理
|
||||
for i := range d.points {
|
||||
point := &d.points[i]
|
||||
pointFiles := d.processPoint(point, path, args)
|
||||
d.mergeFiles(&files, pointFiles)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ type Addition struct {
|
||||
ShowReadme bool `json:"show_readme" type:"bool" default:"true" help:"show README、LICENSE file"`
|
||||
Token string `json:"token" type:"string" required:"false" help:"GitHub token, if you want to access private repositories or increase the rate limit"`
|
||||
ShowAllVersion bool `json:"show_all_version" type:"bool" default:"false" help:"show all versions"`
|
||||
ConcurrentRequests bool `json:"concurrent_requests" type:"bool" default:"false" help:"To concurrently request the GitHub API, you must enter a GitHub token"`
|
||||
GitHubProxy string `json:"gh_proxy" type:"string" default:"" help:"GitHub proxy, e.g. https://ghproxy.net/github.com or https://gh-proxy.com/github.com "`
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ type MountPoint struct {
|
||||
}
|
||||
|
||||
// 请求最新版本
|
||||
func (m *MountPoint) RequestRelease(get func(url string) (*resty.Response, error), refresh bool) {
|
||||
func (m *MountPoint) RequestLatestRelease(get func(url string) (*resty.Response, error), refresh bool) {
|
||||
if m.Repo == "" {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 发送 GET 请求
|
||||
@@ -23,7 +23,7 @@ func (d *GithubReleases) GetRequest(url string) (*resty.Response, error) {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode() != 200 {
|
||||
log.Warn("failed to get request: ", res.StatusCode(), res.String())
|
||||
utils.Log.Warnf("failed to get request: %s %d %s", url, res.StatusCode(), res.String())
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
261
drivers/gofile/driver.go
Normal file
261
drivers/gofile/driver.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package gofile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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/internal/op"
|
||||
)
|
||||
|
||||
type Gofile struct {
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
accountId string
|
||||
}
|
||||
|
||||
func (d *Gofile) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *Gofile) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *Gofile) Init(ctx context.Context) error {
|
||||
if d.APIToken == "" {
|
||||
return fmt.Errorf("API token is required")
|
||||
}
|
||||
|
||||
// Get account ID
|
||||
accountId, err := d.getAccountId(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get account ID: %w", err)
|
||||
}
|
||||
d.accountId = accountId
|
||||
|
||||
// Get account info to set root folder if not specified
|
||||
if d.RootFolderID == "" {
|
||||
accountInfo, err := d.getAccountInfo(ctx, accountId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get account info: %w", err)
|
||||
}
|
||||
d.RootFolderID = accountInfo.Data.RootFolder
|
||||
}
|
||||
|
||||
// Save driver storage
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Gofile) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
var folderId string
|
||||
if dir.GetID() == "" {
|
||||
folderId = d.GetRootId()
|
||||
} else {
|
||||
folderId = dir.GetID()
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/contents/%s", folderId)
|
||||
|
||||
var response ContentsResponse
|
||||
err := d.getJSON(ctx, endpoint, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var objects []model.Obj
|
||||
|
||||
// Process children or contents
|
||||
contents := response.Data.Children
|
||||
if contents == nil {
|
||||
contents = response.Data.Contents
|
||||
}
|
||||
|
||||
for _, content := range contents {
|
||||
objects = append(objects, d.convertContentToObj(content))
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
if file.IsDir() {
|
||||
return nil, errs.NotFile
|
||||
}
|
||||
|
||||
// Create a direct link for the file
|
||||
directLink, err := d.createDirectLink(ctx, file.GetID())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create direct link: %w", err)
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: directLink,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
var parentId string
|
||||
if parentDir.GetID() == "" {
|
||||
parentId = d.GetRootId()
|
||||
} else {
|
||||
parentId = parentDir.GetID()
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"parentFolderId": parentId,
|
||||
"folderName": dirName,
|
||||
}
|
||||
|
||||
var response CreateFolderResponse
|
||||
err := d.postJSON(ctx, "/contents/createFolder", data, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: response.Data.ID,
|
||||
Name: response.Data.Name,
|
||||
IsFolder: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
var dstId string
|
||||
if dstDir.GetID() == "" {
|
||||
dstId = d.GetRootId()
|
||||
} else {
|
||||
dstId = dstDir.GetID()
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"contentsId": srcObj.GetID(),
|
||||
"folderId": dstId,
|
||||
}
|
||||
|
||||
err := d.putJSON(ctx, "/contents/move", data, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return updated object
|
||||
return &model.Object{
|
||||
ID: srcObj.GetID(),
|
||||
Name: srcObj.GetName(),
|
||||
Size: srcObj.GetSize(),
|
||||
Modified: srcObj.ModTime(),
|
||||
IsFolder: srcObj.IsDir(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
data := map[string]interface{}{
|
||||
"attribute": "name",
|
||||
"attributeValue": newName,
|
||||
}
|
||||
|
||||
var response UpdateResponse
|
||||
err := d.putJSON(ctx, fmt.Sprintf("/contents/%s/update", srcObj.GetID()), data, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: srcObj.GetID(),
|
||||
Name: newName,
|
||||
Size: srcObj.GetSize(),
|
||||
Modified: srcObj.ModTime(),
|
||||
IsFolder: srcObj.IsDir(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
var dstId string
|
||||
if dstDir.GetID() == "" {
|
||||
dstId = d.GetRootId()
|
||||
} else {
|
||||
dstId = dstDir.GetID()
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"contentsId": srcObj.GetID(),
|
||||
"folderId": dstId,
|
||||
}
|
||||
|
||||
var response CopyResponse
|
||||
err := d.postJSON(ctx, "/contents/copy", data, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the new ID from the response
|
||||
newId := srcObj.GetID()
|
||||
if response.Data.CopiedContents != nil {
|
||||
if id, ok := response.Data.CopiedContents[srcObj.GetID()]; ok {
|
||||
newId = id
|
||||
}
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: newId,
|
||||
Name: srcObj.GetName(),
|
||||
Size: srcObj.GetSize(),
|
||||
Modified: srcObj.ModTime(),
|
||||
IsFolder: srcObj.IsDir(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Remove(ctx context.Context, obj model.Obj) error {
|
||||
data := map[string]interface{}{
|
||||
"contentsId": obj.GetID(),
|
||||
}
|
||||
|
||||
return d.deleteJSON(ctx, "/contents", data)
|
||||
}
|
||||
|
||||
func (d *Gofile) Put(ctx context.Context, dstDir model.Obj, fileStreamer model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
var folderId string
|
||||
if dstDir.GetID() == "" {
|
||||
folderId = d.GetRootId()
|
||||
} else {
|
||||
folderId = dstDir.GetID()
|
||||
}
|
||||
|
||||
response, err := d.uploadFile(ctx, folderId, fileStreamer, up)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: response.Data.FileId,
|
||||
Name: response.Data.FileName,
|
||||
Size: fileStreamer.GetSize(),
|
||||
IsFolder: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Gofile) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Gofile) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Gofile) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Gofile)(nil)
|
||||
26
drivers/gofile/meta.go
Normal file
26
drivers/gofile/meta.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package gofile
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
APIToken string `json:"api_token" required:"true" help:"Get your API token from your Gofile profile page"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Gofile",
|
||||
DefaultRoot: "",
|
||||
LocalSort: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Gofile{}
|
||||
})
|
||||
}
|
||||
124
drivers/gofile/types.go
Normal file
124
drivers/gofile/types.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package gofile
|
||||
|
||||
import "time"
|
||||
|
||||
type APIResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type AccountResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type AccountInfoResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Email string `json:"email"`
|
||||
RootFolder string `json:"rootFolder"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type Content struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // "file" or "folder"
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
ModTime int64 `json:"modTime,omitempty"`
|
||||
DirectLink string `json:"directLink,omitempty"`
|
||||
Children map[string]Content `json:"children,omitempty"`
|
||||
ParentFolder string `json:"parentFolder,omitempty"`
|
||||
MD5 string `json:"md5,omitempty"`
|
||||
MimeType string `json:"mimeType,omitempty"`
|
||||
Link string `json:"link,omitempty"`
|
||||
}
|
||||
|
||||
type ContentsResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
IsOwner bool `json:"isOwner"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
ParentFolder string `json:"parentFolder"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
ChildrenList []string `json:"childrenList,omitempty"`
|
||||
Children map[string]Content `json:"children,omitempty"`
|
||||
Contents map[string]Content `json:"contents,omitempty"`
|
||||
Public bool `json:"public,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
Expiry int64 `json:"expiry,omitempty"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type UploadResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
DownloadPage string `json:"downloadPage"`
|
||||
Code string `json:"code"`
|
||||
ParentFolder string `json:"parentFolder"`
|
||||
FileId string `json:"fileId"`
|
||||
FileName string `json:"fileName"`
|
||||
GuestToken string `json:"guestToken,omitempty"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type DirectLinkResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
DirectLink string `json:"directLink"`
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type CreateFolderResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
ParentFolder string `json:"parentFolder"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type CopyResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
CopiedContents map[string]string `json:"copiedContents"` // oldId -> newId mapping
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type UpdateResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Status string `json:"status"`
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
Code string `json:"code"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (c *Content) ModifiedTime() time.Time {
|
||||
if c.ModTime > 0 {
|
||||
return time.Unix(c.ModTime, 0)
|
||||
}
|
||||
return time.Unix(c.CreateTime, 0)
|
||||
}
|
||||
|
||||
func (c *Content) IsDir() bool {
|
||||
return c.Type == "folder"
|
||||
}
|
||||
257
drivers/gofile/util.go
Normal file
257
drivers/gofile/util.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package gofile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
const (
|
||||
baseAPI = "https://api.gofile.io"
|
||||
uploadAPI = "https://upload.gofile.io"
|
||||
)
|
||||
|
||||
func (d *Gofile) request(ctx context.Context, method, endpoint string, body io.Reader, headers map[string]string) (*http.Response, error) {
|
||||
var url string
|
||||
if strings.HasPrefix(endpoint, "http") {
|
||||
url = endpoint
|
||||
} else {
|
||||
url = baseAPI + endpoint
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+d.APIToken)
|
||||
req.Header.Set("User-Agent", "AList/3.0")
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
return base.HttpClient.Do(req)
|
||||
}
|
||||
|
||||
func (d *Gofile) getJSON(ctx context.Context, endpoint string, result interface{}) error {
|
||||
resp, err := d.request(ctx, "GET", endpoint, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return d.handleError(resp)
|
||||
}
|
||||
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
|
||||
func (d *Gofile) postJSON(ctx context.Context, endpoint string, data interface{}, result interface{}) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
resp, err := d.request(ctx, "POST", endpoint, bytes.NewBuffer(jsonData), headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return d.handleError(resp)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Gofile) putJSON(ctx context.Context, endpoint string, data interface{}, result interface{}) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
resp, err := d.request(ctx, "PUT", endpoint, bytes.NewBuffer(jsonData), headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return d.handleError(resp)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Gofile) deleteJSON(ctx context.Context, endpoint string, data interface{}) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
resp, err := d.request(ctx, "DELETE", endpoint, bytes.NewBuffer(jsonData), headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return d.handleError(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Gofile) handleError(resp *http.Response) error {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var errorResp ErrorResponse
|
||||
if err := json.Unmarshal(body, &errorResp); err == nil {
|
||||
return fmt.Errorf("gofile API error: %s (code: %s)", errorResp.Error.Message, errorResp.Error.Code)
|
||||
}
|
||||
|
||||
return fmt.Errorf("gofile API error: HTTP %d - %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
func (d *Gofile) uploadFile(ctx context.Context, folderId string, file model.FileStreamer, up driver.UpdateProgress) (*UploadResponse, error) {
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
|
||||
if folderId != "" {
|
||||
writer.WriteField("folderId", folderId)
|
||||
}
|
||||
|
||||
part, err := writer.CreateFormFile("file", filepath.Base(file.GetName()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Copy with progress tracking if available
|
||||
if up != nil {
|
||||
reader := &progressReader{
|
||||
reader: file,
|
||||
total: file.GetSize(),
|
||||
up: up,
|
||||
}
|
||||
_, err = io.Copy(part, reader)
|
||||
} else {
|
||||
_, err = io.Copy(part, file)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writer.Close()
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": writer.FormDataContentType(),
|
||||
}
|
||||
|
||||
resp, err := d.request(ctx, "POST", uploadAPI+"/uploadfile", &body, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, d.handleError(resp)
|
||||
}
|
||||
|
||||
var result UploadResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (d *Gofile) createDirectLink(ctx context.Context, contentId string) (string, error) {
|
||||
data := map[string]interface{}{}
|
||||
|
||||
var result DirectLinkResponse
|
||||
err := d.postJSON(ctx, fmt.Sprintf("/contents/%s/directlinks", contentId), data, &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.Data.DirectLink, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) convertContentToObj(content Content) model.Obj {
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: content.ID,
|
||||
Name: content.Name,
|
||||
Size: content.Size,
|
||||
Modified: content.ModifiedTime(),
|
||||
IsFolder: content.IsDir(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Gofile) getAccountId(ctx context.Context) (string, error) {
|
||||
var result AccountResponse
|
||||
err := d.getJSON(ctx, "/accounts/getid", &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.Data.ID, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) getAccountInfo(ctx context.Context, accountId string) (*AccountInfoResponse, error) {
|
||||
var result AccountInfoResponse
|
||||
err := d.getJSON(ctx, fmt.Sprintf("/accounts/%s", accountId), &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// progressReader wraps an io.Reader to track upload progress
|
||||
type progressReader struct {
|
||||
reader io.Reader
|
||||
total int64
|
||||
read int64
|
||||
up driver.UpdateProgress
|
||||
}
|
||||
|
||||
func (pr *progressReader) Read(p []byte) (n int, err error) {
|
||||
n, err = pr.reader.Read(p)
|
||||
pr.read += int64(n)
|
||||
if pr.up != nil && pr.total > 0 {
|
||||
progress := float64(pr.read) * 100 / float64(pr.total)
|
||||
pr.up(progress)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
@@ -94,6 +94,7 @@ func RemoveJSComment(data string) string {
|
||||
}
|
||||
if inComment && v == '*' && i+1 < len(data) && data[i+1] == '/' {
|
||||
inComment = false
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if v == '/' && i+1 < len(data) {
|
||||
@@ -108,6 +109,9 @@ func RemoveJSComment(data string) string {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if inComment || inSingleLineComment {
|
||||
continue
|
||||
}
|
||||
result.WriteByte(v)
|
||||
}
|
||||
|
||||
|
||||
433
drivers/mediafire/driver.go
Normal file
433
drivers/mediafire/driver.go
Normal file
@@ -0,0 +1,433 @@
|
||||
package mediafire
|
||||
|
||||
/*
|
||||
Package mediafire
|
||||
Author: Da3zKi7<da3zki7@duck.com>
|
||||
Date: 2025-09-11
|
||||
|
||||
D@' 3z K!7 - The King Of Cracking
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/cron"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
type Mediafire struct {
|
||||
model.Storage
|
||||
Addition
|
||||
cron *cron.Cron
|
||||
|
||||
actionToken string
|
||||
|
||||
appBase string
|
||||
apiBase string
|
||||
hostBase string
|
||||
maxRetries int
|
||||
|
||||
secChUa string
|
||||
secChUaPlatform string
|
||||
userAgent string
|
||||
}
|
||||
|
||||
func (d *Mediafire) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *Mediafire) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *Mediafire) Init(ctx context.Context) error {
|
||||
if d.SessionToken == "" {
|
||||
return fmt.Errorf("Init :: [MediaFire] {critical} missing sessionToken")
|
||||
}
|
||||
|
||||
if d.Cookie == "" {
|
||||
return fmt.Errorf("Init :: [MediaFire] {critical} missing Cookie")
|
||||
}
|
||||
|
||||
if _, err := d.getSessionToken(ctx); err != nil {
|
||||
|
||||
d.renewToken(ctx)
|
||||
|
||||
num := rand.Intn(4) + 6
|
||||
|
||||
d.cron = cron.NewCron(time.Minute * time.Duration(num))
|
||||
d.cron.Do(func() {
|
||||
d.renewToken(ctx)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files, err := d.getFiles(ctx, dir.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
||||
return d.fileToObj(src), nil
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Mediafire) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
|
||||
downloadUrl, err := d.getDirectDownloadLink(ctx, file.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := base.NoRedirectClient.R().SetDoNotParseResponse(true).SetContext(ctx).Get(downloadUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = res.RawBody().Close()
|
||||
}()
|
||||
|
||||
if res.StatusCode() == 302 {
|
||||
downloadUrl = res.Header().Get("location")
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: downloadUrl,
|
||||
Header: http.Header{
|
||||
"Origin": []string{d.appBase},
|
||||
"Referer": []string{d.appBase + "/"},
|
||||
"sec-ch-ua": []string{d.secChUa},
|
||||
"sec-ch-ua-platform": []string{d.secChUaPlatform},
|
||||
"User-Agent": []string{d.userAgent},
|
||||
//"User-Agent": []string{base.UserAgent},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
data := map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
"parent_key": parentDir.GetID(),
|
||||
"foldername": dirName,
|
||||
}
|
||||
|
||||
var resp MediafireFolderCreateResponse
|
||||
_, err := d.postForm("/folder/create.php", data, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return nil, fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
created, _ := time.Parse("2006-01-02T15:04:05Z", resp.Response.CreatedUTC)
|
||||
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: resp.Response.FolderKey,
|
||||
Name: resp.Response.Name,
|
||||
Size: 0,
|
||||
Modified: created,
|
||||
Ctime: created,
|
||||
IsFolder: true,
|
||||
},
|
||||
Thumbnail: model.Thumbnail{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
var data map[string]string
|
||||
var endpoint string
|
||||
|
||||
if srcObj.IsDir() {
|
||||
|
||||
endpoint = "/folder/move.php"
|
||||
data = map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
"folder_key_src": srcObj.GetID(),
|
||||
"folder_key_dst": dstDir.GetID(),
|
||||
}
|
||||
} else {
|
||||
|
||||
endpoint = "/file/move.php"
|
||||
data = map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
"quick_key": srcObj.GetID(),
|
||||
"folder_key": dstDir.GetID(),
|
||||
}
|
||||
}
|
||||
|
||||
var resp MediafireMoveResponse
|
||||
_, err := d.postForm(endpoint, data, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return nil, fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
return srcObj, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
var data map[string]string
|
||||
var endpoint string
|
||||
|
||||
if srcObj.IsDir() {
|
||||
|
||||
endpoint = "/folder/update.php"
|
||||
data = map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
"folder_key": srcObj.GetID(),
|
||||
"foldername": newName,
|
||||
}
|
||||
} else {
|
||||
|
||||
endpoint = "/file/update.php"
|
||||
data = map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
"quick_key": srcObj.GetID(),
|
||||
"filename": newName,
|
||||
}
|
||||
}
|
||||
|
||||
var resp MediafireRenameResponse
|
||||
_, err := d.postForm(endpoint, data, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return nil, fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: srcObj.GetID(),
|
||||
Name: newName,
|
||||
Size: srcObj.GetSize(),
|
||||
Modified: srcObj.ModTime(),
|
||||
Ctime: srcObj.CreateTime(),
|
||||
IsFolder: srcObj.IsDir(),
|
||||
},
|
||||
Thumbnail: model.Thumbnail{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
var data map[string]string
|
||||
var endpoint string
|
||||
|
||||
if srcObj.IsDir() {
|
||||
|
||||
endpoint = "/folder/copy.php"
|
||||
data = map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
"folder_key_src": srcObj.GetID(),
|
||||
"folder_key_dst": dstDir.GetID(),
|
||||
}
|
||||
} else {
|
||||
|
||||
endpoint = "/file/copy.php"
|
||||
data = map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
"quick_key": srcObj.GetID(),
|
||||
"folder_key": dstDir.GetID(),
|
||||
}
|
||||
}
|
||||
|
||||
var resp MediafireCopyResponse
|
||||
_, err := d.postForm(endpoint, data, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return nil, fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
var newID string
|
||||
if srcObj.IsDir() {
|
||||
if len(resp.Response.NewFolderKeys) > 0 {
|
||||
newID = resp.Response.NewFolderKeys[0]
|
||||
}
|
||||
} else {
|
||||
if len(resp.Response.NewQuickKeys) > 0 {
|
||||
newID = resp.Response.NewQuickKeys[0]
|
||||
}
|
||||
}
|
||||
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: newID,
|
||||
Name: srcObj.GetName(),
|
||||
Size: srcObj.GetSize(),
|
||||
Modified: srcObj.ModTime(),
|
||||
Ctime: srcObj.CreateTime(),
|
||||
IsFolder: srcObj.IsDir(),
|
||||
},
|
||||
Thumbnail: model.Thumbnail{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) Remove(ctx context.Context, obj model.Obj) error {
|
||||
var data map[string]string
|
||||
var endpoint string
|
||||
|
||||
if obj.IsDir() {
|
||||
|
||||
endpoint = "/folder/delete.php"
|
||||
data = map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
"folder_key": obj.GetID(),
|
||||
}
|
||||
} else {
|
||||
|
||||
endpoint = "/file/delete.php"
|
||||
data = map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
"quick_key": obj.GetID(),
|
||||
}
|
||||
}
|
||||
|
||||
var resp MediafireRemoveResponse
|
||||
_, err := d.postForm(endpoint, data, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
|
||||
_, err := d.PutResult(ctx, dstDir, file, up)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Mediafire) PutResult(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
|
||||
tempFile, err := file.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
osFile, ok := tempFile.(*os.File)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected *os.File, got %T", tempFile)
|
||||
}
|
||||
|
||||
fileHash, err := d.calculateSHA256(osFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
checkResp, err := d.uploadCheck(ctx, file.GetName(), file.GetSize(), fileHash, dstDir.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if checkResp.Response.ResumableUpload.AllUnitsReady == "yes" {
|
||||
up(100.0)
|
||||
}
|
||||
|
||||
if checkResp.Response.HashExists == "yes" && checkResp.Response.InAccount == "yes" {
|
||||
up(100.0)
|
||||
existingFile, err := d.getExistingFileInfo(ctx, fileHash, file.GetName(), dstDir.GetID())
|
||||
if err == nil {
|
||||
return existingFile, nil
|
||||
}
|
||||
}
|
||||
|
||||
var pollKey string
|
||||
|
||||
if checkResp.Response.ResumableUpload.AllUnitsReady != "yes" {
|
||||
|
||||
var err error
|
||||
|
||||
pollKey, err = d.uploadUnits(ctx, osFile, checkResp, file.GetName(), fileHash, dstDir.GetID(), up)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
|
||||
pollKey = checkResp.Response.ResumableUpload.UploadKey
|
||||
}
|
||||
|
||||
//fmt.Printf("pollKey: %+v\n", pollKey)
|
||||
|
||||
pollResp, err := d.pollUpload(ctx, pollKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quickKey := pollResp.Response.Doupload.QuickKey
|
||||
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: quickKey,
|
||||
Name: file.GetName(),
|
||||
Size: file.GetSize(),
|
||||
},
|
||||
Thumbnail: model.Thumbnail{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||
// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Mediafire) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||
// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Mediafire) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||
// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Mediafire) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
|
||||
// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional
|
||||
// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir
|
||||
// return errs.NotImplement to use an internal archive tool
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
//func (d *Mediafire) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
// return nil, errs.NotSupport
|
||||
//}
|
||||
|
||||
var _ driver.Driver = (*Mediafire)(nil)
|
||||
54
drivers/mediafire/meta.go
Normal file
54
drivers/mediafire/meta.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package mediafire
|
||||
|
||||
/*
|
||||
Package mediafire
|
||||
Author: Da3zKi7<da3zki7@duck.com>
|
||||
Date: 2025-09-11
|
||||
|
||||
D@' 3z K!7 - The King Of Cracking
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootPath
|
||||
//driver.RootID
|
||||
|
||||
SessionToken string `json:"session_token" required:"true" type:"string" help:"Required for MediaFire API"`
|
||||
Cookie string `json:"cookie" required:"true" type:"string" help:"Required for navigation"`
|
||||
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
ChunkSize int64 `json:"chunk_size" type:"number" default:"100"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "MediaFire",
|
||||
LocalSort: false,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: false,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "/",
|
||||
CheckStatus: false,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Mediafire{
|
||||
appBase: "https://app.mediafire.com",
|
||||
apiBase: "https://www.mediafire.com/api/1.5",
|
||||
hostBase: "https://www.mediafire.com",
|
||||
maxRetries: 3,
|
||||
secChUa: "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"139\", \"Google Chrome\";v=\"139\"",
|
||||
secChUaPlatform: "Windows",
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
||||
}
|
||||
})
|
||||
}
|
||||
232
drivers/mediafire/types.go
Normal file
232
drivers/mediafire/types.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package mediafire
|
||||
|
||||
/*
|
||||
Package mediafire
|
||||
Author: Da3zKi7<da3zki7@duck.com>
|
||||
Date: 2025-09-11
|
||||
|
||||
D@' 3z K!7 - The King Of Cracking
|
||||
*/
|
||||
|
||||
type MediafireRenewTokenResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
SessionToken string `json:"session_token"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type MediafireResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
FolderContent struct {
|
||||
ChunkSize string `json:"chunk_size"`
|
||||
ContentType string `json:"content_type"`
|
||||
ChunkNumber string `json:"chunk_number"`
|
||||
FolderKey string `json:"folderkey"`
|
||||
Folders []MediafireFolder `json:"folders,omitempty"`
|
||||
Files []MediafireFile `json:"files,omitempty"`
|
||||
MoreChunks string `json:"more_chunks"`
|
||||
} `json:"folder_content"`
|
||||
Result string `json:"result"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type MediafireFolder struct {
|
||||
FolderKey string `json:"folderkey"`
|
||||
Name string `json:"name"`
|
||||
Created string `json:"created"`
|
||||
CreatedUTC string `json:"created_utc"`
|
||||
}
|
||||
|
||||
type MediafireFile struct {
|
||||
QuickKey string `json:"quickkey"`
|
||||
Filename string `json:"filename"`
|
||||
Size string `json:"size"`
|
||||
Created string `json:"created"`
|
||||
CreatedUTC string `json:"created_utc"`
|
||||
MimeType string `json:"mimetype"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
ID string
|
||||
Name string
|
||||
Size int64
|
||||
CreatedUTC string
|
||||
IsFolder bool
|
||||
}
|
||||
|
||||
type FolderContentResponse struct {
|
||||
Folders []MediafireFolder
|
||||
Files []MediafireFile
|
||||
MoreChunks bool
|
||||
}
|
||||
|
||||
type MediafireLinksResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
Links []struct {
|
||||
QuickKey string `json:"quickkey"`
|
||||
View string `json:"view"`
|
||||
NormalDownload string `json:"normal_download"`
|
||||
OneTime struct {
|
||||
Download string `json:"download"`
|
||||
View string `json:"view"`
|
||||
} `json:"one_time"`
|
||||
} `json:"links"`
|
||||
OneTimeKeyRequestCount string `json:"one_time_key_request_count"`
|
||||
OneTimeKeyRequestMaxCount string `json:"one_time_key_request_max_count"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type MediafireDirectDownloadResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
Links []struct {
|
||||
QuickKey string `json:"quickkey"`
|
||||
DirectDownload string `json:"direct_download"`
|
||||
} `json:"links"`
|
||||
DirectDownloadFreeBandwidth string `json:"direct_download_free_bandwidth"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type MediafireFolderCreateResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
FolderKey string `json:"folder_key"`
|
||||
UploadKey string `json:"upload_key"`
|
||||
ParentFolderKey string `json:"parent_folderkey"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Created string `json:"created"`
|
||||
CreatedUTC string `json:"created_utc"`
|
||||
Privacy string `json:"privacy"`
|
||||
FileCount string `json:"file_count"`
|
||||
FolderCount string `json:"folder_count"`
|
||||
Revision string `json:"revision"`
|
||||
DropboxEnabled string `json:"dropbox_enabled"`
|
||||
Flag string `json:"flag"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
NewDeviceRevision int `json:"new_device_revision"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type MediafireMoveResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
Asynchronous string `json:"asynchronous,omitempty"`
|
||||
NewNames []string `json:"new_names"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
NewDeviceRevision int `json:"new_device_revision"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type MediafireRenameResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
Asynchronous string `json:"asynchronous,omitempty"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
NewDeviceRevision int `json:"new_device_revision"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type MediafireCopyResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
Asynchronous string `json:"asynchronous,omitempty"`
|
||||
NewQuickKeys []string `json:"new_quickkeys,omitempty"`
|
||||
NewFolderKeys []string `json:"new_folderkeys,omitempty"`
|
||||
SkippedCount string `json:"skipped_count,omitempty"`
|
||||
OtherCount string `json:"other_count,omitempty"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
NewDeviceRevision int `json:"new_device_revision"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type MediafireRemoveResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
Asynchronous string `json:"asynchronous,omitempty"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
NewDeviceRevision int `json:"new_device_revision"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type MediafireCheckResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
HashExists string `json:"hash_exists"`
|
||||
InAccount string `json:"in_account"`
|
||||
InFolder string `json:"in_folder"`
|
||||
FileExists string `json:"file_exists"`
|
||||
ResumableUpload struct {
|
||||
AllUnitsReady string `json:"all_units_ready"`
|
||||
NumberOfUnits string `json:"number_of_units"`
|
||||
UnitSize string `json:"unit_size"`
|
||||
Bitmap struct {
|
||||
Count string `json:"count"`
|
||||
Words []string `json:"words"`
|
||||
} `json:"bitmap"`
|
||||
UploadKey string `json:"upload_key"`
|
||||
} `json:"resumable_upload"`
|
||||
AvailableSpace string `json:"available_space"`
|
||||
UsedStorageSize string `json:"used_storage_size"`
|
||||
StorageLimit string `json:"storage_limit"`
|
||||
StorageLimitExceeded string `json:"storage_limit_exceeded"`
|
||||
UploadURL struct {
|
||||
Simple string `json:"simple"`
|
||||
SimpleFallback string `json:"simple_fallback"`
|
||||
Resumable string `json:"resumable"`
|
||||
ResumableFallback string `json:"resumable_fallback"`
|
||||
} `json:"upload_url"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
} `json:"response"`
|
||||
}
|
||||
type MediafireActionTokenResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
ActionToken string `json:"action_token"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type MediafirePollResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
Doupload struct {
|
||||
Result string `json:"result"`
|
||||
Status string `json:"status"`
|
||||
Description string `json:"description"`
|
||||
QuickKey string `json:"quickkey"`
|
||||
Hash string `json:"hash"`
|
||||
Filename string `json:"filename"`
|
||||
Size string `json:"size"`
|
||||
Created string `json:"created"`
|
||||
CreatedUTC string `json:"created_utc"`
|
||||
Revision string `json:"revision"`
|
||||
} `json:"doupload"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type MediafireFileSearchResponse struct {
|
||||
Response struct {
|
||||
Action string `json:"action"`
|
||||
FileInfo []File `json:"file_info"`
|
||||
Result string `json:"result"`
|
||||
CurrentAPIVersion string `json:"current_api_version"`
|
||||
} `json:"response"`
|
||||
}
|
||||
626
drivers/mediafire/util.go
Normal file
626
drivers/mediafire/util.go
Normal file
@@ -0,0 +1,626 @@
|
||||
package mediafire
|
||||
|
||||
/*
|
||||
Package mediafire
|
||||
Author: Da3zKi7<da3zki7@duck.com>
|
||||
Date: 2025-09-11
|
||||
|
||||
D@' 3z K!7 - The King Of Cracking
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
func (d *Mediafire) getSessionToken(ctx context.Context) (string, error) {
|
||||
tokenURL := d.hostBase + "/application/get_session_token.php"
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
req.Header.Set("Content-Length", "0")
|
||||
req.Header.Set("Cookie", d.Cookie)
|
||||
req.Header.Set("DNT", "1")
|
||||
req.Header.Set("Origin", d.hostBase)
|
||||
req.Header.Set("Priority", "u=1, i")
|
||||
req.Header.Set("Referer", (d.hostBase + "/"))
|
||||
req.Header.Set("Sec-Ch-Ua", d.secChUa)
|
||||
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
|
||||
req.Header.Set("Sec-Ch-Ua-Platform", d.secChUaPlatform)
|
||||
req.Header.Set("Sec-Fetch-Dest", "empty")
|
||||
req.Header.Set("Sec-Fetch-Mode", "cors")
|
||||
req.Header.Set("Sec-Fetch-Site", "same-site")
|
||||
req.Header.Set("User-Agent", d.userAgent)
|
||||
//req.Header.Set("Connection", "keep-alive")
|
||||
|
||||
resp, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
//fmt.Printf("getSessionToken :: Raw response: %s\n", string(body))
|
||||
//fmt.Printf("getSessionToken :: Parsed response: %+v\n", resp)
|
||||
|
||||
var tokenResp struct {
|
||||
Response struct {
|
||||
SessionToken string `json:"session_token"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if tokenResp.Response.SessionToken == "" {
|
||||
return "", fmt.Errorf("empty session token received")
|
||||
}
|
||||
|
||||
cookieMap := make(map[string]string)
|
||||
for _, cookie := range resp.Cookies() {
|
||||
cookieMap[cookie.Name] = cookie.Value
|
||||
}
|
||||
|
||||
if len(cookieMap) > 0 {
|
||||
|
||||
var cookies []string
|
||||
for name, value := range cookieMap {
|
||||
cookies = append(cookies, fmt.Sprintf("%s=%s", name, value))
|
||||
}
|
||||
d.Cookie = strings.Join(cookies, "; ")
|
||||
op.MustSaveDriverStorage(d)
|
||||
|
||||
//fmt.Printf("getSessionToken :: Captured cookies: %s\n", d.Cookie)
|
||||
}
|
||||
|
||||
} else {
|
||||
return "", fmt.Errorf("getSessionToken :: failed to get session token, status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
d.SessionToken = tokenResp.Response.SessionToken
|
||||
|
||||
//fmt.Printf("Init :: Obtain Session Token %v", d.SessionToken)
|
||||
|
||||
op.MustSaveDriverStorage(d)
|
||||
|
||||
return d.SessionToken, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) renewToken(_ context.Context) error {
|
||||
query := map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
}
|
||||
|
||||
var resp MediafireRenewTokenResponse
|
||||
_, err := d.postForm("/user/renew_session_token.php", query, &resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to renew token: %w", err)
|
||||
}
|
||||
|
||||
//fmt.Printf("getInfo :: Raw response: %s\n", string(body))
|
||||
//fmt.Printf("getInfo :: Parsed response: %+v\n", resp)
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return fmt.Errorf("MediaFire token renewal failed: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
d.SessionToken = resp.Response.SessionToken
|
||||
|
||||
//fmt.Printf("Init :: Renew Session Token: %s", resp.Response.Result)
|
||||
|
||||
op.MustSaveDriverStorage(d)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) getFiles(ctx context.Context, folderKey string) ([]File, error) {
|
||||
files := make([]File, 0)
|
||||
hasMore := true
|
||||
chunkNumber := 1
|
||||
|
||||
for hasMore {
|
||||
resp, err := d.getFolderContent(ctx, folderKey, chunkNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, folder := range resp.Folders {
|
||||
files = append(files, File{
|
||||
ID: folder.FolderKey,
|
||||
Name: folder.Name,
|
||||
Size: 0,
|
||||
CreatedUTC: folder.CreatedUTC,
|
||||
IsFolder: true,
|
||||
})
|
||||
}
|
||||
|
||||
for _, file := range resp.Files {
|
||||
size, _ := strconv.ParseInt(file.Size, 10, 64)
|
||||
files = append(files, File{
|
||||
ID: file.QuickKey,
|
||||
Name: file.Filename,
|
||||
Size: size,
|
||||
CreatedUTC: file.CreatedUTC,
|
||||
IsFolder: false,
|
||||
})
|
||||
}
|
||||
|
||||
hasMore = resp.MoreChunks
|
||||
chunkNumber++
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) getFolderContent(ctx context.Context, folderKey string, chunkNumber int) (*FolderContentResponse, error) {
|
||||
|
||||
foldersResp, err := d.getFolderContentByType(ctx, folderKey, "folders", chunkNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filesResp, err := d.getFolderContentByType(ctx, folderKey, "files", chunkNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FolderContentResponse{
|
||||
Folders: foldersResp.Response.FolderContent.Folders,
|
||||
Files: filesResp.Response.FolderContent.Files,
|
||||
MoreChunks: foldersResp.Response.FolderContent.MoreChunks == "yes" || filesResp.Response.FolderContent.MoreChunks == "yes",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) getFolderContentByType(_ context.Context, folderKey, contentType string, chunkNumber int) (*MediafireResponse, error) {
|
||||
data := map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
"folder_key": folderKey,
|
||||
"content_type": contentType,
|
||||
"chunk": strconv.Itoa(chunkNumber),
|
||||
"chunk_size": strconv.FormatInt(d.ChunkSize, 10),
|
||||
"details": "yes",
|
||||
"order_direction": d.OrderDirection,
|
||||
"order_by": d.OrderBy,
|
||||
"filter": "",
|
||||
}
|
||||
|
||||
var resp MediafireResponse
|
||||
_, err := d.postForm("/folder/get_content.php", data, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return nil, fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) fileToObj(f File) *model.ObjThumb {
|
||||
created, _ := time.Parse("2006-01-02T15:04:05Z", f.CreatedUTC)
|
||||
|
||||
var thumbnailURL string
|
||||
if !f.IsFolder && f.ID != "" {
|
||||
thumbnailURL = d.hostBase + "/convkey/acaa/" + f.ID + "3g.jpg"
|
||||
}
|
||||
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: f.ID,
|
||||
//Path: "",
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
Modified: created,
|
||||
Ctime: created,
|
||||
IsFolder: f.IsFolder,
|
||||
},
|
||||
Thumbnail: model.Thumbnail{
|
||||
Thumbnail: thumbnailURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Mediafire) getForm(endpoint string, query map[string]string, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
|
||||
req.SetQueryParams(query)
|
||||
|
||||
req.SetHeaders(map[string]string{
|
||||
"Cookie": d.Cookie,
|
||||
//"User-Agent": base.UserAgent,
|
||||
"User-Agent": d.userAgent,
|
||||
"Origin": d.appBase,
|
||||
"Referer": d.appBase + "/",
|
||||
})
|
||||
|
||||
// If response OK
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
|
||||
// Targets MediaFire API
|
||||
res, err := req.Get(d.apiBase + endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) postForm(endpoint string, data map[string]string, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
|
||||
req.SetFormData(data)
|
||||
|
||||
req.SetHeaders(map[string]string{
|
||||
"Cookie": d.Cookie,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
//"User-Agent": base.UserAgent,
|
||||
"User-Agent": d.userAgent,
|
||||
"Origin": d.appBase,
|
||||
"Referer": d.appBase + "/",
|
||||
})
|
||||
|
||||
// If response OK
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
|
||||
// Targets MediaFire API
|
||||
res, err := req.Post(d.apiBase + endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) getDirectDownloadLink(_ context.Context, fileID string) (string, error) {
|
||||
data := map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"quick_key": fileID,
|
||||
"link_type": "direct_download",
|
||||
"response_format": "json",
|
||||
}
|
||||
|
||||
var resp MediafireDirectDownloadResponse
|
||||
_, err := d.getForm("/file/get_links.php", data, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return "", fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
if len(resp.Response.Links) == 0 {
|
||||
return "", fmt.Errorf("no download links found")
|
||||
}
|
||||
|
||||
return resp.Response.Links[0].DirectDownload, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) calculateSHA256(file *os.File) (string, error) {
|
||||
hasher := sha256.New()
|
||||
if _, err := file.Seek(0, 0); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(hasher.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) uploadCheck(ctx context.Context, filename string, filesize int64, filehash, folderKey string) (*MediafireCheckResponse, error) {
|
||||
|
||||
actionToken, err := d.getActionToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get action token: %w", err)
|
||||
}
|
||||
|
||||
query := map[string]string{
|
||||
"session_token": actionToken, /* d.SessionToken */
|
||||
"filename": filename,
|
||||
"size": strconv.FormatInt(filesize, 10),
|
||||
"hash": filehash,
|
||||
"folder_key": folderKey,
|
||||
"resumable": "yes",
|
||||
"response_format": "json",
|
||||
}
|
||||
|
||||
var resp MediafireCheckResponse
|
||||
_, err = d.postForm("/upload/check.php", query, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//fmt.Printf("uploadCheck :: Raw response: %s\n", string(body))
|
||||
//fmt.Printf("uploadCheck :: Parsed response: %+v\n", resp)
|
||||
|
||||
//fmt.Printf("uploadCheck :: ResumableUpload section: %+v\n", resp.Response.ResumableUpload)
|
||||
//fmt.Printf("uploadCheck :: Upload key specifically: '%s'\n", resp.Response.ResumableUpload.UploadKey)
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return nil, fmt.Errorf("MediaFire upload check failed: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) resumableUpload(ctx context.Context, folderKey, uploadKey string, unitData []byte, unitID int, fileHash, filename string, totalFileSize int64) (string, error) {
|
||||
actionToken, err := d.getActionToken(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
url := d.apiBase + "/upload/resumable.php"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(unitData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("folder_key", folderKey)
|
||||
q.Add("response_format", "json")
|
||||
q.Add("session_token", actionToken)
|
||||
q.Add("key", uploadKey)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
req.Header.Set("x-filehash", fileHash)
|
||||
req.Header.Set("x-filesize", strconv.FormatInt(totalFileSize, 10))
|
||||
req.Header.Set("x-unit-id", strconv.Itoa(unitID))
|
||||
req.Header.Set("x-unit-size", strconv.FormatInt(int64(len(unitData)), 10))
|
||||
req.Header.Set("x-unit-hash", d.sha256Hex(bytes.NewReader(unitData)))
|
||||
req.Header.Set("x-filename", filename)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.ContentLength = int64(len(unitData))
|
||||
|
||||
/* fmt.Printf("Debug resumable upload request:\n")
|
||||
fmt.Printf(" URL: %s\n", req.URL.String())
|
||||
fmt.Printf(" Headers: %+v\n", req.Header)
|
||||
fmt.Printf(" Unit ID: %d\n", unitID)
|
||||
fmt.Printf(" Unit Size: %d\n", len(unitData))
|
||||
fmt.Printf(" Upload Key: %s\n", uploadKey)
|
||||
fmt.Printf(" Action Token: %s\n", actionToken) */
|
||||
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
//fmt.Printf("MediaFire resumable upload response (status %d): %s\n", res.StatusCode, string(body))
|
||||
|
||||
var uploadResp struct {
|
||||
Response struct {
|
||||
Doupload struct {
|
||||
Key string `json:"key"`
|
||||
} `json:"doupload"`
|
||||
Result string `json:"result"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &uploadResp); err != nil {
|
||||
return "", fmt.Errorf("failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return "", fmt.Errorf("resumable upload failed with status %d", res.StatusCode)
|
||||
}
|
||||
|
||||
return uploadResp.Response.Doupload.Key, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) uploadUnits(ctx context.Context, file *os.File, checkResp *MediafireCheckResponse, filename, fileHash, folderKey string, up driver.UpdateProgress) (string, error) {
|
||||
unitSize, _ := strconv.ParseInt(checkResp.Response.ResumableUpload.UnitSize, 10, 64)
|
||||
numUnits, _ := strconv.Atoi(checkResp.Response.ResumableUpload.NumberOfUnits)
|
||||
uploadKey := checkResp.Response.ResumableUpload.UploadKey
|
||||
|
||||
stringWords := checkResp.Response.ResumableUpload.Bitmap.Words
|
||||
intWords := make([]int, len(stringWords))
|
||||
for i, word := range stringWords {
|
||||
intWords[i], _ = strconv.Atoi(word)
|
||||
}
|
||||
|
||||
var finalUploadKey string
|
||||
|
||||
for unitID := 0; unitID < numUnits; unitID++ {
|
||||
|
||||
if utils.IsCanceled(ctx) {
|
||||
return "", ctx.Err()
|
||||
}
|
||||
|
||||
if d.isUnitUploaded(intWords, unitID) {
|
||||
up(float64(unitID+1) * 100 / float64(numUnits))
|
||||
continue
|
||||
}
|
||||
|
||||
uploadKey, err := d.uploadSingleUnit(ctx, file, unitID, unitSize, fileHash, filename, uploadKey, folderKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
finalUploadKey = uploadKey
|
||||
|
||||
up(float64(unitID+1) * 100 / float64(numUnits))
|
||||
}
|
||||
|
||||
return finalUploadKey, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) uploadSingleUnit(ctx context.Context, file *os.File, unitID int, unitSize int64, fileHash, filename, uploadKey, folderKey string) (string, error) {
|
||||
start := int64(unitID) * unitSize
|
||||
size := unitSize
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fileSize := stat.Size()
|
||||
|
||||
if start+size > fileSize {
|
||||
size = fileSize - start
|
||||
}
|
||||
|
||||
unitData := make([]byte, size)
|
||||
if _, err := file.ReadAt(unitData, start); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return d.resumableUpload(ctx, folderKey, uploadKey, unitData, unitID, fileHash, filename, fileSize)
|
||||
}
|
||||
|
||||
func (d *Mediafire) getActionToken(_ context.Context) (string, error) {
|
||||
|
||||
if d.actionToken != "" {
|
||||
return d.actionToken, nil
|
||||
}
|
||||
|
||||
data := map[string]string{
|
||||
"type": "upload",
|
||||
"lifespan": "1440",
|
||||
"response_format": "json",
|
||||
"session_token": d.SessionToken,
|
||||
}
|
||||
|
||||
var resp MediafireActionTokenResponse
|
||||
_, err := d.postForm("/user/get_action_token.php", data, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return "", fmt.Errorf("MediaFire action token failed: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
return resp.Response.ActionToken, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) pollUpload(ctx context.Context, key string) (*MediafirePollResponse, error) {
|
||||
|
||||
actionToken, err := d.getActionToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get action token: %w", err)
|
||||
}
|
||||
|
||||
//fmt.Printf("Debug Key: %+v\n", key)
|
||||
|
||||
query := map[string]string{
|
||||
"key": key,
|
||||
"response_format": "json",
|
||||
"session_token": actionToken, /* d.SessionToken */
|
||||
}
|
||||
|
||||
var resp MediafirePollResponse
|
||||
_, err = d.postForm("/upload/poll_upload.php", query, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//fmt.Printf("pollUpload :: Raw response: %s\n", string(body))
|
||||
//fmt.Printf("pollUpload :: Parsed response: %+v\n", resp)
|
||||
|
||||
//fmt.Printf("pollUpload :: Debug Result: %+v\n", resp.Response.Result)
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return nil, fmt.Errorf("MediaFire poll upload failed: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (d *Mediafire) sha256Hex(r io.Reader) string {
|
||||
h := sha256.New()
|
||||
io.Copy(h, r)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (d *Mediafire) isUnitUploaded(words []int, unitID int) bool {
|
||||
wordIndex := unitID / 16
|
||||
bitIndex := unitID % 16
|
||||
if wordIndex >= len(words) {
|
||||
return false
|
||||
}
|
||||
return (words[wordIndex]>>bitIndex)&1 == 1
|
||||
}
|
||||
|
||||
func (d *Mediafire) getExistingFileInfo(ctx context.Context, fileHash, filename, folderKey string) (*model.ObjThumb, error) {
|
||||
|
||||
if fileInfo, err := d.getFileByHash(ctx, fileHash); err == nil && fileInfo != nil {
|
||||
return fileInfo, nil
|
||||
}
|
||||
|
||||
files, err := d.getFiles(ctx, folderKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.Name == filename && !file.IsFolder {
|
||||
return d.fileToObj(file), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("existing file not found")
|
||||
}
|
||||
|
||||
func (d *Mediafire) getFileByHash(_ context.Context, hash string) (*model.ObjThumb, error) {
|
||||
query := map[string]string{
|
||||
"session_token": d.SessionToken,
|
||||
"response_format": "json",
|
||||
"hash": hash,
|
||||
}
|
||||
|
||||
var resp MediafireFileSearchResponse
|
||||
_, err := d.postForm("/file/get_info.php", query, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Response.Result != "Success" {
|
||||
return nil, fmt.Errorf("MediaFire file search failed: %s", resp.Response.Result)
|
||||
}
|
||||
|
||||
if len(resp.Response.FileInfo) == 0 {
|
||||
return nil, fmt.Errorf("file not found by hash")
|
||||
}
|
||||
|
||||
file := resp.Response.FileInfo[0]
|
||||
return d.fileToObj(file), nil
|
||||
}
|
||||
@@ -83,7 +83,7 @@ type Group struct {
|
||||
Type int `json:"type"`
|
||||
Name string `json:"name"`
|
||||
IsAdministrator int `json:"is_administrator"`
|
||||
Role int `json:"role"`
|
||||
Role []int `json:"role"`
|
||||
Avatar string `json:"avatar_url"`
|
||||
IsStick int `json:"is_stick"`
|
||||
Nickname string `json:"nickname"`
|
||||
|
||||
13
go.mod
13
go.mod
@@ -3,10 +3,12 @@ module github.com/alist-org/alist/v3
|
||||
go 1.23.4
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
|
||||
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0
|
||||
github.com/KirCute/sftpd-alist v0.0.12
|
||||
github.com/ProtonMail/go-crypto v1.0.0
|
||||
github.com/SheltonZhu/115driver v1.0.34
|
||||
github.com/SheltonZhu/115driver v1.1.2
|
||||
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
|
||||
@@ -79,11 +81,7 @@ require (
|
||||
gorm.io/gorm v1.25.11
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
|
||||
)
|
||||
require github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
|
||||
require (
|
||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||
@@ -109,7 +107,6 @@ require (
|
||||
github.com/ipfs/boxo v0.12.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78
|
||||
@@ -268,4 +265,4 @@ require (
|
||||
lukechampine.com/blake3 v1.1.7 // indirect
|
||||
)
|
||||
|
||||
// replace github.com/xhofe/115-sdk-go => ../../xhofe/115-sdk-go
|
||||
replace github.com/SheltonZhu/115driver => github.com/okatu-loli/115driver v1.1.2
|
||||
|
||||
17
go.sum
17
go.sum
@@ -21,10 +21,16 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
@@ -40,8 +46,6 @@ github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4
|
||||
github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
|
||||
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
|
||||
github.com/SheltonZhu/115driver v1.0.34 h1:zhMLp4vgq7GksqvSxQQDOVfK6EOHldQl4b2n8tnZ+EE=
|
||||
github.com/SheltonZhu/115driver v1.0.34/go.mod h1:rKvNd4Y4OkXv1TMbr/SKjGdcvMQxh6AW5Tw9w0CJb7E=
|
||||
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=
|
||||
@@ -172,7 +176,6 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg=
|
||||
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -398,6 +401,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.3.1 h1:DLQQEgHUAGZB6RVlceB1f6A94O206exxW2RIMH+gMUc=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.3.1/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
@@ -483,6 +488,8 @@ github.com/ncw/swift/v2 v2.0.3 h1:8R9dmgFIWs+RiVlisCEfiQiik1hjuR0JnOkLxaP9ihg=
|
||||
github.com/ncw/swift/v2 v2.0.3/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78 h1:MYzLheyVx1tJVDqfu3YnN4jtnyALNzLvwl+f58TcvQY=
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
|
||||
github.com/okatu-loli/115driver v1.1.2 h1:XZT3r/51SZRQGzre2IeA+0/k4T1FneqArdhE4Wd600Q=
|
||||
github.com/okatu-loli/115driver v1.1.2/go.mod h1:rKvNd4Y4OkXv1TMbr/SKjGdcvMQxh6AW5Tw9w0CJb7E=
|
||||
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=
|
||||
@@ -492,6 +499,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -739,8 +748,6 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
||||
@@ -3,6 +3,7 @@ package data
|
||||
import "github.com/alist-org/alist/v3/cmd/flags"
|
||||
|
||||
func InitData() {
|
||||
initRoles()
|
||||
initUser()
|
||||
initSettings()
|
||||
initTasks()
|
||||
|
||||
@@ -26,7 +26,7 @@ func initDevData() {
|
||||
Username: "Noah",
|
||||
Password: "hsu",
|
||||
BasePath: "/data",
|
||||
Role: 0,
|
||||
Role: nil,
|
||||
Permission: 512,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
52
internal/bootstrap/data/role.go
Normal file
52
internal/bootstrap/data/role.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package data
|
||||
|
||||
// initRoles creates the default admin and guest roles if missing.
|
||||
// These roles are essential and must not be modified or removed.
|
||||
|
||||
import (
|
||||
"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/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func initRoles() {
|
||||
guestRole, err := op.GetRoleByName("guest")
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
guestRole = &model.Role{
|
||||
ID: uint(model.GUEST),
|
||||
Name: "guest",
|
||||
Description: "Guest",
|
||||
PermissionScopes: []model.PermissionEntry{
|
||||
{Path: "/", Permission: 0},
|
||||
},
|
||||
}
|
||||
if err := op.CreateRole(guestRole); err != nil {
|
||||
utils.Log.Fatalf("[init role] Failed to create guest role: %v", err)
|
||||
}
|
||||
} else {
|
||||
utils.Log.Fatalf("[init role] Failed to get guest role: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = op.GetRoleByName("admin")
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
adminRole := &model.Role{
|
||||
ID: uint(model.ADMIN),
|
||||
Name: "admin",
|
||||
Description: "Administrator",
|
||||
PermissionScopes: []model.PermissionEntry{
|
||||
{Path: "/", Permission: 0xFFFF},
|
||||
},
|
||||
}
|
||||
if err := op.CreateRole(adminRole); err != nil {
|
||||
utils.Log.Fatalf("[init role] Failed to create admin role: %v", err)
|
||||
}
|
||||
} else {
|
||||
utils.Log.Fatalf("[init role] Failed to get admin role: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,7 @@ func InitialSettings() []model.SettingItem {
|
||||
} else {
|
||||
token = random.Token()
|
||||
}
|
||||
defaultRoleID := strconv.Itoa(model.GUEST)
|
||||
initialSettingItems = []model.SettingItem{
|
||||
// site settings
|
||||
{Key: conf.VERSION, Value: conf.Version, Type: conf.TypeString, Group: model.SITE, Flag: model.READONLY},
|
||||
@@ -103,6 +104,10 @@ func InitialSettings() []model.SettingItem {
|
||||
{Key: conf.AllowIndexed, Value: "false", Type: conf.TypeBool, Group: model.SITE},
|
||||
{Key: conf.AllowMounted, Value: "true", Type: conf.TypeBool, Group: model.SITE},
|
||||
{Key: conf.RobotsTxt, Value: "User-agent: *\nAllow: /", Type: conf.TypeText, Group: model.SITE},
|
||||
{Key: conf.AllowRegister, Value: "false", Type: conf.TypeBool, Group: model.SITE},
|
||||
{Key: conf.DefaultRole, Value: defaultRoleID, Type: conf.TypeSelect, Group: model.SITE},
|
||||
// newui settings
|
||||
{Key: conf.UseNewui, Value: "false", Type: conf.TypeBool, Group: model.SITE},
|
||||
// style settings
|
||||
{Key: conf.Logo, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeText, Group: model.STYLE},
|
||||
{Key: conf.Favicon, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeString, Group: model.STYLE},
|
||||
@@ -160,6 +165,9 @@ func InitialSettings() []model.SettingItem {
|
||||
{Key: conf.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL},
|
||||
{Key: conf.IgnoreDirectLinkParams, Value: "sign,alist_ts", Type: conf.TypeString, Group: model.GLOBAL},
|
||||
{Key: conf.WebauthnLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
|
||||
{Key: conf.MaxDevices, Value: "0", Type: conf.TypeNumber, Group: model.GLOBAL},
|
||||
{Key: conf.DeviceEvictPolicy, Value: "deny", Type: conf.TypeSelect, Options: "deny,evict_oldest", Group: model.GLOBAL},
|
||||
{Key: conf.DeviceSessionTTL, Value: "86400", Type: conf.TypeNumber, Group: model.GLOBAL},
|
||||
|
||||
// single settings
|
||||
{Key: conf.Token, Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"os"
|
||||
|
||||
"github.com/alist-org/alist/v3/cmd/flags"
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
@@ -14,45 +14,16 @@ import (
|
||||
)
|
||||
|
||||
func initUser() {
|
||||
admin, err := op.GetAdmin()
|
||||
adminPassword := random.String(8)
|
||||
envpass := os.Getenv("ALIST_ADMIN_PASSWORD")
|
||||
if flags.Dev {
|
||||
adminPassword = "admin"
|
||||
} else if len(envpass) > 0 {
|
||||
adminPassword = envpass
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
salt := random.String(16)
|
||||
admin = &model.User{
|
||||
Username: "admin",
|
||||
Salt: salt,
|
||||
PwdHash: model.TwoHashPwd(adminPassword, salt),
|
||||
Role: model.ADMIN,
|
||||
BasePath: "/",
|
||||
Authn: "[]",
|
||||
// 0(can see hidden) - 7(can remove) & 12(can read archives) - 13(can decompress archives)
|
||||
Permission: 0x30FF,
|
||||
}
|
||||
if err := op.CreateUser(admin); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
utils.Log.Infof("Successfully created the admin user and the initial password is: %s", adminPassword)
|
||||
}
|
||||
} else {
|
||||
utils.Log.Fatalf("[init user] Failed to get admin user: %v", err)
|
||||
}
|
||||
}
|
||||
guest, err := op.GetGuest()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
salt := random.String(16)
|
||||
guestRole, _ := op.GetRoleByName("guest")
|
||||
guest = &model.User{
|
||||
Username: "guest",
|
||||
PwdHash: model.TwoHashPwd("guest", salt),
|
||||
Salt: salt,
|
||||
Role: model.GUEST,
|
||||
Role: model.Roles{int(guestRole.ID)},
|
||||
BasePath: "/",
|
||||
Permission: 0,
|
||||
Disabled: true,
|
||||
@@ -65,4 +36,35 @@ func initUser() {
|
||||
utils.Log.Fatalf("[init user] Failed to get guest user: %v", err)
|
||||
}
|
||||
}
|
||||
admin, err := op.GetAdmin()
|
||||
adminPassword := random.String(8)
|
||||
envpass := os.Getenv("ALIST_ADMIN_PASSWORD")
|
||||
if flags.Dev {
|
||||
adminPassword = "admin"
|
||||
} else if len(envpass) > 0 {
|
||||
adminPassword = envpass
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
salt := random.String(16)
|
||||
adminRole, _ := op.GetRoleByName("admin")
|
||||
admin = &model.User{
|
||||
Username: "admin",
|
||||
Salt: salt,
|
||||
PwdHash: model.TwoHashPwd(adminPassword, salt),
|
||||
Role: model.Roles{int(adminRole.ID)},
|
||||
BasePath: "/",
|
||||
Authn: "[]",
|
||||
// 0(can see hidden) - 7(can remove) & 12(can read archives) - 13(can decompress archives)
|
||||
Permission: 0xFFFF,
|
||||
}
|
||||
if err := op.CreateUser(admin); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
utils.Log.Infof("Successfully created the admin user and the initial password is: %s", adminPassword)
|
||||
}
|
||||
} else {
|
||||
utils.Log.Fatalf("[init user] Failed to get admin user: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,15 @@ import (
|
||||
|
||||
func init() {
|
||||
formatter := logrus.TextFormatter{
|
||||
ForceColors: true,
|
||||
EnvironmentOverrideColors: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
FullTimestamp: true,
|
||||
}
|
||||
if os.Getenv("NO_COLOR") != "" || os.Getenv("ALIST_NO_COLOR") == "1" {
|
||||
formatter.DisableColors = true
|
||||
} else {
|
||||
formatter.ForceColors = true
|
||||
formatter.EnvironmentOverrideColors = true
|
||||
}
|
||||
logrus.SetFormatter(&formatter)
|
||||
utils.Log.SetFormatter(&formatter)
|
||||
// logrus.SetLevel(logrus.DebugLevel)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_24_0"
|
||||
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_32_0"
|
||||
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_41_0"
|
||||
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_46_0"
|
||||
)
|
||||
|
||||
type VersionPatches struct {
|
||||
@@ -32,4 +33,10 @@ var UpgradePatches = []VersionPatches{
|
||||
v3_41_0.GrantAdminPermissions,
|
||||
},
|
||||
},
|
||||
{
|
||||
Version: "v3.46.0",
|
||||
Patches: []func(){
|
||||
v3_46_0.ConvertLegacyRoles,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
186
internal/bootstrap/patch/v3_46_0/convert_role.go
Normal file
186
internal/bootstrap/patch/v3_46_0/convert_role.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package v3_46_0
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ConvertLegacyRoles migrates old integer role values to a new role model with permission scopes.
|
||||
func ConvertLegacyRoles() {
|
||||
guestRole, err := op.GetRoleByName("guest")
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
guestRole = &model.Role{
|
||||
ID: uint(model.GUEST),
|
||||
Name: "guest",
|
||||
Description: "Guest",
|
||||
PermissionScopes: []model.PermissionEntry{
|
||||
{
|
||||
Path: "/",
|
||||
Permission: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err = op.CreateRole(guestRole); err != nil {
|
||||
utils.Log.Errorf("[convert roles] failed to create guest role: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
utils.Log.Errorf("[convert roles] failed to get guest role: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
adminRole, err := op.GetRoleByName("admin")
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
adminRole = &model.Role{
|
||||
ID: uint(model.ADMIN),
|
||||
Name: "admin",
|
||||
Description: "Administrator",
|
||||
PermissionScopes: []model.PermissionEntry{
|
||||
{
|
||||
Path: "/",
|
||||
Permission: 0x33FF,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err = op.CreateRole(adminRole); err != nil {
|
||||
utils.Log.Errorf("[convert roles] failed to create admin role: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
utils.Log.Errorf("[convert roles] failed to get admin role: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
generalRole, err := op.GetRoleByName("general")
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
generalRole = &model.Role{
|
||||
ID: uint(model.NEWGENERAL),
|
||||
Name: "general",
|
||||
Description: "General User",
|
||||
PermissionScopes: []model.PermissionEntry{
|
||||
{
|
||||
Path: "/",
|
||||
Permission: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err = op.CreateRole(generalRole); err != nil {
|
||||
utils.Log.Errorf("[convert roles] failed create general role: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
utils.Log.Errorf("[convert roles] failed get general role: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rawDb := db.GetDb()
|
||||
table := conf.Conf.Database.TablePrefix + "users"
|
||||
rows, err := rawDb.Table(table).Select("id, username, role").Rows()
|
||||
if err != nil {
|
||||
utils.Log.Errorf("[convert roles] failed to get users: %v", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var updatedCount int
|
||||
for rows.Next() {
|
||||
var id uint
|
||||
var username string
|
||||
var rawRole []byte
|
||||
|
||||
if err := rows.Scan(&id, &username, &rawRole); err != nil {
|
||||
utils.Log.Warnf("[convert roles] skip user scan err: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
utils.Log.Debugf("[convert roles] user: %s raw role: %s", username, string(rawRole))
|
||||
|
||||
if len(rawRole) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var oldRoles []int
|
||||
wasSingleInt := false
|
||||
if err := json.Unmarshal(rawRole, &oldRoles); err != nil {
|
||||
var single int
|
||||
if err := json.Unmarshal(rawRole, &single); err != nil {
|
||||
utils.Log.Warnf("[convert roles] user %s has invalid role: %s", username, string(rawRole))
|
||||
continue
|
||||
}
|
||||
oldRoles = []int{single}
|
||||
wasSingleInt = true
|
||||
}
|
||||
|
||||
var newRoles model.Roles
|
||||
for _, r := range oldRoles {
|
||||
switch r {
|
||||
case model.ADMIN:
|
||||
newRoles = append(newRoles, int(adminRole.ID))
|
||||
case model.GUEST:
|
||||
newRoles = append(newRoles, int(guestRole.ID))
|
||||
case model.GENERAL:
|
||||
newRoles = append(newRoles, int(generalRole.ID))
|
||||
default:
|
||||
newRoles = append(newRoles, r)
|
||||
}
|
||||
}
|
||||
|
||||
if wasSingleInt {
|
||||
err := rawDb.Table(table).Where("id = ?", id).Update("role", newRoles).Error
|
||||
if err != nil {
|
||||
utils.Log.Errorf("[convert roles] failed to update user %s: %v", username, err)
|
||||
} else {
|
||||
updatedCount++
|
||||
utils.Log.Infof("[convert roles] updated user %s: %v → %v", username, oldRoles, newRoles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils.Log.Infof("[convert roles] completed role conversion for %d users", updatedCount)
|
||||
}
|
||||
|
||||
func IsLegacyRoleDetected() bool {
|
||||
rawDb := db.GetDb()
|
||||
table := conf.Conf.Database.TablePrefix + "users"
|
||||
rows, err := rawDb.Table(table).Select("role").Rows()
|
||||
if err != nil {
|
||||
utils.Log.Errorf("[role check] failed to scan user roles: %v", err)
|
||||
return false
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var raw sql.RawBytes
|
||||
if err := rows.Scan(&raw); err != nil {
|
||||
continue
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var roles []int
|
||||
if err := json.Unmarshal(raw, &roles); err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var single int
|
||||
if err := json.Unmarshal(raw, &single); err == nil {
|
||||
utils.Log.Infof("[role check] detected legacy int role: %d", single)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -16,6 +16,9 @@ const (
|
||||
AllowIndexed = "allow_indexed"
|
||||
AllowMounted = "allow_mounted"
|
||||
RobotsTxt = "robots_txt"
|
||||
AllowRegister = "allow_register"
|
||||
DefaultRole = "default_role"
|
||||
UseNewui = "use_newui"
|
||||
|
||||
Logo = "logo"
|
||||
Favicon = "favicon"
|
||||
@@ -45,6 +48,9 @@ const (
|
||||
ForwardDirectLinkParams = "forward_direct_link_params"
|
||||
IgnoreDirectLinkParams = "ignore_direct_link_params"
|
||||
WebauthnLoginEnabled = "webauthn_login_enabled"
|
||||
MaxDevices = "max_devices"
|
||||
DeviceEvictPolicy = "device_evict_policy"
|
||||
DeviceSessionTTL = "device_session_ttl"
|
||||
|
||||
// index
|
||||
SearchIndex = "search_index"
|
||||
|
||||
@@ -12,7 +12,7 @@ var db *gorm.DB
|
||||
|
||||
func Init(d *gorm.DB) {
|
||||
db = d
|
||||
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey))
|
||||
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.Role), new(model.Label), new(model.LabelFileBinding), new(model.ObjFile), new(model.Session))
|
||||
if err != nil {
|
||||
log.Fatalf("failed migrate database: %s", err.Error())
|
||||
}
|
||||
|
||||
79
internal/db/label.go
Normal file
79
internal/db/label.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetLabels Get all label from database order by id
|
||||
func GetLabels(pageIndex, pageSize int) ([]model.Label, int64, error) {
|
||||
labelDB := db.Model(&model.Label{})
|
||||
var count int64
|
||||
if err := labelDB.Count(&count).Error; err != nil {
|
||||
return nil, 0, errors.Wrapf(err, "failed get label count")
|
||||
}
|
||||
var labels []model.Label
|
||||
if err := labelDB.Order(columnName("id")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&labels).Error; err != nil {
|
||||
return nil, 0, errors.WithStack(err)
|
||||
}
|
||||
return labels, count, nil
|
||||
}
|
||||
|
||||
// GetLabelById Get Label by id, used to update label usually
|
||||
func GetLabelById(id uint) (*model.Label, error) {
|
||||
var label model.Label
|
||||
label.ID = id
|
||||
if err := db.First(&label).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return &label, nil
|
||||
}
|
||||
|
||||
// CreateLabel just insert label to database
|
||||
func CreateLabel(label model.Label) (uint, error) {
|
||||
label.CreateTime = time.Now()
|
||||
err := errors.WithStack(db.Create(&label).Error)
|
||||
if err != nil {
|
||||
return label.ID, errors.WithMessage(err, "failed create label in database")
|
||||
}
|
||||
return label.ID, nil
|
||||
}
|
||||
|
||||
// UpdateLabel just update storage in database
|
||||
func UpdateLabel(label *model.Label) (*model.Label, error) {
|
||||
label.CreateTime = time.Now()
|
||||
_, err := GetLabelById(label.ID)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed get old label")
|
||||
}
|
||||
err = errors.WithStack(db.Save(label).Error)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed create label in database")
|
||||
}
|
||||
return label, nil
|
||||
}
|
||||
|
||||
// DeleteLabelById just delete label from database by id
|
||||
func DeleteLabelById(id uint) error {
|
||||
return errors.WithStack(db.Delete(&model.Label{}, id).Error)
|
||||
}
|
||||
|
||||
// GetLabelByIds Get label from database order by ids
|
||||
func GetLabelByIds(ids []uint) ([]model.Label, error) {
|
||||
labelDB := db.Model(&model.Label{})
|
||||
var labels []model.Label
|
||||
if err := labelDB.Where(ids).Find(&labels).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
// GetLabelByName Get Label by name
|
||||
func GetLabelByName(name string) bool {
|
||||
var label model.Label
|
||||
result := db.Where("name = ?", name).First(&label)
|
||||
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound)
|
||||
return exists
|
||||
}
|
||||
192
internal/db/label_file_binding.go
Normal file
192
internal/db/label_file_binding.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetLabelIds Get all label_ids from database order by file_name
|
||||
func GetLabelIds(userId uint, fileName string) ([]uint, error) {
|
||||
//fmt.Printf(">>> [GetLabelIds] userId: %d, fileName: %s\n", userId, fileName)
|
||||
labelFileBinDingDB := db.Model(&model.LabelFileBinding{})
|
||||
var labelIds []uint
|
||||
if err := labelFileBinDingDB.Where("file_name = ?", fileName).Where("user_id = ?", userId).Pluck("label_id", &labelIds).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return labelIds, nil
|
||||
}
|
||||
|
||||
func CreateLabelFileBinDing(fileName string, labelId, userId uint) error {
|
||||
var labelFileBinDing model.LabelFileBinding
|
||||
labelFileBinDing.UserId = userId
|
||||
labelFileBinDing.LabelId = labelId
|
||||
labelFileBinDing.FileName = fileName
|
||||
labelFileBinDing.CreateTime = time.Now()
|
||||
err := errors.WithStack(db.Create(&labelFileBinDing).Error)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed create label in database")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLabelFileBinDingByLabelIdExists Get Label by label_id, used to del label usually
|
||||
func GetLabelFileBinDingByLabelIdExists(labelId, userId uint) bool {
|
||||
var labelFileBinDing model.LabelFileBinding
|
||||
result := db.Where("label_id = ?", labelId).Where("user_id = ?", userId).First(&labelFileBinDing)
|
||||
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound)
|
||||
return exists
|
||||
}
|
||||
|
||||
// DelLabelFileBinDingByFileName used to del usually
|
||||
func DelLabelFileBinDingByFileName(userId uint, fileName string) error {
|
||||
return errors.WithStack(db.Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinding{}).Error)
|
||||
}
|
||||
|
||||
// DelLabelFileBinDingById used to del usually
|
||||
func DelLabelFileBinDingById(labelId, userId uint, fileName string) error {
|
||||
return errors.WithStack(db.Where("label_id = ?", labelId).Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinding{}).Error)
|
||||
}
|
||||
|
||||
func GetLabelFileBinDingByLabelId(labelIds []uint, userId uint) (result []model.LabelFileBinding, err error) {
|
||||
if err := db.Where("label_id in (?)", labelIds).Where("user_id = ?", userId).Find(&result).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetLabelBindingsByFileNamesPublic(fileNames []string) (map[string][]uint, error) {
|
||||
var binds []model.LabelFileBinding
|
||||
if err := db.Where("file_name IN ?", fileNames).Find(&binds).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
out := make(map[string][]uint, len(fileNames))
|
||||
seen := make(map[string]struct{}, len(binds))
|
||||
for _, b := range binds {
|
||||
key := fmt.Sprintf("%s-%d", b.FileName, b.LabelId)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out[b.FileName] = append(out[b.FileName], b.LabelId)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func GetLabelsByFileNamesPublic(fileNames []string) (map[string][]model.Label, error) {
|
||||
bindMap, err := GetLabelBindingsByFileNamesPublic(fileNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idSet := make(map[uint]struct{})
|
||||
for _, ids := range bindMap {
|
||||
for _, id := range ids {
|
||||
idSet[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(idSet) == 0 {
|
||||
return make(map[string][]model.Label, 0), nil
|
||||
}
|
||||
allIDs := make([]uint, 0, len(idSet))
|
||||
for id := range idSet {
|
||||
allIDs = append(allIDs, id)
|
||||
}
|
||||
labels, err := GetLabelByIds(allIDs) // 你已有的函数
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labelByID := make(map[uint]model.Label, len(labels))
|
||||
for _, l := range labels {
|
||||
labelByID[l.ID] = l
|
||||
}
|
||||
|
||||
out := make(map[string][]model.Label, len(bindMap))
|
||||
for fname, ids := range bindMap {
|
||||
for _, id := range ids {
|
||||
if lab, ok := labelByID[id]; ok {
|
||||
out[fname] = append(out[fname], lab)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ListLabelFileBinDing(userId uint, labelIDs []uint, fileName string, page, pageSize int) ([]model.LabelFileBinding, int64, error) {
|
||||
q := db.Model(&model.LabelFileBinding{}).Where("user_id = ?", userId)
|
||||
|
||||
if len(labelIDs) > 0 {
|
||||
q = q.Where("label_id IN ?", labelIDs)
|
||||
}
|
||||
if fileName != "" {
|
||||
q = q.Where("file_name LIKE ?", "%"+fileName+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
var rows []model.LabelFileBinding
|
||||
if err := q.
|
||||
Order("id DESC").
|
||||
Offset((page - 1) * pageSize).
|
||||
Limit(pageSize).
|
||||
Find(&rows).Error; err != nil {
|
||||
return nil, 0, errors.WithStack(err)
|
||||
}
|
||||
return rows, total, nil
|
||||
}
|
||||
|
||||
func RestoreLabelFileBindings(bindings []model.LabelFileBinding, keepIDs bool, override bool) error {
|
||||
if len(bindings) == 0 {
|
||||
return nil
|
||||
}
|
||||
tx := db.Begin()
|
||||
|
||||
if override {
|
||||
type key struct {
|
||||
uid uint
|
||||
name string
|
||||
}
|
||||
toDel := make(map[key]struct{}, len(bindings))
|
||||
for i := range bindings {
|
||||
k := key{uid: bindings[i].UserId, name: bindings[i].FileName}
|
||||
toDel[k] = struct{}{}
|
||||
}
|
||||
for k := range toDel {
|
||||
if err := tx.Where("user_id = ? AND file_name = ?", k.uid, k.name).
|
||||
Delete(&model.LabelFileBinding{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range bindings {
|
||||
b := bindings[i]
|
||||
if !keepIDs {
|
||||
b.ID = 0
|
||||
}
|
||||
if b.CreateTime.IsZero() {
|
||||
b.CreateTime = time.Now()
|
||||
}
|
||||
if override {
|
||||
if err := tx.Create(&b).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&b).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.WithStack(tx.Commit().Error)
|
||||
}
|
||||
31
internal/db/obj_file.go
Normal file
31
internal/db/obj_file.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetFileByNameExists Get file by name
|
||||
func GetFileByNameExists(name string) bool {
|
||||
var label model.ObjFile
|
||||
result := db.Where("name = ?", name).First(&label)
|
||||
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound)
|
||||
return exists
|
||||
}
|
||||
|
||||
// GetFileByName Get file by name
|
||||
func GetFileByName(name string, userId uint) (objFile model.ObjFile, err error) {
|
||||
if err = db.Where("name = ?", name).Where("user_id = ?", userId).First(&objFile).Error; err != nil {
|
||||
return objFile, errors.WithStack(err)
|
||||
}
|
||||
return objFile, nil
|
||||
}
|
||||
|
||||
func CreateObjFile(obj model.ObjFile) error {
|
||||
err := errors.WithStack(db.Create(&obj).Error)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed create file in database")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
103
internal/db/role.go
Normal file
103
internal/db/role.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetRole(id uint) (*model.Role, error) {
|
||||
var r model.Role
|
||||
if err := db.First(&r, id).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "failed get role")
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func GetRoleByName(name string) (*model.Role, error) {
|
||||
r := model.Role{Name: name}
|
||||
if err := db.Where(r).First(&r).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "failed get role")
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func GetRoles(pageIndex, pageSize int) (roles []model.Role, count int64, err error) {
|
||||
roleDB := db.Model(&model.Role{})
|
||||
if err = roleDB.Count(&count).Error; err != nil {
|
||||
return nil, 0, errors.Wrapf(err, "failed get roles count")
|
||||
}
|
||||
if err = roleDB.Order(columnName("id")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&roles).Error; err != nil {
|
||||
return nil, 0, errors.Wrapf(err, "failed get find roles")
|
||||
}
|
||||
return roles, count, nil
|
||||
}
|
||||
|
||||
func GetAllRoles() ([]model.Role, error) {
|
||||
var roles []model.Role
|
||||
if err := db.Find(&roles).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func CreateRole(r *model.Role) error {
|
||||
if err := db.Create(r).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if r.Default {
|
||||
if err := db.Model(&model.Role{}).Where("id <> ?", r.ID).Update("default", false).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateRole(r *model.Role) error {
|
||||
if err := db.Save(r).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if r.Default {
|
||||
if err := db.Model(&model.Role{}).Where("id <> ?", r.ID).Update("default", false).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteRole(id uint) error {
|
||||
return errors.WithStack(db.Delete(&model.Role{}, id).Error)
|
||||
}
|
||||
|
||||
func UpdateRolePermissionsPathPrefix(oldPath, newPath string) ([]uint, error) {
|
||||
var roles []model.Role
|
||||
var modifiedRoleIDs []uint
|
||||
|
||||
if err := db.Find(&roles).Error; err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to load roles")
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
updated := false
|
||||
for i, entry := range role.PermissionScopes {
|
||||
entryPath := path.Clean(entry.Path)
|
||||
oldPathClean := path.Clean(oldPath)
|
||||
|
||||
if entryPath == oldPathClean {
|
||||
role.PermissionScopes[i].Path = newPath
|
||||
updated = true
|
||||
} else if strings.HasPrefix(entryPath, oldPathClean+"/") {
|
||||
role.PermissionScopes[i].Path = newPath + entryPath[len(oldPathClean):]
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
if updated {
|
||||
if err := UpdateRole(&role); err != nil {
|
||||
return nil, errors.WithMessagef(err, "failed to update role ID %d", role.ID)
|
||||
}
|
||||
modifiedRoleIDs = append(modifiedRoleIDs, role.ID)
|
||||
}
|
||||
}
|
||||
return modifiedRoleIDs, nil
|
||||
}
|
||||
69
internal/db/session.go
Normal file
69
internal/db/session.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func GetSession(userID uint, deviceKey string) (*model.Session, error) {
|
||||
s := model.Session{UserID: userID, DeviceKey: deviceKey}
|
||||
if err := db.Select("user_id, device_key, last_active, status, user_agent, ip").Where(&s).First(&s).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "failed find session")
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func CreateSession(s *model.Session) error {
|
||||
return errors.WithStack(db.Create(s).Error)
|
||||
}
|
||||
|
||||
func UpsertSession(s *model.Session) error {
|
||||
return errors.WithStack(db.Clauses(clause.OnConflict{UpdateAll: true}).Create(s).Error)
|
||||
}
|
||||
|
||||
func DeleteSession(userID uint, deviceKey string) error {
|
||||
return errors.WithStack(db.Where("user_id = ? AND device_key = ?", userID, deviceKey).Delete(&model.Session{}).Error)
|
||||
}
|
||||
|
||||
func CountActiveSessionsByUser(userID uint) (int64, error) {
|
||||
var count int64
|
||||
err := db.Model(&model.Session{}).
|
||||
Where("user_id = ? AND status = ?", userID, model.SessionActive).
|
||||
Count(&count).Error
|
||||
return count, errors.WithStack(err)
|
||||
}
|
||||
|
||||
func DeleteSessionsBefore(ts int64) error {
|
||||
return errors.WithStack(db.Where("last_active < ?", ts).Delete(&model.Session{}).Error)
|
||||
}
|
||||
|
||||
// GetOldestActiveSession returns the oldest active session for the specified user.
|
||||
func GetOldestActiveSession(userID uint) (*model.Session, error) {
|
||||
var s model.Session
|
||||
if err := db.Where("user_id = ? AND status = ?", userID, model.SessionActive).
|
||||
Order("last_active ASC").First(&s).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "failed get oldest active session")
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func UpdateSessionLastActive(userID uint, deviceKey string, lastActive int64) error {
|
||||
return errors.WithStack(db.Model(&model.Session{}).Where("user_id = ? AND device_key = ?", userID, deviceKey).Update("last_active", lastActive).Error)
|
||||
}
|
||||
|
||||
func ListSessionsByUser(userID uint) ([]model.Session, error) {
|
||||
var sessions []model.Session
|
||||
err := db.Select("user_id, device_key, last_active, status, user_agent, ip").Where("user_id = ? AND status = ?", userID, model.SessionActive).Find(&sessions).Error
|
||||
return sessions, errors.WithStack(err)
|
||||
}
|
||||
|
||||
func ListSessions() ([]model.Session, error) {
|
||||
var sessions []model.Session
|
||||
err := db.Select("user_id, device_key, last_active, status, user_agent, ip").Where("status = ?", model.SessionActive).Find(&sessions).Error
|
||||
return sessions, errors.WithStack(err)
|
||||
}
|
||||
|
||||
func MarkInactive(sessionID string) error {
|
||||
return errors.WithStack(db.Model(&model.Session{}).Where("device_key = ?", sessionID).Update("status", model.SessionInactive).Error)
|
||||
}
|
||||
@@ -2,19 +2,42 @@ package db
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetUserByRole(role int) (*model.User, error) {
|
||||
user := model.User{Role: role}
|
||||
if err := db.Where(user).Take(&user).Error; err != nil {
|
||||
var users []model.User
|
||||
if err := db.Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
for i := range users {
|
||||
if users[i].Role.Contains(role) {
|
||||
return &users[i], nil
|
||||
}
|
||||
}
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
func GetUsersByRole(roleID int) ([]model.User, error) {
|
||||
var users []model.User
|
||||
if err := db.Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []model.User
|
||||
for _, u := range users {
|
||||
if slices.Contains(u.Role, roleID) {
|
||||
result = append(result, u)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetUserByName(username string) (*model.User, error) {
|
||||
@@ -60,6 +83,14 @@ func GetUsers(pageIndex, pageSize int) (users []model.User, count int64, err err
|
||||
return users, count, nil
|
||||
}
|
||||
|
||||
func GetAllUsers() ([]model.User, error) {
|
||||
var users []model.User
|
||||
if err := db.Find(&users).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func DeleteUserById(id uint) error {
|
||||
return errors.WithStack(db.Delete(&model.User{}, id).Error)
|
||||
}
|
||||
@@ -100,3 +131,50 @@ func RemoveAuthn(u *model.User, id string) error {
|
||||
}
|
||||
return UpdateAuthn(u.ID, string(res))
|
||||
}
|
||||
|
||||
func UpdateUserBasePathPrefix(oldPath, newPath string, usersOpt ...[]model.User) ([]string, error) {
|
||||
var users []model.User
|
||||
var modifiedUsernames []string
|
||||
|
||||
oldPathClean := path.Clean(oldPath)
|
||||
|
||||
if len(usersOpt) > 0 {
|
||||
users = usersOpt[0]
|
||||
} else {
|
||||
if err := db.Find(&users).Error; err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to load users")
|
||||
}
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
basePath := path.Clean(user.BasePath)
|
||||
updated := false
|
||||
|
||||
if basePath == oldPathClean {
|
||||
user.BasePath = path.Clean(newPath)
|
||||
updated = true
|
||||
} else if strings.HasPrefix(basePath, oldPathClean+"/") {
|
||||
user.BasePath = path.Clean(newPath + basePath[len(oldPathClean):])
|
||||
updated = true
|
||||
}
|
||||
|
||||
if updated {
|
||||
if err := UpdateUser(&user); err != nil {
|
||||
return nil, errors.WithMessagef(err, "failed to update user ID %d", user.ID)
|
||||
}
|
||||
modifiedUsernames = append(modifiedUsernames, user.Username)
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedUsernames, nil
|
||||
}
|
||||
|
||||
func CountUsersByRoleAndEnabledExclude(roleID uint, excludeUserID uint) (int64, error) {
|
||||
var count int64
|
||||
jsonValue := fmt.Sprintf("[%d]", roleID)
|
||||
err := db.Model(&model.User{}).
|
||||
Where("disabled = ? AND id != ?", false, excludeUserID).
|
||||
Where("JSON_CONTAINS(role, ?)", jsonValue).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
138
internal/device/session.go
Normal file
138
internal/device/session.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Handle verifies device sessions for a user and upserts current session.
|
||||
func Handle(userID uint, deviceKey, ua, ip string) error {
|
||||
ttl := setting.GetInt(conf.DeviceSessionTTL, 86400)
|
||||
if ttl > 0 {
|
||||
_ = db.DeleteSessionsBefore(time.Now().Unix() - int64(ttl))
|
||||
}
|
||||
|
||||
ip = utils.MaskIP(ip)
|
||||
|
||||
now := time.Now().Unix()
|
||||
sess, err := db.GetSession(userID, deviceKey)
|
||||
if err == nil {
|
||||
if sess.Status == model.SessionInactive {
|
||||
return errors.WithStack(errs.SessionInactive)
|
||||
}
|
||||
sess.Status = model.SessionActive
|
||||
sess.LastActive = now
|
||||
sess.UserAgent = ua
|
||||
sess.IP = ip
|
||||
return db.UpsertSession(sess)
|
||||
}
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
max := setting.GetInt(conf.MaxDevices, 0)
|
||||
if max > 0 {
|
||||
count, err := db.CountActiveSessionsByUser(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count >= int64(max) {
|
||||
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
|
||||
if policy == "evict_oldest" {
|
||||
if oldest, err := db.GetOldestActiveSession(userID); err == nil {
|
||||
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return errors.WithStack(errs.TooManyDevices)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s := &model.Session{UserID: userID, DeviceKey: deviceKey, UserAgent: ua, IP: ip, LastActive: now, Status: model.SessionActive}
|
||||
return db.CreateSession(s)
|
||||
}
|
||||
|
||||
// EnsureActiveOnLogin is used only in login flow:
|
||||
// - If session exists (even Inactive): reactivate and refresh fields.
|
||||
// - If not exists: apply max-devices policy, then create Active session.
|
||||
func EnsureActiveOnLogin(userID uint, deviceKey, ua, ip string) error {
|
||||
ip = utils.MaskIP(ip)
|
||||
now := time.Now().Unix()
|
||||
|
||||
sess, err := db.GetSession(userID, deviceKey)
|
||||
if err == nil {
|
||||
if sess.Status == model.SessionInactive {
|
||||
max := setting.GetInt(conf.MaxDevices, 0)
|
||||
if max > 0 {
|
||||
count, err := db.CountActiveSessionsByUser(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count >= int64(max) {
|
||||
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
|
||||
if policy == "evict_oldest" {
|
||||
if oldest, gerr := db.GetOldestActiveSession(userID); gerr == nil {
|
||||
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return errors.WithStack(errs.TooManyDevices)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sess.Status = model.SessionActive
|
||||
sess.LastActive = now
|
||||
sess.UserAgent = ua
|
||||
sess.IP = ip
|
||||
return db.UpsertSession(sess)
|
||||
}
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
max := setting.GetInt(conf.MaxDevices, 0)
|
||||
if max > 0 {
|
||||
count, err := db.CountActiveSessionsByUser(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count >= int64(max) {
|
||||
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
|
||||
if policy == "evict_oldest" {
|
||||
if oldest, gerr := db.GetOldestActiveSession(userID); gerr == nil {
|
||||
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return errors.WithStack(errs.TooManyDevices)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return db.CreateSession(&model.Session{
|
||||
UserID: userID,
|
||||
DeviceKey: deviceKey,
|
||||
UserAgent: ua,
|
||||
IP: ip,
|
||||
LastActive: now,
|
||||
Status: model.SessionActive,
|
||||
})
|
||||
}
|
||||
|
||||
// Refresh updates last_active for the session.
|
||||
func Refresh(userID uint, deviceKey string) {
|
||||
_ = db.UpdateSessionLastActive(userID, deviceKey, time.Now().Unix())
|
||||
}
|
||||
8
internal/errs/device.go
Normal file
8
internal/errs/device.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package errs
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
TooManyDevices = errors.New("too many active devices")
|
||||
SessionInactive = errors.New("session inactive")
|
||||
)
|
||||
@@ -4,4 +4,5 @@ import "errors"
|
||||
|
||||
var (
|
||||
EmptyToken = errors.New("empty token")
|
||||
LinkIsDir = errors.New("link is dir")
|
||||
)
|
||||
|
||||
7
internal/errs/role.go
Normal file
7
internal/errs/role.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package errs
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrChangeDefaultRole = errors.New("cannot modify admin role")
|
||||
)
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"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/server/common"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -45,8 +46,13 @@ func list(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error)
|
||||
}
|
||||
|
||||
func whetherHide(user *model.User, meta *model.Meta, path string) bool {
|
||||
// if is admin, don't hide
|
||||
if user == nil || user.CanSeeHides() {
|
||||
// if user is nil, don't hide
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, path)
|
||||
// if user has see-hides permission, don't hide
|
||||
if common.HasPermission(perm, common.PermSeeHides) {
|
||||
return false
|
||||
}
|
||||
// if meta is nil, don't hide
|
||||
|
||||
12
internal/model/label.go
Normal file
12
internal/model/label.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Label struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"` // unique key
|
||||
Type int `json:"type"` // use to type
|
||||
Name string `json:"name"` // use to name
|
||||
Description string `json:"description"` // use to description
|
||||
BgColor string `json:"bg_color"` // use to bg_color
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
}
|
||||
11
internal/model/label_file_binding.go
Normal file
11
internal/model/label_file_binding.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type LabelFileBinding struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"` // unique key
|
||||
UserId uint `json:"user_id"` // use to user_id
|
||||
LabelId uint `json:"label_id"` // use to label_id
|
||||
FileName string `json:"file_name"` // use to file_name
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
}
|
||||
@@ -55,6 +55,21 @@ type FileStreamer interface {
|
||||
|
||||
type UpdateProgress func(percentage float64)
|
||||
|
||||
// Reference implementation from OpenListTeam:
|
||||
// https://github.com/OpenListTeam/OpenList/blob/a703b736c9346c483bae56905a39bc07bf781cff/internal/model/obj.go#L58
|
||||
func UpdateProgressWithRange(inner UpdateProgress, start, end float64) UpdateProgress {
|
||||
return func(p float64) {
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
if p > 100 {
|
||||
p = 100
|
||||
}
|
||||
scaled := start + (end-start)*(p/100.0)
|
||||
inner(scaled)
|
||||
}
|
||||
}
|
||||
|
||||
type URL interface {
|
||||
URL() string
|
||||
}
|
||||
|
||||
18
internal/model/obj_file.go
Normal file
18
internal/model/obj_file.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type ObjFile struct {
|
||||
Id string `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Modified time.Time `json:"modified"`
|
||||
Created time.Time `json:"created"`
|
||||
Sign string `json:"sign"`
|
||||
Thumb string `json:"thumb"`
|
||||
Type int `json:"type"`
|
||||
HashInfoStr string `json:"hashinfo"`
|
||||
}
|
||||
27
internal/model/paths.go
Normal file
27
internal/model/paths.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Paths []string
|
||||
|
||||
func (p Paths) Value() (driver.Value, error) {
|
||||
return json.Marshal([]string(p))
|
||||
}
|
||||
|
||||
func (p *Paths) Scan(value interface{}) error {
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, (*[]string)(p))
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), (*[]string)(p))
|
||||
case nil:
|
||||
*p = nil
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("cannot scan %T", value)
|
||||
}
|
||||
}
|
||||
53
internal/model/role.go
Normal file
53
internal/model/role.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PermissionEntry defines permission bitmask for a specific path.
|
||||
type PermissionEntry struct {
|
||||
Path string `json:"path"` // path prefix, e.g. "/admin"
|
||||
Permission int32 `json:"permission"` // bitmask permissions
|
||||
}
|
||||
|
||||
// Role represents a permission template which can be bound to users.
|
||||
type Role struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"unique" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Default bool `json:"default" gorm:"default:false"`
|
||||
// PermissionScopes stores structured permission list and is ignored by gorm.
|
||||
PermissionScopes []PermissionEntry `json:"permission_scopes" gorm:"-"`
|
||||
// RawPermission is the JSON representation of PermissionScopes stored in DB.
|
||||
RawPermission string `json:"-" gorm:"type:text"`
|
||||
}
|
||||
|
||||
// BeforeSave GORM hook serializes PermissionScopes into RawPermission.
|
||||
func (r *Role) BeforeSave(tx *gorm.DB) error {
|
||||
if len(r.PermissionScopes) == 0 {
|
||||
r.RawPermission = ""
|
||||
return nil
|
||||
}
|
||||
bs, err := json.Marshal(r.PermissionScopes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.RawPermission = string(bs)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterFind GORM hook deserializes RawPermission into PermissionScopes.
|
||||
func (r *Role) AfterFind(tx *gorm.DB) error {
|
||||
if r.RawPermission == "" {
|
||||
r.PermissionScopes = nil
|
||||
return nil
|
||||
}
|
||||
var scopes []PermissionEntry
|
||||
if err := json.Unmarshal([]byte(r.RawPermission), &scopes); err != nil {
|
||||
return err
|
||||
}
|
||||
r.PermissionScopes = scopes
|
||||
return nil
|
||||
}
|
||||
36
internal/model/roles.go
Normal file
36
internal/model/roles.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Roles []int
|
||||
|
||||
func (r Roles) Value() (driver.Value, error) {
|
||||
return json.Marshal([]int(r))
|
||||
}
|
||||
|
||||
func (r *Roles) Scan(value interface{}) error {
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, (*[]int)(r))
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), (*[]int)(r))
|
||||
case nil:
|
||||
*r = nil
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("cannot scan %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r Roles) Contains(role int) bool {
|
||||
for _, v := range r {
|
||||
if v == role {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
16
internal/model/session.go
Normal file
16
internal/model/session.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
// Session represents a device session of a user.
|
||||
type Session struct {
|
||||
UserID uint `json:"user_id" gorm:"index"`
|
||||
DeviceKey string `json:"device_key" gorm:"primaryKey;size:64"`
|
||||
UserAgent string `json:"user_agent" gorm:"size:255"`
|
||||
IP string `json:"ip" gorm:"size:64"`
|
||||
LastActive int64 `json:"last_active"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
const (
|
||||
SessionActive = iota
|
||||
SessionInactive
|
||||
)
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
GENERAL = iota
|
||||
GUEST // only one exists
|
||||
ADMIN
|
||||
NEWGENERAL
|
||||
)
|
||||
|
||||
const StaticHashSalt = "https://github.com/alist-org/alist"
|
||||
@@ -29,7 +30,8 @@ type User struct {
|
||||
Salt string `json:"-"` // unique salt
|
||||
Password string `json:"password"` // password
|
||||
BasePath string `json:"base_path"` // base path
|
||||
Role int `json:"role"` // user's role
|
||||
Role Roles `json:"role" gorm:"type:text"` // user's roles
|
||||
RolesDetail []Role `json:"-" gorm:"-"`
|
||||
Disabled bool `json:"disabled"`
|
||||
// Determine permissions by bit
|
||||
// 0: can see hidden files
|
||||
@@ -46,6 +48,7 @@ type User struct {
|
||||
// 11: ftp/sftp write
|
||||
// 12: can read archives
|
||||
// 13: can decompress archives
|
||||
// 14: check path limit
|
||||
Permission int32 `json:"permission"`
|
||||
OtpSecret string `json:"-"`
|
||||
SsoID string `json:"sso_id"` // unique by sso platform
|
||||
@@ -53,11 +56,11 @@ type User struct {
|
||||
}
|
||||
|
||||
func (u *User) IsGuest() bool {
|
||||
return u.Role == GUEST
|
||||
return u.Role.Contains(GUEST)
|
||||
}
|
||||
|
||||
func (u *User) IsAdmin() bool {
|
||||
return u.Role == ADMIN
|
||||
return u.Role.Contains(ADMIN)
|
||||
}
|
||||
|
||||
func (u *User) ValidateRawPassword(password string) error {
|
||||
@@ -137,8 +140,34 @@ func (u *User) CanDecompress() bool {
|
||||
return (u.Permission>>13)&1 == 1
|
||||
}
|
||||
|
||||
func (u *User) CheckPathLimit() bool {
|
||||
return (u.Permission>>14)&1 == 1
|
||||
}
|
||||
|
||||
func (u *User) JoinPath(reqPath string) (string, error) {
|
||||
return utils.JoinBasePath(u.BasePath, reqPath)
|
||||
if reqPath == "/" {
|
||||
return utils.FixAndCleanPath(u.BasePath), nil
|
||||
}
|
||||
path, err := utils.JoinBasePath(u.BasePath, reqPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if path != "/" && u.CheckPathLimit() {
|
||||
basePaths := GetAllBasePathsFromRoles(u)
|
||||
match := false
|
||||
for _, base := range basePaths {
|
||||
if utils.IsSubPath(base, path) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return "", errs.PermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func StaticHash(password string) string {
|
||||
@@ -179,3 +208,33 @@ func (u *User) WebAuthnCredentials() []webauthn.Credential {
|
||||
func (u *User) WebAuthnIcon() string {
|
||||
return "https://alistgo.com/logo.svg"
|
||||
}
|
||||
|
||||
// FetchRole is used to load role details by id. It should be set by the op package
|
||||
// to avoid an import cycle between model and op.
|
||||
var FetchRole func(uint) (*Role, error)
|
||||
|
||||
// GetAllBasePathsFromRoles returns all permission paths from user's roles
|
||||
func GetAllBasePathsFromRoles(u *User) []string {
|
||||
basePaths := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for _, rid := range u.Role {
|
||||
if FetchRole == nil {
|
||||
continue
|
||||
}
|
||||
role, err := FetchRole(uint(rid))
|
||||
if err != nil || role == nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if entry.Path == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[entry.Path]; !ok {
|
||||
basePaths = append(basePaths, entry.Path)
|
||||
seen[entry.Path] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return basePaths
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package op
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
@@ -82,6 +83,18 @@ var settingItemHooks = map[string]SettingItemHook{
|
||||
conf.SlicesMap[conf.IgnoreDirectLinkParams] = strings.Split(item.Value, ",")
|
||||
return nil
|
||||
},
|
||||
conf.DefaultRole: func(item *model.SettingItem) error {
|
||||
v := strings.TrimSpace(item.Value)
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
id, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
_, err = GetRole(uint(id))
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
func RegisterSettingItemHook(key string, hook SettingItemHook) {
|
||||
|
||||
24
internal/op/label.go
Normal file
24
internal/op/label.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package op
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func DeleteLabelById(ctx context.Context, id, userId uint) error {
|
||||
_, err := db.GetLabelById(id)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed get label")
|
||||
}
|
||||
|
||||
if db.GetLabelFileBinDingByLabelIdExists(id, userId) {
|
||||
return errors.New("label have binding relationships")
|
||||
}
|
||||
|
||||
// delete the label in the database
|
||||
if err := db.DeleteLabelById(id); err != nil {
|
||||
return errors.WithMessage(err, "failed delete label in database")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
195
internal/op/label_file_binding.go
Normal file
195
internal/op/label_file_binding.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package op
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CreateLabelFileBinDingReq struct {
|
||||
Id string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Modified time.Time `json:"modified"`
|
||||
Created time.Time `json:"created"`
|
||||
Sign string `json:"sign"`
|
||||
Thumb string `json:"thumb"`
|
||||
Type int `json:"type"`
|
||||
HashInfoStr string `json:"hashinfo"`
|
||||
LabelIds string `json:"label_ids"`
|
||||
LabelIDs []uint64 `json:"labelIdList"`
|
||||
}
|
||||
|
||||
type ObjLabelResp struct {
|
||||
Id string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Modified time.Time `json:"modified"`
|
||||
Created time.Time `json:"created"`
|
||||
Sign string `json:"sign"`
|
||||
Thumb string `json:"thumb"`
|
||||
Type int `json:"type"`
|
||||
HashInfoStr string `json:"hashinfo"`
|
||||
LabelList []model.Label `json:"label_list"`
|
||||
}
|
||||
|
||||
func GetLabelByFileName(userId uint, fileName string) ([]model.Label, error) {
|
||||
labelIds, err := db.GetLabelIds(userId, fileName)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed get label_file_binding")
|
||||
}
|
||||
var labels []model.Label
|
||||
if len(labelIds) > 0 {
|
||||
if labels, err = db.GetLabelByIds(labelIds); err != nil {
|
||||
return nil, errors.WithMessage(err, "failed labels in database")
|
||||
}
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
func GetLabelsByFileNamesPublic(fileNames []string) (map[string][]model.Label, error) {
|
||||
return db.GetLabelsByFileNamesPublic(fileNames)
|
||||
}
|
||||
|
||||
func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
|
||||
if err := db.DelLabelFileBinDingByFileName(userId, req.Name); err != nil {
|
||||
return errors.WithMessage(err, "failed del label_file_bin_ding in database")
|
||||
}
|
||||
|
||||
ids, err := collectLabelIDs(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if err = db.CreateLabelFileBinDing(req.Name, uint(id), userId); err != nil {
|
||||
return errors.WithMessage(err, "failed labels in database")
|
||||
}
|
||||
}
|
||||
|
||||
if !db.GetFileByNameExists(req.Name) {
|
||||
objFile := model.ObjFile{
|
||||
Id: req.Id,
|
||||
UserId: userId,
|
||||
Path: req.Path,
|
||||
Name: req.Name,
|
||||
Size: req.Size,
|
||||
IsDir: req.IsDir,
|
||||
Modified: req.Modified,
|
||||
Created: req.Created,
|
||||
Sign: req.Sign,
|
||||
Thumb: req.Thumb,
|
||||
Type: req.Type,
|
||||
HashInfoStr: req.HashInfoStr,
|
||||
}
|
||||
if err := db.CreateObjFile(objFile); err != nil {
|
||||
return errors.WithMessage(err, "failed file in database")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetFileByLabel(userId uint, labelId string) (result []ObjLabelResp, err error) {
|
||||
labelMap := strings.Split(labelId, ",")
|
||||
var labelIds []uint
|
||||
var labelsFile []model.LabelFileBinding
|
||||
var labels []model.Label
|
||||
var labelsFileMap = make(map[string][]model.Label)
|
||||
var labelsMap = make(map[uint]model.Label)
|
||||
if labelIds, err = StringSliceToUintSlice(labelMap); err != nil {
|
||||
return nil, errors.WithMessage(err, "failed string to uint err")
|
||||
}
|
||||
//查询标签信息
|
||||
if labels, err = db.GetLabelByIds(labelIds); err != nil {
|
||||
return nil, errors.WithMessage(err, "failed labels in database")
|
||||
}
|
||||
for _, val := range labels {
|
||||
labelsMap[val.ID] = val
|
||||
}
|
||||
//查询标签对应文件名列表
|
||||
if labelsFile, err = db.GetLabelFileBinDingByLabelId(labelIds, userId); err != nil {
|
||||
return nil, errors.WithMessage(err, "failed labels in database")
|
||||
}
|
||||
for _, value := range labelsFile {
|
||||
var labelTemp model.Label
|
||||
labelTemp = labelsMap[value.LabelId]
|
||||
labelsFileMap[value.FileName] = append(labelsFileMap[value.FileName], labelTemp)
|
||||
}
|
||||
for index, v := range labelsFileMap {
|
||||
objFile, err := db.GetFileByName(index, userId)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed GetFileByName in database")
|
||||
}
|
||||
objLabel := ObjLabelResp{
|
||||
Id: objFile.Id,
|
||||
Path: objFile.Path,
|
||||
Name: objFile.Name,
|
||||
Size: objFile.Size,
|
||||
IsDir: objFile.IsDir,
|
||||
Modified: objFile.Modified,
|
||||
Created: objFile.Created,
|
||||
Sign: objFile.Sign,
|
||||
Thumb: objFile.Thumb,
|
||||
Type: objFile.Type,
|
||||
HashInfoStr: objFile.HashInfoStr,
|
||||
LabelList: v,
|
||||
}
|
||||
result = append(result, objLabel)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func StringSliceToUintSlice(strSlice []string) ([]uint, error) {
|
||||
uintSlice := make([]uint, len(strSlice))
|
||||
for i, str := range strSlice {
|
||||
// 使用strconv.ParseUint将字符串转换为uint64
|
||||
uint64Value, err := strconv.ParseUint(str, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err // 如果转换失败,返回错误
|
||||
}
|
||||
// 将uint64值转换为uint(注意:这里可能存在精度损失,如果uint64值超出了uint的范围)
|
||||
uintSlice[i] = uint(uint64Value)
|
||||
}
|
||||
return uintSlice, nil
|
||||
}
|
||||
|
||||
func RestoreLabelFileBindings(bindings []model.LabelFileBinding, keepIDs bool, override bool) error {
|
||||
return db.RestoreLabelFileBindings(bindings, keepIDs, override)
|
||||
}
|
||||
|
||||
func collectLabelIDs(req CreateLabelFileBinDingReq) ([]uint64, error) {
|
||||
if len(req.LabelIDs) > 0 {
|
||||
return req.LabelIDs, nil
|
||||
}
|
||||
s := strings.TrimSpace(req.LabelIds)
|
||||
if s == "" {
|
||||
return nil, nil
|
||||
}
|
||||
replacer := strings.NewReplacer(",", ",", "、", ",", ";", ",", ";", ",")
|
||||
s = replacer.Replace(s)
|
||||
parts := strings.Split(s, ",")
|
||||
ids := make([]uint64, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseUint(p, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid label ID '%s': %v", p, err)
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
195
internal/op/role.go
Normal file
195
internal/op/role.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package op
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"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/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/singleflight"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
var roleCache = cache.NewMemCache[*model.Role](cache.WithShards[*model.Role](2))
|
||||
var roleG singleflight.Group[*model.Role]
|
||||
|
||||
func init() {
|
||||
model.FetchRole = GetRole
|
||||
}
|
||||
|
||||
func GetRole(id uint) (*model.Role, error) {
|
||||
key := fmt.Sprint(id)
|
||||
if r, ok := roleCache.Get(key); ok {
|
||||
return r, nil
|
||||
}
|
||||
r, err, _ := roleG.Do(key, func() (*model.Role, error) {
|
||||
_r, err := db.GetRole(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roleCache.Set(key, _r, cache.WithEx[*model.Role](time.Hour))
|
||||
return _r, nil
|
||||
})
|
||||
return r, err
|
||||
}
|
||||
|
||||
func GetRoleByName(name string) (*model.Role, error) {
|
||||
if r, ok := roleCache.Get(name); ok {
|
||||
return r, nil
|
||||
}
|
||||
r, err, _ := roleG.Do(name, func() (*model.Role, error) {
|
||||
_r, err := db.GetRoleByName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roleCache.Set(name, _r, cache.WithEx[*model.Role](time.Hour))
|
||||
return _r, nil
|
||||
})
|
||||
return r, err
|
||||
}
|
||||
|
||||
func GetDefaultRoleID() int {
|
||||
item, err := GetSettingItemByKey(conf.DefaultRole)
|
||||
if err == nil && item != nil && item.Value != "" {
|
||||
if id, err := strconv.Atoi(item.Value); err == nil && id != 0 {
|
||||
return id
|
||||
}
|
||||
if r, err := db.GetRoleByName(item.Value); err == nil {
|
||||
return int(r.ID)
|
||||
}
|
||||
}
|
||||
var r model.Role
|
||||
if err := db.GetDb().Where("`default` = ?", true).First(&r).Error; err == nil {
|
||||
return int(r.ID)
|
||||
}
|
||||
return int(model.GUEST)
|
||||
}
|
||||
|
||||
func GetRolesByUserID(userID uint) ([]model.Role, error) {
|
||||
user, err := GetUserById(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var roles []model.Role
|
||||
for _, roleID := range user.Role {
|
||||
key := fmt.Sprint(roleID)
|
||||
|
||||
if r, ok := roleCache.Get(key); ok {
|
||||
roles = append(roles, *r)
|
||||
continue
|
||||
}
|
||||
|
||||
r, err, _ := roleG.Do(key, func() (*model.Role, error) {
|
||||
_r, err := db.GetRole(uint(roleID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roleCache.Set(key, _r, cache.WithEx[*model.Role](time.Hour))
|
||||
return _r, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roles = append(roles, *r)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func GetRoles(pageIndex, pageSize int) ([]model.Role, int64, error) {
|
||||
return db.GetRoles(pageIndex, pageSize)
|
||||
}
|
||||
|
||||
func CreateRole(r *model.Role) error {
|
||||
for i := range r.PermissionScopes {
|
||||
r.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path)
|
||||
}
|
||||
roleCache.Del(fmt.Sprint(r.ID))
|
||||
roleCache.Del(r.Name)
|
||||
if err := db.CreateRole(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Default {
|
||||
roleCache.Clear()
|
||||
item, err := GetSettingItemByKey(conf.DefaultRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Value = strconv.Itoa(int(r.ID))
|
||||
if err := SaveSettingItem(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateRole(r *model.Role) error {
|
||||
old, err := db.GetRole(r.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch old.Name {
|
||||
case "admin":
|
||||
return errs.ErrChangeDefaultRole
|
||||
case "guest":
|
||||
r.Name = "guest"
|
||||
}
|
||||
for i := range r.PermissionScopes {
|
||||
r.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path)
|
||||
}
|
||||
//if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 &&
|
||||
// old.PermissionScopes[0].Path != r.PermissionScopes[0].Path {
|
||||
//
|
||||
// oldPath := old.PermissionScopes[0].Path
|
||||
// newPath := r.PermissionScopes[0].Path
|
||||
//
|
||||
// users, err := db.GetUsersByRole(int(r.ID))
|
||||
// if err != nil {
|
||||
// return errors.WithMessage(err, "failed to get users by role")
|
||||
// }
|
||||
//
|
||||
// modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath, users)
|
||||
// if err != nil {
|
||||
// return errors.WithMessage(err, "failed to update user base path when role updated")
|
||||
// }
|
||||
//
|
||||
// for _, name := range modifiedUsernames {
|
||||
// userCache.Del(name)
|
||||
// }
|
||||
//}
|
||||
roleCache.Del(fmt.Sprint(r.ID))
|
||||
roleCache.Del(r.Name)
|
||||
if err := db.UpdateRole(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Default {
|
||||
roleCache.Clear()
|
||||
item, err := GetSettingItemByKey(conf.DefaultRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Value = strconv.Itoa(int(r.ID))
|
||||
if err := SaveSettingItem(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteRole(id uint) error {
|
||||
old, err := db.GetRole(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if old.Name == "admin" || old.Name == "guest" {
|
||||
return errs.ErrChangeDefaultRole
|
||||
}
|
||||
roleCache.Del(fmt.Sprint(id))
|
||||
roleCache.Del(old.Name)
|
||||
return db.DeleteRole(id)
|
||||
}
|
||||
@@ -41,11 +41,28 @@ func GetStorageByMountPath(mountPath string) (driver.Driver, error) {
|
||||
return storageDriver, nil
|
||||
}
|
||||
|
||||
func firstPathSegment(p string) string {
|
||||
p = utils.FixAndCleanPath(p)
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
if p == "" {
|
||||
return ""
|
||||
}
|
||||
if i := strings.Index(p, "/"); i >= 0 {
|
||||
return p[:i]
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// CreateStorage Save the storage to database so storage can get an id
|
||||
// then instantiate corresponding driver and save it in memory
|
||||
func CreateStorage(ctx context.Context, storage model.Storage) (uint, error) {
|
||||
storage.Modified = time.Now()
|
||||
storage.MountPath = utils.FixAndCleanPath(storage.MountPath)
|
||||
|
||||
//if storage.MountPath == "/" {
|
||||
// return 0, errors.New("Mount path cannot be '/'")
|
||||
//}
|
||||
|
||||
var err error
|
||||
// check driver first
|
||||
driverName := storage.Driver
|
||||
@@ -205,6 +222,9 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
|
||||
}
|
||||
storage.Modified = time.Now()
|
||||
storage.MountPath = utils.FixAndCleanPath(storage.MountPath)
|
||||
//if storage.MountPath == "/" {
|
||||
// return errors.New("Mount path cannot be '/'")
|
||||
//}
|
||||
err = db.UpdateStorage(&storage)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed update storage in database")
|
||||
@@ -216,6 +236,29 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
|
||||
if oldStorage.MountPath != storage.MountPath {
|
||||
// mount path renamed, need to drop the storage
|
||||
storagesMap.Delete(oldStorage.MountPath)
|
||||
modifiedRoleIDs, err := db.UpdateRolePermissionsPathPrefix(oldStorage.MountPath, storage.MountPath)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to update role permissions")
|
||||
}
|
||||
for _, id := range modifiedRoleIDs {
|
||||
roleCache.Del(fmt.Sprint(id))
|
||||
}
|
||||
|
||||
//modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldStorage.MountPath, storage.MountPath)
|
||||
//if err != nil {
|
||||
// return errors.WithMessage(err, "failed to update user base path")
|
||||
//}
|
||||
for _, id := range modifiedRoleIDs {
|
||||
roleCache.Del(fmt.Sprint(id))
|
||||
|
||||
users, err := db.GetUsersByRole(int(id))
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to get users by role")
|
||||
}
|
||||
for _, user := range users {
|
||||
userCache.Del(user.Username)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed get storage driver")
|
||||
@@ -236,6 +279,34 @@ func DeleteStorageById(ctx context.Context, id uint) error {
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed get storage")
|
||||
}
|
||||
firstMount := firstPathSegment(storage.MountPath)
|
||||
if firstMount != "" {
|
||||
roles, err := db.GetAllRoles()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to load roles")
|
||||
}
|
||||
users, err := db.GetAllUsers()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to load users")
|
||||
}
|
||||
var usedBy []string
|
||||
for _, r := range roles {
|
||||
for _, entry := range r.PermissionScopes {
|
||||
if firstPathSegment(entry.Path) == firstMount {
|
||||
usedBy = append(usedBy, "role:"+r.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, u := range users {
|
||||
if firstPathSegment(u.BasePath) == firstMount {
|
||||
usedBy = append(usedBy, "user:"+u.Username)
|
||||
}
|
||||
}
|
||||
if len(usedBy) > 0 {
|
||||
return errors.Errorf("storage is used by %s, please cancel usage first", strings.Join(usedBy, ", "))
|
||||
}
|
||||
}
|
||||
if !storage.Disabled {
|
||||
storageDriver, err := GetStorageByMountPath(storage.MountPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,7 +18,11 @@ var adminUser *model.User
|
||||
|
||||
func GetAdmin() (*model.User, error) {
|
||||
if adminUser == nil {
|
||||
user, err := db.GetUserByRole(model.ADMIN)
|
||||
role, err := GetRoleByName("admin")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := db.GetUserByRole(int(role.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -29,7 +33,11 @@ func GetAdmin() (*model.User, error) {
|
||||
|
||||
func GetGuest() (*model.User, error) {
|
||||
if guestUser == nil {
|
||||
user, err := db.GetUserByRole(model.GUEST)
|
||||
role, err := GetRoleByName("guest")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := db.GetUserByRole(int(role.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -42,6 +50,10 @@ func GetUserByRole(role int) (*model.User, error) {
|
||||
return db.GetUserByRole(role)
|
||||
}
|
||||
|
||||
func GetUsersByRole(role int) ([]model.User, error) {
|
||||
return db.GetUsersByRole(role)
|
||||
}
|
||||
|
||||
func GetUserByName(username string) (*model.User, error) {
|
||||
if username == "" {
|
||||
return nil, errs.EmptyUsername
|
||||
@@ -70,7 +82,25 @@ func GetUsers(pageIndex, pageSize int) (users []model.User, count int64, err err
|
||||
|
||||
func CreateUser(u *model.User) error {
|
||||
u.BasePath = utils.FixAndCleanPath(u.BasePath)
|
||||
return db.CreateUser(u)
|
||||
|
||||
err := db.CreateUser(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
roles, err := GetRolesByUserID(u.ID)
|
||||
if err == nil {
|
||||
for _, role := range roles {
|
||||
if len(role.PermissionScopes) > 0 {
|
||||
u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
|
||||
break
|
||||
}
|
||||
}
|
||||
_ = db.UpdateUser(u)
|
||||
userCache.Del(u.Username)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteUserById(id uint) error {
|
||||
@@ -98,6 +128,17 @@ func UpdateUser(u *model.User) error {
|
||||
}
|
||||
userCache.Del(old.Username)
|
||||
u.BasePath = utils.FixAndCleanPath(u.BasePath)
|
||||
//if len(u.Role) > 0 {
|
||||
// roles, err := GetRolesByUserID(u.ID)
|
||||
// if err == nil {
|
||||
// for _, role := range roles {
|
||||
// if len(role.PermissionScopes) > 0 {
|
||||
// u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
return db.UpdateUser(u)
|
||||
}
|
||||
|
||||
@@ -128,3 +169,11 @@ func DelUserCache(username string) error {
|
||||
userCache.Del(username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func CountEnabledAdminsExcluding(userID uint) (int64, error) {
|
||||
adminRole, err := GetRoleByName("admin")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return db.CountUsersByRoleAndEnabledExclude(adminRole.ID, userID)
|
||||
}
|
||||
|
||||
8
internal/session/session.go
Normal file
8
internal/session/session.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package session
|
||||
|
||||
import "github.com/alist-org/alist/v3/internal/db"
|
||||
|
||||
// MarkInactive marks the session with the given ID as inactive.
|
||||
func MarkInactive(sessionID string) error {
|
||||
return db.MarkInactive(sessionID)
|
||||
}
|
||||
30
pkg/utils/mask.go
Normal file
30
pkg/utils/mask.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
|
||||
// MaskIP anonymizes middle segments of an IP address.
|
||||
func MaskIP(ip string) string {
|
||||
if ip == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(ip, ":") {
|
||||
parts := strings.Split(ip, ":")
|
||||
if len(parts) > 2 {
|
||||
for i := 1; i < len(parts)-1; i++ {
|
||||
if parts[i] != "" {
|
||||
parts[i] = "*"
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ":")
|
||||
}
|
||||
return ip
|
||||
}
|
||||
parts := strings.Split(ip, ".")
|
||||
if len(parts) == 4 {
|
||||
for i := 1; i < len(parts)-1; i++ {
|
||||
parts[i] = "*"
|
||||
}
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
return ip
|
||||
}
|
||||
@@ -88,6 +88,13 @@ func JoinBasePath(basePath, reqPath string) (string, error) {
|
||||
strings.Contains(reqPath, "/../") {
|
||||
return "", errs.RelativePath
|
||||
}
|
||||
|
||||
reqPath = FixAndCleanPath(reqPath)
|
||||
|
||||
if strings.HasPrefix(reqPath, "/") {
|
||||
return reqPath, nil
|
||||
}
|
||||
|
||||
return stdpath.Join(FixAndCleanPath(basePath), FixAndCleanPath(reqPath)), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"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/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/dlclark/regexp2"
|
||||
)
|
||||
|
||||
func IsStorageSignEnabled(rawPath string) bool {
|
||||
@@ -32,30 +28,11 @@ func IsApply(metaPath, reqPath string, applySub bool) bool {
|
||||
}
|
||||
|
||||
func CanAccess(user *model.User, meta *model.Meta, reqPath string, password string) bool {
|
||||
// if the reqPath is in hide (only can check the nearest meta) and user can't see hides, can't access
|
||||
if meta != nil && !user.CanSeeHides() && meta.Hide != "" &&
|
||||
IsApply(meta.Path, path.Dir(reqPath), meta.HSub) { // the meta should apply to the parent of current path
|
||||
for _, hide := range strings.Split(meta.Hide, "\n") {
|
||||
re := regexp2.MustCompile(hide, regexp2.None)
|
||||
if isMatch, _ := re.MatchString(path.Base(reqPath)); isMatch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
// if is not guest and can access without password
|
||||
if user.CanAccessWithoutPassword() {
|
||||
return true
|
||||
}
|
||||
// if meta is nil or password is empty, can access
|
||||
if meta == nil || meta.Password == "" {
|
||||
return true
|
||||
}
|
||||
// if meta doesn't apply to sub_folder, can access
|
||||
if !utils.PathEqual(meta.Path, reqPath) && !meta.PSub {
|
||||
return true
|
||||
}
|
||||
// validate password
|
||||
return meta.Password == password
|
||||
// Deprecated: CanAccess is kept for backward compatibility.
|
||||
// The logic has been moved to CanAccessWithRoles which performs the
|
||||
// necessary checks based on role permissions. This wrapper ensures
|
||||
// older calls still work without relying on user permission bits.
|
||||
return CanAccessWithRoles(user, meta, reqPath, password)
|
||||
}
|
||||
|
||||
// ShouldProxy TODO need optimize
|
||||
|
||||
137
server/common/role_perm.go
Normal file
137
server/common/role_perm.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
PermSeeHides = iota
|
||||
PermAccessWithoutPassword
|
||||
PermAddOfflineDownload
|
||||
PermWrite
|
||||
PermRename
|
||||
PermMove
|
||||
PermCopy
|
||||
PermRemove
|
||||
PermWebdavRead
|
||||
PermWebdavManage
|
||||
PermFTPAccess
|
||||
PermFTPManage
|
||||
PermReadArchives
|
||||
PermDecompress
|
||||
PermPathLimit
|
||||
)
|
||||
|
||||
func HasPermission(perm int32, bit uint) bool {
|
||||
return (perm>>bit)&1 == 1
|
||||
}
|
||||
|
||||
func MergeRolePermissions(u *model.User, reqPath string) int32 {
|
||||
if u == nil {
|
||||
return 0
|
||||
}
|
||||
var perm int32
|
||||
for _, rid := range u.Role {
|
||||
role, err := op.GetRole(uint(rid))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) {
|
||||
for _, entry := range role.PermissionScopes {
|
||||
perm |= entry.Permission
|
||||
}
|
||||
} else {
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.IsSubPath(entry.Path, reqPath) {
|
||||
perm |= entry.Permission
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return perm
|
||||
}
|
||||
|
||||
func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password string) bool {
|
||||
if !CanReadPathByRole(u, reqPath) {
|
||||
return false
|
||||
}
|
||||
perm := MergeRolePermissions(u, reqPath)
|
||||
if meta != nil && !HasPermission(perm, PermSeeHides) && meta.Hide != "" &&
|
||||
IsApply(meta.Path, path.Dir(reqPath), meta.HSub) {
|
||||
for _, hide := range strings.Split(meta.Hide, "\n") {
|
||||
re := regexp2.MustCompile(hide, regexp2.None)
|
||||
if isMatch, _ := re.MatchString(path.Base(reqPath)); isMatch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if HasPermission(perm, PermAccessWithoutPassword) {
|
||||
return true
|
||||
}
|
||||
if meta == nil || meta.Password == "" {
|
||||
return true
|
||||
}
|
||||
if !utils.PathEqual(meta.Path, reqPath) && !meta.PSub {
|
||||
return true
|
||||
}
|
||||
return meta.Password == password
|
||||
}
|
||||
|
||||
func CanReadPathByRole(u *model.User, reqPath string) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) {
|
||||
return len(u.Role) > 0
|
||||
}
|
||||
for _, rid := range u.Role {
|
||||
role, err := op.GetRole(uint(rid))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.PathEqual(entry.Path, reqPath) || utils.IsSubPath(entry.Path, reqPath) || utils.IsSubPath(reqPath, entry.Path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasChildPermission checks whether any child path under reqPath grants the
|
||||
// specified permission bit.
|
||||
func HasChildPermission(u *model.User, reqPath string, bit uint) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
for _, rid := range u.Role {
|
||||
role, err := op.GetRole(uint(rid))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.IsSubPath(reqPath, entry.Path) && HasPermission(entry.Permission, bit) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckPathLimitWithRoles checks whether the path is allowed when the user has
|
||||
// the `PermPathLimit` permission for the target path. When the user does not
|
||||
// have this permission, the check passes by default.
|
||||
func CheckPathLimitWithRoles(u *model.User, reqPath string) bool {
|
||||
perm := MergeRolePermissions(u, reqPath)
|
||||
if HasPermission(perm, PermPathLimit) {
|
||||
return CanReadPathByRole(u, reqPath)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"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/common"
|
||||
"github.com/alist-org/alist/v3/server/ftp"
|
||||
"math/rand"
|
||||
"net"
|
||||
@@ -130,7 +131,8 @@ func (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if userObj.Disabled || !userObj.CanFTPAccess() {
|
||||
perm := common.MergeRolePermissions(userObj, userObj.BasePath)
|
||||
if userObj.Disabled || !common.HasPermission(perm, common.PermFTPAccess) {
|
||||
return nil, errors.New("user is not allowed to access via FTP")
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ func Mkdir(ctx context.Context, path string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !user.CanWrite() || !user.CanFTPManage() {
|
||||
perm := common.MergeRolePermissions(user, reqPath)
|
||||
if !common.HasPermission(perm, common.PermWrite) || !common.HasPermission(perm, common.PermFTPManage) {
|
||||
meta, err := op.GetNearestMeta(stdpath.Dir(reqPath))
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
@@ -34,7 +35,8 @@ func Mkdir(ctx context.Context, path string) error {
|
||||
|
||||
func Remove(ctx context.Context, path string) error {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
if !user.CanRemove() || !user.CanFTPManage() {
|
||||
perm := common.MergeRolePermissions(user, path)
|
||||
if !common.HasPermission(perm, common.PermRemove) || !common.HasPermission(perm, common.PermFTPManage) {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
reqPath, err := user.JoinPath(path)
|
||||
@@ -56,13 +58,14 @@ func Rename(ctx context.Context, oldPath, newPath string) error {
|
||||
}
|
||||
srcDir, srcBase := stdpath.Split(srcPath)
|
||||
dstDir, dstBase := stdpath.Split(dstPath)
|
||||
permSrc := common.MergeRolePermissions(user, srcPath)
|
||||
if srcDir == dstDir {
|
||||
if !user.CanRename() || !user.CanFTPManage() {
|
||||
if !common.HasPermission(permSrc, common.PermRename) || !common.HasPermission(permSrc, common.PermFTPManage) {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
return fs.Rename(ctx, srcPath, dstBase)
|
||||
} else {
|
||||
if !user.CanFTPManage() || !user.CanMove() || (srcBase != dstBase && !user.CanRename()) {
|
||||
if !common.HasPermission(permSrc, common.PermFTPManage) || !common.HasPermission(permSrc, common.PermMove) || (srcBase != dstBase && !common.HasPermission(permSrc, common.PermRename)) {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
if err = fs.Move(ctx, srcPath, dstDir); err != nil {
|
||||
|
||||
@@ -30,7 +30,7 @@ func OpenDownload(ctx context.Context, reqPath string, offset int64) (*FileDownl
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, "meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
|
||||
if !common.CanAccessWithRoles(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
|
||||
return nil, errs.PermissionDenied
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func Stat(ctx context.Context, path string) (os.FileInfo, error) {
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, "meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
|
||||
if !common.CanAccessWithRoles(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
|
||||
return nil, errs.PermissionDenied
|
||||
}
|
||||
obj, err := fs.Get(ctx, reqPath, &fs.GetArgs{})
|
||||
@@ -148,7 +148,7 @@ func List(ctx context.Context, path string) ([]os.FileInfo, error) {
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, "meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
|
||||
if !common.CanAccessWithRoles(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
|
||||
return nil, errs.PermissionDenied
|
||||
}
|
||||
objs, err := fs.List(ctx, reqPath, &fs.ListArgs{})
|
||||
|
||||
@@ -35,8 +35,10 @@ func uploadAuth(ctx context.Context, path string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !(common.CanAccess(user, meta, path, ctx.Value("meta_pass").(string)) &&
|
||||
((user.CanFTPManage() && user.CanWrite()) || common.CanWrite(meta, stdpath.Dir(path)))) {
|
||||
perm := common.MergeRolePermissions(user, path)
|
||||
if !(common.CanAccessWithRoles(user, meta, path, ctx.Value("meta_pass").(string)) &&
|
||||
((common.HasPermission(perm, common.PermFTPManage) && common.HasPermission(perm, common.PermWrite)) ||
|
||||
common.CanWrite(meta, stdpath.Dir(path)))) {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -78,15 +78,20 @@ func FsArchiveMeta(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanReadArchives() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
reqPath, err := user.JoinPath(req.Path)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, reqPath) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, reqPath)
|
||||
if !common.HasPermission(perm, common.PermReadArchives) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
meta, err := op.GetNearestMeta(reqPath)
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
@@ -156,15 +161,20 @@ func FsArchiveList(c *gin.Context) {
|
||||
}
|
||||
req.Validate()
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanReadArchives() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
reqPath, err := user.JoinPath(req.Path)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, reqPath) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, reqPath)
|
||||
if !common.HasPermission(perm, common.PermReadArchives) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
meta, err := op.GetNearestMeta(reqPath)
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
@@ -242,10 +252,6 @@ func FsArchiveDecompress(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanDecompress() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
srcPaths := make([]string, 0, len(req.Name))
|
||||
for _, name := range req.Name {
|
||||
srcPath, err := user.JoinPath(stdpath.Join(req.SrcDir, name))
|
||||
@@ -253,6 +259,10 @@ func FsArchiveDecompress(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, srcPath) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
srcPaths = append(srcPaths, srcPath)
|
||||
}
|
||||
dstDir, err := user.JoinPath(req.DstDir)
|
||||
@@ -260,8 +270,17 @@ func FsArchiveDecompress(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, dstDir) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
tasks := make([]task.TaskExtensionInfo, 0, len(srcPaths))
|
||||
for _, srcPath := range srcPaths {
|
||||
perm := common.MergeRolePermissions(user, srcPath)
|
||||
if !common.HasPermission(perm, common.PermDecompress) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
t, e := fs.ArchiveDecompress(c, srcPath, dstDir, model.ArchiveDecompressArgs{
|
||||
ArchiveInnerArgs: model.ArchiveInnerArgs{
|
||||
ArchiveArgs: model.ArchiveArgs{
|
||||
|
||||
@@ -3,12 +3,22 @@ package handles
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/go-cache"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/device"
|
||||
"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/internal/session"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pquerna/otp/totp"
|
||||
@@ -77,25 +87,74 @@ func loginHash(c *gin.Context, req *LoginReq) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
clientID := c.GetHeader("Client-Id")
|
||||
if clientID == "" {
|
||||
clientID = c.Query("client_id")
|
||||
}
|
||||
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s",
|
||||
user.ID, clientID))
|
||||
|
||||
if err := device.EnsureActiveOnLogin(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
|
||||
if errors.Is(err, errs.TooManyDevices) {
|
||||
common.ErrorResp(c, err, 403)
|
||||
} else {
|
||||
common.ErrorResp(c, err, 400, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// generate token
|
||||
token, err := common.GenerateToken(user)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, gin.H{"token": token})
|
||||
common.SuccessResp(c, gin.H{"token": token, "device_key": key})
|
||||
loginCache.Del(ip)
|
||||
}
|
||||
|
||||
type RegisterReq struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// Register a new user
|
||||
func Register(c *gin.Context) {
|
||||
if !setting.GetBool(conf.AllowRegister) {
|
||||
common.ErrorStrResp(c, "registration is disabled", 403)
|
||||
return
|
||||
}
|
||||
var req RegisterReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
user := &model.User{
|
||||
Username: req.Username,
|
||||
Role: model.Roles{op.GetDefaultRoleID()},
|
||||
Authn: "[]",
|
||||
}
|
||||
user.SetPassword(req.Password)
|
||||
if err := op.CreateUser(user); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
|
||||
type UserResp struct {
|
||||
model.User
|
||||
Otp bool `json:"otp"`
|
||||
RoleNames []string `json:"role_names"`
|
||||
Permissions []model.PermissionEntry `json:"permissions"`
|
||||
}
|
||||
|
||||
// CurrentUser get current user by token
|
||||
// if token is empty, return guest user
|
||||
func CurrentUser(c *gin.Context) {
|
||||
user := c.MustGet("user").(*model.User)
|
||||
|
||||
userResp := UserResp{
|
||||
User: *user,
|
||||
}
|
||||
@@ -103,6 +162,30 @@ func CurrentUser(c *gin.Context) {
|
||||
if userResp.OtpSecret != "" {
|
||||
userResp.Otp = true
|
||||
}
|
||||
|
||||
var roleNames []string
|
||||
permMap := map[string]int32{}
|
||||
paths := make([]string, 0)
|
||||
|
||||
for _, role := range user.RolesDetail {
|
||||
roleNames = append(roleNames, role.Name)
|
||||
for _, entry := range role.PermissionScopes {
|
||||
cleanPath := path.Clean("/" + strings.TrimPrefix(entry.Path, "/"))
|
||||
if _, ok := permMap[cleanPath]; !ok {
|
||||
paths = append(paths, cleanPath)
|
||||
}
|
||||
permMap[cleanPath] |= entry.Permission
|
||||
}
|
||||
}
|
||||
userResp.RoleNames = roleNames
|
||||
|
||||
for _, fullPath := range paths {
|
||||
userResp.Permissions = append(userResp.Permissions, model.PermissionEntry{
|
||||
Path: fullPath,
|
||||
Permission: permMap[fullPath],
|
||||
})
|
||||
}
|
||||
|
||||
common.SuccessResp(c, userResp)
|
||||
}
|
||||
|
||||
@@ -187,6 +270,13 @@ func Verify2FA(c *gin.Context) {
|
||||
}
|
||||
|
||||
func LogOut(c *gin.Context) {
|
||||
if keyVal, ok := c.Get("device_key"); ok {
|
||||
if err := session.MarkInactive(keyVal.(string)); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
c.Set("session_inactive", true)
|
||||
}
|
||||
err := common.InvalidateToken(c.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
|
||||
@@ -29,20 +29,29 @@ func FsRecursiveMove(c *gin.Context) {
|
||||
}
|
||||
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanMove() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
srcDir, err := user.JoinPath(req.SrcDir)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, srcDir) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
dstDir, err := user.JoinPath(req.DstDir)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, dstDir) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, srcDir)
|
||||
if !common.HasPermission(perm, common.PermMove) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
|
||||
meta, err := op.GetNearestMeta(srcDir)
|
||||
if err != nil {
|
||||
@@ -149,16 +158,20 @@ func FsBatchRename(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanRename() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
|
||||
reqPath, err := user.JoinPath(req.SrcDir)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, reqPath) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, reqPath)
|
||||
if !common.HasPermission(perm, common.PermRename) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
|
||||
meta, err := op.GetNearestMeta(reqPath)
|
||||
if err != nil {
|
||||
@@ -194,14 +207,19 @@ func FsRegexRename(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanRename() {
|
||||
reqPath, err := user.JoinPath(req.SrcDir)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, reqPath) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
|
||||
reqPath, err := user.JoinPath(req.SrcDir)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
perm := common.MergeRolePermissions(user, reqPath)
|
||||
if !common.HasPermission(perm, common.PermRename) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,12 @@ func FsMkdir(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !user.CanWrite() {
|
||||
if !common.CheckPathLimitWithRoles(user, reqPath) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, reqPath)
|
||||
if !common.HasPermission(perm, common.PermWrite) {
|
||||
meta, err := op.GetNearestMeta(stdpath.Dir(reqPath))
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
@@ -73,20 +78,29 @@ func FsMove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanMove() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
srcDir, err := user.JoinPath(req.SrcDir)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, srcDir) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
dstDir, err := user.JoinPath(req.DstDir)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, dstDir) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
permMove := common.MergeRolePermissions(user, srcDir)
|
||||
if !common.HasPermission(permMove, common.PermMove) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
if !req.Overwrite {
|
||||
for _, name := range req.Names {
|
||||
if res, _ := fs.Get(c, stdpath.Join(dstDir, name), &fs.GetArgs{NoLog: true}); res != nil {
|
||||
@@ -116,20 +130,29 @@ func FsCopy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanCopy() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
srcDir, err := user.JoinPath(req.SrcDir)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, srcDir) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
dstDir, err := user.JoinPath(req.DstDir)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, dstDir) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, srcDir)
|
||||
if !common.HasPermission(perm, common.PermCopy) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
if !req.Overwrite {
|
||||
for _, name := range req.Names {
|
||||
if res, _ := fs.Get(c, stdpath.Join(dstDir, name), &fs.GetArgs{NoLog: true}); res != nil {
|
||||
@@ -167,15 +190,20 @@ func FsRename(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanRename() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
reqPath, err := user.JoinPath(req.Path)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, reqPath) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, reqPath)
|
||||
if !common.HasPermission(perm, common.PermRename) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
if !req.Overwrite {
|
||||
dstPath := stdpath.Join(stdpath.Dir(reqPath), req.Name)
|
||||
if dstPath != reqPath {
|
||||
@@ -208,15 +236,20 @@ func FsRemove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanRemove() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
reqDir, err := user.JoinPath(req.Dir)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, reqDir) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, reqDir)
|
||||
if !common.HasPermission(perm, common.PermRemove) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
for _, name := range req.Names {
|
||||
err := fs.Remove(c, stdpath.Join(reqDir, name))
|
||||
if err != nil {
|
||||
@@ -240,15 +273,20 @@ func FsRemoveEmptyDirectory(c *gin.Context) {
|
||||
}
|
||||
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanRemove() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
srcDir, err := user.JoinPath(req.SrcDir)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, srcDir) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, srcDir)
|
||||
if !common.HasPermission(perm, common.PermRemove) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
|
||||
meta, err := op.GetNearestMeta(srcDir)
|
||||
if err != nil {
|
||||
|
||||
@@ -48,7 +48,7 @@ type ObjResp struct {
|
||||
}
|
||||
|
||||
type FsListResp struct {
|
||||
Content []ObjResp `json:"content"`
|
||||
Content []ObjLabelResp `json:"content"`
|
||||
Total int64 `json:"total"`
|
||||
Readme string `json:"readme"`
|
||||
Header string `json:"header"`
|
||||
@@ -56,6 +56,22 @@ type FsListResp struct {
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
type ObjLabelResp struct {
|
||||
Id string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Modified time.Time `json:"modified"`
|
||||
Created time.Time `json:"created"`
|
||||
Sign string `json:"sign"`
|
||||
Thumb string `json:"thumb"`
|
||||
Type int `json:"type"`
|
||||
HashInfoStr string `json:"hashinfo"`
|
||||
HashInfo map[*utils.HashType]string `json:"hash_info"`
|
||||
LabelList []model.Label `json:"label_list"`
|
||||
}
|
||||
|
||||
func FsList(c *gin.Context) {
|
||||
var req ListReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
@@ -77,11 +93,12 @@ func FsList(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
c.Set("meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, req.Password) {
|
||||
if !common.CanAccessWithRoles(user, meta, reqPath, req.Password) {
|
||||
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
|
||||
return
|
||||
}
|
||||
if !user.CanWrite() && !common.CanWrite(meta, reqPath) && req.Refresh {
|
||||
perm := common.MergeRolePermissions(user, reqPath)
|
||||
if !common.HasPermission(perm, common.PermWrite) && !common.CanWrite(meta, reqPath) && req.Refresh {
|
||||
common.ErrorStrResp(c, "Refresh without permission", 403)
|
||||
return
|
||||
}
|
||||
@@ -90,7 +107,14 @@ func FsList(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
total, objs := pagination(objs, &req.PageReq)
|
||||
filtered := make([]model.Obj, 0, len(objs))
|
||||
for _, obj := range objs {
|
||||
childPath := stdpath.Join(reqPath, obj.GetName())
|
||||
if common.CanReadPathByRole(user, childPath) {
|
||||
filtered = append(filtered, obj)
|
||||
}
|
||||
}
|
||||
total, objs := pagination(filtered, &req.PageReq)
|
||||
provider := "unknown"
|
||||
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
|
||||
if err == nil {
|
||||
@@ -101,7 +125,7 @@ func FsList(c *gin.Context) {
|
||||
Total: int64(total),
|
||||
Readme: getReadme(meta, reqPath),
|
||||
Header: getHeader(meta, reqPath),
|
||||
Write: user.CanWrite() || common.CanWrite(meta, reqPath),
|
||||
Write: common.HasPermission(perm, common.PermWrite) || common.CanWrite(meta, reqPath),
|
||||
Provider: provider,
|
||||
})
|
||||
}
|
||||
@@ -135,7 +159,7 @@ func FsDirs(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
c.Set("meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, req.Password) {
|
||||
if !common.CanAccessWithRoles(user, meta, reqPath, req.Password) {
|
||||
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
|
||||
return
|
||||
}
|
||||
@@ -144,7 +168,14 @@ func FsDirs(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
dirs := filterDirs(objs)
|
||||
visible := make([]model.Obj, 0, len(objs))
|
||||
for _, obj := range objs {
|
||||
childPath := stdpath.Join(reqPath, obj.GetName())
|
||||
if common.CanReadPathByRole(user, childPath) {
|
||||
visible = append(visible, obj)
|
||||
}
|
||||
}
|
||||
dirs := filterDirs(visible)
|
||||
common.SuccessResp(c, dirs)
|
||||
}
|
||||
|
||||
@@ -207,11 +238,25 @@ func pagination(objs []model.Obj, req *model.PageReq) (int, []model.Obj) {
|
||||
return total, objs[start:end]
|
||||
}
|
||||
|
||||
func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjResp {
|
||||
var resp []ObjResp
|
||||
func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjLabelResp {
|
||||
var resp []ObjLabelResp
|
||||
|
||||
names := make([]string, 0, len(objs))
|
||||
for _, obj := range objs {
|
||||
if !obj.IsDir() {
|
||||
names = append(names, obj.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
labelsByName, _ := op.GetLabelsByFileNamesPublic(names)
|
||||
|
||||
for _, obj := range objs {
|
||||
var labels []model.Label
|
||||
if !obj.IsDir() {
|
||||
labels = labelsByName[obj.GetName()]
|
||||
}
|
||||
thumb, _ := model.GetThumb(obj)
|
||||
resp = append(resp, ObjResp{
|
||||
resp = append(resp, ObjLabelResp{
|
||||
Id: obj.GetID(),
|
||||
Path: obj.GetPath(),
|
||||
Name: obj.GetName(),
|
||||
@@ -224,6 +269,7 @@ func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjResp {
|
||||
Sign: common.Sign(obj, parent, encrypt),
|
||||
Thumb: thumb,
|
||||
Type: utils.GetObjType(obj.GetName(), obj.IsDir()),
|
||||
LabelList: labels,
|
||||
})
|
||||
}
|
||||
return resp
|
||||
@@ -240,7 +286,7 @@ type FsGetResp struct {
|
||||
Readme string `json:"readme"`
|
||||
Header string `json:"header"`
|
||||
Provider string `json:"provider"`
|
||||
Related []ObjResp `json:"related"`
|
||||
Related []ObjLabelResp `json:"related"`
|
||||
}
|
||||
|
||||
func FsGet(c *gin.Context) {
|
||||
@@ -263,7 +309,7 @@ func FsGet(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
c.Set("meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, req.Password) {
|
||||
if !common.CanAccessWithRoles(user, meta, reqPath, req.Password) {
|
||||
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
|
||||
return
|
||||
}
|
||||
@@ -391,7 +437,7 @@ func FsOther(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
c.Set("meta", meta)
|
||||
if !common.CanAccess(user, meta, req.Path, req.Password) {
|
||||
if !common.CanAccessWithRoles(user, meta, req.Path, req.Password) {
|
||||
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
|
||||
return
|
||||
}
|
||||
|
||||
99
server/handles/label.go
Normal file
99
server/handles/label.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package handles
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"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/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func ListLabel(c *gin.Context) {
|
||||
var req model.PageReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
req.Validate()
|
||||
log.Debugf("%+v", req)
|
||||
labels, total, err := db.GetLabels(req.Page, req.PerPage)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, common.PageResp{
|
||||
Content: labels,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
func GetLabel(c *gin.Context) {
|
||||
idStr := c.Query("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
label, err := db.GetLabelById(uint(id))
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, label)
|
||||
}
|
||||
|
||||
func CreateLabel(c *gin.Context) {
|
||||
var req model.Label
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if db.GetLabelByName(req.Name) {
|
||||
common.ErrorResp(c, errors.New("label name is exists"), 401)
|
||||
return
|
||||
}
|
||||
if id, err := db.CreateLabel(req); err != nil {
|
||||
common.ErrorWithDataResp(c, err, 500, gin.H{
|
||||
"id": id,
|
||||
}, true)
|
||||
} else {
|
||||
common.SuccessResp(c, gin.H{
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateLabel(c *gin.Context) {
|
||||
var req model.Label
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if label, err := db.UpdateLabel(&req); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
} else {
|
||||
common.SuccessResp(c, label)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteLabel(c *gin.Context) {
|
||||
idStr := c.Query("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
userObj, ok := c.Value("user").(*model.User)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
if err = op.DeleteLabelById(c, uint(id), userObj.ID); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
250
server/handles/label_file_binding.go
Normal file
250
server/handles/label_file_binding.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package handles
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"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/gin-gonic/gin"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DelLabelFileBinDingReq struct {
|
||||
FileName string `json:"file_name"`
|
||||
LabelId string `json:"label_id"`
|
||||
}
|
||||
|
||||
type pageResp[T any] struct {
|
||||
Content []T `json:"content"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type restoreLabelBindingsReq struct {
|
||||
KeepIDs bool `json:"keep_ids"`
|
||||
Override bool `json:"override"`
|
||||
Bindings []model.LabelFileBinding `json:"bindings"`
|
||||
}
|
||||
|
||||
func GetLabelByFileName(c *gin.Context) {
|
||||
fileName := c.Query("file_name")
|
||||
if fileName == "" {
|
||||
common.ErrorResp(c, errors.New("file_name must not empty"), 400)
|
||||
return
|
||||
}
|
||||
decodedFileName, err := url.QueryUnescape(fileName)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, errors.New("invalid file_name"), 400)
|
||||
return
|
||||
}
|
||||
fmt.Println(">>> 原始 fileName:", fileName)
|
||||
fmt.Println(">>> 解码后 fileName:", decodedFileName)
|
||||
userObj, ok := c.Value("user").(*model.User)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
labels, err := op.GetLabelByFileName(userObj.ID, decodedFileName)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, labels)
|
||||
}
|
||||
|
||||
func CreateLabelFileBinDing(c *gin.Context) {
|
||||
var req op.CreateLabelFileBinDingReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if req.IsDir == true {
|
||||
common.ErrorStrResp(c, "Unable to bind folder", 400)
|
||||
return
|
||||
}
|
||||
userObj, ok := c.Value("user").(*model.User)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
if err := op.CreateLabelFileBinDing(req, userObj.ID); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
} else {
|
||||
common.SuccessResp(c, gin.H{
|
||||
"msg": "添加成功!",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func DelLabelByFileName(c *gin.Context) {
|
||||
var req DelLabelFileBinDingReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
userObj, ok := c.Value("user").(*model.User)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
labelId, err := strconv.ParseUint(req.LabelId, 10, 64)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, fmt.Errorf("invalid label ID '%s': %v", req.LabelId, err), 500, true)
|
||||
return
|
||||
}
|
||||
if err = db.DelLabelFileBinDingById(uint(labelId), userObj.ID, req.FileName); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
|
||||
func GetFileByLabel(c *gin.Context) {
|
||||
labelId := c.Query("label_id")
|
||||
if labelId == "" {
|
||||
common.ErrorResp(c, errors.New("file_name must not empty"), 400)
|
||||
return
|
||||
}
|
||||
userObj, ok := c.Value("user").(*model.User)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
fileList, err := op.GetFileByLabel(userObj.ID, labelId)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, fileList)
|
||||
}
|
||||
|
||||
func ListLabelFileBinding(c *gin.Context) {
|
||||
userObj, ok := c.Value("user").(*model.User)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
sizeStr := c.DefaultQuery("page_size", "50")
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
pageSize, err := strconv.Atoi(sizeStr)
|
||||
if err != nil || pageSize <= 0 || pageSize > 200 {
|
||||
pageSize = 50
|
||||
}
|
||||
|
||||
fileName := c.Query("file_name")
|
||||
labelIDStr := c.Query("label_id")
|
||||
var labelIDs []uint
|
||||
if labelIDStr != "" {
|
||||
parts := strings.Split(labelIDStr, ",")
|
||||
for _, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
id64, err := strconv.ParseUint(strings.TrimSpace(p), 10, 64)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, fmt.Errorf("invalid label_id '%s': %v", p, err), 400)
|
||||
return
|
||||
}
|
||||
labelIDs = append(labelIDs, uint(id64))
|
||||
}
|
||||
}
|
||||
|
||||
list, total, err := db.ListLabelFileBinDing(userObj.ID, labelIDs, fileName, page, pageSize)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, pageResp[model.LabelFileBinding]{
|
||||
Content: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
func RestoreLabelFileBinding(c *gin.Context) {
|
||||
var req restoreLabelBindingsReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if len(req.Bindings) == 0 {
|
||||
common.ErrorStrResp(c, "empty bindings", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if u, ok := c.Value("user").(*model.User); ok {
|
||||
for i := range req.Bindings {
|
||||
if req.Bindings[i].UserId == 0 {
|
||||
req.Bindings[i].UserId = u.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range req.Bindings {
|
||||
b := req.Bindings[i]
|
||||
if b.UserId == 0 || b.LabelId == 0 || strings.TrimSpace(b.FileName) == "" {
|
||||
common.ErrorStrResp(c, "invalid binding: user_id/label_id/file_name required", 400)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := op.RestoreLabelFileBindings(req.Bindings, req.KeepIDs, req.Override); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, gin.H{
|
||||
"msg": fmt.Sprintf("restored %d rows", len(req.Bindings)),
|
||||
})
|
||||
}
|
||||
|
||||
func CreateLabelFileBinDingBatch(c *gin.Context) {
|
||||
var req struct {
|
||||
Items []op.CreateLabelFileBinDingReq `json:"items" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || len(req.Items) == 0 {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
|
||||
userObj, ok := c.Value("user").(*model.User)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
|
||||
type perResult struct {
|
||||
Name string `json:"name"`
|
||||
Ok bool `json:"ok"`
|
||||
ErrMsg string `json:"errMsg,omitempty"`
|
||||
}
|
||||
results := make([]perResult, 0, len(req.Items))
|
||||
succeed := 0
|
||||
|
||||
for _, item := range req.Items {
|
||||
if item.IsDir {
|
||||
results = append(results, perResult{Name: item.Name, Ok: false, ErrMsg: "Unable to bind folder"})
|
||||
continue
|
||||
}
|
||||
if err := op.CreateLabelFileBinDing(item, userObj.ID); err != nil {
|
||||
results = append(results, perResult{Name: item.Name, Ok: false, ErrMsg: err.Error()})
|
||||
continue
|
||||
}
|
||||
succeed++
|
||||
results = append(results, perResult{Name: item.Name, Ok: true})
|
||||
}
|
||||
|
||||
common.SuccessResp(c, gin.H{
|
||||
"total": len(req.Items),
|
||||
"succeed": succeed,
|
||||
"failed": len(req.Items) - succeed,
|
||||
"results": results,
|
||||
})
|
||||
}
|
||||
@@ -131,7 +131,7 @@ func ladpRegister(username string) (*model.User, error) {
|
||||
Password: random.String(16),
|
||||
Permission: int32(setting.GetInt(conf.LdapDefaultPermission, 0)),
|
||||
BasePath: setting.GetStr(conf.LdapDefaultDir),
|
||||
Role: 0,
|
||||
Role: nil,
|
||||
Disabled: false,
|
||||
}
|
||||
if err := db.CreateUser(user); err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/drivers/pikpak"
|
||||
"github.com/alist-org/alist/v3/drivers/thunder"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/offline_download/tool"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
@@ -253,10 +254,6 @@ type AddOfflineDownloadReq struct {
|
||||
|
||||
func AddOfflineDownload(c *gin.Context) {
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanAddOfflineDownloadTasks() {
|
||||
common.ErrorStrResp(c, "permission denied", 403)
|
||||
return
|
||||
}
|
||||
|
||||
var req AddOfflineDownloadReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
@@ -268,6 +265,15 @@ func AddOfflineDownload(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
if !common.CheckPathLimitWithRoles(user, reqPath) {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, reqPath)
|
||||
if !common.HasPermission(perm, common.PermAddOfflineDownload) {
|
||||
common.ErrorStrResp(c, "permission denied", 403)
|
||||
return
|
||||
}
|
||||
var tasks []task.TaskExtensionInfo
|
||||
for _, url := range req.Urls {
|
||||
t, err := tool.AddURL(c, &tool.AddURLArgs{
|
||||
|
||||
117
server/handles/role.go
Normal file
117
server/handles/role.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package handles
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"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/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func ListRoles(c *gin.Context) {
|
||||
var req model.PageReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
req.Validate()
|
||||
log.Debugf("%+v", req)
|
||||
roles, total, err := op.GetRoles(req.Page, req.PerPage)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, common.PageResp{Content: roles, Total: total})
|
||||
}
|
||||
|
||||
func GetRole(c *gin.Context) {
|
||||
idStr := c.Query("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
role, err := op.GetRole(uint(id))
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, role)
|
||||
}
|
||||
|
||||
func CreateRole(c *gin.Context) {
|
||||
var req model.Role
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if err := op.CreateRole(&req); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateRole(c *gin.Context) {
|
||||
var req struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
PermissionScopes []model.PermissionEntry `json:"permission_scopes"`
|
||||
Default *bool `json:"default"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
role, err := op.GetRole(req.ID)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
switch role.Name {
|
||||
case "admin":
|
||||
common.ErrorResp(c, errs.ErrChangeDefaultRole, 403)
|
||||
return
|
||||
|
||||
case "guest":
|
||||
req.Name = "guest"
|
||||
}
|
||||
role.Name = req.Name
|
||||
role.Description = req.Description
|
||||
role.PermissionScopes = req.PermissionScopes
|
||||
if req.Default != nil {
|
||||
role.Default = *req.Default
|
||||
}
|
||||
if err := op.UpdateRole(role); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteRole(c *gin.Context) {
|
||||
idStr := c.Query("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
role, err := op.GetRole(uint(id))
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
if role.Name == "admin" || role.Name == "guest" {
|
||||
common.ErrorResp(c, errs.ErrChangeDefaultRole, 403)
|
||||
return
|
||||
}
|
||||
if err := op.DeleteRole(uint(id)); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
@@ -43,12 +43,18 @@ func Search(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
nodes, total, err := search.Search(c, req.SearchReq)
|
||||
var (
|
||||
filteredNodes []model.SearchNode
|
||||
)
|
||||
for len(filteredNodes) < req.PerPage {
|
||||
nodes, _, err := search.Search(c, req.SearchReq)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
var filteredNodes []model.SearchNode
|
||||
if len(nodes) == 0 {
|
||||
break
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if !strings.HasPrefix(node.Parent, user.BasePath) {
|
||||
continue
|
||||
@@ -57,14 +63,19 @@ func Search(c *gin.Context) {
|
||||
if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
continue
|
||||
}
|
||||
if !common.CanAccess(user, meta, path.Join(node.Parent, node.Name), req.Password) {
|
||||
if !common.CanAccessWithRoles(user, meta, path.Join(node.Parent, node.Name), req.Password) {
|
||||
continue
|
||||
}
|
||||
filteredNodes = append(filteredNodes, node)
|
||||
if len(filteredNodes) >= req.PerPage {
|
||||
break
|
||||
}
|
||||
}
|
||||
req.Page++
|
||||
}
|
||||
common.SuccessResp(c, common.PageResp{
|
||||
Content: utils.MustSliceConvert(filteredNodes, nodeToSearchResp),
|
||||
Total: total,
|
||||
Total: int64(len(filteredNodes)),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
92
server/handles/session.go
Normal file
92
server/handles/session.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package handles
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SessionResp struct {
|
||||
SessionID string `json:"session_id"`
|
||||
UserID uint `json:"user_id,omitempty"`
|
||||
LastActive int64 `json:"last_active"`
|
||||
Status int `json:"status"`
|
||||
UA string `json:"ua"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
func ListMySessions(c *gin.Context) {
|
||||
user := c.MustGet("user").(*model.User)
|
||||
sessions, err := db.ListSessionsByUser(user.ID)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
resp := make([]SessionResp, len(sessions))
|
||||
for i, s := range sessions {
|
||||
resp[i] = SessionResp{
|
||||
SessionID: s.DeviceKey,
|
||||
LastActive: s.LastActive,
|
||||
Status: s.Status,
|
||||
UA: s.UserAgent,
|
||||
IP: s.IP,
|
||||
}
|
||||
}
|
||||
common.SuccessResp(c, resp)
|
||||
}
|
||||
|
||||
type EvictSessionReq struct {
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
func EvictMySession(c *gin.Context) {
|
||||
var req EvictSessionReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if _, err := db.GetSession(user.ID, req.SessionID); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if err := db.MarkInactive(req.SessionID); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
|
||||
func ListSessions(c *gin.Context) {
|
||||
sessions, err := db.ListSessions()
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
resp := make([]SessionResp, len(sessions))
|
||||
for i, s := range sessions {
|
||||
resp[i] = SessionResp{
|
||||
SessionID: s.DeviceKey,
|
||||
UserID: s.UserID,
|
||||
LastActive: s.LastActive,
|
||||
Status: s.Status,
|
||||
UA: s.UserAgent,
|
||||
IP: s.IP,
|
||||
}
|
||||
}
|
||||
common.SuccessResp(c, resp)
|
||||
}
|
||||
|
||||
func EvictSession(c *gin.Context) {
|
||||
var req EvictSessionReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if err := db.MarkInactive(req.SessionID); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
@@ -14,6 +14,21 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func getRoleOptions() string {
|
||||
roles, _, err := op.GetRoles(1, model.MaxInt)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
names := make([]string, 0, len(roles))
|
||||
for _, r := range roles {
|
||||
if r.Name == "admin" || r.Name == "guest" {
|
||||
continue
|
||||
}
|
||||
names = append(names, r.Name)
|
||||
}
|
||||
return strings.Join(names, ",")
|
||||
}
|
||||
|
||||
func ResetToken(c *gin.Context) {
|
||||
token := random.Token()
|
||||
item := model.SettingItem{Key: "token", Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE}
|
||||
@@ -34,6 +49,17 @@ func GetSetting(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if item.Key == conf.DefaultRole {
|
||||
copy := *item
|
||||
copy.Options = getRoleOptions()
|
||||
if id, err := strconv.Atoi(copy.Value); err == nil {
|
||||
if r, err := op.GetRole(uint(id)); err == nil {
|
||||
copy.Value = r.Name
|
||||
}
|
||||
}
|
||||
common.SuccessResp(c, copy)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, item)
|
||||
} else {
|
||||
items, err := op.GetSettingItemInKeys(strings.Split(keys, ","))
|
||||
@@ -41,6 +67,17 @@ func GetSetting(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
for i := range items {
|
||||
if items[i].Key == conf.DefaultRole {
|
||||
if id, err := strconv.Atoi(items[i].Value); err == nil {
|
||||
if r, err := op.GetRole(uint(id)); err == nil {
|
||||
items[i].Value = r.Name
|
||||
}
|
||||
}
|
||||
items[i].Options = getRoleOptions()
|
||||
break
|
||||
}
|
||||
}
|
||||
common.SuccessResp(c, items)
|
||||
}
|
||||
}
|
||||
@@ -51,6 +88,22 @@ func SaveSettings(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range req {
|
||||
if req[i].Key == conf.DefaultRole {
|
||||
role, err := op.GetRoleByName(req[i].Value)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if role.Name == "admin" || role.Name == "guest" {
|
||||
common.ErrorStrResp(c, "cannot set admin or guest as default role", 400)
|
||||
return
|
||||
}
|
||||
req[i].Value = strconv.Itoa(int(role.ID))
|
||||
}
|
||||
}
|
||||
|
||||
if err := op.SaveSettingItems(req); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
@@ -88,6 +141,17 @@ func ListSettings(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
for i := range settings {
|
||||
if settings[i].Key == conf.DefaultRole {
|
||||
if id, err := strconv.Atoi(settings[i].Value); err == nil {
|
||||
if r, err := op.GetRole(uint(id)); err == nil {
|
||||
settings[i].Value = r.Name
|
||||
}
|
||||
}
|
||||
settings[i].Options = getRoleOptions()
|
||||
break
|
||||
}
|
||||
}
|
||||
common.SuccessResp(c, settings)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -154,7 +155,7 @@ func autoRegister(username, userID string, err error) (*model.User, error) {
|
||||
Password: random.String(16),
|
||||
Permission: int32(setting.GetInt(conf.SSODefaultPermission, 0)),
|
||||
BasePath: setting.GetStr(conf.SSODefaultDir),
|
||||
Role: 0,
|
||||
Role: model.Roles{op.GetDefaultRoleID()},
|
||||
Disabled: false,
|
||||
SsoID: userID,
|
||||
}
|
||||
@@ -256,6 +257,7 @@ func OIDCLoginCallback(c *gin.Context) {
|
||||
user, err = autoRegister(userID, userID, err)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
}
|
||||
token, err := common.GenerateToken(user)
|
||||
|
||||
@@ -18,7 +18,7 @@ type TaskInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Creator string `json:"creator"`
|
||||
CreatorRole int `json:"creator_role"`
|
||||
CreatorRole model.Roles `json:"creator_role"`
|
||||
State tache.State `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Progress float64 `json:"progress"`
|
||||
@@ -39,7 +39,7 @@ func getTaskInfo[T task.TaskExtensionInfo](task T) TaskInfo {
|
||||
progress = 100
|
||||
}
|
||||
creatorName := ""
|
||||
creatorRole := -1
|
||||
var creatorRole model.Roles
|
||||
if task.GetCreator() != nil {
|
||||
creatorName = task.GetCreator().Username
|
||||
creatorRole = task.GetCreator().Role
|
||||
|
||||
@@ -3,6 +3,8 @@ package handles
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
@@ -35,6 +37,9 @@ func CreateUser(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if len(req.Role) == 0 {
|
||||
req.Role = model.Roles{op.GetDefaultRoleID()}
|
||||
}
|
||||
if req.IsAdmin() || req.IsGuest() {
|
||||
common.ErrorStrResp(c, "admin or guest user can not be created", 400, true)
|
||||
return
|
||||
@@ -60,10 +65,18 @@ func UpdateUser(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
if user.Role != req.Role {
|
||||
common.ErrorStrResp(c, "role can not be changed", 400)
|
||||
|
||||
if user.Username == "admin" {
|
||||
if !utils.SliceEqual(user.Role, req.Role) {
|
||||
common.ErrorStrResp(c, "cannot change role of admin user", 403)
|
||||
return
|
||||
}
|
||||
//if user.Username != req.Username {
|
||||
// common.ErrorStrResp(c, "cannot change username of admin user", 403)
|
||||
// return
|
||||
//}
|
||||
}
|
||||
|
||||
if req.Password == "" {
|
||||
req.PwdHash = user.PwdHash
|
||||
req.Salt = user.Salt
|
||||
@@ -74,10 +87,25 @@ func UpdateUser(c *gin.Context) {
|
||||
if req.OtpSecret == "" {
|
||||
req.OtpSecret = user.OtpSecret
|
||||
}
|
||||
if req.Disabled && req.IsAdmin() {
|
||||
common.ErrorStrResp(c, "admin user can not be disabled", 400)
|
||||
if req.Disabled && user.IsAdmin() {
|
||||
count, err := op.CountEnabledAdminsExcluding(user.ID)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
if count == 0 {
|
||||
common.ErrorStrResp(c, "at least one enabled admin must be kept", 400)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !utils.SliceEqual(user.Role, req.Role) {
|
||||
if req.IsAdmin() || req.IsGuest() {
|
||||
common.ErrorStrResp(c, "cannot assign admin or guest role to user", 400, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := op.UpdateUser(&req); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
|
||||
@@ -2,11 +2,16 @@ package middlewares
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/device"
|
||||
"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/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -23,7 +28,9 @@ func Auth(c *gin.Context) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("user", admin)
|
||||
if !HandleSession(c, admin) {
|
||||
return
|
||||
}
|
||||
log.Debugf("use admin token: %+v", admin)
|
||||
c.Next()
|
||||
return
|
||||
@@ -40,7 +47,18 @@ func Auth(c *gin.Context) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("user", guest)
|
||||
if len(guest.Role) > 0 {
|
||||
roles, err := op.GetRolesByUserID(guest.ID)
|
||||
if err != nil {
|
||||
common.ErrorStrResp(c, fmt.Sprintf("Fail to load guest roles: %v", err), 500)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
guest.RolesDetail = roles
|
||||
}
|
||||
if !HandleSession(c, guest) {
|
||||
return
|
||||
}
|
||||
log.Debugf("use empty token: %+v", guest)
|
||||
c.Next()
|
||||
return
|
||||
@@ -68,11 +86,45 @@ func Auth(c *gin.Context) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("user", user)
|
||||
if len(user.Role) > 0 {
|
||||
roles, err := op.GetRolesByUserID(user.ID)
|
||||
if err != nil {
|
||||
common.ErrorStrResp(c, fmt.Sprintf("Fail to load roles: %v", err), 500)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
user.RolesDetail = roles
|
||||
}
|
||||
if !HandleSession(c, user) {
|
||||
return
|
||||
}
|
||||
log.Debugf("use login token: %+v", user)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// HandleSession verifies device sessions and stores context values.
|
||||
func HandleSession(c *gin.Context, user *model.User) bool {
|
||||
clientID := c.GetHeader("Client-Id")
|
||||
if clientID == "" {
|
||||
clientID = c.Query("client_id")
|
||||
}
|
||||
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s", user.ID, clientID))
|
||||
if err := device.Handle(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
|
||||
token := c.GetHeader("Authorization")
|
||||
if errors.Is(err, errs.SessionInactive) {
|
||||
_ = common.InvalidateToken(token)
|
||||
common.ErrorResp(c, err, 401)
|
||||
} else {
|
||||
common.ErrorResp(c, err, 403)
|
||||
}
|
||||
c.Abort()
|
||||
return false
|
||||
}
|
||||
c.Set("device_key", key)
|
||||
c.Set("user", user)
|
||||
return true
|
||||
}
|
||||
|
||||
func Authn(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 {
|
||||
@@ -122,6 +174,19 @@ func Authn(c *gin.Context) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if len(user.Role) > 0 {
|
||||
var roles []model.Role
|
||||
for _, roleID := range user.Role {
|
||||
role, err := op.GetRole(uint(roleID))
|
||||
if err != nil {
|
||||
common.ErrorStrResp(c, fmt.Sprintf("load role %d failed", roleID), 500)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
roles = append(roles, *role)
|
||||
}
|
||||
user.RolesDetail = roles
|
||||
}
|
||||
c.Set("user", user)
|
||||
log.Debugf("use login token: %+v", user)
|
||||
c.Next()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user