larc r4

234 lines ยท 5.7 KB Raw
1 package protocol
2
3 import (
4 "bytes"
5 "encoding/base64"
6 "fmt"
7 "io"
8 "net/http"
9 "time"
10
11 "github.com/bytedance/sonic"
12
13 "github.com/lain/larc/internal/core"
14 )
15
16 /* HTTP client for larc server communication.
17 * Handles auth, JSON API calls, and blob transfers. */
18
19 // Client is the HTTP client for larc server
20 type Client struct {
21 baseURL string
22 username string
23 password string
24 httpClient *http.Client
25 }
26
27 // NewClient creates a new client for the given server URL
28 func NewClient(baseURL string) *Client {
29 return &Client{
30 baseURL: baseURL,
31 httpClient: &http.Client{
32 Timeout: 30 * time.Second,
33 },
34 }
35 }
36
37 // SetAuth sets Basic Auth credentials
38 func (c *Client) SetAuth(username, password string) {
39 c.username = username
40 c.password = password
41 }
42
43 func (c *Client) doRequest(method, path string, body io.Reader) (*http.Response, error) {
44 url := c.baseURL + path
45
46 req, err := http.NewRequest(method, url, body)
47 if err != nil {
48 return nil, fmt.Errorf("create request: %w", err)
49 }
50
51 if c.username != "" {
52 auth := base64.StdEncoding.EncodeToString([]byte(c.username + ":" + c.password))
53 req.Header.Set("Authorization", "Basic "+auth)
54 }
55
56 if body != nil {
57 req.Header.Set("Content-Type", "application/json")
58 }
59
60 return c.httpClient.Do(req)
61 }
62
63 func (c *Client) doJSON(method, path string, body any, result any) error {
64 var bodyReader io.Reader
65 if body != nil {
66 data, err := sonic.Marshal(body)
67 if err != nil {
68 return fmt.Errorf("marshal body: %w", err)
69 }
70 bodyReader = bytes.NewReader(data)
71 }
72
73 resp, err := c.doRequest(method, path, bodyReader)
74 if err != nil {
75 return err
76 }
77 defer resp.Body.Close()
78
79 if resp.StatusCode >= 400 {
80 bodyBytes, _ := io.ReadAll(resp.Body)
81 return fmt.Errorf("server error %d: %s", resp.StatusCode, string(bodyBytes))
82 }
83
84 if result != nil {
85 if err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(result); err != nil {
86 return fmt.Errorf("decode response: %w", err)
87 }
88 }
89
90 return nil
91 }
92
93 // RepoInfo contains repository metadata
94 type RepoInfo struct {
95 Name string `json:"name"`
96 LatestRev int64 `json:"latest_rev"`
97 BranchCount int `json:"branch_count"`
98 DefaultBranch string `json:"default_branch"`
99 }
100
101 // GetInfo returns repository info
102 func (c *Client) GetInfo(repoName string) (*RepoInfo, error) {
103 var info RepoInfo
104 if err := c.doJSON("GET", "/api/"+repoName+"/info", nil, &info); err != nil {
105 return nil, err
106 }
107 return &info, nil
108 }
109
110 // GetLatestRevision returns the latest revision number
111 func (c *Client) GetLatestRevision(repoName string) (int64, error) {
112 var result struct {
113 Revision int64 `json:"revision"`
114 }
115 if err := c.doJSON("GET", "/api/"+repoName+"/rev/latest", nil, &result); err != nil {
116 return 0, err
117 }
118 return result.Revision, nil
119 }
120
121 // GetRevision returns revision details
122 func (c *Client) GetRevision(repoName string, revNum int64) (*core.Revision, error) {
123 var rev core.Revision
124 path := fmt.Sprintf("/api/%s/rev/%d", repoName, revNum)
125 if err := c.doJSON("GET", path, nil, &rev); err != nil {
126 return nil, err
127 }
128 return &rev, nil
129 }
130
131 // GetBranches returns all branches
132 func (c *Client) GetBranches(repoName string) ([]*core.Branch, error) {
133 var branches []*core.Branch
134 if err := c.doJSON("GET", "/api/"+repoName+"/branches", nil, &branches); err != nil {
135 return nil, err
136 }
137 return branches, nil
138 }
139
140 // GetBlob downloads blob content
141 func (c *Client) GetBlob(repoName, hash string) ([]byte, error) {
142 path := fmt.Sprintf("/api/%s/blob/%s", repoName, hash)
143 resp, err := c.doRequest("GET", path, nil)
144 if err != nil {
145 return nil, err
146 }
147 defer resp.Body.Close()
148
149 if resp.StatusCode >= 400 {
150 return nil, fmt.Errorf("blob not found: %s", hash)
151 }
152
153 return io.ReadAll(resp.Body)
154 }
155
156 // UploadBlob uploads blob content and returns hash
157 func (c *Client) UploadBlob(repoName string, data []byte) (string, error) {
158 path := "/api/" + repoName + "/blob"
159 resp, err := c.doRequest("POST", path, bytes.NewReader(data))
160 if err != nil {
161 return "", err
162 }
163 defer resp.Body.Close()
164
165 if resp.StatusCode >= 400 {
166 bodyBytes, _ := io.ReadAll(resp.Body)
167 return "", fmt.Errorf("upload failed: %s", string(bodyBytes))
168 }
169
170 var result struct {
171 Hash string `json:"hash"`
172 Size int `json:"size"`
173 }
174 if err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&result); err != nil {
175 return "", err
176 }
177
178 return result.Hash, nil
179 }
180
181 // GetTree returns tree for a revision
182 func (c *Client) GetTree(repoName string, revNum int64) (*core.Tree, error) {
183 var tree core.Tree
184 path := fmt.Sprintf("/api/%s/tree/%d", repoName, revNum)
185 if err := c.doJSON("GET", path, nil, &tree); err != nil {
186 return nil, err
187 }
188 return &tree, nil
189 }
190
191 // PullRequest is the request for pulling revisions
192 type PullRequest struct {
193 FromRev int64 `json:"from_rev"`
194 ToRev int64 `json:"to_rev"`
195 Branch string `json:"branch"`
196 }
197
198 // PullResponse contains pulled data
199 type PullResponse struct {
200 Revisions []*core.Revision `json:"revisions"`
201 }
202
203 // GetRevisions returns revisions in range
204 func (c *Client) GetRevisions(repoName string, fromRev, toRev int64) ([]*core.Revision, error) {
205 var revisions []*core.Revision
206
207 for r := fromRev; r <= toRev; r++ {
208 rev, err := c.GetRevision(repoName, r)
209 if err != nil {
210 return nil, fmt.Errorf("get revision %d: %w", r, err)
211 }
212 revisions = append(revisions, rev)
213 }
214
215 return revisions, nil
216 }
217
218 // CommitRequest is the request for creating a commit
219 type CommitRequest struct {
220 Branch string `json:"branch"`
221 Message string `json:"message"`
222 Author string `json:"author"`
223 Entries []core.TreeEntry `json:"entries"`
224 }
225
226 // Commit creates a new revision on the server
227 func (c *Client) Commit(repoName string, req *CommitRequest) (*core.Revision, error) {
228 var rev core.Revision
229 if err := c.doJSON("POST", "/api/"+repoName+"/commit", req, &rev); err != nil {
230 return nil, err
231 }
232 return &rev, nil
233 }
234