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

236 lines
5.8 KiB
Go

package pan115open
import (
"context"
"net/http"
"strconv"
"strings"
"sync"
"time"
"litepan/internal/domain"
"litepan/internal/driver"
"litepan/internal/httpx"
)
type Driver struct {
add Addition
client *http.Client
oauthBase string
intervalGate driver.RequestIntervalGate
persist driver.AuthPersistFunc
mu sync.Mutex
token string
refresh string
pickMu sync.RWMutex
pickBy map[string]string
}
var config = driver.Config{
Name: "115_open",
DisplayName: "115网盘Open",
Description: "115网盘官方API接入,支持文件管理、上传下载等功能",
CardTags: []string{"官方授权", "OAuth", "支持302", "SHA1"},
SortOrder: 1,
AuthLabel: "OAuth",
CardColor: "#22A7F0",
CardLogo: "/logos/115.png",
DefaultRoot: "0",
AuthType: driver.AuthToken,
OAuthName: "115网盘Open",
TokenLifetime: 2 * time.Hour,
RefreshAdvance: 15 * time.Minute,
ProvideHashes: []string{"sha1"},
}
func New() driver.Driver { return &Driver{} }
func init() { driver.Register(New) }
func (d *Driver) Config() driver.Config { return config }
func (d *Driver) GetAddition() any { return &d.add }
func (d *Driver) SetAuthCredentials(creds domain.AuthCredentials) {
d.mu.Lock()
defer d.mu.Unlock()
d.token = strings.TrimSpace(creds.AccessToken)
d.refresh = strings.TrimSpace(creds.RefreshToken)
}
func (d *Driver) SetAuthPersister(fn driver.AuthPersistFunc) { d.persist = fn }
func (d *Driver) SetOAuthServer(baseURL string) { d.oauthBase = baseURL }
func (d *Driver) SetRequestIntervalGate(gate driver.RequestIntervalGate) { d.intervalGate = gate }
func (d *Driver) Init(ctx context.Context) error {
if d.client == nil {
d.client = httpx.NewClient(httpx.ClientOptions{Timeout: 30 * time.Second})
}
d.mu.Lock()
token := d.token
refresh := d.refresh
d.mu.Unlock()
if token == "" && refresh == "" {
token = strings.TrimSpace(d.add.AccessToken)
refresh = strings.TrimSpace(d.add.RefreshToken)
}
if token == "" && refresh == "" {
return domain.Errorf(domain.CodeValidation, "access_token 与 refresh_token 不能都为空")
}
d.mu.Lock()
d.token = token
d.refresh = refresh
d.mu.Unlock()
if strings.TrimSpace(token) == "" {
if _, err := d.doRefresh(ctx); err != nil {
return err
}
}
return d.Ping(ctx)
}
func (d *Driver) Drop(context.Context) error {
httpx.CloseClient(d.client)
return nil
}
func (d *Driver) Ping(ctx context.Context) error {
query := urlValues(map[string]string{
"cid": d.rootID(),
"limit": "1",
"offset": "0",
"show_dir": "1",
})
var page listPageResp
return d.apiCallFull(ctx, http.MethodGet, pathList, query, nil, &page)
}
func listPageLimit(pageNum int, remaining int64) int {
if remaining <= 0 {
return 0
}
cap := listPageFollow
switch pageNum {
case 1:
cap = listPageFirst
case 2:
cap = listPageSecond
}
if int64(cap) > remaining {
return int(remaining)
}
return cap
}
func (d *Driver) ListFiles(ctx context.Context, parentID string) ([]domain.FileItem, error) {
parent := d.normalizeParent(parentID)
var items []domain.FileItem
offset := 0
fetched := 0
totalCount := int64(0)
pageNum := 0
for {
pageNum++
limit := listPageFirst
if pageNum > 1 {
remaining := totalCount - int64(fetched)
if totalCount > 0 && remaining <= 0 {
break
}
if totalCount > 0 {
limit = listPageLimit(pageNum, remaining)
} else {
limit = listPageLimit(pageNum, int64(listPageFollow))
}
if limit <= 0 {
break
}
}
query := urlValues(map[string]string{
"cid": parent,
"limit": strconv.Itoa(limit),
"offset": strconv.Itoa(offset),
"show_dir": "1",
})
var page listPageResp
if err := d.apiCallFull(ctx, http.MethodGet, pathList, query, nil, &page); err != nil {
return nil, err
}
if len(page.Data) == 0 {
break
}
if fetched == 0 {
totalCount = page.Count
}
for _, f := range page.Data {
if isTrashed(f) {
continue
}
d.rememberPickCode(f)
items = append(items, fileToItem(f))
}
fetched += len(page.Data)
if totalCount > 0 && int64(fetched) >= totalCount {
break
}
if len(page.Data) < limit {
break
}
offset += len(page.Data)
}
return items, nil
}
func (d *Driver) GetFileInfo(ctx context.Context, fileID string) (*domain.FileItem, error) {
id := strings.TrimSpace(fileID)
root := d.rootID()
if id == "" || id == "0" || id == root {
return &domain.FileItem{
ID: root,
Name: "根目录",
IsDir: true,
IDKind: domain.IDStable,
}, nil
}
query := urlValues(map[string]string{"file_id": id})
var info fileEntry
if err := d.apiCall(ctx, http.MethodGet, pathFileInfo, query, nil, &info); err != nil {
return nil, err
}
if info.entryID() == "" {
return nil, domain.Errf(domain.CodeNotFound)
}
d.rememberPickCode(info)
item := fileToItem(info)
return &item, nil
}
var (
_ driver.Driver = (*Driver)(nil)
_ driver.InfoGetter = (*Driver)(nil)
_ driver.Downloader = (*Driver)(nil)
_ driver.Deleter = (*Driver)(nil)
_ driver.Mover = (*Driver)(nil)
_ driver.Copier = (*Driver)(nil)
_ driver.Renamer = (*Driver)(nil)
_ driver.FolderCreator = (*Driver)(nil)
_ driver.AuthRefresher = (*Driver)(nil)
_ driver.AuthCredentialConsumer = (*Driver)(nil)
_ driver.AuthPersistConsumer = (*Driver)(nil)
_ driver.OAuthConsumer = (*Driver)(nil)
_ driver.RequestIntervalConsumer = (*Driver)(nil)
_ driver.LocalUploader = (*Driver)(nil)
_ driver.OfflineDownloadProvider = (*Driver)(nil)
_ driver.OfflineURLDownloader = (*Driver)(nil)
_ driver.OfflineTaskRefresher = (*Driver)(nil)
_ driver.OfflineTaskDeleter = (*Driver)(nil)
_ driver.OfflineTorrentDownloader = (*Driver)(nil)
)