Skip to content

fix: clean up application images when an upload fails - #1057

Merged
eternal-flame-AD merged 2 commits into
gotify:masterfrom
SulimanAbdulrazzaq:fix/concurrent-app-image-upload
Oct 11, 2026
Merged

eternal-flame-AD merged 2 commits into
gotify:masterfrom
SulimanAbdulrazzaq:fix/concurrent-app-image-upload

Conversation

@SulimanAbdulrazzaq

@SulimanAbdulrazzaq SulimanAbdulrazzaq commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Refs #1047

As suggested in the review, this drops the mutex and fixes the file-system side of image uploads:

  • Partial files: ctx.SaveUploadedFile copies the upload with no cleanup, so a copy that fails partway leaves a partial file in the image directory. The upload now goes through saveImage/writeImage, which removes the file if it can't be written completely.
  • Database update failure: the old image used to be deleted before UpdateApplication. If the update failed, the application still referenced a deleted image, and the new file was left behind unreferenced. The old image is now deleted only after the update succeeds. If the update fails, the new file is removed and the old image is kept.

Two uploads to the same application overlapping in time still need an atomic update of the image field. As discussed, that's better done with a transaction or a dedicated DB method, so it's left out here.

Tests:

  • Test_UploadAppImage_UpdateFails_KeepsExistingImage: on master the image directory ends up holding only the new, unreferenced file (expected: "existing.png", actual: "<new name>.png"). With this change the application keeps existing.png and it's the only file left.
  • Test_WriteImage_RemovesPartialFileOnError: a reader that fails after some data leaves no file behind.

make test-coverage (go test --race ./...) passes, and golangci-lint reports 0 issues.

@SulimanAbdulrazzaq
SulimanAbdulrazzaq requested a review from a team as a code owner September 26, 2026 09:32
@codecov

codecov Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.57%. Comparing base (2e67149) to head (2942fe9).

Files with missing lines Patch % Lines
api/application.go 83.33% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1057      +/-   ##
==========================================
+ Coverage   76.50%   76.57%   +0.07%     
==========================================
  Files          68       68              
  Lines        3694     3714      +20     
==========================================
+ Hits         2826     2844      +18     
- Misses        656      657       +1     
- Partials      212      213       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@eternal-flame-AD eternal-flame-AD left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the core for this issue isn't the magic string in the database having race condition (it does not, it guarantees to store the most recent value). The problem is the file system has race conditions

Comment thread api/application.go Outdated
Comment thread api/application.go Outdated
A failed write left a partially written image file behind, and a failed
database update after saving the new image had already deleted the image
the application still referenced.

Write the uploaded image with a helper that removes the file if it can't
be written completely, and only delete the old image once the
application has been updated. If the update fails, remove the new image
instead.
@SulimanAbdulrazzaq
SulimanAbdulrazzaq force-pushed the fix/concurrent-app-image-upload branch from e2e2bc4 to efb90f3 Compare September 26, 2026 16:57
@SulimanAbdulrazzaq SulimanAbdulrazzaq changed the title fix: serialize application image changes fix: clean up application images when an upload fails Sep 26, 2026
@SulimanAbdulrazzaq

Copy link
Copy Markdown
Contributor Author

@eternal-flame-AD thanks for the review. I reworked this along the lines you suggested: no mutex, and cleanup of the files on failed uploads and failed updates. Details are in the updated description. Could you take another look?

@SulimanAbdulrazzaq

Copy link
Copy Markdown
Contributor Author

@eternal-flame-AD @jmattheis when you have a moment, could one of you take a look at this one? The checks are green on the current head.

jmattheis
jmattheis previously approved these changes Oct 10, 2026

@jmattheis jmattheis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, @eternal-flame-AD do you want to recheck?

@eternal-flame-AD eternal-flame-AD left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine although I would prefer migrating to atomic fs with fixed file names (or your database inline solution) directly

@jmattheis

Copy link
Copy Markdown
Member

I agree, but it does improve things so I'm okay with merging.

@SulimanAbdulrazzaq

Copy link
Copy Markdown
Contributor Author

I merged the latest master into this branch to resolve the conflict. The only conflict was the import block of api/application.go: master now imports io for the read-error handling it added in UploadApplicationImage, so the resolution keeps both io and mime/multipart. Nothing else changed: the diff against master is still api/application.go and api/application_test.go, and the merged upload function reads as before (master's file.Open/Read error checks, then this change's saveImage and cleanup).

GitHub dismissed the earlier approvals when the branch was updated, so @jmattheis @eternal-flame-AD could you re-approve when you have a moment? CI is running on the merge commit.

@eternal-flame-AD
eternal-flame-AD added this pull request to the merge queue Oct 11, 2026
Merged via the queue into gotify:master with commit 6a49e9c Oct 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants