feat: add cloud transfer and cache optimizations

This commit is contained in:
ShukeBta
2026-06-09 19:03:28 +08:00
parent a55fc502ca
commit 7d7f3cc758
27 changed files with 1972 additions and 117 deletions
+78
View File
@@ -3,8 +3,10 @@ package cloud
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
@@ -62,6 +64,47 @@ func TestQuarkListAndResolve(t *testing.T) {
}
}
func TestQuarkListPaginates(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/file/sort" {
t.Fatalf("unexpected path %s", r.URL.Path)
}
page, _ := strconv.Atoi(r.URL.Query().Get("_page"))
w.Write([]byte(`{"status":200,"code":0,"data":{"list":[` + quarkPagePayload(page) + `]}}`))
}))
defer srv.Close()
p, err := New(TypeQuark, map[string]any{"cookie": "kps=abc", "base": srv.URL}, srv.Client())
if err != nil {
t.Fatal(err)
}
entries, err := p.List(context.Background(), "0")
if err != nil {
t.Fatalf("list: %v", err)
}
if len(entries) != 101 {
t.Fatalf("entries = %d, want 101", len(entries))
}
if entries[100].ID != "f100" || entries[100].Name != "Movie.100.mkv" {
t.Fatalf("last entry wrong: %#v", entries[100])
}
}
func quarkPagePayload(page int) string {
count := 100
offset := 0
if page > 1 {
count = 1
offset = 100
}
items := make([]string, 0, count)
for i := 0; i < count; i++ {
n := offset + i
items = append(items, fmt.Sprintf(`{"fid":"f%d","file_name":"Movie.%03d.mkv","dir":false,"size":%d}`, n, n, n))
}
return strings.Join(items, ",")
}
func TestQuarkForce302(t *testing.T) {
p := newQuark(map[string]any{"cookie": "c", "force_302": "true"}, http.DefaultClient)
if p.proxy {
@@ -128,6 +171,41 @@ func Test115ListAndResolve(t *testing.T) {
}
}
func Test115ListPaginates(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/files" {
t.Fatalf("unexpected path %s", r.URL.Path)
}
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
count := 100
if offset > 0 {
count = 1
}
items := make([]string, 0, count)
for i := 0; i < count; i++ {
n := offset + i
items = append(items, fmt.Sprintf(`{"fid":"%d","n":"Movie.%03d.mkv","s":%d,"pc":"pick%d"}`, n, n, n, n))
}
w.Write([]byte(`{"state":true,"data":[` + strings.Join(items, ",") + `]}`))
}))
defer srv.Close()
p, err := New(Type115, map[string]any{"cookie": "UID=1; CID=2", "base": srv.URL}, srv.Client())
if err != nil {
t.Fatal(err)
}
entries, err := p.List(context.Background(), "0")
if err != nil {
t.Fatalf("list: %v", err)
}
if len(entries) != 101 {
t.Fatalf("entries = %d, want 101", len(entries))
}
if entries[100].ID != "100" || entries[100].PickCode != "pick100" {
t.Fatalf("last entry wrong: %#v", entries[100])
}
}
// Test115DownURLEndpointAndError exercises the live fetchDownURLPayload path:
// it must POST an m115-encrypted `data` body to /app/chrome/downurl?t=... and
// surface 115's error when state=false (no decryption needed for that branch).
+52 -45
View File
@@ -91,52 +91,59 @@ func (p *pan115Provider) List(ctx context.Context, dirID string) ([]FileEntry, e
if dirID == "" {
dirID = "0"
}
q := url.Values{}
q.Set("aid", "1")
q.Set("cid", dirID)
q.Set("o", "user_ptime")
q.Set("asc", "0")
q.Set("offset", "0")
q.Set("show_dir", "1")
q.Set("limit", "100")
q.Set("format", "json")
resp, err := p.get(ctx, p.webBase+"/files?"+q.Encode())
if err != nil {
return nil, err
}
defer resp.Body.Close()
var r struct {
State bool `json:"state"`
Error string `json:"error"`
Data []struct {
Fid string `json:"fid"` // file id (files only)
Cid string `json:"cid"` // category id (dirs use this)
N string `json:"n"` // name
S json.Number `json:"s"` // size
Pc string `json:"pc"` // pickcode
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return nil, fmt.Errorf("115: decode list: %w", err)
}
if !r.State {
return nil, fmt.Errorf("115: list failed: %s", r.Error)
}
out := make([]FileEntry, 0, len(r.Data))
for _, it := range r.Data {
isDir := it.Fid == ""
id := it.Fid
if isDir {
id = it.Cid
const pageSize = 100
out := make([]FileEntry, 0, pageSize)
for offset := 0; ; offset += pageSize {
q := url.Values{}
q.Set("aid", "1")
q.Set("cid", dirID)
q.Set("o", "user_ptime")
q.Set("asc", "0")
q.Set("offset", strconv.Itoa(offset))
q.Set("show_dir", "1")
q.Set("limit", strconv.Itoa(pageSize))
q.Set("format", "json")
resp, err := p.get(ctx, p.webBase+"/files?"+q.Encode())
if err != nil {
return nil, err
}
var r struct {
State bool `json:"state"`
Error string `json:"error"`
Data []struct {
Fid string `json:"fid"` // file id (files only)
Cid string `json:"cid"` // category id (dirs use this)
N string `json:"n"` // name
S json.Number `json:"s"` // size
Pc string `json:"pc"` // pickcode
} `json:"data"`
}
err = json.NewDecoder(resp.Body).Decode(&r)
_ = resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("115: decode list: %w", err)
}
if !r.State {
return nil, fmt.Errorf("115: list failed: %s", r.Error)
}
for _, it := range r.Data {
isDir := it.Fid == ""
id := it.Fid
if isDir {
id = it.Cid
}
size, _ := it.S.Int64()
out = append(out, FileEntry{
ID: id,
Name: it.N,
IsDir: isDir,
Size: size,
PickCode: it.Pc,
})
}
if len(r.Data) < pageSize {
break
}
size, _ := it.S.Int64()
out = append(out, FileEntry{
ID: id,
Name: it.N,
IsDir: isDir,
Size: size,
PickCode: it.Pc,
})
}
return out, nil
}
+49 -32
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
@@ -85,38 +86,54 @@ func (q *quarkProvider) List(ctx context.Context, dirID string) ([]FileEntry, er
if dirID == "" {
dirID = "0"
}
path := fmt.Sprintf("/file/sort?pr=ucpro&fr=pc&uc_param_str=&pdir_fid=%s&_page=1&_size=100&_fetch_total=1&_sort=file_type:asc,updated_at:desc", dirID)
resp, err := q.do(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var r quarkResp
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return nil, fmt.Errorf("quark: decode list: %w", err)
}
if r.Code != 0 && r.Status != 200 {
return nil, fmt.Errorf("quark: list failed: %s", r.Message)
}
var data struct {
List []struct {
Fid string `json:"fid"`
FileName string `json:"file_name"`
Dir bool `json:"dir"`
Size int64 `json:"size"`
} `json:"list"`
}
if err := json.Unmarshal(r.Data, &data); err != nil {
return nil, fmt.Errorf("quark: decode list data: %w", err)
}
out := make([]FileEntry, 0, len(data.List))
for _, it := range data.List {
out = append(out, FileEntry{
ID: it.Fid,
Name: it.FileName,
IsDir: it.Dir,
Size: it.Size,
})
const pageSize = 100
out := make([]FileEntry, 0, pageSize)
for page := 1; ; page++ {
query := url.Values{}
query.Set("pr", "ucpro")
query.Set("fr", "pc")
query.Set("uc_param_str", "")
query.Set("pdir_fid", dirID)
query.Set("_page", fmt.Sprint(page))
query.Set("_size", fmt.Sprint(pageSize))
query.Set("_fetch_total", "1")
query.Set("_sort", "file_type:asc,updated_at:desc")
path := "/file/sort?" + query.Encode()
resp, err := q.do(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
}
var r quarkResp
err = json.NewDecoder(resp.Body).Decode(&r)
_ = resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("quark: decode list: %w", err)
}
if r.Code != 0 && r.Status != 200 {
return nil, fmt.Errorf("quark: list failed: %s", r.Message)
}
var data struct {
List []struct {
Fid string `json:"fid"`
FileName string `json:"file_name"`
Dir bool `json:"dir"`
Size int64 `json:"size"`
} `json:"list"`
}
if err := json.Unmarshal(r.Data, &data); err != nil {
return nil, fmt.Errorf("quark: decode list data: %w", err)
}
for _, it := range data.List {
out = append(out, FileEntry{
ID: it.Fid,
Name: it.FileName,
IsDir: it.Dir,
Size: it.Size,
})
}
if len(data.List) < pageSize {
break
}
}
return out, nil
}