aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/git-lfs-server
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/git-lfs-server')
-rw-r--r--cmd/git-lfs-server/main.go60
1 files changed, 40 insertions, 20 deletions
diff --git a/cmd/git-lfs-server/main.go b/cmd/git-lfs-server/main.go
index aa2a0c6..4ff62f6 100644
--- a/cmd/git-lfs-server/main.go
+++ b/cmd/git-lfs-server/main.go
@@ -21,6 +21,7 @@ import (
21 21
22 "github.com/minio/minio-go/v7" 22 "github.com/minio/minio-go/v7"
23 "github.com/minio/minio-go/v7/pkg/credentials" 23 "github.com/minio/minio-go/v7/pkg/credentials"
24 "github.com/rs/xid"
24) 25)
25 26
26type operation string 27type operation string
@@ -123,10 +124,14 @@ type lfsError struct {
123 RequestID string `json:"request_id,omitempty"` 124 RequestID string `json:"request_id,omitempty"`
124} 125}
125 126
126func makeRespError(w http.ResponseWriter, message string, code int) { 127func makeRespError(ctx context.Context, w http.ResponseWriter, message string, code int) {
128 err := lfsError{Message: message}
129 if val := ctx.Value(requestIDKey); val != nil {
130 err.RequestID = val.(string)
131 }
127 w.Header().Set("Content-Type", lfsMIME+"; charset=utf-8") 132 w.Header().Set("Content-Type", lfsMIME+"; charset=utf-8")
128 w.WriteHeader(code) 133 w.WriteHeader(code)
129 json.NewEncoder(w).Encode(lfsError{Message: message}) 134 json.NewEncoder(w).Encode(err)
130} 135}
131 136
132func makeObjError(obj parsedBatchObject, message string, code int) batchResponseObject { 137func makeObjError(obj parsedBatchObject, message string, code int) batchResponseObject {
@@ -153,7 +158,7 @@ func (h *handler) handleDownloadObject(ctx context.Context, repo string, obj par
153 } 158 }
154 // TODO: consider not making this an object-specific, but rather a 159 // TODO: consider not making this an object-specific, but rather a
155 // generic error such that the entire Batch API request fails. 160 // generic error such that the entire Batch API request fails.
156 log("Failed to query object information (full path: %s): %s", fullPath, err) 161 reqlog(ctx, "Failed to query object information (full path: %s): %s", fullPath, err)
157 return makeObjError(obj, "Failed to query object information", http.StatusInternalServerError) 162 return makeObjError(obj, "Failed to query object information", http.StatusInternalServerError)
158 } 163 }
159 if info.ChecksumSHA256 != "" && strings.ToLower(info.ChecksumSHA256) != obj.fullHash { 164 if info.ChecksumSHA256 != "" && strings.ToLower(info.ChecksumSHA256) != obj.fullHash {
@@ -167,7 +172,7 @@ func (h *handler) handleDownloadObject(ctx context.Context, repo string, obj par
167 if err != nil { 172 if err != nil {
168 // TODO: consider not making this an object-specific, but rather a 173 // TODO: consider not making this an object-specific, but rather a
169 // generic error such that the entire Batch API request fails. 174 // generic error such that the entire Batch API request fails.
170 log("Failed to generate action href (full path: %s): %s", fullPath, err) 175 reqlog(ctx, "Failed to generate action href (full path: %s): %s", fullPath, err)
171 return makeObjError(obj, "Failed to generate action href", http.StatusInternalServerError) 176 return makeObjError(obj, "Failed to generate action href", http.StatusInternalServerError)
172 } 177 }
173 178
@@ -205,52 +210,58 @@ func isLFSMediaType(t string) bool {
205 210
206var re = regexp.MustCompile(`^([a-zA-Z0-9-_/]+)\.git/info/lfs/objects/batch$`) 211var re = regexp.MustCompile(`^([a-zA-Z0-9-_/]+)\.git/info/lfs/objects/batch$`)
207 212
213type requestID struct{}
214
215var requestIDKey requestID
216
208func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 217func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
218 ctx := context.WithValue(r.Context(), requestIDKey, xid.New().String())
219
209 reqPath := os.Getenv("PATH_INFO") 220 reqPath := os.Getenv("PATH_INFO")
210 if reqPath == "" { 221 if reqPath == "" {
211 reqPath = r.URL.Path 222 reqPath = r.URL.Path
212 } 223 }
213 log("reqPath: %s", reqPath) 224 reqlog(ctx, "reqPath: %s", reqPath)
214 reqPath = strings.TrimPrefix(path.Clean(reqPath), "/") 225 reqPath = strings.TrimPrefix(path.Clean(reqPath), "/")
215 log("Cleaned reqPath: %s", reqPath) 226 reqlog(ctx, "Cleaned reqPath: %s", reqPath)
216 submatches := re.FindStringSubmatch(reqPath) 227 submatches := re.FindStringSubmatch(reqPath)
217 if len(submatches) != 2 { 228 if len(submatches) != 2 {
218 log("Got path: %s, did not match regex", reqPath) 229 reqlog(ctx, "Got path: %s, did not match regex", reqPath)
219 makeRespError(w, "Not found", http.StatusNotFound) 230 makeRespError(ctx, w, "Not found", http.StatusNotFound)
220 return 231 return
221 } 232 }
222 repo := strings.TrimPrefix(path.Clean(submatches[1]), "/") 233 repo := strings.TrimPrefix(path.Clean(submatches[1]), "/")
223 log("Repository: %s", repo) 234 reqlog(ctx, "Repository: %s", repo)
224 235
225 if !slices.ContainsFunc(r.Header.Values("Accept"), isLFSMediaType) { 236 if !slices.ContainsFunc(r.Header.Values("Accept"), isLFSMediaType) {
226 makeRespError(w, "Expected "+lfsMIME+" (with UTF-8 charset) in list of acceptable response media types", http.StatusNotAcceptable) 237 makeRespError(ctx, w, "Expected "+lfsMIME+" (with UTF-8 charset) in list of acceptable response media types", http.StatusNotAcceptable)
227 return 238 return
228 } 239 }
229 if !isLFSMediaType(r.Header.Get("Content-Type")) { 240 if !isLFSMediaType(r.Header.Get("Content-Type")) {
230 makeRespError(w, "Expected request Content-Type to be "+lfsMIME+" (with UTF-8 charset)", http.StatusUnsupportedMediaType) 241 makeRespError(ctx, w, "Expected request Content-Type to be "+lfsMIME+" (with UTF-8 charset)", http.StatusUnsupportedMediaType)
231 return 242 return
232 } 243 }
233 244
234 var body batchRequest 245 var body batchRequest
235 if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 246 if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
236 makeRespError(w, "Failed to parse request body as JSON", http.StatusBadRequest) 247 makeRespError(ctx, w, "Failed to parse request body as JSON", http.StatusBadRequest)
237 return 248 return
238 } 249 }
239 250
240 if body.HashAlgo != hashAlgoSHA256 { 251 if body.HashAlgo != hashAlgoSHA256 {
241 makeRespError(w, "Unsupported hash algorithm specified", http.StatusConflict) 252 makeRespError(ctx, w, "Unsupported hash algorithm specified", http.StatusConflict)
242 return 253 return
243 } 254 }
244 255
245 // TODO: handle authentication 256 // TODO: handle authentication
246 // right now, we're just trying to make everything publically accessible 257 // right now, we're just trying to make everything publically accessible
247 if body.Operation == operationUpload { 258 if body.Operation == operationUpload {
248 makeRespError(w, "Upload operations are currently not supported", http.StatusForbidden) 259 makeRespError(ctx, w, "Upload operations are currently not supported", http.StatusForbidden)
249 return 260 return
250 } 261 }
251 262
252 if len(body.Transfers) != 0 && !slices.Contains(body.Transfers, transferAdapterBasic) { 263 if len(body.Transfers) != 0 && !slices.Contains(body.Transfers, transferAdapterBasic) {
253 makeRespError(w, "Unsupported transfer adapter specified (supported: basic)", http.StatusConflict) 264 makeRespError(ctx, w, "Unsupported transfer adapter specified (supported: basic)", http.StatusConflict)
254 return 265 return
255 } 266 }
256 267
@@ -263,15 +274,15 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
263 permGranted := err == nil 274 permGranted := err == nil
264 var exitErr *exec.ExitError 275 var exitErr *exec.ExitError
265 if err != nil && !errors.As(err, &exitErr) { 276 if err != nil && !errors.As(err, &exitErr) {
266 log("Error checking access info (running %s): %s", cmd, err) 277 reqlog(ctx, "Error checking access info (running %s): %s", cmd, err)
267 makeRespError(w, "Failed to query access information", http.StatusInternalServerError) 278 makeRespError(ctx, w, "Failed to query access information", http.StatusInternalServerError)
268 return 279 return
269 } 280 }
270 if !permGranted { 281 if !permGranted {
271 // TODO: when handling authorization, make sure to return 403 Forbidden 282 // TODO: when handling authorization, make sure to return 403 Forbidden
272 // here when the user *does* have read permissions, but is not allowed 283 // here when the user *does* have read permissions, but is not allowed
273 // to write when requesting an upload operation. 284 // to write when requesting an upload operation.
274 makeRespError(w, "Repository not found", http.StatusNotFound) 285 makeRespError(ctx, w, "Repository not found", http.StatusNotFound)
275 return 286 return
276 } 287 }
277 288
@@ -279,7 +290,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
279 for _, obj := range body.Objects { 290 for _, obj := range body.Objects {
280 oid := strings.ToLower(obj.OID) 291 oid := strings.ToLower(obj.OID)
281 if !isValidSHA256Hash(oid) { 292 if !isValidSHA256Hash(oid) {
282 makeRespError(w, "Invalid hash format in object ID", http.StatusBadRequest) 293 makeRespError(ctx, w, "Invalid hash format in object ID", http.StatusBadRequest)
283 return 294 return
284 } 295 }
285 objects = append(objects, parsedBatchObject{ 296 objects = append(objects, parsedBatchObject{
@@ -295,7 +306,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
295 HashAlgo: hashAlgoSHA256, 306 HashAlgo: hashAlgoSHA256,
296 } 307 }
297 for _, obj := range objects { 308 for _, obj := range objects {
298 resp.Objects = append(resp.Objects, h.handleDownloadObject(r.Context(), repo, obj)) 309 resp.Objects = append(resp.Objects, h.handleDownloadObject(ctx, repo, obj))
299 } 310 }
300 311
301 w.Header().Set("Content-Type", lfsMIME) 312 w.Header().Set("Content-Type", lfsMIME)
@@ -303,6 +314,15 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
303 json.NewEncoder(w).Encode(resp) 314 json.NewEncoder(w).Encode(resp)
304} 315}
305 316
317func reqlog(ctx context.Context, msg string, args ...any) {
318 fmt.Fprint(os.Stderr, "[gitolfs3] ")
319 if val := ctx.Value(requestIDKey); val != nil {
320 fmt.Fprintf(os.Stderr, "[%s] ", val.(string))
321 }
322 fmt.Fprintf(os.Stderr, msg, args...)
323 fmt.Fprint(os.Stderr, "\n")
324}
325
306func log(msg string, args ...any) { 326func log(msg string, args ...any) {
307 fmt.Fprint(os.Stderr, "[gitolfs3] ") 327 fmt.Fprint(os.Stderr, "[gitolfs3] ")
308 fmt.Fprintf(os.Stderr, msg, args...) 328 fmt.Fprintf(os.Stderr, msg, args...)