package protocol import ( "bytes" "encoding/base64" "fmt" "io" "net/http" "time" "github.com/bytedance/sonic" "github.com/lain/larc/internal/core" ) /* HTTP client for larc server communication. * Handles auth, JSON API calls, and blob transfers. */ // Client is the HTTP client for larc server type Client struct { baseURL string username string password string httpClient *http.Client } // NewClient creates a new client for the given server URL func NewClient(baseURL string) *Client { return &Client{ baseURL: baseURL, httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } // SetAuth sets Basic Auth credentials func (c *Client) SetAuth(username, password string) { c.username = username c.password = password } func (c *Client) doRequest(method, path string, body io.Reader) (*http.Response, error) { url := c.baseURL + path req, err := http.NewRequest(method, url, body) if err != nil { return nil, fmt.Errorf("create request: %w", err) } if c.username != "" { auth := base64.StdEncoding.EncodeToString([]byte(c.username + ":" + c.password)) req.Header.Set("Authorization", "Basic "+auth) } if body != nil { req.Header.Set("Content-Type", "application/json") } return c.httpClient.Do(req) } func (c *Client) doJSON(method, path string, body any, result any) error { var bodyReader io.Reader if body != nil { data, err := sonic.Marshal(body) if err != nil { return fmt.Errorf("marshal body: %w", err) } bodyReader = bytes.NewReader(data) } resp, err := c.doRequest(method, path, bodyReader) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("server error %d: %s", resp.StatusCode, string(bodyBytes)) } if result != nil { if err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(result); err != nil { return fmt.Errorf("decode response: %w", err) } } return nil } // RepoInfo contains repository metadata type RepoInfo struct { Name string `json:"name"` LatestRev int64 `json:"latest_rev"` BranchCount int `json:"branch_count"` DefaultBranch string `json:"default_branch"` } // GetInfo returns repository info func (c *Client) GetInfo(repoName string) (*RepoInfo, error) { var info RepoInfo if err := c.doJSON("GET", "/api/"+repoName+"/info", nil, &info); err != nil { return nil, err } return &info, nil } // GetLatestRevision returns the latest revision number func (c *Client) GetLatestRevision(repoName string) (int64, error) { var result struct { Revision int64 `json:"revision"` } if err := c.doJSON("GET", "/api/"+repoName+"/rev/latest", nil, &result); err != nil { return 0, err } return result.Revision, nil } // GetRevision returns revision details func (c *Client) GetRevision(repoName string, revNum int64) (*core.Revision, error) { var rev core.Revision path := fmt.Sprintf("/api/%s/rev/%d", repoName, revNum) if err := c.doJSON("GET", path, nil, &rev); err != nil { return nil, err } return &rev, nil } // GetBranches returns all branches func (c *Client) GetBranches(repoName string) ([]*core.Branch, error) { var branches []*core.Branch if err := c.doJSON("GET", "/api/"+repoName+"/branches", nil, &branches); err != nil { return nil, err } return branches, nil } // GetBlob downloads blob content func (c *Client) GetBlob(repoName, hash string) ([]byte, error) { path := fmt.Sprintf("/api/%s/blob/%s", repoName, hash) resp, err := c.doRequest("GET", path, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, fmt.Errorf("blob not found: %s", hash) } return io.ReadAll(resp.Body) } // UploadBlob uploads blob content and returns hash func (c *Client) UploadBlob(repoName string, data []byte) (string, error) { path := "/api/" + repoName + "/blob" resp, err := c.doRequest("POST", path, bytes.NewReader(data)) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode >= 400 { bodyBytes, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("upload failed: %s", string(bodyBytes)) } var result struct { Hash string `json:"hash"` Size int `json:"size"` } if err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&result); err != nil { return "", err } return result.Hash, nil } // GetTree returns tree for a revision func (c *Client) GetTree(repoName string, revNum int64) (*core.Tree, error) { /* first get revision to get tree hash */ rev, err := c.GetRevision(repoName, revNum) if err != nil { return nil, err } /* tree is embedded in revision response via /tree endpoint */ path := fmt.Sprintf("/%s/tree/%d/", repoName, revNum) resp, err := c.doRequest("GET", path, nil) if err != nil { return nil, err } defer resp.Body.Close() var tree core.Tree if err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&tree); err != nil { return nil, err } tree.Hash = rev.TreeHash return &tree, nil } // PullRequest is the request for pulling revisions type PullRequest struct { FromRev int64 `json:"from_rev"` ToRev int64 `json:"to_rev"` Branch string `json:"branch"` } // PullResponse contains pulled data type PullResponse struct { Revisions []*core.Revision `json:"revisions"` } // GetRevisions returns revisions in range func (c *Client) GetRevisions(repoName string, fromRev, toRev int64) ([]*core.Revision, error) { var revisions []*core.Revision for r := fromRev; r <= toRev; r++ { rev, err := c.GetRevision(repoName, r) if err != nil { return nil, fmt.Errorf("get revision %d: %w", r, err) } revisions = append(revisions, rev) } return revisions, nil } // CommitRequest is the request for creating a commit type CommitRequest struct { Branch string `json:"branch"` Message string `json:"message"` Author string `json:"author"` Entries []core.TreeEntry `json:"entries"` } // Commit creates a new revision on the server func (c *Client) Commit(repoName string, req *CommitRequest) (*core.Revision, error) { var rev core.Revision if err := c.doJSON("POST", "/api/"+repoName+"/commit", req, &rev); err != nil { return nil, err } return &rev, nil }