2
0
Files
gitcaddy-server/services/pages/pages.go
logikonline 044c65e425 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.
2026-03-16 00:26:53 -04:00

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
}