Initial release of bdrclone

This commit is contained in:
2026-08-13 23:02:25 +08:00
commit d6d1956050
23 changed files with 3042 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
package baidu
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strconv"
"sync"
"testing"
"time"
"gitea.dddbg.com/youbin/bdrclone/internal/config"
)
func TestRemotePathWithRoot(t *testing.T) {
client := New(testConfig("token"))
for input, want := range map[string]string{
"/": "/apps/bdrclone", "docs/a.txt": "/apps/bdrclone/docs/a.txt", "/../a": "/apps/bdrclone/a",
} {
if got := client.RemotePath(input); got != want {
t.Errorf("RemotePath(%q) = %q, want %q", input, got, want)
}
}
if got := client.APIPath("/apps/bdrclone/docs/a.txt"); got != "/docs/a.txt" {
t.Fatalf("APIPath = %q", got)
}
}
func TestExpiredTokenRefreshesAndPersists(t *testing.T) {
var saved Token
var listCalls int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth/2.0/token":
if r.URL.Query().Get("refresh_token") != "refresh-old" {
t.Errorf("unexpected refresh token: %q", r.URL.Query().Get("refresh_token"))
}
fmt.Fprint(w, `{"access_token":"access-new","refresh_token":"refresh-new","expires_in":3600}`)
case "/rest/2.0/xpan/file":
listCalls++
if r.URL.Query().Get("access_token") != "access-new" {
t.Errorf("unexpected access token: %q", r.URL.Query().Get("access_token"))
}
if got := r.Header.Get("User-Agent"); got != "pan.baidu.com" {
t.Errorf("User-Agent = %q", got)
}
fmt.Fprint(w, `{"errno":0,"list":[]}`)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
cfg := testConfig("expired")
cfg.RefreshToken = "refresh-old"
cfg.ExpiresAt = time.Now().Add(-time.Hour)
client := New(cfg, WithEndpoints(server.URL, server.URL, server.URL), WithTokenSaver(func(token Token, _ time.Time) error {
saved = token
return nil
}))
if _, err := client.List(context.Background(), "/"); err != nil {
t.Fatal(err)
}
if saved.AccessToken != "access-new" || saved.RefreshToken != "refresh-new" {
t.Fatalf("saved token = %+v", saved)
}
if listCalls != 1 {
t.Fatalf("list calls = %d", listCalls)
}
}
func TestOpenUsesOfficialUserAgentAndRange(t *testing.T) {
var server *httptest.Server
metadataCalls := 0
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/rest/2.0/xpan/multimedia":
metadataCalls++
fmt.Fprintf(w, `{"errno":0,"list":[{"fs_id":42,"dlink":%q}]}`, server.URL+"/download?x=1")
case "/download":
if got := r.Header.Get("User-Agent"); got != "pan.baidu.com" {
t.Errorf("User-Agent = %q", got)
}
if got := r.Header.Get("Range"); got != "bytes=2-5" {
t.Errorf("Range = %q", got)
}
if got := r.URL.Query().Get("access_token"); got != "token" {
t.Errorf("access_token = %q", got)
}
w.WriteHeader(http.StatusPartialContent)
fmt.Fprint(w, "2345")
default:
http.NotFound(w, r)
}
}))
defer server.Close()
client := New(testConfig("token"), WithEndpoints(server.URL, server.URL, server.URL))
body, err := client.Open(context.Background(), File{FSID: 42, Path: "/a"}, 2, 4)
if err != nil {
t.Fatal(err)
}
defer body.Close()
b, err := io.ReadAll(body)
if err != nil {
t.Fatal(err)
}
if string(b) != "2345" {
t.Fatalf("body = %q", b)
}
body, err = client.Open(context.Background(), File{FSID: 42, Path: "/a"}, 2, 4)
if err != nil {
t.Fatal(err)
}
body.Close()
if metadataCalls != 1 {
t.Fatalf("download metadata calls = %d, want cached URL to be reused", metadataCalls)
}
}
func TestUploadMultipartFlow(t *testing.T) {
content := append(bytes.Repeat([]byte("a"), int(defaultPartSize)), []byte("tail")...)
file, err := os.CreateTemp(t.TempDir(), "upload-*")
if err != nil {
t.Fatal(err)
}
if _, err := file.Write(content); err != nil {
t.Fatal(err)
}
var mu sync.Mutex
parts := map[int][]byte{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/rest/2.0/xpan/file":
if err := r.ParseForm(); err != nil {
t.Error(err)
}
switch r.URL.Query().Get("method") {
case "precreate":
if r.Form.Get("path") != "/apps/bdrclone/remote.bin" {
t.Errorf("precreate path = %q", r.Form.Get("path"))
}
var blocks []string
if err := json.Unmarshal([]byte(r.Form.Get("block_list")), &blocks); err != nil || len(blocks) != 2 {
t.Errorf("block list = %q, err=%v", r.Form.Get("block_list"), err)
}
fmt.Fprint(w, `{"errno":0,"return_type":1,"uploadid":"upload-1","block_list":[0,1]}`)
case "create":
if r.Form.Get("uploadid") != "upload-1" {
t.Errorf("create uploadid = %q", r.Form.Get("uploadid"))
}
fmt.Fprint(w, `{"errno":0,"fs_id":99,"path":"/apps/bdrclone/remote.bin","server_filename":"remote.bin","size":4194308}`)
default:
http.Error(w, "unexpected method", http.StatusBadRequest)
}
case "/rest/2.0/pcs/superfile2":
part, _ := strconv.Atoi(r.URL.Query().Get("partseq"))
if r.URL.Query().Get("uploadid") != "upload-1" {
t.Errorf("part uploadid = %q", r.URL.Query().Get("uploadid"))
}
if err := r.ParseMultipartForm(defaultPartSize + 1024); err != nil {
t.Error(err)
return
}
partFile, _, err := r.FormFile("file")
if err != nil {
t.Error(err)
return
}
b, err := io.ReadAll(partFile)
partFile.Close()
if err != nil {
t.Error(err)
}
mu.Lock()
parts[part] = b
mu.Unlock()
fmt.Fprint(w, `{"md5":"ok"}`)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
cfg := testConfig("token")
cfg.PartSize = defaultPartSize
cfg.UploadParts = 2
client := New(cfg, WithEndpoints(server.URL, server.URL, server.URL))
entry, err := client.Upload(context.Background(), file, int64(len(content)), "/remote.bin", time.Unix(1_700_000_000, 0), nil)
if err != nil {
t.Fatal(err)
}
if entry.FSID != 99 {
t.Fatalf("entry = %+v", entry)
}
mu.Lock()
defer mu.Unlock()
if !bytes.Equal(parts[0], content[:defaultPartSize]) || !bytes.Equal(parts[1], content[defaultPartSize:]) {
t.Fatalf("uploaded parts do not match input: sizes %d, %d", len(parts[0]), len(parts[1]))
}
}
func testConfig(token string) *config.Config {
return &config.Config{
ClientID: "client", ClientSecret: "secret", AccessToken: token,
Root: "/apps/bdrclone", UserAgent: "pan.baidu.com", UploadParts: 1,
}
}