(rw http.ResponseWriter, r *http.Request)
| 373 | } |
| 374 | |
| 375 | func (api *API) HandleEditFiles(rw http.ResponseWriter, r *http.Request) { |
| 376 | ctx := r.Context() |
| 377 | |
| 378 | var req workspacesdk.FileEditRequest |
| 379 | if !httpapi.Read(ctx, rw, r, &req) { |
| 380 | return |
| 381 | } |
| 382 | |
| 383 | if len(req.Files) == 0 { |
| 384 | httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ |
| 385 | Message: "must specify at least one file", |
| 386 | }) |
| 387 | return |
| 388 | } |
| 389 | |
| 390 | // Duplicate entries both read the same file and race to write; |
| 391 | // the first entry's edits are silently lost. Resolve symlinks |
| 392 | // before comparing so two paths that alias the same real file |
| 393 | // (e.g. one via a symlink, one direct) don't slip past as |
| 394 | // distinct keys. prepareFileEdit resolves the path again for |
| 395 | // its own use; the double lstat cost is cheap compared to the |
| 396 | // data-loss risk of silent aliasing. |
| 397 | type seenEntry struct { |
| 398 | caller string |
| 399 | } |
| 400 | seenPaths := make(map[string]seenEntry, len(req.Files)) |
| 401 | for _, f := range req.Files { |
| 402 | // On resolve error, use the raw path; phase 1 surfaces |
| 403 | // the error with its proper status code. |
| 404 | key := f.Path |
| 405 | if resolved, err := api.resolvePath(f.Path); err == nil { |
| 406 | key = resolved |
| 407 | } |
| 408 | if prev, dup := seenPaths[key]; dup { |
| 409 | msg := fmt.Sprintf("duplicate file path %q: combine edits into a single entry's \"edits\" list", f.Path) |
| 410 | if prev.caller != f.Path { |
| 411 | msg = fmt.Sprintf("duplicate file path %q aliases %q (same real file): combine edits into a single entry's \"edits\" list", f.Path, prev.caller) |
| 412 | } |
| 413 | httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ |
| 414 | Message: msg, |
| 415 | }) |
| 416 | return |
| 417 | } |
| 418 | seenPaths[key] = seenEntry{caller: f.Path} |
| 419 | } |
| 420 | |
| 421 | // Phase 1: compute all edits in memory. If any file fails |
| 422 | // (bad path, search miss, permission error), bail before |
| 423 | // writing anything. |
| 424 | var pending []pendingEdit |
| 425 | var combinedErr error |
| 426 | status := http.StatusOK |
| 427 | for _, edit := range req.Files { |
| 428 | s, p, err := api.prepareFileEdit(edit.Path, edit.Edits) |
| 429 | if s > status { |
| 430 | status = s |
| 431 | } |
| 432 | if err != nil { |
nothing calls this directly
no test coverage detected