219 lines
6.6 KiB
Go
219 lines
6.6 KiB
Go
package baidu
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var ErrNotFound = errors.New("remote path not found")
|
|
|
|
func (c *Client) List(ctx context.Context, name string) ([]File, error) {
|
|
remote := c.RemotePath(name)
|
|
var result []File
|
|
for start := 0; ; start += 1000 {
|
|
var response struct {
|
|
List []File `json:"list"`
|
|
}
|
|
q := url.Values{
|
|
"method": {"list"}, "dir": {remote}, "start": {strconv.Itoa(start)},
|
|
"limit": {"1000"}, "order": {"name"},
|
|
}
|
|
if err := c.request(ctx, http.MethodGet, c.xpanFile(), q, nil, &response); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, response.List...)
|
|
if len(response.List) < 1000 {
|
|
return result, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) Stat(ctx context.Context, name string) (File, error) {
|
|
remote := c.RemotePath(name)
|
|
if remote == c.root || (c.root == "/" && remote == "/") {
|
|
return File{Path: remote, ServerFilename: path.Base(remote), IsDir: 1}, nil
|
|
}
|
|
parent := path.Dir(c.APIPath(remote))
|
|
entries, err := c.List(ctx, parent)
|
|
if err != nil {
|
|
return File{}, err
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.Path == remote || entry.Name() == path.Base(remote) {
|
|
return entry, nil
|
|
}
|
|
}
|
|
return File{}, fmt.Errorf("%w: %s", ErrNotFound, name)
|
|
}
|
|
|
|
func (c *Client) Mkdir(ctx context.Context, name string) (File, error) {
|
|
form := url.Values{
|
|
"path": {c.RemotePath(name)}, "size": {"0"}, "isdir": {"1"}, "rtype": {"3"},
|
|
}
|
|
var result File
|
|
if err := c.request(ctx, http.MethodPost, c.xpanFile(), url.Values{"method": {"create"}}, form, &result); err != nil {
|
|
return File{}, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) MkdirAll(ctx context.Context, name string) error {
|
|
target := path.Clean("/" + strings.TrimPrefix(name, "/"))
|
|
if target == "/" {
|
|
return nil
|
|
}
|
|
current := "/"
|
|
for _, component := range strings.Split(strings.TrimPrefix(target, "/"), "/") {
|
|
current = path.Join(current, component)
|
|
if err := c.EnsureDirectory(ctx, current); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) EnsureDirectory(ctx context.Context, name string) error {
|
|
entry, err := c.Stat(ctx, name)
|
|
if err == nil {
|
|
if !entry.IsDirectory() {
|
|
return fmt.Errorf("remote path exists and is not a directory: %s", name)
|
|
}
|
|
return nil
|
|
}
|
|
if !errors.Is(err, ErrNotFound) {
|
|
return fmt.Errorf("check remote directory %s: %w", name, err)
|
|
}
|
|
if _, err := c.Mkdir(ctx, name); err != nil {
|
|
return fmt.Errorf("create remote directory %s: %w", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Delete(ctx context.Context, name string) error {
|
|
remote := c.RemotePath(name)
|
|
if remote == c.root {
|
|
return errors.New("refusing to delete the configured remote root")
|
|
}
|
|
paths, _ := json.Marshal([]string{remote})
|
|
return c.manage(ctx, "delete", string(paths))
|
|
}
|
|
|
|
func (c *Client) Move(ctx context.Context, source, destination string) error {
|
|
src := c.RemotePath(source)
|
|
if src == c.root {
|
|
return errors.New("refusing to move the configured remote root")
|
|
}
|
|
dst := c.RemotePath(destination)
|
|
items := []map[string]string{{"path": src, "dest": path.Dir(dst), "newname": path.Base(dst)}}
|
|
b, _ := json.Marshal(items)
|
|
return c.manage(ctx, "move", string(b))
|
|
}
|
|
|
|
func (c *Client) Copy(ctx context.Context, source, destination string) error {
|
|
src := c.RemotePath(source)
|
|
dst := c.RemotePath(destination)
|
|
items := []map[string]string{{"path": src, "dest": path.Dir(dst), "newname": path.Base(dst)}}
|
|
b, _ := json.Marshal(items)
|
|
return c.manage(ctx, "copy", string(b))
|
|
}
|
|
|
|
func (c *Client) manage(ctx context.Context, operation, fileList string) error {
|
|
q := url.Values{"method": {"filemanager"}, "opera": {operation}}
|
|
form := url.Values{"async": {"0"}, "ondup": {"fail"}, "filelist": {fileList}}
|
|
return c.request(ctx, http.MethodPost, c.xpanFile(), q, form, nil)
|
|
}
|
|
|
|
func (c *Client) Quota(ctx context.Context) (Quota, error) {
|
|
var result Quota
|
|
err := c.request(ctx, http.MethodGet, c.apiBase+"/api/quota", nil, nil, &result)
|
|
return result, err
|
|
}
|
|
|
|
func (c *Client) DownloadURL(ctx context.Context, file File) (string, error) {
|
|
if file.FSID == 0 {
|
|
return "", errors.New("file has no fs_id")
|
|
}
|
|
c.downloadMu.Lock()
|
|
cached, ok := c.downloadURL[file.FSID]
|
|
c.downloadMu.Unlock()
|
|
if ok && time.Now().Before(cached.expiresAt) {
|
|
return cached.url, nil
|
|
}
|
|
ids, _ := json.Marshal([]int64{file.FSID})
|
|
var response struct {
|
|
List []File `json:"list"`
|
|
}
|
|
q := url.Values{"method": {"filemetas"}, "fsids": {string(ids)}, "dlink": {"1"}}
|
|
if err := c.request(ctx, http.MethodGet, c.xpanMultimedia(), q, nil, &response); err != nil {
|
|
return "", err
|
|
}
|
|
if len(response.List) == 0 || response.List[0].DLink == "" {
|
|
return "", errors.New("Baidu returned no download URL")
|
|
}
|
|
u, err := url.Parse(response.List[0].DLink)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parse download URL: %w", err)
|
|
}
|
|
q2 := u.Query()
|
|
q2.Set("access_token", c.token())
|
|
u.RawQuery = q2.Encode()
|
|
result := u.String()
|
|
c.downloadMu.Lock()
|
|
c.downloadURL[file.FSID] = cachedDownloadURL{url: result, expiresAt: time.Now().Add(50 * time.Minute)}
|
|
c.downloadMu.Unlock()
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) Open(ctx context.Context, file File, offset, length int64) (io.ReadCloser, error) {
|
|
for attempt := 0; attempt < 2; attempt++ {
|
|
downloadURL, err := c.DownloadURL(ctx, file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
if offset > 0 || length > 0 {
|
|
end := ""
|
|
if length > 0 {
|
|
end = strconv.FormatInt(offset+length-1, 10)
|
|
}
|
|
req.Header.Set("Range", "bytes="+strconv.FormatInt(offset, 10)+"-"+end)
|
|
}
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("download %s: %w", file.Path, err)
|
|
}
|
|
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusPartialContent {
|
|
if resp.StatusCode == http.StatusOK && offset > 0 {
|
|
if _, err := io.CopyN(io.Discard, resp.Body, offset); err != nil {
|
|
resp.Body.Close()
|
|
return nil, fmt.Errorf("seek download %s to %d: %w", file.Path, offset, err)
|
|
}
|
|
}
|
|
return resp.Body, nil
|
|
}
|
|
b, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
|
resp.Body.Close()
|
|
if attempt == 0 && (resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusNotFound) {
|
|
c.downloadMu.Lock()
|
|
delete(c.downloadURL, file.FSID)
|
|
c.downloadMu.Unlock()
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("download %s: HTTP %s: %s", file.Path, resp.Status, strings.TrimSpace(string(b)))
|
|
}
|
|
return nil, fmt.Errorf("download %s failed after refreshing its temporary URL", file.Path)
|
|
}
|