package agfs
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
var (
ErrNotSupported = fmt.Errorf("operation not supported")
)
type Client struct {
baseURL string
httpClient *http.Client
}
func NewClient(baseURL string) *Client {
return &Client{
baseURL: normalizeBaseURL(baseURL),
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
func NewClientWithHTTPClient(baseURL string, httpClient *http.Client) *Client {
return &Client{
baseURL: normalizeBaseURL(baseURL),
httpClient: httpClient,
}
}
func normalizeBaseURL(baseURL string) string {
if len(baseURL) > 0 && baseURL[len(baseURL)-1] == '/' {
baseURL = baseURL[:len(baseURL)-1]
}
if !strings.Contains(baseURL, "://") {
return baseURL
}
if len(baseURL) < 7 || baseURL[len(baseURL)-7:] != "/api/v1" {
baseURL = baseURL + "/api/v1"
}
return baseURL
}
type ErrorResponse struct {
Error string `json:"error"`
}
type SuccessResponse struct {
Message string `json:"message"`
}
type FileInfoResponse struct {
Name string `json:"name"`
Size int64 `json:"size"`
Mode uint32 `json:"mode"`
ModTime string `json:"modTime"`
IsDir bool `json:"isDir"`
Meta MetaData `json:"meta,omitempty"`
}
type ListResponse struct {
Files []FileInfoResponse `json:"files"`
}
type RenameRequest struct {
NewPath string `json:"newPath"`
}
type ChmodRequest struct {
Mode uint32 `json:"mode"`
}
func (c *Client) doRequest(method, endpoint string, query url.Values, body io.Reader) (*http.Response, error) {
u := c.baseURL + endpoint
if len(query) > 0 {
u += "?" + query.Encode()
}
req, err := http.NewRequest(method, u, body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
return resp, nil
}
func (c *Client) handleErrorResponse(resp *http.Response) error {
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
if resp.StatusCode == http.StatusNotImplemented {
return ErrNotSupported
}
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
func (c *Client) Create(path string) error {
query := url.Values{}
query.Set("path", path)
resp, err := c.doRequest(http.MethodPost, "/files", query, nil)
if err != nil {
return err
}
return c.handleErrorResponse(resp)
}
func (c *Client) Mkdir(path string, perm uint32) error {
query := url.Values{}
query.Set("path", path)
query.Set("mode", fmt.Sprintf("%o", perm))
resp, err := c.doRequest(http.MethodPost, "/directories", query, nil)
if err != nil {
return err
}
return c.handleErrorResponse(resp)
}
func (c *Client) Remove(path string) error {
query := url.Values{}
query.Set("path", path)
query.Set("recursive", "false")
resp, err := c.doRequest(http.MethodDelete, "/files", query, nil)
if err != nil {
return err
}
return c.handleErrorResponse(resp)
}
func (c *Client) RemoveAll(path string) error {
query := url.Values{}
query.Set("path", path)
query.Set("recursive", "true")
resp, err := c.doRequest(http.MethodDelete, "/files", query, nil)
if err != nil {
return err
}
return c.handleErrorResponse(resp)
}
func (c *Client) Read(path string, offset int64, size int64) ([]byte, error) {
query := url.Values{}
query.Set("path", path)
if offset > 0 {
query.Set("offset", fmt.Sprintf("%d", offset))
}
if size >= 0 {
query.Set("size", fmt.Sprintf("%d", size))
}
resp, err := c.doRequest(http.MethodGet, "/files", query, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return data, nil
}
func (c *Client) Write(path string, data []byte) ([]byte, error) {
return c.WriteWithRetry(path, data, 3)
}
func (c *Client) WriteWithRetry(path string, data []byte, maxRetries int) ([]byte, error) {
query := url.Values{}
query.Set("path", path)
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
resp, err := c.doRequest(http.MethodPut, "/files", query, bytes.NewReader(data))
if err != nil {
lastErr = err
if isRetryableError(err) && attempt < maxRetries {
waitTime := time.Duration(1<<uint(attempt)) * time.Second
fmt.Printf("⚠ Upload failed (attempt %d/%d): %v\n", attempt+1, maxRetries+1, err)
fmt.Printf(" Retrying in %v...\n", waitTime)
time.Sleep(waitTime)
continue
}
if attempt >= maxRetries {
fmt.Printf("✗ Upload failed after %d attempts\n", maxRetries+1)
}
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
lastErr = fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
if resp.StatusCode >= 500 && resp.StatusCode < 600 && attempt < maxRetries {
waitTime := time.Duration(1<<uint(attempt)) * time.Second
fmt.Printf("⚠ Server error %d (attempt %d/%d)\n", resp.StatusCode, attempt+1, maxRetries+1)
fmt.Printf(" Retrying in %v...\n", waitTime)
time.Sleep(waitTime)
continue
}
if attempt >= maxRetries {
fmt.Printf("✗ Upload failed after %d attempts\n", maxRetries+1)
}
return nil, lastErr
}
var successResp SuccessResponse
if err := json.NewDecoder(resp.Body).Decode(&successResp); err != nil {
return nil, fmt.Errorf("failed to decode success response: %w", err)
}
if attempt > 0 {
fmt.Printf("✓ Upload succeeded after %d retry(ies)\n", attempt)
}
return []byte(successResp.Message), nil
}
return nil, lastErr
}
func isRetryableError(err error) bool {
if err == nil {
return false
}
if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
return true
}
if netErr, ok := err.(interface{ Temporary() bool }); ok && netErr.Temporary() {
return true
}
errStr := err.Error()
return strings.Contains(errStr, "connection refused") ||
strings.Contains(errStr, "connection reset") ||
strings.Contains(errStr, "broken pipe") ||
strings.Contains(errStr, "timeout")
}
func (c *Client) ReadDir(path string) ([]FileInfo, error) {
query := url.Values{}
query.Set("path", path)
resp, err := c.doRequest(http.MethodGet, "/directories", query, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
var listResp ListResponse
if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil {
return nil, fmt.Errorf("failed to decode list response: %w", err)
}
files := make([]FileInfo, 0, len(listResp.Files))
for _, f := range listResp.Files {
modTime, _ := time.Parse(time.RFC3339Nano, f.ModTime)
files = append(files, FileInfo{
Name: f.Name,
Size: f.Size,
Mode: f.Mode,
ModTime: modTime,
IsDir: f.IsDir,
Meta: f.Meta,
})
}
return files, nil
}
func (c *Client) Stat(path string) (*FileInfo, error) {
query := url.Values{}
query.Set("path", path)
resp, err := c.doRequest(http.MethodGet, "/stat", query, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
var fileInfo FileInfoResponse
if err := json.NewDecoder(resp.Body).Decode(&fileInfo); err != nil {
return nil, fmt.Errorf("failed to decode file info response: %w", err)
}
modTime, _ := time.Parse(time.RFC3339Nano, fileInfo.ModTime)
return &FileInfo{
Name: fileInfo.Name,
Size: fileInfo.Size,
Mode: fileInfo.Mode,
ModTime: modTime,
IsDir: fileInfo.IsDir,
Meta: fileInfo.Meta,
}, nil
}
func (c *Client) Rename(oldPath, newPath string) error {
query := url.Values{}
query.Set("path", oldPath)
reqBody := RenameRequest{NewPath: newPath}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal rename request: %w", err)
}
resp, err := c.doRequest(http.MethodPost, "/rename", query, bytes.NewReader(jsonData))
if err != nil {
return err
}
return c.handleErrorResponse(resp)
}
func (c *Client) Chmod(path string, mode uint32) error {
query := url.Values{}
query.Set("path", path)
reqBody := ChmodRequest{Mode: mode}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal chmod request: %w", err)
}
resp, err := c.doRequest(http.MethodPost, "/chmod", query, bytes.NewReader(jsonData))
if err != nil {
return err
}
return c.handleErrorResponse(resp)
}
func (c *Client) Health() error {
resp, err := c.doRequest(http.MethodGet, "/health", nil, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("health check failed with status: %d", resp.StatusCode)
}
return nil
}
type CapabilitiesResponse struct {
Version string `json:"version"`
Features []string `json:"features"`
}
func (c *Client) GetCapabilities() (*CapabilitiesResponse, error) {
resp, err := c.doRequest(http.MethodGet, "/capabilities", nil, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
return &CapabilitiesResponse{
Version: "unknown",
Features: []string{},
}, nil
}
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
var caps CapabilitiesResponse
if err := json.NewDecoder(resp.Body).Decode(&caps); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &caps, nil
}
func (c *Client) ReadStream(path string) (io.ReadCloser, error) {
query := url.Values{}
query.Set("path", path)
query.Set("stream", "true")
streamClient := &http.Client{
Timeout: 0,
}
reqURL := fmt.Sprintf("%s/files?%s", c.baseURL, query.Encode())
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := streamClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
return resp.Body, nil
}
type GrepRequest struct {
Path string `json:"path"`
Pattern string `json:"pattern"`
Recursive bool `json:"recursive"`
CaseInsensitive bool `json:"case_insensitive"`
NodeLimit int `json:"node_limit"`
}
type GrepMatch struct {
File string `json:"file"`
Line int `json:"line"`
Content string `json:"content"`
}
type GrepResponse struct {
Matches []GrepMatch `json:"matches"`
Count int `json:"count"`
}
type DigestRequest struct {
Algorithm string `json:"algorithm"`
Path string `json:"path"`
}
type DigestResponse struct {
Algorithm string `json:"algorithm"`
Path string `json:"path"`
Digest string `json:"digest"`
}
func (c *Client) Grep(path, pattern string, recursive, caseInsensitive bool, nodeLimit int) (*GrepResponse, error) {
nl := 0
if nodeLimit > 0 {
nl = nodeLimit
}
reqBody := GrepRequest{
Path: path,
Pattern: pattern,
Recursive: recursive,
CaseInsensitive: caseInsensitive,
NodeLimit: nl,
}
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
reqURL := fmt.Sprintf("%s/grep", c.baseURL)
req, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
var grepResp GrepResponse
if err := json.NewDecoder(resp.Body).Decode(&grepResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &grepResp, nil
}
func (c *Client) Digest(path, algorithm string) (*DigestResponse, error) {
reqBody := DigestRequest{
Algorithm: algorithm,
Path: path,
}
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
reqURL := fmt.Sprintf("%s/digest", c.baseURL)
req, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
var digestResp DigestResponse
if err := json.NewDecoder(resp.Body).Decode(&digestResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &digestResp, nil
}
func (c *Client) OpenHandle(path string, flags OpenFlag, mode uint32) (int64, error) {
query := url.Values{}
query.Set("path", path)
query.Set("flags", fmt.Sprintf("%d", flags))
query.Set("mode", fmt.Sprintf("%o", mode))
resp, err := c.doRequest(http.MethodPost, "/handles/open", query, nil)
if err != nil {
return 0, fmt.Errorf("open handle request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
if resp.StatusCode == http.StatusNotImplemented {
return 0, ErrNotSupported
}
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return 0, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return 0, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
var handleResp HandleResponse
if err := json.NewDecoder(resp.Body).Decode(&handleResp); err != nil {
return 0, fmt.Errorf("failed to decode handle response: %w", err)
}
return handleResp.HandleID, nil
}
func (c *Client) CloseHandle(handleID int64) error {
endpoint := fmt.Sprintf("/handles/%d", handleID)
resp, err := c.doRequest(http.MethodDelete, endpoint, nil, nil)
if err != nil {
return fmt.Errorf("close handle request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
return nil
}
func (c *Client) ReadHandle(handleID int64, offset int64, size int) ([]byte, error) {
endpoint := fmt.Sprintf("/handles/%d/read", handleID)
query := url.Values{}
query.Set("offset", fmt.Sprintf("%d", offset))
query.Set("size", fmt.Sprintf("%d", size))
resp, err := c.doRequest(http.MethodGet, endpoint, query, nil)
if err != nil {
return nil, fmt.Errorf("read handle request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return data, nil
}
func (c *Client) ReadHandleStream(handleID int64) (io.ReadCloser, error) {
endpoint := fmt.Sprintf("/handles/%d/stream", handleID)
streamClient := &http.Client{
Timeout: 0,
}
reqURL := fmt.Sprintf("%s%s", c.baseURL, endpoint)
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := streamClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
return resp.Body, nil
}
func (c *Client) WriteHandle(handleID int64, data []byte, offset int64) (int, error) {
endpoint := fmt.Sprintf("/handles/%d/write", handleID)
query := url.Values{}
query.Set("offset", fmt.Sprintf("%d", offset))
req, err := http.NewRequest(http.MethodPut, c.baseURL+endpoint+"?"+query.Encode(), bytes.NewReader(data))
if err != nil {
return 0, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := c.httpClient.Do(req)
if err != nil {
return 0, fmt.Errorf("write handle request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return 0, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return 0, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
var result struct {
BytesWritten int `json:"bytes_written"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return len(data), nil
}
return result.BytesWritten, nil
}
func (c *Client) SyncHandle(handleID int64) error {
endpoint := fmt.Sprintf("/handles/%d/sync", handleID)
resp, err := c.doRequest(http.MethodPost, endpoint, nil, nil)
if err != nil {
return fmt.Errorf("sync handle request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
return nil
}
func (c *Client) SeekHandle(handleID int64, offset int64, whence int) (int64, error) {
endpoint := fmt.Sprintf("/handles/%d/seek", handleID)
query := url.Values{}
query.Set("offset", fmt.Sprintf("%d", offset))
query.Set("whence", fmt.Sprintf("%d", whence))
resp, err := c.doRequest(http.MethodPost, endpoint, query, nil)
if err != nil {
return 0, fmt.Errorf("seek handle request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return 0, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return 0, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
var result struct {
Offset int64 `json:"offset"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return 0, fmt.Errorf("failed to decode response: %w", err)
}
return result.Offset, nil
}
func (c *Client) GetHandle(handleID int64) (*HandleInfo, error) {
endpoint := fmt.Sprintf("/handles/%d", handleID)
resp, err := c.doRequest(http.MethodGet, endpoint, nil, nil)
if err != nil {
return nil, fmt.Errorf("get handle request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
var handleInfo HandleInfo
if err := json.NewDecoder(resp.Body).Decode(&handleInfo); err != nil {
return nil, fmt.Errorf("failed to decode handle info: %w", err)
}
return &handleInfo, nil
}
func (c *Client) StatHandle(handleID int64) (*FileInfo, error) {
endpoint := fmt.Sprintf("/handles/%d/stat", handleID)
resp, err := c.doRequest(http.MethodGet, endpoint, nil, nil)
if err != nil {
return nil, fmt.Errorf("stat handle request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("HTTP %d: failed to decode error response", resp.StatusCode)
}
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, errResp.Error)
}
var fileInfo FileInfoResponse
if err := json.NewDecoder(resp.Body).Decode(&fileInfo); err != nil {
return nil, fmt.Errorf("failed to decode file info response: %w", err)
}
modTime, _ := time.Parse(time.RFC3339Nano, fileInfo.ModTime)
return &FileInfo{
Name: fileInfo.Name,
Size: fileInfo.Size,
Mode: fileInfo.Mode,
ModTime: modTime,
IsDir: fileInfo.IsDir,
Meta: fileInfo.Meta,
}, nil
}