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}}