mirror of
https://github.com/AlistGo/alist.git
synced 2025-11-25 19:37:41 +08:00
Compare commits
30 Commits
fix/docker
...
v3.52.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
5b8c26510b |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -25,6 +25,8 @@ jobs:
|
|||||||
- android-arm64
|
- android-arm64
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
|
env:
|
||||||
|
GOPROXY: https://proxy.golang.org,direct
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:edge
|
FROM alpine:3.20.7
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG INSTALL_FFMPEG=false
|
ARG INSTALL_FFMPEG=false
|
||||||
@@ -31,4 +31,4 @@ RUN /entrypoint.sh version
|
|||||||
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
|
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
|
||||||
VOLUME /opt/alist/data/
|
VOLUME /opt/alist/data/
|
||||||
EXPOSE 5244 5245
|
EXPOSE 5244 5245
|
||||||
CMD [ "/entrypoint.sh" ]
|
CMD [ "/entrypoint.sh" ]
|
||||||
|
|||||||
@@ -121,8 +121,6 @@ https://alistgo.com/guide/sponsor.html
|
|||||||
### Special sponsors
|
### 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.
|
- [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
|
## Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -118,8 +118,6 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
|
|||||||
### 特别赞助
|
### 特别赞助
|
||||||
|
|
||||||
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器,iPhone,iPad,Mac,Apple TV全平台支持。
|
- [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/) - 阿里云盘资源搜索引擎
|
|
||||||
|
|
||||||
## 贡献者
|
## 贡献者
|
||||||
|
|
||||||
|
|||||||
@@ -120,8 +120,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.
|
- [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/) - 阿里云盘资源搜索引擎
|
|
||||||
|
|
||||||
## コントリビューター
|
## コントリビューター
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package alist_v3
|
package alist_v3
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
@@ -72,15 +73,15 @@ type LoginResp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MeResp struct {
|
type MeResp struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
BasePath string `json:"base_path"`
|
BasePath string `json:"base_path"`
|
||||||
Role []int `json:"role"`
|
Role IntSlice `json:"role"`
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
Permission int `json:"permission"`
|
Permission int `json:"permission"`
|
||||||
SsoId string `json:"sso_id"`
|
SsoId string `json:"sso_id"`
|
||||||
Otp bool `json:"otp"`
|
Otp bool `json:"otp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArchiveMetaReq struct {
|
type ArchiveMetaReq struct {
|
||||||
@@ -168,3 +169,17 @@ type DecompressReq struct {
|
|||||||
PutIntoNewDir bool `json:"put_into_new_dir"`
|
PutIntoNewDir bool `json:"put_into_new_dir"`
|
||||||
SrcDir string `json:"src_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"`
|
RefreshToken string `json:"refresh_token" required:"true"`
|
||||||
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
|
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
|
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"`
|
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"`
|
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"`
|
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/115_share"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/123"
|
_ "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_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/123_share"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/139"
|
_ "github.com/alist-org/alist/v3/drivers/139"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/189"
|
_ "github.com/alist-org/alist/v3/drivers/189"
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ type Addition struct {
|
|||||||
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
|
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"`
|
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"`
|
DownloadAPI string `json:"download_api" type:"select" options:"official,crack,crack_video" default:"official"`
|
||||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
ClientID string `json:"client_id" required:"true" default:"hq9yQ9w9kR4YHj1kyYafLygVocobh7Sf"`
|
||||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
ClientSecret string `json:"client_secret" required:"true" default:"YH2VpZcFJHYNnV6vLfHQXDBhcE7ZChyE"`
|
||||||
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
||||||
AccessToken string
|
AccessToken string
|
||||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ func RemoveJSComment(data string) string {
|
|||||||
}
|
}
|
||||||
if inComment && v == '*' && i+1 < len(data) && data[i+1] == '/' {
|
if inComment && v == '*' && i+1 < len(data) && data[i+1] == '/' {
|
||||||
inComment = false
|
inComment = false
|
||||||
|
i++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if v == '/' && i+1 < len(data) {
|
if v == '/' && i+1 < len(data) {
|
||||||
@@ -108,6 +109,9 @@ func RemoveJSComment(data string) string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if inComment || inSingleLineComment {
|
||||||
|
continue
|
||||||
|
}
|
||||||
result.WriteByte(v)
|
result.WriteByte(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
go.mod
13
go.mod
@@ -3,10 +3,12 @@ module github.com/alist-org/alist/v3
|
|||||||
go 1.23.4
|
go 1.23.4
|
||||||
|
|
||||||
require (
|
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/ftpserverlib-pasvportmap v1.25.0
|
||||||
github.com/KirCute/sftpd-alist v0.0.12
|
github.com/KirCute/sftpd-alist v0.0.12
|
||||||
github.com/ProtonMail/go-crypto v1.0.0
|
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/go-cache v0.0.0-20240804043513-b1a71927bc21
|
||||||
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
|
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
|
||||||
github.com/alist-org/gofakes3 v0.0.7
|
github.com/alist-org/gofakes3 v0.0.7
|
||||||
@@ -79,11 +81,7 @@ require (
|
|||||||
gorm.io/gorm v1.25.11
|
gorm.io/gorm v1.25.11
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||||
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 (
|
require (
|
||||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||||
@@ -109,7 +107,6 @@ require (
|
|||||||
github.com/ipfs/boxo v0.12.0 // indirect
|
github.com/ipfs/boxo v0.12.0 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // 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/matoous/go-nanoid/v2 v2.1.0 // indirect
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27
|
github.com/microcosm-cc/bluemonday v1.0.27
|
||||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78
|
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
|
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=
|
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 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/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 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/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 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
|
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 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
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=
|
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/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 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
|
||||||
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
|
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 h1:9IAu/BYbSLQi8puFjUQApZTxIHqSwrj5d8vpP8vTq4A=
|
||||||
github.com/Unknwon/goconfig v1.0.0/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw=
|
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=
|
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/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 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg=
|
||||||
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE=
|
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.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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:DLQQEgHUAGZB6RVlceB1f6A94O206exxW2RIMH+gMUc=
|
||||||
github.com/larksuite/oapi-sdk-go/v3 v3.3.1/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
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=
|
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/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 h1:MYzLheyVx1tJVDqfu3YnN4jtnyALNzLvwl+f58TcvQY=
|
||||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
|
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 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||||
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
||||||
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
github.com/otiai10/mint v1.5.1 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/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 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
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.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 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
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=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ func InitialSettings() []model.SettingItem {
|
|||||||
} else {
|
} else {
|
||||||
token = random.Token()
|
token = random.Token()
|
||||||
}
|
}
|
||||||
|
defaultRoleID := strconv.Itoa(model.GUEST)
|
||||||
initialSettingItems = []model.SettingItem{
|
initialSettingItems = []model.SettingItem{
|
||||||
// site settings
|
// site settings
|
||||||
{Key: conf.VERSION, Value: conf.Version, Type: conf.TypeString, Group: model.SITE, Flag: model.READONLY},
|
{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.AllowIndexed, Value: "false", Type: conf.TypeBool, Group: model.SITE},
|
||||||
{Key: conf.AllowMounted, Value: "true", 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.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
|
// 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.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},
|
{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.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL},
|
||||||
{Key: conf.IgnoreDirectLinkParams, Value: "sign,alist_ts", Type: conf.TypeString, 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.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
|
// single settings
|
||||||
{Key: conf.Token, Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE},
|
{Key: conf.Token, Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE},
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ import (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
formatter := logrus.TextFormatter{
|
formatter := logrus.TextFormatter{
|
||||||
ForceColors: true,
|
TimestampFormat: "2006-01-02 15:04:05",
|
||||||
EnvironmentOverrideColors: true,
|
FullTimestamp: 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)
|
logrus.SetFormatter(&formatter)
|
||||||
utils.Log.SetFormatter(&formatter)
|
utils.Log.SetFormatter(&formatter)
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ const (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// site
|
// site
|
||||||
VERSION = "version"
|
VERSION = "version"
|
||||||
SiteTitle = "site_title"
|
SiteTitle = "site_title"
|
||||||
Announcement = "announcement"
|
Announcement = "announcement"
|
||||||
AllowIndexed = "allow_indexed"
|
AllowIndexed = "allow_indexed"
|
||||||
AllowMounted = "allow_mounted"
|
AllowMounted = "allow_mounted"
|
||||||
RobotsTxt = "robots_txt"
|
RobotsTxt = "robots_txt"
|
||||||
|
AllowRegister = "allow_register"
|
||||||
|
DefaultRole = "default_role"
|
||||||
|
UseNewui = "use_newui"
|
||||||
|
|
||||||
Logo = "logo"
|
Logo = "logo"
|
||||||
Favicon = "favicon"
|
Favicon = "favicon"
|
||||||
@@ -45,6 +48,9 @@ const (
|
|||||||
ForwardDirectLinkParams = "forward_direct_link_params"
|
ForwardDirectLinkParams = "forward_direct_link_params"
|
||||||
IgnoreDirectLinkParams = "ignore_direct_link_params"
|
IgnoreDirectLinkParams = "ignore_direct_link_params"
|
||||||
WebauthnLoginEnabled = "webauthn_login_enabled"
|
WebauthnLoginEnabled = "webauthn_login_enabled"
|
||||||
|
MaxDevices = "max_devices"
|
||||||
|
DeviceEvictPolicy = "device_evict_policy"
|
||||||
|
DeviceSessionTTL = "device_session_ttl"
|
||||||
|
|
||||||
// index
|
// index
|
||||||
SearchIndex = "search_index"
|
SearchIndex = "search_index"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ var db *gorm.DB
|
|||||||
|
|
||||||
func Init(d *gorm.DB) {
|
func Init(d *gorm.DB) {
|
||||||
db = d
|
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), new(model.Role), new(model.Label), new(model.LabelFileBinDing), new(model.ObjFile))
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("failed migrate database: %s", err.Error())
|
log.Fatalf("failed migrate database: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetLabelIds Get all label_ids from database order by file_name
|
// GetLabelIds Get all label_ids from database order by file_name
|
||||||
func GetLabelIds(userId uint, fileName string) ([]uint, error) {
|
func GetLabelIds(userId uint, fileName string) ([]uint, error) {
|
||||||
labelFileBinDingDB := db.Model(&model.LabelFileBinDing{})
|
//fmt.Printf(">>> [GetLabelIds] userId: %d, fileName: %s\n", userId, fileName)
|
||||||
|
labelFileBinDingDB := db.Model(&model.LabelFileBinding{})
|
||||||
var labelIds []uint
|
var labelIds []uint
|
||||||
if err := labelFileBinDingDB.Where("file_name = ?", fileName).Where("user_id = ?", userId).Pluck("label_id", &labelIds).Error; err != nil {
|
if err := labelFileBinDingDB.Where("file_name = ?", fileName).Where("user_id = ?", userId).Pluck("label_id", &labelIds).Error; err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
@@ -18,7 +21,7 @@ func GetLabelIds(userId uint, fileName string) ([]uint, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CreateLabelFileBinDing(fileName string, labelId, userId uint) error {
|
func CreateLabelFileBinDing(fileName string, labelId, userId uint) error {
|
||||||
var labelFileBinDing model.LabelFileBinDing
|
var labelFileBinDing model.LabelFileBinding
|
||||||
labelFileBinDing.UserId = userId
|
labelFileBinDing.UserId = userId
|
||||||
labelFileBinDing.LabelId = labelId
|
labelFileBinDing.LabelId = labelId
|
||||||
labelFileBinDing.FileName = fileName
|
labelFileBinDing.FileName = fileName
|
||||||
@@ -32,7 +35,7 @@ func CreateLabelFileBinDing(fileName string, labelId, userId uint) error {
|
|||||||
|
|
||||||
// GetLabelFileBinDingByLabelIdExists Get Label by label_id, used to del label usually
|
// GetLabelFileBinDingByLabelIdExists Get Label by label_id, used to del label usually
|
||||||
func GetLabelFileBinDingByLabelIdExists(labelId, userId uint) bool {
|
func GetLabelFileBinDingByLabelIdExists(labelId, userId uint) bool {
|
||||||
var labelFileBinDing model.LabelFileBinDing
|
var labelFileBinDing model.LabelFileBinding
|
||||||
result := db.Where("label_id = ?", labelId).Where("user_id = ?", userId).First(&labelFileBinDing)
|
result := db.Where("label_id = ?", labelId).Where("user_id = ?", userId).First(&labelFileBinDing)
|
||||||
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound)
|
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound)
|
||||||
return exists
|
return exists
|
||||||
@@ -40,17 +43,150 @@ func GetLabelFileBinDingByLabelIdExists(labelId, userId uint) bool {
|
|||||||
|
|
||||||
// DelLabelFileBinDingByFileName used to del usually
|
// DelLabelFileBinDingByFileName used to del usually
|
||||||
func DelLabelFileBinDingByFileName(userId uint, fileName string) error {
|
func DelLabelFileBinDingByFileName(userId uint, fileName string) error {
|
||||||
return errors.WithStack(db.Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinDing{}).Error)
|
return errors.WithStack(db.Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinding{}).Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DelLabelFileBinDingById used to del usually
|
// DelLabelFileBinDingById used to del usually
|
||||||
func DelLabelFileBinDingById(labelId, userId uint, fileName string) error {
|
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)
|
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) {
|
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 {
|
if err := db.Where("label_id in (?)", labelIds).Where("user_id = ?", userId).Find(&result).Error; err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
return result, nil
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,11 +35,27 @@ func GetRoles(pageIndex, pageSize int) (roles []model.Role, count int64, err err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CreateRole(r *model.Role) error {
|
func CreateRole(r *model.Role) error {
|
||||||
return errors.WithStack(db.Create(r).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 {
|
func UpdateRole(r *model.Role) error {
|
||||||
return errors.WithStack(db.Save(r).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 {
|
func DeleteRole(id uint) error {
|
||||||
|
|||||||
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,12 +2,14 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,6 +26,20 @@ func GetUserByRole(role int) (*model.User, error) {
|
|||||||
return nil, gorm.ErrRecordNotFound
|
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) {
|
func GetUserByName(username string) (*model.User, error) {
|
||||||
user := model.User{Username: username}
|
user := model.User{Username: username}
|
||||||
if err := db.Where(user).First(&user).Error; err != nil {
|
if err := db.Where(user).First(&user).Error; err != nil {
|
||||||
@@ -108,25 +124,29 @@ func RemoveAuthn(u *model.User, id string) error {
|
|||||||
return UpdateAuthn(u.ID, string(res))
|
return UpdateAuthn(u.ID, string(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateUserBasePathPrefix(oldPath, newPath string) ([]string, error) {
|
func UpdateUserBasePathPrefix(oldPath, newPath string, usersOpt ...[]model.User) ([]string, error) {
|
||||||
var users []model.User
|
var users []model.User
|
||||||
var modifiedUsernames []string
|
var modifiedUsernames []string
|
||||||
|
|
||||||
if err := db.Find(&users).Error; err != nil {
|
|
||||||
return nil, errors.WithMessage(err, "failed to load users")
|
|
||||||
}
|
|
||||||
|
|
||||||
oldPathClean := path.Clean(oldPath)
|
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 {
|
for _, user := range users {
|
||||||
basePath := path.Clean(user.BasePath)
|
basePath := path.Clean(user.BasePath)
|
||||||
updated := false
|
updated := false
|
||||||
|
|
||||||
if basePath == oldPathClean {
|
if basePath == oldPathClean {
|
||||||
user.BasePath = newPath
|
user.BasePath = path.Clean(newPath)
|
||||||
updated = true
|
updated = true
|
||||||
} else if strings.HasPrefix(basePath, oldPathClean+"/") {
|
} else if strings.HasPrefix(basePath, oldPathClean+"/") {
|
||||||
user.BasePath = newPath + basePath[len(oldPathClean):]
|
user.BasePath = path.Clean(newPath + basePath[len(oldPathClean):])
|
||||||
updated = true
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,3 +160,13 @@ func UpdateUserBasePathPrefix(oldPath, newPath string) ([]string, error) {
|
|||||||
|
|
||||||
return modifiedUsernames, nil
|
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 (
|
var (
|
||||||
EmptyToken = errors.New("empty token")
|
EmptyToken = errors.New("empty token")
|
||||||
|
LinkIsDir = errors.New("link is dir")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package errs
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrChangeDefaultRole = errors.New("cannot modify admin or guest role")
|
ErrChangeDefaultRole = errors.New("cannot modify admin role")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package model
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type LabelFileBinDing struct {
|
type LabelFileBinding struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"` // unique key
|
ID uint `json:"id" gorm:"primaryKey"` // unique key
|
||||||
UserId uint `json:"user_id"` // use to user_id
|
UserId uint `json:"user_id"` // use to user_id
|
||||||
LabelId uint `json:"label_id"` // use to label_id
|
LabelId uint `json:"label_id"` // use to label_id
|
||||||
|
|||||||
@@ -55,6 +55,21 @@ type FileStreamer interface {
|
|||||||
|
|
||||||
type UpdateProgress func(percentage float64)
|
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 {
|
type URL interface {
|
||||||
URL() string
|
URL() string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Role struct {
|
|||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
Name string `json:"name" gorm:"unique" binding:"required"`
|
Name string `json:"name" gorm:"unique" binding:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Default bool `json:"default" gorm:"default:false"`
|
||||||
// PermissionScopes stores structured permission list and is ignored by gorm.
|
// PermissionScopes stores structured permission list and is ignored by gorm.
|
||||||
PermissionScopes []PermissionEntry `json:"permission_scopes" gorm:"-"`
|
PermissionScopes []PermissionEntry `json:"permission_scopes" gorm:"-"`
|
||||||
// RawPermission is the JSON representation of PermissionScopes stored in DB.
|
// RawPermission is the JSON representation of PermissionScopes stored in DB.
|
||||||
|
|||||||
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
|
||||||
|
)
|
||||||
@@ -145,13 +145,28 @@ func (u *User) CheckPathLimit() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) JoinPath(reqPath string) (string, error) {
|
func (u *User) JoinPath(reqPath string) (string, error) {
|
||||||
|
if reqPath == "/" {
|
||||||
|
return utils.FixAndCleanPath(u.BasePath), nil
|
||||||
|
}
|
||||||
path, err := utils.JoinBasePath(u.BasePath, reqPath)
|
path, err := utils.JoinBasePath(u.BasePath, reqPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if u.CheckPathLimit() && !utils.IsSubPath(u.BasePath, path) {
|
|
||||||
return "", errs.PermissionDenied
|
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
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,3 +208,33 @@ func (u *User) WebAuthnCredentials() []webauthn.Credential {
|
|||||||
func (u *User) WebAuthnIcon() string {
|
func (u *User) WebAuthnIcon() string {
|
||||||
return "https://alistgo.com/logo.svg"
|
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 (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"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, ",")
|
conf.SlicesMap[conf.IgnoreDirectLinkParams] = strings.Split(item.Value, ",")
|
||||||
return nil
|
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) {
|
func RegisterSettingItemHook(key string, hook SettingItemHook) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type CreateLabelFileBinDingReq struct {
|
|||||||
Type int `json:"type"`
|
Type int `json:"type"`
|
||||||
HashInfoStr string `json:"hashinfo"`
|
HashInfoStr string `json:"hashinfo"`
|
||||||
LabelIds string `json:"label_ids"`
|
LabelIds string `json:"label_ids"`
|
||||||
|
LabelIDs []uint64 `json:"labelIdList"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ObjLabelResp struct {
|
type ObjLabelResp struct {
|
||||||
@@ -54,23 +55,29 @@ func GetLabelByFileName(userId uint, fileName string) ([]model.Label, error) {
|
|||||||
return labels, nil
|
return labels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetLabelsByFileNamesPublic(fileNames []string) (map[string][]model.Label, error) {
|
||||||
|
return db.GetLabelsByFileNamesPublic(fileNames)
|
||||||
|
}
|
||||||
|
|
||||||
func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
|
func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
|
||||||
if err := db.DelLabelFileBinDingByFileName(userId, req.Name); err != nil {
|
if err := db.DelLabelFileBinDingByFileName(userId, req.Name); err != nil {
|
||||||
return errors.WithMessage(err, "failed del label_file_bin_ding in database")
|
return errors.WithMessage(err, "failed del label_file_bin_ding in database")
|
||||||
}
|
}
|
||||||
if req.LabelIds == "" {
|
|
||||||
|
ids, err := collectLabelIDs(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
labelMap := strings.Split(req.LabelIds, ",")
|
|
||||||
for _, value := range labelMap {
|
for _, id := range ids {
|
||||||
labelId, err := strconv.ParseUint(value, 10, 64)
|
if err = db.CreateLabelFileBinDing(req.Name, uint(id), userId); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid label ID '%s': %v", value, err)
|
|
||||||
}
|
|
||||||
if err = db.CreateLabelFileBinDing(req.Name, uint(labelId), userId); err != nil {
|
|
||||||
return errors.WithMessage(err, "failed labels in database")
|
return errors.WithMessage(err, "failed labels in database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !db.GetFileByNameExists(req.Name) {
|
if !db.GetFileByNameExists(req.Name) {
|
||||||
objFile := model.ObjFile{
|
objFile := model.ObjFile{
|
||||||
Id: req.Id,
|
Id: req.Id,
|
||||||
@@ -86,8 +93,7 @@ func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
|
|||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
HashInfoStr: req.HashInfoStr,
|
HashInfoStr: req.HashInfoStr,
|
||||||
}
|
}
|
||||||
err := db.CreateObjFile(objFile)
|
if err := db.CreateObjFile(objFile); err != nil {
|
||||||
if err != nil {
|
|
||||||
return errors.WithMessage(err, "failed file in database")
|
return errors.WithMessage(err, "failed file in database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +103,7 @@ func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
|
|||||||
func GetFileByLabel(userId uint, labelId string) (result []ObjLabelResp, err error) {
|
func GetFileByLabel(userId uint, labelId string) (result []ObjLabelResp, err error) {
|
||||||
labelMap := strings.Split(labelId, ",")
|
labelMap := strings.Split(labelId, ",")
|
||||||
var labelIds []uint
|
var labelIds []uint
|
||||||
var labelsFile []model.LabelFileBinDing
|
var labelsFile []model.LabelFileBinding
|
||||||
var labels []model.Label
|
var labels []model.Label
|
||||||
var labelsFileMap = make(map[string][]model.Label)
|
var labelsFileMap = make(map[string][]model.Label)
|
||||||
var labelsMap = make(map[uint]model.Label)
|
var labelsMap = make(map[uint]model.Label)
|
||||||
@@ -157,3 +163,33 @@ func StringSliceToUintSlice(strSlice []string) ([]uint, error) {
|
|||||||
}
|
}
|
||||||
return uintSlice, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ package op
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Xhofe/go-cache"
|
"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/db"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
@@ -16,6 +17,10 @@ import (
|
|||||||
var roleCache = cache.NewMemCache[*model.Role](cache.WithShards[*model.Role](2))
|
var roleCache = cache.NewMemCache[*model.Role](cache.WithShards[*model.Role](2))
|
||||||
var roleG singleflight.Group[*model.Role]
|
var roleG singleflight.Group[*model.Role]
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
model.FetchRole = GetRole
|
||||||
|
}
|
||||||
|
|
||||||
func GetRole(id uint) (*model.Role, error) {
|
func GetRole(id uint) (*model.Role, error) {
|
||||||
key := fmt.Sprint(id)
|
key := fmt.Sprint(id)
|
||||||
if r, ok := roleCache.Get(key); ok {
|
if r, ok := roleCache.Get(key); ok {
|
||||||
@@ -47,6 +52,23 @@ func GetRoleByName(name string) (*model.Role, error) {
|
|||||||
return r, err
|
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) {
|
func GetRolesByUserID(userID uint) ([]model.Role, error) {
|
||||||
user, err := GetUserById(userID)
|
user, err := GetUserById(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -89,7 +111,21 @@ func CreateRole(r *model.Role) error {
|
|||||||
}
|
}
|
||||||
roleCache.Del(fmt.Sprint(r.ID))
|
roleCache.Del(fmt.Sprint(r.ID))
|
||||||
roleCache.Del(r.Name)
|
roleCache.Del(r.Name)
|
||||||
return db.CreateRole(r)
|
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 {
|
func UpdateRole(r *model.Role) error {
|
||||||
@@ -97,29 +133,52 @@ func UpdateRole(r *model.Role) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if old.Name == "admin" || old.Name == "guest" {
|
switch old.Name {
|
||||||
|
case "admin":
|
||||||
return errs.ErrChangeDefaultRole
|
return errs.ErrChangeDefaultRole
|
||||||
|
case "guest":
|
||||||
|
r.Name = "guest"
|
||||||
}
|
}
|
||||||
for i := range r.PermissionScopes {
|
for i := range r.PermissionScopes {
|
||||||
r.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path)
|
r.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path)
|
||||||
}
|
}
|
||||||
if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 &&
|
//if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 &&
|
||||||
old.PermissionScopes[0].Path != r.PermissionScopes[0].Path {
|
// old.PermissionScopes[0].Path != r.PermissionScopes[0].Path {
|
||||||
|
//
|
||||||
oldPath := old.PermissionScopes[0].Path
|
// oldPath := old.PermissionScopes[0].Path
|
||||||
newPath := r.PermissionScopes[0].Path
|
// newPath := r.PermissionScopes[0].Path
|
||||||
modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath)
|
//
|
||||||
if err != nil {
|
// users, err := db.GetUsersByRole(int(r.ID))
|
||||||
return errors.WithMessage(err, "failed to update user base path when role updated")
|
// if err != nil {
|
||||||
}
|
// return errors.WithMessage(err, "failed to get users by role")
|
||||||
|
// }
|
||||||
for _, name := range modifiedUsernames {
|
//
|
||||||
userCache.Del(name)
|
// 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(fmt.Sprint(r.ID))
|
||||||
roleCache.Del(r.Name)
|
roleCache.Del(r.Name)
|
||||||
return db.UpdateRole(r)
|
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 {
|
func DeleteRole(id uint) error {
|
||||||
|
|||||||
@@ -232,12 +232,20 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
|
|||||||
roleCache.Del(fmt.Sprint(id))
|
roleCache.Del(fmt.Sprint(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldStorage.MountPath, storage.MountPath)
|
//modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldStorage.MountPath, storage.MountPath)
|
||||||
if err != nil {
|
//if err != nil {
|
||||||
return errors.WithMessage(err, "failed to update user base path")
|
// return errors.WithMessage(err, "failed to update user base path")
|
||||||
}
|
//}
|
||||||
for _, name := range modifiedUsernames {
|
for _, id := range modifiedRoleIDs {
|
||||||
userCache.Del(name)
|
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 {
|
if err != nil {
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ func GetUserByRole(role int) (*model.User, error) {
|
|||||||
return db.GetUserByRole(role)
|
return db.GetUserByRole(role)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUsersByRole(role int) ([]model.User, error) {
|
||||||
|
return db.GetUsersByRole(role)
|
||||||
|
}
|
||||||
|
|
||||||
func GetUserByName(username string) (*model.User, error) {
|
func GetUserByName(username string) (*model.User, error) {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return nil, errs.EmptyUsername
|
return nil, errs.EmptyUsername
|
||||||
@@ -124,17 +128,17 @@ func UpdateUser(u *model.User) error {
|
|||||||
}
|
}
|
||||||
userCache.Del(old.Username)
|
userCache.Del(old.Username)
|
||||||
u.BasePath = utils.FixAndCleanPath(u.BasePath)
|
u.BasePath = utils.FixAndCleanPath(u.BasePath)
|
||||||
if len(u.Role) > 0 {
|
//if len(u.Role) > 0 {
|
||||||
roles, err := GetRolesByUserID(u.ID)
|
// roles, err := GetRolesByUserID(u.ID)
|
||||||
if err == nil {
|
// if err == nil {
|
||||||
for _, role := range roles {
|
// for _, role := range roles {
|
||||||
if len(role.PermissionScopes) > 0 {
|
// if len(role.PermissionScopes) > 0 {
|
||||||
u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
|
// u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
return db.UpdateUser(u)
|
return db.UpdateUser(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,3 +169,11 @@ func DelUserCache(username string) error {
|
|||||||
userCache.Del(username)
|
userCache.Del(username)
|
||||||
return nil
|
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, "/../") {
|
strings.Contains(reqPath, "/../") {
|
||||||
return "", errs.RelativePath
|
return "", errs.RelativePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reqPath = FixAndCleanPath(reqPath)
|
||||||
|
|
||||||
|
if strings.HasPrefix(reqPath, "/") {
|
||||||
|
return reqPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
return stdpath.Join(FixAndCleanPath(basePath), FixAndCleanPath(reqPath)), nil
|
return stdpath.Join(FixAndCleanPath(basePath), FixAndCleanPath(reqPath)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,17 +43,23 @@ func MergeRolePermissions(u *model.User, reqPath string) int32 {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, entry := range role.PermissionScopes {
|
if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) {
|
||||||
if utils.IsSubPath(entry.Path, reqPath) {
|
for _, entry := range role.PermissionScopes {
|
||||||
perm |= entry.Permission
|
perm |= entry.Permission
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for _, entry := range role.PermissionScopes {
|
||||||
|
if utils.IsSubPath(entry.Path, reqPath) {
|
||||||
|
perm |= entry.Permission
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return perm
|
return perm
|
||||||
}
|
}
|
||||||
|
|
||||||
func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password string) bool {
|
func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password string) bool {
|
||||||
if !canReadPathByRole(u, reqPath) {
|
if !CanReadPathByRole(u, reqPath) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
perm := MergeRolePermissions(u, reqPath)
|
perm := MergeRolePermissions(u, reqPath)
|
||||||
@@ -78,7 +84,30 @@ func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password strin
|
|||||||
return meta.Password == password
|
return meta.Password == password
|
||||||
}
|
}
|
||||||
|
|
||||||
func canReadPathByRole(u *model.User, reqPath string) bool {
|
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 {
|
if u == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -88,7 +117,7 @@ func canReadPathByRole(u *model.User, reqPath string) bool {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, entry := range role.PermissionScopes {
|
for _, entry := range role.PermissionScopes {
|
||||||
if utils.IsSubPath(entry.Path, reqPath) {
|
if utils.IsSubPath(reqPath, entry.Path) && HasPermission(entry.Permission, bit) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +131,7 @@ func canReadPathByRole(u *model.User, reqPath string) bool {
|
|||||||
func CheckPathLimitWithRoles(u *model.User, reqPath string) bool {
|
func CheckPathLimitWithRoles(u *model.User, reqPath string) bool {
|
||||||
perm := MergeRolePermissions(u, reqPath)
|
perm := MergeRolePermissions(u, reqPath)
|
||||||
if HasPermission(perm, PermPathLimit) {
|
if HasPermission(perm, PermPathLimit) {
|
||||||
return canReadPathByRole(u, reqPath)
|
return CanReadPathByRole(u, reqPath)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,22 @@ package handles
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"image/png"
|
"image/png"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Xhofe/go-cache"
|
"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/model"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
"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/alist-org/alist/v3/server/common"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
@@ -79,16 +87,62 @@ func loginHash(c *gin.Context, req *LoginReq) {
|
|||||||
return
|
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
|
// generate token
|
||||||
token, err := common.GenerateToken(user)
|
token, err := common.GenerateToken(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResp(c, err, 400, true)
|
common.ErrorResp(c, err, 400, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
common.SuccessResp(c, gin.H{"token": token})
|
common.SuccessResp(c, gin.H{"token": token, "device_key": key})
|
||||||
loginCache.Del(ip)
|
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 {
|
type UserResp struct {
|
||||||
model.User
|
model.User
|
||||||
Otp bool `json:"otp"`
|
Otp bool `json:"otp"`
|
||||||
@@ -216,6 +270,13 @@ func Verify2FA(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LogOut(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"))
|
err := common.InvalidateToken(c.GetHeader("Authorization"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResp(c, err, 500)
|
common.ErrorResp(c, err, 500)
|
||||||
|
|||||||
@@ -107,14 +107,21 @@ func FsList(c *gin.Context) {
|
|||||||
common.ErrorResp(c, err, 500)
|
common.ErrorResp(c, err, 500)
|
||||||
return
|
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"
|
provider := "unknown"
|
||||||
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
|
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
provider = storage.GetStorage().Driver
|
provider = storage.GetStorage().Driver
|
||||||
}
|
}
|
||||||
common.SuccessResp(c, FsListResp{
|
common.SuccessResp(c, FsListResp{
|
||||||
Content: toObjsResp(objs, reqPath, isEncrypt(meta, reqPath), user.ID),
|
Content: toObjsResp(objs, reqPath, isEncrypt(meta, reqPath)),
|
||||||
Total: int64(total),
|
Total: int64(total),
|
||||||
Readme: getReadme(meta, reqPath),
|
Readme: getReadme(meta, reqPath),
|
||||||
Header: getHeader(meta, reqPath),
|
Header: getHeader(meta, reqPath),
|
||||||
@@ -161,7 +168,14 @@ func FsDirs(c *gin.Context) {
|
|||||||
common.ErrorResp(c, err, 500)
|
common.ErrorResp(c, err, 500)
|
||||||
return
|
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)
|
common.SuccessResp(c, dirs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,12 +238,22 @@ func pagination(objs []model.Obj, req *model.PageReq) (int, []model.Obj) {
|
|||||||
return total, objs[start:end]
|
return total, objs[start:end]
|
||||||
}
|
}
|
||||||
|
|
||||||
func toObjsResp(objs []model.Obj, parent string, encrypt bool, userId uint) []ObjLabelResp {
|
func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjLabelResp {
|
||||||
var resp []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 {
|
for _, obj := range objs {
|
||||||
var labels []model.Label
|
var labels []model.Label
|
||||||
if obj.IsDir() == false {
|
if !obj.IsDir() {
|
||||||
labels, _ = op.GetLabelByFileName(userId, obj.GetName())
|
labels = labelsByName[obj.GetName()]
|
||||||
}
|
}
|
||||||
thumb, _ := model.GetThumb(obj)
|
thumb, _ := model.GetThumb(obj)
|
||||||
resp = append(resp, ObjLabelResp{
|
resp = append(resp, ObjLabelResp{
|
||||||
@@ -369,7 +393,7 @@ func FsGet(c *gin.Context) {
|
|||||||
Readme: getReadme(meta, reqPath),
|
Readme: getReadme(meta, reqPath),
|
||||||
Header: getHeader(meta, reqPath),
|
Header: getHeader(meta, reqPath),
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
Related: toObjsResp(related, parentPath, isEncrypt(parentMeta, parentPath), user.ID),
|
Related: toObjsResp(related, parentPath, isEncrypt(parentMeta, parentPath)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"github.com/alist-org/alist/v3/internal/op"
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
"github.com/alist-org/alist/v3/server/common"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DelLabelFileBinDingReq struct {
|
type DelLabelFileBinDingReq struct {
|
||||||
@@ -16,18 +18,36 @@ type DelLabelFileBinDingReq struct {
|
|||||||
LabelId string `json:"label_id"`
|
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) {
|
func GetLabelByFileName(c *gin.Context) {
|
||||||
fileName := c.Query("file_name")
|
fileName := c.Query("file_name")
|
||||||
if fileName == "" {
|
if fileName == "" {
|
||||||
common.ErrorResp(c, errors.New("file_name must not empty"), 400)
|
common.ErrorResp(c, errors.New("file_name must not empty"), 400)
|
||||||
return
|
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)
|
userObj, ok := c.Value("user").(*model.User)
|
||||||
if !ok {
|
if !ok {
|
||||||
common.ErrorStrResp(c, "user invalid", 401)
|
common.ErrorStrResp(c, "user invalid", 401)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
labels, err := op.GetLabelByFileName(userObj.ID, fileName)
|
labels, err := op.GetLabelByFileName(userObj.ID, decodedFileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResp(c, err, 500, true)
|
common.ErrorResp(c, err, 500, true)
|
||||||
return
|
return
|
||||||
@@ -101,3 +121,130 @@ func GetFileByLabel(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
common.SuccessResp(c, fileList)
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func GetRole(c *gin.Context) {
|
|||||||
|
|
||||||
func CreateRole(c *gin.Context) {
|
func CreateRole(c *gin.Context) {
|
||||||
var req model.Role
|
var req model.Role
|
||||||
if err := c.ShouldBind(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
common.ErrorResp(c, err, 400)
|
common.ErrorResp(c, err, 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -56,8 +56,14 @@ func CreateRole(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateRole(c *gin.Context) {
|
func UpdateRole(c *gin.Context) {
|
||||||
var req model.Role
|
var req struct {
|
||||||
if err := c.ShouldBind(&req); err != nil {
|
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)
|
common.ErrorResp(c, err, 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -66,11 +72,21 @@ func UpdateRole(c *gin.Context) {
|
|||||||
common.ErrorResp(c, err, 500, true)
|
common.ErrorResp(c, err, 500, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if role.Name == "admin" || role.Name == "guest" {
|
switch role.Name {
|
||||||
|
case "admin":
|
||||||
common.ErrorResp(c, errs.ErrChangeDefaultRole, 403)
|
common.ErrorResp(c, errs.ErrChangeDefaultRole, 403)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
case "guest":
|
||||||
|
req.Name = "guest"
|
||||||
}
|
}
|
||||||
if err := op.UpdateRole(&req); err != nil {
|
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)
|
common.ErrorResp(c, err, 500, true)
|
||||||
} else {
|
} else {
|
||||||
common.SuccessResp(c)
|
common.SuccessResp(c)
|
||||||
|
|||||||
@@ -43,28 +43,39 @@ func Search(c *gin.Context) {
|
|||||||
common.ErrorResp(c, err, 400)
|
common.ErrorResp(c, err, 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nodes, total, err := search.Search(c, req.SearchReq)
|
var (
|
||||||
if err != nil {
|
filteredNodes []model.SearchNode
|
||||||
common.ErrorResp(c, err, 500)
|
)
|
||||||
return
|
for len(filteredNodes) < req.PerPage {
|
||||||
}
|
nodes, _, err := search.Search(c, req.SearchReq)
|
||||||
var filteredNodes []model.SearchNode
|
if err != nil {
|
||||||
for _, node := range nodes {
|
common.ErrorResp(c, err, 500)
|
||||||
if !strings.HasPrefix(node.Parent, user.BasePath) {
|
return
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
meta, err := op.GetNearestMeta(node.Parent)
|
if len(nodes) == 0 {
|
||||||
if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
break
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if !common.CanAccessWithRoles(user, meta, path.Join(node.Parent, node.Name), req.Password) {
|
for _, node := range nodes {
|
||||||
continue
|
if !strings.HasPrefix(node.Parent, user.BasePath) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
meta, err := op.GetNearestMeta(node.Parent)
|
||||||
|
if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !common.CanAccessWithRoles(user, meta, path.Join(node.Parent, node.Name), req.Password) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filteredNodes = append(filteredNodes, node)
|
||||||
|
if len(filteredNodes) >= req.PerPage {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
filteredNodes = append(filteredNodes, node)
|
req.Page++
|
||||||
}
|
}
|
||||||
common.SuccessResp(c, common.PageResp{
|
common.SuccessResp(c, common.PageResp{
|
||||||
Content: utils.MustSliceConvert(filteredNodes, nodeToSearchResp),
|
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"
|
"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) {
|
func ResetToken(c *gin.Context) {
|
||||||
token := random.Token()
|
token := random.Token()
|
||||||
item := model.SettingItem{Key: "token", Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE}
|
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)
|
common.ErrorResp(c, err, 400)
|
||||||
return
|
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)
|
common.SuccessResp(c, item)
|
||||||
} else {
|
} else {
|
||||||
items, err := op.GetSettingItemInKeys(strings.Split(keys, ","))
|
items, err := op.GetSettingItemInKeys(strings.Split(keys, ","))
|
||||||
@@ -41,6 +67,17 @@ func GetSetting(c *gin.Context) {
|
|||||||
common.ErrorResp(c, err, 400)
|
common.ErrorResp(c, err, 400)
|
||||||
return
|
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)
|
common.SuccessResp(c, items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +88,22 @@ func SaveSettings(c *gin.Context) {
|
|||||||
common.ErrorResp(c, err, 400)
|
common.ErrorResp(c, err, 400)
|
||||||
return
|
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 {
|
if err := op.SaveSettingItems(req); err != nil {
|
||||||
common.ErrorResp(c, err, 500)
|
common.ErrorResp(c, err, 500)
|
||||||
} else {
|
} else {
|
||||||
@@ -88,6 +141,17 @@ func ListSettings(c *gin.Context) {
|
|||||||
common.ErrorResp(c, err, 400)
|
common.ErrorResp(c, err, 400)
|
||||||
return
|
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)
|
common.SuccessResp(c, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package handles
|
package handles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
"github.com/alist-org/alist/v3/server/common"
|
||||||
@@ -36,6 +37,9 @@ func CreateUser(c *gin.Context) {
|
|||||||
common.ErrorResp(c, err, 400)
|
common.ErrorResp(c, err, 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(req.Role) == 0 {
|
||||||
|
req.Role = model.Roles{op.GetDefaultRoleID()}
|
||||||
|
}
|
||||||
if req.IsAdmin() || req.IsGuest() {
|
if req.IsAdmin() || req.IsGuest() {
|
||||||
common.ErrorStrResp(c, "admin or guest user can not be created", 400, true)
|
common.ErrorStrResp(c, "admin or guest user can not be created", 400, true)
|
||||||
return
|
return
|
||||||
@@ -67,10 +71,10 @@ func UpdateUser(c *gin.Context) {
|
|||||||
common.ErrorStrResp(c, "cannot change role of admin user", 403)
|
common.ErrorStrResp(c, "cannot change role of admin user", 403)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if user.Username != req.Username {
|
//if user.Username != req.Username {
|
||||||
common.ErrorStrResp(c, "cannot change username of admin user", 403)
|
// common.ErrorStrResp(c, "cannot change username of admin user", 403)
|
||||||
return
|
// return
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Password == "" {
|
if req.Password == "" {
|
||||||
@@ -83,10 +87,25 @@ func UpdateUser(c *gin.Context) {
|
|||||||
if req.OtpSecret == "" {
|
if req.OtpSecret == "" {
|
||||||
req.OtpSecret = user.OtpSecret
|
req.OtpSecret = user.OtpSecret
|
||||||
}
|
}
|
||||||
if req.Disabled && req.IsAdmin() {
|
if req.Disabled && user.IsAdmin() {
|
||||||
common.ErrorStrResp(c, "admin user can not be disabled", 400)
|
count, err := op.CountEnabledAdminsExcluding(user.ID)
|
||||||
return
|
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 {
|
if err := op.UpdateUser(&req); err != nil {
|
||||||
common.ErrorResp(c, err, 500)
|
common.ErrorResp(c, err, 500)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ package middlewares
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"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/model"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
"github.com/alist-org/alist/v3/internal/setting"
|
"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/common"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -24,7 +28,9 @@ func Auth(c *gin.Context) {
|
|||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Set("user", admin)
|
if !HandleSession(c, admin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Debugf("use admin token: %+v", admin)
|
log.Debugf("use admin token: %+v", admin)
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
@@ -41,7 +47,18 @@ func Auth(c *gin.Context) {
|
|||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
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)
|
log.Debugf("use empty token: %+v", guest)
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
@@ -78,11 +95,36 @@ func Auth(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
user.RolesDetail = roles
|
user.RolesDetail = roles
|
||||||
}
|
}
|
||||||
c.Set("user", user)
|
if !HandleSession(c, user) {
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Debugf("use login token: %+v", user)
|
log.Debugf("use login token: %+v", user)
|
||||||
c.Next()
|
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) {
|
func Authn(c *gin.Context) {
|
||||||
token := c.GetHeader("Authorization")
|
token := c.GetHeader("Authorization")
|
||||||
if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 {
|
if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 {
|
||||||
|
|||||||
26
server/middlewares/session_refresh.go
Normal file
26
server/middlewares/session_refresh.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/internal/device"
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SessionRefresh updates session's last_active after successful requests.
|
||||||
|
func SessionRefresh(c *gin.Context) {
|
||||||
|
c.Next()
|
||||||
|
if c.Writer.Status() >= 400 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if inactive, ok := c.Get("session_inactive"); ok {
|
||||||
|
if b, ok := inactive.(bool); ok && b {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userVal, uok := c.Get("user")
|
||||||
|
keyVal, kok := c.Get("device_key")
|
||||||
|
if uok && kok {
|
||||||
|
user := userVal.(*model.User)
|
||||||
|
device.Refresh(user.ID, keyVal.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ func Init(e *gin.Engine) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Cors(e)
|
Cors(e)
|
||||||
|
e.Use(middlewares.SessionRefresh)
|
||||||
g := e.Group(conf.URL.Path)
|
g := e.Group(conf.URL.Path)
|
||||||
if conf.Conf.Scheme.HttpPort != -1 && conf.Conf.Scheme.HttpsPort != -1 && conf.Conf.Scheme.ForceHttps {
|
if conf.Conf.Scheme.HttpPort != -1 && conf.Conf.Scheme.HttpsPort != -1 && conf.Conf.Scheme.ForceHttps {
|
||||||
e.Use(middlewares.ForceHttps)
|
e.Use(middlewares.ForceHttps)
|
||||||
@@ -61,6 +62,7 @@ func Init(e *gin.Engine) {
|
|||||||
api.POST("/auth/login", handles.Login)
|
api.POST("/auth/login", handles.Login)
|
||||||
api.POST("/auth/login/hash", handles.LoginHash)
|
api.POST("/auth/login/hash", handles.LoginHash)
|
||||||
api.POST("/auth/login/ldap", handles.LoginLdap)
|
api.POST("/auth/login/ldap", handles.LoginLdap)
|
||||||
|
api.POST("/auth/register", handles.Register)
|
||||||
auth.GET("/me", handles.CurrentUser)
|
auth.GET("/me", handles.CurrentUser)
|
||||||
auth.POST("/me/update", handles.UpdateCurrent)
|
auth.POST("/me/update", handles.UpdateCurrent)
|
||||||
auth.GET("/me/sshkey/list", handles.ListMyPublicKey)
|
auth.GET("/me/sshkey/list", handles.ListMyPublicKey)
|
||||||
@@ -69,6 +71,8 @@ func Init(e *gin.Engine) {
|
|||||||
auth.POST("/auth/2fa/generate", handles.Generate2FA)
|
auth.POST("/auth/2fa/generate", handles.Generate2FA)
|
||||||
auth.POST("/auth/2fa/verify", handles.Verify2FA)
|
auth.POST("/auth/2fa/verify", handles.Verify2FA)
|
||||||
auth.GET("/auth/logout", handles.LogOut)
|
auth.GET("/auth/logout", handles.LogOut)
|
||||||
|
auth.GET("/me/sessions", handles.ListMySessions)
|
||||||
|
auth.POST("/me/sessions/evict", handles.EvictMySession)
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
api.GET("/auth/sso", handles.SSOLoginRedirect)
|
api.GET("/auth/sso", handles.SSOLoginRedirect)
|
||||||
@@ -92,6 +96,8 @@ func Init(e *gin.Engine) {
|
|||||||
|
|
||||||
_fs(auth.Group("/fs"))
|
_fs(auth.Group("/fs"))
|
||||||
_task(auth.Group("/task", middlewares.AuthNotGuest))
|
_task(auth.Group("/task", middlewares.AuthNotGuest))
|
||||||
|
_label(auth.Group("/label"))
|
||||||
|
_labelFileBinding(auth.Group("/label_file_binding"))
|
||||||
admin(auth.Group("/admin", middlewares.AuthAdmin))
|
admin(auth.Group("/admin", middlewares.AuthAdmin))
|
||||||
if flags.Debug || flags.Dev {
|
if flags.Debug || flags.Dev {
|
||||||
debug(g.Group("/debug"))
|
debug(g.Group("/debug"))
|
||||||
@@ -170,17 +176,21 @@ func admin(g *gin.RouterGroup) {
|
|||||||
index.GET("/progress", middlewares.SearchIndex, handles.GetProgress)
|
index.GET("/progress", middlewares.SearchIndex, handles.GetProgress)
|
||||||
|
|
||||||
label := g.Group("/label")
|
label := g.Group("/label")
|
||||||
label.GET("/list", handles.ListLabel)
|
|
||||||
label.GET("/get", handles.GetLabel)
|
|
||||||
label.POST("/create", handles.CreateLabel)
|
label.POST("/create", handles.CreateLabel)
|
||||||
label.POST("/update", handles.UpdateLabel)
|
label.POST("/update", handles.UpdateLabel)
|
||||||
label.POST("/delete", handles.DeleteLabel)
|
label.POST("/delete", handles.DeleteLabel)
|
||||||
|
|
||||||
labelFileBinding := g.Group("/label_file_binding")
|
labelFileBinding := g.Group("/label_file_binding")
|
||||||
labelFileBinding.GET("/get", handles.GetLabelByFileName)
|
labelFileBinding.GET("/list", handles.ListLabelFileBinding)
|
||||||
labelFileBinding.GET("/get_file_by_label", handles.GetFileByLabel)
|
|
||||||
labelFileBinding.POST("/create", handles.CreateLabelFileBinDing)
|
labelFileBinding.POST("/create", handles.CreateLabelFileBinDing)
|
||||||
|
labelFileBinding.POST("/create_batch", handles.CreateLabelFileBinDingBatch)
|
||||||
labelFileBinding.POST("/delete", handles.DelLabelByFileName)
|
labelFileBinding.POST("/delete", handles.DelLabelByFileName)
|
||||||
|
labelFileBinding.POST("/restore", handles.RestoreLabelFileBinding)
|
||||||
|
|
||||||
|
session := g.Group("/session")
|
||||||
|
session.GET("/list", handles.ListSessions)
|
||||||
|
session.POST("/evict", handles.EvictSession)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func _fs(g *gin.RouterGroup) {
|
func _fs(g *gin.RouterGroup) {
|
||||||
@@ -216,6 +226,16 @@ func _task(g *gin.RouterGroup) {
|
|||||||
handles.SetupTaskRoute(g)
|
handles.SetupTaskRoute(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _label(g *gin.RouterGroup) {
|
||||||
|
g.GET("/list", handles.ListLabel)
|
||||||
|
g.GET("/get", handles.GetLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _labelFileBinding(g *gin.RouterGroup) {
|
||||||
|
g.GET("/get", handles.GetLabelByFileName)
|
||||||
|
g.GET("/get_file_by_label", handles.GetFileByLabel)
|
||||||
|
}
|
||||||
|
|
||||||
func Cors(r *gin.Engine) {
|
func Cors(r *gin.Engine) {
|
||||||
config := cors.DefaultConfig()
|
config := cors.DefaultConfig()
|
||||||
// config.AllowAllOrigins = true
|
// config.AllowAllOrigins = true
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
@@ -12,9 +13,11 @@ import (
|
|||||||
"github.com/alist-org/alist/v3/server/middlewares"
|
"github.com/alist-org/alist/v3/server/middlewares"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
|
"github.com/alist-org/alist/v3/internal/device"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
"github.com/alist-org/alist/v3/internal/setting"
|
"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/common"
|
||||||
"github.com/alist-org/alist/v3/server/webdav"
|
"github.com/alist-org/alist/v3/server/webdav"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -69,6 +72,13 @@ func WebDAVAuth(c *gin.Context) {
|
|||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s", admin.ID, c.ClientIP()))
|
||||||
|
if err := device.Handle(admin.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
|
||||||
|
c.Status(http.StatusForbidden)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set("device_key", key)
|
||||||
c.Set("user", admin)
|
c.Set("user", admin)
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
@@ -95,6 +105,9 @@ func WebDAVAuth(c *gin.Context) {
|
|||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if roles, err := op.GetRolesByUserID(user.ID); err == nil {
|
||||||
|
user.RolesDetail = roles
|
||||||
|
}
|
||||||
reqPath := c.Param("path")
|
reqPath := c.Param("path")
|
||||||
if reqPath == "" {
|
if reqPath == "" {
|
||||||
reqPath = "/"
|
reqPath = "/"
|
||||||
@@ -107,7 +120,8 @@ func WebDAVAuth(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
perm := common.MergeRolePermissions(user, reqPath)
|
perm := common.MergeRolePermissions(user, reqPath)
|
||||||
if user.Disabled || !common.HasPermission(perm, common.PermWebdavRead) {
|
webdavRead := common.HasPermission(perm, common.PermWebdavRead)
|
||||||
|
if user.Disabled || (!webdavRead && (c.Request.Method != "PROPFIND" || !common.HasChildPermission(user, reqPath, common.PermWebdavRead))) {
|
||||||
if c.Request.Method == "OPTIONS" {
|
if c.Request.Method == "OPTIONS" {
|
||||||
c.Set("user", guest)
|
c.Set("user", guest)
|
||||||
c.Next()
|
c.Next()
|
||||||
@@ -142,6 +156,13 @@ func WebDAVAuth(c *gin.Context) {
|
|||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s", user.ID, c.ClientIP()))
|
||||||
|
if err := device.Handle(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
|
||||||
|
c.Status(http.StatusForbidden)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set("device_key", key)
|
||||||
c.Set("user", user)
|
c.Set("user", user)
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn
|
|||||||
depth = 0
|
depth = 0
|
||||||
}
|
}
|
||||||
meta, _ := op.GetNearestMeta(name)
|
meta, _ := op.GetNearestMeta(name)
|
||||||
|
user := ctx.Value("user").(*model.User)
|
||||||
// Read directory names.
|
// Read directory names.
|
||||||
objs, err := fs.List(context.WithValue(ctx, "meta", meta), name, &fs.ListArgs{})
|
objs, err := fs.List(context.WithValue(ctx, "meta", meta), name, &fs.ListArgs{})
|
||||||
//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
|
//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
|
||||||
@@ -108,6 +109,9 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn
|
|||||||
|
|
||||||
for _, fileInfo := range objs {
|
for _, fileInfo := range objs {
|
||||||
filename := path.Join(name, fileInfo.GetName())
|
filename := path.Join(name, fileInfo.GetName())
|
||||||
|
if !common.CanReadPathByRole(user, filename) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
|
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -648,6 +648,98 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
|
|||||||
|
|
||||||
mw := multistatusWriter{w: w}
|
mw := multistatusWriter{w: w}
|
||||||
|
|
||||||
|
if utils.PathEqual(reqPath, user.BasePath) {
|
||||||
|
hasRootPerm := false
|
||||||
|
for _, role := range user.RolesDetail {
|
||||||
|
for _, entry := range role.PermissionScopes {
|
||||||
|
if utils.PathEqual(entry.Path, user.BasePath) {
|
||||||
|
hasRootPerm = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasRootPerm {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasRootPerm {
|
||||||
|
basePaths := model.GetAllBasePathsFromRoles(user)
|
||||||
|
type infoItem struct {
|
||||||
|
path string
|
||||||
|
info model.Obj
|
||||||
|
}
|
||||||
|
infos := []infoItem{{reqPath, fi}}
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for _, p := range basePaths {
|
||||||
|
if !utils.IsSubPath(user.BasePath, p) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rel := strings.TrimPrefix(
|
||||||
|
strings.TrimPrefix(
|
||||||
|
utils.FixAndCleanPath(p),
|
||||||
|
utils.FixAndCleanPath(user.BasePath),
|
||||||
|
),
|
||||||
|
"/",
|
||||||
|
)
|
||||||
|
dir := strings.Split(rel, "/")[0]
|
||||||
|
if dir == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[dir]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[dir] = struct{}{}
|
||||||
|
sp := utils.FixAndCleanPath(path.Join(user.BasePath, dir))
|
||||||
|
info, err := fs.Get(ctx, sp, &fs.GetArgs{})
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
infos = append(infos, infoItem{sp, info})
|
||||||
|
}
|
||||||
|
for _, item := range infos {
|
||||||
|
var pstats []Propstat
|
||||||
|
if pf.Propname != nil {
|
||||||
|
pnames, err := propnames(ctx, h.LockSystem, item.info)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
pstat := Propstat{Status: http.StatusOK}
|
||||||
|
for _, xmlname := range pnames {
|
||||||
|
pstat.Props = append(pstat.Props, Property{XMLName: xmlname})
|
||||||
|
}
|
||||||
|
pstats = append(pstats, pstat)
|
||||||
|
} else if pf.Allprop != nil {
|
||||||
|
pstats, err = allprop(ctx, h.LockSystem, item.info, pf.Prop)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pstats, err = props(ctx, h.LockSystem, item.info, pf.Prop)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rel := strings.TrimPrefix(
|
||||||
|
strings.TrimPrefix(
|
||||||
|
utils.FixAndCleanPath(item.path),
|
||||||
|
utils.FixAndCleanPath(user.BasePath),
|
||||||
|
),
|
||||||
|
"/",
|
||||||
|
)
|
||||||
|
href := utils.EncodePath(path.Join("/", h.Prefix, rel), true)
|
||||||
|
if href != "/" && item.info.IsDir() {
|
||||||
|
href += "/"
|
||||||
|
}
|
||||||
|
if err := mw.write(makePropstatResponse(href, pstats)); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := mw.close(); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
walkFn := func(reqPath string, info model.Obj, err error) error {
|
walkFn := func(reqPath string, info model.Obj, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -671,7 +763,14 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
href := path.Join(h.Prefix, strings.TrimPrefix(reqPath, user.BasePath))
|
rel := strings.TrimPrefix(
|
||||||
|
strings.TrimPrefix(
|
||||||
|
utils.FixAndCleanPath(reqPath),
|
||||||
|
utils.FixAndCleanPath(user.BasePath),
|
||||||
|
),
|
||||||
|
"/",
|
||||||
|
)
|
||||||
|
href := utils.EncodePath(path.Join("/", h.Prefix, rel), true)
|
||||||
if href != "/" && info.IsDir() {
|
if href != "/" && info.IsDir() {
|
||||||
href += "/"
|
href += "/"
|
||||||
}
|
}
|
||||||
@@ -734,7 +833,7 @@ func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (statu
|
|||||||
|
|
||||||
func makePropstatResponse(href string, pstats []Propstat) *response {
|
func makePropstatResponse(href string, pstats []Propstat) *response {
|
||||||
resp := response{
|
resp := response{
|
||||||
Href: []string{(&url.URL{Path: href}).EscapedPath()},
|
Href: []string{href},
|
||||||
Propstat: make([]propstat, 0, len(pstats)),
|
Propstat: make([]propstat, 0, len(pstats)),
|
||||||
}
|
}
|
||||||
for _, p := range pstats {
|
for _, p := range pstats {
|
||||||
|
|||||||
Reference in New Issue
Block a user