fork of whitequark.org/git-pages with mods for tangled

Add "dry run" capability for all destructive endpoints.

Changed files
+31 -7
src
+1
README.md
··· 70 - The `POST` method requires an `application/json` body containing a Forgejo/Gitea/Gogs/GitHub webhook event payload. Requests where the `ref` key contains anything other than `refs/heads/pages` are ignored, and only the `pages` branch is used. The `repository.clone_url` key contains a repository URL to be shallowly cloned. 71 - If the received contents is empty, performs the same action as `DELETE`. 72 * In response to a `DELETE` request, the server unpublishes a site. The URL of the request must be the root URL of the site that is being unpublished. Site data remains stored for an indeterminate period of time, but becomes completely inaccessible. 73 * All updates to site content are atomic (subject to consistency guarantees of the storage backend). That is, there is an instantaneous moment during an update before which the server will return the old content and after which it will return the new content. 74 * Files with a certain name, when placed in the root of a site, have special functions: 75 - [Netlify `_redirects`][_redirects] file can be used to specify HTTP redirect and rewrite rules. The _git-pages_ implementation currently does not support placeholders, query parameters, or conditions, and may differ from Netlify in other minor ways. If you find that a supported `_redirects` file feature does not work the same as on Netlify, please file an issue. (Note that _git-pages_ does not perform URL normalization; `/foo` and `/foo/` are *not* the same, unlike with Netlify.)
··· 70 - The `POST` method requires an `application/json` body containing a Forgejo/Gitea/Gogs/GitHub webhook event payload. Requests where the `ref` key contains anything other than `refs/heads/pages` are ignored, and only the `pages` branch is used. The `repository.clone_url` key contains a repository URL to be shallowly cloned. 71 - If the received contents is empty, performs the same action as `DELETE`. 72 * In response to a `DELETE` request, the server unpublishes a site. The URL of the request must be the root URL of the site that is being unpublished. Site data remains stored for an indeterminate period of time, but becomes completely inaccessible. 73 + * If a `Dry-Run: yes` header is provided with a `PUT`, `DELETE`, or `POST` request, only the authorization checks are run; no destructive updates are made. Note that this functionality was added in _git-pages_ v0.2.0. 74 * All updates to site content are atomic (subject to consistency guarantees of the storage backend). That is, there is an instantaneous moment during an update before which the server will return the old content and after which it will return the new content. 75 * Files with a certain name, when placed in the root of a site, have special functions: 76 - [Netlify `_redirects`][_redirects] file can be used to specify HTTP redirect and rewrite rules. The _git-pages_ implementation currently does not support placeholders, query parameters, or conditions, and may differ from Netlify in other minor ways. If you find that a supported `_redirects` file feature does not work the same as on Netlify, please file an issue. (Note that _git-pages_ does not perform URL normalization; `/foo` and `/foo/` are *not* the same, unlike with Netlify.)
+30 -7
src/pages.go
··· 405 return nil 406 } 407 408 func putPage(w http.ResponseWriter, r *http.Request) error { 409 var result UpdateResult 410 ··· 424 defer cancel() 425 426 contentType := getMediaType(r.Header.Get("Content-Type")) 427 - 428 - if contentType == "application/x-www-form-urlencoded" { 429 auth, err := AuthorizeUpdateFromRepository(r) 430 if err != nil { 431 return err ··· 450 return err 451 } 452 453 result = UpdateFromRepository(updateCtx, webRoot, repoURL, branch) 454 - } else { 455 _, err := AuthorizeUpdateFromArchive(r) 456 if err != nil { 457 return err 458 } 459 460 // request body contains archive ··· 518 return err 519 } 520 521 err = backend.DeleteManifest(r.Context(), makeWebRoot(host, projectName)) 522 if err != nil { 523 w.WriteHeader(http.StatusInternalServerError) ··· 618 return err 619 } 620 621 resultChan := make(chan UpdateResult) 622 go func(ctx context.Context) { 623 ctx, cancel := context.WithTimeout(ctx, time.Duration(config.Limits.UpdateTimeout)) ··· 645 w.WriteHeader(http.StatusGatewayTimeout) 646 fmt.Fprintln(w, "update timeout") 647 case UpdateNoChange: 648 - w.WriteHeader(http.StatusOK) 649 fmt.Fprintln(w, "unchanged") 650 case UpdateCreated: 651 - w.WriteHeader(http.StatusOK) 652 fmt.Fprintln(w, "created") 653 case UpdateReplaced: 654 - w.WriteHeader(http.StatusOK) 655 fmt.Fprintln(w, "replaced") 656 case UpdateDeleted: 657 - w.WriteHeader(http.StatusOK) 658 fmt.Fprintln(w, "deleted") 659 } 660 if result.manifest != nil {
··· 405 return nil 406 } 407 408 + func checkDryRun(w http.ResponseWriter, r *http.Request) bool { 409 + // "Dry run" requests are used to non-destructively check if the request would have 410 + // successfully been authorized. 411 + if r.Header.Get("Dry-Run") != "" { 412 + fmt.Fprintln(w, "dry-run ok") 413 + return true 414 + } 415 + return false 416 + } 417 + 418 func putPage(w http.ResponseWriter, r *http.Request) error { 419 var result UpdateResult 420 ··· 434 defer cancel() 435 436 contentType := getMediaType(r.Header.Get("Content-Type")) 437 + switch contentType { 438 + case "application/x-www-form-urlencoded": 439 auth, err := AuthorizeUpdateFromRepository(r) 440 if err != nil { 441 return err ··· 460 return err 461 } 462 463 + if checkDryRun(w, r) { 464 + return nil 465 + } 466 + 467 result = UpdateFromRepository(updateCtx, webRoot, repoURL, branch) 468 + 469 + default: 470 _, err := AuthorizeUpdateFromArchive(r) 471 if err != nil { 472 return err 473 + } 474 + 475 + if checkDryRun(w, r) { 476 + return nil 477 } 478 479 // request body contains archive ··· 537 return err 538 } 539 540 + if checkDryRun(w, r) { 541 + return nil 542 + } 543 + 544 err = backend.DeleteManifest(r.Context(), makeWebRoot(host, projectName)) 545 if err != nil { 546 w.WriteHeader(http.StatusInternalServerError) ··· 641 return err 642 } 643 644 + if checkDryRun(w, r) { 645 + return nil 646 + } 647 + 648 resultChan := make(chan UpdateResult) 649 go func(ctx context.Context) { 650 ctx, cancel := context.WithTimeout(ctx, time.Duration(config.Limits.UpdateTimeout)) ··· 672 w.WriteHeader(http.StatusGatewayTimeout) 673 fmt.Fprintln(w, "update timeout") 674 case UpdateNoChange: 675 fmt.Fprintln(w, "unchanged") 676 case UpdateCreated: 677 fmt.Fprintln(w, "created") 678 case UpdateReplaced: 679 fmt.Fprintln(w, "replaced") 680 case UpdateDeleted: 681 fmt.Fprintln(w, "deleted") 682 } 683 if result.manifest != nil {