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.
314 lines
9.1 KiB
Go
314 lines
9.1 KiB
Go
// Copyright 2026 MarketAlly. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package pages
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
|
"code.gitcaddy.com/server/v3/modules/git"
|
|
"code.gitcaddy.com/server/v3/modules/json"
|
|
pages_module "code.gitcaddy.com/server/v3/modules/pages"
|
|
"code.gitcaddy.com/server/v3/modules/setting"
|
|
)
|
|
|
|
const (
|
|
// LandingConfigPath is the path to the landing page configuration file
|
|
LandingConfigPath = ".gitea/landing.yaml"
|
|
// LandingConfigPathAlt is an alternative path for the configuration file
|
|
LandingConfigPathAlt = ".gitea/landing.yml"
|
|
)
|
|
|
|
// GetPagesConfig returns the pages configuration for a repository
|
|
// It first tries to load from the database cache, then falls back to parsing the file
|
|
func GetPagesConfig(ctx context.Context, repo *repo_model.Repository) (*pages_module.LandingConfig, error) {
|
|
// Check if pages is configured in DB
|
|
dbConfig, err := repo_model.GetPagesConfigByRepoID(ctx, repo.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Try to load and parse the config from the repository
|
|
fileConfig, fileHash, err := loadConfigFromRepo(ctx, repo)
|
|
if err != nil {
|
|
// If file doesn't exist, check if DB has config
|
|
if dbConfig != nil && dbConfig.ConfigJSON != "" {
|
|
// Use cached config
|
|
var config pages_module.LandingConfig
|
|
if err := json.Unmarshal([]byte(dbConfig.ConfigJSON), &config); err != nil {
|
|
return nil, err
|
|
}
|
|
return &config, nil
|
|
}
|
|
// If Pages is enabled but no config file, return a default config
|
|
if dbConfig != nil && dbConfig.Enabled {
|
|
return getDefaultConfig(repo, string(dbConfig.Template)), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// If we have a file config, check if cache needs updating
|
|
if dbConfig != nil && dbConfig.ConfigHash == fileHash {
|
|
// Cache is up to date, use cached config
|
|
var config pages_module.LandingConfig
|
|
if err := json.Unmarshal([]byte(dbConfig.ConfigJSON), &config); err != nil {
|
|
// Cache is corrupted, use file config
|
|
return fileConfig, nil
|
|
}
|
|
return &config, nil
|
|
}
|
|
|
|
// Update cache with new config
|
|
configJSON, err := json.Marshal(fileConfig)
|
|
if err != nil {
|
|
return fileConfig, nil // Return config even if caching fails
|
|
}
|
|
|
|
if dbConfig == nil {
|
|
// Create new cache entry
|
|
dbConfig = &repo_model.PagesConfig{
|
|
RepoID: repo.ID,
|
|
Enabled: fileConfig.Enabled,
|
|
Template: repo_model.PagesTemplate(fileConfig.Template),
|
|
ConfigJSON: string(configJSON),
|
|
ConfigHash: fileHash,
|
|
}
|
|
if err := repo_model.CreatePagesConfig(ctx, dbConfig); err != nil {
|
|
// Log but don't fail
|
|
return fileConfig, nil
|
|
}
|
|
} else {
|
|
// Update existing cache
|
|
dbConfig.Enabled = fileConfig.Enabled
|
|
dbConfig.Template = repo_model.PagesTemplate(fileConfig.Template)
|
|
dbConfig.ConfigJSON = string(configJSON)
|
|
dbConfig.ConfigHash = fileHash
|
|
if err := repo_model.UpdatePagesConfig(ctx, dbConfig); err != nil {
|
|
// Log but don't fail
|
|
return fileConfig, nil
|
|
}
|
|
}
|
|
|
|
return fileConfig, nil
|
|
}
|
|
|
|
// loadConfigFromRepo loads the landing.yaml configuration from the repository
|
|
|
|
// getDefaultConfig returns a default landing page configuration
|
|
func getDefaultConfig(repo *repo_model.Repository, template string) *pages_module.LandingConfig {
|
|
if template == "" || !pages_module.IsValidTemplate(template) {
|
|
template = "open-source-hero"
|
|
}
|
|
return &pages_module.LandingConfig{
|
|
Enabled: true,
|
|
Template: template,
|
|
Brand: pages_module.BrandConfig{
|
|
Name: repo.Name,
|
|
},
|
|
Hero: pages_module.HeroConfig{
|
|
Headline: repo.Name,
|
|
Subheadline: repo.Description,
|
|
},
|
|
Theme: pages_module.ThemeConfig{
|
|
Mode: "auto",
|
|
},
|
|
}
|
|
}
|
|
|
|
func loadConfigFromRepo(ctx context.Context, repo *repo_model.Repository) (*pages_module.LandingConfig, string, error) {
|
|
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer gitRepo.Close()
|
|
|
|
// Try to get the default branch
|
|
branch := repo.DefaultBranch
|
|
if branch == "" {
|
|
branch = "main"
|
|
}
|
|
|
|
// Try to get the commit
|
|
commit, err := gitRepo.GetBranchCommit(branch)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Try to get the config file
|
|
var content []byte
|
|
entry, err := commit.GetTreeEntryByPath(LandingConfigPath)
|
|
if err != nil {
|
|
// Try alternative path
|
|
entry, err = commit.GetTreeEntryByPath(LandingConfigPathAlt)
|
|
if err != nil {
|
|
return nil, "", errors.New("landing config not found")
|
|
}
|
|
}
|
|
|
|
reader, err := entry.Blob().DataAsync()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer reader.Close()
|
|
|
|
content = make([]byte, entry.Blob().Size())
|
|
_, err = reader.Read(content)
|
|
if err != nil && err.Error() != "EOF" {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Parse the config
|
|
config, err := pages_module.ParseLandingConfig(content)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Calculate hash for cache invalidation
|
|
hash := pages_module.HashConfig(content)
|
|
|
|
return config, hash, nil
|
|
}
|
|
|
|
// IsPagesEnabled checks if pages is enabled for a repository
|
|
func IsPagesEnabled(ctx context.Context, repo *repo_model.Repository) (bool, error) {
|
|
config, err := GetPagesConfig(ctx, repo)
|
|
if err != nil {
|
|
return false, nil // Not enabled if config doesn't exist
|
|
}
|
|
return config.Enabled, nil
|
|
}
|
|
|
|
// EnablePages enables pages for a repository with default config
|
|
func EnablePages(ctx context.Context, repo *repo_model.Repository, template string) error {
|
|
if !pages_module.IsValidTemplate(template) {
|
|
template = "open-source-hero"
|
|
}
|
|
|
|
return repo_model.EnablePages(ctx, repo.ID, repo_model.PagesTemplate(template))
|
|
}
|
|
|
|
// DisablePages disables pages for a repository
|
|
func DisablePages(ctx context.Context, repo *repo_model.Repository) error {
|
|
return repo_model.DisablePages(ctx, repo.ID)
|
|
}
|
|
|
|
// GetPagesSubdomain returns the subdomain for a repository's pages
|
|
func GetPagesSubdomain(repo *repo_model.Repository) string {
|
|
// Format: {repo}-{owner}.{domain}
|
|
return fmt.Sprintf("%s-%s", strings.ToLower(repo.Name), strings.ToLower(repo.OwnerName))
|
|
}
|
|
|
|
// GetPagesURL returns the full URL for a repository's pages
|
|
func GetPagesURL(repo *repo_model.Repository) string {
|
|
subdomain := GetPagesSubdomain(repo)
|
|
// This should be configurable
|
|
// Extract domain from settings
|
|
domain := setting.Domain
|
|
return fmt.Sprintf("https://%s.%s", subdomain, domain)
|
|
}
|
|
|
|
// GetPagesDomains returns all custom domains for a repository's pages
|
|
func GetPagesDomains(ctx context.Context, repoID int64) ([]*repo_model.PagesDomain, error) {
|
|
return repo_model.GetPagesDomainsByRepoID(ctx, repoID)
|
|
}
|
|
|
|
// AddPagesDomain adds a custom domain for pages
|
|
func AddPagesDomain(ctx context.Context, repoID int64, domain string, sslExternal bool) (*repo_model.PagesDomain, error) {
|
|
// Normalize domain
|
|
domain = strings.ToLower(strings.TrimSpace(domain))
|
|
|
|
// Check if domain already exists
|
|
existing, err := repo_model.GetPagesDomainByDomain(ctx, domain)
|
|
if err == nil && existing != nil {
|
|
return nil, repo_model.ErrPagesDomainAlreadyExist{Domain: domain}
|
|
}
|
|
|
|
sslStatus := repo_model.SSLStatusPending
|
|
if sslExternal {
|
|
sslStatus = repo_model.SSLStatusActive
|
|
}
|
|
|
|
pagesDomain := &repo_model.PagesDomain{
|
|
RepoID: repoID,
|
|
Domain: domain,
|
|
SSLStatus: sslStatus,
|
|
}
|
|
|
|
if err := repo_model.CreatePagesDomain(ctx, pagesDomain); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return pagesDomain, nil
|
|
}
|
|
|
|
// RemovePagesDomain removes a custom domain
|
|
func RemovePagesDomain(ctx context.Context, repoID, domainID int64) error {
|
|
domain, err := repo_model.GetPagesDomainByID(ctx, domainID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify domain belongs to this repo
|
|
if domain.RepoID != repoID {
|
|
return repo_model.ErrPagesDomainNotExist{ID: domainID}
|
|
}
|
|
|
|
return repo_model.DeletePagesDomain(ctx, domainID)
|
|
}
|
|
|
|
// VerifyDomain verifies a custom domain by checking DNS records
|
|
func VerifyDomain(ctx context.Context, domainID int64) error {
|
|
domain, err := repo_model.GetPagesDomainByID(ctx, domainID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: Implement actual DNS verification
|
|
// For now, just mark as verified
|
|
return repo_model.VerifyPagesDomain(ctx, domain.ID)
|
|
}
|
|
|
|
// GetRepoByPagesDomain returns the repository for a pages domain
|
|
func GetRepoByPagesDomain(ctx context.Context, domainName string) (*repo_model.Repository, error) {
|
|
domain, err := repo_model.GetPagesDomainByDomain(ctx, domainName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !domain.Verified {
|
|
return nil, errors.New("domain not verified")
|
|
}
|
|
|
|
return repo_model.GetRepositoryByID(ctx, domain.RepoID)
|
|
}
|
|
|
|
// HasPublicLanding checks if a repository has public landing enabled
|
|
// This allows private repos to have a public-facing landing page
|
|
func HasPublicLanding(ctx context.Context, repo *repo_model.Repository) bool {
|
|
config, err := GetPagesConfig(ctx, repo)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return config.Enabled && config.PublicLanding
|
|
}
|
|
|
|
// 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
|
|
}
|
|
return config.Enabled && config.Advanced.PublicReleases
|
|
}
|