From 044c65e42593f85973e8dd2305ba197078c7d0ca Mon Sep 17 00:00:00 2001 From: logikonline Date: Mon, 16 Mar 2026 00:26:53 -0400 Subject: [PATCH] feat(releases): add public release downloads setting for limited repos Add PublicReleaseDownloads repository setting to allow direct release downloads without authentication for Limited visibility repos. Adds migration v369, checkbox in Advanced Settings (only shown for Limited repos with releases enabled), and updates attachment serving logic to check HasPublicReleases. Moves release download routes outside auth requirement when public downloads enabled. Complements existing pages config PublicReleases toggle. --- models/migrations/migrations.go | 1 + models/migrations/v1_26/v369.go | 16 ++++++++++++++++ models/repo/repo.go | 1 + options/locale/locale_en-US.json | 1 + routers/web/repo/attachment.go | 8 ++++++-- routers/web/repo/setting/setting.go | 5 +++++ routers/web/web.go | 8 ++++++-- services/forms/repo_form.go | 3 ++- services/pages/pages.go | 10 ++++++++-- templates/repo/settings/options.tmpl | 9 +++++++++ 10 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 models/migrations/v1_26/v369.go diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index b1f0ceb97d..9113e0c62f 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -443,6 +443,7 @@ func prepareMigrationTasks() []*migration { newMigration(366, "Add page experiment tables for A/B testing", v1_26.AddPageExperimentTables), newMigration(367, "Add pages translation table for multi-language support", v1_26.AddPagesTranslationTable), newMigration(368, "Add owner_display_name to repository", v1_26.AddOwnerDisplayNameToRepository), + newMigration(369, "Add public_release_downloads to repository", v1_26.AddPublicReleaseDownloadsToRepository), } return preparedMigrations } diff --git a/models/migrations/v1_26/v369.go b/models/migrations/v1_26/v369.go new file mode 100644 index 0000000000..e34527c504 --- /dev/null +++ b/models/migrations/v1_26/v369.go @@ -0,0 +1,16 @@ +// Copyright 2026 MarketAlly. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "xorm.io/xorm" +) + +func AddPublicReleaseDownloadsToRepository(x *xorm.Engine) error { + type Repository struct { + PublicReleaseDownloads bool `xorm:"NOT NULL DEFAULT false"` + } + + return x.Sync(new(Repository)) +} diff --git a/models/repo/repo.go b/models/repo/repo.go index f5fc7f1585..5d473cf70a 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -223,6 +223,7 @@ type Repository struct { BlogEnabled bool `xorm:"NOT NULL DEFAULT false"` WishlistEnabled bool `xorm:"NOT NULL DEFAULT false"` PublicAppIntegration bool `xorm:"NOT NULL DEFAULT true"` + PublicReleaseDownloads bool `xorm:"NOT NULL DEFAULT false"` ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"` TrustModel TrustModelType diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 0d9a18d5e8..4ad73dfbcb 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2346,6 +2346,7 @@ "repo.settings.pulls.default_delete_branch_after_merge": "Delete pull request branch after merge by default", "repo.settings.pulls.default_allow_edits_from_maintainers": "Allow edits from maintainers by default", "repo.settings.releases_desc": "Enable Repository Releases", + "repo.settings.public_release_downloads_desc": "Allow direct release downloads without authentication (for Limited visibility repos)", "repo.settings.packages_desc": "Enable Repository Packages Registry", "repo.settings.projects_desc": "Enable Projects", "repo.settings.projects_mode_desc": "Projects Mode (which kinds of projects to show)", diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 54db8f131a..4a56a851d1 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -20,6 +20,7 @@ import ( "code.gitcaddy.com/server/v3/services/attachment" "code.gitcaddy.com/server/v3/services/context" "code.gitcaddy.com/server/v3/services/context/upload" + pages_service "code.gitcaddy.com/server/v3/services/pages" repo_service "code.gitcaddy.com/server/v3/services/repository" ) @@ -141,8 +142,11 @@ func ServeAttachment(ctx *context.Context, uuid string) { return } if !perm.CanRead(unitType) { - ctx.HTTPError(http.StatusNotFound) - return + // Allow public release downloads when the repo has public releases enabled + if unitType != unit.TypeReleases || !pages_service.HasPublicReleases(ctx, repository) { + ctx.HTTPError(http.StatusNotFound) + return + } } } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index a620812e4f..aa139ba3ae 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -536,6 +536,11 @@ func handleSettingsPostAdvanced(ctx *context.Context) { repoChanged = true } + if repo.PublicReleaseDownloads != form.EnablePublicReleaseDownloads { + repo.PublicReleaseDownloads = form.EnablePublicReleaseDownloads + repoChanged = true + } + if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() { units = append(units, newRepoUnit(repo, unit_model.TypeCode, nil)) } else if !unit_model.TypeCode.UnitGlobalDisabled() { diff --git a/routers/web/web.go b/routers/web/web.go index a391705323..da200ad492 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1616,6 +1616,12 @@ func registerWebRoutes(m *web.Router) { }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader, reqSubscriptionForCode) // end "/{username}/{reponame}": repo tags + m.Group("/{username}/{reponame}", func() { // public release downloads (no reqRepoReleaseReader — access checked in handler via HasPublicReleases) + m.Get("/releases/attachments/{uuid}", repo.GetAttachment) + m.Get("/releases/download/{vTag}/{fileName}", repo.RedirectDownload) + }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty) + // end "/{username}/{reponame}": public release downloads + m.Group("/{username}/{reponame}", func() { // repo releases m.Group("/releases", func() { m.Get("", repo.Releases) @@ -1624,8 +1630,6 @@ func registerWebRoutes(m *web.Router) { m.Get("/tag/*", repo.SingleRelease) m.Get("/latest", repo.LatestRelease) }, ctxDataSet("EnableFeed", setting.Other.EnableFeed)) - m.Get("/releases/attachments/{uuid}", repo.GetAttachment) - m.Get("/releases/download/{vTag}/{fileName}", repo.RedirectDownload) m.Group("/releases", func() { m.Get("/new", repo.NewRelease) m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 4f903c4073..de4e936bb5 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -132,7 +132,8 @@ type RepoSettingForm struct { EnableProjects bool ProjectsMode string - EnableReleases bool + EnableReleases bool + EnablePublicReleaseDownloads bool EnablePackages bool diff --git a/services/pages/pages.go b/services/pages/pages.go index 0d75c502ce..66b6d2cd22 100644 --- a/services/pages/pages.go +++ b/services/pages/pages.go @@ -296,9 +296,15 @@ func HasPublicLanding(ctx context.Context, repo *repo_model.Repository) bool { return config.Enabled && config.PublicLanding } -// HasPublicReleases checks if a repository has public releases enabled -// This allows private repos to have publicly accessible releases +// HasPublicReleases checks if a repository has public releases enabled. +// This can be enabled either via the repo-level PublicReleaseDownloads setting +// (in Advanced Settings) or via the pages config PublicReleases toggle. func HasPublicReleases(ctx context.Context, repo *repo_model.Repository) bool { + // Check repo-level setting first (Advanced Settings toggle) + if repo.PublicReleaseDownloads { + return true + } + // Fall back to pages config config, err := GetPagesConfig(ctx, repo) if err != nil { return false diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 55da6d0bc2..b16d7d660c 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -518,6 +518,15 @@ + {{if and $isReleasesEnabled .Repository.IsLimited}} +
+
+ + +
+
+ {{end}} + {{$isPackagesEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePackages}} {{$isPackagesGlobalDisabled := ctx.Consts.RepoUnitTypePackages.UnitGlobalDisabled}}