mirror of
https://github.com/AlistGo/alist.git
synced 2025-04-23 13:54:04 +08:00
feat(doubao_share): support doubao_share link
This commit is contained in:
parent
88abb323cb
commit
7a5019f818
@ -24,6 +24,7 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
|
||||
_ "github.com/alist-org/alist/v3/drivers/crypt"
|
||||
_ "github.com/alist-org/alist/v3/drivers/doubao"
|
||||
_ "github.com/alist-org/alist/v3/drivers/doubao_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/dropbox"
|
||||
_ "github.com/alist-org/alist/v3/drivers/febbox"
|
||||
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
||||
|
177
drivers/doubao_share/driver.go
Normal file
177
drivers/doubao_share/driver.go
Normal file
@ -0,0 +1,177 @@
|
||||
package doubao_share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type DoubaoShare struct {
|
||||
model.Storage
|
||||
Addition
|
||||
RootFiles []RootFileList
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Init(ctx context.Context) error {
|
||||
// 初始化 虚拟分享列表
|
||||
if err := d.initShareList(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
// 检查是否为根目录
|
||||
if dir.GetID() == "" && dir.GetPath() == "/" {
|
||||
return d.listRootDirectory(ctx)
|
||||
}
|
||||
|
||||
// 非根目录,处理不同情况
|
||||
if fo, ok := dir.(*FileObject); ok {
|
||||
if fo.ShareID == "" {
|
||||
// 虚拟目录,需要列出子目录
|
||||
return d.listVirtualDirectoryContent(dir)
|
||||
} else {
|
||||
// 具有分享ID的目录,获取此分享下的文件
|
||||
shareId, relativePath, err := d._findShareAndPath(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.getFilesInPath(ctx, shareId, dir.GetID(), relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用通用方法
|
||||
shareId, relativePath, err := d._findShareAndPath(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取指定路径下的文件
|
||||
return d.getFilesInPath(ctx, shareId, dir.GetID(), relativePath)
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
var downloadUrl string
|
||||
|
||||
if u, ok := file.(*FileObject); ok {
|
||||
switch u.NodeType {
|
||||
case VideoType, AudioType:
|
||||
var r GetVideoFileUrlResp
|
||||
_, err := d.request("/samantha/media/get_play_info", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"key": u.Key,
|
||||
"share_id": u.ShareID,
|
||||
"node_id": file.GetID(),
|
||||
})
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadUrl = r.Data.OriginalMediaInfo.MainURL
|
||||
default:
|
||||
var r GetFileUrlResp
|
||||
_, err := d.request("/alice/message/get_file_url", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"uris": []string{u.Key},
|
||||
"type": FileNodeType[u.NodeType],
|
||||
})
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadUrl = r.Data.FileUrls[0].MainURL
|
||||
}
|
||||
|
||||
// 生成标准的Content-Disposition
|
||||
contentDisposition := generateContentDisposition(u.Name)
|
||||
|
||||
return &model.Link{
|
||||
URL: downloadUrl,
|
||||
Header: http.Header{
|
||||
"User-Agent": []string{UserAgent},
|
||||
"Content-Disposition": []string{contentDisposition},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("can't convert obj to URL")
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
// TODO create folder, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
// TODO move obj, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
// TODO rename obj, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
// TODO copy obj, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Remove(ctx context.Context, obj model.Obj) error {
|
||||
// TODO remove obj, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
// TODO upload file, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||
// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||
// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||
// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
|
||||
// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional
|
||||
// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir
|
||||
// return errs.NotImplement to use an internal archive tool
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
//func (d *DoubaoShare) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
// return nil, errs.NotSupport
|
||||
//}
|
||||
|
||||
var _ driver.Driver = (*DoubaoShare)(nil)
|
32
drivers/doubao_share/meta.go
Normal file
32
drivers/doubao_share/meta.go
Normal file
@ -0,0 +1,32 @@
|
||||
package doubao_share
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootPath
|
||||
Cookie string `json:"cookie" type:"text"`
|
||||
ShareIds string `json:"share_ids" type:"text" required:"true"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "DoubaoShare",
|
||||
LocalSort: true,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: true,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "/",
|
||||
CheckStatus: false,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &DoubaoShare{}
|
||||
})
|
||||
}
|
207
drivers/doubao_share/types.go
Normal file
207
drivers/doubao_share/types.go
Normal file
@ -0,0 +1,207 @@
|
||||
package doubao_share
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
type BaseResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type NodeInfoData struct {
|
||||
Share ShareInfo `json:"share,omitempty"`
|
||||
Creator CreatorInfo `json:"creator,omitempty"`
|
||||
NodeList []File `json:"node_list,omitempty"`
|
||||
NodeInfo File `json:"node_info,omitempty"`
|
||||
Children []File `json:"children,omitempty"`
|
||||
Path FilePath `json:"path,omitempty"`
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
HasMore bool `json:"has_more,omitempty"`
|
||||
}
|
||||
|
||||
type NodeInfoResp struct {
|
||||
BaseResp
|
||||
NodeInfoData `json:"data"`
|
||||
}
|
||||
|
||||
type RootFileList struct {
|
||||
ShareID string
|
||||
VirtualPath string
|
||||
NodeInfo NodeInfoData
|
||||
Child *[]RootFileList
|
||||
}
|
||||
|
||||
type File struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
NodeType int `json:"node_type"`
|
||||
Size int64 `json:"size"`
|
||||
Source int `json:"source"`
|
||||
NameReviewStatus int `json:"name_review_status"`
|
||||
ContentReviewStatus int `json:"content_review_status"`
|
||||
RiskReviewStatus int `json:"risk_review_status"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
ParentID string `json:"parent_id"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
UpdateTime int64 `json:"update_time"`
|
||||
}
|
||||
|
||||
type FileObject struct {
|
||||
model.Object
|
||||
ShareID string
|
||||
Key string
|
||||
NodeID string
|
||||
NodeType int
|
||||
}
|
||||
|
||||
type ShareInfo struct {
|
||||
ShareID string `json:"share_id"`
|
||||
FirstNode struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
NodeType int `json:"node_type"`
|
||||
Size int `json:"size"`
|
||||
Source int `json:"source"`
|
||||
Content struct {
|
||||
LinkFileType string `json:"link_file_type"`
|
||||
ImageWidth int `json:"image_width"`
|
||||
ImageHeight int `json:"image_height"`
|
||||
AiSkillStatus int `json:"ai_skill_status"`
|
||||
} `json:"content"`
|
||||
NameReviewStatus int `json:"name_review_status"`
|
||||
ContentReviewStatus int `json:"content_review_status"`
|
||||
RiskReviewStatus int `json:"risk_review_status"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
ParentID string `json:"parent_id"`
|
||||
CreateTime int `json:"create_time"`
|
||||
UpdateTime int `json:"update_time"`
|
||||
} `json:"first_node"`
|
||||
NodeCount int `json:"node_count"`
|
||||
CreateTime int `json:"create_time"`
|
||||
Channel string `json:"channel"`
|
||||
InfluencerType int `json:"influencer_type"`
|
||||
}
|
||||
|
||||
type CreatorInfo struct {
|
||||
EntityID string `json:"entity_id"`
|
||||
UserName string `json:"user_name"`
|
||||
NickName string `json:"nick_name"`
|
||||
Avatar struct {
|
||||
OriginURL string `json:"origin_url"`
|
||||
TinyURL string `json:"tiny_url"`
|
||||
URI string `json:"uri"`
|
||||
} `json:"avatar"`
|
||||
}
|
||||
|
||||
type FilePath []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
NodeType int `json:"node_type"`
|
||||
Size int `json:"size"`
|
||||
Source int `json:"source"`
|
||||
NameReviewStatus int `json:"name_review_status"`
|
||||
ContentReviewStatus int `json:"content_review_status"`
|
||||
RiskReviewStatus int `json:"risk_review_status"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
ParentID string `json:"parent_id"`
|
||||
CreateTime int `json:"create_time"`
|
||||
UpdateTime int `json:"update_time"`
|
||||
}
|
||||
|
||||
type GetFileUrlResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
FileUrls []struct {
|
||||
URI string `json:"uri"`
|
||||
MainURL string `json:"main_url"`
|
||||
BackURL string `json:"back_url"`
|
||||
} `json:"file_urls"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type GetVideoFileUrlResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
MediaType string `json:"media_type"`
|
||||
MediaInfo []struct {
|
||||
Meta struct {
|
||||
Height string `json:"height"`
|
||||
Width string `json:"width"`
|
||||
Format string `json:"format"`
|
||||
Duration float64 `json:"duration"`
|
||||
CodecType string `json:"codec_type"`
|
||||
Definition string `json:"definition"`
|
||||
} `json:"meta"`
|
||||
MainURL string `json:"main_url"`
|
||||
BackupURL string `json:"backup_url"`
|
||||
} `json:"media_info"`
|
||||
OriginalMediaInfo struct {
|
||||
Meta struct {
|
||||
Height string `json:"height"`
|
||||
Width string `json:"width"`
|
||||
Format string `json:"format"`
|
||||
Duration float64 `json:"duration"`
|
||||
CodecType string `json:"codec_type"`
|
||||
Definition string `json:"definition"`
|
||||
} `json:"meta"`
|
||||
MainURL string `json:"main_url"`
|
||||
BackupURL string `json:"backup_url"`
|
||||
} `json:"original_media_info"`
|
||||
PosterURL string `json:"poster_url"`
|
||||
PlayableStatus int `json:"playable_status"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type CommonResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Message string `json:"message,omitempty"` // 错误情况下的消息
|
||||
Data json.RawMessage `json:"data,omitempty"` // 原始数据,稍后解析
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Locale string `json:"locale"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// IsSuccess 判断响应是否成功
|
||||
func (r *CommonResp) IsSuccess() bool {
|
||||
return r.Code == 0
|
||||
}
|
||||
|
||||
// GetError 获取错误信息
|
||||
func (r *CommonResp) GetError() error {
|
||||
if r.IsSuccess() {
|
||||
return nil
|
||||
}
|
||||
// 优先使用message字段
|
||||
errMsg := r.Message
|
||||
if errMsg == "" {
|
||||
errMsg = r.Msg
|
||||
}
|
||||
// 如果error对象存在且有详细消息,则使用error中的信息
|
||||
if r.Error != nil && r.Error.Message != "" {
|
||||
errMsg = r.Error.Message
|
||||
}
|
||||
|
||||
return fmt.Errorf("[doubao] API error (code: %d): %s", r.Code, errMsg)
|
||||
}
|
||||
|
||||
// UnmarshalData 将data字段解析为指定类型
|
||||
func (r *CommonResp) UnmarshalData(v interface{}) error {
|
||||
if !r.IsSuccess() {
|
||||
return r.GetError()
|
||||
}
|
||||
|
||||
if len(r.Data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(r.Data, v)
|
||||
}
|
744
drivers/doubao_share/util.go
Normal file
744
drivers/doubao_share/util.go
Normal file
@ -0,0 +1,744 @@
|
||||
package doubao_share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DirectoryType = 1
|
||||
FileType = 2
|
||||
LinkType = 3
|
||||
ImageType = 4
|
||||
PagesType = 5
|
||||
VideoType = 6
|
||||
AudioType = 7
|
||||
MeetingMinutesType = 8
|
||||
)
|
||||
|
||||
var FileNodeType = map[int]string{
|
||||
1: "directory",
|
||||
2: "file",
|
||||
3: "link",
|
||||
4: "image",
|
||||
5: "pages",
|
||||
6: "video",
|
||||
7: "audio",
|
||||
8: "meeting_minutes",
|
||||
}
|
||||
|
||||
const (
|
||||
BaseURL = "https://www.doubao.com"
|
||||
FileDataType = "file"
|
||||
ImgDataType = "image"
|
||||
VideoDataType = "video"
|
||||
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
|
||||
)
|
||||
|
||||
func (d *DoubaoShare) request(path string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
reqUrl := BaseURL + path
|
||||
req := base.RestyClient.R()
|
||||
|
||||
req.SetHeaders(map[string]string{
|
||||
"Cookie": d.Cookie,
|
||||
"User-Agent": UserAgent,
|
||||
})
|
||||
|
||||
req.SetQueryParams(map[string]string{
|
||||
"version_code": "20800",
|
||||
"device_platform": "web",
|
||||
})
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
|
||||
var commonResp CommonResp
|
||||
|
||||
res, err := req.Execute(method, reqUrl)
|
||||
log.Debugln(res.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body := res.Body()
|
||||
// 先解析为通用响应
|
||||
if err = json.Unmarshal(body, &commonResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 检查响应是否成功
|
||||
if !commonResp.IsSuccess() {
|
||||
return body, commonResp.GetError()
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
if err = json.Unmarshal(body, resp); err != nil {
|
||||
return body, err
|
||||
}
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) getFiles(dirId, nodeId, cursor string) (resp []File, err error) {
|
||||
var r NodeInfoResp
|
||||
|
||||
var body = base.Json{
|
||||
"share_id": dirId,
|
||||
"node_id": nodeId,
|
||||
}
|
||||
// 如果有游标,则设置游标和大小
|
||||
if cursor != "" {
|
||||
body["cursor"] = cursor
|
||||
body["size"] = 50
|
||||
} else {
|
||||
body["need_full_path"] = false
|
||||
}
|
||||
|
||||
_, err = d.request("/samantha/aispace/share/node_info", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(body)
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.NodeInfoData.Children != nil {
|
||||
resp = r.NodeInfoData.Children
|
||||
}
|
||||
|
||||
if r.NodeInfoData.NextCursor != "-1" {
|
||||
// 递归获取下一页
|
||||
nextFiles, err := d.getFiles(dirId, nodeId, r.NodeInfoData.NextCursor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp = append(r.NodeInfoData.Children, nextFiles...)
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) getShareOverview(shareId, cursor string) (resp []File, err error) {
|
||||
return d.getShareOverviewWithHistory(shareId, cursor, make(map[string]bool))
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) getShareOverviewWithHistory(shareId, cursor string, cursorHistory map[string]bool) (resp []File, err error) {
|
||||
var r NodeInfoResp
|
||||
|
||||
var body = base.Json{
|
||||
"share_id": shareId,
|
||||
}
|
||||
// 如果有游标,则设置游标和大小
|
||||
if cursor != "" {
|
||||
body["cursor"] = cursor
|
||||
body["size"] = 50
|
||||
} else {
|
||||
body["need_full_path"] = false
|
||||
}
|
||||
|
||||
_, err = d.request("/samantha/aispace/share/overview", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(body)
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.NodeInfoData.NodeList != nil {
|
||||
resp = r.NodeInfoData.NodeList
|
||||
}
|
||||
|
||||
if r.NodeInfoData.NextCursor != "-1" {
|
||||
// 检查游标是否重复出现,防止无限循环
|
||||
if cursorHistory[r.NodeInfoData.NextCursor] {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 记录当前游标
|
||||
cursorHistory[r.NodeInfoData.NextCursor] = true
|
||||
|
||||
// 递归获取下一页
|
||||
nextFiles, err := d.getShareOverviewWithHistory(shareId, r.NodeInfoData.NextCursor, cursorHistory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp = append(resp, nextFiles...)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) initShareList() error {
|
||||
if d.Addition.ShareIds == "" {
|
||||
return fmt.Errorf("share_ids is empty")
|
||||
}
|
||||
|
||||
// 解析分享配置
|
||||
shareConfigs, rootShares, err := d._parseShareConfigs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查路径冲突
|
||||
if err := d._detectPathConflicts(shareConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
rootMap := d._buildTreeStructure(shareConfigs, rootShares)
|
||||
|
||||
// 提取顶级节点
|
||||
topLevelNodes := d._extractTopLevelNodes(rootMap, rootShares)
|
||||
if len(topLevelNodes) == 0 {
|
||||
return fmt.Errorf("no valid share_ids found")
|
||||
}
|
||||
|
||||
// 存储结果
|
||||
d.RootFiles = topLevelNodes
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 从配置中解析分享ID和路径
|
||||
func (d *DoubaoShare) _parseShareConfigs() (map[string]string, []string, error) {
|
||||
shareConfigs := make(map[string]string) // 路径 -> 分享ID
|
||||
rootShares := make([]string, 0) // 根目录显示的分享ID
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(d.Addition.ShareIds), "\n")
|
||||
if len(lines) == 0 {
|
||||
return nil, nil, fmt.Errorf("no share_ids found")
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析分享ID和路径
|
||||
parts := strings.Split(line, "|")
|
||||
var shareId, sharePath string
|
||||
|
||||
if len(parts) == 1 {
|
||||
// 无路径分享,直接在根目录显示
|
||||
shareId = _extractShareId(parts[0])
|
||||
if shareId != "" {
|
||||
rootShares = append(rootShares, shareId)
|
||||
}
|
||||
continue
|
||||
} else if len(parts) >= 2 {
|
||||
shareId = _extractShareId(parts[0])
|
||||
sharePath = strings.Trim(parts[1], "/")
|
||||
}
|
||||
|
||||
if shareId == "" {
|
||||
log.Warnf("[doubao_share] Invalid Share_id Format: %s", line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 空路径也加入根目录显示
|
||||
if sharePath == "" {
|
||||
rootShares = append(rootShares, shareId)
|
||||
continue
|
||||
}
|
||||
|
||||
// 添加到路径映射
|
||||
shareConfigs[sharePath] = shareId
|
||||
}
|
||||
|
||||
return shareConfigs, rootShares, nil
|
||||
}
|
||||
|
||||
// 检测路径冲突
|
||||
func (d *DoubaoShare) _detectPathConflicts(shareConfigs map[string]string) error {
|
||||
// 检查直接路径冲突
|
||||
pathToShareIds := make(map[string][]string)
|
||||
for sharePath, id := range shareConfigs {
|
||||
pathToShareIds[sharePath] = append(pathToShareIds[sharePath], id)
|
||||
}
|
||||
|
||||
for sharePath, ids := range pathToShareIds {
|
||||
if len(ids) > 1 {
|
||||
return fmt.Errorf("路径冲突: 路径 '%s' 被多个不同的分享ID使用: %s",
|
||||
sharePath, strings.Join(ids, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// 检查层次冲突
|
||||
for path1, id1 := range shareConfigs {
|
||||
for path2, id2 := range shareConfigs {
|
||||
if path1 == path2 || id1 == id2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查前缀冲突
|
||||
if strings.HasPrefix(path2, path1+"/") || strings.HasPrefix(path1, path2+"/") {
|
||||
return fmt.Errorf("路径冲突: 路径 '%s' (ID: %s) 与路径 '%s' (ID: %s) 存在层次冲突",
|
||||
path1, id1, path2, id2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
func (d *DoubaoShare) _buildTreeStructure(shareConfigs map[string]string, rootShares []string) map[string]*RootFileList {
|
||||
rootMap := make(map[string]*RootFileList)
|
||||
|
||||
// 添加所有分享节点
|
||||
for sharePath, shareId := range shareConfigs {
|
||||
children := make([]RootFileList, 0)
|
||||
rootMap[sharePath] = &RootFileList{
|
||||
ShareID: shareId,
|
||||
VirtualPath: sharePath,
|
||||
NodeInfo: NodeInfoData{},
|
||||
Child: &children,
|
||||
}
|
||||
}
|
||||
|
||||
// 构建父子关系
|
||||
for sharePath, node := range rootMap {
|
||||
if sharePath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
pathParts := strings.Split(sharePath, "/")
|
||||
if len(pathParts) > 1 {
|
||||
parentPath := strings.Join(pathParts[:len(pathParts)-1], "/")
|
||||
|
||||
// 确保所有父级路径都已创建
|
||||
_ensurePathExists(rootMap, parentPath)
|
||||
|
||||
// 添加当前节点到父节点
|
||||
if parent, exists := rootMap[parentPath]; exists {
|
||||
*parent.Child = append(*parent.Child, *node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootMap
|
||||
}
|
||||
|
||||
// 提取顶级节点
|
||||
func (d *DoubaoShare) _extractTopLevelNodes(rootMap map[string]*RootFileList, rootShares []string) []RootFileList {
|
||||
var topLevelNodes []RootFileList
|
||||
|
||||
// 添加根目录分享
|
||||
for _, shareId := range rootShares {
|
||||
children := make([]RootFileList, 0)
|
||||
topLevelNodes = append(topLevelNodes, RootFileList{
|
||||
ShareID: shareId,
|
||||
VirtualPath: "",
|
||||
NodeInfo: NodeInfoData{},
|
||||
Child: &children,
|
||||
})
|
||||
}
|
||||
|
||||
// 添加顶级目录
|
||||
for rootPath, node := range rootMap {
|
||||
if rootPath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
isTopLevel := true
|
||||
pathParts := strings.Split(rootPath, "/")
|
||||
|
||||
if len(pathParts) > 1 {
|
||||
parentPath := strings.Join(pathParts[:len(pathParts)-1], "/")
|
||||
if _, exists := rootMap[parentPath]; exists {
|
||||
isTopLevel = false
|
||||
}
|
||||
}
|
||||
|
||||
if isTopLevel {
|
||||
topLevelNodes = append(topLevelNodes, *node)
|
||||
}
|
||||
}
|
||||
|
||||
return topLevelNodes
|
||||
}
|
||||
|
||||
// 确保路径存在,创建所有必要的中间节点
|
||||
func _ensurePathExists(rootMap map[string]*RootFileList, path string) {
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果路径已存在,不需要再处理
|
||||
if _, exists := rootMap[path]; exists {
|
||||
return
|
||||
}
|
||||
|
||||
// 创建当前路径节点
|
||||
children := make([]RootFileList, 0)
|
||||
rootMap[path] = &RootFileList{
|
||||
ShareID: "",
|
||||
VirtualPath: path,
|
||||
NodeInfo: NodeInfoData{},
|
||||
Child: &children,
|
||||
}
|
||||
|
||||
// 处理父路径
|
||||
pathParts := strings.Split(path, "/")
|
||||
if len(pathParts) > 1 {
|
||||
parentPath := strings.Join(pathParts[:len(pathParts)-1], "/")
|
||||
|
||||
// 确保父路径存在
|
||||
_ensurePathExists(rootMap, parentPath)
|
||||
|
||||
// 将当前节点添加为父节点的子节点
|
||||
if parent, exists := rootMap[parentPath]; exists {
|
||||
*parent.Child = append(*parent.Child, *rootMap[path])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// _extractShareId 从URL或直接ID中提取分享ID
|
||||
func _extractShareId(input string) string {
|
||||
input = strings.TrimSpace(input)
|
||||
if strings.HasPrefix(input, "http") {
|
||||
regex := regexp.MustCompile(`/drive/s/([a-zA-Z0-9]+)`)
|
||||
if matches := regex.FindStringSubmatch(input); len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return input // 直接返回ID
|
||||
}
|
||||
|
||||
// _findRootFileByShareID 查找指定ShareID的配置
|
||||
func _findRootFileByShareID(rootFiles []RootFileList, shareID string) *RootFileList {
|
||||
for i, rf := range rootFiles {
|
||||
if rf.ShareID == shareID {
|
||||
return &rootFiles[i]
|
||||
}
|
||||
if rf.Child != nil && len(*rf.Child) > 0 {
|
||||
if found := _findRootFileByShareID(*rf.Child, shareID); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// _findNodeByPath 查找指定路径的节点
|
||||
func _findNodeByPath(rootFiles []RootFileList, path string) *RootFileList {
|
||||
for i, rf := range rootFiles {
|
||||
if rf.VirtualPath == path {
|
||||
return &rootFiles[i]
|
||||
}
|
||||
if rf.Child != nil && len(*rf.Child) > 0 {
|
||||
if found := _findNodeByPath(*rf.Child, path); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// _findShareByPath 根据路径查找分享和相对路径
|
||||
func _findShareByPath(rootFiles []RootFileList, path string) (*RootFileList, string) {
|
||||
// 完全匹配或子路径匹配
|
||||
for i, rf := range rootFiles {
|
||||
if rf.VirtualPath == path {
|
||||
return &rootFiles[i], ""
|
||||
}
|
||||
|
||||
if rf.VirtualPath != "" && strings.HasPrefix(path, rf.VirtualPath+"/") {
|
||||
relPath := strings.TrimPrefix(path, rf.VirtualPath+"/")
|
||||
|
||||
// 先检查子节点
|
||||
if rf.Child != nil && len(*rf.Child) > 0 {
|
||||
if child, childPath := _findShareByPath(*rf.Child, path); child != nil {
|
||||
return child, childPath
|
||||
}
|
||||
}
|
||||
|
||||
return &rootFiles[i], relPath
|
||||
}
|
||||
|
||||
// 递归检查子节点
|
||||
if rf.Child != nil && len(*rf.Child) > 0 {
|
||||
if child, childPath := _findShareByPath(*rf.Child, path); child != nil {
|
||||
return child, childPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查根目录分享
|
||||
for i, rf := range rootFiles {
|
||||
if rf.VirtualPath == "" && rf.ShareID != "" {
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
if len(parts) > 0 && parts[0] == rf.ShareID {
|
||||
if len(parts) > 1 {
|
||||
return &rootFiles[i], parts[1]
|
||||
}
|
||||
return &rootFiles[i], ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// _findShareAndPath 根据给定路径查找对应的ShareID和相对路径
|
||||
func (d *DoubaoShare) _findShareAndPath(dir model.Obj) (string, string, error) {
|
||||
dirPath := dir.GetPath()
|
||||
|
||||
// 如果是根目录,返回空值表示需要列出所有分享
|
||||
if dirPath == "/" || dirPath == "" {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
// 检查是否是 FileObject 类型,并获取 ShareID
|
||||
if fo, ok := dir.(*FileObject); ok && fo.ShareID != "" {
|
||||
// 直接使用对象中存储的 ShareID
|
||||
// 计算相对路径(移除前导斜杠)
|
||||
relativePath := strings.TrimPrefix(dirPath, "/")
|
||||
|
||||
// 递归查找对应的 RootFile
|
||||
found := _findRootFileByShareID(d.RootFiles, fo.ShareID)
|
||||
if found != nil {
|
||||
if found.VirtualPath != "" {
|
||||
// 如果此分享配置了路径前缀,需要考虑相对路径的计算
|
||||
if strings.HasPrefix(relativePath, found.VirtualPath) {
|
||||
return fo.ShareID, strings.TrimPrefix(relativePath, found.VirtualPath+"/"), nil
|
||||
}
|
||||
}
|
||||
return fo.ShareID, relativePath, nil
|
||||
}
|
||||
|
||||
// 如果找不到对应的 RootFile 配置,仍然使用对象中的 ShareID
|
||||
return fo.ShareID, relativePath, nil
|
||||
}
|
||||
|
||||
// 移除开头的斜杠
|
||||
cleanPath := strings.TrimPrefix(dirPath, "/")
|
||||
|
||||
// 先检查是否有直接匹配的根目录分享
|
||||
for _, rootFile := range d.RootFiles {
|
||||
if rootFile.VirtualPath == "" && rootFile.ShareID != "" {
|
||||
// 检查是否匹配当前路径的第一部分
|
||||
parts := strings.SplitN(cleanPath, "/", 2)
|
||||
if len(parts) > 0 && parts[0] == rootFile.ShareID {
|
||||
if len(parts) > 1 {
|
||||
return rootFile.ShareID, parts[1], nil
|
||||
}
|
||||
return rootFile.ShareID, "", nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查找匹配此路径的分享或虚拟目录
|
||||
share, relPath := _findShareByPath(d.RootFiles, cleanPath)
|
||||
if share != nil {
|
||||
return share.ShareID, relPath, nil
|
||||
}
|
||||
|
||||
log.Warnf("[doubao_share] No matching share path found: %s", dirPath)
|
||||
return "", "", fmt.Errorf("no matching share path found: %s", dirPath)
|
||||
}
|
||||
|
||||
// convertToFileObject 将File转换为FileObject
|
||||
func (d *DoubaoShare) convertToFileObject(file File, shareId string, relativePath string) *FileObject {
|
||||
// 构建文件对象
|
||||
obj := &FileObject{
|
||||
Object: model.Object{
|
||||
ID: file.ID,
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
Modified: time.Unix(file.UpdateTime, 0),
|
||||
Ctime: time.Unix(file.CreateTime, 0),
|
||||
IsFolder: file.NodeType == DirectoryType,
|
||||
Path: path.Join(relativePath, file.Name),
|
||||
},
|
||||
ShareID: shareId,
|
||||
Key: file.Key,
|
||||
NodeID: file.ID,
|
||||
NodeType: file.NodeType,
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// getFilesInPath 获取指定分享和路径下的文件
|
||||
func (d *DoubaoShare) getFilesInPath(ctx context.Context, shareId, nodeId, relativePath string) ([]model.Obj, error) {
|
||||
var (
|
||||
files []File
|
||||
err error
|
||||
)
|
||||
|
||||
// 调用overview接口获取分享链接信息 nodeId
|
||||
if nodeId == "" {
|
||||
files, err = d.getShareOverview(shareId, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get share link information: %w", err)
|
||||
}
|
||||
|
||||
result := make([]model.Obj, 0, len(files))
|
||||
for _, file := range files {
|
||||
result = append(result, d.convertToFileObject(file, shareId, "/"))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
} else {
|
||||
files, err = d.getFiles(shareId, nodeId, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get share file: %w", err)
|
||||
}
|
||||
|
||||
result := make([]model.Obj, 0, len(files))
|
||||
for _, file := range files {
|
||||
result = append(result, d.convertToFileObject(file, shareId, path.Join("/", relativePath)))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// listRootDirectory 处理根目录的内容展示
|
||||
func (d *DoubaoShare) listRootDirectory(ctx context.Context) ([]model.Obj, error) {
|
||||
objects := make([]model.Obj, 0)
|
||||
|
||||
// 分组处理:直接显示的分享内容 vs 虚拟目录
|
||||
var directShareIDs []string
|
||||
addedDirs := make(map[string]bool)
|
||||
|
||||
// 处理所有根节点
|
||||
for _, rootFile := range d.RootFiles {
|
||||
if rootFile.VirtualPath == "" && rootFile.ShareID != "" {
|
||||
// 无路径分享,记录ShareID以便后续获取内容
|
||||
directShareIDs = append(directShareIDs, rootFile.ShareID)
|
||||
} else {
|
||||
// 有路径的分享,显示第一级目录
|
||||
parts := strings.SplitN(rootFile.VirtualPath, "/", 2)
|
||||
firstLevel := parts[0]
|
||||
|
||||
// 避免重复添加同名目录
|
||||
if _, exists := addedDirs[firstLevel]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// 创建虚拟目录对象
|
||||
obj := &FileObject{
|
||||
Object: model.Object{
|
||||
ID: "",
|
||||
Name: firstLevel,
|
||||
Modified: time.Now(),
|
||||
Ctime: time.Now(),
|
||||
IsFolder: true,
|
||||
Path: path.Join("/", firstLevel),
|
||||
},
|
||||
ShareID: rootFile.ShareID,
|
||||
Key: "",
|
||||
NodeID: "",
|
||||
NodeType: DirectoryType,
|
||||
}
|
||||
objects = append(objects, obj)
|
||||
addedDirs[firstLevel] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 处理直接显示的分享内容
|
||||
for _, shareID := range directShareIDs {
|
||||
shareFiles, err := d.getFilesInPath(ctx, shareID, "", "")
|
||||
if err != nil {
|
||||
log.Warnf("[doubao_share] Failed to get list of files in share %s: %s", shareID, err)
|
||||
continue
|
||||
}
|
||||
objects = append(objects, shareFiles...)
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
// listVirtualDirectoryContent 列出虚拟目录的内容
|
||||
func (d *DoubaoShare) listVirtualDirectoryContent(dir model.Obj) ([]model.Obj, error) {
|
||||
dirPath := strings.TrimPrefix(dir.GetPath(), "/")
|
||||
objects := make([]model.Obj, 0)
|
||||
|
||||
// 递归查找此路径的节点
|
||||
node := _findNodeByPath(d.RootFiles, dirPath)
|
||||
|
||||
if node != nil && node.Child != nil {
|
||||
// 显示此节点的所有子节点
|
||||
for _, child := range *node.Child {
|
||||
// 计算显示名称(取路径的最后一部分)
|
||||
displayName := child.VirtualPath
|
||||
if child.VirtualPath != "" {
|
||||
parts := strings.Split(child.VirtualPath, "/")
|
||||
displayName = parts[len(parts)-1]
|
||||
} else if child.ShareID != "" {
|
||||
displayName = child.ShareID
|
||||
}
|
||||
|
||||
obj := &FileObject{
|
||||
Object: model.Object{
|
||||
ID: "",
|
||||
Name: displayName,
|
||||
Modified: time.Now(),
|
||||
Ctime: time.Now(),
|
||||
IsFolder: true,
|
||||
Path: path.Join("/", child.VirtualPath),
|
||||
},
|
||||
ShareID: child.ShareID,
|
||||
Key: "",
|
||||
NodeID: "",
|
||||
NodeType: DirectoryType,
|
||||
}
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
// generateContentDisposition 生成符合RFC 5987标准的Content-Disposition头部
|
||||
func generateContentDisposition(filename string) string {
|
||||
// 按照RFC 2047进行编码,用于filename部分
|
||||
encodedName := urlEncode(filename)
|
||||
|
||||
// 按照RFC 5987进行编码,用于filename*部分
|
||||
encodedNameRFC5987 := encodeRFC5987(filename)
|
||||
|
||||
return fmt.Sprintf("attachment; filename=\"%s\"; filename*=utf-8''%s",
|
||||
encodedName, encodedNameRFC5987)
|
||||
}
|
||||
|
||||
// encodeRFC5987 按照RFC 5987规范编码字符串,适用于HTTP头部参数中的非ASCII字符
|
||||
func encodeRFC5987(s string) string {
|
||||
var buf strings.Builder
|
||||
for _, r := range []byte(s) {
|
||||
// 根据RFC 5987,只有字母、数字和部分特殊符号可以不编码
|
||||
if (r >= 'a' && r <= 'z') ||
|
||||
(r >= 'A' && r <= 'Z') ||
|
||||
(r >= '0' && r <= '9') ||
|
||||
r == '-' || r == '.' || r == '_' || r == '~' {
|
||||
buf.WriteByte(r)
|
||||
} else {
|
||||
// 其他字符都需要百分号编码
|
||||
fmt.Fprintf(&buf, "%%%02X", r)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func urlEncode(s string) string {
|
||||
s = url.QueryEscape(s)
|
||||
s = strings.ReplaceAll(s, "+", "%20")
|
||||
return s
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user