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) }