mirror of
https://github.com/AlistGo/alist.git
synced 2025-11-25 19:37:41 +08:00
Compare commits
1 Commits
v3.52.0
...
fix/docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbf26cd222 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -25,8 +25,6 @@ jobs:
|
||||
- android-arm64
|
||||
name: Build
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org,direct
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
|
||||
@@ -121,6 +121,8 @@ https://alistgo.com/guide/sponsor.html
|
||||
### Special sponsors
|
||||
|
||||
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
|
||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
||||
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
@@ -118,6 +118,8 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
|
||||
### 特别赞助
|
||||
|
||||
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器,iPhone,iPad,Mac,Apple TV全平台支持。
|
||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
|
||||
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||
|
||||
## 贡献者
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@ https://alistgo.com/guide/sponsor.html
|
||||
### スペシャルスポンサー
|
||||
|
||||
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
|
||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
||||
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||
|
||||
## コントリビューター
|
||||
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
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)
|
||||
@@ -1,33 +0,0 @@
|
||||
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{}
|
||||
})
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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 ""
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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,7 +1,6 @@
|
||||
package alist_v3
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
@@ -73,15 +72,15 @@ type LoginResp struct {
|
||||
}
|
||||
|
||||
type MeResp struct {
|
||||
Id int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
BasePath string `json:"base_path"`
|
||||
Role IntSlice `json:"role"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Permission int `json:"permission"`
|
||||
SsoId string `json:"sso_id"`
|
||||
Otp bool `json:"otp"`
|
||||
Id int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
BasePath string `json:"base_path"`
|
||||
Role []int `json:"role"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Permission int `json:"permission"`
|
||||
SsoId string `json:"sso_id"`
|
||||
Otp bool `json:"otp"`
|
||||
}
|
||||
|
||||
type ArchiveMetaReq struct {
|
||||
@@ -169,17 +168,3 @@ type DecompressReq struct {
|
||||
PutIntoNewDir bool `json:"put_into_new_dir"`
|
||||
SrcDir string `json:"src_dir"`
|
||||
}
|
||||
|
||||
type IntSlice []int
|
||||
|
||||
func (s *IntSlice) UnmarshalJSON(data []byte) error {
|
||||
if len(data) > 0 && data[0] == '[' {
|
||||
return json.Unmarshal(data, (*[]int)(s))
|
||||
}
|
||||
var single int
|
||||
if err := json.Unmarshal(data, &single); err != nil {
|
||||
return err
|
||||
}
|
||||
*s = []int{single}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type Addition struct {
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
|
||||
OauthTokenURL string `json:"oauth_token_url" default:"https://api.alistgo.com/alist/ali_open/token"`
|
||||
OauthTokenURL string `json:"oauth_token_url" default:"https://api.nn.ci/alist/ali_open/token"`
|
||||
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
|
||||
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
|
||||
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/115_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123_link"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123_open"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/139"
|
||||
_ "github.com/alist-org/alist/v3/drivers/189"
|
||||
|
||||
@@ -11,8 +11,8 @@ type Addition struct {
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
DownloadAPI string `json:"download_api" type:"select" options:"official,crack,crack_video" default:"official"`
|
||||
ClientID string `json:"client_id" required:"true" default:"hq9yQ9w9kR4YHj1kyYafLygVocobh7Sf"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"YH2VpZcFJHYNnV6vLfHQXDBhcE7ZChyE"`
|
||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
||||
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
||||
AccessToken string
|
||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||
|
||||
@@ -94,7 +94,6 @@ func RemoveJSComment(data string) string {
|
||||
}
|
||||
if inComment && v == '*' && i+1 < len(data) && data[i+1] == '/' {
|
||||
inComment = false
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if v == '/' && i+1 < len(data) {
|
||||
@@ -109,9 +108,6 @@ func RemoveJSComment(data string) string {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if inComment || inSingleLineComment {
|
||||
continue
|
||||
}
|
||||
result.WriteByte(v)
|
||||
}
|
||||
|
||||
|
||||
13
go.mod
13
go.mod
@@ -3,12 +3,10 @@ module github.com/alist-org/alist/v3
|
||||
go 1.23.4
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
|
||||
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0
|
||||
github.com/KirCute/sftpd-alist v0.0.12
|
||||
github.com/ProtonMail/go-crypto v1.0.0
|
||||
github.com/SheltonZhu/115driver v1.1.2
|
||||
github.com/SheltonZhu/115driver v1.0.34
|
||||
github.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21
|
||||
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
|
||||
github.com/alist-org/gofakes3 v0.0.7
|
||||
@@ -81,7 +79,11 @@ require (
|
||||
gorm.io/gorm v1.25.11
|
||||
)
|
||||
|
||||
require github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||
@@ -107,6 +109,7 @@ require (
|
||||
github.com/ipfs/boxo v0.12.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78
|
||||
@@ -265,4 +268,4 @@ require (
|
||||
lukechampine.com/blake3 v1.1.7 // indirect
|
||||
)
|
||||
|
||||
replace github.com/SheltonZhu/115driver => github.com/okatu-loli/115driver v1.1.2
|
||||
// replace github.com/xhofe/115-sdk-go => ../../xhofe/115-sdk-go
|
||||
|
||||
17
go.sum
17
go.sum
@@ -21,16 +21,10 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
@@ -46,6 +40,8 @@ github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4
|
||||
github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
|
||||
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
|
||||
github.com/SheltonZhu/115driver v1.0.34 h1:zhMLp4vgq7GksqvSxQQDOVfK6EOHldQl4b2n8tnZ+EE=
|
||||
github.com/SheltonZhu/115driver v1.0.34/go.mod h1:rKvNd4Y4OkXv1TMbr/SKjGdcvMQxh6AW5Tw9w0CJb7E=
|
||||
github.com/Unknwon/goconfig v1.0.0 h1:9IAu/BYbSLQi8puFjUQApZTxIHqSwrj5d8vpP8vTq4A=
|
||||
github.com/Unknwon/goconfig v1.0.0/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw=
|
||||
github.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21 h1:h6q5E9aMBhhdqouW81LozVPI1I+Pu6IxL2EKpfm5OjY=
|
||||
@@ -176,6 +172,7 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg=
|
||||
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -401,8 +398,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.3.1 h1:DLQQEgHUAGZB6RVlceB1f6A94O206exxW2RIMH+gMUc=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.3.1/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
@@ -488,8 +483,6 @@ github.com/ncw/swift/v2 v2.0.3 h1:8R9dmgFIWs+RiVlisCEfiQiik1hjuR0JnOkLxaP9ihg=
|
||||
github.com/ncw/swift/v2 v2.0.3/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78 h1:MYzLheyVx1tJVDqfu3YnN4jtnyALNzLvwl+f58TcvQY=
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
|
||||
github.com/okatu-loli/115driver v1.1.2 h1:XZT3r/51SZRQGzre2IeA+0/k4T1FneqArdhE4Wd600Q=
|
||||
github.com/okatu-loli/115driver v1.1.2/go.mod h1:rKvNd4Y4OkXv1TMbr/SKjGdcvMQxh6AW5Tw9w0CJb7E=
|
||||
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
||||
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
||||
@@ -499,8 +492,6 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -748,6 +739,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
||||
@@ -91,7 +91,6 @@ func InitialSettings() []model.SettingItem {
|
||||
} else {
|
||||
token = random.Token()
|
||||
}
|
||||
defaultRoleID := strconv.Itoa(model.GUEST)
|
||||
initialSettingItems = []model.SettingItem{
|
||||
// site settings
|
||||
{Key: conf.VERSION, Value: conf.Version, Type: conf.TypeString, Group: model.SITE, Flag: model.READONLY},
|
||||
@@ -104,10 +103,6 @@ func InitialSettings() []model.SettingItem {
|
||||
{Key: conf.AllowIndexed, Value: "false", Type: conf.TypeBool, Group: model.SITE},
|
||||
{Key: conf.AllowMounted, Value: "true", Type: conf.TypeBool, Group: model.SITE},
|
||||
{Key: conf.RobotsTxt, Value: "User-agent: *\nAllow: /", Type: conf.TypeText, Group: model.SITE},
|
||||
{Key: conf.AllowRegister, Value: "false", Type: conf.TypeBool, Group: model.SITE},
|
||||
{Key: conf.DefaultRole, Value: defaultRoleID, Type: conf.TypeSelect, Group: model.SITE},
|
||||
// newui settings
|
||||
{Key: conf.UseNewui, Value: "false", Type: conf.TypeBool, Group: model.SITE},
|
||||
// style settings
|
||||
{Key: conf.Logo, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeText, Group: model.STYLE},
|
||||
{Key: conf.Favicon, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeString, Group: model.STYLE},
|
||||
@@ -165,9 +160,6 @@ func InitialSettings() []model.SettingItem {
|
||||
{Key: conf.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL},
|
||||
{Key: conf.IgnoreDirectLinkParams, Value: "sign,alist_ts", Type: conf.TypeString, Group: model.GLOBAL},
|
||||
{Key: conf.WebauthnLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
|
||||
{Key: conf.MaxDevices, Value: "0", Type: conf.TypeNumber, Group: model.GLOBAL},
|
||||
{Key: conf.DeviceEvictPolicy, Value: "deny", Type: conf.TypeSelect, Options: "deny,evict_oldest", Group: model.GLOBAL},
|
||||
{Key: conf.DeviceSessionTTL, Value: "86400", Type: conf.TypeNumber, Group: model.GLOBAL},
|
||||
|
||||
// single settings
|
||||
{Key: conf.Token, Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE},
|
||||
|
||||
@@ -14,14 +14,10 @@ import (
|
||||
|
||||
func init() {
|
||||
formatter := logrus.TextFormatter{
|
||||
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
|
||||
ForceColors: true,
|
||||
EnvironmentOverrideColors: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
FullTimestamp: true,
|
||||
}
|
||||
logrus.SetFormatter(&formatter)
|
||||
utils.Log.SetFormatter(&formatter)
|
||||
|
||||
@@ -10,15 +10,12 @@ const (
|
||||
|
||||
const (
|
||||
// site
|
||||
VERSION = "version"
|
||||
SiteTitle = "site_title"
|
||||
Announcement = "announcement"
|
||||
AllowIndexed = "allow_indexed"
|
||||
AllowMounted = "allow_mounted"
|
||||
RobotsTxt = "robots_txt"
|
||||
AllowRegister = "allow_register"
|
||||
DefaultRole = "default_role"
|
||||
UseNewui = "use_newui"
|
||||
VERSION = "version"
|
||||
SiteTitle = "site_title"
|
||||
Announcement = "announcement"
|
||||
AllowIndexed = "allow_indexed"
|
||||
AllowMounted = "allow_mounted"
|
||||
RobotsTxt = "robots_txt"
|
||||
|
||||
Logo = "logo"
|
||||
Favicon = "favicon"
|
||||
@@ -48,9 +45,6 @@ const (
|
||||
ForwardDirectLinkParams = "forward_direct_link_params"
|
||||
IgnoreDirectLinkParams = "ignore_direct_link_params"
|
||||
WebauthnLoginEnabled = "webauthn_login_enabled"
|
||||
MaxDevices = "max_devices"
|
||||
DeviceEvictPolicy = "device_evict_policy"
|
||||
DeviceSessionTTL = "device_session_ttl"
|
||||
|
||||
// index
|
||||
SearchIndex = "search_index"
|
||||
|
||||
@@ -12,7 +12,7 @@ var db *gorm.DB
|
||||
|
||||
func Init(d *gorm.DB) {
|
||||
db = d
|
||||
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.Role), new(model.Label), new(model.LabelFileBinding), new(model.ObjFile), new(model.Session))
|
||||
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))
|
||||
if err != nil {
|
||||
log.Fatalf("failed migrate database: %s", err.Error())
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetLabelIds Get all label_ids from database order by file_name
|
||||
func GetLabelIds(userId uint, fileName string) ([]uint, error) {
|
||||
//fmt.Printf(">>> [GetLabelIds] userId: %d, fileName: %s\n", userId, fileName)
|
||||
labelFileBinDingDB := db.Model(&model.LabelFileBinding{})
|
||||
labelFileBinDingDB := db.Model(&model.LabelFileBinDing{})
|
||||
var labelIds []uint
|
||||
if err := labelFileBinDingDB.Where("file_name = ?", fileName).Where("user_id = ?", userId).Pluck("label_id", &labelIds).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
@@ -21,7 +18,7 @@ func GetLabelIds(userId uint, fileName string) ([]uint, error) {
|
||||
}
|
||||
|
||||
func CreateLabelFileBinDing(fileName string, labelId, userId uint) error {
|
||||
var labelFileBinDing model.LabelFileBinding
|
||||
var labelFileBinDing model.LabelFileBinDing
|
||||
labelFileBinDing.UserId = userId
|
||||
labelFileBinDing.LabelId = labelId
|
||||
labelFileBinDing.FileName = fileName
|
||||
@@ -35,7 +32,7 @@ func CreateLabelFileBinDing(fileName string, labelId, userId uint) error {
|
||||
|
||||
// GetLabelFileBinDingByLabelIdExists Get Label by label_id, used to del label usually
|
||||
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)
|
||||
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound)
|
||||
return exists
|
||||
@@ -43,150 +40,17 @@ func GetLabelFileBinDingByLabelIdExists(labelId, userId uint) bool {
|
||||
|
||||
// DelLabelFileBinDingByFileName used to del usually
|
||||
func DelLabelFileBinDingByFileName(userId uint, fileName string) error {
|
||||
return errors.WithStack(db.Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinding{}).Error)
|
||||
return errors.WithStack(db.Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinDing{}).Error)
|
||||
}
|
||||
|
||||
// DelLabelFileBinDingById used to del usually
|
||||
func DelLabelFileBinDingById(labelId, userId uint, fileName string) error {
|
||||
return errors.WithStack(db.Where("label_id = ?", labelId).Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinding{}).Error)
|
||||
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 {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetLabelBindingsByFileNamesPublic(fileNames []string) (map[string][]uint, error) {
|
||||
var binds []model.LabelFileBinding
|
||||
if err := db.Where("file_name IN ?", fileNames).Find(&binds).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
out := make(map[string][]uint, len(fileNames))
|
||||
seen := make(map[string]struct{}, len(binds))
|
||||
for _, b := range binds {
|
||||
key := fmt.Sprintf("%s-%d", b.FileName, b.LabelId)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out[b.FileName] = append(out[b.FileName], b.LabelId)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func GetLabelsByFileNamesPublic(fileNames []string) (map[string][]model.Label, error) {
|
||||
bindMap, err := GetLabelBindingsByFileNamesPublic(fileNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idSet := make(map[uint]struct{})
|
||||
for _, ids := range bindMap {
|
||||
for _, id := range ids {
|
||||
idSet[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(idSet) == 0 {
|
||||
return make(map[string][]model.Label, 0), nil
|
||||
}
|
||||
allIDs := make([]uint, 0, len(idSet))
|
||||
for id := range idSet {
|
||||
allIDs = append(allIDs, id)
|
||||
}
|
||||
labels, err := GetLabelByIds(allIDs) // 你已有的函数
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labelByID := make(map[uint]model.Label, len(labels))
|
||||
for _, l := range labels {
|
||||
labelByID[l.ID] = l
|
||||
}
|
||||
|
||||
out := make(map[string][]model.Label, len(bindMap))
|
||||
for fname, ids := range bindMap {
|
||||
for _, id := range ids {
|
||||
if lab, ok := labelByID[id]; ok {
|
||||
out[fname] = append(out[fname], lab)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ListLabelFileBinDing(userId uint, labelIDs []uint, fileName string, page, pageSize int) ([]model.LabelFileBinding, int64, error) {
|
||||
q := db.Model(&model.LabelFileBinding{}).Where("user_id = ?", userId)
|
||||
|
||||
if len(labelIDs) > 0 {
|
||||
q = q.Where("label_id IN ?", labelIDs)
|
||||
}
|
||||
if fileName != "" {
|
||||
q = q.Where("file_name LIKE ?", "%"+fileName+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
var rows []model.LabelFileBinding
|
||||
if err := q.
|
||||
Order("id DESC").
|
||||
Offset((page - 1) * pageSize).
|
||||
Limit(pageSize).
|
||||
Find(&rows).Error; err != nil {
|
||||
return nil, 0, errors.WithStack(err)
|
||||
}
|
||||
return rows, total, nil
|
||||
}
|
||||
|
||||
func RestoreLabelFileBindings(bindings []model.LabelFileBinding, keepIDs bool, override bool) error {
|
||||
if len(bindings) == 0 {
|
||||
return nil
|
||||
}
|
||||
tx := db.Begin()
|
||||
|
||||
if override {
|
||||
type key struct {
|
||||
uid uint
|
||||
name string
|
||||
}
|
||||
toDel := make(map[key]struct{}, len(bindings))
|
||||
for i := range bindings {
|
||||
k := key{uid: bindings[i].UserId, name: bindings[i].FileName}
|
||||
toDel[k] = struct{}{}
|
||||
}
|
||||
for k := range toDel {
|
||||
if err := tx.Where("user_id = ? AND file_name = ?", k.uid, k.name).
|
||||
Delete(&model.LabelFileBinding{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range bindings {
|
||||
b := bindings[i]
|
||||
if !keepIDs {
|
||||
b.ID = 0
|
||||
}
|
||||
if b.CreateTime.IsZero() {
|
||||
b.CreateTime = time.Now()
|
||||
}
|
||||
if override {
|
||||
if err := tx.Create(&b).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&b).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.WithStack(tx.Commit().Error)
|
||||
}
|
||||
|
||||
@@ -35,27 +35,11 @@ func GetRoles(pageIndex, pageSize int) (roles []model.Role, count int64, err err
|
||||
}
|
||||
|
||||
func CreateRole(r *model.Role) error {
|
||||
if err := db.Create(r).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if r.Default {
|
||||
if err := db.Model(&model.Role{}).Where("id <> ?", r.ID).Update("default", false).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return errors.WithStack(db.Create(r).Error)
|
||||
}
|
||||
|
||||
func UpdateRole(r *model.Role) error {
|
||||
if err := db.Save(r).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if r.Default {
|
||||
if err := db.Model(&model.Role{}).Where("id <> ?", r.ID).Update("default", false).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return errors.WithStack(db.Save(r).Error)
|
||||
}
|
||||
|
||||
func DeleteRole(id uint) error {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
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,14 +2,12 @@ package db
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -26,20 +24,6 @@ func GetUserByRole(role int) (*model.User, error) {
|
||||
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) {
|
||||
user := model.User{Username: username}
|
||||
if err := db.Where(user).First(&user).Error; err != nil {
|
||||
@@ -124,29 +108,25 @@ func RemoveAuthn(u *model.User, id string) error {
|
||||
return UpdateAuthn(u.ID, string(res))
|
||||
}
|
||||
|
||||
func UpdateUserBasePathPrefix(oldPath, newPath string, usersOpt ...[]model.User) ([]string, error) {
|
||||
func UpdateUserBasePathPrefix(oldPath, newPath string) ([]string, error) {
|
||||
var users []model.User
|
||||
var modifiedUsernames []string
|
||||
|
||||
oldPathClean := path.Clean(oldPath)
|
||||
|
||||
if len(usersOpt) > 0 {
|
||||
users = usersOpt[0]
|
||||
} else {
|
||||
if err := db.Find(&users).Error; err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to load users")
|
||||
}
|
||||
if err := db.Find(&users).Error; err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to load users")
|
||||
}
|
||||
|
||||
oldPathClean := path.Clean(oldPath)
|
||||
|
||||
for _, user := range users {
|
||||
basePath := path.Clean(user.BasePath)
|
||||
updated := false
|
||||
|
||||
if basePath == oldPathClean {
|
||||
user.BasePath = path.Clean(newPath)
|
||||
user.BasePath = newPath
|
||||
updated = true
|
||||
} else if strings.HasPrefix(basePath, oldPathClean+"/") {
|
||||
user.BasePath = path.Clean(newPath + basePath[len(oldPathClean):])
|
||||
user.BasePath = newPath + basePath[len(oldPathClean):]
|
||||
updated = true
|
||||
}
|
||||
|
||||
@@ -160,13 +140,3 @@ func UpdateUserBasePathPrefix(oldPath, newPath string, usersOpt ...[]model.User)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
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())
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package errs
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
TooManyDevices = errors.New("too many active devices")
|
||||
SessionInactive = errors.New("session inactive")
|
||||
)
|
||||
@@ -4,5 +4,4 @@ import "errors"
|
||||
|
||||
var (
|
||||
EmptyToken = errors.New("empty token")
|
||||
LinkIsDir = errors.New("link is dir")
|
||||
)
|
||||
|
||||
@@ -3,5 +3,5 @@ package errs
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrChangeDefaultRole = errors.New("cannot modify admin role")
|
||||
ErrChangeDefaultRole = errors.New("cannot modify admin or guest role")
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package model
|
||||
|
||||
import "time"
|
||||
|
||||
type LabelFileBinding struct {
|
||||
type LabelFileBinDing struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"` // unique key
|
||||
UserId uint `json:"user_id"` // use to user_id
|
||||
LabelId uint `json:"label_id"` // use to label_id
|
||||
|
||||
@@ -55,21 +55,6 @@ type FileStreamer interface {
|
||||
|
||||
type UpdateProgress func(percentage float64)
|
||||
|
||||
// Reference implementation from OpenListTeam:
|
||||
// https://github.com/OpenListTeam/OpenList/blob/a703b736c9346c483bae56905a39bc07bf781cff/internal/model/obj.go#L58
|
||||
func UpdateProgressWithRange(inner UpdateProgress, start, end float64) UpdateProgress {
|
||||
return func(p float64) {
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
if p > 100 {
|
||||
p = 100
|
||||
}
|
||||
scaled := start + (end-start)*(p/100.0)
|
||||
inner(scaled)
|
||||
}
|
||||
}
|
||||
|
||||
type URL interface {
|
||||
URL() string
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ type Role struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"unique" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Default bool `json:"default" gorm:"default:false"`
|
||||
// PermissionScopes stores structured permission list and is ignored by gorm.
|
||||
PermissionScopes []PermissionEntry `json:"permission_scopes" gorm:"-"`
|
||||
// RawPermission is the JSON representation of PermissionScopes stored in DB.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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,28 +145,13 @@ func (u *User) CheckPathLimit() bool {
|
||||
}
|
||||
|
||||
func (u *User) JoinPath(reqPath string) (string, error) {
|
||||
if reqPath == "/" {
|
||||
return utils.FixAndCleanPath(u.BasePath), nil
|
||||
}
|
||||
path, err := utils.JoinBasePath(u.BasePath, reqPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if path != "/" && u.CheckPathLimit() {
|
||||
basePaths := GetAllBasePathsFromRoles(u)
|
||||
match := false
|
||||
for _, base := range basePaths {
|
||||
if utils.IsSubPath(base, path) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return "", errs.PermissionDenied
|
||||
}
|
||||
if u.CheckPathLimit() && !utils.IsSubPath(u.BasePath, path) {
|
||||
return "", errs.PermissionDenied
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
@@ -208,33 +193,3 @@ func (u *User) WebAuthnCredentials() []webauthn.Credential {
|
||||
func (u *User) WebAuthnIcon() string {
|
||||
return "https://alistgo.com/logo.svg"
|
||||
}
|
||||
|
||||
// FetchRole is used to load role details by id. It should be set by the op package
|
||||
// to avoid an import cycle between model and op.
|
||||
var FetchRole func(uint) (*Role, error)
|
||||
|
||||
// GetAllBasePathsFromRoles returns all permission paths from user's roles
|
||||
func GetAllBasePathsFromRoles(u *User) []string {
|
||||
basePaths := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for _, rid := range u.Role {
|
||||
if FetchRole == nil {
|
||||
continue
|
||||
}
|
||||
role, err := FetchRole(uint(rid))
|
||||
if err != nil || role == nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if entry.Path == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[entry.Path]; !ok {
|
||||
basePaths = append(basePaths, entry.Path)
|
||||
seen[entry.Path] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return basePaths
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package op
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
@@ -83,18 +82,6 @@ var settingItemHooks = map[string]SettingItemHook{
|
||||
conf.SlicesMap[conf.IgnoreDirectLinkParams] = strings.Split(item.Value, ",")
|
||||
return nil
|
||||
},
|
||||
conf.DefaultRole: func(item *model.SettingItem) error {
|
||||
v := strings.TrimSpace(item.Value)
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
id, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
_, err = GetRole(uint(id))
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
func RegisterSettingItemHook(key string, hook SettingItemHook) {
|
||||
|
||||
@@ -23,7 +23,6 @@ type CreateLabelFileBinDingReq struct {
|
||||
Type int `json:"type"`
|
||||
HashInfoStr string `json:"hashinfo"`
|
||||
LabelIds string `json:"label_ids"`
|
||||
LabelIDs []uint64 `json:"labelIdList"`
|
||||
}
|
||||
|
||||
type ObjLabelResp struct {
|
||||
@@ -55,29 +54,23 @@ func GetLabelByFileName(userId uint, fileName string) ([]model.Label, error) {
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
func GetLabelsByFileNamesPublic(fileNames []string) (map[string][]model.Label, error) {
|
||||
return db.GetLabelsByFileNamesPublic(fileNames)
|
||||
}
|
||||
|
||||
func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
|
||||
if err := db.DelLabelFileBinDingByFileName(userId, req.Name); err != nil {
|
||||
return errors.WithMessage(err, "failed del label_file_bin_ding in database")
|
||||
}
|
||||
|
||||
ids, err := collectLabelIDs(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
if req.LabelIds == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if err = db.CreateLabelFileBinDing(req.Name, uint(id), userId); err != nil {
|
||||
labelMap := strings.Split(req.LabelIds, ",")
|
||||
for _, value := range labelMap {
|
||||
labelId, err := strconv.ParseUint(value, 10, 64)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
if !db.GetFileByNameExists(req.Name) {
|
||||
objFile := model.ObjFile{
|
||||
Id: req.Id,
|
||||
@@ -93,7 +86,8 @@ func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
|
||||
Type: req.Type,
|
||||
HashInfoStr: req.HashInfoStr,
|
||||
}
|
||||
if err := db.CreateObjFile(objFile); err != nil {
|
||||
err := db.CreateObjFile(objFile)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed file in database")
|
||||
}
|
||||
}
|
||||
@@ -103,7 +97,7 @@ func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
|
||||
func GetFileByLabel(userId uint, labelId string) (result []ObjLabelResp, err error) {
|
||||
labelMap := strings.Split(labelId, ",")
|
||||
var labelIds []uint
|
||||
var labelsFile []model.LabelFileBinding
|
||||
var labelsFile []model.LabelFileBinDing
|
||||
var labels []model.Label
|
||||
var labelsFileMap = make(map[string][]model.Label)
|
||||
var labelsMap = make(map[uint]model.Label)
|
||||
@@ -163,33 +157,3 @@ func StringSliceToUintSlice(strSlice []string) ([]uint, error) {
|
||||
}
|
||||
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,11 +2,10 @@ package op
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/go-cache"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
@@ -17,10 +16,6 @@ import (
|
||||
var roleCache = cache.NewMemCache[*model.Role](cache.WithShards[*model.Role](2))
|
||||
var roleG singleflight.Group[*model.Role]
|
||||
|
||||
func init() {
|
||||
model.FetchRole = GetRole
|
||||
}
|
||||
|
||||
func GetRole(id uint) (*model.Role, error) {
|
||||
key := fmt.Sprint(id)
|
||||
if r, ok := roleCache.Get(key); ok {
|
||||
@@ -52,23 +47,6 @@ func GetRoleByName(name string) (*model.Role, error) {
|
||||
return r, err
|
||||
}
|
||||
|
||||
func GetDefaultRoleID() int {
|
||||
item, err := GetSettingItemByKey(conf.DefaultRole)
|
||||
if err == nil && item != nil && item.Value != "" {
|
||||
if id, err := strconv.Atoi(item.Value); err == nil && id != 0 {
|
||||
return id
|
||||
}
|
||||
if r, err := db.GetRoleByName(item.Value); err == nil {
|
||||
return int(r.ID)
|
||||
}
|
||||
}
|
||||
var r model.Role
|
||||
if err := db.GetDb().Where("`default` = ?", true).First(&r).Error; err == nil {
|
||||
return int(r.ID)
|
||||
}
|
||||
return int(model.GUEST)
|
||||
}
|
||||
|
||||
func GetRolesByUserID(userID uint) ([]model.Role, error) {
|
||||
user, err := GetUserById(userID)
|
||||
if err != nil {
|
||||
@@ -111,21 +89,7 @@ func CreateRole(r *model.Role) error {
|
||||
}
|
||||
roleCache.Del(fmt.Sprint(r.ID))
|
||||
roleCache.Del(r.Name)
|
||||
if err := db.CreateRole(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Default {
|
||||
roleCache.Clear()
|
||||
item, err := GetSettingItemByKey(conf.DefaultRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Value = strconv.Itoa(int(r.ID))
|
||||
if err := SaveSettingItem(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return db.CreateRole(r)
|
||||
}
|
||||
|
||||
func UpdateRole(r *model.Role) error {
|
||||
@@ -133,52 +97,29 @@ func UpdateRole(r *model.Role) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch old.Name {
|
||||
case "admin":
|
||||
if old.Name == "admin" || old.Name == "guest" {
|
||||
return errs.ErrChangeDefaultRole
|
||||
case "guest":
|
||||
r.Name = "guest"
|
||||
}
|
||||
for i := range r.PermissionScopes {
|
||||
r.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path)
|
||||
}
|
||||
//if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 &&
|
||||
// old.PermissionScopes[0].Path != r.PermissionScopes[0].Path {
|
||||
//
|
||||
// oldPath := old.PermissionScopes[0].Path
|
||||
// newPath := r.PermissionScopes[0].Path
|
||||
//
|
||||
// users, err := db.GetUsersByRole(int(r.ID))
|
||||
// if err != nil {
|
||||
// return errors.WithMessage(err, "failed to get users by role")
|
||||
// }
|
||||
//
|
||||
// modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath, users)
|
||||
// if err != nil {
|
||||
// return errors.WithMessage(err, "failed to update user base path when role updated")
|
||||
// }
|
||||
//
|
||||
// for _, name := range modifiedUsernames {
|
||||
// userCache.Del(name)
|
||||
// }
|
||||
//}
|
||||
if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 &&
|
||||
old.PermissionScopes[0].Path != r.PermissionScopes[0].Path {
|
||||
|
||||
oldPath := old.PermissionScopes[0].Path
|
||||
newPath := r.PermissionScopes[0].Path
|
||||
modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to update user base path when role updated")
|
||||
}
|
||||
|
||||
for _, name := range modifiedUsernames {
|
||||
userCache.Del(name)
|
||||
}
|
||||
}
|
||||
roleCache.Del(fmt.Sprint(r.ID))
|
||||
roleCache.Del(r.Name)
|
||||
if err := db.UpdateRole(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Default {
|
||||
roleCache.Clear()
|
||||
item, err := GetSettingItemByKey(conf.DefaultRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Value = strconv.Itoa(int(r.ID))
|
||||
if err := SaveSettingItem(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return db.UpdateRole(r)
|
||||
}
|
||||
|
||||
func DeleteRole(id uint) error {
|
||||
|
||||
@@ -232,20 +232,12 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
|
||||
roleCache.Del(fmt.Sprint(id))
|
||||
}
|
||||
|
||||
//modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldStorage.MountPath, storage.MountPath)
|
||||
//if err != nil {
|
||||
// return errors.WithMessage(err, "failed to update user base path")
|
||||
//}
|
||||
for _, id := range modifiedRoleIDs {
|
||||
roleCache.Del(fmt.Sprint(id))
|
||||
|
||||
users, err := db.GetUsersByRole(int(id))
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to get users by role")
|
||||
}
|
||||
for _, user := range users {
|
||||
userCache.Del(user.Username)
|
||||
}
|
||||
modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldStorage.MountPath, storage.MountPath)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to update user base path")
|
||||
}
|
||||
for _, name := range modifiedUsernames {
|
||||
userCache.Del(name)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -50,10 +50,6 @@ func GetUserByRole(role int) (*model.User, error) {
|
||||
return db.GetUserByRole(role)
|
||||
}
|
||||
|
||||
func GetUsersByRole(role int) ([]model.User, error) {
|
||||
return db.GetUsersByRole(role)
|
||||
}
|
||||
|
||||
func GetUserByName(username string) (*model.User, error) {
|
||||
if username == "" {
|
||||
return nil, errs.EmptyUsername
|
||||
@@ -128,17 +124,17 @@ func UpdateUser(u *model.User) error {
|
||||
}
|
||||
userCache.Del(old.Username)
|
||||
u.BasePath = utils.FixAndCleanPath(u.BasePath)
|
||||
//if len(u.Role) > 0 {
|
||||
// roles, err := GetRolesByUserID(u.ID)
|
||||
// if err == nil {
|
||||
// for _, role := range roles {
|
||||
// if len(role.PermissionScopes) > 0 {
|
||||
// u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
if len(u.Role) > 0 {
|
||||
roles, err := GetRolesByUserID(u.ID)
|
||||
if err == nil {
|
||||
for _, role := range roles {
|
||||
if len(role.PermissionScopes) > 0 {
|
||||
u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return db.UpdateUser(u)
|
||||
}
|
||||
|
||||
@@ -169,11 +165,3 @@ func DelUserCache(username string) error {
|
||||
userCache.Del(username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func CountEnabledAdminsExcluding(userID uint) (int64, error) {
|
||||
adminRole, err := GetRoleByName("admin")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return db.CountUsersByRoleAndEnabledExclude(adminRole.ID, userID)
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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,13 +88,6 @@ func JoinBasePath(basePath, reqPath string) (string, error) {
|
||||
strings.Contains(reqPath, "/../") {
|
||||
return "", errs.RelativePath
|
||||
}
|
||||
|
||||
reqPath = FixAndCleanPath(reqPath)
|
||||
|
||||
if strings.HasPrefix(reqPath, "/") {
|
||||
return reqPath, nil
|
||||
}
|
||||
|
||||
return stdpath.Join(FixAndCleanPath(basePath), FixAndCleanPath(reqPath)), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -43,23 +43,17 @@ func MergeRolePermissions(u *model.User, reqPath string) int32 {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) {
|
||||
for _, entry := range role.PermissionScopes {
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.IsSubPath(entry.Path, reqPath) {
|
||||
perm |= entry.Permission
|
||||
}
|
||||
} else {
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.IsSubPath(entry.Path, reqPath) {
|
||||
perm |= entry.Permission
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return perm
|
||||
}
|
||||
|
||||
func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password string) bool {
|
||||
if !CanReadPathByRole(u, reqPath) {
|
||||
if !canReadPathByRole(u, reqPath) {
|
||||
return false
|
||||
}
|
||||
perm := MergeRolePermissions(u, reqPath)
|
||||
@@ -84,30 +78,7 @@ func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password strin
|
||||
return meta.Password == password
|
||||
}
|
||||
|
||||
func CanReadPathByRole(u *model.User, reqPath string) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) {
|
||||
return len(u.Role) > 0
|
||||
}
|
||||
for _, rid := range u.Role {
|
||||
role, err := op.GetRole(uint(rid))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.PathEqual(entry.Path, reqPath) || utils.IsSubPath(entry.Path, reqPath) || utils.IsSubPath(reqPath, entry.Path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasChildPermission checks whether any child path under reqPath grants the
|
||||
// specified permission bit.
|
||||
func HasChildPermission(u *model.User, reqPath string, bit uint) bool {
|
||||
func canReadPathByRole(u *model.User, reqPath string) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
@@ -117,7 +88,7 @@ func HasChildPermission(u *model.User, reqPath string, bit uint) bool {
|
||||
continue
|
||||
}
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.IsSubPath(reqPath, entry.Path) && HasPermission(entry.Permission, bit) {
|
||||
if utils.IsSubPath(entry.Path, reqPath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -131,7 +102,7 @@ func HasChildPermission(u *model.User, reqPath string, bit uint) bool {
|
||||
func CheckPathLimitWithRoles(u *model.User, reqPath string) bool {
|
||||
perm := MergeRolePermissions(u, reqPath)
|
||||
if HasPermission(perm, PermPathLimit) {
|
||||
return CanReadPathByRole(u, reqPath)
|
||||
return canReadPathByRole(u, reqPath)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -3,22 +3,14 @@ package handles
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/go-cache"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/device"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/internal/session"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pquerna/otp/totp"
|
||||
@@ -87,62 +79,16 @@ func loginHash(c *gin.Context, req *LoginReq) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
clientID := c.GetHeader("Client-Id")
|
||||
if clientID == "" {
|
||||
clientID = c.Query("client_id")
|
||||
}
|
||||
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s",
|
||||
user.ID, clientID))
|
||||
|
||||
if err := device.EnsureActiveOnLogin(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
|
||||
if errors.Is(err, errs.TooManyDevices) {
|
||||
common.ErrorResp(c, err, 403)
|
||||
} else {
|
||||
common.ErrorResp(c, err, 400, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// generate token
|
||||
token, err := common.GenerateToken(user)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, gin.H{"token": token, "device_key": key})
|
||||
common.SuccessResp(c, gin.H{"token": token})
|
||||
loginCache.Del(ip)
|
||||
}
|
||||
|
||||
type RegisterReq struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// Register a new user
|
||||
func Register(c *gin.Context) {
|
||||
if !setting.GetBool(conf.AllowRegister) {
|
||||
common.ErrorStrResp(c, "registration is disabled", 403)
|
||||
return
|
||||
}
|
||||
var req RegisterReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
user := &model.User{
|
||||
Username: req.Username,
|
||||
Role: model.Roles{op.GetDefaultRoleID()},
|
||||
Authn: "[]",
|
||||
}
|
||||
user.SetPassword(req.Password)
|
||||
if err := op.CreateUser(user); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
|
||||
type UserResp struct {
|
||||
model.User
|
||||
Otp bool `json:"otp"`
|
||||
@@ -270,13 +216,6 @@ func Verify2FA(c *gin.Context) {
|
||||
}
|
||||
|
||||
func LogOut(c *gin.Context) {
|
||||
if keyVal, ok := c.Get("device_key"); ok {
|
||||
if err := session.MarkInactive(keyVal.(string)); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
c.Set("session_inactive", true)
|
||||
}
|
||||
err := common.InvalidateToken(c.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
|
||||
@@ -107,21 +107,14 @@ func FsList(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
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)
|
||||
total, objs := pagination(objs, &req.PageReq)
|
||||
provider := "unknown"
|
||||
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
|
||||
if err == nil {
|
||||
provider = storage.GetStorage().Driver
|
||||
}
|
||||
common.SuccessResp(c, FsListResp{
|
||||
Content: toObjsResp(objs, reqPath, isEncrypt(meta, reqPath)),
|
||||
Content: toObjsResp(objs, reqPath, isEncrypt(meta, reqPath), user.ID),
|
||||
Total: int64(total),
|
||||
Readme: getReadme(meta, reqPath),
|
||||
Header: getHeader(meta, reqPath),
|
||||
@@ -168,14 +161,7 @@ func FsDirs(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
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)
|
||||
dirs := filterDirs(objs)
|
||||
common.SuccessResp(c, dirs)
|
||||
}
|
||||
|
||||
@@ -238,22 +224,12 @@ func pagination(objs []model.Obj, req *model.PageReq) (int, []model.Obj) {
|
||||
return total, objs[start:end]
|
||||
}
|
||||
|
||||
func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjLabelResp {
|
||||
func toObjsResp(objs []model.Obj, parent string, encrypt bool, userId uint) []ObjLabelResp {
|
||||
var resp []ObjLabelResp
|
||||
|
||||
names := make([]string, 0, len(objs))
|
||||
for _, obj := range objs {
|
||||
if !obj.IsDir() {
|
||||
names = append(names, obj.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
labelsByName, _ := op.GetLabelsByFileNamesPublic(names)
|
||||
|
||||
for _, obj := range objs {
|
||||
var labels []model.Label
|
||||
if !obj.IsDir() {
|
||||
labels = labelsByName[obj.GetName()]
|
||||
if obj.IsDir() == false {
|
||||
labels, _ = op.GetLabelByFileName(userId, obj.GetName())
|
||||
}
|
||||
thumb, _ := model.GetThumb(obj)
|
||||
resp = append(resp, ObjLabelResp{
|
||||
@@ -393,7 +369,7 @@ func FsGet(c *gin.Context) {
|
||||
Readme: getReadme(meta, reqPath),
|
||||
Header: getHeader(meta, reqPath),
|
||||
Provider: provider,
|
||||
Related: toObjsResp(related, parentPath, isEncrypt(parentMeta, parentPath)),
|
||||
Related: toObjsResp(related, parentPath, isEncrypt(parentMeta, parentPath), user.ID),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DelLabelFileBinDingReq struct {
|
||||
@@ -18,36 +16,18 @@ type DelLabelFileBinDingReq struct {
|
||||
LabelId string `json:"label_id"`
|
||||
}
|
||||
|
||||
type pageResp[T any] struct {
|
||||
Content []T `json:"content"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type restoreLabelBindingsReq struct {
|
||||
KeepIDs bool `json:"keep_ids"`
|
||||
Override bool `json:"override"`
|
||||
Bindings []model.LabelFileBinding `json:"bindings"`
|
||||
}
|
||||
|
||||
func GetLabelByFileName(c *gin.Context) {
|
||||
fileName := c.Query("file_name")
|
||||
if fileName == "" {
|
||||
common.ErrorResp(c, errors.New("file_name must not empty"), 400)
|
||||
return
|
||||
}
|
||||
decodedFileName, err := url.QueryUnescape(fileName)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, errors.New("invalid file_name"), 400)
|
||||
return
|
||||
}
|
||||
fmt.Println(">>> 原始 fileName:", fileName)
|
||||
fmt.Println(">>> 解码后 fileName:", decodedFileName)
|
||||
userObj, ok := c.Value("user").(*model.User)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
labels, err := op.GetLabelByFileName(userObj.ID, decodedFileName)
|
||||
labels, err := op.GetLabelByFileName(userObj.ID, fileName)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
@@ -121,130 +101,3 @@ func GetFileByLabel(c *gin.Context) {
|
||||
}
|
||||
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) {
|
||||
var req model.Role
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
@@ -56,14 +56,8 @@ func CreateRole(c *gin.Context) {
|
||||
}
|
||||
|
||||
func UpdateRole(c *gin.Context) {
|
||||
var req struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
PermissionScopes []model.PermissionEntry `json:"permission_scopes"`
|
||||
Default *bool `json:"default"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
var req model.Role
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
@@ -72,21 +66,11 @@ func UpdateRole(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
switch role.Name {
|
||||
case "admin":
|
||||
if role.Name == "admin" || role.Name == "guest" {
|
||||
common.ErrorResp(c, errs.ErrChangeDefaultRole, 403)
|
||||
return
|
||||
|
||||
case "guest":
|
||||
req.Name = "guest"
|
||||
}
|
||||
role.Name = req.Name
|
||||
role.Description = req.Description
|
||||
role.PermissionScopes = req.PermissionScopes
|
||||
if req.Default != nil {
|
||||
role.Default = *req.Default
|
||||
}
|
||||
if err := op.UpdateRole(role); err != nil {
|
||||
if err := op.UpdateRole(&req); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
|
||||
@@ -43,39 +43,28 @@ func Search(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
var (
|
||||
filteredNodes []model.SearchNode
|
||||
)
|
||||
for len(filteredNodes) < req.PerPage {
|
||||
nodes, _, err := search.Search(c, req.SearchReq)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
nodes, total, err := search.Search(c, req.SearchReq)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
var filteredNodes []model.SearchNode
|
||||
for _, node := range nodes {
|
||||
if !strings.HasPrefix(node.Parent, user.BasePath) {
|
||||
continue
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
break
|
||||
meta, err := op.GetNearestMeta(node.Parent)
|
||||
if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
continue
|
||||
}
|
||||
for _, node := range nodes {
|
||||
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
|
||||
}
|
||||
if !common.CanAccessWithRoles(user, meta, path.Join(node.Parent, node.Name), req.Password) {
|
||||
continue
|
||||
}
|
||||
req.Page++
|
||||
filteredNodes = append(filteredNodes, node)
|
||||
}
|
||||
common.SuccessResp(c, common.PageResp{
|
||||
Content: utils.MustSliceConvert(filteredNodes, nodeToSearchResp),
|
||||
Total: int64(len(filteredNodes)),
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
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,21 +14,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func getRoleOptions() string {
|
||||
roles, _, err := op.GetRoles(1, model.MaxInt)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
names := make([]string, 0, len(roles))
|
||||
for _, r := range roles {
|
||||
if r.Name == "admin" || r.Name == "guest" {
|
||||
continue
|
||||
}
|
||||
names = append(names, r.Name)
|
||||
}
|
||||
return strings.Join(names, ",")
|
||||
}
|
||||
|
||||
func ResetToken(c *gin.Context) {
|
||||
token := random.Token()
|
||||
item := model.SettingItem{Key: "token", Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE}
|
||||
@@ -49,17 +34,6 @@ func GetSetting(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if item.Key == conf.DefaultRole {
|
||||
copy := *item
|
||||
copy.Options = getRoleOptions()
|
||||
if id, err := strconv.Atoi(copy.Value); err == nil {
|
||||
if r, err := op.GetRole(uint(id)); err == nil {
|
||||
copy.Value = r.Name
|
||||
}
|
||||
}
|
||||
common.SuccessResp(c, copy)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, item)
|
||||
} else {
|
||||
items, err := op.GetSettingItemInKeys(strings.Split(keys, ","))
|
||||
@@ -67,17 +41,6 @@ func GetSetting(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
for i := range items {
|
||||
if items[i].Key == conf.DefaultRole {
|
||||
if id, err := strconv.Atoi(items[i].Value); err == nil {
|
||||
if r, err := op.GetRole(uint(id)); err == nil {
|
||||
items[i].Value = r.Name
|
||||
}
|
||||
}
|
||||
items[i].Options = getRoleOptions()
|
||||
break
|
||||
}
|
||||
}
|
||||
common.SuccessResp(c, items)
|
||||
}
|
||||
}
|
||||
@@ -88,22 +51,6 @@ func SaveSettings(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range req {
|
||||
if req[i].Key == conf.DefaultRole {
|
||||
role, err := op.GetRoleByName(req[i].Value)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if role.Name == "admin" || role.Name == "guest" {
|
||||
common.ErrorStrResp(c, "cannot set admin or guest as default role", 400)
|
||||
return
|
||||
}
|
||||
req[i].Value = strconv.Itoa(int(role.ID))
|
||||
}
|
||||
}
|
||||
|
||||
if err := op.SaveSettingItems(req); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
@@ -141,17 +88,6 @@ func ListSettings(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
for i := range settings {
|
||||
if settings[i].Key == conf.DefaultRole {
|
||||
if id, err := strconv.Atoi(settings[i].Value); err == nil {
|
||||
if r, err := op.GetRole(uint(id)); err == nil {
|
||||
settings[i].Value = r.Name
|
||||
}
|
||||
}
|
||||
settings[i].Options = getRoleOptions()
|
||||
break
|
||||
}
|
||||
}
|
||||
common.SuccessResp(c, settings)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package handles
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"strconv"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
@@ -37,9 +36,6 @@ func CreateUser(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if len(req.Role) == 0 {
|
||||
req.Role = model.Roles{op.GetDefaultRoleID()}
|
||||
}
|
||||
if req.IsAdmin() || req.IsGuest() {
|
||||
common.ErrorStrResp(c, "admin or guest user can not be created", 400, true)
|
||||
return
|
||||
@@ -71,10 +67,10 @@ func UpdateUser(c *gin.Context) {
|
||||
common.ErrorStrResp(c, "cannot change role of admin user", 403)
|
||||
return
|
||||
}
|
||||
//if user.Username != req.Username {
|
||||
// common.ErrorStrResp(c, "cannot change username of admin user", 403)
|
||||
// return
|
||||
//}
|
||||
if user.Username != req.Username {
|
||||
common.ErrorStrResp(c, "cannot change username of admin user", 403)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Password == "" {
|
||||
@@ -87,25 +83,10 @@ func UpdateUser(c *gin.Context) {
|
||||
if req.OtpSecret == "" {
|
||||
req.OtpSecret = user.OtpSecret
|
||||
}
|
||||
if req.Disabled && user.IsAdmin() {
|
||||
count, err := op.CountEnabledAdminsExcluding(user.ID)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
if count == 0 {
|
||||
common.ErrorStrResp(c, "at least one enabled admin must be kept", 400)
|
||||
return
|
||||
}
|
||||
if req.Disabled && req.IsAdmin() {
|
||||
common.ErrorStrResp(c, "admin user can not be disabled", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.SliceEqual(user.Role, req.Role) {
|
||||
if req.IsAdmin() || req.IsGuest() {
|
||||
common.ErrorStrResp(c, "cannot assign admin or guest role to user", 400, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := op.UpdateUser(&req); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
|
||||
@@ -2,16 +2,12 @@ package middlewares
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/device"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -28,9 +24,7 @@ func Auth(c *gin.Context) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if !HandleSession(c, admin) {
|
||||
return
|
||||
}
|
||||
c.Set("user", admin)
|
||||
log.Debugf("use admin token: %+v", admin)
|
||||
c.Next()
|
||||
return
|
||||
@@ -47,18 +41,7 @@ func Auth(c *gin.Context) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
c.Set("user", guest)
|
||||
log.Debugf("use empty token: %+v", guest)
|
||||
c.Next()
|
||||
return
|
||||
@@ -95,36 +78,11 @@ func Auth(c *gin.Context) {
|
||||
}
|
||||
user.RolesDetail = roles
|
||||
}
|
||||
if !HandleSession(c, user) {
|
||||
return
|
||||
}
|
||||
c.Set("user", user)
|
||||
log.Debugf("use login token: %+v", user)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// HandleSession verifies device sessions and stores context values.
|
||||
func HandleSession(c *gin.Context, user *model.User) bool {
|
||||
clientID := c.GetHeader("Client-Id")
|
||||
if clientID == "" {
|
||||
clientID = c.Query("client_id")
|
||||
}
|
||||
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s", user.ID, clientID))
|
||||
if err := device.Handle(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
|
||||
token := c.GetHeader("Authorization")
|
||||
if errors.Is(err, errs.SessionInactive) {
|
||||
_ = common.InvalidateToken(token)
|
||||
common.ErrorResp(c, err, 401)
|
||||
} else {
|
||||
common.ErrorResp(c, err, 403)
|
||||
}
|
||||
c.Abort()
|
||||
return false
|
||||
}
|
||||
c.Set("device_key", key)
|
||||
c.Set("user", user)
|
||||
return true
|
||||
}
|
||||
|
||||
func Authn(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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,7 +22,6 @@ func Init(e *gin.Engine) {
|
||||
})
|
||||
}
|
||||
Cors(e)
|
||||
e.Use(middlewares.SessionRefresh)
|
||||
g := e.Group(conf.URL.Path)
|
||||
if conf.Conf.Scheme.HttpPort != -1 && conf.Conf.Scheme.HttpsPort != -1 && conf.Conf.Scheme.ForceHttps {
|
||||
e.Use(middlewares.ForceHttps)
|
||||
@@ -62,7 +61,6 @@ func Init(e *gin.Engine) {
|
||||
api.POST("/auth/login", handles.Login)
|
||||
api.POST("/auth/login/hash", handles.LoginHash)
|
||||
api.POST("/auth/login/ldap", handles.LoginLdap)
|
||||
api.POST("/auth/register", handles.Register)
|
||||
auth.GET("/me", handles.CurrentUser)
|
||||
auth.POST("/me/update", handles.UpdateCurrent)
|
||||
auth.GET("/me/sshkey/list", handles.ListMyPublicKey)
|
||||
@@ -71,8 +69,6 @@ func Init(e *gin.Engine) {
|
||||
auth.POST("/auth/2fa/generate", handles.Generate2FA)
|
||||
auth.POST("/auth/2fa/verify", handles.Verify2FA)
|
||||
auth.GET("/auth/logout", handles.LogOut)
|
||||
auth.GET("/me/sessions", handles.ListMySessions)
|
||||
auth.POST("/me/sessions/evict", handles.EvictMySession)
|
||||
|
||||
// auth
|
||||
api.GET("/auth/sso", handles.SSOLoginRedirect)
|
||||
@@ -96,8 +92,6 @@ func Init(e *gin.Engine) {
|
||||
|
||||
_fs(auth.Group("/fs"))
|
||||
_task(auth.Group("/task", middlewares.AuthNotGuest))
|
||||
_label(auth.Group("/label"))
|
||||
_labelFileBinding(auth.Group("/label_file_binding"))
|
||||
admin(auth.Group("/admin", middlewares.AuthAdmin))
|
||||
if flags.Debug || flags.Dev {
|
||||
debug(g.Group("/debug"))
|
||||
@@ -176,21 +170,17 @@ func admin(g *gin.RouterGroup) {
|
||||
index.GET("/progress", middlewares.SearchIndex, handles.GetProgress)
|
||||
|
||||
label := g.Group("/label")
|
||||
label.GET("/list", handles.ListLabel)
|
||||
label.GET("/get", handles.GetLabel)
|
||||
label.POST("/create", handles.CreateLabel)
|
||||
label.POST("/update", handles.UpdateLabel)
|
||||
label.POST("/delete", handles.DeleteLabel)
|
||||
|
||||
labelFileBinding := g.Group("/label_file_binding")
|
||||
labelFileBinding.GET("/list", handles.ListLabelFileBinding)
|
||||
labelFileBinding.GET("/get", handles.GetLabelByFileName)
|
||||
labelFileBinding.GET("/get_file_by_label", handles.GetFileByLabel)
|
||||
labelFileBinding.POST("/create", handles.CreateLabelFileBinDing)
|
||||
labelFileBinding.POST("/create_batch", handles.CreateLabelFileBinDingBatch)
|
||||
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) {
|
||||
@@ -226,16 +216,6 @@ func _task(g *gin.RouterGroup) {
|
||||
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) {
|
||||
config := cors.DefaultConfig()
|
||||
// config.AllowAllOrigins = true
|
||||
|
||||
@@ -3,7 +3,6 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -13,11 +12,9 @@ import (
|
||||
"github.com/alist-org/alist/v3/server/middlewares"
|
||||
|
||||
"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/op"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/alist-org/alist/v3/server/webdav"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -72,13 +69,6 @@ func WebDAVAuth(c *gin.Context) {
|
||||
c.Abort()
|
||||
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.Next()
|
||||
return
|
||||
@@ -105,9 +95,6 @@ func WebDAVAuth(c *gin.Context) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if roles, err := op.GetRolesByUserID(user.ID); err == nil {
|
||||
user.RolesDetail = roles
|
||||
}
|
||||
reqPath := c.Param("path")
|
||||
if reqPath == "" {
|
||||
reqPath = "/"
|
||||
@@ -120,8 +107,7 @@ func WebDAVAuth(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, reqPath)
|
||||
webdavRead := common.HasPermission(perm, common.PermWebdavRead)
|
||||
if user.Disabled || (!webdavRead && (c.Request.Method != "PROPFIND" || !common.HasChildPermission(user, reqPath, common.PermWebdavRead))) {
|
||||
if user.Disabled || !common.HasPermission(perm, common.PermWebdavRead) {
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.Set("user", guest)
|
||||
c.Next()
|
||||
@@ -156,13 +142,6 @@ func WebDAVAuth(c *gin.Context) {
|
||||
c.Abort()
|
||||
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.Next()
|
||||
}
|
||||
|
||||
@@ -94,7 +94,6 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn
|
||||
depth = 0
|
||||
}
|
||||
meta, _ := op.GetNearestMeta(name)
|
||||
user := ctx.Value("user").(*model.User)
|
||||
// Read directory names.
|
||||
objs, err := fs.List(context.WithValue(ctx, "meta", meta), name, &fs.ListArgs{})
|
||||
//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
|
||||
@@ -109,9 +108,6 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn
|
||||
|
||||
for _, fileInfo := range objs {
|
||||
filename := path.Join(name, fileInfo.GetName())
|
||||
if !common.CanReadPathByRole(user, filename) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
|
||||
return err
|
||||
|
||||
@@ -648,98 +648,6 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
|
||||
|
||||
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 {
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -763,14 +671,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel := strings.TrimPrefix(
|
||||
strings.TrimPrefix(
|
||||
utils.FixAndCleanPath(reqPath),
|
||||
utils.FixAndCleanPath(user.BasePath),
|
||||
),
|
||||
"/",
|
||||
)
|
||||
href := utils.EncodePath(path.Join("/", h.Prefix, rel), true)
|
||||
href := path.Join(h.Prefix, strings.TrimPrefix(reqPath, user.BasePath))
|
||||
if href != "/" && info.IsDir() {
|
||||
href += "/"
|
||||
}
|
||||
@@ -833,7 +734,7 @@ func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (statu
|
||||
|
||||
func makePropstatResponse(href string, pstats []Propstat) *response {
|
||||
resp := response{
|
||||
Href: []string{href},
|
||||
Href: []string{(&url.URL{Path: href}).EscapedPath()},
|
||||
Propstat: make([]propstat, 0, len(pstats)),
|
||||
}
|
||||
for _, p := range pstats {
|
||||
|
||||
Reference in New Issue
Block a user