From b6f4034c71b9f68817c0758a9a3a3fb3ea595ff2 Mon Sep 17 00:00:00 2001 From: Tomas Balsys Date: Wed, 26 Nov 2025 21:18:09 +0200 Subject: [PATCH] Protect changing in parallel using HTTP 412 precondition failed --- {.github => .gitea}/workflows/release.yaml | 9 ++--- examples/base/main.go | 36 +++++++++++++++++++ .../records/RecordUpsertPanel.svelte | 12 +++++-- 3 files changed, 48 insertions(+), 9 deletions(-) rename {.github => .gitea}/workflows/release.yaml (82%) diff --git a/.github/workflows/release.yaml b/.gitea/workflows/release.yaml similarity index 82% rename from .github/workflows/release.yaml rename to .gitea/workflows/release.yaml index 5cdb04e8..bea1d1b6 100644 --- a/.github/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -10,11 +10,6 @@ jobs: env: flags: "" steps: - # re-enable auto-snapshot from goreleaser-action@v3 - # (https://github.com/goreleaser/goreleaser-action-v4-auto-snapshot-example) - - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} - run: echo "flags=--snapshot" >> $GITHUB_ENV - - name: Checkout uses: actions/checkout@v4 with: @@ -28,7 +23,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '>=1.24.8' + go-version: ">=1.24.8" # This step usually is not needed because the /ui/dist is pregenerated locally # but its here to ensure that each release embeds the latest admin ui artifacts. @@ -50,7 +45,7 @@ jobs: uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser - version: '~> v2' + version: "~> v2" args: release --clean ${{ env.flags }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/examples/base/main.go b/examples/base/main.go index 1ea529b4..ec757f06 100644 --- a/examples/base/main.go +++ b/examples/base/main.go @@ -116,11 +116,47 @@ func main() { Priority: 999, // execute as latest as possible to allow users to provide their own route }) + app.OnRecordViewRequest().BindFunc(addLastModified) + + app.OnRecordUpdateRequest().BindFunc(ifUnmodifiedSince) + + app.OnRecordDeleteRequest().BindFunc(ifUnmodifiedSince) + if err := app.Start(); err != nil { log.Fatal(err) } } +func addLastModified(e *core.RecordRequestEvent) error { + updated := e.Record.GetString("updated") + + if updated != "" { + e.Response.Header().Add("Last-Modified", updated) + } + + return e.Next() +} + +func ifUnmodifiedSince(e *core.RecordRequestEvent) error { + updated := e.Record.GetString("updated") + + if updated != "" { + header := e.Request.Header.Get("If-Unmodified-Since") + + if header == "" || header != updated { + e.Response.Header().Add("Last-Modified", updated) + + if header == "" { + return e.Error(http.StatusPreconditionRequired, "Header If-Unmodified-Since is required", nil) + } else if header != updated { + return e.Error(http.StatusPreconditionFailed, "Record was modified after retrieval", nil) + } + } + } + + return e.Next() +} + // the default pb_public dir location is relative to the executable func defaultPublicDir() string { if osutils.IsProbablyGoRun() { diff --git a/ui/src/components/records/RecordUpsertPanel.svelte b/ui/src/components/records/RecordUpsertPanel.svelte index 7bc9a992..15985d63 100644 --- a/ui/src/components/records/RecordUpsertPanel.svelte +++ b/ui/src/components/records/RecordUpsertPanel.svelte @@ -278,7 +278,11 @@ if (isNew) { result = await ApiClient.collection(collection.id).create(data); } else { - result = await ApiClient.collection(collection.id).update(record.id, data); + result = await ApiClient.collection(collection.id).update(record.id, data, { + headers: { + "If-Unmodified-Since": record.updated, + }, + }); } addSuccessToast(isNew ? "Successfully created record." : "Successfully updated record."); @@ -318,7 +322,11 @@ confirm(`Do you really want to delete the selected record?`, () => { return ApiClient.collection(original.collectionId) - .delete(original.id) + .delete(original.id, { + headers: { + "If-Unmodified-Since": record.updated, + }, + }) .then(() => { forceHide(); addSuccessToast("Successfully deleted record.");