745 lines
18 KiB
Go
745 lines
18 KiB
Go
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
|
||
}
|