2
0

refactor(pages): deduplicate image upload/delete handlers
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m51s
Build and Release / Unit Tests (push) Successful in 8m56s
Build and Release / Lint (push) Successful in 9m26s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m50s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h7m28s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m1s

Extract common image upload and delete logic into pagesUploadImage and pagesDeleteImage helper functions. Eliminates ~90 lines of duplicated code across logo, favicon, and hero image handlers. Uses function parameters for field-specific operations (get/set config fields, locale keys, file prefixes). Makes it easier to add new image upload fields in the future.
This commit is contained in:
2026-03-15 23:20:22 -04:00
parent 573aa49a22
commit 5c6b91f6c9

View File

@@ -261,23 +261,33 @@ func PagesBrandPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/brand")
}
const maxBrandImageSize = 5 * 1024 * 1024 // 5 MB
const maxPagesImageSize = 5 * 1024 * 1024 // 5 MB
// PagesBrandUploadLogo handles logo image file upload.
func PagesBrandUploadLogo(ctx *context.Context) {
// pagesUploadImage is a shared helper for uploading images to pages config fields.
// formField is the multipart form field name, filePrefix is used in the stored filename,
// redirectPath is the settings page to redirect to, getOld/setNew read/write the config field,
// and successKey is the locale key for the success flash message.
func pagesUploadImage(
ctx *context.Context,
formField, filePrefix, redirectPath string,
getOld func(*pages_module.LandingConfig) string,
setNew func(*pages_module.LandingConfig, string),
errorKey, tooLargeKey, notImageKey, successKey string,
) {
repo := ctx.Repo.Repository
redirect := repo.Link() + redirectPath
file, header, err := ctx.Req.FormFile("brand_logo")
file, header, err := ctx.Req.FormFile(formField)
if err != nil {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_upload_error"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
ctx.Flash.Error(ctx.Tr(errorKey))
ctx.Redirect(redirect)
return
}
defer file.Close()
if header.Size > maxBrandImageSize {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_image_too_large"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
if header.Size > maxPagesImageSize {
ctx.Flash.Error(ctx.Tr(tooLargeKey))
ctx.Redirect(redirect)
return
}
@@ -289,18 +299,18 @@ func PagesBrandUploadLogo(ctx *context.Context) {
st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_not_an_image"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
ctx.Flash.Error(ctx.Tr(notImageKey))
ctx.Redirect(redirect)
return
}
hash := sha256.Sum256(data)
filename := fmt.Sprintf("pages-logo-%d-%x", repo.ID, hash[:8])
filename := fmt.Sprintf("%s-%d-%x", filePrefix, repo.ID, hash[:8])
config := getPagesLandingConfig(ctx)
if config.Brand.UploadedLogo != "" {
_ = storage.RepoAvatars.Delete(config.Brand.UploadedLogo)
if old := getOld(config); old != "" {
_ = storage.RepoAvatars.Delete(old)
}
if err := storage.SaveFrom(storage.RepoAvatars, filename, func(w io.Writer) error {
@@ -311,108 +321,72 @@ func PagesBrandUploadLogo(ctx *context.Context) {
return
}
config.Brand.UploadedLogo = filename
setNew(config, filename)
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.brand_logo_uploaded"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
ctx.Flash.Success(ctx.Tr(successKey))
ctx.Redirect(redirect)
}
// pagesDeleteImage is a shared helper for deleting uploaded images from pages config fields.
func pagesDeleteImage(
ctx *context.Context,
redirectPath string,
getOld func(*pages_module.LandingConfig) string,
clearField func(*pages_module.LandingConfig),
successKey string,
) {
repo := ctx.Repo.Repository
config := getPagesLandingConfig(ctx)
if old := getOld(config); old != "" {
_ = storage.RepoAvatars.Delete(old)
clearField(config)
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
}
ctx.Flash.Success(ctx.Tr(successKey))
ctx.Redirect(repo.Link() + redirectPath)
}
// PagesBrandUploadLogo handles logo image file upload.
func PagesBrandUploadLogo(ctx *context.Context) {
pagesUploadImage(ctx, "brand_logo", "pages-logo", "/settings/pages/brand",
func(c *pages_module.LandingConfig) string { return c.Brand.UploadedLogo },
func(c *pages_module.LandingConfig, f string) { c.Brand.UploadedLogo = f },
"repo.settings.pages.brand_upload_error", "repo.settings.pages.brand_image_too_large",
"repo.settings.pages.brand_not_an_image", "repo.settings.pages.brand_logo_uploaded")
}
// PagesBrandDeleteLogo removes the uploaded logo image.
func PagesBrandDeleteLogo(ctx *context.Context) {
repo := ctx.Repo.Repository
config := getPagesLandingConfig(ctx)
if config.Brand.UploadedLogo != "" {
_ = storage.RepoAvatars.Delete(config.Brand.UploadedLogo)
config.Brand.UploadedLogo = ""
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.brand_logo_deleted"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
pagesDeleteImage(ctx, "/settings/pages/brand",
func(c *pages_module.LandingConfig) string { return c.Brand.UploadedLogo },
func(c *pages_module.LandingConfig) { c.Brand.UploadedLogo = "" },
"repo.settings.pages.brand_logo_deleted")
}
// PagesBrandUploadFavicon handles favicon file upload.
func PagesBrandUploadFavicon(ctx *context.Context) {
repo := ctx.Repo.Repository
file, header, err := ctx.Req.FormFile("brand_favicon")
if err != nil {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_upload_error"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
return
}
defer file.Close()
if header.Size > maxBrandImageSize {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_image_too_large"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
return
}
data, err := io.ReadAll(file)
if err != nil {
ctx.ServerError("ReadAll", err)
return
}
st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.brand_not_an_image"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
return
}
hash := sha256.Sum256(data)
filename := fmt.Sprintf("pages-favicon-%d-%x", repo.ID, hash[:8])
config := getPagesLandingConfig(ctx)
if config.Brand.UploadedFavicon != "" {
_ = storage.RepoAvatars.Delete(config.Brand.UploadedFavicon)
}
if err := storage.SaveFrom(storage.RepoAvatars, filename, func(w io.Writer) error {
_, err := w.Write(data)
return err
}); err != nil {
ctx.ServerError("SaveFrom", err)
return
}
config.Brand.UploadedFavicon = filename
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.brand_favicon_uploaded"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
pagesUploadImage(ctx, "brand_favicon", "pages-favicon", "/settings/pages/brand",
func(c *pages_module.LandingConfig) string { return c.Brand.UploadedFavicon },
func(c *pages_module.LandingConfig, f string) { c.Brand.UploadedFavicon = f },
"repo.settings.pages.brand_upload_error", "repo.settings.pages.brand_image_too_large",
"repo.settings.pages.brand_not_an_image", "repo.settings.pages.brand_favicon_uploaded")
}
// PagesBrandDeleteFavicon removes the uploaded favicon.
func PagesBrandDeleteFavicon(ctx *context.Context) {
repo := ctx.Repo.Repository
config := getPagesLandingConfig(ctx)
if config.Brand.UploadedFavicon != "" {
_ = storage.RepoAvatars.Delete(config.Brand.UploadedFavicon)
config.Brand.UploadedFavicon = ""
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.brand_favicon_deleted"))
ctx.Redirect(repo.Link() + "/settings/pages/brand")
pagesDeleteImage(ctx, "/settings/pages/brand",
func(c *pages_module.LandingConfig) string { return c.Brand.UploadedFavicon },
func(c *pages_module.LandingConfig) { c.Brand.UploadedFavicon = "" },
"repo.settings.pages.brand_favicon_deleted")
}
func PagesHero(ctx *context.Context) {
@@ -444,83 +418,21 @@ func PagesHeroPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/hero")
}
const maxHeroImageSize = 5 * 1024 * 1024 // 5MB
// PagesHeroUploadImage handles hero image file upload.
func PagesHeroUploadImage(ctx *context.Context) {
repo := ctx.Repo.Repository
file, header, err := ctx.Req.FormFile("hero_image")
if err != nil {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.hero_upload_error"))
ctx.Redirect(repo.Link() + "/settings/pages/hero")
return
}
defer file.Close()
if header.Size > maxHeroImageSize {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.hero_image_too_large"))
ctx.Redirect(repo.Link() + "/settings/pages/hero")
return
}
data, err := io.ReadAll(file)
if err != nil {
ctx.ServerError("ReadAll", err)
return
}
st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.hero_not_an_image"))
ctx.Redirect(repo.Link() + "/settings/pages/hero")
return
}
hash := sha256.Sum256(data)
filename := fmt.Sprintf("pages-hero-%d-%x", repo.ID, hash[:8])
config := getPagesLandingConfig(ctx)
// Delete old uploaded image if it exists
if config.Hero.UploadedImage != "" {
_ = storage.RepoAvatars.Delete(config.Hero.UploadedImage)
}
if err := storage.SaveFrom(storage.RepoAvatars, filename, func(w io.Writer) error {
_, err := w.Write(data)
return err
}); err != nil {
ctx.ServerError("SaveFrom", err)
return
}
config.Hero.UploadedImage = filename
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.hero_image_uploaded"))
ctx.Redirect(repo.Link() + "/settings/pages/hero")
pagesUploadImage(ctx, "hero_image", "pages-hero", "/settings/pages/hero",
func(c *pages_module.LandingConfig) string { return c.Hero.UploadedImage },
func(c *pages_module.LandingConfig, f string) { c.Hero.UploadedImage = f },
"repo.settings.pages.hero_upload_error", "repo.settings.pages.hero_image_too_large",
"repo.settings.pages.hero_not_an_image", "repo.settings.pages.hero_image_uploaded")
}
// PagesHeroDeleteImage removes the uploaded hero image.
func PagesHeroDeleteImage(ctx *context.Context) {
repo := ctx.Repo.Repository
config := getPagesLandingConfig(ctx)
if config.Hero.UploadedImage != "" {
_ = storage.RepoAvatars.Delete(config.Hero.UploadedImage)
config.Hero.UploadedImage = ""
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.hero_image_deleted"))
ctx.Redirect(repo.Link() + "/settings/pages/hero")
pagesDeleteImage(ctx, "/settings/pages/hero",
func(c *pages_module.LandingConfig) string { return c.Hero.UploadedImage },
func(c *pages_module.LandingConfig) { c.Hero.UploadedImage = "" },
"repo.settings.pages.hero_image_deleted")
}
func PagesContent(ctx *context.Context) {