Files
2026-07-27 19:31:43 +08:00

348 lines
8.8 KiB
Go

package pan123open
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strings"
"time"
"litepan/internal/domain"
"litepan/internal/driver"
)
const (
pathTrash = "/api/v1/file/trash"
pathMove = "/api/v1/file/move"
deleteChunk = 100
copyChunk = 100
asyncCopyStatusDone = "2"
asyncCopyStatusFail = "3"
downloadPartSize = 10 * 1024 * 1024
downloadConcurrency = 3
)
func (d *Driver) ResolveDownload(ctx context.Context, req driver.DownloadRequest) (*domain.DownloadInfo, error) {
params := url.Values{}
params.Set("fileId", strings.TrimSpace(req.FileID))
var out struct {
DownloadURL string `json:"downloadUrl"`
}
if err := d.apiCall(ctx, http.MethodGet, pathDownload, params, nil, &out); err != nil {
return nil, err
}
if out.DownloadURL == "" {
return nil, domain.Errorf(domain.CodeDriverError, "获取下载链接失败:响应缺少 downloadUrl")
}
mode := domain.DownloadRedirect
if strings.EqualFold(strings.TrimSpace(d.add.DownloadMode), "proxy") {
mode = domain.DownloadProxy
}
return &domain.DownloadInfo{
URL: out.DownloadURL,
Mode: mode,
ChunkSize: downloadPartSize,
Concurrency: downloadConcurrency,
}, nil
}
func normalizeFileIDs(fileIDs []string) []string {
out := make([]string, 0, len(fileIDs))
for _, id := range fileIDs {
id = strings.TrimSpace(id)
if id != "" {
out = append(out, id)
}
}
return out
}
func chunkStrings(items []string, size int) [][]string {
if size <= 0 {
size = 100
}
var chunks [][]string
for i := 0; i < len(items); i += size {
end := i + size
if end > len(items) {
end = len(items)
}
chunks = append(chunks, items[i:end])
}
return chunks
}
func (d *Driver) CopyFiles(ctx context.Context, fileIDs []string, targetParentID string) error {
ids := normalizeFileIDs(fileIDs)
if len(ids) == 0 {
return nil
}
target := d.normalizeParent(targetParentID)
if err := d.guardCopySameDirectory(ctx, ids, target); err != nil {
return err
}
if len(ids) == 1 {
_, err := d.copySingleFile(ctx, ids[0], target)
return mapCopyError(err)
}
for _, chunk := range chunkStrings(ids, copyChunk) {
if err := d.copyMultipleFiles(ctx, chunk, target); err != nil {
return mapCopyError(err)
}
}
return nil
}
func (d *Driver) guardCopySameDirectory(ctx context.Context, ids []string, target string) error {
var parents []string
for _, id := range ids {
pid, err := d.fileParentID(ctx, id)
if err != nil || pid == "" {
continue
}
parents = append(parents, d.normalizeParent(pid))
}
if len(parents) == 0 {
return nil
}
for _, p := range parents {
if p != target {
return nil
}
}
return domain.Errorf(domain.CodeValidation, "123云盘Open不支持复制到同一目录")
}
func (d *Driver) fileParentID(ctx context.Context, fileID string) (string, error) {
id := strings.TrimSpace(fileID)
if id == "" {
return "", domain.Errorf(domain.CodeValidation, "file_id 不能为空")
}
params := url.Values{}
params.Set("fileID", id)
var raw json.RawMessage
if err := d.apiCall(ctx, http.MethodGet, pathFileDetail, params, nil, &raw); err != nil {
return "", err
}
entry, err := parseFileDetail(raw)
if err != nil {
return "", domain.Wrap(domain.CodeDriverError, err)
}
return entry.parentID(), nil
}
func (d *Driver) copySingleFile(ctx context.Context, fileID, targetParentID string) (string, error) {
body := map[string]any{
"fileId": fileID,
"targetDirId": targetParentID,
}
var out copyResp
err := d.apiCall(ctx, http.MethodPost, pathCopy, nil, body, &out)
if err != nil {
body["targetDirID"] = targetParentID
delete(body, "targetDirId")
err = d.apiCall(ctx, http.MethodPost, pathCopy, nil, body, &out)
if err != nil {
return "", err
}
}
return out.targetFileID(), nil
}
func (d *Driver) copyMultipleFiles(ctx context.Context, fileIDs []string, targetParentID string) error {
body := map[string]any{
"fileIds": fileIDs,
"targetDirId": targetParentID,
}
var out asyncCopyResp
err := d.apiCall(ctx, http.MethodPost, pathAsyncCopy, nil, body, &out)
if err != nil {
body["targetDirID"] = targetParentID
delete(body, "targetDirId")
err = d.apiCall(ctx, http.MethodPost, pathAsyncCopy, nil, body, &out)
if err != nil {
return err
}
}
return d.waitAsyncCopyDone(ctx, out.taskID())
}
func (d *Driver) waitAsyncCopyDone(ctx context.Context, taskID string) error {
if strings.TrimSpace(taskID) == "" {
return domain.Errorf(domain.CodeDriverError, "批量复制任务缺少 taskId")
}
for attempt := 0; attempt < 30; attempt++ {
params := url.Values{}
params.Set("taskId", taskID)
var out asyncCopyProcessResp
if err := d.apiCall(ctx, http.MethodGet, pathAsyncCopyProcess, params, nil, &out); err != nil {
return err
}
switch out.status() {
case asyncCopyStatusDone:
return nil
case asyncCopyStatusFail:
msg := strings.TrimSpace(out.Message)
if msg == "" {
msg = strings.TrimSpace(out.FailReason)
}
if msg == "" {
msg = "批量复制任务失败"
}
return domain.Errorf(domain.CodeDriverError, "%s", msg)
}
delay := 500 * time.Millisecond
if attempt >= 10 {
delay = time.Second
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
}
}
return domain.Errorf(domain.CodeDriverError, "批量复制任务超时,请稍后刷新目标目录查看结果")
}
type copyResp struct {
TargetFileID json.Number `json:"targetFileId"`
TargetFileID2 json.Number `json:"targetFileID"`
}
func (r copyResp) targetFileID() string {
if s := r.TargetFileID2.String(); s != "" && s != "0" {
return s
}
return r.TargetFileID.String()
}
type asyncCopyResp struct {
TaskID json.Number `json:"taskId"`
TaskID2 json.Number `json:"taskID"`
}
func (r asyncCopyResp) taskID() string {
if s := r.TaskID2.String(); s != "" && s != "0" {
return s
}
return r.TaskID.String()
}
type asyncCopyProcessResp struct {
Status json.Number `json:"status"`
TaskStatus json.Number `json:"taskStatus"`
Process json.Number `json:"process"`
Message string `json:"message"`
FailReason string `json:"failReason"`
}
func (r asyncCopyProcessResp) status() string {
for _, n := range []json.Number{r.Status, r.TaskStatus, r.Process} {
if s := n.String(); s != "" && s != "0" {
return s
}
}
return ""
}
func mapCopyError(err error) error {
if err == nil {
return nil
}
if ae, ok := domain.AsAppError(err); ok {
if strings.Contains(ae.Message, "不能复制目录") {
return domain.Errorf(domain.CodeValidation, "123云盘官方Open接口暂不支持复制文件夹")
}
return err
}
if strings.Contains(err.Error(), "不能复制目录") {
return domain.Errorf(domain.CodeValidation, "123云盘官方Open接口暂不支持复制文件夹")
}
return err
}
func (d *Driver) DeleteFiles(ctx context.Context, fileIDs []string) error {
ids := normalizeFileIDs(fileIDs)
if len(ids) == 0 {
return nil
}
for _, chunk := range chunkStrings(ids, deleteChunk) {
body := map[string]any{"fileIDs": chunk}
if err := d.apiCall(ctx, http.MethodPost, pathTrash, nil, body, nil); err != nil {
return err
}
}
return nil
}
func (d *Driver) MoveFiles(ctx context.Context, fileIDs []string, targetParentID, _ string) error {
ids := normalizeFileIDs(fileIDs)
if len(ids) == 0 {
return nil
}
target := d.normalizeParent(targetParentID)
for _, chunk := range chunkStrings(ids, deleteChunk) {
body := map[string]any{
"fileIDs": chunk,
"toParentFileId": target,
}
if err := d.apiCall(ctx, http.MethodPost, pathMove, nil, body, nil); err != nil {
return err
}
}
return nil
}
func (d *Driver) RenameFile(ctx context.Context, fileID, newName string) error {
id := strings.TrimSpace(fileID)
name := strings.TrimSpace(newName)
if id == "" {
return domain.Errorf(domain.CodeValidation, "file_id 不能为空")
}
if name == "" {
return domain.Errorf(domain.CodeValidation, "新名称不能为空")
}
body := map[string]any{
"fileId": id,
"fileName": name,
}
return d.apiCall(ctx, http.MethodPut, pathRename, nil, body, nil)
}
type mkdirResp struct {
DirID json.Number `json:"dirID"`
DirId json.Number `json:"dirId"`
FileID json.Number `json:"fileId"`
}
func (d *Driver) CreateFolder(ctx context.Context, parentID, name string) (*domain.FileItem, error) {
folderName := strings.TrimSpace(name)
if folderName == "" {
return nil, domain.Errorf(domain.CodeValidation, "文件夹名称不能为空")
}
parent := d.normalizeParent(parentID)
body := map[string]any{
"name": folderName,
"parentID": parent,
}
var out mkdirResp
if err := d.apiCall(ctx, http.MethodPost, pathMkdir, nil, body, &out); err != nil {
return nil, err
}
folderID := strings.TrimSpace(out.DirID.String())
if folderID == "" {
folderID = strings.TrimSpace(out.DirId.String())
}
if folderID == "" {
folderID = strings.TrimSpace(out.FileID.String())
}
return &domain.FileItem{
ID: folderID,
Name: folderName,
IsDir: true,
}, nil
}