初始提交: Gitea 项目代码
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
const defaultMaxRerunAttempts = 50
|
||||
|
||||
// Actions settings
|
||||
var (
|
||||
Actions = struct {
|
||||
Enabled bool
|
||||
LogStorage *Storage // how the created logs should be stored
|
||||
LogRetentionDays int64 `ini:"LOG_RETENTION_DAYS"`
|
||||
LogCompression logCompression `ini:"LOG_COMPRESSION"`
|
||||
ArtifactStorage *Storage // how the created artifacts should be stored
|
||||
ArtifactRetentionDays int64 `ini:"ARTIFACT_RETENTION_DAYS"`
|
||||
DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"`
|
||||
ZombieTaskTimeout time.Duration `ini:"ZOMBIE_TASK_TIMEOUT"`
|
||||
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
|
||||
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
|
||||
SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"`
|
||||
WorkflowDirs []string `ini:"WORKFLOW_DIRS"`
|
||||
MaxRerunAttempts int64 `ini:"MAX_RERUN_ATTEMPTS"`
|
||||
}{
|
||||
Enabled: true,
|
||||
DefaultActionsURL: defaultActionsURLGitHub,
|
||||
SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"},
|
||||
WorkflowDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
MaxRerunAttempts: defaultMaxRerunAttempts,
|
||||
}
|
||||
)
|
||||
|
||||
type defaultActionsURL string
|
||||
|
||||
func (url defaultActionsURL) URL() string {
|
||||
switch url {
|
||||
case defaultActionsURLGitHub:
|
||||
return "https://github.com"
|
||||
case defaultActionsURLSelf:
|
||||
return strings.TrimSuffix(AppURL, "/")
|
||||
default:
|
||||
// This should never happen, but just in case, use GitHub as fallback
|
||||
return "https://github.com"
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
defaultActionsURLGitHub = "github" // https://github.com
|
||||
defaultActionsURLSelf = "self" // the root URL of the self-hosted Gitea instance
|
||||
// DefaultActionsURL only supports GitHub and the self-hosted Gitea.
|
||||
// It's intentionally not supported more, so please be cautious before adding more like "gitea" or "gitlab".
|
||||
// If you get some trouble with `uses: username/action_name@version` in your workflow,
|
||||
// please consider to use `uses: https://the_url_you_want_to_use/username/action_name@version` instead.
|
||||
)
|
||||
|
||||
type logCompression string
|
||||
|
||||
func (c logCompression) IsValid() bool {
|
||||
return c.IsNone() || c.IsZstd()
|
||||
}
|
||||
|
||||
func (c logCompression) IsNone() bool {
|
||||
return string(c) == "none"
|
||||
}
|
||||
|
||||
func (c logCompression) IsZstd() bool {
|
||||
return c == "" || string(c) == "zstd"
|
||||
}
|
||||
|
||||
func loadActionsFrom(rootCfg ConfigProvider) error {
|
||||
sec := rootCfg.Section("actions")
|
||||
err := sec.MapTo(&Actions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to map Actions settings: %v", err)
|
||||
}
|
||||
|
||||
if urls := string(Actions.DefaultActionsURL); urls != defaultActionsURLGitHub && urls != defaultActionsURLSelf {
|
||||
url := strings.Split(urls, ",")[0]
|
||||
if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") {
|
||||
log.Error("[actions] DEFAULT_ACTIONS_URL does not support %q as custom URL any longer, fallback to %q",
|
||||
urls,
|
||||
defaultActionsURLGitHub,
|
||||
)
|
||||
Actions.DefaultActionsURL = defaultActionsURLGitHub
|
||||
} else {
|
||||
return fmt.Errorf("unsupported [actions] DEFAULT_ACTIONS_URL: %q", urls)
|
||||
}
|
||||
}
|
||||
|
||||
// don't support to read configuration from [actions]
|
||||
Actions.LogStorage, err = getStorage(rootCfg, "actions_log", "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// default to 1 year
|
||||
if Actions.LogRetentionDays <= 0 {
|
||||
Actions.LogRetentionDays = 365
|
||||
}
|
||||
|
||||
actionsSec, _ := rootCfg.GetSection("actions.artifacts")
|
||||
|
||||
Actions.ArtifactStorage, err = getStorage(rootCfg, "actions_artifacts", "", actionsSec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// default to 90 days in Github Actions
|
||||
if Actions.ArtifactRetentionDays <= 0 {
|
||||
Actions.ArtifactRetentionDays = 90
|
||||
}
|
||||
|
||||
Actions.ZombieTaskTimeout = sec.Key("ZOMBIE_TASK_TIMEOUT").MustDuration(10 * time.Minute)
|
||||
Actions.EndlessTaskTimeout = sec.Key("ENDLESS_TASK_TIMEOUT").MustDuration(3 * time.Hour)
|
||||
Actions.AbandonedJobTimeout = sec.Key("ABANDONED_JOB_TIMEOUT").MustDuration(24 * time.Hour)
|
||||
|
||||
if Actions.MaxRerunAttempts <= 0 {
|
||||
Actions.MaxRerunAttempts = defaultMaxRerunAttempts
|
||||
}
|
||||
|
||||
if !Actions.LogCompression.IsValid() {
|
||||
return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression)
|
||||
}
|
||||
|
||||
workflowDirs := make([]string, 0, len(Actions.WorkflowDirs))
|
||||
for _, dir := range Actions.WorkflowDirs {
|
||||
dir = strings.TrimSpace(dir)
|
||||
if dir == "" {
|
||||
continue
|
||||
}
|
||||
dir = strings.ReplaceAll(dir, `\`, `/`)
|
||||
dir = strings.TrimRight(dir, "/")
|
||||
workflowDirs = append(workflowDirs, dir)
|
||||
}
|
||||
if len(workflowDirs) == 0 {
|
||||
return errors.New("[actions] WORKFLOW_DIRS must contain at least one entry")
|
||||
}
|
||||
Actions.WorkflowDirs = workflowDirs
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_getStorageInheritNameSectionTypeForActions(t *testing.T) {
|
||||
iniStr := `
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadActionsFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", Actions.LogStorage.Type)
|
||||
assert.Equal(t, "actions_log/", Actions.LogStorage.MinioConfig.BasePath)
|
||||
assert.EqualValues(t, "minio", Actions.ArtifactStorage.Type)
|
||||
assert.Equal(t, "actions_artifacts/", Actions.ArtifactStorage.MinioConfig.BasePath)
|
||||
|
||||
iniStr = `
|
||||
[storage.actions_log]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadActionsFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", Actions.LogStorage.Type)
|
||||
assert.Equal(t, "actions_log/", Actions.LogStorage.MinioConfig.BasePath)
|
||||
assert.EqualValues(t, "local", Actions.ArtifactStorage.Type)
|
||||
assert.Equal(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path))
|
||||
|
||||
iniStr = `
|
||||
[storage.actions_log]
|
||||
STORAGE_TYPE = my_storage
|
||||
|
||||
[storage.my_storage]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadActionsFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", Actions.LogStorage.Type)
|
||||
assert.Equal(t, "actions_log/", Actions.LogStorage.MinioConfig.BasePath)
|
||||
assert.EqualValues(t, "local", Actions.ArtifactStorage.Type)
|
||||
assert.Equal(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path))
|
||||
|
||||
iniStr = `
|
||||
[storage.actions_artifacts]
|
||||
STORAGE_TYPE = my_storage
|
||||
|
||||
[storage.my_storage]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadActionsFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "local", Actions.LogStorage.Type)
|
||||
assert.Equal(t, "actions_log", filepath.Base(Actions.LogStorage.Path))
|
||||
assert.EqualValues(t, "minio", Actions.ArtifactStorage.Type)
|
||||
assert.Equal(t, "actions_artifacts/", Actions.ArtifactStorage.MinioConfig.BasePath)
|
||||
|
||||
iniStr = `
|
||||
[storage.actions_artifacts]
|
||||
STORAGE_TYPE = my_storage
|
||||
|
||||
[storage.my_storage]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadActionsFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "local", Actions.LogStorage.Type)
|
||||
assert.Equal(t, "actions_log", filepath.Base(Actions.LogStorage.Path))
|
||||
assert.EqualValues(t, "minio", Actions.ArtifactStorage.Type)
|
||||
assert.Equal(t, "actions_artifacts/", Actions.ArtifactStorage.MinioConfig.BasePath)
|
||||
|
||||
iniStr = ``
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadActionsFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "local", Actions.LogStorage.Type)
|
||||
assert.Equal(t, "actions_log", filepath.Base(Actions.LogStorage.Path))
|
||||
assert.EqualValues(t, "local", Actions.ArtifactStorage.Type)
|
||||
assert.Equal(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path))
|
||||
}
|
||||
|
||||
func Test_WorkflowDirs(t *testing.T) {
|
||||
oldActions := Actions
|
||||
defer func() {
|
||||
Actions = oldActions
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
iniStr string
|
||||
wantDirs []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
iniStr: `[actions]`,
|
||||
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "single dir",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows",
|
||||
wantDirs: []string{".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "custom order",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows,.gitea/workflows",
|
||||
wantDirs: []string{".github/workflows", ".gitea/workflows"},
|
||||
},
|
||||
{
|
||||
name: "whitespace trimming",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows , .github/workflows ",
|
||||
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "trailing slash normalization",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows/,.github/workflows/",
|
||||
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "only commas and whitespace",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = , , ,",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(tt.iniStr)
|
||||
require.NoError(t, err)
|
||||
err = loadActionsFrom(cfg)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantDirs, Actions.WorkflowDirs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getDefaultActionsURLForActions(t *testing.T) {
|
||||
oldActions := Actions
|
||||
oldAppURL := AppURL
|
||||
defer func() {
|
||||
Actions = oldActions
|
||||
AppURL = oldAppURL
|
||||
}()
|
||||
|
||||
AppURL = "http://test_get_default_actions_url_for_actions:3000/"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
iniStr string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
wantURL string
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
iniStr: `
|
||||
[actions]
|
||||
`,
|
||||
wantErr: assert.NoError,
|
||||
wantURL: "https://github.com",
|
||||
},
|
||||
{
|
||||
name: "github",
|
||||
iniStr: `
|
||||
[actions]
|
||||
DEFAULT_ACTIONS_URL = github
|
||||
`,
|
||||
wantErr: assert.NoError,
|
||||
wantURL: "https://github.com",
|
||||
},
|
||||
{
|
||||
name: "self",
|
||||
iniStr: `
|
||||
[actions]
|
||||
DEFAULT_ACTIONS_URL = self
|
||||
`,
|
||||
wantErr: assert.NoError,
|
||||
wantURL: "http://test_get_default_actions_url_for_actions:3000",
|
||||
},
|
||||
{
|
||||
name: "custom url",
|
||||
iniStr: `
|
||||
[actions]
|
||||
DEFAULT_ACTIONS_URL = https://gitea.com
|
||||
`,
|
||||
wantErr: assert.NoError,
|
||||
wantURL: "https://github.com",
|
||||
},
|
||||
{
|
||||
name: "custom urls",
|
||||
iniStr: `
|
||||
[actions]
|
||||
DEFAULT_ACTIONS_URL = https://gitea.com,https://github.com
|
||||
`,
|
||||
wantErr: assert.NoError,
|
||||
wantURL: "https://github.com",
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
iniStr: `
|
||||
[actions]
|
||||
DEFAULT_ACTIONS_URL = gitea
|
||||
`,
|
||||
wantErr: assert.Error,
|
||||
wantURL: "https://github.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(tt.iniStr)
|
||||
require.NoError(t, err)
|
||||
if !tt.wantErr(t, loadActionsFrom(cfg)) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.wantURL, Actions.DefaultActionsURL.URL())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// Admin settings
|
||||
var Admin struct {
|
||||
DisableRegularOrgCreation bool
|
||||
DefaultEmailNotification string
|
||||
UserDisabledFeatures container.Set[string]
|
||||
ExternalUserDisableFeatures container.Set[string]
|
||||
}
|
||||
|
||||
var validUserFeatures = container.SetOf(
|
||||
UserFeatureDeletion,
|
||||
UserFeatureManageSSHKeys,
|
||||
UserFeatureManageGPGKeys,
|
||||
UserFeatureManageMFA,
|
||||
UserFeatureManageCredentials,
|
||||
UserFeatureChangeUsername,
|
||||
UserFeatureChangeFullName,
|
||||
)
|
||||
|
||||
func loadAdminFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("admin")
|
||||
Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
|
||||
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
|
||||
Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
|
||||
Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...).Union(Admin.UserDisabledFeatures)
|
||||
|
||||
for feature := range Admin.UserDisabledFeatures {
|
||||
if !validUserFeatures.Contains(feature) {
|
||||
log.Warn("USER_DISABLED_FEATURES contains unknown feature %q", feature)
|
||||
}
|
||||
}
|
||||
for feature := range Admin.ExternalUserDisableFeatures {
|
||||
if !validUserFeatures.Contains(feature) && !Admin.UserDisabledFeatures.Contains(feature) {
|
||||
log.Warn("EXTERNAL_USER_DISABLE_FEATURES contains unknown feature %q", feature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
UserFeatureDeletion = "deletion"
|
||||
UserFeatureManageSSHKeys = "manage_ssh_keys"
|
||||
UserFeatureManageGPGKeys = "manage_gpg_keys"
|
||||
UserFeatureManageMFA = "manage_mfa"
|
||||
UserFeatureManageCredentials = "manage_credentials"
|
||||
UserFeatureChangeUsername = "change_username"
|
||||
UserFeatureChangeFullName = "change_full_name"
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// API settings
|
||||
var API = struct {
|
||||
EnableSwagger bool
|
||||
SwaggerURL string
|
||||
MaxResponseItems int
|
||||
DefaultPagingNum int
|
||||
DefaultGitTreesPerPage int
|
||||
DefaultMaxBlobSize int64
|
||||
DefaultMaxResponseSize int64
|
||||
}{
|
||||
EnableSwagger: true,
|
||||
SwaggerURL: "",
|
||||
MaxResponseItems: 50,
|
||||
DefaultPagingNum: 30,
|
||||
DefaultGitTreesPerPage: 1000,
|
||||
DefaultMaxBlobSize: 10485760,
|
||||
DefaultMaxResponseSize: 104857600,
|
||||
}
|
||||
|
||||
func loadAPIFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "api", &API)
|
||||
|
||||
defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort
|
||||
u, err := url.Parse(rootCfg.Section("server").Key("ROOT_URL").MustString(defaultAppURL))
|
||||
if err != nil {
|
||||
log.Fatal("Invalid ROOT_URL '%s': %s", AppURL, err)
|
||||
}
|
||||
u.Path = path.Join(u.Path, "api", "swagger")
|
||||
API.SwaggerURL = u.String()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !bindata
|
||||
|
||||
package setting
|
||||
|
||||
const HasBuiltinBindata = false
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build bindata
|
||||
|
||||
package setting
|
||||
|
||||
const HasBuiltinBindata = true
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
type AttachmentSettingType struct {
|
||||
Storage *Storage
|
||||
AllowedTypes string
|
||||
MaxSize int64
|
||||
MaxFiles int
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
var Attachment AttachmentSettingType
|
||||
|
||||
func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
|
||||
Attachment = AttachmentSettingType{
|
||||
AllowedTypes: ".avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip",
|
||||
MaxSize: 100,
|
||||
MaxFiles: 5,
|
||||
Enabled: true,
|
||||
}
|
||||
sec, _ := rootCfg.GetSection("attachment")
|
||||
if sec == nil {
|
||||
Attachment.Storage, err = getStorage(rootCfg, "attachments", "", nil)
|
||||
return err
|
||||
}
|
||||
|
||||
Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(Attachment.AllowedTypes)
|
||||
Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(Attachment.MaxSize)
|
||||
Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(Attachment.MaxFiles)
|
||||
Attachment.Enabled = sec.Key("ENABLED").MustBool(Attachment.Enabled)
|
||||
Attachment.Storage, err = getStorage(rootCfg, "attachments", "", sec)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getStorageCustomType(t *testing.T) {
|
||||
iniStr := `
|
||||
[attachment]
|
||||
STORAGE_TYPE = my_minio
|
||||
MINIO_BUCKET = gitea-attachment
|
||||
|
||||
[storage.my_minio]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ENDPOINT = my_minio:9000
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadAttachmentFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", Attachment.Storage.Type)
|
||||
assert.Equal(t, "my_minio:9000", Attachment.Storage.MinioConfig.Endpoint)
|
||||
assert.Equal(t, "gitea-attachment", Attachment.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "attachments/", Attachment.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_getStorageTypeSectionOverridesStorageSection(t *testing.T) {
|
||||
iniStr := `
|
||||
[attachment]
|
||||
STORAGE_TYPE = minio
|
||||
|
||||
[storage.minio]
|
||||
MINIO_BUCKET = gitea-minio
|
||||
|
||||
[storage]
|
||||
MINIO_BUCKET = gitea
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadAttachmentFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", Attachment.Storage.Type)
|
||||
assert.Equal(t, "gitea-minio", Attachment.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "attachments/", Attachment.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_getStorageSpecificOverridesStorage(t *testing.T) {
|
||||
iniStr := `
|
||||
[attachment]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_BUCKET = gitea-attachment
|
||||
|
||||
[storage.attachments]
|
||||
MINIO_BUCKET = gitea
|
||||
|
||||
[storage]
|
||||
STORAGE_TYPE = local
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadAttachmentFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", Attachment.Storage.Type)
|
||||
assert.Equal(t, "gitea-attachment", Attachment.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "attachments/", Attachment.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_getStorageGetDefaults(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData("")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadAttachmentFrom(cfg))
|
||||
|
||||
// default storage is local, so bucket is empty
|
||||
assert.Empty(t, Attachment.Storage.MinioConfig.Bucket)
|
||||
}
|
||||
|
||||
func Test_getStorageInheritNameSectionType(t *testing.T) {
|
||||
iniStr := `
|
||||
[storage.attachments]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadAttachmentFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", Attachment.Storage.Type)
|
||||
}
|
||||
|
||||
func Test_AttachmentStorage(t *testing.T) {
|
||||
iniStr := `
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ENDPOINT = s3.my-domain.net
|
||||
MINIO_BUCKET = gitea
|
||||
MINIO_LOCATION = homenet
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_ACCESS_KEY_ID = correct_key
|
||||
MINIO_SECRET_ACCESS_KEY = correct_key
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadAttachmentFrom(cfg))
|
||||
storage := Attachment.Storage
|
||||
|
||||
assert.EqualValues(t, "minio", storage.Type)
|
||||
assert.Equal(t, "gitea", storage.MinioConfig.Bucket)
|
||||
}
|
||||
|
||||
func Test_AttachmentStorage1(t *testing.T) {
|
||||
iniStr := `
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadAttachmentFrom(cfg))
|
||||
assert.EqualValues(t, "minio", Attachment.Storage.Type)
|
||||
assert.Equal(t, "gitea", Attachment.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "attachments/", Attachment.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// Cache represents cache settings
|
||||
type Cache struct {
|
||||
Adapter string
|
||||
Interval int
|
||||
Conn string
|
||||
TTL time.Duration `ini:"ITEM_TTL"`
|
||||
}
|
||||
|
||||
// CacheService the global cache
|
||||
var CacheService = struct {
|
||||
Cache `ini:"cache"`
|
||||
|
||||
LastCommit struct {
|
||||
TTL time.Duration `ini:"ITEM_TTL"`
|
||||
CommitsCount int64
|
||||
} `ini:"cache.last_commit"`
|
||||
}{
|
||||
Cache: Cache{
|
||||
Adapter: "memory",
|
||||
Interval: 60,
|
||||
TTL: 16 * time.Hour,
|
||||
},
|
||||
LastCommit: struct {
|
||||
TTL time.Duration `ini:"ITEM_TTL"`
|
||||
CommitsCount int64
|
||||
}{
|
||||
TTL: 8760 * time.Hour,
|
||||
CommitsCount: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
// MemcacheMaxTTL represents the maximum memcache TTL
|
||||
const MemcacheMaxTTL = 30 * 24 * time.Hour
|
||||
|
||||
func loadCacheFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("cache")
|
||||
if err := sec.MapTo(&CacheService); err != nil {
|
||||
log.Fatal("Failed to map Cache settings: %v", err)
|
||||
}
|
||||
|
||||
CacheService.Adapter = sec.Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache", "twoqueue"})
|
||||
switch CacheService.Adapter {
|
||||
case "memory":
|
||||
case "redis", "memcache":
|
||||
CacheService.Conn = strings.Trim(sec.Key("HOST").String(), "\" ")
|
||||
case "twoqueue":
|
||||
CacheService.Conn = strings.TrimSpace(sec.Key("HOST").String())
|
||||
if CacheService.Conn == "" {
|
||||
CacheService.Conn = "50000"
|
||||
}
|
||||
default:
|
||||
log.Fatal("Unknown cache adapter: %s", CacheService.Adapter)
|
||||
}
|
||||
|
||||
sec = rootCfg.Section("cache.last_commit")
|
||||
CacheService.LastCommit.CommitsCount = sec.Key("COMMITS_COUNT").MustInt64(1000)
|
||||
}
|
||||
|
||||
// TTLSeconds returns the TTLSeconds or unix timestamp for memcache
|
||||
func (c Cache) TTLSeconds() int64 {
|
||||
if c.Adapter == "memcache" && c.TTL > MemcacheMaxTTL {
|
||||
return time.Now().Add(c.TTL).Unix()
|
||||
}
|
||||
return int64(c.TTL.Seconds())
|
||||
}
|
||||
|
||||
// LastCommitCacheTTLSeconds returns the TTLSeconds or unix timestamp for memcache
|
||||
func LastCommitCacheTTLSeconds() int64 {
|
||||
if CacheService.Adapter == "memcache" && CacheService.LastCommit.TTL > MemcacheMaxTTL {
|
||||
return time.Now().Add(CacheService.LastCommit.TTL).Unix()
|
||||
}
|
||||
return int64(CacheService.LastCommit.TTL.Seconds())
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
var Camo = struct {
|
||||
Enabled bool
|
||||
ServerURL string `ini:"SERVER_URL"`
|
||||
HMACKey string `ini:"HMAC_KEY"`
|
||||
Always bool
|
||||
}{}
|
||||
|
||||
func loadCamoFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "camo", &Camo)
|
||||
if Camo.Enabled {
|
||||
oldValue := rootCfg.Section("camo").Key("ALLWAYS").MustString("")
|
||||
if oldValue != "" {
|
||||
log.Warn("camo.ALLWAYS is deprecated, use camo.ALWAYS instead")
|
||||
Camo.Always, _ = strconv.ParseBool(oldValue)
|
||||
}
|
||||
|
||||
if Camo.ServerURL == "" || Camo.HMACKey == "" {
|
||||
log.Fatal(`Camo settings require "SERVER_URL" and HMAC_KEY`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting/config"
|
||||
)
|
||||
|
||||
type PictureStruct struct {
|
||||
DisableGravatar *config.Option[bool]
|
||||
EnableFederatedAvatar *config.Option[bool]
|
||||
}
|
||||
|
||||
type OpenWithEditorApp struct {
|
||||
DisplayName string
|
||||
OpenURL string
|
||||
}
|
||||
|
||||
type OpenWithEditorAppsType []OpenWithEditorApp
|
||||
|
||||
// ToTextareaString is only used in templates, for help prompt only
|
||||
// TODO: OPEN-WITH-EDITOR-APP-JSON: Because there is no "rich UI", a plain text editor is used to manage the list of apps
|
||||
// Maybe we can use some better formats like Yaml in the future, then a simple textarea can manage the config clearly
|
||||
func (t OpenWithEditorAppsType) ToTextareaString() string {
|
||||
var ret strings.Builder
|
||||
for _, app := range t {
|
||||
ret.WriteString(app.DisplayName + " = " + app.OpenURL + "\n")
|
||||
}
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func openWithEditorAppsDefaultValue() OpenWithEditorAppsType {
|
||||
return OpenWithEditorAppsType{
|
||||
{
|
||||
DisplayName: "VS Code",
|
||||
OpenURL: "vscode://vscode.git/clone?url={url}",
|
||||
},
|
||||
{
|
||||
DisplayName: "VSCodium",
|
||||
OpenURL: "vscodium://vscode.git/clone?url={url}",
|
||||
},
|
||||
{
|
||||
DisplayName: "Intellij IDEA",
|
||||
OpenURL: "jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo={url}",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type RepositoryStruct struct {
|
||||
OpenWithEditorApps *config.Option[OpenWithEditorAppsType]
|
||||
GitGuideRemoteName *config.Option[string]
|
||||
}
|
||||
|
||||
type ConfigStruct struct {
|
||||
Picture *PictureStruct
|
||||
Repository *RepositoryStruct
|
||||
Instance *InstanceStruct
|
||||
}
|
||||
|
||||
var (
|
||||
defaultConfig *ConfigStruct
|
||||
defaultConfigOnce sync.Once
|
||||
)
|
||||
|
||||
func initDefaultConfig() {
|
||||
config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
|
||||
defaultConfig = &ConfigStruct{
|
||||
Picture: &PictureStruct{
|
||||
DisableGravatar: config.NewOption[bool]("picture.disable_gravatar").WithDefaultSimple(true).WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}),
|
||||
EnableFederatedAvatar: config.NewOption[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
|
||||
},
|
||||
Repository: &RepositoryStruct{
|
||||
OpenWithEditorApps: config.NewOption[OpenWithEditorAppsType]("repository.open-with.editor-apps").WithEmptyAsDefault().WithDefaultFunc(openWithEditorAppsDefaultValue),
|
||||
GitGuideRemoteName: config.NewOption[string]("repository.git-guide-remote-name").WithEmptyAsDefault().WithDefaultSimple("origin"),
|
||||
},
|
||||
Instance: &InstanceStruct{
|
||||
WebBanner: config.NewOption[WebBannerType]("instance.web_banner"),
|
||||
MaintenanceMode: config.NewOption[MaintenanceModeType]("instance.maintenance_mode"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Config() *ConfigStruct {
|
||||
defaultConfigOnce.Do(initDefaultConfig)
|
||||
return defaultConfig
|
||||
}
|
||||
|
||||
type cfgSecKeyGetter struct{}
|
||||
|
||||
func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) {
|
||||
if key == "" {
|
||||
return "", false
|
||||
}
|
||||
cfgSec, err := CfgProvider.GetSection(sec)
|
||||
if err != nil {
|
||||
log.Error("Unable to get config section: %q", sec)
|
||||
return "", false
|
||||
}
|
||||
cfgKey := ConfigSectionKey(cfgSec, key)
|
||||
if cfgKey == nil {
|
||||
return "", false
|
||||
}
|
||||
return cfgKey.Value(), true
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var getterMu sync.RWMutex
|
||||
|
||||
type CfgSecKeyGetter interface {
|
||||
GetValue(sec, key string) (v string, has bool)
|
||||
}
|
||||
|
||||
var cfgSecKeyGetterInternal CfgSecKeyGetter
|
||||
|
||||
func SetCfgSecKeyGetter(p CfgSecKeyGetter) {
|
||||
getterMu.Lock()
|
||||
cfgSecKeyGetterInternal = p
|
||||
getterMu.Unlock()
|
||||
}
|
||||
|
||||
func GetCfgSecKeyGetter() CfgSecKeyGetter {
|
||||
getterMu.RLock()
|
||||
defer getterMu.RUnlock()
|
||||
return cfgSecKeyGetterInternal
|
||||
}
|
||||
|
||||
type DynKeyGetter interface {
|
||||
GetValue(ctx context.Context, key string) (v string, has bool)
|
||||
GetRevision(ctx context.Context) int
|
||||
InvalidateCache()
|
||||
}
|
||||
|
||||
var dynKeyGetterInternal DynKeyGetter
|
||||
|
||||
func SetDynGetter(p DynKeyGetter) {
|
||||
getterMu.Lock()
|
||||
dynKeyGetterInternal = p
|
||||
getterMu.Unlock()
|
||||
}
|
||||
|
||||
func GetDynGetter() DynKeyGetter {
|
||||
getterMu.RLock()
|
||||
defer getterMu.RUnlock()
|
||||
return dynKeyGetterInternal
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
type CfgSecKey struct {
|
||||
Sec, Key string
|
||||
}
|
||||
|
||||
// OptionInterface is used to overcome Golang's generic interface limitation
|
||||
type OptionInterface interface {
|
||||
GetDefaultValue() any
|
||||
}
|
||||
|
||||
type Option[T any] struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
cfgSecKey CfgSecKey
|
||||
dynKey string
|
||||
|
||||
value T
|
||||
defSimple T
|
||||
defFunc func() T
|
||||
emptyAsDef bool
|
||||
has bool
|
||||
revision int
|
||||
}
|
||||
|
||||
func (opt *Option[T]) GetDefaultValue() any {
|
||||
return opt.DefaultValue()
|
||||
}
|
||||
|
||||
func (opt *Option[T]) parse(key, valStr string) (v T) {
|
||||
v = opt.DefaultValue()
|
||||
if valStr != "" {
|
||||
if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
|
||||
log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (opt *Option[T]) HasValue(ctx context.Context) bool {
|
||||
_, _, has := opt.ValueRevision(ctx)
|
||||
return has
|
||||
}
|
||||
|
||||
func (opt *Option[T]) Value(ctx context.Context) (v T) {
|
||||
v, _, _ = opt.ValueRevision(ctx)
|
||||
return v
|
||||
}
|
||||
|
||||
func isZeroOrEmpty(v any) bool {
|
||||
if v == nil {
|
||||
return true // interface itself is nil
|
||||
}
|
||||
r := reflect.ValueOf(v)
|
||||
if r.IsZero() {
|
||||
return true
|
||||
}
|
||||
|
||||
if r.Kind() == reflect.Slice || r.Kind() == reflect.Map {
|
||||
if r.IsNil() {
|
||||
return true
|
||||
}
|
||||
return r.Len() == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (opt *Option[T]) ValueRevision(ctx context.Context) (v T, rev int, has bool) {
|
||||
dg := GetDynGetter()
|
||||
if dg == nil {
|
||||
// this is an edge case: the database is not initialized but the system setting is going to be used
|
||||
// it should panic to avoid inconsistent config values (from config / system setting) and fix the code
|
||||
panic("no config dyn value getter")
|
||||
}
|
||||
|
||||
rev = dg.GetRevision(ctx)
|
||||
|
||||
// if the revision in the database doesn't change, use the last value
|
||||
opt.mu.RLock()
|
||||
if rev == opt.revision {
|
||||
v = opt.value
|
||||
has = opt.has
|
||||
opt.mu.RUnlock()
|
||||
return v, rev, has
|
||||
}
|
||||
opt.mu.RUnlock()
|
||||
|
||||
// try to parse the config and cache it
|
||||
var valStr *string
|
||||
if dynVal, hasDbValue := dg.GetValue(ctx, opt.dynKey); hasDbValue {
|
||||
valStr = &dynVal
|
||||
} else if cfgVal, hasCfgValue := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); hasCfgValue {
|
||||
valStr = &cfgVal
|
||||
}
|
||||
if valStr == nil {
|
||||
v = opt.DefaultValue()
|
||||
has = false
|
||||
} else {
|
||||
v = opt.parse(opt.dynKey, *valStr)
|
||||
if opt.emptyAsDef && isZeroOrEmpty(v) {
|
||||
v = opt.DefaultValue()
|
||||
} else {
|
||||
has = true
|
||||
}
|
||||
}
|
||||
|
||||
opt.mu.Lock()
|
||||
opt.value = v
|
||||
opt.revision = rev
|
||||
opt.has = has
|
||||
opt.mu.Unlock()
|
||||
return v, rev, has
|
||||
}
|
||||
|
||||
func (opt *Option[T]) DynKey() string {
|
||||
return opt.dynKey
|
||||
}
|
||||
|
||||
// WithDefaultFunc sets the default value with a function
|
||||
// The "def" value might be changed during runtime (e.g.: Unmarshal with default), so it shouldn't use the same pointer or slice
|
||||
func (opt *Option[T]) WithDefaultFunc(f func() T) *Option[T] {
|
||||
opt.defFunc = f
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *Option[T]) WithDefaultSimple(def T) *Option[T] {
|
||||
v := any(def)
|
||||
switch v.(type) {
|
||||
case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
default:
|
||||
// TODO: use reflect to support convertible basic types like `type State string`
|
||||
r := reflect.ValueOf(v)
|
||||
if r.Kind() != reflect.Struct {
|
||||
panic("invalid type for default value, use WithDefaultFunc instead")
|
||||
}
|
||||
}
|
||||
opt.defSimple = def
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *Option[T]) WithEmptyAsDefault() *Option[T] {
|
||||
opt.emptyAsDef = true
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *Option[T]) DefaultValue() T {
|
||||
if opt.defFunc != nil {
|
||||
return opt.defFunc()
|
||||
}
|
||||
return opt.defSimple
|
||||
}
|
||||
|
||||
func (opt *Option[T]) WithFileConfig(cfgSecKey CfgSecKey) *Option[T] {
|
||||
opt.cfgSecKey = cfgSecKey
|
||||
return opt
|
||||
}
|
||||
|
||||
var allConfigOptions = map[string]OptionInterface{}
|
||||
|
||||
func NewOption[T any](dynKey string) *Option[T] {
|
||||
v := &Option[T]{dynKey: dynKey}
|
||||
allConfigOptions[dynKey] = v
|
||||
return v
|
||||
}
|
||||
|
||||
func GetConfigOption(dynKey string) OptionInterface {
|
||||
return allConfigOptions[dynKey]
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvConfigKeyPrefixGitea = "GITEA__"
|
||||
EnvConfigKeySuffixFile = "__FILE"
|
||||
)
|
||||
|
||||
const escapeRegexpString = "_0[xX](([0-9a-fA-F][0-9a-fA-F])+)_"
|
||||
|
||||
var escapeRegex = regexp.MustCompile(escapeRegexpString)
|
||||
|
||||
func CollectEnvConfigKeys() (keys []string) {
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, EnvConfigKeyPrefixGitea) {
|
||||
k, _, _ := strings.Cut(env, "=")
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func ClearEnvConfigKeys() {
|
||||
for _, k := range CollectEnvConfigKeys() {
|
||||
_ = os.Unsetenv(k)
|
||||
}
|
||||
}
|
||||
|
||||
// decodeEnvSectionKey will decode a portable string encoded Section__Key pair
|
||||
// Portable strings are considered to be of the form [A-Z0-9_]*
|
||||
// We will encode a disallowed value as the UTF8 byte string preceded by _0X and
|
||||
// followed by _. E.g. _0X2C_ for a '-' and _0X2E_ for '.'
|
||||
// Section and Key are separated by a plain '__'.
|
||||
// The entire section can be encoded as a UTF8 byte string
|
||||
func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
|
||||
inKey := false
|
||||
last := 0
|
||||
escapeStringIndices := escapeRegex.FindAllStringIndex(encoded, -1)
|
||||
for _, unescapeIdx := range escapeStringIndices {
|
||||
preceding := encoded[last:unescapeIdx[0]]
|
||||
if !inKey {
|
||||
if before, after, cutOk := strings.Cut(preceding, "__"); cutOk {
|
||||
section += before
|
||||
inKey = true
|
||||
key += after
|
||||
} else {
|
||||
section += preceding
|
||||
}
|
||||
} else {
|
||||
key += preceding
|
||||
}
|
||||
toDecode := encoded[unescapeIdx[0]+3 : unescapeIdx[1]-1]
|
||||
decodedBytes := make([]byte, len(toDecode)/2)
|
||||
for i := 0; i < len(toDecode)/2; i++ {
|
||||
// Can ignore error here as we know these should be hexadecimal from the regexp
|
||||
byteInt, _ := strconv.ParseInt(toDecode[2*i:2*i+2], 16, 8)
|
||||
decodedBytes[i] = byte(byteInt)
|
||||
}
|
||||
if inKey {
|
||||
key += string(decodedBytes)
|
||||
} else {
|
||||
section += string(decodedBytes)
|
||||
}
|
||||
last = unescapeIdx[1]
|
||||
}
|
||||
remaining := encoded[last:]
|
||||
if !inKey {
|
||||
if before, after, cutOk := strings.Cut(remaining, "__"); cutOk {
|
||||
section += before
|
||||
key += after
|
||||
} else {
|
||||
section += remaining
|
||||
}
|
||||
} else {
|
||||
key += remaining
|
||||
}
|
||||
section = strings.ToLower(section)
|
||||
ok = key != ""
|
||||
if !ok {
|
||||
section = ""
|
||||
key = ""
|
||||
}
|
||||
return ok, section, key
|
||||
}
|
||||
|
||||
// decodeEnvironmentKey decode the environment key to section and key
|
||||
// The environment key is in the form of GITEA__SECTION__KEY or GITEA__SECTION__KEY__FILE
|
||||
func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) {
|
||||
if !strings.HasPrefix(envKey, prefixGitea) {
|
||||
return false, "", "", false
|
||||
}
|
||||
if strings.HasSuffix(envKey, suffixFile) {
|
||||
useFileValue = true
|
||||
envKey = envKey[:len(envKey)-len(suffixFile)]
|
||||
}
|
||||
ok, section, key = decodeEnvSectionKey(envKey[len(prefixGitea):])
|
||||
return ok, section, key, useFileValue
|
||||
}
|
||||
|
||||
func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
|
||||
for _, kv := range envs {
|
||||
before, after, ok := strings.Cut(kv, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// parse the environment variable to config section name and key name
|
||||
envKey := before
|
||||
envValue := after
|
||||
ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(EnvConfigKeyPrefixGitea, EnvConfigKeySuffixFile, envKey)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// use environment value as config value, or read the file content as value if the key indicates a file
|
||||
keyValue := envValue //nolint:staticcheck // false positive
|
||||
if useFileValue {
|
||||
fileContent, err := os.ReadFile(envValue)
|
||||
if err != nil {
|
||||
log.Error("Error reading file for %s : %v", envKey, envValue, err)
|
||||
continue
|
||||
}
|
||||
if bytes.HasSuffix(fileContent, []byte("\r\n")) {
|
||||
fileContent = fileContent[:len(fileContent)-2]
|
||||
} else if bytes.HasSuffix(fileContent, []byte("\n")) {
|
||||
fileContent = fileContent[:len(fileContent)-1]
|
||||
}
|
||||
keyValue = string(fileContent)
|
||||
}
|
||||
|
||||
// try to set the config value if necessary
|
||||
section, err := cfg.GetSection(sectionName)
|
||||
if err != nil {
|
||||
section, err = cfg.NewSection(sectionName)
|
||||
if err != nil {
|
||||
log.Error("Error creating section: %s : %v", sectionName, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
key := ConfigSectionKey(section, keyName)
|
||||
if key == nil {
|
||||
changed = true
|
||||
key, err = section.NewKey(keyName, keyValue)
|
||||
if err != nil {
|
||||
log.Error("Error creating key: %s in section: %s with value: %s : %v", keyName, sectionName, keyValue, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
oldValue := key.Value()
|
||||
if !changed && oldValue != keyValue {
|
||||
changed = true
|
||||
}
|
||||
key.SetValue(keyValue)
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func UnsetUnnecessaryEnvVars() {
|
||||
// Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want,
|
||||
// but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users.
|
||||
// So at the moment we just keep "unsetting the unnecessary environment variables".
|
||||
|
||||
// HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig".
|
||||
// But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist,
|
||||
// then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set.
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDecodeEnvSectionKey(t *testing.T) {
|
||||
ok, section, key := decodeEnvSectionKey("SEC__KEY")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "sec", section)
|
||||
assert.Equal(t, "KEY", key)
|
||||
|
||||
ok, section, key = decodeEnvSectionKey("sec__key")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "sec", section)
|
||||
assert.Equal(t, "key", key)
|
||||
|
||||
ok, section, key = decodeEnvSectionKey("LOG_0x2E_CONSOLE__STDERR")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "log.console", section)
|
||||
assert.Equal(t, "STDERR", key)
|
||||
|
||||
ok, section, key = decodeEnvSectionKey("SEC")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Empty(t, key)
|
||||
}
|
||||
|
||||
func TestDecodeEnvironmentKey(t *testing.T) {
|
||||
prefix := "GITEA__"
|
||||
suffix := "__FILE"
|
||||
|
||||
ok, section, key, file := decodeEnvironmentKey(prefix, suffix, "SEC__KEY")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Empty(t, key)
|
||||
assert.False(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Empty(t, key)
|
||||
assert.False(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA____KEY")
|
||||
assert.True(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Equal(t, "KEY", key)
|
||||
assert.False(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC__KEY")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "sec", section)
|
||||
assert.Equal(t, "KEY", key)
|
||||
assert.False(t, file)
|
||||
|
||||
// with "__FILE" suffix, it doesn't support to write "[sec].FILE" to config (no such key FILE is used in Gitea)
|
||||
// but it could be fixed in the future by adding a new suffix like "__VALUE" (no such key VALUE is used in Gitea either)
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC__FILE")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Empty(t, key)
|
||||
assert.True(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC__KEY__FILE")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "sec", section)
|
||||
assert.Equal(t, "KEY", key)
|
||||
assert.True(t, file)
|
||||
|
||||
ok, _, _, _ = decodeEnvironmentKey("PREFIX__", "", "PREFIX__SEC__KEY")
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestEnvironmentToConfig(t *testing.T) {
|
||||
cfg, _ := NewConfigProviderFromData("")
|
||||
|
||||
changed := EnvironmentToConfig(cfg, nil)
|
||||
assert.False(t, changed)
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[sec]
|
||||
key = old
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
changed = EnvironmentToConfig(cfg, []string{"GITEA__sec__key=new"})
|
||||
assert.True(t, changed)
|
||||
assert.Equal(t, "new", cfg.Section("sec").Key("key").String())
|
||||
|
||||
changed = EnvironmentToConfig(cfg, []string{"GITEA__sec__key=new"})
|
||||
assert.False(t, changed)
|
||||
|
||||
tmpFile := t.TempDir() + "/the-file"
|
||||
_ = os.WriteFile(tmpFile, []byte("value-from-file"), 0o644)
|
||||
changed = EnvironmentToConfig(cfg, []string{"GITEA__sec__key__FILE=" + tmpFile})
|
||||
assert.True(t, changed)
|
||||
assert.Equal(t, "value-from-file", cfg.Section("sec").Key("key").String())
|
||||
|
||||
cfg, _ = NewConfigProviderFromData("")
|
||||
_ = os.WriteFile(tmpFile, []byte("value-from-file\n"), 0o644)
|
||||
EnvironmentToConfig(cfg, []string{"GITEA__sec__key__FILE=" + tmpFile})
|
||||
assert.Equal(t, "value-from-file", cfg.Section("sec").Key("key").String())
|
||||
|
||||
cfg, _ = NewConfigProviderFromData("")
|
||||
_ = os.WriteFile(tmpFile, []byte("value-from-file\r\n"), 0o644)
|
||||
EnvironmentToConfig(cfg, []string{"GITEA__sec__key__FILE=" + tmpFile})
|
||||
assert.Equal(t, "value-from-file", cfg.Section("sec").Key("key").String())
|
||||
|
||||
cfg, _ = NewConfigProviderFromData("")
|
||||
_ = os.WriteFile(tmpFile, []byte("value-from-file\n\n"), 0o644)
|
||||
EnvironmentToConfig(cfg, []string{"GITEA__sec__key__FILE=" + tmpFile})
|
||||
assert.Equal(t, "value-from-file\n", cfg.Section("sec").Key("key").String())
|
||||
}
|
||||
|
||||
func TestEnvironmentToConfigSubSecKey(t *testing.T) {
|
||||
// the INI package has a quirk: by default, the keys are inherited.
|
||||
// when maintaining the keys, the newly added sub key should not be affected by the parent key.
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[sec]
|
||||
key = some
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
changed := EnvironmentToConfig(cfg, []string{"GITEA__sec_0X2E_sub__key=some"})
|
||||
assert.True(t, changed)
|
||||
|
||||
tmpFile := t.TempDir() + "/test-sub-sec-key.ini"
|
||||
defer os.Remove(tmpFile)
|
||||
err = cfg.SaveTo(tmpFile)
|
||||
assert.NoError(t, err)
|
||||
bs, err := os.ReadFile(tmpFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `[sec]
|
||||
key = some
|
||||
|
||||
[sec.sub]
|
||||
key = some
|
||||
`, string(bs))
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/setting/config"
|
||||
)
|
||||
|
||||
// WebBannerType fields are directly used in templates,
|
||||
// do remember to update the template if you change the fields
|
||||
type WebBannerType struct {
|
||||
DisplayEnabled bool
|
||||
ContentMessage string
|
||||
StartTimeUnix int64
|
||||
EndTimeUnix int64
|
||||
}
|
||||
|
||||
func (b WebBannerType) ShouldDisplay() bool {
|
||||
if !b.DisplayEnabled || b.ContentMessage == "" {
|
||||
return false
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
if b.StartTimeUnix > 0 && now < b.StartTimeUnix {
|
||||
return false
|
||||
}
|
||||
if b.EndTimeUnix > 0 && now > b.EndTimeUnix {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type MaintenanceModeType struct {
|
||||
AdminWebAccessOnly bool
|
||||
StartTimeUnix int64
|
||||
EndTimeUnix int64
|
||||
}
|
||||
|
||||
func (m MaintenanceModeType) IsActive() bool {
|
||||
if !m.AdminWebAccessOnly {
|
||||
return false
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
if m.StartTimeUnix > 0 && now < m.StartTimeUnix {
|
||||
return false
|
||||
}
|
||||
if m.EndTimeUnix > 0 && now > m.EndTimeUnix {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type InstanceStruct struct {
|
||||
WebBanner *config.Option[WebBannerType]
|
||||
MaintenanceMode *config.Option[MaintenanceModeType]
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"gopkg.in/ini.v1" //nolint:depguard // wrapper for this package
|
||||
)
|
||||
|
||||
type ConfigKey interface {
|
||||
Name() string
|
||||
Value() string
|
||||
SetValue(v string)
|
||||
|
||||
In(defaultVal string, candidates []string) string
|
||||
String() string
|
||||
Strings(delim string) []string
|
||||
Bool() (bool, error)
|
||||
|
||||
MustString(defaultVal string) string
|
||||
MustBool(defaultVal ...bool) bool
|
||||
MustInt(defaultVal ...int) int
|
||||
MustInt64(defaultVal ...int64) int64
|
||||
MustDuration(defaultVal ...time.Duration) time.Duration
|
||||
}
|
||||
|
||||
type ConfigSection interface {
|
||||
Name() string
|
||||
MapTo(any) error
|
||||
HasKey(key string) bool
|
||||
NewKey(name, value string) (ConfigKey, error)
|
||||
Key(key string) ConfigKey
|
||||
DeleteKey(key string)
|
||||
Keys() []ConfigKey
|
||||
ChildSections() []ConfigSection
|
||||
}
|
||||
|
||||
// ConfigProvider represents a config provider
|
||||
type ConfigProvider interface {
|
||||
Section(section string) ConfigSection
|
||||
Sections() []ConfigSection
|
||||
NewSection(name string) (ConfigSection, error)
|
||||
GetSection(name string) (ConfigSection, error)
|
||||
DeleteSection(name string)
|
||||
Save() error
|
||||
SaveTo(filename string) error
|
||||
|
||||
DisableSaving()
|
||||
PrepareSaving() (ConfigProvider, error)
|
||||
IsLoadedFromEmpty() bool
|
||||
}
|
||||
|
||||
type iniConfigProvider struct {
|
||||
file string
|
||||
ini *ini.File
|
||||
|
||||
disableSaving bool // disable the "Save" method because the config options could be polluted
|
||||
loadedFromEmpty bool // whether the file has not existed previously
|
||||
}
|
||||
|
||||
type iniConfigSection struct {
|
||||
sec *ini.Section
|
||||
}
|
||||
|
||||
var (
|
||||
_ ConfigProvider = (*iniConfigProvider)(nil)
|
||||
_ ConfigSection = (*iniConfigSection)(nil)
|
||||
_ ConfigKey = (*ini.Key)(nil)
|
||||
)
|
||||
|
||||
// ConfigSectionKey only searches the keys in the given section, but it is O(n).
|
||||
// ini package has a special behavior: with "[sec] a=1" and an empty "[sec.sub]",
|
||||
// then in "[sec.sub]", Key()/HasKey() can always see "a=1" because it always tries parent sections.
|
||||
// It returns nil if the key doesn't exist.
|
||||
func ConfigSectionKey(sec ConfigSection, key string) ConfigKey {
|
||||
if sec == nil {
|
||||
return nil
|
||||
}
|
||||
for _, k := range sec.Keys() {
|
||||
if k.Name() == key {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ConfigSectionKeyString(sec ConfigSection, key string, def ...string) string {
|
||||
k := ConfigSectionKey(sec, key)
|
||||
if k != nil && k.String() != "" {
|
||||
return k.String()
|
||||
}
|
||||
if len(def) > 0 {
|
||||
return def[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ConfigSectionKeyBool(sec ConfigSection, key string, def ...bool) bool {
|
||||
k := ConfigSectionKey(sec, key)
|
||||
if k != nil && k.String() != "" {
|
||||
b, _ := strconv.ParseBool(k.String())
|
||||
return b
|
||||
}
|
||||
if len(def) > 0 {
|
||||
return def[0]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ConfigInheritedKey works like ini.Section.Key(), but it always returns a new key instance, it is O(n) because NewKey is O(n)
|
||||
// and the returned key is safe to be used with "MustXxx", it doesn't change the parent's values.
|
||||
// Otherwise, ini.Section.Key().MustXxx would pollute the parent section's keys.
|
||||
// It never returns nil.
|
||||
func ConfigInheritedKey(sec ConfigSection, key string) ConfigKey {
|
||||
k := sec.Key(key)
|
||||
if k != nil && k.String() != "" {
|
||||
newKey, _ := sec.NewKey(k.Name(), k.String())
|
||||
return newKey
|
||||
}
|
||||
newKey, _ := sec.NewKey(key, "")
|
||||
return newKey
|
||||
}
|
||||
|
||||
func ConfigInheritedKeyString(sec ConfigSection, key string, def ...string) string {
|
||||
k := sec.Key(key)
|
||||
if k != nil && k.String() != "" {
|
||||
return k.String()
|
||||
}
|
||||
if len(def) > 0 {
|
||||
return def[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) Name() string {
|
||||
return s.sec.Name()
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) MapTo(v any) error {
|
||||
return s.sec.MapTo(v)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) HasKey(key string) bool {
|
||||
return s.sec.HasKey(key)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) NewKey(name, value string) (ConfigKey, error) {
|
||||
return s.sec.NewKey(name, value)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) Key(key string) ConfigKey {
|
||||
return s.sec.Key(key)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) Keys() (keys []ConfigKey) {
|
||||
for _, k := range s.sec.Keys() {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) DeleteKey(key string) {
|
||||
s.sec.DeleteKey(key)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
|
||||
for _, s := range s.sec.ChildSections() {
|
||||
sections = append(sections, &iniConfigSection{s})
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
func configProviderLoadOptions() ini.LoadOptions {
|
||||
return ini.LoadOptions{
|
||||
KeyValueDelimiterOnWrite: " = ",
|
||||
IgnoreContinuation: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewConfigProviderFromData this function is mainly for testing purpose
|
||||
func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
|
||||
cfg, err := ini.LoadSources(configProviderLoadOptions(), strings.NewReader(configContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.NameMapper = ini.SnackCase
|
||||
return &iniConfigProvider{
|
||||
ini: cfg,
|
||||
loadedFromEmpty: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewConfigProviderFromFile load configuration from file.
|
||||
// NOTE: do not print any log except error.
|
||||
func NewConfigProviderFromFile(file string) (ConfigProvider, error) {
|
||||
cfg := ini.Empty(configProviderLoadOptions())
|
||||
loadedFromEmpty := true
|
||||
|
||||
if file != "" {
|
||||
isExist, err := util.IsExist(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to check if %q exists: %v", file, err)
|
||||
}
|
||||
if isExist {
|
||||
if err = cfg.Append(file); err != nil {
|
||||
return nil, fmt.Errorf("failed to load config file %q: %v", file, err)
|
||||
}
|
||||
loadedFromEmpty = false
|
||||
}
|
||||
}
|
||||
|
||||
cfg.NameMapper = ini.SnackCase
|
||||
return &iniConfigProvider{
|
||||
file: file,
|
||||
ini: cfg,
|
||||
loadedFromEmpty: loadedFromEmpty,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *iniConfigProvider) Section(section string) ConfigSection {
|
||||
return &iniConfigSection{sec: p.ini.Section(section)}
|
||||
}
|
||||
|
||||
func (p *iniConfigProvider) Sections() (sections []ConfigSection) {
|
||||
for _, s := range p.ini.Sections() {
|
||||
sections = append(sections, &iniConfigSection{s})
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
func (p *iniConfigProvider) NewSection(name string) (ConfigSection, error) {
|
||||
sec, err := p.ini.NewSection(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iniConfigSection{sec: sec}, nil
|
||||
}
|
||||
|
||||
func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) {
|
||||
sec, err := p.ini.GetSection(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iniConfigSection{sec: sec}, nil
|
||||
}
|
||||
|
||||
func (p *iniConfigProvider) DeleteSection(name string) {
|
||||
p.ini.DeleteSection(name)
|
||||
}
|
||||
|
||||
var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save")
|
||||
|
||||
// Save saves the content into file
|
||||
func (p *iniConfigProvider) Save() error {
|
||||
if p.disableSaving {
|
||||
return errDisableSaving
|
||||
}
|
||||
filename := p.file
|
||||
if filename == "" {
|
||||
return errors.New("config file path must not be empty")
|
||||
}
|
||||
if p.loadedFromEmpty {
|
||||
if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to create %q: %v", filename, err)
|
||||
}
|
||||
}
|
||||
if err := p.ini.SaveTo(filename); err != nil {
|
||||
return fmt.Errorf("failed to save %q: %v", filename, err)
|
||||
}
|
||||
|
||||
// Change permissions to be more restrictive
|
||||
fi, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine current conf file permissions: %v", err)
|
||||
}
|
||||
|
||||
if fi.Mode().Perm() > 0o600 {
|
||||
if err = os.Chmod(filename, 0o600); err != nil {
|
||||
log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *iniConfigProvider) SaveTo(filename string) error {
|
||||
if p.disableSaving {
|
||||
return errDisableSaving
|
||||
}
|
||||
return p.ini.SaveTo(filename)
|
||||
}
|
||||
|
||||
// DisableSaving disables the saving function, use PrepareSaving to get clear config options.
|
||||
func (p *iniConfigProvider) DisableSaving() {
|
||||
p.disableSaving = true
|
||||
}
|
||||
|
||||
// PrepareSaving loads the ini from file again to get clear config options.
|
||||
// Otherwise, the "MustXxx" calls would have polluted the current config provider,
|
||||
// it makes the "Save" outputs a lot of garbage options
|
||||
// After the INI package gets refactored, no "MustXxx" pollution, this workaround can be dropped.
|
||||
func (p *iniConfigProvider) PrepareSaving() (ConfigProvider, error) {
|
||||
if p.file == "" {
|
||||
return nil, errors.New("no config file to save")
|
||||
}
|
||||
return NewConfigProviderFromFile(p.file)
|
||||
}
|
||||
|
||||
func (p *iniConfigProvider) IsLoadedFromEmpty() bool {
|
||||
return p.loadedFromEmpty
|
||||
}
|
||||
|
||||
func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
|
||||
if err := rootCfg.Section(sectionName).MapTo(setting); err != nil {
|
||||
log.Fatal("Failed to map %s settings: %v", sectionName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// StartupProblems contains the messages for various startup problems, including: setting option, file/folder, etc
|
||||
var StartupProblems []string
|
||||
|
||||
func LogStartupProblem(skip int, level log.Level, format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
log.Log(skip+1, level, "%s", msg)
|
||||
StartupProblems = append(StartupProblems, msg)
|
||||
}
|
||||
|
||||
func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
|
||||
if rootCfg.Section(oldSection).HasKey(oldKey) {
|
||||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
|
||||
}
|
||||
}
|
||||
|
||||
// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
|
||||
func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
|
||||
if rootCfg.Section(oldSection).HasKey(oldKey) {
|
||||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
ini.PrettyFormat = false
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfigProviderBehaviors(t *testing.T) {
|
||||
t.Run("BuggyKeyOverwritten", func(t *testing.T) {
|
||||
cfg, _ := NewConfigProviderFromData(`
|
||||
[foo]
|
||||
key =
|
||||
`)
|
||||
sec := cfg.Section("foo")
|
||||
secSub := cfg.Section("foo.bar")
|
||||
secSub.Key("key").MustString("1") // try to read a key from subsection
|
||||
assert.Equal(t, "1", sec.Key("key").String()) // TODO: BUGGY! the key in [foo] is overwritten
|
||||
})
|
||||
|
||||
t.Run("SubsectionSeeParentKeys", func(t *testing.T) {
|
||||
cfg, _ := NewConfigProviderFromData(`
|
||||
[foo]
|
||||
key = 123
|
||||
`)
|
||||
secSub := cfg.Section("foo.bar.xxx")
|
||||
assert.Equal(t, "123", secSub.Key("key").String())
|
||||
})
|
||||
t.Run("TrailingSlash", func(t *testing.T) {
|
||||
cfg, _ := NewConfigProviderFromData(`
|
||||
[foo]
|
||||
key = E:\
|
||||
xxx = yyy
|
||||
`)
|
||||
sec := cfg.Section("foo")
|
||||
assert.Equal(t, "E:\\", sec.Key("key").String())
|
||||
assert.Equal(t, "yyy", sec.Key("xxx").String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigProviderHelper(t *testing.T) {
|
||||
cfg, _ := NewConfigProviderFromData(`
|
||||
[foo]
|
||||
empty =
|
||||
key = 123
|
||||
`)
|
||||
|
||||
sec := cfg.Section("foo")
|
||||
secSub := cfg.Section("foo.bar")
|
||||
|
||||
// test empty key
|
||||
assert.Equal(t, "def", ConfigSectionKeyString(sec, "empty", "def"))
|
||||
assert.Equal(t, "xyz", ConfigSectionKeyString(secSub, "empty", "xyz"))
|
||||
|
||||
// test non-inherited key, only see the keys in current section
|
||||
assert.NotNil(t, ConfigSectionKey(sec, "key"))
|
||||
assert.Nil(t, ConfigSectionKey(secSub, "key"))
|
||||
|
||||
// test default behavior
|
||||
assert.Equal(t, "123", ConfigSectionKeyString(sec, "key"))
|
||||
assert.Empty(t, ConfigSectionKeyString(secSub, "key"))
|
||||
assert.Equal(t, "def", ConfigSectionKeyString(secSub, "key", "def"))
|
||||
|
||||
assert.Equal(t, "123", ConfigInheritedKeyString(secSub, "key"))
|
||||
|
||||
// Workaround for ini package's BuggyKeyOverwritten behavior
|
||||
assert.Empty(t, ConfigSectionKeyString(sec, "empty"))
|
||||
assert.Empty(t, ConfigSectionKeyString(secSub, "empty"))
|
||||
assert.Equal(t, "def", ConfigInheritedKey(secSub, "empty").MustString("def"))
|
||||
assert.Equal(t, "def", ConfigInheritedKey(secSub, "empty").MustString("xyz"))
|
||||
assert.Empty(t, ConfigSectionKeyString(sec, "empty"))
|
||||
assert.Equal(t, "def", ConfigSectionKeyString(secSub, "empty"))
|
||||
}
|
||||
|
||||
func TestNewConfigProviderFromFile(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromFile("no-such.ini")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, cfg.IsLoadedFromEmpty())
|
||||
|
||||
// load non-existing file and save
|
||||
testFile := t.TempDir() + "/test.ini"
|
||||
testFile1 := t.TempDir() + "/test1.ini"
|
||||
cfg, err = NewConfigProviderFromFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
sec, _ := cfg.NewSection("foo")
|
||||
_, _ = sec.NewKey("k1", "a")
|
||||
assert.NoError(t, cfg.Save())
|
||||
_, _ = sec.NewKey("k2", "b")
|
||||
assert.NoError(t, cfg.SaveTo(testFile1))
|
||||
|
||||
bs, err := os.ReadFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[foo]\nk1 = a\n", string(bs))
|
||||
|
||||
bs, err = os.ReadFile(testFile1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[foo]\nk1 = a\nk2 = b\n", string(bs))
|
||||
|
||||
// load existing file and save
|
||||
cfg, err = NewConfigProviderFromFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "a", cfg.Section("foo").Key("k1").String())
|
||||
sec, _ = cfg.NewSection("bar")
|
||||
_, _ = sec.NewKey("k1", "b")
|
||||
assert.NoError(t, cfg.Save())
|
||||
bs, err = os.ReadFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[foo]\nk1 = a\n\n[bar]\nk1 = b\n", string(bs))
|
||||
}
|
||||
|
||||
func TestDisableSaving(t *testing.T) {
|
||||
testFile := t.TempDir() + "/test.ini"
|
||||
_ = os.WriteFile(testFile, []byte("k1=a\nk2=b"), 0o644)
|
||||
cfg, err := NewConfigProviderFromFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cfg.DisableSaving()
|
||||
err = cfg.Save()
|
||||
assert.ErrorIs(t, err, errDisableSaving)
|
||||
|
||||
saveCfg, err := cfg.PrepareSaving()
|
||||
assert.NoError(t, err)
|
||||
|
||||
saveCfg.Section("").Key("k1").MustString("x")
|
||||
saveCfg.Section("").Key("k2").SetValue("y")
|
||||
saveCfg.Section("").Key("k3").SetValue("z")
|
||||
err = saveCfg.Save()
|
||||
assert.NoError(t, err)
|
||||
|
||||
bs, err := os.ReadFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "k1 = a\nk2 = y\nk3 = z\n", string(bs))
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// CORSConfig defines CORS settings
|
||||
var CORSConfig = struct {
|
||||
Enabled bool
|
||||
AllowDomain []string // FIXME: this option is from legacy code, it actually works as "AllowedOrigins". When refactoring in the future, the config option should also be renamed together.
|
||||
Methods []string
|
||||
MaxAge time.Duration
|
||||
AllowCredentials bool
|
||||
Headers []string
|
||||
}{
|
||||
AllowDomain: []string{"*"},
|
||||
Methods: []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
Headers: []string{"Content-Type", "User-Agent"},
|
||||
MaxAge: 10 * time.Minute,
|
||||
}
|
||||
|
||||
func loadCorsFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "cors", &CORSConfig)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import "reflect"
|
||||
|
||||
// GetCronSettings maps the cron subsection to the provided config
|
||||
func GetCronSettings(name string, config any) (any, error) {
|
||||
return getCronSettings(CfgProvider, name, config)
|
||||
}
|
||||
|
||||
func getCronSettings(rootCfg ConfigProvider, name string, config any) (any, error) {
|
||||
if err := rootCfg.Section("cron." + name).MapTo(config); err != nil {
|
||||
return config, err
|
||||
}
|
||||
|
||||
typ := reflect.TypeOf(config).Elem()
|
||||
val := reflect.ValueOf(config).Elem()
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
tpField := typ.Field(i)
|
||||
if tpField.Type.Kind() == reflect.Struct && tpField.Anonymous {
|
||||
if err := rootCfg.Section("cron." + name).MapTo(field.Addr().Interface()); err != nil {
|
||||
return config, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getCronSettings(t *testing.T) {
|
||||
type BaseStruct struct {
|
||||
Base bool
|
||||
Second string
|
||||
}
|
||||
|
||||
type Extended struct {
|
||||
BaseStruct
|
||||
Extend bool
|
||||
}
|
||||
|
||||
iniStr := `
|
||||
[cron.test]
|
||||
BASE = true
|
||||
SECOND = white rabbit
|
||||
EXTEND = true
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
extended := &Extended{
|
||||
BaseStruct: BaseStruct{
|
||||
Second: "queen of hearts",
|
||||
},
|
||||
}
|
||||
|
||||
_, err = getCronSettings(cfg, "test", extended)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, extended.Base)
|
||||
assert.Equal(t, "white rabbit", extended.Second)
|
||||
assert.True(t, extended.Extend)
|
||||
}
|
||||
|
||||
// Test_getCronSettings2 tests that getCronSettings can not handle two levels of embedding
|
||||
func Test_getCronSettings2(t *testing.T) {
|
||||
type BaseStruct struct {
|
||||
Enabled bool
|
||||
RunAtStart bool
|
||||
Schedule string
|
||||
}
|
||||
|
||||
type Extended struct {
|
||||
BaseStruct
|
||||
Extend bool
|
||||
}
|
||||
type Extended2 struct {
|
||||
Extended
|
||||
Third string
|
||||
}
|
||||
|
||||
iniStr := `
|
||||
[cron.test]
|
||||
ENABLED = TRUE
|
||||
RUN_AT_START = TRUE
|
||||
SCHEDULE = @every 1h
|
||||
EXTEND = true
|
||||
THIRD = white rabbit
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
extended := &Extended2{
|
||||
Extended: Extended{
|
||||
BaseStruct: BaseStruct{
|
||||
Enabled: false,
|
||||
RunAtStart: false,
|
||||
Schedule: "@every 72h",
|
||||
},
|
||||
Extend: false,
|
||||
},
|
||||
Third: "black rabbit",
|
||||
}
|
||||
|
||||
_, err = getCronSettings(cfg, "test", extended)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// This confirms the first level of embedding works
|
||||
assert.Equal(t, "white rabbit", extended.Third)
|
||||
assert.True(t, extended.Extend)
|
||||
|
||||
// This confirms 2 levels of embedding doesn't work
|
||||
assert.False(t, extended.Enabled)
|
||||
assert.False(t, extended.RunAtStart)
|
||||
assert.Equal(t, "@every 72h", extended.Schedule)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultSQLiteBusyTimeout = 20 * 1000
|
||||
|
||||
var (
|
||||
// SupportedDatabaseTypes includes all XORM supported databases type, sqlite3 maybe added by the tag-controlled drivers
|
||||
SupportedDatabaseTypes = []string{"mysql", "postgres", "mssql"}
|
||||
// DatabaseTypeNames contains the friendly names for all database types
|
||||
DatabaseTypeNames = map[string]string{"mysql": "MySQL", "postgres": "PostgreSQL", "mssql": "MSSQL", DatabaseTypeSQLite3: "SQLite3"}
|
||||
|
||||
// Database holds the database settings
|
||||
Database = struct {
|
||||
Type DatabaseType
|
||||
Host string
|
||||
Name string
|
||||
User string
|
||||
Passwd string
|
||||
Schema string
|
||||
SSLMode string
|
||||
Path string
|
||||
|
||||
SQLiteBusyTimeout int
|
||||
SQLiteJournalMode string
|
||||
|
||||
LogSQL bool
|
||||
CharsetCollation string
|
||||
DBConnectRetries int
|
||||
DBConnectBackoff time.Duration
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
IterateBufferSize int
|
||||
AutoMigration bool
|
||||
SlowQueryThreshold time.Duration
|
||||
}{
|
||||
IterateBufferSize: 50,
|
||||
SlowQueryThreshold: 5 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
// LoadDBSetting loads the database settings
|
||||
func LoadDBSetting() {
|
||||
loadDBSetting(CfgProvider)
|
||||
}
|
||||
|
||||
func loadDBSetting(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("database")
|
||||
Database.Type = DatabaseType(sec.Key("DB_TYPE").String())
|
||||
|
||||
Database.Host = sec.Key("HOST").String()
|
||||
Database.Name = sec.Key("NAME").String()
|
||||
Database.User = sec.Key("USER").String()
|
||||
Database.Passwd = sec.Key("PASSWD").String()
|
||||
|
||||
Database.Schema = sec.Key("SCHEMA").String()
|
||||
Database.SSLMode = sec.Key("SSL_MODE").MustString("disable")
|
||||
Database.CharsetCollation = sec.Key("CHARSET_COLLATION").String()
|
||||
|
||||
Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db"))
|
||||
|
||||
Database.SQLiteBusyTimeout = sec.Key("SQLITE_TIMEOUT").MustInt(DefaultSQLiteBusyTimeout)
|
||||
// mattn driver isn't really affected by this timeout, but other drivers are affected
|
||||
// the default value was 500 (0.5s), to avoid breaking existing users, make sure the timeout is long enough (at least, 5 seconds)
|
||||
if Database.SQLiteBusyTimeout < 5000 {
|
||||
Database.SQLiteBusyTimeout = DefaultSQLiteBusyTimeout
|
||||
}
|
||||
Database.SQLiteJournalMode = sec.Key("SQLITE_JOURNAL_MODE").MustString("")
|
||||
|
||||
Database.MaxIdleConns = sec.Key("MAX_IDLE_CONNS").MustInt(2)
|
||||
if Database.Type.IsMySQL() {
|
||||
Database.ConnMaxLifetime = sec.Key("CONN_MAX_LIFETIME").MustDuration(3 * time.Second)
|
||||
} else {
|
||||
Database.ConnMaxLifetime = sec.Key("CONN_MAX_LIFETIME").MustDuration(0)
|
||||
}
|
||||
Database.MaxOpenConns = sec.Key("MAX_OPEN_CONNS").MustInt(0)
|
||||
|
||||
Database.IterateBufferSize = sec.Key("ITERATE_BUFFER_SIZE").MustInt(50)
|
||||
Database.LogSQL = sec.Key("LOG_SQL").MustBool(false)
|
||||
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
||||
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
||||
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
||||
Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(Database.SlowQueryThreshold)
|
||||
}
|
||||
|
||||
// DatabaseType FIXME: it is also used directly with "schemas.DBType", so the names must be consistent
|
||||
type DatabaseType string
|
||||
|
||||
const DatabaseTypeSQLite3 = "sqlite3"
|
||||
|
||||
func (t DatabaseType) IsSQLite3() bool {
|
||||
return t == DatabaseTypeSQLite3
|
||||
}
|
||||
|
||||
func (t DatabaseType) IsMySQL() bool {
|
||||
return t == "mysql"
|
||||
}
|
||||
|
||||
func (t DatabaseType) IsMSSQL() bool {
|
||||
return t == "mssql"
|
||||
}
|
||||
|
||||
func (t DatabaseType) IsPostgreSQL() bool {
|
||||
return t == "postgres"
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"gitea.dev/modules/log"
|
||||
|
||||
"github.com/42wim/httpsig"
|
||||
)
|
||||
|
||||
// Federation settings
|
||||
var (
|
||||
Federation = struct {
|
||||
Enabled bool
|
||||
ShareUserStatistics bool
|
||||
MaxSize int64
|
||||
Algorithms []string
|
||||
DigestAlgorithm string
|
||||
GetHeaders []string
|
||||
PostHeaders []string
|
||||
}{
|
||||
Enabled: false,
|
||||
ShareUserStatistics: true,
|
||||
MaxSize: 4,
|
||||
Algorithms: []string{"rsa-sha256", "rsa-sha512", "ed25519"},
|
||||
DigestAlgorithm: "SHA-256",
|
||||
GetHeaders: []string{"(request-target)", "Date"},
|
||||
PostHeaders: []string{"(request-target)", "Date", "Digest"},
|
||||
}
|
||||
)
|
||||
|
||||
// HttpsigAlgs is a constant slice of httpsig algorithm objects
|
||||
var HttpsigAlgs []httpsig.Algorithm
|
||||
|
||||
func loadFederationFrom(rootCfg ConfigProvider) {
|
||||
if err := rootCfg.Section("federation").MapTo(&Federation); err != nil {
|
||||
log.Fatal("Failed to map Federation settings: %v", err)
|
||||
} else if !httpsig.IsSupportedDigestAlgorithm(Federation.DigestAlgorithm) {
|
||||
log.Fatal("unsupported digest algorithm: %s", Federation.DigestAlgorithm)
|
||||
return
|
||||
}
|
||||
|
||||
// Get MaxSize in bytes instead of MiB
|
||||
Federation.MaxSize = 1 << 20 * Federation.MaxSize
|
||||
|
||||
HttpsigAlgs = make([]httpsig.Algorithm, len(Federation.Algorithms))
|
||||
for i, alg := range Federation.Algorithms {
|
||||
HttpsigAlgs[i] = httpsig.Algorithm(alg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// Git settings
|
||||
var Git = struct {
|
||||
Path string
|
||||
HomePath string
|
||||
DisableDiffHighlight bool
|
||||
|
||||
MaxGitDiffLines int
|
||||
MaxGitDiffLineCharacters int
|
||||
MaxGitDiffFiles int
|
||||
CommitsRangeSize int // CommitsRangeSize the default commits range size
|
||||
BranchesRangeSize int // BranchesRangeSize the default branches range size
|
||||
VerbosePush bool
|
||||
VerbosePushDelay time.Duration
|
||||
GCArgs []string `ini:"GC_ARGS" delim:" "`
|
||||
EnableAutoGitWireProtocol bool
|
||||
PullRequestPushMessage bool
|
||||
LargeObjectThreshold int64
|
||||
DisableCoreProtectNTFS bool
|
||||
DisablePartialClone bool
|
||||
DiffRenameSimilarityThreshold string
|
||||
Timeout struct {
|
||||
Migrate int
|
||||
Mirror int
|
||||
GC int `ini:"GC"`
|
||||
} `ini:"git.timeout"`
|
||||
}{
|
||||
DisableDiffHighlight: false,
|
||||
MaxGitDiffLines: 1000,
|
||||
MaxGitDiffLineCharacters: 5000,
|
||||
MaxGitDiffFiles: 100,
|
||||
CommitsRangeSize: 50,
|
||||
BranchesRangeSize: 20,
|
||||
VerbosePush: true,
|
||||
VerbosePushDelay: 5 * time.Second,
|
||||
GCArgs: []string{},
|
||||
EnableAutoGitWireProtocol: true,
|
||||
PullRequestPushMessage: true,
|
||||
LargeObjectThreshold: 1024 * 1024,
|
||||
DisablePartialClone: false,
|
||||
DiffRenameSimilarityThreshold: "50%",
|
||||
Timeout: struct {
|
||||
Migrate int
|
||||
Mirror int
|
||||
GC int `ini:"GC"`
|
||||
}{
|
||||
Migrate: 600,
|
||||
Mirror: 300,
|
||||
GC: 60,
|
||||
},
|
||||
}
|
||||
|
||||
type GitConfigType struct {
|
||||
Options map[string]string // git config key is case-insensitive, always use lower-case
|
||||
}
|
||||
|
||||
func (c *GitConfigType) SetOption(key, val string) {
|
||||
c.Options[strings.ToLower(key)] = val
|
||||
}
|
||||
|
||||
func (c *GitConfigType) GetOption(key string) string {
|
||||
return c.Options[strings.ToLower(key)]
|
||||
}
|
||||
|
||||
var GitConfig = GitConfigType{
|
||||
Options: make(map[string]string),
|
||||
}
|
||||
|
||||
func loadGitFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("git")
|
||||
if err := sec.MapTo(&Git); err != nil {
|
||||
log.Fatal("Failed to map Git settings: %v", err)
|
||||
}
|
||||
|
||||
secGitConfig := rootCfg.Section("git.config")
|
||||
GitConfig.Options = make(map[string]string)
|
||||
GitConfig.SetOption("diff.algorithm", "histogram")
|
||||
GitConfig.SetOption("core.logAllRefUpdates", "true")
|
||||
GitConfig.SetOption("gc.reflogExpire", "90")
|
||||
|
||||
secGitReflog := rootCfg.Section("git.reflog")
|
||||
if secGitReflog.HasKey("ENABLED") {
|
||||
deprecatedSetting(rootCfg, "git.reflog", "ENABLED", "git.config", "core.logAllRefUpdates", "1.21")
|
||||
GitConfig.SetOption("core.logAllRefUpdates", secGitReflog.Key("ENABLED").In("true", []string{"true", "false"}))
|
||||
}
|
||||
if secGitReflog.HasKey("EXPIRATION") {
|
||||
deprecatedSetting(rootCfg, "git.reflog", "EXPIRATION", "git.config", "core.reflogExpire", "1.21")
|
||||
GitConfig.SetOption("gc.reflogExpire", secGitReflog.Key("EXPIRATION").String())
|
||||
}
|
||||
|
||||
for _, key := range secGitConfig.Keys() {
|
||||
GitConfig.SetOption(key.Name(), key.String())
|
||||
}
|
||||
|
||||
Git.HomePath = sec.Key("HOME_PATH").MustString("home")
|
||||
if !filepath.IsAbs(Git.HomePath) {
|
||||
Git.HomePath = filepath.Join(AppDataPath, Git.HomePath)
|
||||
} else {
|
||||
Git.HomePath = filepath.Clean(Git.HomePath)
|
||||
}
|
||||
|
||||
// validate for a integer percentage between 0% and 100%
|
||||
if !regexp.MustCompile(`^([0-9]|[1-9][0-9]|100)%$`).MatchString(Git.DiffRenameSimilarityThreshold) {
|
||||
log.Fatal("Invalid git.DIFF_RENAME_SIMILARITY_THRESHOLD: %s", Git.DiffRenameSimilarityThreshold)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGitConfig(t *testing.T) {
|
||||
oldGit := Git
|
||||
oldGitConfig := GitConfig
|
||||
defer func() {
|
||||
Git = oldGit
|
||||
GitConfig = oldGitConfig
|
||||
}()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[git.config]
|
||||
a.b = 1
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadGitFrom(cfg)
|
||||
assert.Equal(t, "1", GitConfig.Options["a.b"])
|
||||
assert.Equal(t, "histogram", GitConfig.Options["diff.algorithm"])
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[git.config]
|
||||
diff.algorithm = other
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadGitFrom(cfg)
|
||||
assert.Equal(t, "other", GitConfig.Options["diff.algorithm"])
|
||||
}
|
||||
|
||||
func TestGitReflog(t *testing.T) {
|
||||
defer test.MockVariableValue(&Git)
|
||||
defer test.MockVariableValue(&GitConfig)
|
||||
|
||||
// default reflog config without legacy options
|
||||
cfg, err := NewConfigProviderFromData(``)
|
||||
assert.NoError(t, err)
|
||||
loadGitFrom(cfg)
|
||||
|
||||
assert.Equal(t, "true", GitConfig.GetOption("core.logAllRefUpdates"))
|
||||
assert.Equal(t, "90", GitConfig.GetOption("gc.reflogExpire"))
|
||||
|
||||
// custom reflog config by legacy options
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[git.reflog]
|
||||
ENABLED = false
|
||||
EXPIRATION = 123
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadGitFrom(cfg)
|
||||
|
||||
assert.Equal(t, "false", GitConfig.GetOption("core.logAllRefUpdates"))
|
||||
assert.Equal(t, "123", GitConfig.GetOption("gc.reflogExpire"))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/nosql"
|
||||
)
|
||||
|
||||
// GlobalLock represents configuration of global lock
|
||||
var GlobalLock = struct {
|
||||
ServiceType string
|
||||
ServiceConnStr string
|
||||
}{
|
||||
ServiceType: "memory",
|
||||
}
|
||||
|
||||
func loadGlobalLockFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("global_lock")
|
||||
GlobalLock.ServiceType = sec.Key("SERVICE_TYPE").MustString("memory")
|
||||
switch GlobalLock.ServiceType {
|
||||
case "memory":
|
||||
case "redis":
|
||||
connStr := sec.Key("SERVICE_CONN_STR").String()
|
||||
if connStr == "" {
|
||||
log.Fatal("SERVICE_CONN_STR is empty for redis")
|
||||
}
|
||||
u := nosql.ToRedisURI(connStr)
|
||||
if u == nil {
|
||||
log.Fatal("SERVICE_CONN_STR %s is not a valid redis connection string", connStr)
|
||||
}
|
||||
GlobalLock.ServiceConnStr = connStr
|
||||
default:
|
||||
log.Fatal("Unknown sync lock service type: %s", GlobalLock.ServiceType)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import "gitea.dev/modules/glob"
|
||||
|
||||
type GlobMatcher struct {
|
||||
compiledGlob glob.Glob
|
||||
patternString string
|
||||
}
|
||||
|
||||
var _ glob.Glob = (*GlobMatcher)(nil)
|
||||
|
||||
func (g *GlobMatcher) Match(s string) bool {
|
||||
return g.compiledGlob.Match(s)
|
||||
}
|
||||
|
||||
func (g *GlobMatcher) PatternString() string {
|
||||
return g.patternString
|
||||
}
|
||||
|
||||
func GlobMatcherCompile(pattern string, separators ...rune) (*GlobMatcher, error) {
|
||||
g, err := glob.Compile(pattern, separators...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GlobMatcher{
|
||||
compiledGlob: g,
|
||||
patternString: pattern,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// Global settings
|
||||
var (
|
||||
// RunUser is the OS user that Gitea is running as. ini:"RUN_USER"
|
||||
RunUser string
|
||||
// RunMode is the running mode of Gitea, it only accepts two values: "dev" and "prod".
|
||||
// Non-dev values will be replaced by "prod". ini: "RUN_MODE"
|
||||
RunMode string
|
||||
// IsProd is true if RunMode is not "dev"
|
||||
IsProd bool
|
||||
|
||||
// AppName is the Application name, used in the page title. ini: "APP_NAME"
|
||||
AppName string
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadGlobalLockConfig(t *testing.T) {
|
||||
t.Run("DefaultGlobalLockConfig", func(t *testing.T) {
|
||||
iniStr := ``
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
loadGlobalLockFrom(cfg)
|
||||
assert.Equal(t, "memory", GlobalLock.ServiceType)
|
||||
})
|
||||
|
||||
t.Run("RedisGlobalLockConfig", func(t *testing.T) {
|
||||
iniStr := `
|
||||
[global_lock]
|
||||
SERVICE_TYPE = redis
|
||||
SERVICE_CONN_STR = addrs=127.0.0.1:6379 db=0
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
loadGlobalLockFrom(cfg)
|
||||
assert.Equal(t, "redis", GlobalLock.ServiceType)
|
||||
assert.Equal(t, "addrs=127.0.0.1:6379 db=0", GlobalLock.ServiceConnStr)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
func GetHighlightMapping() map[string]string {
|
||||
highlightMapping := map[string]string{}
|
||||
if CfgProvider == nil {
|
||||
return highlightMapping
|
||||
}
|
||||
|
||||
keys := CfgProvider.Section("highlight.mapping").Keys()
|
||||
for _, key := range keys {
|
||||
highlightMapping[key.Name()] = key.Value()
|
||||
}
|
||||
return highlightMapping
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// defaultI18nLangNames must be a slice, we need the order
|
||||
var defaultI18nLangNames = []string{
|
||||
"en-US", "English",
|
||||
"zh-CN", "简体中文",
|
||||
"zh-HK", "繁體中文(香港)",
|
||||
"zh-TW", "繁體中文(台灣)",
|
||||
"de-DE", "Deutsch",
|
||||
"fr-FR", "Français",
|
||||
"ga-IE", "Gaeilge",
|
||||
"nl-NL", "Nederlands",
|
||||
"lv-LV", "Latviešu",
|
||||
"ru-RU", "Русский",
|
||||
"uk-UA", "Українська",
|
||||
"ja-JP", "日本語",
|
||||
"es-ES", "Español",
|
||||
"pt-BR", "Português do Brasil",
|
||||
"pt-PT", "Português de Portugal",
|
||||
"pl-PL", "Polski",
|
||||
"bg-BG", "Български",
|
||||
"it-IT", "Italiano",
|
||||
"fi-FI", "Suomi",
|
||||
"tr-TR", "Türkçe",
|
||||
"cs-CZ", "Čeština",
|
||||
"sv-SE", "Svenska",
|
||||
"ko-KR", "한국어",
|
||||
"el-GR", "Ελληνικά",
|
||||
"fa-IR", "فارسی",
|
||||
"hu-HU", "Magyar nyelv",
|
||||
"id-ID", "Bahasa Indonesia",
|
||||
"ml-IN", "മലയാളം",
|
||||
}
|
||||
|
||||
func defaultI18nLangs() (res []string) {
|
||||
for i := 0; i < len(defaultI18nLangNames); i += 2 {
|
||||
res = append(res, defaultI18nLangNames[i])
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func defaultI18nNames() (res []string) {
|
||||
for i := 0; i < len(defaultI18nLangNames); i += 2 {
|
||||
res = append(res, defaultI18nLangNames[i+1])
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
var (
|
||||
// I18n settings
|
||||
Langs []string
|
||||
Names []string
|
||||
)
|
||||
|
||||
func loadI18nFrom(rootCfg ConfigProvider) {
|
||||
Langs = rootCfg.Section("i18n").Key("LANGS").Strings(",")
|
||||
if len(Langs) == 0 {
|
||||
Langs = defaultI18nLangs()
|
||||
}
|
||||
Names = rootCfg.Section("i18n").Key("NAMES").Strings(",")
|
||||
if len(Names) == 0 {
|
||||
Names = defaultI18nNames()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
const IncomingEmailTokenPlaceholder = "%{token}"
|
||||
|
||||
var IncomingEmail = struct {
|
||||
Enabled bool
|
||||
ReplyToAddress string
|
||||
Host string
|
||||
Port int
|
||||
UseTLS bool `ini:"USE_TLS"`
|
||||
SkipTLSVerify bool `ini:"SKIP_TLS_VERIFY"`
|
||||
Username string
|
||||
Password string
|
||||
Mailbox string
|
||||
DeleteHandledMessage bool
|
||||
MaximumMessageSize uint32
|
||||
}{
|
||||
Mailbox: "INBOX",
|
||||
DeleteHandledMessage: true,
|
||||
MaximumMessageSize: 10485760,
|
||||
}
|
||||
|
||||
func loadIncomingEmailFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "email.incoming", &IncomingEmail)
|
||||
|
||||
if !IncomingEmail.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkReplyToAddress(); err != nil {
|
||||
log.Fatal("Invalid incoming_mail.REPLY_TO_ADDRESS (%s): %v", IncomingEmail.ReplyToAddress, err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkReplyToAddress() error {
|
||||
parsed, err := mail.ParseAddress(IncomingEmail.ReplyToAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if parsed.Name != "" {
|
||||
return errors.New("name must not be set")
|
||||
}
|
||||
|
||||
placeholderCount := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmailTokenPlaceholder)
|
||||
userPart, _, _ := strings.Cut(IncomingEmail.ReplyToAddress, "@")
|
||||
if placeholderCount != 1 || !strings.Contains(userPart, IncomingEmailTokenPlaceholder) {
|
||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmailTokenPlaceholder)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// Indexer settings
|
||||
var Indexer = struct {
|
||||
IssueType string
|
||||
IssuePath string
|
||||
IssueConnStr string
|
||||
IssueConnAuth string
|
||||
IssueIndexerName string
|
||||
StartupTimeout time.Duration
|
||||
|
||||
RepoIndexerEnabled bool
|
||||
RepoIndexerRepoTypes []string
|
||||
RepoType string
|
||||
RepoPath string
|
||||
RepoConnStr string
|
||||
RepoIndexerName string
|
||||
MaxIndexerFileSize int64
|
||||
IncludePatterns []*GlobMatcher
|
||||
ExcludePatterns []*GlobMatcher
|
||||
ExcludeVendored bool
|
||||
|
||||
TypeBleveMaxFuzzniess int
|
||||
}{
|
||||
IssueType: "bleve",
|
||||
IssuePath: "indexers/issues.bleve",
|
||||
IssueConnStr: "",
|
||||
IssueConnAuth: "",
|
||||
IssueIndexerName: "gitea_issues",
|
||||
|
||||
RepoIndexerEnabled: false,
|
||||
RepoIndexerRepoTypes: []string{"sources", "forks", "mirrors", "templates"},
|
||||
RepoType: "bleve",
|
||||
RepoPath: "indexers/repos.bleve",
|
||||
RepoConnStr: "",
|
||||
RepoIndexerName: "gitea_codes",
|
||||
MaxIndexerFileSize: 1024 * 1024,
|
||||
ExcludeVendored: true,
|
||||
}
|
||||
|
||||
func loadIndexerFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("indexer")
|
||||
Indexer.IssueType = sec.Key("ISSUE_INDEXER_TYPE").MustString("bleve")
|
||||
if Indexer.IssueType == "bleve" {
|
||||
Indexer.IssuePath = filepath.ToSlash(sec.Key("ISSUE_INDEXER_PATH").MustString(filepath.ToSlash(filepath.Join(AppDataPath, "indexers/issues.bleve"))))
|
||||
if !filepath.IsAbs(Indexer.IssuePath) {
|
||||
Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
|
||||
}
|
||||
checkOverlappedPath("[indexer].ISSUE_INDEXER_PATH", Indexer.IssuePath)
|
||||
} else {
|
||||
Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
|
||||
if Indexer.IssueType == "meilisearch" {
|
||||
u, err := url.Parse(Indexer.IssueConnStr)
|
||||
if err != nil {
|
||||
log.Warn("Failed to parse ISSUE_INDEXER_CONN_STR: %v", err)
|
||||
u = &url.URL{}
|
||||
}
|
||||
Indexer.IssueConnAuth, _ = u.User.Password()
|
||||
u.User = nil
|
||||
Indexer.IssueConnStr = u.String()
|
||||
}
|
||||
}
|
||||
|
||||
Indexer.IssueIndexerName = sec.Key("ISSUE_INDEXER_NAME").MustString(Indexer.IssueIndexerName)
|
||||
|
||||
Indexer.RepoIndexerEnabled = sec.Key("REPO_INDEXER_ENABLED").MustBool(false)
|
||||
Indexer.RepoIndexerRepoTypes = strings.Split(sec.Key("REPO_INDEXER_REPO_TYPES").MustString("sources,forks,mirrors,templates"), ",")
|
||||
Indexer.RepoType = sec.Key("REPO_INDEXER_TYPE").MustString("bleve")
|
||||
Indexer.RepoPath = filepath.ToSlash(sec.Key("REPO_INDEXER_PATH").MustString(filepath.ToSlash(filepath.Join(AppDataPath, "indexers/repos.bleve"))))
|
||||
if !filepath.IsAbs(Indexer.RepoPath) {
|
||||
Indexer.RepoPath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.RepoPath))
|
||||
}
|
||||
Indexer.RepoConnStr = sec.Key("REPO_INDEXER_CONN_STR").MustString("")
|
||||
Indexer.RepoIndexerName = sec.Key("REPO_INDEXER_NAME").MustString("gitea_codes")
|
||||
|
||||
Indexer.IncludePatterns = IndexerGlobFromString(sec.Key("REPO_INDEXER_INCLUDE").MustString(""))
|
||||
Indexer.ExcludePatterns = IndexerGlobFromString(sec.Key("REPO_INDEXER_EXCLUDE").MustString(""))
|
||||
Indexer.ExcludeVendored = sec.Key("REPO_INDEXER_EXCLUDE_VENDORED").MustBool(true)
|
||||
Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024)
|
||||
Indexer.StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(30 * time.Second)
|
||||
Indexer.TypeBleveMaxFuzzniess = sec.Key("TYPE_BLEVE_MAX_FUZZINESS").MustInt(0)
|
||||
}
|
||||
|
||||
// IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing
|
||||
func IndexerGlobFromString(globstr string) []*GlobMatcher {
|
||||
extarr := make([]*GlobMatcher, 0, 10)
|
||||
for expr := range strings.SplitSeq(strings.ToLower(globstr), ",") {
|
||||
expr = strings.TrimSpace(expr)
|
||||
if expr != "" {
|
||||
if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil {
|
||||
log.Warn("Invalid glob expression '%s' (skipped): %v", expr, err)
|
||||
} else {
|
||||
extarr = append(extarr, g)
|
||||
}
|
||||
}
|
||||
}
|
||||
return extarr
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type indexerMatchList struct {
|
||||
value string
|
||||
position int
|
||||
}
|
||||
|
||||
func Test_newIndexerGlobSettings(t *testing.T) {
|
||||
checkGlobMatch(t, "", []indexerMatchList{})
|
||||
checkGlobMatch(t, " ", []indexerMatchList{})
|
||||
checkGlobMatch(t, "data, */data, */data/*, **/data/*, **/data/**", []indexerMatchList{
|
||||
{"", -1},
|
||||
{"don't", -1},
|
||||
{"data", 0},
|
||||
{"/data", 1},
|
||||
{"x/data", 1},
|
||||
{"x/data/y", 2},
|
||||
{"a/b/c/data/z", 3},
|
||||
{"a/b/c/data/x/y/z", 4},
|
||||
})
|
||||
checkGlobMatch(t, "*.txt, txt, **.txt, **txt, **txt*", []indexerMatchList{
|
||||
{"my.txt", 0},
|
||||
{"don't", -1},
|
||||
{"mytxt", 3},
|
||||
{"/data/my.txt", 2},
|
||||
{"data/my.txt", 2},
|
||||
{"data/txt", 3},
|
||||
{"data/thistxtfile", 4},
|
||||
{"/data/thistxtfile", 4},
|
||||
})
|
||||
checkGlobMatch(t, "data/**/*.txt, data/**.txt", []indexerMatchList{
|
||||
{"data/a/b/c/d.txt", 0},
|
||||
{"data/a.txt", 1},
|
||||
})
|
||||
checkGlobMatch(t, "**/*.txt, data/**.txt", []indexerMatchList{
|
||||
{"data/a/b/c/d.txt", 0},
|
||||
{"data/a.txt", 0},
|
||||
{"a.txt", -1},
|
||||
})
|
||||
}
|
||||
|
||||
func checkGlobMatch(t *testing.T, globstr string, list []indexerMatchList) {
|
||||
glist := IndexerGlobFromString(globstr)
|
||||
if len(list) == 0 {
|
||||
assert.Empty(t, glist)
|
||||
return
|
||||
}
|
||||
assert.NotEmpty(t, glist)
|
||||
for _, m := range list {
|
||||
found := false
|
||||
for pos, g := range glist {
|
||||
if g.Match(m.value) {
|
||||
assert.Equal(t, m.position, pos, "Test string `%s` doesn't match `%s`@%d, but matches @%d", m.value, globstr, m.position, pos)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
assert.Equal(t, -1, m.position, "Test string `%s` doesn't match `%s` anywhere; expected @%d", m.value, globstr, m.position)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/generate"
|
||||
)
|
||||
|
||||
// LFS represents the server-side configuration for Git LFS.
|
||||
// Ideally these options should be in a section like "[lfs_server]",
|
||||
// but they are in "[server]" section due to historical reasons.
|
||||
// Could be refactored in the future while keeping backwards compatibility.
|
||||
var LFS = struct {
|
||||
StartServer bool `ini:"LFS_START_SERVER"`
|
||||
AllowPureSSH bool `ini:"LFS_ALLOW_PURE_SSH"`
|
||||
JWTSecretBytes []byte `ini:"-"`
|
||||
HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
|
||||
MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
|
||||
LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"`
|
||||
MaxBatchSize int `ini:"LFS_MAX_BATCH_SIZE"`
|
||||
|
||||
Storage *Storage
|
||||
}{}
|
||||
|
||||
// LFSClient represents configuration for Gitea's LFS clients, for example: mirroring upstream Git LFS
|
||||
var LFSClient = struct {
|
||||
BatchSize int `ini:"BATCH_SIZE"`
|
||||
BatchOperationConcurrency int `ini:"BATCH_OPERATION_CONCURRENCY"`
|
||||
}{}
|
||||
|
||||
func loadLFSFrom(rootCfg ConfigProvider) error {
|
||||
mustMapSetting(rootCfg, "lfs_client", &LFSClient)
|
||||
|
||||
mustMapSetting(rootCfg, "server", &LFS)
|
||||
sec := rootCfg.Section("server")
|
||||
|
||||
lfsSec, _ := rootCfg.GetSection("lfs")
|
||||
|
||||
// Specifically default PATH to LFS_CONTENT_PATH
|
||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
||||
// if these are removed, the warning will not be shown
|
||||
deprecatedSetting(rootCfg, "server", "LFS_CONTENT_PATH", "lfs", "PATH", "v1.19.0")
|
||||
|
||||
if val := sec.Key("LFS_CONTENT_PATH").String(); val != "" {
|
||||
if lfsSec == nil {
|
||||
lfsSec = rootCfg.Section("lfs")
|
||||
}
|
||||
lfsSec.Key("PATH").MustString(val)
|
||||
}
|
||||
|
||||
var err error
|
||||
LFS.Storage, err = getStorage(rootCfg, "lfs", "", lfsSec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rest of LFS service settings
|
||||
if LFS.LocksPagingNum == 0 {
|
||||
LFS.LocksPagingNum = 50
|
||||
}
|
||||
|
||||
if LFSClient.BatchSize < 1 {
|
||||
LFSClient.BatchSize = 20
|
||||
}
|
||||
|
||||
if LFSClient.BatchOperationConcurrency < 1 {
|
||||
// match the default git-lfs's `lfs.concurrenttransfers` https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-config.adoc#upload-and-download-transfer-settings
|
||||
LFSClient.BatchOperationConcurrency = 8
|
||||
}
|
||||
|
||||
LFS.HTTPAuthExpiry = sec.Key("LFS_HTTP_AUTH_EXPIRY").MustDuration(24 * time.Hour)
|
||||
|
||||
if !LFS.StartServer || !InstallLock {
|
||||
return nil
|
||||
}
|
||||
|
||||
jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
||||
LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64)
|
||||
if err != nil {
|
||||
LFS.JWTSecretBytes, jwtSecretBase64 = generate.NewJwtSecretWithBase64()
|
||||
|
||||
// Save secret
|
||||
saveCfg, err := rootCfg.PrepareSaving()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
|
||||
}
|
||||
rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
if err := saveCfg.Save(); err != nil {
|
||||
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getStorageInheritNameSectionTypeForLFS(t *testing.T) {
|
||||
iniStr := `
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", LFS.Storage.Type)
|
||||
assert.Equal(t, "lfs/", LFS.Storage.MinioConfig.BasePath)
|
||||
|
||||
iniStr = `
|
||||
[server]
|
||||
LFS_CONTENT_PATH = path_ignored
|
||||
[lfs]
|
||||
PATH = path_used
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "local", LFS.Storage.Type)
|
||||
assert.Contains(t, LFS.Storage.Path, "path_used")
|
||||
|
||||
iniStr = `
|
||||
[server]
|
||||
LFS_CONTENT_PATH = deprecatedpath
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "local", LFS.Storage.Type)
|
||||
assert.Contains(t, LFS.Storage.Path, "deprecatedpath")
|
||||
|
||||
iniStr = `
|
||||
[storage.lfs]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", LFS.Storage.Type)
|
||||
assert.Equal(t, "lfs/", LFS.Storage.MinioConfig.BasePath)
|
||||
|
||||
iniStr = `
|
||||
[lfs]
|
||||
STORAGE_TYPE = my_minio
|
||||
|
||||
[storage.my_minio]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", LFS.Storage.Type)
|
||||
assert.Equal(t, "lfs/", LFS.Storage.MinioConfig.BasePath)
|
||||
|
||||
iniStr = `
|
||||
[lfs]
|
||||
STORAGE_TYPE = my_minio
|
||||
MINIO_BASE_PATH = my_lfs/
|
||||
|
||||
[storage.my_minio]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", LFS.Storage.Type)
|
||||
assert.Equal(t, "my_lfs/", LFS.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_LFSStorage1(t *testing.T) {
|
||||
iniStr := `
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
assert.EqualValues(t, "minio", LFS.Storage.Type)
|
||||
assert.Equal(t, "gitea", LFS.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "lfs/", LFS.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_LFSClientServerConfigs(t *testing.T) {
|
||||
iniStr := `
|
||||
[server]
|
||||
LFS_MAX_BATCH_SIZE = 100
|
||||
[lfs_client]
|
||||
# will default to 20
|
||||
BATCH_SIZE = 0
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
assert.Equal(t, 100, LFS.MaxBatchSize)
|
||||
assert.Equal(t, 20, LFSClient.BatchSize)
|
||||
assert.Equal(t, 8, LFSClient.BatchOperationConcurrency)
|
||||
|
||||
iniStr = `
|
||||
[lfs_client]
|
||||
BATCH_SIZE = 50
|
||||
BATCH_OPERATION_CONCURRENCY = 10
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
assert.Equal(t, 50, LFSClient.BatchSize)
|
||||
assert.Equal(t, 10, LFSClient.BatchOperationConcurrency)
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
golog "log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
type LogGlobalConfig struct {
|
||||
RootPath string
|
||||
|
||||
Mode string
|
||||
Level log.Level
|
||||
StacktraceLogLevel log.Level
|
||||
BufferLen int
|
||||
|
||||
EnableSSHLog bool
|
||||
|
||||
AccessLogTemplate string
|
||||
RequestIDHeaders []string
|
||||
}
|
||||
|
||||
var Log LogGlobalConfig
|
||||
|
||||
const accessLogTemplateDefault = `{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"`
|
||||
|
||||
func loadLogGlobalFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("log")
|
||||
|
||||
Log.Level = log.LevelFromString(sec.Key("LEVEL").MustString(log.INFO.String()))
|
||||
Log.StacktraceLogLevel = log.LevelFromString(sec.Key("STACKTRACE_LEVEL").MustString(log.NONE.String()))
|
||||
Log.BufferLen = sec.Key("BUFFER_LEN").MustInt(10000)
|
||||
Log.Mode = sec.Key("MODE").MustString("console")
|
||||
|
||||
Log.RootPath = sec.Key("ROOT_PATH").MustString(filepath.Join(AppWorkPath, "log"))
|
||||
if !filepath.IsAbs(Log.RootPath) {
|
||||
Log.RootPath = filepath.Join(AppWorkPath, Log.RootPath)
|
||||
}
|
||||
Log.RootPath = util.FilePathJoinAbs(Log.RootPath)
|
||||
|
||||
Log.EnableSSHLog = sec.Key("ENABLE_SSH_LOG").MustBool(false)
|
||||
|
||||
Log.AccessLogTemplate = sec.Key("ACCESS_LOG_TEMPLATE").MustString(accessLogTemplateDefault)
|
||||
Log.RequestIDHeaders = sec.Key("REQUEST_ID_HEADERS").Strings(",")
|
||||
}
|
||||
|
||||
func prepareLoggerConfig(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("log")
|
||||
|
||||
if !sec.HasKey("logger.default.MODE") {
|
||||
sec.Key("logger.default.MODE").MustString(",")
|
||||
}
|
||||
|
||||
deprecatedSetting(rootCfg, "log", "ACCESS", "log", "logger.access.MODE", "1.21")
|
||||
deprecatedSetting(rootCfg, "log", "ENABLE_ACCESS_LOG", "log", "logger.access.MODE", "1.21")
|
||||
if val := sec.Key("ACCESS").String(); val != "" {
|
||||
sec.Key("logger.access.MODE").MustString(val)
|
||||
}
|
||||
if sec.HasKey("ENABLE_ACCESS_LOG") && !sec.Key("ENABLE_ACCESS_LOG").MustBool() {
|
||||
sec.Key("logger.access.MODE").SetValue("")
|
||||
}
|
||||
|
||||
deprecatedSetting(rootCfg, "log", "ROUTER", "log", "logger.router.MODE", "1.21")
|
||||
deprecatedSetting(rootCfg, "log", "DISABLE_ROUTER_LOG", "log", "logger.router.MODE", "1.21")
|
||||
if val := sec.Key("ROUTER").String(); val != "" {
|
||||
sec.Key("logger.router.MODE").MustString(val)
|
||||
}
|
||||
if !sec.HasKey("logger.router.MODE") {
|
||||
sec.Key("logger.router.MODE").MustString(",") // use default logger
|
||||
}
|
||||
if sec.HasKey("DISABLE_ROUTER_LOG") && sec.Key("DISABLE_ROUTER_LOG").MustBool() {
|
||||
sec.Key("logger.router.MODE").SetValue("")
|
||||
}
|
||||
|
||||
deprecatedSetting(rootCfg, "log", "XORM", "log", "logger.xorm.MODE", "1.21")
|
||||
deprecatedSetting(rootCfg, "log", "ENABLE_XORM_LOG", "log", "logger.xorm.MODE", "1.21")
|
||||
if val := sec.Key("XORM").String(); val != "" {
|
||||
sec.Key("logger.xorm.MODE").MustString(val)
|
||||
}
|
||||
if !sec.HasKey("logger.xorm.MODE") {
|
||||
sec.Key("logger.xorm.MODE").MustString(",") // use default logger
|
||||
}
|
||||
if sec.HasKey("ENABLE_XORM_LOG") && !sec.Key("ENABLE_XORM_LOG").MustBool() {
|
||||
sec.Key("logger.xorm.MODE").SetValue("")
|
||||
}
|
||||
}
|
||||
|
||||
func LogPrepareFilenameForWriter(fileName, defaultFileName string) string {
|
||||
if fileName == "" {
|
||||
fileName = defaultFileName
|
||||
}
|
||||
if !filepath.IsAbs(fileName) {
|
||||
fileName = filepath.Join(Log.RootPath, fileName)
|
||||
} else {
|
||||
fileName = filepath.Clean(fileName)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(fileName), os.ModePerm); err != nil {
|
||||
panic(fmt.Sprintf("unable to create directory for log %q: %v", fileName, err.Error()))
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
|
||||
func loadLogModeByName(rootCfg ConfigProvider, loggerName, modeName string) (writerName, writerType string, writerMode log.WriterMode, err error) {
|
||||
sec := rootCfg.Section("log." + modeName)
|
||||
|
||||
writerMode = log.WriterMode{}
|
||||
writerType = ConfigSectionKeyString(sec, "MODE")
|
||||
if writerType == "" {
|
||||
writerType = modeName
|
||||
}
|
||||
|
||||
writerName = modeName
|
||||
defaultFlags := "stdflags"
|
||||
defaultFilaName := "gitea.log"
|
||||
if loggerName == "access" {
|
||||
// "access" logger is special, by default it doesn't have output flags, so it also needs a new writer name to avoid conflicting with other writers.
|
||||
// so "access" logger's writer name is usually "file.access" or "console.access"
|
||||
writerName += ".access"
|
||||
defaultFlags = "none"
|
||||
defaultFilaName = "access.log"
|
||||
}
|
||||
|
||||
writerMode.Level = log.LevelFromString(ConfigInheritedKeyString(sec, "LEVEL", Log.Level.String()))
|
||||
writerMode.StacktraceLevel = log.LevelFromString(ConfigInheritedKeyString(sec, "STACKTRACE_LEVEL", Log.StacktraceLogLevel.String()))
|
||||
writerMode.Prefix = ConfigInheritedKeyString(sec, "PREFIX")
|
||||
writerMode.Expression = ConfigInheritedKeyString(sec, "EXPRESSION")
|
||||
writerMode.Flags = log.FlagsFromString(ConfigInheritedKeyString(sec, "FLAGS", defaultFlags))
|
||||
|
||||
switch writerType {
|
||||
case "console":
|
||||
useStderr := ConfigInheritedKey(sec, "STDERR").MustBool(false)
|
||||
defaultCanColor := log.CanColorStdout
|
||||
if useStderr {
|
||||
defaultCanColor = log.CanColorStderr
|
||||
}
|
||||
writerOption := log.WriterConsoleOption{Stderr: useStderr}
|
||||
writerMode.Colorize = ConfigInheritedKey(sec, "COLORIZE").MustBool(defaultCanColor)
|
||||
writerMode.WriterOption = writerOption
|
||||
case "file":
|
||||
fileName := LogPrepareFilenameForWriter(ConfigInheritedKey(sec, "FILE_NAME").String(), defaultFilaName)
|
||||
writerOption := log.WriterFileOption{}
|
||||
writerOption.FileName = fileName + filenameSuffix // FIXME: the suffix doesn't seem right, see its related comments
|
||||
writerOption.LogRotate = ConfigInheritedKey(sec, "LOG_ROTATE").MustBool(true)
|
||||
writerOption.MaxSize = 1 << uint(ConfigInheritedKey(sec, "MAX_SIZE_SHIFT").MustInt(28))
|
||||
writerOption.DailyRotate = ConfigInheritedKey(sec, "DAILY_ROTATE").MustBool(true)
|
||||
writerOption.MaxDays = ConfigInheritedKey(sec, "MAX_DAYS").MustInt(7)
|
||||
writerOption.Compress = ConfigInheritedKey(sec, "COMPRESS").MustBool(true)
|
||||
writerOption.CompressionLevel = ConfigInheritedKey(sec, "COMPRESSION_LEVEL").MustInt(-1)
|
||||
writerMode.WriterOption = writerOption
|
||||
case "conn":
|
||||
writerOption := log.WriterConnOption{}
|
||||
writerOption.ReconnectOnMsg = ConfigInheritedKey(sec, "RECONNECT_ON_MSG").MustBool()
|
||||
writerOption.Reconnect = ConfigInheritedKey(sec, "RECONNECT").MustBool()
|
||||
writerOption.Protocol = ConfigInheritedKey(sec, "PROTOCOL").In("tcp", []string{"tcp", "unix", "udp"})
|
||||
writerOption.Addr = ConfigInheritedKey(sec, "ADDR").MustString(":7020")
|
||||
writerMode.WriterOption = writerOption
|
||||
default:
|
||||
if !log.HasEventWriter(writerType) {
|
||||
return "", "", writerMode, fmt.Errorf("invalid log writer type (mode): %s, maybe it needs something like 'MODE=file' in [log.%s] section", writerType, modeName)
|
||||
}
|
||||
}
|
||||
|
||||
return writerName, writerType, writerMode, nil
|
||||
}
|
||||
|
||||
var filenameSuffix = ""
|
||||
|
||||
// RestartLogsWithPIDSuffix restarts the logs with a PID suffix on files
|
||||
// FIXME: it seems not right, it breaks log rotating or log collectors
|
||||
func RestartLogsWithPIDSuffix() {
|
||||
filenameSuffix = fmt.Sprintf(".%d", os.Getpid())
|
||||
initAllLoggers() // when forking, before restarting, rename logger file and re-init all loggers
|
||||
}
|
||||
|
||||
func InitLoggersForTest() {
|
||||
initAllLoggers()
|
||||
}
|
||||
|
||||
var initLoggerDisabled bool
|
||||
|
||||
// initAllLoggers creates all the log services
|
||||
func initAllLoggers() {
|
||||
if initLoggerDisabled {
|
||||
return
|
||||
}
|
||||
initManagedLoggers(log.GetManager(), CfgProvider)
|
||||
|
||||
golog.SetFlags(0)
|
||||
golog.SetPrefix("")
|
||||
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
|
||||
}
|
||||
|
||||
func DisableLoggerInit() {
|
||||
initLoggerDisabled = true
|
||||
}
|
||||
|
||||
func initManagedLoggers(manager *log.LoggerManager, cfg ConfigProvider) {
|
||||
loadLogGlobalFrom(cfg)
|
||||
prepareLoggerConfig(cfg)
|
||||
|
||||
initLoggerByName(manager, cfg, log.DEFAULT) // default
|
||||
initLoggerByName(manager, cfg, "access")
|
||||
initLoggerByName(manager, cfg, "router")
|
||||
initLoggerByName(manager, cfg, "xorm")
|
||||
}
|
||||
|
||||
func initLoggerByName(manager *log.LoggerManager, rootCfg ConfigProvider, loggerName string) {
|
||||
sec := rootCfg.Section("log")
|
||||
keyPrefix := "logger." + loggerName
|
||||
|
||||
disabled := sec.HasKey(keyPrefix+".MODE") && sec.Key(keyPrefix+".MODE").String() == ""
|
||||
if disabled {
|
||||
return
|
||||
}
|
||||
|
||||
modeVal := sec.Key(keyPrefix + ".MODE").String()
|
||||
if modeVal == "," {
|
||||
modeVal = Log.Mode
|
||||
}
|
||||
|
||||
var eventWriters []log.EventWriter
|
||||
modes := strings.SplitSeq(modeVal, ",")
|
||||
for modeName := range modes {
|
||||
modeName = strings.TrimSpace(modeName)
|
||||
if modeName == "" {
|
||||
continue
|
||||
}
|
||||
writerName, writerType, writerMode, err := loadLogModeByName(rootCfg, loggerName, modeName)
|
||||
if err != nil {
|
||||
log.FallbackErrorf("Failed to load writer mode %q for logger %s: %v", modeName, loggerName, err)
|
||||
continue
|
||||
}
|
||||
if writerMode.BufferLen == 0 {
|
||||
writerMode.BufferLen = Log.BufferLen
|
||||
}
|
||||
eventWriter := manager.GetSharedWriter(writerName)
|
||||
if eventWriter == nil {
|
||||
eventWriter, err = manager.NewSharedWriter(writerName, writerType, writerMode)
|
||||
if err != nil {
|
||||
log.FallbackErrorf("Failed to create event writer for logger %s: %v", loggerName, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
eventWriters = append(eventWriters, eventWriter)
|
||||
}
|
||||
|
||||
manager.GetLogger(loggerName).ReplaceAllWriters(eventWriters...)
|
||||
}
|
||||
|
||||
func InitSQLLoggersForCli(level log.Level) {
|
||||
log.SetupStderrLogger("xorm", "console-stderr", level)
|
||||
}
|
||||
|
||||
func IsAccessLogEnabled() bool {
|
||||
return log.IsLoggerEnabled("access")
|
||||
}
|
||||
|
||||
func IsRouteLogEnabled() bool {
|
||||
return log.IsLoggerEnabled("router")
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func initLoggersByConfig(t *testing.T, config string) (*log.LoggerManager, func()) {
|
||||
oldLogConfig := Log
|
||||
Log = LogGlobalConfig{}
|
||||
defer func() {
|
||||
Log = oldLogConfig
|
||||
}()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
manager := log.NewManager()
|
||||
initManagedLoggers(manager, cfg)
|
||||
return manager, manager.Close
|
||||
}
|
||||
|
||||
func toJSON(v any) string {
|
||||
b, _ := json.MarshalIndent(v, "", "\t")
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestLogConfigDefault(t *testing.T) {
|
||||
manager, managerClose := initLoggersByConfig(t, ``)
|
||||
defer managerClose()
|
||||
|
||||
writerDump := `
|
||||
{
|
||||
"console": {
|
||||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
"StacktraceLevel": "none",
|
||||
"WriterOption": {
|
||||
"Stderr": false
|
||||
},
|
||||
"WriterType": "console"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
dump := manager.GetLogger(log.DEFAULT).DumpWriters()
|
||||
require.JSONEq(t, writerDump, toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("access").DumpWriters()
|
||||
require.JSONEq(t, "{}", toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("router").DumpWriters()
|
||||
require.JSONEq(t, writerDump, toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("xorm").DumpWriters()
|
||||
require.JSONEq(t, writerDump, toJSON(dump))
|
||||
}
|
||||
|
||||
func TestLogConfigDisable(t *testing.T) {
|
||||
manager, managerClose := initLoggersByConfig(t, `
|
||||
[log]
|
||||
logger.router.MODE =
|
||||
logger.xorm.MODE =
|
||||
`)
|
||||
defer managerClose()
|
||||
|
||||
writerDump := `
|
||||
{
|
||||
"console": {
|
||||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
"StacktraceLevel": "none",
|
||||
"WriterOption": {
|
||||
"Stderr": false
|
||||
},
|
||||
"WriterType": "console"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
dump := manager.GetLogger(log.DEFAULT).DumpWriters()
|
||||
require.JSONEq(t, writerDump, toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("access").DumpWriters()
|
||||
require.JSONEq(t, "{}", toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("router").DumpWriters()
|
||||
require.JSONEq(t, "{}", toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("xorm").DumpWriters()
|
||||
require.JSONEq(t, "{}", toJSON(dump))
|
||||
}
|
||||
|
||||
func TestLogConfigLegacyDefault(t *testing.T) {
|
||||
manager, managerClose := initLoggersByConfig(t, `
|
||||
[log]
|
||||
MODE = console
|
||||
`)
|
||||
defer managerClose()
|
||||
|
||||
writerDump := `
|
||||
{
|
||||
"console": {
|
||||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
"StacktraceLevel": "none",
|
||||
"WriterOption": {
|
||||
"Stderr": false
|
||||
},
|
||||
"WriterType": "console"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
dump := manager.GetLogger(log.DEFAULT).DumpWriters()
|
||||
require.JSONEq(t, writerDump, toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("access").DumpWriters()
|
||||
require.JSONEq(t, "{}", toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("router").DumpWriters()
|
||||
require.JSONEq(t, writerDump, toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("xorm").DumpWriters()
|
||||
require.JSONEq(t, writerDump, toJSON(dump))
|
||||
}
|
||||
|
||||
func TestLogConfigLegacyMode(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
tempPath := func(file string) string {
|
||||
return filepath.Join(tempDir, file)
|
||||
}
|
||||
|
||||
manager, managerClose := initLoggersByConfig(t, `
|
||||
[log]
|
||||
ROOT_PATH = `+tempDir+`
|
||||
MODE = file
|
||||
ROUTER = file
|
||||
ACCESS = file
|
||||
`)
|
||||
defer managerClose()
|
||||
|
||||
writerDump := `
|
||||
{
|
||||
"file": {
|
||||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
"StacktraceLevel": "none",
|
||||
"WriterOption": {
|
||||
"Compress": true,
|
||||
"CompressionLevel": -1,
|
||||
"DailyRotate": true,
|
||||
"FileName": "$FILENAME",
|
||||
"LogRotate": true,
|
||||
"MaxDays": 7,
|
||||
"MaxSize": 268435456
|
||||
},
|
||||
"WriterType": "file"
|
||||
}
|
||||
}
|
||||
`
|
||||
writerDumpAccess := `
|
||||
{
|
||||
"file.access": {
|
||||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Flags": "none",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
"StacktraceLevel": "none",
|
||||
"WriterOption": {
|
||||
"Compress": true,
|
||||
"CompressionLevel": -1,
|
||||
"DailyRotate": true,
|
||||
"FileName": "$FILENAME",
|
||||
"LogRotate": true,
|
||||
"MaxDays": 7,
|
||||
"MaxSize": 268435456
|
||||
},
|
||||
"WriterType": "file"
|
||||
}
|
||||
}
|
||||
`
|
||||
dump := manager.GetLogger(log.DEFAULT).DumpWriters()
|
||||
require.JSONEq(t, strings.ReplaceAll(writerDump, "$FILENAME", tempPath("gitea.log")), toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("access").DumpWriters()
|
||||
require.JSONEq(t, strings.ReplaceAll(writerDumpAccess, "$FILENAME", tempPath("access.log")), toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("router").DumpWriters()
|
||||
require.JSONEq(t, strings.ReplaceAll(writerDump, "$FILENAME", tempPath("gitea.log")), toJSON(dump))
|
||||
}
|
||||
|
||||
func TestLogConfigLegacyModeDisable(t *testing.T) {
|
||||
manager, managerClose := initLoggersByConfig(t, `
|
||||
[log]
|
||||
ROUTER = file
|
||||
ACCESS = file
|
||||
DISABLE_ROUTER_LOG = true
|
||||
ENABLE_ACCESS_LOG = false
|
||||
`)
|
||||
defer managerClose()
|
||||
|
||||
dump := manager.GetLogger("access").DumpWriters()
|
||||
require.JSONEq(t, "{}", toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("router").DumpWriters()
|
||||
require.JSONEq(t, "{}", toJSON(dump))
|
||||
}
|
||||
|
||||
func TestLogConfigNewConfig(t *testing.T) {
|
||||
manager, managerClose := initLoggersByConfig(t, `
|
||||
[log]
|
||||
logger.access.MODE = console
|
||||
logger.xorm.MODE = console, console-1
|
||||
|
||||
[log.console]
|
||||
LEVEL = warn
|
||||
|
||||
[log.console-1]
|
||||
MODE = console
|
||||
LEVEL = error
|
||||
STDERR = true
|
||||
`)
|
||||
defer managerClose()
|
||||
|
||||
writerDump := `
|
||||
{
|
||||
"console": {
|
||||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "warn",
|
||||
"Prefix": "",
|
||||
"StacktraceLevel": "none",
|
||||
"WriterOption": {
|
||||
"Stderr": false
|
||||
},
|
||||
"WriterType": "console"
|
||||
},
|
||||
"console-1": {
|
||||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "error",
|
||||
"Prefix": "",
|
||||
"StacktraceLevel": "none",
|
||||
"WriterOption": {
|
||||
"Stderr": true
|
||||
},
|
||||
"WriterType": "console"
|
||||
}
|
||||
}
|
||||
`
|
||||
writerDumpAccess := `
|
||||
{
|
||||
"console.access": {
|
||||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Flags": "none",
|
||||
"Level": "warn",
|
||||
"Prefix": "",
|
||||
"StacktraceLevel": "none",
|
||||
"WriterOption": {
|
||||
"Stderr": false
|
||||
},
|
||||
"WriterType": "console"
|
||||
}
|
||||
}
|
||||
`
|
||||
dump := manager.GetLogger("xorm").DumpWriters()
|
||||
require.JSONEq(t, writerDump, toJSON(dump))
|
||||
|
||||
dump = manager.GetLogger("access").DumpWriters()
|
||||
require.JSONEq(t, writerDumpAccess, toJSON(dump))
|
||||
}
|
||||
|
||||
func TestLogConfigModeFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
tempPath := func(file string) string {
|
||||
return filepath.Join(tempDir, file)
|
||||
}
|
||||
|
||||
manager, managerClose := initLoggersByConfig(t, `
|
||||
[log]
|
||||
ROOT_PATH = `+tempDir+`
|
||||
BUFFER_LEN = 10
|
||||
MODE = file, file1
|
||||
|
||||
[log.file1]
|
||||
MODE = file
|
||||
LEVEL = error
|
||||
STACKTRACE_LEVEL = fatal
|
||||
EXPRESSION = filter
|
||||
FLAGS = medfile
|
||||
PREFIX = "[Prefix] "
|
||||
FILE_NAME = file-xxx.log
|
||||
LOG_ROTATE = false
|
||||
MAX_SIZE_SHIFT = 1
|
||||
DAILY_ROTATE = false
|
||||
MAX_DAYS = 90
|
||||
COMPRESS = false
|
||||
COMPRESSION_LEVEL = 4
|
||||
`)
|
||||
defer managerClose()
|
||||
|
||||
writerDump := `
|
||||
{
|
||||
"file": {
|
||||
"BufferLen": 10,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
"StacktraceLevel": "none",
|
||||
"WriterOption": {
|
||||
"Compress": true,
|
||||
"CompressionLevel": -1,
|
||||
"DailyRotate": true,
|
||||
"FileName": "$FILENAME-0",
|
||||
"LogRotate": true,
|
||||
"MaxDays": 7,
|
||||
"MaxSize": 268435456
|
||||
},
|
||||
"WriterType": "file"
|
||||
},
|
||||
"file1": {
|
||||
"BufferLen": 10,
|
||||
"Colorize": false,
|
||||
"Expression": "filter",
|
||||
"Flags": "medfile",
|
||||
"Level": "error",
|
||||
"Prefix": "[Prefix] ",
|
||||
"StacktraceLevel": "fatal",
|
||||
"WriterOption": {
|
||||
"Compress": false,
|
||||
"CompressionLevel": 4,
|
||||
"DailyRotate": false,
|
||||
"FileName": "$FILENAME-1",
|
||||
"LogRotate": false,
|
||||
"MaxDays": 90,
|
||||
"MaxSize": 2
|
||||
},
|
||||
"WriterType": "file"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
dump := manager.GetLogger(log.DEFAULT).DumpWriters()
|
||||
expected := writerDump
|
||||
expected = strings.ReplaceAll(expected, "$FILENAME-0", tempPath("gitea.log"))
|
||||
expected = strings.ReplaceAll(expected, "$FILENAME-1", tempPath("file-xxx.log"))
|
||||
require.JSONEq(t, expected, toJSON(dump))
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
)
|
||||
|
||||
// Mailer represents mail service.
|
||||
type Mailer struct {
|
||||
// Mailer
|
||||
Name string `ini:"NAME"`
|
||||
From string `ini:"FROM"`
|
||||
EnvelopeFrom string `ini:"ENVELOPE_FROM"`
|
||||
OverrideEnvelopeFrom bool `ini:"-"`
|
||||
FromName string `ini:"-"`
|
||||
FromEmail string `ini:"-"`
|
||||
SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"`
|
||||
SubjectPrefix string `ini:"SUBJECT_PREFIX"`
|
||||
OverrideHeader map[string][]string `ini:"-"`
|
||||
|
||||
// Embed attachment images as inline base64 img src attribute
|
||||
EmbedAttachmentImages bool
|
||||
|
||||
// SMTP sender
|
||||
Protocol string `ini:"PROTOCOL"`
|
||||
SMTPAddr string `ini:"SMTP_ADDR"`
|
||||
SMTPPort string `ini:"SMTP_PORT"`
|
||||
User string `ini:"USER"`
|
||||
Passwd string `ini:"PASSWD"`
|
||||
EnableHelo bool `ini:"ENABLE_HELO"`
|
||||
HeloHostname string `ini:"HELO_HOSTNAME"`
|
||||
ForceTrustServerCert bool `ini:"FORCE_TRUST_SERVER_CERT"`
|
||||
UseClientCert bool `ini:"USE_CLIENT_CERT"`
|
||||
ClientCertFile string `ini:"CLIENT_CERT_FILE"`
|
||||
ClientKeyFile string `ini:"CLIENT_KEY_FILE"`
|
||||
|
||||
// Sendmail sender
|
||||
SendmailPath string `ini:"SENDMAIL_PATH"`
|
||||
SendmailArgs []string `ini:"-"`
|
||||
SendmailTimeout time.Duration `ini:"SENDMAIL_TIMEOUT"`
|
||||
SendmailConvertCRLF bool `ini:"SENDMAIL_CONVERT_CRLF"`
|
||||
|
||||
// Customization
|
||||
FromDisplayNameFormat string `ini:"FROM_DISPLAY_NAME_FORMAT"`
|
||||
FromDisplayNameFormatTemplate *template.Template `ini:"-"`
|
||||
}
|
||||
|
||||
// MailService the global mailer
|
||||
var MailService *Mailer
|
||||
|
||||
func loadMailsFrom(rootCfg ConfigProvider) {
|
||||
loadMailerFrom(rootCfg)
|
||||
loadRegisterMailFrom(rootCfg)
|
||||
loadNotifyMailFrom(rootCfg)
|
||||
loadIncomingEmailFrom(rootCfg)
|
||||
}
|
||||
|
||||
func loadMailerFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("mailer")
|
||||
// Check mailer setting.
|
||||
if !sec.Key("ENABLED").MustBool() {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle Deprecations and map on to new configuration
|
||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
||||
// if these are removed, the warning will not be shown
|
||||
deprecatedSetting(rootCfg, "mailer", "MAILER_TYPE", "mailer", "PROTOCOL", "v1.19.0")
|
||||
if sec.HasKey("MAILER_TYPE") && !sec.HasKey("PROTOCOL") {
|
||||
if sec.Key("MAILER_TYPE").String() == "sendmail" {
|
||||
sec.Key("PROTOCOL").MustString("sendmail")
|
||||
}
|
||||
}
|
||||
|
||||
deprecatedSetting(rootCfg, "mailer", "HOST", "mailer", "SMTP_ADDR", "v1.19.0")
|
||||
if sec.HasKey("HOST") && !sec.HasKey("SMTP_ADDR") {
|
||||
givenHost := sec.Key("HOST").String()
|
||||
addr, port, err := net.SplitHostPort(givenHost)
|
||||
if err != nil && strings.Contains(err.Error(), "missing port in address") {
|
||||
addr = givenHost
|
||||
} else if err != nil {
|
||||
log.Fatal("Invalid mailer.HOST (%s): %v", givenHost, err)
|
||||
}
|
||||
if addr == "" {
|
||||
addr = "127.0.0.1"
|
||||
}
|
||||
sec.Key("SMTP_ADDR").MustString(addr)
|
||||
sec.Key("SMTP_PORT").MustString(port)
|
||||
}
|
||||
|
||||
deprecatedSetting(rootCfg, "mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL", "v1.19.0")
|
||||
if sec.HasKey("IS_TLS_ENABLED") && !sec.HasKey("PROTOCOL") {
|
||||
if sec.Key("IS_TLS_ENABLED").MustBool() {
|
||||
sec.Key("PROTOCOL").MustString("smtps")
|
||||
} else {
|
||||
sec.Key("PROTOCOL").MustString("smtp+starttls")
|
||||
}
|
||||
}
|
||||
|
||||
deprecatedSetting(rootCfg, "mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO", "v1.19.0")
|
||||
if sec.HasKey("DISABLE_HELO") && !sec.HasKey("ENABLE_HELO") {
|
||||
sec.Key("ENABLE_HELO").MustBool(!sec.Key("DISABLE_HELO").MustBool())
|
||||
}
|
||||
|
||||
deprecatedSetting(rootCfg, "mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT", "v1.19.0")
|
||||
if sec.HasKey("SKIP_VERIFY") && !sec.HasKey("FORCE_TRUST_SERVER_CERT") {
|
||||
sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(sec.Key("SKIP_VERIFY").MustBool())
|
||||
}
|
||||
|
||||
deprecatedSetting(rootCfg, "mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT", "v1.19.0")
|
||||
if sec.HasKey("USE_CERTIFICATE") && !sec.HasKey("USE_CLIENT_CERT") {
|
||||
sec.Key("USE_CLIENT_CERT").MustBool(sec.Key("USE_CERTIFICATE").MustBool())
|
||||
}
|
||||
|
||||
deprecatedSetting(rootCfg, "mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE", "v1.19.0")
|
||||
if sec.HasKey("CERT_FILE") && !sec.HasKey("CLIENT_CERT_FILE") {
|
||||
sec.Key("CERT_FILE").MustString(sec.Key("CERT_FILE").String())
|
||||
}
|
||||
|
||||
deprecatedSetting(rootCfg, "mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE", "v1.19.0")
|
||||
if sec.HasKey("KEY_FILE") && !sec.HasKey("CLIENT_KEY_FILE") {
|
||||
sec.Key("KEY_FILE").MustString(sec.Key("KEY_FILE").String())
|
||||
}
|
||||
|
||||
deprecatedSetting(rootCfg, "mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT", "v1.19.0")
|
||||
if sec.HasKey("ENABLE_HTML_ALTERNATIVE") && !sec.HasKey("SEND_AS_PLAIN_TEXT") {
|
||||
sec.Key("SEND_AS_PLAIN_TEXT").MustBool(!sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false))
|
||||
}
|
||||
|
||||
if sec.HasKey("PROTOCOL") && sec.Key("PROTOCOL").String() == "smtp+startls" {
|
||||
log.Error("Deprecated fallback `[mailer]` `PROTOCOL = smtp+startls` present. Use `[mailer]` `PROTOCOL = smtp+starttls`` instead. This fallback will be removed in v1.19.0")
|
||||
sec.Key("PROTOCOL").SetValue("smtp+starttls")
|
||||
}
|
||||
|
||||
// Set default values & validate
|
||||
sec.Key("NAME").MustString(AppName)
|
||||
sec.Key("PROTOCOL").In("", []string{"smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy"})
|
||||
sec.Key("ENABLE_HELO").MustBool(true)
|
||||
sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(false)
|
||||
sec.Key("USE_CLIENT_CERT").MustBool(false)
|
||||
sec.Key("SENDMAIL_PATH").MustString("sendmail")
|
||||
sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute)
|
||||
sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(true)
|
||||
sec.Key("FROM").MustString(sec.Key("USER").String())
|
||||
|
||||
// Now map the values on to the MailService
|
||||
MailService = &Mailer{}
|
||||
if err := sec.MapTo(MailService); err != nil {
|
||||
log.Fatal("Unable to map [mailer] section on to MailService. Error: %v", err)
|
||||
}
|
||||
|
||||
overrideHeader := rootCfg.Section("mailer.override_header").Keys()
|
||||
MailService.OverrideHeader = make(map[string][]string)
|
||||
for _, key := range overrideHeader {
|
||||
MailService.OverrideHeader[key.Name()] = key.Strings(",")
|
||||
}
|
||||
|
||||
// Infer SMTPPort if not set
|
||||
if MailService.SMTPPort == "" {
|
||||
switch MailService.Protocol {
|
||||
case "smtp":
|
||||
MailService.SMTPPort = "25"
|
||||
case "smtps":
|
||||
MailService.SMTPPort = "465"
|
||||
case "smtp+starttls":
|
||||
MailService.SMTPPort = "587"
|
||||
}
|
||||
}
|
||||
|
||||
// Infer Protocol
|
||||
if MailService.Protocol == "" {
|
||||
if strings.ContainsAny(MailService.SMTPAddr, "/\\") {
|
||||
MailService.Protocol = "smtp+unix"
|
||||
} else {
|
||||
switch MailService.SMTPPort {
|
||||
case "25":
|
||||
MailService.Protocol = "smtp"
|
||||
case "465":
|
||||
MailService.Protocol = "smtps"
|
||||
case "587":
|
||||
MailService.Protocol = "smtp+starttls"
|
||||
default:
|
||||
log.Error("unable to infer unspecified mailer.PROTOCOL from mailer.SMTP_PORT = %q, assume using smtps", MailService.SMTPPort)
|
||||
MailService.Protocol = "smtps"
|
||||
if MailService.SMTPPort == "" {
|
||||
MailService.SMTPPort = "465"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we want to warn if users use SMTP on a non-local IP;
|
||||
// we might as well take the opportunity to check that it has an IP at all
|
||||
// This check is not needed for sendmail
|
||||
switch MailService.Protocol {
|
||||
case "sendmail":
|
||||
var err error
|
||||
MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String())
|
||||
if err != nil {
|
||||
log.Error("Failed to parse Sendmail args: '%s' with error %v", sec.Key("SENDMAIL_ARGS").String(), err)
|
||||
}
|
||||
case "smtp", "smtps", "smtp+starttls", "smtp+unix":
|
||||
ips := tryResolveAddr(MailService.SMTPAddr)
|
||||
if MailService.Protocol == "smtp" {
|
||||
for _, ip := range ips {
|
||||
if !ip.IP.IsLoopback() {
|
||||
log.Warn("connecting over insecure SMTP protocol to non-local address is not recommended")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case "dummy": // just mention and do nothing
|
||||
}
|
||||
|
||||
if MailService.From != "" {
|
||||
parsed, err := mail.ParseAddress(MailService.From)
|
||||
if err != nil {
|
||||
log.Fatal("Invalid mailer.FROM (%s): %v", MailService.From, err)
|
||||
}
|
||||
MailService.FromName = parsed.Name
|
||||
MailService.FromEmail = parsed.Address
|
||||
} else {
|
||||
log.Error("no mailer.FROM provided, email system may not work.")
|
||||
}
|
||||
|
||||
MailService.FromDisplayNameFormatTemplate, _ = template.New("mailFrom").Parse("{{ .DisplayName }}")
|
||||
if MailService.FromDisplayNameFormat != "" {
|
||||
template, err := template.New("mailFrom").Parse(MailService.FromDisplayNameFormat)
|
||||
if err != nil {
|
||||
log.Error("mailer.FROM_DISPLAY_NAME_FORMAT is no valid template: %v", err)
|
||||
} else {
|
||||
MailService.FromDisplayNameFormatTemplate = template
|
||||
}
|
||||
}
|
||||
|
||||
switch MailService.EnvelopeFrom {
|
||||
case "":
|
||||
MailService.OverrideEnvelopeFrom = false
|
||||
case "<>":
|
||||
MailService.EnvelopeFrom = ""
|
||||
MailService.OverrideEnvelopeFrom = true
|
||||
default:
|
||||
parsed, err := mail.ParseAddress(MailService.EnvelopeFrom)
|
||||
if err != nil {
|
||||
log.Fatal("Invalid mailer.ENVELOPE_FROM (%s): %v", MailService.EnvelopeFrom, err)
|
||||
}
|
||||
MailService.OverrideEnvelopeFrom = true
|
||||
MailService.EnvelopeFrom = parsed.Address
|
||||
}
|
||||
}
|
||||
|
||||
func loadRegisterMailFrom(rootCfg ConfigProvider) {
|
||||
if !rootCfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").MustBool() {
|
||||
return
|
||||
} else if MailService == nil {
|
||||
log.Warn("Register Mail Service: Mail Service is not enabled")
|
||||
return
|
||||
}
|
||||
Service.RegisterEmailConfirm = true
|
||||
}
|
||||
|
||||
func loadNotifyMailFrom(rootCfg ConfigProvider) {
|
||||
if !rootCfg.Section("service").Key("ENABLE_NOTIFY_MAIL").MustBool() {
|
||||
return
|
||||
} else if MailService == nil {
|
||||
log.Warn("Notify Mail Service: Mail Service is not enabled")
|
||||
return
|
||||
}
|
||||
Service.EnableNotifyMail = true
|
||||
}
|
||||
|
||||
func tryResolveAddr(addr string) []net.IPAddr {
|
||||
if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") {
|
||||
addr = addr[1 : len(addr)-1]
|
||||
}
|
||||
ip := net.ParseIP(addr)
|
||||
if ip != nil {
|
||||
return []net.IPAddr{{IP: ip}}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
ips, err := net.DefaultResolver.LookupIPAddr(ctx, addr)
|
||||
if err != nil {
|
||||
log.Warn("could not look up mailer.SMTP_ADDR: %v", err)
|
||||
return nil
|
||||
}
|
||||
return ips
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_loadMailerFrom(t *testing.T) {
|
||||
kases := map[string]*Mailer{
|
||||
"smtp.mydomain.test": {
|
||||
SMTPAddr: "smtp.mydomain.test",
|
||||
SMTPPort: "465",
|
||||
},
|
||||
"smtp.mydomain.test:123": {
|
||||
SMTPAddr: "smtp.mydomain.test",
|
||||
SMTPPort: "123",
|
||||
},
|
||||
":123": {
|
||||
SMTPAddr: "127.0.0.1",
|
||||
SMTPPort: "123",
|
||||
},
|
||||
}
|
||||
for host, kase := range kases {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
cfg, _ := NewConfigProviderFromData("")
|
||||
sec := cfg.Section("mailer")
|
||||
sec.NewKey("ENABLED", "true")
|
||||
sec.NewKey("HOST", host)
|
||||
|
||||
// Check mailer setting
|
||||
loadMailerFrom(cfg)
|
||||
|
||||
assert.Equal(t, kase.SMTPAddr, MailService.SMTPAddr)
|
||||
assert.Equal(t, kase.SMTPPort, MailService.SMTPPort)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSettingsForInstallMailServiceFlags(t *testing.T) {
|
||||
defer test.MockVariableValue(&Service)()
|
||||
defer test.MockVariableValue(&MailService)()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[database]
|
||||
DB_TYPE = postgres
|
||||
|
||||
[mailer]
|
||||
ENABLED = true
|
||||
SMTP_ADDR = 127.0.0.1
|
||||
SMTP_PORT = 465
|
||||
FROM = noreply@example.com
|
||||
|
||||
[service]
|
||||
REGISTER_EMAIL_CONFIRM = true
|
||||
ENABLE_NOTIFY_MAIL = true
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadDBSetting(cfg)
|
||||
loadServiceFrom(cfg)
|
||||
loadMailsFrom(cfg)
|
||||
|
||||
assert.True(t, Service.RegisterEmailConfirm)
|
||||
assert.True(t, Service.EnableNotifyMail)
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// ExternalMarkupRenderers represents the external markup renderers
|
||||
var (
|
||||
ExternalMarkupRenderers []*MarkupRenderer
|
||||
ExternalSanitizerRules []MarkupSanitizerRule
|
||||
MermaidMaxSourceCharacters int
|
||||
)
|
||||
|
||||
const (
|
||||
RenderContentModeSanitized = "sanitized"
|
||||
RenderContentModeNoSanitizer = "no-sanitizer"
|
||||
RenderContentModeIframe = "iframe"
|
||||
)
|
||||
|
||||
type MarkdownRenderOptions struct {
|
||||
NewLineHardBreak bool
|
||||
ShortIssuePattern bool // Actually it is a "markup" option because it is used in "post processor"
|
||||
}
|
||||
|
||||
type MarkdownMathCodeBlockOptions struct {
|
||||
ParseInlineDollar bool
|
||||
ParseInlineParentheses bool
|
||||
ParseBlockDollar bool
|
||||
ParseBlockSquareBrackets bool
|
||||
}
|
||||
|
||||
// Markdown settings
|
||||
var Markdown = struct {
|
||||
RenderOptionsComment MarkdownRenderOptions `ini:"-"`
|
||||
RenderOptionsWiki MarkdownRenderOptions `ini:"-"`
|
||||
RenderOptionsRepoFile MarkdownRenderOptions `ini:"-"`
|
||||
|
||||
CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` // Actually it is a "markup" option because it is used in "post processor"
|
||||
FileNamePatterns []string `ini:"-"`
|
||||
|
||||
EnableMath bool
|
||||
MathCodeBlockDetection []string
|
||||
MathCodeBlockOptions MarkdownMathCodeBlockOptions `ini:"-"`
|
||||
}{
|
||||
EnableMath: true,
|
||||
}
|
||||
|
||||
// MarkupRenderer defines the external parser configured in ini
|
||||
type MarkupRenderer struct {
|
||||
MarkupName string
|
||||
Command string
|
||||
FilePatterns []string
|
||||
IsInputFile bool
|
||||
NeedPostProcess bool
|
||||
MarkupSanitizerRules []MarkupSanitizerRule
|
||||
RenderContentMode string
|
||||
RenderContentSandbox string
|
||||
}
|
||||
|
||||
// MarkupSanitizerRule defines the policy for whitelisting attributes on
|
||||
// certain elements.
|
||||
type MarkupSanitizerRule struct {
|
||||
Element string
|
||||
AllowAttr string
|
||||
Regexp string
|
||||
AllowDataURIImages bool
|
||||
}
|
||||
|
||||
func loadMarkupFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "markdown", &Markdown)
|
||||
|
||||
markdownFileExtensions := rootCfg.Section("markdown").Key("FILE_EXTENSIONS").Strings(",")
|
||||
if len(markdownFileExtensions) == 0 || len(markdownFileExtensions) == 1 && markdownFileExtensions[0] == "" {
|
||||
markdownFileExtensions = []string{".md", ".markdown", ".mdown", ".mkd", ".livemd"}
|
||||
}
|
||||
Markdown.FileNamePatterns = fileExtensionsToPatterns("markdown", markdownFileExtensions)
|
||||
|
||||
const none = "none"
|
||||
|
||||
const renderOptionShortIssuePattern = "short-issue-pattern"
|
||||
const renderOptionNewLineHardBreak = "new-line-hard-break"
|
||||
cfgMarkdown := rootCfg.Section("markdown")
|
||||
parseMarkdownRenderOptions := func(key string, defaults []string) (ret MarkdownRenderOptions) {
|
||||
options := cfgMarkdown.Key(key).Strings(",")
|
||||
options = util.IfEmpty(options, defaults)
|
||||
for _, opt := range options {
|
||||
switch opt {
|
||||
case renderOptionShortIssuePattern:
|
||||
ret.ShortIssuePattern = true
|
||||
case renderOptionNewLineHardBreak:
|
||||
ret.NewLineHardBreak = true
|
||||
case none:
|
||||
ret = MarkdownRenderOptions{}
|
||||
case "":
|
||||
default:
|
||||
log.Error("Unknown markdown render option in %s: %s", key, opt)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
Markdown.RenderOptionsComment = parseMarkdownRenderOptions("RENDER_OPTIONS_COMMENT", []string{renderOptionShortIssuePattern, renderOptionNewLineHardBreak})
|
||||
Markdown.RenderOptionsWiki = parseMarkdownRenderOptions("RENDER_OPTIONS_WIKI", []string{renderOptionShortIssuePattern})
|
||||
Markdown.RenderOptionsRepoFile = parseMarkdownRenderOptions("RENDER_OPTIONS_REPO_FILE", nil)
|
||||
|
||||
const mathCodeInlineDollar = "inline-dollar"
|
||||
const mathCodeInlineParentheses = "inline-parentheses"
|
||||
const mathCodeBlockDollar = "block-dollar"
|
||||
const mathCodeBlockSquareBrackets = "block-square-brackets"
|
||||
Markdown.MathCodeBlockDetection = util.IfEmpty(Markdown.MathCodeBlockDetection, []string{mathCodeInlineDollar, mathCodeBlockDollar})
|
||||
Markdown.MathCodeBlockOptions = MarkdownMathCodeBlockOptions{}
|
||||
for _, s := range Markdown.MathCodeBlockDetection {
|
||||
switch s {
|
||||
case mathCodeInlineDollar:
|
||||
Markdown.MathCodeBlockOptions.ParseInlineDollar = true
|
||||
case mathCodeInlineParentheses:
|
||||
Markdown.MathCodeBlockOptions.ParseInlineParentheses = true
|
||||
case mathCodeBlockDollar:
|
||||
Markdown.MathCodeBlockOptions.ParseBlockDollar = true
|
||||
case mathCodeBlockSquareBrackets:
|
||||
Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets = true
|
||||
case none:
|
||||
Markdown.MathCodeBlockOptions = MarkdownMathCodeBlockOptions{}
|
||||
case "":
|
||||
default:
|
||||
log.Error("Unknown math code block detection option: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(50000)
|
||||
ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10)
|
||||
ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10)
|
||||
|
||||
for _, sec := range rootCfg.Section("markup").ChildSections() {
|
||||
name := strings.TrimPrefix(sec.Name(), "markup.")
|
||||
if name == "" {
|
||||
log.Warn("name is empty, markup " + sec.Name() + "ignored")
|
||||
continue
|
||||
}
|
||||
|
||||
if name == "sanitizer" || strings.HasPrefix(name, "sanitizer.") {
|
||||
newMarkupSanitizer(name, sec)
|
||||
} else {
|
||||
newMarkupRenderer(name, sec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newMarkupSanitizer(name string, sec ConfigSection) {
|
||||
rule, ok := createMarkupSanitizerRule(name, sec)
|
||||
if ok {
|
||||
if after, found := strings.CutPrefix(name, "sanitizer."); found {
|
||||
names := strings.SplitN(after, ".", 2)
|
||||
name = names[0]
|
||||
}
|
||||
for _, renderer := range ExternalMarkupRenderers {
|
||||
if name == renderer.MarkupName {
|
||||
renderer.MarkupSanitizerRules = append(renderer.MarkupSanitizerRules, rule)
|
||||
return
|
||||
}
|
||||
}
|
||||
ExternalSanitizerRules = append(ExternalSanitizerRules, rule)
|
||||
}
|
||||
}
|
||||
|
||||
func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerRule, bool) {
|
||||
var rule MarkupSanitizerRule
|
||||
|
||||
ok := false
|
||||
if sec.HasKey("ALLOW_DATA_URI_IMAGES") {
|
||||
rule.AllowDataURIImages = sec.Key("ALLOW_DATA_URI_IMAGES").MustBool(false)
|
||||
ok = true
|
||||
}
|
||||
|
||||
if sec.HasKey("ELEMENT") || sec.HasKey("ALLOW_ATTR") {
|
||||
rule.Element = sec.Key("ELEMENT").Value()
|
||||
rule.AllowAttr = sec.Key("ALLOW_ATTR").Value()
|
||||
|
||||
if rule.Element == "" || rule.AllowAttr == "" {
|
||||
log.Error("Missing required values from markup.%s. Must have ELEMENT and ALLOW_ATTR defined!", name)
|
||||
return rule, false
|
||||
}
|
||||
|
||||
regexpStr := sec.Key("REGEXP").Value()
|
||||
if regexpStr != "" {
|
||||
hasPrefix := strings.HasPrefix(regexpStr, "^")
|
||||
hasSuffix := strings.HasSuffix(regexpStr, "$")
|
||||
if !hasPrefix || !hasSuffix {
|
||||
log.Error("In markup.%s: REGEXP must start with ^ and end with $ to be strict", name)
|
||||
// to avoid breaking existing user configurations and satisfy the strict requirement in addSanitizerRules
|
||||
if !hasPrefix {
|
||||
regexpStr = "^.*" + regexpStr
|
||||
}
|
||||
if !hasSuffix {
|
||||
regexpStr += ".*$"
|
||||
}
|
||||
}
|
||||
_, err := regexp.Compile(regexpStr)
|
||||
if err != nil {
|
||||
log.Error("In markup.%s: REGEXP (%s) failed to compile: %v", name, regexpStr, err)
|
||||
return rule, false
|
||||
}
|
||||
rule.Regexp = regexpStr
|
||||
}
|
||||
|
||||
ok = true
|
||||
}
|
||||
|
||||
if !ok {
|
||||
log.Error("Missing required keys from markup.%s. Must have ELEMENT and ALLOW_ATTR or ALLOW_DATA_URI_IMAGES defined!", name)
|
||||
return rule, false
|
||||
}
|
||||
|
||||
return rule, true
|
||||
}
|
||||
|
||||
var extensionReg = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^(\.[-\w]+)+$`)
|
||||
})
|
||||
|
||||
func fileExtensionsToPatterns(sectionName string, extensions []string) []string {
|
||||
patterns := make([]string, 0, len(extensions))
|
||||
for _, extension := range extensions {
|
||||
if !extensionReg().MatchString(extension) {
|
||||
log.Warn("Config section %s file extension %s is invalid. Extension ignored", sectionName, extension)
|
||||
} else {
|
||||
patterns = append(patterns, "*"+extension)
|
||||
}
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
func newMarkupRenderer(name string, sec ConfigSection) {
|
||||
if !sec.Key("ENABLED").MustBool(false) {
|
||||
return
|
||||
}
|
||||
|
||||
fileNamePatterns := fileExtensionsToPatterns(name, sec.Key("FILE_EXTENSIONS").Strings(","))
|
||||
if len(fileNamePatterns) == 0 {
|
||||
log.Warn("Config section %s file extension is empty, markup render is ignored", name)
|
||||
return
|
||||
}
|
||||
|
||||
command := sec.Key("RENDER_COMMAND").MustString("")
|
||||
if command == "" {
|
||||
log.Warn(" RENDER_COMMAND is empty, markup " + name + " ignored")
|
||||
return
|
||||
}
|
||||
|
||||
if sec.HasKey("DISABLE_SANITIZER") {
|
||||
log.Error("Deprecated setting `[markup.*]` `DISABLE_SANITIZER` present. This fallback will be removed in v1.18.0")
|
||||
}
|
||||
|
||||
renderContentMode := sec.Key("RENDER_CONTENT_MODE").MustString(RenderContentModeSanitized)
|
||||
if !sec.HasKey("RENDER_CONTENT_MODE") && sec.Key("DISABLE_SANITIZER").MustBool(false) {
|
||||
renderContentMode = RenderContentModeNoSanitizer // if only the legacy DISABLE_SANITIZER exists, use it
|
||||
}
|
||||
if renderContentMode != RenderContentModeSanitized &&
|
||||
renderContentMode != RenderContentModeNoSanitizer &&
|
||||
renderContentMode != RenderContentModeIframe {
|
||||
log.Error("invalid RENDER_CONTENT_MODE: %q, default to %q", renderContentMode, RenderContentModeSanitized)
|
||||
renderContentMode = RenderContentModeSanitized
|
||||
}
|
||||
|
||||
// ATTENTION! at the moment, only a safe set like "allow-scripts" are allowed for sandbox mode.
|
||||
// "allow-same-origin" should NEVER be used, it leads to XSS attack: makes the JS in iframe can access parent window's config and send requests with user's credentials.
|
||||
renderContentSandbox := sec.Key("RENDER_CONTENT_SANDBOX").MustString("allow-scripts allow-popups")
|
||||
if renderContentSandbox == "disabled" {
|
||||
renderContentSandbox = ""
|
||||
}
|
||||
|
||||
ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
|
||||
MarkupName: name,
|
||||
FilePatterns: fileNamePatterns,
|
||||
Command: command,
|
||||
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
|
||||
|
||||
RenderContentMode: renderContentMode,
|
||||
RenderContentSandbox: renderContentSandbox,
|
||||
|
||||
// if no sanitizer is needed, no post process is needed
|
||||
NeedPostProcess: sec.Key("NEED_POST_PROCESS").MustBool(renderContentMode == RenderContentModeSanitized),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadMarkup(t *testing.T) {
|
||||
cfg, _ := NewConfigProviderFromData(``)
|
||||
loadMarkupFrom(cfg)
|
||||
assert.Equal(t, MarkdownMathCodeBlockOptions{ParseInlineDollar: true, ParseBlockDollar: true}, Markdown.MathCodeBlockOptions)
|
||||
assert.Equal(t, MarkdownRenderOptions{NewLineHardBreak: true, ShortIssuePattern: true}, Markdown.RenderOptionsComment)
|
||||
assert.Equal(t, MarkdownRenderOptions{ShortIssuePattern: true}, Markdown.RenderOptionsWiki)
|
||||
assert.Equal(t, MarkdownRenderOptions{}, Markdown.RenderOptionsRepoFile)
|
||||
|
||||
t.Run("Math", func(t *testing.T) {
|
||||
cfg, _ = NewConfigProviderFromData(`
|
||||
[markdown]
|
||||
MATH_CODE_BLOCK_DETECTION = none
|
||||
`)
|
||||
loadMarkupFrom(cfg)
|
||||
assert.Equal(t, MarkdownMathCodeBlockOptions{}, Markdown.MathCodeBlockOptions)
|
||||
|
||||
cfg, _ = NewConfigProviderFromData(`
|
||||
[markdown]
|
||||
MATH_CODE_BLOCK_DETECTION = inline-dollar, inline-parentheses, block-dollar, block-square-brackets
|
||||
`)
|
||||
loadMarkupFrom(cfg)
|
||||
assert.Equal(t, MarkdownMathCodeBlockOptions{ParseInlineDollar: true, ParseInlineParentheses: true, ParseBlockDollar: true, ParseBlockSquareBrackets: true}, Markdown.MathCodeBlockOptions)
|
||||
})
|
||||
|
||||
t.Run("Render", func(t *testing.T) {
|
||||
cfg, _ = NewConfigProviderFromData(`
|
||||
[markdown]
|
||||
RENDER_OPTIONS_COMMENT = none
|
||||
`)
|
||||
loadMarkupFrom(cfg)
|
||||
assert.Equal(t, MarkdownRenderOptions{}, Markdown.RenderOptionsComment)
|
||||
|
||||
cfg, _ = NewConfigProviderFromData(`
|
||||
[markdown]
|
||||
RENDER_OPTIONS_REPO_FILE = short-issue-pattern, new-line-hard-break
|
||||
`)
|
||||
loadMarkupFrom(cfg)
|
||||
assert.Equal(t, MarkdownRenderOptions{NewLineHardBreak: true, ShortIssuePattern: true}, Markdown.RenderOptionsRepoFile)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// Metrics settings
|
||||
var Metrics = struct {
|
||||
Enabled bool
|
||||
Token string
|
||||
EnabledIssueByLabel bool
|
||||
EnabledIssueByRepository bool
|
||||
}{
|
||||
Enabled: false,
|
||||
Token: "",
|
||||
EnabledIssueByLabel: false,
|
||||
EnabledIssueByRepository: false,
|
||||
}
|
||||
|
||||
func loadMetricsFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "metrics", &Metrics)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// Migrations settings
|
||||
var Migrations = struct {
|
||||
MaxAttempts int
|
||||
RetryBackoff int
|
||||
AllowedDomains string
|
||||
BlockedDomains string
|
||||
AllowLocalNetworks bool
|
||||
SkipTLSVerify bool
|
||||
}{
|
||||
MaxAttempts: 3,
|
||||
RetryBackoff: 3,
|
||||
}
|
||||
|
||||
func loadMigrationsFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("migrations")
|
||||
Migrations.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(Migrations.MaxAttempts)
|
||||
Migrations.RetryBackoff = sec.Key("RETRY_BACKOFF").MustInt(Migrations.RetryBackoff)
|
||||
|
||||
Migrations.AllowedDomains = sec.Key("ALLOWED_DOMAINS").MustString("")
|
||||
Migrations.BlockedDomains = sec.Key("BLOCKED_DOMAINS").MustString("")
|
||||
Migrations.AllowLocalNetworks = sec.Key("ALLOW_LOCALNETWORKS").MustBool(false)
|
||||
Migrations.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool(false)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import "strings"
|
||||
|
||||
// MimeTypeMap defines custom mime type mapping settings
|
||||
var MimeTypeMap = struct {
|
||||
Enabled bool
|
||||
Map map[string]string
|
||||
}{
|
||||
Enabled: false,
|
||||
Map: map[string]string{},
|
||||
}
|
||||
|
||||
func loadMimeTypeMapFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("repository.mimetype_mapping")
|
||||
keys := sec.Keys()
|
||||
m := make(map[string]string, len(keys))
|
||||
for _, key := range keys {
|
||||
m[strings.ToLower(key.Name())] = key.Value()
|
||||
}
|
||||
MimeTypeMap.Map = m
|
||||
if len(keys) > 0 {
|
||||
MimeTypeMap.Enabled = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// Mirror settings
|
||||
var Mirror = struct {
|
||||
Enabled bool
|
||||
DisableNewPull bool
|
||||
DisableNewPush bool
|
||||
DefaultInterval time.Duration
|
||||
MinInterval time.Duration
|
||||
}{
|
||||
Enabled: true,
|
||||
DisableNewPull: false,
|
||||
DisableNewPush: false,
|
||||
MinInterval: 10 * time.Minute,
|
||||
DefaultInterval: 8 * time.Hour,
|
||||
}
|
||||
|
||||
func loadMirrorFrom(rootCfg ConfigProvider) {
|
||||
// Handle old configuration through `[repository]` `DISABLE_MIRRORS`
|
||||
// - please note this was badly named and only disabled the creation of new pull mirrors
|
||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
||||
// if these are removed, the warning will not be shown
|
||||
deprecatedSetting(rootCfg, "repository", "DISABLE_MIRRORS", "mirror", "ENABLED", "v1.19.0")
|
||||
if ConfigSectionKeyBool(rootCfg.Section("repository"), "DISABLE_MIRRORS") {
|
||||
Mirror.DisableNewPull = true
|
||||
}
|
||||
|
||||
if err := rootCfg.Section("mirror").MapTo(&Mirror); err != nil {
|
||||
log.Fatal("Failed to map Mirror settings: %v", err)
|
||||
}
|
||||
|
||||
if !Mirror.Enabled {
|
||||
Mirror.DisableNewPull = true
|
||||
Mirror.DisableNewPush = true
|
||||
}
|
||||
|
||||
if Mirror.MinInterval.Minutes() < 1 {
|
||||
log.Warn("Mirror.MinInterval is too low, set to 1 minute")
|
||||
Mirror.MinInterval = 1 * time.Minute
|
||||
}
|
||||
if Mirror.DefaultInterval < Mirror.MinInterval {
|
||||
Mirror.DefaultInterval = max(time.Hour*8, Mirror.MinInterval)
|
||||
log.Warn("Mirror.DefaultInterval is less than Mirror.MinInterval, set to %s", Mirror.DefaultInterval.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"math"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
"gitea.dev/modules/generate"
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// OAuth2UsernameType is enum describing the way gitea generates its 'username' from oauth2 data
|
||||
type OAuth2UsernameType string
|
||||
|
||||
const (
|
||||
OAuth2UsernameUserid OAuth2UsernameType = "userid" // use user id (sub) field as gitea's username
|
||||
OAuth2UsernameNickname OAuth2UsernameType = "nickname" // use nickname field
|
||||
OAuth2UsernameEmail OAuth2UsernameType = "email" // use email field
|
||||
OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username" // use preferred_username field
|
||||
)
|
||||
|
||||
func (username OAuth2UsernameType) isValid() bool {
|
||||
switch username {
|
||||
case OAuth2UsernameUserid, OAuth2UsernameNickname, OAuth2UsernameEmail, OAuth2UsernamePreferredUsername:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OAuth2AccountLinkingType is enum describing behaviour of linking with existing account
|
||||
type OAuth2AccountLinkingType string
|
||||
|
||||
const (
|
||||
// OAuth2AccountLinkingDisabled error will be displayed if account exist
|
||||
OAuth2AccountLinkingDisabled OAuth2AccountLinkingType = "disabled"
|
||||
// OAuth2AccountLinkingLogin account linking login will be displayed if account exist
|
||||
OAuth2AccountLinkingLogin OAuth2AccountLinkingType = "login"
|
||||
// OAuth2AccountLinkingAuto account will be automatically linked if account exist
|
||||
OAuth2AccountLinkingAuto OAuth2AccountLinkingType = "auto"
|
||||
)
|
||||
|
||||
func (accountLinking OAuth2AccountLinkingType) isValid() bool {
|
||||
switch accountLinking {
|
||||
case OAuth2AccountLinkingDisabled, OAuth2AccountLinkingLogin, OAuth2AccountLinkingAuto:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OAuth2Client settings
|
||||
var OAuth2Client struct {
|
||||
RegisterEmailConfirm bool
|
||||
OpenIDConnectScopes []string
|
||||
EnableAutoRegistration bool
|
||||
Username OAuth2UsernameType
|
||||
UpdateAvatar bool
|
||||
AccountLinking OAuth2AccountLinkingType
|
||||
}
|
||||
|
||||
func loadOAuth2ClientFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("oauth2_client")
|
||||
OAuth2Client.RegisterEmailConfirm = sec.Key("REGISTER_EMAIL_CONFIRM").MustBool(Service.RegisterEmailConfirm)
|
||||
OAuth2Client.OpenIDConnectScopes = parseScopes(sec, "OPENID_CONNECT_SCOPES")
|
||||
OAuth2Client.EnableAutoRegistration = sec.Key("ENABLE_AUTO_REGISTRATION").MustBool()
|
||||
OAuth2Client.Username = OAuth2UsernameType(sec.Key("USERNAME").MustString(string(OAuth2UsernameNickname)))
|
||||
if !OAuth2Client.Username.isValid() {
|
||||
OAuth2Client.Username = OAuth2UsernameNickname
|
||||
log.Warn("[oauth2_client].USERNAME setting is invalid, falls back to %q", OAuth2Client.Username)
|
||||
}
|
||||
OAuth2Client.UpdateAvatar = sec.Key("UPDATE_AVATAR").MustBool()
|
||||
OAuth2Client.AccountLinking = OAuth2AccountLinkingType(sec.Key("ACCOUNT_LINKING").MustString(string(OAuth2AccountLinkingLogin)))
|
||||
if !OAuth2Client.AccountLinking.isValid() {
|
||||
log.Warn("Account linking setting is not valid: '%s', will fallback to '%s'", OAuth2Client.AccountLinking, OAuth2AccountLinkingLogin)
|
||||
OAuth2Client.AccountLinking = OAuth2AccountLinkingLogin
|
||||
}
|
||||
}
|
||||
|
||||
func parseScopes(sec ConfigSection, name string) []string {
|
||||
parts := sec.Key(name).Strings(" ")
|
||||
scopes := make([]string, 0, len(parts))
|
||||
for _, scope := range parts {
|
||||
if scope != "" {
|
||||
scopes = append(scopes, scope)
|
||||
}
|
||||
}
|
||||
return scopes
|
||||
}
|
||||
|
||||
var OAuth2 = struct {
|
||||
Enabled bool
|
||||
AccessTokenExpirationTime int64
|
||||
RefreshTokenExpirationTime int64
|
||||
InvalidateRefreshTokens bool
|
||||
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
|
||||
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
|
||||
JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"`
|
||||
MaxTokenLength int
|
||||
DefaultApplications []string
|
||||
CustomSchemes []string
|
||||
}{
|
||||
Enabled: true,
|
||||
AccessTokenExpirationTime: 3600,
|
||||
RefreshTokenExpirationTime: 730,
|
||||
InvalidateRefreshTokens: false,
|
||||
JWTSigningAlgorithm: "RS256",
|
||||
JWTSigningPrivateKeyFile: "jwt/private.pem",
|
||||
MaxTokenLength: math.MaxInt16,
|
||||
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"},
|
||||
}
|
||||
|
||||
func loadOAuth2From(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("oauth2")
|
||||
if err := sec.MapTo(&OAuth2); err != nil {
|
||||
log.Fatal("Failed to map OAuth2 settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if sec.HasKey("DEFAULT_APPLICATIONS") && sec.Key("DEFAULT_APPLICATIONS").String() == "" {
|
||||
OAuth2.DefaultApplications = nil
|
||||
}
|
||||
|
||||
// Handle the rename of ENABLE to ENABLED
|
||||
deprecatedSetting(rootCfg, "oauth2", "ENABLE", "oauth2", "ENABLED", "v1.23.0")
|
||||
if sec.HasKey("ENABLE") && !sec.HasKey("ENABLED") {
|
||||
OAuth2.Enabled = sec.Key("ENABLE").MustBool(OAuth2.Enabled)
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
|
||||
OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
|
||||
}
|
||||
|
||||
// FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET"
|
||||
// Because this secret is also used as GeneralTokenSigningSecret (as a quick not-that-breaking fix for some legacy problems).
|
||||
// Including: account validation token, etc ...
|
||||
// In main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
|
||||
jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
|
||||
if InstallLock {
|
||||
jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
|
||||
if err != nil {
|
||||
jwtSecretBytes, jwtSecretBase64 = generate.NewJwtSecretWithBase64()
|
||||
saveCfg, err := rootCfg.PrepareSaving()
|
||||
if err != nil {
|
||||
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
|
||||
}
|
||||
rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
if err := saveCfg.Save(); err != nil {
|
||||
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
|
||||
}
|
||||
}
|
||||
generalSigningSecret.Store(&jwtSecretBytes)
|
||||
}
|
||||
}
|
||||
|
||||
var generalSigningSecret atomic.Pointer[[]byte]
|
||||
|
||||
func GetGeneralTokenSigningSecret() []byte {
|
||||
old := generalSigningSecret.Load()
|
||||
if old == nil || len(*old) == 0 {
|
||||
jwtSecret, _ := generate.NewJwtSecretWithBase64()
|
||||
if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
|
||||
return jwtSecret
|
||||
}
|
||||
return *generalSigningSecret.Load()
|
||||
}
|
||||
return *old
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/generate"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetGeneralSigningSecret(t *testing.T) {
|
||||
// when there is no general signing secret, it should be generated, and keep the same value
|
||||
generalSigningSecret.Store(nil)
|
||||
s1 := GetGeneralTokenSigningSecret()
|
||||
assert.NotNil(t, s1)
|
||||
s2 := GetGeneralTokenSigningSecret()
|
||||
assert.Equal(t, s1, s2)
|
||||
|
||||
// the config value should always override any pre-generated value
|
||||
cfg, _ := NewConfigProviderFromData(`
|
||||
[oauth2]
|
||||
JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||
`)
|
||||
defer test.MockVariableValue(&InstallLock, true)()
|
||||
loadOAuth2From(cfg)
|
||||
actual := GetGeneralTokenSigningSecret()
|
||||
expected, _ := generate.DecodeJwtSecretBase64("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
|
||||
assert.Len(t, actual, 32)
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestGetGeneralSigningSecretSave(t *testing.T) {
|
||||
defer test.MockVariableValue(&InstallLock, true)()
|
||||
|
||||
old := GetGeneralTokenSigningSecret()
|
||||
assert.Len(t, old, 32)
|
||||
|
||||
tmpFile := t.TempDir() + "/app.ini"
|
||||
_ = os.WriteFile(tmpFile, nil, 0o644)
|
||||
cfg, _ := NewConfigProviderFromFile(tmpFile)
|
||||
loadOAuth2From(cfg)
|
||||
generated := GetGeneralTokenSigningSecret()
|
||||
assert.Len(t, generated, 32)
|
||||
assert.NotEqual(t, old, generated)
|
||||
|
||||
generalSigningSecret.Store(nil)
|
||||
cfg, _ = NewConfigProviderFromFile(tmpFile)
|
||||
loadOAuth2From(cfg)
|
||||
again := GetGeneralTokenSigningSecret()
|
||||
assert.Equal(t, generated, again)
|
||||
|
||||
iniContent, err := os.ReadFile(tmpFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(iniContent), "JWT_SECRET = ")
|
||||
}
|
||||
|
||||
func TestOauth2DefaultApplications(t *testing.T) {
|
||||
cfg, _ := NewConfigProviderFromData(``)
|
||||
loadOAuth2From(cfg)
|
||||
assert.Equal(t, []string{"git-credential-oauth", "git-credential-manager", "tea"}, OAuth2.DefaultApplications)
|
||||
|
||||
cfg, _ = NewConfigProviderFromData(`[oauth2]
|
||||
DEFAULT_APPLICATIONS = tea
|
||||
`)
|
||||
loadOAuth2From(cfg)
|
||||
assert.Equal(t, []string{"tea"}, OAuth2.DefaultApplications)
|
||||
|
||||
cfg, _ = NewConfigProviderFromData(`[oauth2]
|
||||
DEFAULT_APPLICATIONS =
|
||||
`)
|
||||
loadOAuth2From(cfg)
|
||||
assert.Nil(t, OAuth2.DefaultApplications)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import "gitea.dev/modules/log"
|
||||
|
||||
type OtherConfig struct {
|
||||
ShowFooterVersion bool
|
||||
ShowFooterTemplateLoadTime bool
|
||||
ShowFooterPoweredBy bool
|
||||
EnableFeed bool
|
||||
EnableSitemap bool
|
||||
}
|
||||
|
||||
var Other = OtherConfig{
|
||||
ShowFooterVersion: true,
|
||||
ShowFooterTemplateLoadTime: true,
|
||||
ShowFooterPoweredBy: true,
|
||||
EnableSitemap: true,
|
||||
EnableFeed: true,
|
||||
}
|
||||
|
||||
func loadOtherFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("other")
|
||||
if err := sec.MapTo(&Other); err != nil {
|
||||
log.Fatal("Failed to map [other] settings: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
// Package registry settings
|
||||
var (
|
||||
Packages = struct {
|
||||
Storage *Storage
|
||||
Enabled bool
|
||||
|
||||
LimitTotalOwnerCount int64
|
||||
LimitTotalOwnerSize int64
|
||||
LimitSizeAlpine int64
|
||||
LimitSizeArch int64
|
||||
LimitSizeCargo int64
|
||||
LimitSizeChef int64
|
||||
LimitSizeComposer int64
|
||||
LimitSizeConan int64
|
||||
LimitSizeConda int64
|
||||
LimitSizeContainer int64
|
||||
LimitSizeCran int64
|
||||
LimitSizeDebian int64
|
||||
LimitSizeGeneric int64
|
||||
LimitSizeGo int64
|
||||
LimitSizeHelm int64
|
||||
LimitSizeMaven int64
|
||||
LimitSizeNpm int64
|
||||
LimitSizeNuGet int64
|
||||
LimitSizePub int64
|
||||
LimitSizePyPI int64
|
||||
LimitSizeRpm int64
|
||||
LimitSizeRubyGems int64
|
||||
LimitSizeSwift int64
|
||||
LimitSizeTerraformState int64
|
||||
LimitSizeVagrant int64
|
||||
|
||||
DefaultRPMSignEnabled bool
|
||||
}{
|
||||
Enabled: true,
|
||||
LimitTotalOwnerCount: -1,
|
||||
}
|
||||
)
|
||||
|
||||
func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
|
||||
sec, _ := rootCfg.GetSection("packages")
|
||||
if sec == nil {
|
||||
Packages.Storage, err = getStorage(rootCfg, "packages", "", nil)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = sec.MapTo(&Packages); err != nil {
|
||||
return fmt.Errorf("failed to map Packages settings: %v", err)
|
||||
}
|
||||
|
||||
Packages.Storage, err = getStorage(rootCfg, "packages", "", sec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
|
||||
Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE")
|
||||
Packages.LimitSizeArch = mustBytes(sec, "LIMIT_SIZE_ARCH")
|
||||
Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO")
|
||||
Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF")
|
||||
Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER")
|
||||
Packages.LimitSizeConan = mustBytes(sec, "LIMIT_SIZE_CONAN")
|
||||
Packages.LimitSizeConda = mustBytes(sec, "LIMIT_SIZE_CONDA")
|
||||
Packages.LimitSizeContainer = mustBytes(sec, "LIMIT_SIZE_CONTAINER")
|
||||
Packages.LimitSizeCran = mustBytes(sec, "LIMIT_SIZE_CRAN")
|
||||
Packages.LimitSizeDebian = mustBytes(sec, "LIMIT_SIZE_DEBIAN")
|
||||
Packages.LimitSizeGeneric = mustBytes(sec, "LIMIT_SIZE_GENERIC")
|
||||
Packages.LimitSizeGo = mustBytes(sec, "LIMIT_SIZE_GO")
|
||||
Packages.LimitSizeHelm = mustBytes(sec, "LIMIT_SIZE_HELM")
|
||||
Packages.LimitSizeMaven = mustBytes(sec, "LIMIT_SIZE_MAVEN")
|
||||
Packages.LimitSizeNpm = mustBytes(sec, "LIMIT_SIZE_NPM")
|
||||
Packages.LimitSizeNuGet = mustBytes(sec, "LIMIT_SIZE_NUGET")
|
||||
Packages.LimitSizePub = mustBytes(sec, "LIMIT_SIZE_PUB")
|
||||
Packages.LimitSizePyPI = mustBytes(sec, "LIMIT_SIZE_PYPI")
|
||||
Packages.LimitSizeRpm = mustBytes(sec, "LIMIT_SIZE_RPM")
|
||||
Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
|
||||
Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
|
||||
Packages.LimitSizeTerraformState = mustBytes(sec, "LIMIT_SIZE_TERRAFORM_STATE")
|
||||
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
|
||||
Packages.DefaultRPMSignEnabled = sec.Key("DEFAULT_RPM_SIGN_ENABLED").MustBool(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustBytes(section ConfigSection, key string) int64 {
|
||||
const noLimit = "-1"
|
||||
|
||||
value := section.Key(key).MustString(noLimit)
|
||||
if value == noLimit {
|
||||
return -1
|
||||
}
|
||||
bytes, err := humanize.ParseBytes(value)
|
||||
if err != nil || bytes > math.MaxInt64 {
|
||||
return -1
|
||||
}
|
||||
return int64(bytes)
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMustBytes(t *testing.T) {
|
||||
test := func(value string) int64 {
|
||||
cfg, err := NewConfigProviderFromData("[test]")
|
||||
assert.NoError(t, err)
|
||||
sec := cfg.Section("test")
|
||||
sec.NewKey("VALUE", value)
|
||||
|
||||
return mustBytes(sec, "VALUE")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, -1, test(""))
|
||||
assert.EqualValues(t, -1, test("-1"))
|
||||
assert.EqualValues(t, 0, test("0"))
|
||||
assert.EqualValues(t, 1, test("1"))
|
||||
assert.EqualValues(t, 10000, test("10000"))
|
||||
assert.EqualValues(t, 1000000, test("1 mb"))
|
||||
assert.EqualValues(t, 1048576, test("1mib"))
|
||||
assert.EqualValues(t, 1782579, test("1.7mib"))
|
||||
assert.EqualValues(t, -1, test("1 yib")) // too large
|
||||
}
|
||||
|
||||
func Test_getStorageInheritNameSectionTypeForPackages(t *testing.T) {
|
||||
// packages storage inherits from storage if nothing configured
|
||||
iniStr := `
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadPackagesFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", Packages.Storage.Type)
|
||||
assert.Equal(t, "packages/", Packages.Storage.MinioConfig.BasePath)
|
||||
|
||||
// we can also configure packages storage directly
|
||||
iniStr = `
|
||||
[storage.packages]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadPackagesFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", Packages.Storage.Type)
|
||||
assert.Equal(t, "packages/", Packages.Storage.MinioConfig.BasePath)
|
||||
|
||||
// or we can indicate the storage type in the packages section
|
||||
iniStr = `
|
||||
[packages]
|
||||
STORAGE_TYPE = my_minio
|
||||
|
||||
[storage.my_minio]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadPackagesFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", Packages.Storage.Type)
|
||||
assert.Equal(t, "packages/", Packages.Storage.MinioConfig.BasePath)
|
||||
|
||||
// or we can indicate the storage type and minio base path in the packages section
|
||||
iniStr = `
|
||||
[packages]
|
||||
STORAGE_TYPE = my_minio
|
||||
MINIO_BASE_PATH = my_packages/
|
||||
|
||||
[storage.my_minio]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadPackagesFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", Packages.Storage.Type)
|
||||
assert.Equal(t, "my_packages/", Packages.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_PackageStorage1(t *testing.T) {
|
||||
iniStr := `
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
[packages]
|
||||
MINIO_BASE_PATH = packages/
|
||||
SERVE_DIRECT = true
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ENDPOINT = s3.my-domain.net
|
||||
MINIO_BUCKET = gitea
|
||||
MINIO_LOCATION = homenet
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_ACCESS_KEY_ID = correct_key
|
||||
MINIO_SECRET_ACCESS_KEY = correct_key
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadPackagesFrom(cfg))
|
||||
storage := Packages.Storage
|
||||
|
||||
assert.EqualValues(t, "minio", storage.Type)
|
||||
assert.Equal(t, "gitea", storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "packages/", storage.MinioConfig.BasePath)
|
||||
assert.True(t, storage.MinioConfig.ServeDirect)
|
||||
}
|
||||
|
||||
func Test_PackageStorage2(t *testing.T) {
|
||||
iniStr := `
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
[storage.packages]
|
||||
MINIO_BASE_PATH = packages/
|
||||
SERVE_DIRECT = true
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ENDPOINT = s3.my-domain.net
|
||||
MINIO_BUCKET = gitea
|
||||
MINIO_LOCATION = homenet
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_ACCESS_KEY_ID = correct_key
|
||||
MINIO_SECRET_ACCESS_KEY = correct_key
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadPackagesFrom(cfg))
|
||||
storage := Packages.Storage
|
||||
|
||||
assert.EqualValues(t, "minio", storage.Type)
|
||||
assert.Equal(t, "gitea", storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "packages/", storage.MinioConfig.BasePath)
|
||||
assert.True(t, storage.MinioConfig.ServeDirect)
|
||||
}
|
||||
|
||||
func Test_PackageStorage3(t *testing.T) {
|
||||
iniStr := `
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
[packages]
|
||||
STORAGE_TYPE = my_cfg
|
||||
MINIO_BASE_PATH = my_packages/
|
||||
SERVE_DIRECT = true
|
||||
[storage.my_cfg]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ENDPOINT = s3.my-domain.net
|
||||
MINIO_BUCKET = gitea
|
||||
MINIO_LOCATION = homenet
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_ACCESS_KEY_ID = correct_key
|
||||
MINIO_SECRET_ACCESS_KEY = correct_key
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadPackagesFrom(cfg))
|
||||
storage := Packages.Storage
|
||||
|
||||
assert.EqualValues(t, "minio", storage.Type)
|
||||
assert.Equal(t, "gitea", storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "my_packages/", storage.MinioConfig.BasePath)
|
||||
assert.True(t, storage.MinioConfig.ServeDirect)
|
||||
}
|
||||
|
||||
func Test_PackageStorage4(t *testing.T) {
|
||||
iniStr := `
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
[storage.packages]
|
||||
STORAGE_TYPE = my_cfg
|
||||
MINIO_BASE_PATH = my_packages/
|
||||
SERVE_DIRECT = true
|
||||
[storage.my_cfg]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ENDPOINT = s3.my-domain.net
|
||||
MINIO_BUCKET = gitea
|
||||
MINIO_LOCATION = homenet
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_ACCESS_KEY_ID = correct_key
|
||||
MINIO_SECRET_ACCESS_KEY = correct_key
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadPackagesFrom(cfg))
|
||||
storage := Packages.Storage
|
||||
|
||||
assert.EqualValues(t, "minio", storage.Type)
|
||||
assert.Equal(t, "gitea", storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "my_packages/", storage.MinioConfig.BasePath)
|
||||
assert.True(t, storage.MinioConfig.ServeDirect)
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/tempdir"
|
||||
)
|
||||
|
||||
var (
|
||||
// AppPath represents the path to the gitea binary
|
||||
AppPath string
|
||||
|
||||
// AppWorkPath is the "working directory" of Gitea. It maps to the: WORK_PATH in app.ini, "--work-path" flag, environment variable GITEA_WORK_DIR.
|
||||
// If that is not set it is the default set here by the linker or failing that the directory of AppPath.
|
||||
// It is used as the base path for several other paths.
|
||||
AppWorkPath string
|
||||
CustomPath string // Custom directory path. Env: GITEA_CUSTOM
|
||||
CustomConf string
|
||||
|
||||
appWorkPathBuiltin string
|
||||
customPathBuiltin string
|
||||
customConfBuiltin string
|
||||
|
||||
AppWorkPathMismatch bool
|
||||
)
|
||||
|
||||
func getAppPath() (string, error) {
|
||||
var appPath string
|
||||
var err error
|
||||
if IsWindows && filepath.IsAbs(os.Args[0]) {
|
||||
appPath = filepath.Clean(os.Args[0])
|
||||
} else {
|
||||
appPath, err = exec.LookPath(os.Args[0])
|
||||
}
|
||||
if err != nil {
|
||||
if !errors.Is(err, exec.ErrDot) {
|
||||
return "", err
|
||||
}
|
||||
appPath, err = filepath.Abs(os.Args[0])
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
appPath, err = filepath.Abs(appPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Note: (legacy code) we don't use path.Dir here because it does not handle case which path starts with two "/" in Windows: "//psf/Home/..."
|
||||
return strings.ReplaceAll(appPath, "\\", "/"), err
|
||||
}
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
if AppPath, err = getAppPath(); err != nil {
|
||||
log.Fatal("Failed to get app path: %v", err)
|
||||
}
|
||||
|
||||
if AppWorkPath == "" {
|
||||
AppWorkPath = filepath.Dir(AppPath)
|
||||
}
|
||||
|
||||
appWorkPathBuiltin = AppWorkPath
|
||||
customPathBuiltin = CustomPath
|
||||
customConfBuiltin = CustomConf
|
||||
}
|
||||
|
||||
type ArgWorkPathAndCustomConf struct {
|
||||
WorkPath string
|
||||
CustomPath string
|
||||
CustomConf string
|
||||
}
|
||||
|
||||
type stringWithDefault struct {
|
||||
Value string
|
||||
IsSet bool
|
||||
}
|
||||
|
||||
func (s *stringWithDefault) Set(v string) {
|
||||
s.Value = v
|
||||
s.IsSet = true
|
||||
}
|
||||
|
||||
// InitWorkPathAndCommonConfig will set AppWorkPath, CustomPath and CustomConf, init default config provider by CustomConf and load common settings,
|
||||
func InitWorkPathAndCommonConfig(getEnvFn func(name string) string, args ArgWorkPathAndCustomConf) {
|
||||
InitWorkPathAndCfgProvider(getEnvFn, args)
|
||||
LoadCommonSettings()
|
||||
}
|
||||
|
||||
// InitWorkPathAndCfgProvider will set AppWorkPath, CustomPath and CustomConf, init default config provider by CustomConf
|
||||
func InitWorkPathAndCfgProvider(getEnvFn func(name string) string, args ArgWorkPathAndCustomConf) {
|
||||
tryAbsPath := func(paths ...string) string {
|
||||
s := paths[len(paths)-1]
|
||||
for i := len(paths) - 2; i >= 0; i-- {
|
||||
if filepath.IsAbs(s) {
|
||||
break
|
||||
}
|
||||
s = filepath.Join(paths[i], s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var err error
|
||||
tmpWorkPath := stringWithDefault{Value: appWorkPathBuiltin}
|
||||
if tmpWorkPath.Value == "" {
|
||||
tmpWorkPath.Value = filepath.Dir(AppPath)
|
||||
}
|
||||
tmpCustomPath := stringWithDefault{Value: customPathBuiltin}
|
||||
if tmpCustomPath.Value == "" {
|
||||
tmpCustomPath.Value = "custom"
|
||||
}
|
||||
tmpCustomConf := stringWithDefault{Value: customConfBuiltin}
|
||||
if tmpCustomConf.Value == "" {
|
||||
tmpCustomConf.Value = "conf/app.ini"
|
||||
}
|
||||
|
||||
readFromEnv := func() {
|
||||
envWorkPath := getEnvFn("GITEA_WORK_DIR")
|
||||
if envWorkPath != "" {
|
||||
tmpWorkPath.Set(envWorkPath)
|
||||
if !filepath.IsAbs(tmpWorkPath.Value) {
|
||||
log.Fatal("GITEA_WORK_DIR (work path) must be absolute path")
|
||||
}
|
||||
}
|
||||
|
||||
envCustomPath := getEnvFn("GITEA_CUSTOM")
|
||||
if envCustomPath != "" {
|
||||
tmpCustomPath.Set(envCustomPath)
|
||||
if !filepath.IsAbs(tmpCustomPath.Value) {
|
||||
log.Fatal("GITEA_CUSTOM (custom path) must be absolute path")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readFromArgs := func() {
|
||||
if args.WorkPath != "" {
|
||||
tmpWorkPath.Set(args.WorkPath)
|
||||
if !filepath.IsAbs(tmpWorkPath.Value) {
|
||||
log.Fatal("--work-path must be absolute path")
|
||||
}
|
||||
}
|
||||
if args.CustomPath != "" {
|
||||
tmpCustomPath.Set(args.CustomPath) // if it is not abs, it will be based on work-path, it shouldn't happen
|
||||
if !filepath.IsAbs(tmpCustomPath.Value) {
|
||||
log.Error("--custom-path must be absolute path")
|
||||
}
|
||||
}
|
||||
if args.CustomConf != "" {
|
||||
tmpCustomConf.Set(args.CustomConf)
|
||||
if !filepath.IsAbs(tmpCustomConf.Value) {
|
||||
// the config path can be relative to the real current working path
|
||||
if tmpCustomConf.Value, err = filepath.Abs(tmpCustomConf.Value); err != nil {
|
||||
log.Fatal("Failed to get absolute path of config %q: %v", tmpCustomConf.Value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readFromEnv()
|
||||
readFromArgs()
|
||||
|
||||
if !tmpCustomConf.IsSet {
|
||||
tmpCustomConf.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value, tmpCustomConf.Value))
|
||||
}
|
||||
|
||||
// only read the config but do not load/init anything more, because the AppWorkPath and CustomPath are not ready
|
||||
InitCfgProvider(tmpCustomConf.Value)
|
||||
if HasInstallLock(CfgProvider) {
|
||||
ClearEnvConfigKeys() // if the instance has been installed, do not pass the environment variables to sub-processes
|
||||
}
|
||||
configWorkPath := ConfigSectionKeyString(CfgProvider.Section(""), "WORK_PATH")
|
||||
if configWorkPath != "" {
|
||||
if !filepath.IsAbs(configWorkPath) {
|
||||
log.Fatal("WORK_PATH in %q must be absolute path", configWorkPath)
|
||||
}
|
||||
configWorkPath = filepath.Clean(configWorkPath)
|
||||
if tmpWorkPath.Value != "" && (getEnvFn("GITEA_WORK_DIR") != "" || args.WorkPath != "") {
|
||||
fi1, err1 := os.Stat(tmpWorkPath.Value)
|
||||
fi2, err2 := os.Stat(configWorkPath)
|
||||
if err1 != nil || err2 != nil || !os.SameFile(fi1, fi2) {
|
||||
AppWorkPathMismatch = true
|
||||
}
|
||||
}
|
||||
tmpWorkPath.Set(configWorkPath)
|
||||
}
|
||||
|
||||
tmpCustomPath.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value))
|
||||
|
||||
AppWorkPath = tmpWorkPath.Value
|
||||
CustomPath = tmpCustomPath.Value
|
||||
CustomConf = tmpCustomConf.Value
|
||||
}
|
||||
|
||||
func MockBuiltinPaths(workPath, customPath, customConf string) func() {
|
||||
oldApp, oldCustom, oldConf := appWorkPathBuiltin, customPathBuiltin, customConfBuiltin
|
||||
appWorkPathBuiltin, customPathBuiltin, customConfBuiltin = workPath, customPath, customConf
|
||||
return func() { appWorkPathBuiltin, customPathBuiltin, customConfBuiltin = oldApp, oldCustom, oldConf }
|
||||
}
|
||||
|
||||
// AppDataTempDir returns a managed temporary directory for the application data.
|
||||
// Using empty sub will get the managed base temp directory, and it's safe to delete it.
|
||||
// Gitea only creates subdirectories under it, but not the APP_TEMP_PATH directory itself.
|
||||
// * When APP_TEMP_PATH="/tmp": the managed temp directory is "/tmp/gitea-tmp"
|
||||
// * When APP_TEMP_PATH is not set: the managed temp directory is "/{APP_DATA_PATH}/tmp"
|
||||
func AppDataTempDir(sub string) *tempdir.TempDir {
|
||||
if appTempPathInternal != "" {
|
||||
return tempdir.New(appTempPathInternal, "gitea-tmp/"+sub)
|
||||
}
|
||||
if AppDataPath == "" {
|
||||
panic("setting.AppDataPath is not set")
|
||||
}
|
||||
return tempdir.New(AppDataPath, "tmp/"+sub)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type envVars map[string]string
|
||||
|
||||
func (e envVars) Getenv(key string) string {
|
||||
return e[key]
|
||||
}
|
||||
|
||||
func TestInitWorkPathAndCommonConfig(t *testing.T) {
|
||||
testInit := func(defaultWorkPath, defaultCustomPath, defaultCustomConf string) {
|
||||
AppWorkPathMismatch = false
|
||||
AppWorkPath = defaultWorkPath
|
||||
appWorkPathBuiltin = defaultWorkPath
|
||||
CustomPath = defaultCustomPath
|
||||
customPathBuiltin = defaultCustomPath
|
||||
CustomConf = defaultCustomConf
|
||||
customConfBuiltin = defaultCustomConf
|
||||
}
|
||||
|
||||
fp := filepath.Join
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
dirFoo := fp(tmpDir, "foo")
|
||||
dirBar := fp(tmpDir, "bar")
|
||||
dirXxx := fp(tmpDir, "xxx")
|
||||
dirYyy := fp(tmpDir, "yyy")
|
||||
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
testInit(dirFoo, "", "")
|
||||
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{})
|
||||
assert.Equal(t, dirFoo, AppWorkPath)
|
||||
assert.Equal(t, fp(dirFoo, "custom"), CustomPath)
|
||||
assert.Equal(t, fp(dirFoo, "custom/conf/app.ini"), CustomConf)
|
||||
})
|
||||
|
||||
t.Run("WorkDir(env)", func(t *testing.T) {
|
||||
testInit(dirFoo, "", "")
|
||||
InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{})
|
||||
assert.Equal(t, dirBar, AppWorkPath)
|
||||
assert.Equal(t, fp(dirBar, "custom"), CustomPath)
|
||||
assert.Equal(t, fp(dirBar, "custom/conf/app.ini"), CustomConf)
|
||||
})
|
||||
|
||||
t.Run("WorkDir(env,arg)", func(t *testing.T) {
|
||||
testInit(dirFoo, "", "")
|
||||
InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirXxx})
|
||||
assert.Equal(t, dirXxx, AppWorkPath)
|
||||
assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
|
||||
assert.Equal(t, fp(dirXxx, "custom/conf/app.ini"), CustomConf)
|
||||
})
|
||||
|
||||
t.Run("CustomPath(env)", func(t *testing.T) {
|
||||
testInit(dirFoo, "", "")
|
||||
InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{})
|
||||
assert.Equal(t, dirFoo, AppWorkPath)
|
||||
assert.Equal(t, fp(dirBar, "custom1"), CustomPath)
|
||||
assert.Equal(t, fp(dirBar, "custom1/conf/app.ini"), CustomConf)
|
||||
})
|
||||
|
||||
t.Run("CustomPath(env,arg)", func(t *testing.T) {
|
||||
testInit(dirFoo, "", "")
|
||||
InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{CustomPath: "custom2"})
|
||||
assert.Equal(t, dirFoo, AppWorkPath)
|
||||
assert.Equal(t, fp(dirFoo, "custom2"), CustomPath)
|
||||
assert.Equal(t, fp(dirFoo, "custom2/conf/app.ini"), CustomConf)
|
||||
})
|
||||
|
||||
t.Run("CustomConf", func(t *testing.T) {
|
||||
testInit(dirFoo, "", "")
|
||||
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: "app1.ini"})
|
||||
assert.Equal(t, dirFoo, AppWorkPath)
|
||||
cwd, _ := os.Getwd()
|
||||
assert.Equal(t, fp(cwd, "app1.ini"), CustomConf)
|
||||
|
||||
testInit(dirFoo, "", "")
|
||||
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: fp(dirBar, "app1.ini")})
|
||||
assert.Equal(t, dirFoo, AppWorkPath)
|
||||
assert.Equal(t, fp(dirBar, "app1.ini"), CustomConf)
|
||||
})
|
||||
|
||||
t.Run("CustomConfOverrideWorkPath", func(t *testing.T) {
|
||||
iniWorkPath := fp(tmpDir, "app-workpath.ini")
|
||||
_ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644)
|
||||
|
||||
testInit(dirFoo, "", "")
|
||||
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath})
|
||||
assert.Equal(t, dirXxx, AppWorkPath)
|
||||
assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
|
||||
assert.Equal(t, iniWorkPath, CustomConf)
|
||||
assert.False(t, AppWorkPathMismatch)
|
||||
|
||||
testInit(dirFoo, "", "")
|
||||
InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath})
|
||||
assert.Equal(t, dirXxx, AppWorkPath)
|
||||
assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
|
||||
assert.Equal(t, iniWorkPath, CustomConf)
|
||||
assert.True(t, AppWorkPathMismatch)
|
||||
|
||||
testInit(dirFoo, "", "")
|
||||
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirBar, CustomConf: iniWorkPath})
|
||||
assert.Equal(t, dirXxx, AppWorkPath)
|
||||
assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
|
||||
assert.Equal(t, iniWorkPath, CustomConf)
|
||||
assert.True(t, AppWorkPathMismatch)
|
||||
})
|
||||
|
||||
t.Run("Builtin", func(t *testing.T) {
|
||||
testInit(dirFoo, dirBar, dirXxx)
|
||||
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{})
|
||||
assert.Equal(t, dirFoo, AppWorkPath)
|
||||
assert.Equal(t, dirBar, CustomPath)
|
||||
assert.Equal(t, dirXxx, CustomConf)
|
||||
|
||||
testInit(dirFoo, "custom1", "cfg.ini")
|
||||
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{})
|
||||
assert.Equal(t, dirFoo, AppWorkPath)
|
||||
assert.Equal(t, fp(dirFoo, "custom1"), CustomPath)
|
||||
assert.Equal(t, fp(dirFoo, "custom1/cfg.ini"), CustomConf)
|
||||
|
||||
testInit(dirFoo, "custom1", "cfg.ini")
|
||||
InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirYyy}.Getenv, ArgWorkPathAndCustomConf{})
|
||||
assert.Equal(t, dirYyy, AppWorkPath)
|
||||
assert.Equal(t, fp(dirYyy, "custom1"), CustomPath)
|
||||
assert.Equal(t, fp(dirYyy, "custom1/cfg.ini"), CustomConf)
|
||||
|
||||
testInit(dirFoo, "custom1", "cfg.ini")
|
||||
InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": dirYyy}.Getenv, ArgWorkPathAndCustomConf{})
|
||||
assert.Equal(t, dirFoo, AppWorkPath)
|
||||
assert.Equal(t, dirYyy, CustomPath)
|
||||
assert.Equal(t, fp(dirYyy, "cfg.ini"), CustomConf)
|
||||
|
||||
iniWorkPath := fp(tmpDir, "app-workpath.ini")
|
||||
_ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644)
|
||||
testInit(dirFoo, "custom1", "cfg.ini")
|
||||
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath})
|
||||
assert.Equal(t, dirXxx, AppWorkPath)
|
||||
assert.Equal(t, fp(dirXxx, "custom1"), CustomPath)
|
||||
assert.Equal(t, iniWorkPath, CustomConf)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// Avatar settings
|
||||
|
||||
var (
|
||||
Avatar = struct {
|
||||
Storage *Storage
|
||||
|
||||
MaxWidth int
|
||||
MaxHeight int
|
||||
MaxFileSize int64
|
||||
MaxOriginSize int64
|
||||
RenderedSizeFactor int
|
||||
}{
|
||||
MaxWidth: 4096,
|
||||
MaxHeight: 4096,
|
||||
MaxFileSize: 1048576,
|
||||
MaxOriginSize: 262144,
|
||||
RenderedSizeFactor: 2,
|
||||
}
|
||||
|
||||
GravatarSource string
|
||||
|
||||
RepoAvatar = struct {
|
||||
Storage *Storage
|
||||
|
||||
Fallback string
|
||||
FallbackImage string
|
||||
}{}
|
||||
)
|
||||
|
||||
func loadAvatarsFrom(rootCfg ConfigProvider) error {
|
||||
sec := rootCfg.Section("picture")
|
||||
|
||||
avatarSec := rootCfg.Section("avatar")
|
||||
storageType := sec.Key("AVATAR_STORAGE_TYPE").MustString("")
|
||||
// Specifically default PATH to AVATAR_UPLOAD_PATH
|
||||
avatarSec.Key("PATH").MustString(sec.Key("AVATAR_UPLOAD_PATH").String())
|
||||
|
||||
var err error
|
||||
Avatar.Storage, err = getStorage(rootCfg, "avatars", storageType, avatarSec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Avatar.MaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096)
|
||||
Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(4096)
|
||||
Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576)
|
||||
Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_ORIGIN_SIZE").MustInt64(262144)
|
||||
Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(2)
|
||||
|
||||
switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source {
|
||||
case "duoshuo":
|
||||
GravatarSource = "http://gravatar.duoshuo.com/avatar/"
|
||||
case "gravatar":
|
||||
GravatarSource = "https://secure.gravatar.com/avatar/"
|
||||
case "libravatar":
|
||||
GravatarSource = "https://seccdn.libravatar.org/avatar/"
|
||||
default:
|
||||
GravatarSource = source
|
||||
}
|
||||
|
||||
deprecatedSettingDB(rootCfg, "picture", "DISABLE_GRAVATAR")
|
||||
deprecatedSettingDB(rootCfg, "picture", "ENABLE_FEDERATED_AVATAR")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadRepoAvatarFrom(rootCfg ConfigProvider) error {
|
||||
sec := rootCfg.Section("picture")
|
||||
|
||||
repoAvatarSec := rootCfg.Section("repo-avatar")
|
||||
storageType := sec.Key("REPOSITORY_AVATAR_STORAGE_TYPE").MustString("")
|
||||
// Specifically default PATH to AVATAR_UPLOAD_PATH
|
||||
repoAvatarSec.Key("PATH").MustString(sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").String())
|
||||
|
||||
var err error
|
||||
RepoAvatar.Storage, err = getStorage(rootCfg, "repo-avatars", storageType, repoAvatarSec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
RepoAvatar.Fallback = sec.Key("REPOSITORY_AVATAR_FALLBACK").MustString("none")
|
||||
RepoAvatar.FallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString(AppSubURL + "/assets/img/repo_default.png")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// Project settings
|
||||
var (
|
||||
Project = struct {
|
||||
ProjectBoardBasicKanbanType []string
|
||||
ProjectBoardBugTriageType []string
|
||||
}{
|
||||
ProjectBoardBasicKanbanType: []string{"To Do", "In Progress", "Done"},
|
||||
ProjectBoardBugTriageType: []string{"Needs Triage", "High Priority", "Low Priority", "Closed"},
|
||||
}
|
||||
)
|
||||
|
||||
func loadProjectFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "project", &Project)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// Proxy settings
|
||||
var Proxy = struct {
|
||||
Enabled bool
|
||||
ProxyURL string
|
||||
ProxyURLFixed *url.URL
|
||||
ProxyHosts []string
|
||||
}{
|
||||
Enabled: false,
|
||||
ProxyURL: "",
|
||||
ProxyHosts: []string{},
|
||||
}
|
||||
|
||||
func loadProxyFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("proxy")
|
||||
Proxy.Enabled = sec.Key("PROXY_ENABLED").MustBool(false)
|
||||
Proxy.ProxyURL = sec.Key("PROXY_URL").MustString("")
|
||||
if Proxy.ProxyURL != "" {
|
||||
var err error
|
||||
Proxy.ProxyURLFixed, err = url.Parse(Proxy.ProxyURL)
|
||||
if err != nil {
|
||||
log.Error("Global PROXY_URL is not valid")
|
||||
Proxy.ProxyURL = ""
|
||||
}
|
||||
}
|
||||
Proxy.ProxyHosts = sec.Key("PROXY_HOSTS").Strings(",")
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// QueueSettings represent the settings for a queue from the ini
|
||||
type QueueSettings struct {
|
||||
Name string // not an INI option, it is the name for [queue.the-name] section
|
||||
|
||||
Type string
|
||||
Datadir string
|
||||
ConnStr string // for leveldb or redis
|
||||
Length int // max queue length before blocking
|
||||
|
||||
QueueName, SetName string // the name suffix for storage (db key, redis key), "set" is for unique queue
|
||||
|
||||
BatchLength int
|
||||
MaxWorkers int
|
||||
}
|
||||
|
||||
func GetQueueSettings(rootCfg ConfigProvider, name string) (QueueSettings, error) {
|
||||
queueSettingsDefault := QueueSettings{
|
||||
Type: "level", // dummy, channel, level, redis
|
||||
Datadir: "queues/common", // relative to AppDataPath
|
||||
Length: 100000, // queue length before a channel queue will block
|
||||
|
||||
QueueName: "_queue",
|
||||
SetName: "_unique",
|
||||
BatchLength: 20,
|
||||
MaxWorkers: runtime.NumCPU() / 2,
|
||||
}
|
||||
if queueSettingsDefault.MaxWorkers < 1 {
|
||||
queueSettingsDefault.MaxWorkers = 1
|
||||
}
|
||||
if queueSettingsDefault.MaxWorkers > 10 {
|
||||
queueSettingsDefault.MaxWorkers = 10
|
||||
}
|
||||
|
||||
// deep copy default settings
|
||||
cfg := QueueSettings{}
|
||||
if cfgBs, err := json.Marshal(queueSettingsDefault); err != nil {
|
||||
return cfg, err
|
||||
} else if err = json.Unmarshal(cfgBs, &cfg); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
cfg.Name = name
|
||||
if sec, err := rootCfg.GetSection("queue"); err == nil {
|
||||
if err = sec.MapTo(&cfg); err != nil {
|
||||
log.Error("Failed to map queue common config for %q: %v", name, err)
|
||||
return cfg, nil
|
||||
}
|
||||
}
|
||||
if sec, err := rootCfg.GetSection("queue." + name); err == nil {
|
||||
if err = sec.MapTo(&cfg); err != nil {
|
||||
log.Error("Failed to map queue spec config for %q: %v", name, err)
|
||||
return cfg, nil
|
||||
}
|
||||
if sec.HasKey("CONN_STR") {
|
||||
cfg.ConnStr = sec.Key("CONN_STR").String()
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Datadir == "" {
|
||||
cfg.Datadir = queueSettingsDefault.Datadir
|
||||
}
|
||||
if !filepath.IsAbs(cfg.Datadir) {
|
||||
cfg.Datadir = filepath.Join(AppDataPath, cfg.Datadir)
|
||||
}
|
||||
cfg.Datadir = filepath.ToSlash(cfg.Datadir)
|
||||
|
||||
if cfg.Type == "redis" && cfg.ConnStr == "" {
|
||||
cfg.ConnStr = "redis://127.0.0.1:6379/0"
|
||||
}
|
||||
|
||||
if cfg.Length <= 0 {
|
||||
cfg.Length = queueSettingsDefault.Length
|
||||
}
|
||||
if cfg.MaxWorkers <= 0 {
|
||||
cfg.MaxWorkers = queueSettingsDefault.MaxWorkers
|
||||
}
|
||||
if cfg.BatchLength <= 0 {
|
||||
cfg.BatchLength = queueSettingsDefault.BatchLength
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func LoadQueueSettings() {
|
||||
loadQueueFrom(CfgProvider)
|
||||
}
|
||||
|
||||
func loadQueueFrom(rootCfg ConfigProvider) {
|
||||
hasOld := false
|
||||
handleOldLengthConfiguration := func(rootCfg ConfigProvider, newQueueName, oldSection, oldKey string) {
|
||||
if rootCfg.Section(oldSection).HasKey(oldKey) {
|
||||
hasOld = true
|
||||
log.Error("Removed queue option: `[%s].%s`. Use new options in `[queue.%s]`", oldSection, oldKey, newQueueName)
|
||||
}
|
||||
}
|
||||
handleOldLengthConfiguration(rootCfg, "issue_indexer", "indexer", "ISSUE_INDEXER_QUEUE_TYPE")
|
||||
handleOldLengthConfiguration(rootCfg, "issue_indexer", "indexer", "ISSUE_INDEXER_QUEUE_BATCH_NUMBER")
|
||||
handleOldLengthConfiguration(rootCfg, "issue_indexer", "indexer", "ISSUE_INDEXER_QUEUE_DIR")
|
||||
handleOldLengthConfiguration(rootCfg, "issue_indexer", "indexer", "ISSUE_INDEXER_QUEUE_CONN_STR")
|
||||
handleOldLengthConfiguration(rootCfg, "issue_indexer", "indexer", "UPDATE_BUFFER_LEN")
|
||||
handleOldLengthConfiguration(rootCfg, "mailer", "mailer", "SEND_BUFFER_LEN")
|
||||
handleOldLengthConfiguration(rootCfg, "pr_patch_checker", "repository", "PULL_REQUEST_QUEUE_LENGTH")
|
||||
handleOldLengthConfiguration(rootCfg, "mirror", "repository", "MIRROR_QUEUE_LENGTH")
|
||||
if hasOld {
|
||||
log.Fatal("Please update your app.ini to remove deprecated config options")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// enumerates all the policy repository creating
|
||||
const (
|
||||
RepoCreatingLastUserVisibility = "last"
|
||||
RepoCreatingPrivate = "private"
|
||||
RepoCreatingPublic = "public"
|
||||
)
|
||||
|
||||
// enumerates the values for [repository.pull-request] DEFAULT_TITLE_SOURCE
|
||||
const (
|
||||
RepoPRTitleSourceFirstCommit = "first-commit"
|
||||
RepoPRTitleSourceAuto = "auto"
|
||||
)
|
||||
|
||||
// ItemsPerPage maximum items per page in forks, watchers and stars of a repo
|
||||
const ItemsPerPage = 40
|
||||
|
||||
// Repository settings
|
||||
var (
|
||||
Repository = struct {
|
||||
DetectedCharsetsOrder []string
|
||||
DetectedCharsetScore map[string]int `ini:"-"`
|
||||
AnsiCharset string
|
||||
ForcePrivate bool
|
||||
DefaultPrivate string
|
||||
DefaultPushCreatePrivate bool
|
||||
MaxCreationLimit int
|
||||
UserMaxCreationLimit int
|
||||
OrgMaxCreationLimit int
|
||||
PreferredLicenses []string
|
||||
DisableHTTPGit bool
|
||||
AccessControlAllowOrigin string
|
||||
UseCompatSSHURI bool
|
||||
GoGetCloneURLProtocol string
|
||||
DefaultCloseIssuesViaCommitsInAnyBranch bool
|
||||
EnablePushCreateUser bool
|
||||
EnablePushCreateOrg bool
|
||||
DisabledRepoUnits []string
|
||||
DefaultRepoUnits []string
|
||||
DefaultForkRepoUnits []string
|
||||
DefaultMirrorRepoUnits []string
|
||||
DefaultTemplateRepoUnits []string
|
||||
PrefixArchiveFiles bool
|
||||
DisableMigrations bool
|
||||
DisableStars bool `ini:"DISABLE_STARS"`
|
||||
DefaultBranch string
|
||||
AllowAdoptionOfUnadoptedRepositories bool
|
||||
AllowDeleteOfUnadoptedRepositories bool
|
||||
DisableDownloadSourceArchives bool
|
||||
AllowForkWithoutMaximumLimit bool
|
||||
AllowForkIntoSameOwner bool
|
||||
|
||||
// StreamArchives makes Gitea stream git archive files to the client directly instead of creating an archive first.
|
||||
// Ideally all users should use this streaming method. However, at the moment we don't know whether there are
|
||||
// any users who still need the old behavior, so we introduce this option, intentionally not documenting it.
|
||||
// After one or two releases, if no one complains, we will remove this option and always use streaming.
|
||||
StreamArchives bool
|
||||
|
||||
// Repository editor settings
|
||||
Editor struct {
|
||||
LineWrapExtensions []string
|
||||
} `ini:"-"`
|
||||
|
||||
// Repository upload settings
|
||||
Upload struct {
|
||||
Enabled bool
|
||||
AllowedTypes string
|
||||
FileMaxSize int64
|
||||
MaxFiles int
|
||||
} `ini:"-"`
|
||||
|
||||
// Pull request settings
|
||||
PullRequest struct {
|
||||
WorkInProgressPrefixes []string
|
||||
CloseKeywords []string
|
||||
ReopenKeywords []string
|
||||
DefaultMergeStyle string
|
||||
DefaultMergeMessageCommitsLimit int
|
||||
DefaultMergeMessageSize int
|
||||
DefaultMergeMessageAllAuthors bool
|
||||
DefaultMergeMessageMaxApprovers int
|
||||
DefaultMergeMessageOfficialApproversOnly bool
|
||||
PopulateSquashCommentWithCommitMessages bool
|
||||
AddCoCommitterTrailers bool
|
||||
RetargetChildrenOnMerge bool
|
||||
DelayCheckForInactiveDays int
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
DefaultTitleSource string
|
||||
} `ini:"repository.pull-request"`
|
||||
|
||||
// Issue Setting
|
||||
Issue struct {
|
||||
LockReasons []string
|
||||
MaxPinned int
|
||||
} `ini:"repository.issue"`
|
||||
|
||||
Release struct {
|
||||
AllowedTypes string
|
||||
DefaultPagingNum int
|
||||
FileMaxSize int64
|
||||
MaxFiles int64
|
||||
} `ini:"repository.release"`
|
||||
|
||||
Signing struct {
|
||||
SigningKey string
|
||||
SigningName string
|
||||
SigningEmail string
|
||||
SigningFormat string
|
||||
InitialCommit []string
|
||||
CRUDActions []string `ini:"CRUD_ACTIONS"`
|
||||
Merges []string
|
||||
Wiki []string
|
||||
DefaultTrustModel string
|
||||
TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"`
|
||||
} `ini:"repository.signing"`
|
||||
}{
|
||||
DetectedCharsetsOrder: []string{
|
||||
"UTF-8",
|
||||
"UTF-16BE",
|
||||
"UTF-16LE",
|
||||
"UTF-32BE",
|
||||
"UTF-32LE",
|
||||
"ISO-8859-1",
|
||||
"windows-1252",
|
||||
"ISO-8859-2",
|
||||
"windows-1250",
|
||||
"ISO-8859-5",
|
||||
"ISO-8859-6",
|
||||
"ISO-8859-7",
|
||||
"windows-1253",
|
||||
"ISO-8859-8-I",
|
||||
"windows-1255",
|
||||
"ISO-8859-8",
|
||||
"windows-1251",
|
||||
"windows-1256",
|
||||
"KOI8-R",
|
||||
"ISO-8859-9",
|
||||
"windows-1254",
|
||||
"Shift_JIS",
|
||||
"GB18030",
|
||||
"EUC-JP",
|
||||
"EUC-KR",
|
||||
"Big5",
|
||||
"ISO-2022-JP",
|
||||
"ISO-2022-KR",
|
||||
"ISO-2022-CN",
|
||||
"IBM424_rtl",
|
||||
"IBM424_ltr",
|
||||
"IBM420_rtl",
|
||||
"IBM420_ltr",
|
||||
},
|
||||
DetectedCharsetScore: map[string]int{},
|
||||
AnsiCharset: "",
|
||||
ForcePrivate: false,
|
||||
DefaultPrivate: RepoCreatingLastUserVisibility,
|
||||
DefaultPushCreatePrivate: true,
|
||||
MaxCreationLimit: -1,
|
||||
UserMaxCreationLimit: -1,
|
||||
OrgMaxCreationLimit: -1,
|
||||
PreferredLicenses: []string{"Apache License 2.0", "MIT License"},
|
||||
DisableHTTPGit: false,
|
||||
AccessControlAllowOrigin: "",
|
||||
UseCompatSSHURI: false,
|
||||
DefaultCloseIssuesViaCommitsInAnyBranch: false,
|
||||
EnablePushCreateUser: false,
|
||||
EnablePushCreateOrg: false,
|
||||
DisabledRepoUnits: []string{},
|
||||
DefaultRepoUnits: []string{},
|
||||
DefaultForkRepoUnits: []string{},
|
||||
DefaultMirrorRepoUnits: []string{},
|
||||
DefaultTemplateRepoUnits: []string{},
|
||||
PrefixArchiveFiles: true,
|
||||
DisableMigrations: false,
|
||||
DisableStars: false,
|
||||
DefaultBranch: "main",
|
||||
AllowForkWithoutMaximumLimit: true,
|
||||
StreamArchives: true,
|
||||
|
||||
// Repository editor settings
|
||||
Editor: struct {
|
||||
LineWrapExtensions []string
|
||||
}{
|
||||
LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,.livemd,", ","),
|
||||
},
|
||||
|
||||
// Repository upload settings
|
||||
Upload: struct {
|
||||
Enabled bool
|
||||
AllowedTypes string
|
||||
FileMaxSize int64
|
||||
MaxFiles int
|
||||
}{
|
||||
Enabled: true,
|
||||
AllowedTypes: "",
|
||||
FileMaxSize: 50,
|
||||
MaxFiles: 5,
|
||||
},
|
||||
|
||||
// Pull request settings
|
||||
PullRequest: struct {
|
||||
WorkInProgressPrefixes []string
|
||||
CloseKeywords []string
|
||||
ReopenKeywords []string
|
||||
DefaultMergeStyle string
|
||||
DefaultMergeMessageCommitsLimit int
|
||||
DefaultMergeMessageSize int
|
||||
DefaultMergeMessageAllAuthors bool
|
||||
DefaultMergeMessageMaxApprovers int
|
||||
DefaultMergeMessageOfficialApproversOnly bool
|
||||
PopulateSquashCommentWithCommitMessages bool
|
||||
AddCoCommitterTrailers bool
|
||||
RetargetChildrenOnMerge bool
|
||||
DelayCheckForInactiveDays int
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
DefaultTitleSource string
|
||||
}{
|
||||
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
|
||||
// Same as GitHub. See
|
||||
// https://help.github.com/articles/closing-issues-via-commit-messages
|
||||
CloseKeywords: strings.Split("close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved", ","),
|
||||
ReopenKeywords: strings.Split("reopen,reopens,reopened", ","),
|
||||
DefaultMergeStyle: "merge",
|
||||
DefaultMergeMessageCommitsLimit: 50,
|
||||
DefaultMergeMessageSize: 5 * 1024,
|
||||
DefaultMergeMessageAllAuthors: false,
|
||||
DefaultMergeMessageMaxApprovers: 10,
|
||||
DefaultMergeMessageOfficialApproversOnly: true,
|
||||
PopulateSquashCommentWithCommitMessages: false,
|
||||
AddCoCommitterTrailers: true,
|
||||
RetargetChildrenOnMerge: true,
|
||||
DelayCheckForInactiveDays: 7,
|
||||
DefaultTitleSource: RepoPRTitleSourceAuto,
|
||||
},
|
||||
|
||||
// Issue settings
|
||||
Issue: struct {
|
||||
LockReasons []string
|
||||
MaxPinned int
|
||||
}{
|
||||
LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","),
|
||||
MaxPinned: 3,
|
||||
},
|
||||
|
||||
Release: struct {
|
||||
AllowedTypes string
|
||||
DefaultPagingNum int
|
||||
FileMaxSize int64
|
||||
MaxFiles int64
|
||||
}{
|
||||
AllowedTypes: "",
|
||||
DefaultPagingNum: 10,
|
||||
FileMaxSize: 2048,
|
||||
MaxFiles: 5,
|
||||
},
|
||||
|
||||
// Signing settings
|
||||
Signing: struct {
|
||||
SigningKey string
|
||||
SigningName string
|
||||
SigningEmail string
|
||||
SigningFormat string
|
||||
InitialCommit []string
|
||||
CRUDActions []string `ini:"CRUD_ACTIONS"`
|
||||
Merges []string
|
||||
Wiki []string
|
||||
DefaultTrustModel string
|
||||
TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"`
|
||||
}{
|
||||
SigningKey: "default",
|
||||
SigningName: "",
|
||||
SigningEmail: "",
|
||||
SigningFormat: "openpgp", // git.SigningKeyFormatOpenPGP
|
||||
InitialCommit: []string{"always"},
|
||||
CRUDActions: []string{"pubkey", "twofa", "parentsigned"},
|
||||
Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"},
|
||||
Wiki: []string{"never"},
|
||||
DefaultTrustModel: "collaborator",
|
||||
TrustedSSHKeys: []string{},
|
||||
},
|
||||
}
|
||||
RepoRootPath string
|
||||
ScriptType = "bash"
|
||||
)
|
||||
|
||||
func loadRepositoryFrom(rootCfg ConfigProvider) {
|
||||
var err error
|
||||
// Determine and create root git repository path.
|
||||
sec := rootCfg.Section("repository")
|
||||
Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool()
|
||||
Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool()
|
||||
Repository.GoGetCloneURLProtocol = sec.Key("GO_GET_CLONE_URL_PROTOCOL").MustString("https")
|
||||
// MAX_CREATION_LIMIT is a shortcut that sets the default for the two per-type limits below.
|
||||
// USER_/ORG_MAX_CREATION_LIMIT take precedence when explicitly set.
|
||||
Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1)
|
||||
Repository.UserMaxCreationLimit = sec.Key("USER_MAX_CREATION_LIMIT").MustInt(Repository.MaxCreationLimit)
|
||||
Repository.OrgMaxCreationLimit = sec.Key("ORG_MAX_CREATION_LIMIT").MustInt(Repository.MaxCreationLimit)
|
||||
Repository.DefaultBranch = sec.Key("DEFAULT_BRANCH").MustString(Repository.DefaultBranch)
|
||||
RepoRootPath = sec.Key("ROOT").MustString(filepath.Join(AppDataPath, "gitea-repositories"))
|
||||
if !filepath.IsAbs(RepoRootPath) {
|
||||
RepoRootPath = filepath.Join(AppWorkPath, RepoRootPath)
|
||||
} else {
|
||||
RepoRootPath = filepath.Clean(RepoRootPath)
|
||||
}
|
||||
|
||||
checkOverlappedPath("[repository].ROOT", RepoRootPath)
|
||||
|
||||
defaultDetectedCharsetsOrder := make([]string, 0, len(Repository.DetectedCharsetsOrder))
|
||||
for _, charset := range Repository.DetectedCharsetsOrder {
|
||||
defaultDetectedCharsetsOrder = append(defaultDetectedCharsetsOrder, strings.ToLower(strings.TrimSpace(charset)))
|
||||
}
|
||||
ScriptType = sec.Key("SCRIPT_TYPE").MustString("bash")
|
||||
|
||||
if _, err := exec.LookPath(ScriptType); err != nil {
|
||||
log.Warn("SCRIPT_TYPE %q is not on the current PATH. Are you sure that this is the correct SCRIPT_TYPE?", ScriptType)
|
||||
}
|
||||
|
||||
if err = sec.MapTo(&Repository); err != nil {
|
||||
log.Fatal("Failed to map Repository settings: %v", err)
|
||||
} else if err = rootCfg.Section("repository.editor").MapTo(&Repository.Editor); err != nil {
|
||||
log.Fatal("Failed to map Repository.Editor settings: %v", err)
|
||||
} else if err = rootCfg.Section("repository.upload").MapTo(&Repository.Upload); err != nil {
|
||||
log.Fatal("Failed to map Repository.Upload settings: %v", err)
|
||||
} else if err = rootCfg.Section("repository.pull-request").MapTo(&Repository.PullRequest); err != nil {
|
||||
log.Fatal("Failed to map Repository.PullRequest settings: %v", err)
|
||||
}
|
||||
|
||||
if !rootCfg.Section("packages").Key("ENABLED").MustBool(Packages.Enabled) {
|
||||
Repository.DisabledRepoUnits = append(Repository.DisabledRepoUnits, "repo.packages")
|
||||
}
|
||||
|
||||
if !rootCfg.Section("actions").Key("ENABLED").MustBool(Actions.Enabled) {
|
||||
Repository.DisabledRepoUnits = append(Repository.DisabledRepoUnits, "repo.actions")
|
||||
}
|
||||
|
||||
// Handle default trustmodel settings
|
||||
Repository.Signing.DefaultTrustModel = strings.ToLower(strings.TrimSpace(Repository.Signing.DefaultTrustModel))
|
||||
if Repository.Signing.DefaultTrustModel == "default" {
|
||||
Repository.Signing.DefaultTrustModel = "collaborator"
|
||||
}
|
||||
|
||||
// Handle preferred charset orders
|
||||
preferred := make([]string, 0, len(Repository.DetectedCharsetsOrder))
|
||||
for _, charset := range Repository.DetectedCharsetsOrder {
|
||||
canonicalCharset := strings.ToLower(strings.TrimSpace(charset))
|
||||
preferred = append(preferred, canonicalCharset)
|
||||
// remove it from the defaults
|
||||
for i, charset := range defaultDetectedCharsetsOrder {
|
||||
if charset == canonicalCharset {
|
||||
defaultDetectedCharsetsOrder = append(defaultDetectedCharsetsOrder[:i], defaultDetectedCharsetsOrder[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i := 0
|
||||
for _, charset := range preferred {
|
||||
// Add the defaults
|
||||
if charset == "defaults" {
|
||||
for _, charset := range defaultDetectedCharsetsOrder {
|
||||
canonicalCharset := strings.ToLower(strings.TrimSpace(charset))
|
||||
if _, has := Repository.DetectedCharsetScore[canonicalCharset]; !has {
|
||||
Repository.DetectedCharsetScore[canonicalCharset] = i
|
||||
i++
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, has := Repository.DetectedCharsetScore[charset]; !has {
|
||||
Repository.DetectedCharsetScore[charset] = i
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
if err := loadRepoArchiveFrom(rootCfg); err != nil {
|
||||
log.Fatal("loadRepoArchiveFrom: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import "fmt"
|
||||
|
||||
var RepoArchive = struct {
|
||||
Storage *Storage
|
||||
}{}
|
||||
|
||||
func loadRepoArchiveFrom(rootCfg ConfigProvider) (err error) {
|
||||
sec, _ := rootCfg.GetSection("repo-archive")
|
||||
if sec == nil {
|
||||
RepoArchive.Storage, err = getStorage(rootCfg, "repo-archive", "", nil)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sec.MapTo(&RepoArchive); err != nil {
|
||||
return fmt.Errorf("mapto repoarchive failed: %v", err)
|
||||
}
|
||||
|
||||
RepoArchive.Storage, err = getStorage(rootCfg, "repo-archive", "", sec)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getStorageInheritNameSectionTypeForRepoArchive(t *testing.T) {
|
||||
// packages storage inherits from storage if nothing configured
|
||||
iniStr := `
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", RepoArchive.Storage.Type)
|
||||
assert.Equal(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
|
||||
|
||||
// we can also configure packages storage directly
|
||||
iniStr = `
|
||||
[storage.repo-archive]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", RepoArchive.Storage.Type)
|
||||
assert.Equal(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
|
||||
|
||||
// or we can indicate the storage type in the packages section
|
||||
iniStr = `
|
||||
[repo-archive]
|
||||
STORAGE_TYPE = my_minio
|
||||
|
||||
[storage.my_minio]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", RepoArchive.Storage.Type)
|
||||
assert.Equal(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
|
||||
|
||||
// or we can indicate the storage type and minio base path in the packages section
|
||||
iniStr = `
|
||||
[repo-archive]
|
||||
STORAGE_TYPE = my_minio
|
||||
MINIO_BASE_PATH = my_archive/
|
||||
|
||||
[storage.my_minio]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
|
||||
assert.EqualValues(t, "minio", RepoArchive.Storage.Type)
|
||||
assert.Equal(t, "my_archive/", RepoArchive.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_RepoArchiveStorage(t *testing.T) {
|
||||
iniStr := `
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ENDPOINT = s3.my-domain.net
|
||||
MINIO_BUCKET = gitea
|
||||
MINIO_LOCATION = homenet
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_ACCESS_KEY_ID = correct_key
|
||||
MINIO_SECRET_ACCESS_KEY = correct_key
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
storage := RepoArchive.Storage
|
||||
|
||||
assert.EqualValues(t, "minio", storage.Type)
|
||||
assert.Equal(t, "gitea", storage.MinioConfig.Bucket)
|
||||
|
||||
iniStr = `
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
[storage.repo-archive]
|
||||
STORAGE_TYPE = s3
|
||||
[storage.s3]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ENDPOINT = s3.my-domain.net
|
||||
MINIO_BUCKET = gitea
|
||||
MINIO_LOCATION = homenet
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_ACCESS_KEY_ID = correct_key
|
||||
MINIO_SECRET_ACCESS_KEY = correct_key
|
||||
`
|
||||
cfg, err = NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
storage = RepoArchive.Storage
|
||||
|
||||
assert.EqualValues(t, "minio", storage.Type)
|
||||
assert.Equal(t, "gitea", storage.MinioConfig.Bucket)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadRepositoryCreationLimits(t *testing.T) {
|
||||
defer test.MockVariableValue(&Repository.MaxCreationLimit)()
|
||||
defer test.MockVariableValue(&Repository.UserMaxCreationLimit)()
|
||||
defer test.MockVariableValue(&Repository.OrgMaxCreationLimit)()
|
||||
|
||||
t.Run("ShortcutPropagatesToBoth", func(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[repository]
|
||||
MAX_CREATION_LIMIT = 5
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadRepositoryFrom(cfg)
|
||||
assert.Equal(t, 5, Repository.MaxCreationLimit)
|
||||
assert.Equal(t, 5, Repository.UserMaxCreationLimit)
|
||||
assert.Equal(t, 5, Repository.OrgMaxCreationLimit)
|
||||
})
|
||||
|
||||
t.Run("PerTypeKeysOverrideShortcut", func(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[repository]
|
||||
MAX_CREATION_LIMIT = 5
|
||||
USER_MAX_CREATION_LIMIT = 0
|
||||
ORG_MAX_CREATION_LIMIT = -1
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadRepositoryFrom(cfg)
|
||||
assert.Equal(t, 0, Repository.UserMaxCreationLimit)
|
||||
assert.Equal(t, -1, Repository.OrgMaxCreationLimit)
|
||||
})
|
||||
|
||||
t.Run("PartialOverrideOtherInheritsShortcut", func(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[repository]
|
||||
MAX_CREATION_LIMIT = 7
|
||||
ORG_MAX_CREATION_LIMIT = -1
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadRepositoryFrom(cfg)
|
||||
assert.Equal(t, 7, Repository.UserMaxCreationLimit)
|
||||
assert.Equal(t, -1, Repository.OrgMaxCreationLimit)
|
||||
})
|
||||
|
||||
t.Run("NoKeyDefaultsToNoLimit", func(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[repository]
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadRepositoryFrom(cfg)
|
||||
assert.Equal(t, -1, Repository.MaxCreationLimit)
|
||||
assert.Equal(t, -1, Repository.UserMaxCreationLimit)
|
||||
assert.Equal(t, -1, Repository.OrgMaxCreationLimit)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/auth/password/hash"
|
||||
"gitea.dev/modules/generate"
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// Security settings
|
||||
var Security = struct {
|
||||
// TODO: move more settings to this struct in future
|
||||
XFrameOptions string
|
||||
XContentTypeOptions string
|
||||
}{
|
||||
XFrameOptions: "SAMEORIGIN",
|
||||
XContentTypeOptions: "nosniff",
|
||||
}
|
||||
|
||||
var (
|
||||
InstallLock bool
|
||||
SecretKey string
|
||||
InternalToken string // internal access token
|
||||
LogInRememberDays int
|
||||
CookieRememberName string
|
||||
ReverseProxyAuthUser string
|
||||
ReverseProxyAuthEmail string
|
||||
ReverseProxyAuthFullName string
|
||||
ReverseProxyLimit int
|
||||
ReverseProxyLogoutRedirect string
|
||||
ReverseProxyTrustedProxies []string
|
||||
MinPasswordLength int
|
||||
ImportLocalPaths bool
|
||||
DisableGitHooks = true
|
||||
DisableWebhooks bool
|
||||
OnlyAllowPushIfGiteaEnvironmentSet bool
|
||||
PasswordComplexity []string
|
||||
PasswordHashAlgo string
|
||||
PasswordCheckPwn bool
|
||||
SuccessfulTokensCacheSize int
|
||||
DisableQueryAuthToken bool
|
||||
RecordUserSignupMetadata = false
|
||||
TwoFactorAuthEnforced = false
|
||||
)
|
||||
|
||||
// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set
|
||||
// If the secret is loaded from uriKey (file), the file should be non-empty, to guarantee the behavior stable and clear.
|
||||
func loadSecret(sec ConfigSection, uriKey, verbatimKey string) string {
|
||||
// don't allow setting both URI and verbatim string
|
||||
uri := sec.Key(uriKey).String()
|
||||
verbatim := sec.Key(verbatimKey).String()
|
||||
if uri != "" && verbatim != "" {
|
||||
log.Fatal("Cannot specify both %s and %s", uriKey, verbatimKey)
|
||||
}
|
||||
|
||||
// if we have no URI, use verbatim
|
||||
if uri == "" {
|
||||
return verbatim
|
||||
}
|
||||
|
||||
tempURI, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to parse %s (%s): %v", uriKey, uri, err)
|
||||
}
|
||||
switch tempURI.Scheme {
|
||||
case "file":
|
||||
buf, err := os.ReadFile(tempURI.RequestURI())
|
||||
if err != nil {
|
||||
log.Fatal("Failed to read %s (%s): %v", uriKey, tempURI.RequestURI(), err)
|
||||
}
|
||||
val := strings.TrimSpace(string(buf))
|
||||
if val == "" {
|
||||
// The file shouldn't be empty, otherwise we can not know whether the user has ever set the KEY or KEY_URI
|
||||
// For example: if INTERNAL_TOKEN_URI=file:///empty-file,
|
||||
// Then if the token is re-generated during installation and saved to INTERNAL_TOKEN
|
||||
// Then INTERNAL_TOKEN and INTERNAL_TOKEN_URI both exist, that's a fatal error (they shouldn't)
|
||||
log.Fatal("Failed to read %s (%s): the file is empty", uriKey, tempURI.RequestURI())
|
||||
}
|
||||
return val
|
||||
|
||||
// only file URIs are allowed
|
||||
default:
|
||||
log.Fatal("Unsupported URI-Scheme %q (%q = %q)", tempURI.Scheme, uriKey, uri)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// generateSaveInternalToken generates and saves the internal token to app.ini
|
||||
func generateSaveInternalToken(rootCfg ConfigProvider) {
|
||||
token, err := generate.NewInternalToken()
|
||||
if err != nil {
|
||||
log.Fatal("Error generate internal token: %v", err)
|
||||
}
|
||||
|
||||
InternalToken = token
|
||||
saveCfg, err := rootCfg.PrepareSaving()
|
||||
if err != nil {
|
||||
log.Fatal("Error saving internal token: %v", err)
|
||||
}
|
||||
rootCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token)
|
||||
saveCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token)
|
||||
if err = saveCfg.Save(); err != nil {
|
||||
log.Fatal("Error saving internal token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadSecurityFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("security")
|
||||
LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(31)
|
||||
SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
|
||||
if SecretKey == "" {
|
||||
// FIXME: https://github.com/go-gitea/gitea/issues/16832
|
||||
// Until it supports rotating an existing secret key, we shouldn't move users off of the widely used default value
|
||||
SecretKey = "!#@FDEWREWR&*("
|
||||
}
|
||||
|
||||
CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_incredible")
|
||||
|
||||
ReverseProxyAuthUser = sec.Key("REVERSE_PROXY_AUTHENTICATION_USER").MustString("X-WEBAUTH-USER")
|
||||
ReverseProxyAuthEmail = sec.Key("REVERSE_PROXY_AUTHENTICATION_EMAIL").MustString("X-WEBAUTH-EMAIL")
|
||||
ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME")
|
||||
|
||||
ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1)
|
||||
ReverseProxyLogoutRedirect = sec.Key("REVERSE_PROXY_LOGOUT_REDIRECT").String()
|
||||
ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",")
|
||||
if len(ReverseProxyTrustedProxies) == 0 {
|
||||
ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"}
|
||||
}
|
||||
|
||||
MinPasswordLength = sec.Key("MIN_PASSWORD_LENGTH").MustInt(8)
|
||||
ImportLocalPaths = sec.Key("IMPORT_LOCAL_PATHS").MustBool(false)
|
||||
DisableGitHooks = sec.Key("DISABLE_GIT_HOOKS").MustBool(true)
|
||||
DisableWebhooks = sec.Key("DISABLE_WEBHOOKS").MustBool(false)
|
||||
OnlyAllowPushIfGiteaEnvironmentSet = sec.Key("ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET").MustBool(true)
|
||||
|
||||
// Ensure that the provided default hash algorithm is a valid hash algorithm
|
||||
var algorithm *hash.PasswordHashAlgorithm
|
||||
PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(sec.Key("PASSWORD_HASH_ALGO").MustString(""))
|
||||
if algorithm == nil {
|
||||
log.Fatal("The provided password hash algorithm was invalid: %s", sec.Key("PASSWORD_HASH_ALGO").MustString(""))
|
||||
}
|
||||
|
||||
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
|
||||
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
|
||||
|
||||
deprecatedSetting(rootCfg, "cors", "X_FRAME_OPTIONS", "security", "X_FRAME_OPTIONS", "v1.26.0")
|
||||
if sec.HasKey("X_FRAME_OPTIONS") {
|
||||
Security.XFrameOptions = sec.Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions)
|
||||
} else {
|
||||
Security.XFrameOptions = rootCfg.Section("cors").Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions)
|
||||
}
|
||||
|
||||
Security.XContentTypeOptions = sec.Key("X_CONTENT_TYPE_OPTIONS").MustString(Security.XContentTypeOptions)
|
||||
|
||||
twoFactorAuth := sec.Key("TWO_FACTOR_AUTH").String()
|
||||
switch twoFactorAuth {
|
||||
case "":
|
||||
case "enforced":
|
||||
TwoFactorAuthEnforced = true
|
||||
default:
|
||||
log.Fatal("Invalid two-factor auth option: %s", twoFactorAuth)
|
||||
}
|
||||
|
||||
InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
|
||||
if InstallLock && InternalToken == "" {
|
||||
// if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate
|
||||
// some users do cluster deployment, they still depend on this auto-generating behavior.
|
||||
generateSaveInternalToken(rootCfg)
|
||||
}
|
||||
|
||||
cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",")
|
||||
if len(cfgdata) == 0 {
|
||||
cfgdata = []string{"off"}
|
||||
}
|
||||
PasswordComplexity = make([]string, 0, len(cfgdata))
|
||||
for _, name := range cfgdata {
|
||||
name := strings.ToLower(strings.Trim(name, `"`))
|
||||
if name != "" {
|
||||
PasswordComplexity = append(PasswordComplexity, name)
|
||||
}
|
||||
}
|
||||
|
||||
sectionHasDisableQueryAuthToken := sec.HasKey("DISABLE_QUERY_AUTH_TOKEN")
|
||||
|
||||
// TODO: default value should be true in future releases
|
||||
DisableQueryAuthToken = sec.Key("DISABLE_QUERY_AUTH_TOKEN").MustBool(false)
|
||||
|
||||
RecordUserSignupMetadata = sec.Key("RECORD_USER_SIGNUP_METADATA").MustBool(false)
|
||||
|
||||
// warn if the setting is set to false explicitly
|
||||
if sectionHasDisableQueryAuthToken && !DisableQueryAuthToken {
|
||||
log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will default to true in gitea 1.23 and will be removed in gitea 1.24.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// Scheme describes protocol types
|
||||
type Scheme string
|
||||
|
||||
// enumerates all the scheme types
|
||||
const (
|
||||
HTTP Scheme = "http"
|
||||
HTTPS Scheme = "https"
|
||||
FCGI Scheme = "fcgi"
|
||||
FCGIUnix Scheme = "fcgi+unix"
|
||||
HTTPUnix Scheme = "http+unix"
|
||||
)
|
||||
|
||||
// LandingPage describes the default page
|
||||
type LandingPage string
|
||||
|
||||
// enumerates all the landing page types
|
||||
const (
|
||||
LandingPageHome LandingPage = "/"
|
||||
LandingPageExplore LandingPage = "/explore"
|
||||
LandingPageOrganizations LandingPage = "/explore/organizations"
|
||||
LandingPageLogin LandingPage = "/user/login"
|
||||
)
|
||||
|
||||
const (
|
||||
PublicURLAuto = "auto"
|
||||
PublicURLLegacy = "legacy"
|
||||
PublicURLNever = "never"
|
||||
)
|
||||
|
||||
// Server settings
|
||||
var (
|
||||
// AppURL is the Application ROOT_URL. It always has a '/' suffix
|
||||
// It maps to ini:"ROOT_URL"
|
||||
AppURL string
|
||||
|
||||
// PublicURLDetection controls how to use the HTTP request headers to detect public URL
|
||||
PublicURLDetection string
|
||||
|
||||
// AppSubURL represents the sub-url mounting point for gitea, parsed from "ROOT_URL"
|
||||
// It is either "" or starts with '/' and ends without '/', such as '/{sub-path}'.
|
||||
// This value is empty if site does not have sub-url.
|
||||
AppSubURL string
|
||||
|
||||
// UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...",
|
||||
// to make it easier to debug sub-path related problems without a reverse proxy.
|
||||
UseSubURLPath bool
|
||||
|
||||
// AppDataPath is the default path for storing data.
|
||||
// It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data"
|
||||
AppDataPath string
|
||||
|
||||
// LocalURL is the url for locally running applications to contact Gitea. It always has a '/' suffix
|
||||
// It maps to ini:"LOCAL_ROOT_URL" in [server]
|
||||
LocalURL string
|
||||
|
||||
// appTempPathInternal is the temporary path for the app, it is only an internal variable
|
||||
// DO NOT use it directly, always use AppDataTempDir
|
||||
appTempPathInternal string
|
||||
|
||||
Protocol Scheme
|
||||
UseProxyProtocol bool
|
||||
ProxyProtocolTLSBridging bool
|
||||
ProxyProtocolHeaderTimeout time.Duration
|
||||
ProxyProtocolAcceptUnknown bool
|
||||
Domain string
|
||||
HTTPAddr string
|
||||
HTTPPort string
|
||||
LocalUseProxyProtocol bool
|
||||
RedirectOtherPort bool
|
||||
RedirectorUseProxyProtocol bool
|
||||
PortToRedirect string
|
||||
CertFile string
|
||||
KeyFile string
|
||||
StaticRootPath string
|
||||
StaticCacheTime time.Duration
|
||||
EnableGzip bool
|
||||
LandingPageURL LandingPage
|
||||
UnixSocketPermission uint32
|
||||
EnablePprof bool
|
||||
PprofDataPath string
|
||||
EnableAcme bool
|
||||
AcmeTOS bool
|
||||
AcmeLiveDirectory string
|
||||
AcmeEmail string
|
||||
AcmeURL string
|
||||
AcmeCARoot string
|
||||
SSLMinimumVersion string
|
||||
SSLMaximumVersion string
|
||||
SSLCurvePreferences []string
|
||||
SSLCipherSuites []string
|
||||
GracefulRestartable bool
|
||||
GracefulHammerTime time.Duration
|
||||
StartupTimeout time.Duration
|
||||
PerWriteTimeout = 30 * time.Second
|
||||
PerWritePerKbTimeout = 10 * time.Second
|
||||
StaticURLPrefix string // no trailing slash, defaults to AppSubURL, the URL can be relative or absolute
|
||||
)
|
||||
|
||||
func loadServerFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("server")
|
||||
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
|
||||
|
||||
Domain = sec.Key("DOMAIN").MustString("localhost")
|
||||
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
|
||||
HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
|
||||
|
||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
||||
// if these are removed, the warning will not be shown
|
||||
if sec.HasKey("ENABLE_ACME") {
|
||||
EnableAcme = sec.Key("ENABLE_ACME").MustBool(false)
|
||||
} else {
|
||||
deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0")
|
||||
EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
|
||||
}
|
||||
|
||||
protocolCfg := sec.Key("PROTOCOL").String()
|
||||
if protocolCfg != "https" && EnableAcme {
|
||||
log.Fatal("ACME could only be used with HTTPS protocol")
|
||||
}
|
||||
|
||||
switch protocolCfg {
|
||||
case "", "http":
|
||||
Protocol = HTTP
|
||||
case "https":
|
||||
Protocol = HTTPS
|
||||
if EnableAcme {
|
||||
AcmeURL = sec.Key("ACME_URL").MustString("")
|
||||
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
|
||||
|
||||
if sec.HasKey("ACME_ACCEPTTOS") {
|
||||
AcmeTOS = sec.Key("ACME_ACCEPTTOS").MustBool(false)
|
||||
} else {
|
||||
deprecatedSetting(rootCfg, "server", "LETSENCRYPT_ACCEPTTOS", "server", "ACME_ACCEPTTOS", "v1.19.0")
|
||||
AcmeTOS = sec.Key("LETSENCRYPT_ACCEPTTOS").MustBool(false)
|
||||
}
|
||||
if !AcmeTOS {
|
||||
log.Fatal("ACME TOS is not accepted (ACME_ACCEPTTOS).")
|
||||
}
|
||||
|
||||
if sec.HasKey("ACME_DIRECTORY") {
|
||||
AcmeLiveDirectory = sec.Key("ACME_DIRECTORY").MustString("https")
|
||||
} else {
|
||||
deprecatedSetting(rootCfg, "server", "LETSENCRYPT_DIRECTORY", "server", "ACME_DIRECTORY", "v1.19.0")
|
||||
AcmeLiveDirectory = sec.Key("LETSENCRYPT_DIRECTORY").MustString("https")
|
||||
}
|
||||
|
||||
if sec.HasKey("ACME_EMAIL") {
|
||||
AcmeEmail = sec.Key("ACME_EMAIL").MustString("")
|
||||
} else {
|
||||
deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", "v1.19.0")
|
||||
AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("")
|
||||
}
|
||||
} else {
|
||||
CertFile = sec.Key("CERT_FILE").String()
|
||||
KeyFile = sec.Key("KEY_FILE").String()
|
||||
if len(CertFile) > 0 && !filepath.IsAbs(CertFile) {
|
||||
CertFile = filepath.Join(CustomPath, CertFile)
|
||||
}
|
||||
if len(KeyFile) > 0 && !filepath.IsAbs(KeyFile) {
|
||||
KeyFile = filepath.Join(CustomPath, KeyFile)
|
||||
}
|
||||
}
|
||||
SSLMinimumVersion = sec.Key("SSL_MIN_VERSION").MustString("")
|
||||
SSLMaximumVersion = sec.Key("SSL_MAX_VERSION").MustString("")
|
||||
SSLCurvePreferences = sec.Key("SSL_CURVE_PREFERENCES").Strings(",")
|
||||
SSLCipherSuites = sec.Key("SSL_CIPHER_SUITES").Strings(",")
|
||||
case "fcgi":
|
||||
Protocol = FCGI
|
||||
case "fcgi+unix", "unix", "http+unix":
|
||||
switch protocolCfg {
|
||||
case "fcgi+unix":
|
||||
Protocol = FCGIUnix
|
||||
case "unix":
|
||||
log.Warn("unix PROTOCOL value is deprecated, please use http+unix")
|
||||
fallthrough
|
||||
default: // "http+unix"
|
||||
Protocol = HTTPUnix
|
||||
}
|
||||
UnixSocketPermissionRaw := sec.Key("UNIX_SOCKET_PERMISSION").MustString("666")
|
||||
UnixSocketPermissionParsed, err := strconv.ParseUint(UnixSocketPermissionRaw, 8, 32)
|
||||
if err != nil || UnixSocketPermissionParsed > 0o777 {
|
||||
log.Fatal("Failed to parse unixSocketPermission: %s", UnixSocketPermissionRaw)
|
||||
}
|
||||
|
||||
UnixSocketPermission = uint32(UnixSocketPermissionParsed)
|
||||
if !filepath.IsAbs(HTTPAddr) {
|
||||
HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr)
|
||||
}
|
||||
default:
|
||||
log.Fatal("Invalid PROTOCOL %q", protocolCfg)
|
||||
}
|
||||
UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false)
|
||||
ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").MustBool(false)
|
||||
ProxyProtocolHeaderTimeout = sec.Key("PROXY_PROTOCOL_HEADER_TIMEOUT").MustDuration(5 * time.Second)
|
||||
ProxyProtocolAcceptUnknown = sec.Key("PROXY_PROTOCOL_ACCEPT_UNKNOWN").MustBool(false)
|
||||
GracefulRestartable = sec.Key("ALLOW_GRACEFUL_RESTARTS").MustBool(true)
|
||||
GracefulHammerTime = sec.Key("GRACEFUL_HAMMER_TIME").MustDuration(60 * time.Second)
|
||||
StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(0 * time.Second)
|
||||
PerWriteTimeout = sec.Key("PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout)
|
||||
PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
|
||||
|
||||
defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort
|
||||
AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL)
|
||||
PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLAuto)
|
||||
if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy && PublicURLDetection != PublicURLNever {
|
||||
log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection)
|
||||
}
|
||||
|
||||
// Check validity of AppURL
|
||||
appURL, err := url.Parse(AppURL)
|
||||
if err != nil {
|
||||
log.Fatal("Invalid ROOT_URL %q: %s", AppURL, err)
|
||||
}
|
||||
// Remove default ports from AppURL.
|
||||
// (scheme-based URL normalization, RFC 3986 section 6.2.3)
|
||||
if (appURL.Scheme == string(HTTP) && appURL.Port() == "80") || (appURL.Scheme == string(HTTPS) && appURL.Port() == "443") {
|
||||
appURL.Host = appURL.Hostname()
|
||||
}
|
||||
// This should be TrimRight to ensure that there is only a single '/' at the end of AppURL.
|
||||
AppURL = strings.TrimRight(appURL.String(), "/") + "/"
|
||||
|
||||
// AppSubURL should start with '/' and end without '/', such as '/{subpath}'.
|
||||
// This value is empty if site does not have sub-url.
|
||||
AppSubURL = strings.TrimSuffix(appURL.Path, "/")
|
||||
UseSubURLPath = sec.Key("USE_SUB_URL_PATH").MustBool(false)
|
||||
StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/")
|
||||
|
||||
// Check if Domain differs from AppURL domain than update it to AppURL's domain
|
||||
urlHostname := appURL.Hostname()
|
||||
if urlHostname != Domain && net.ParseIP(urlHostname) == nil && urlHostname != "" {
|
||||
Domain = urlHostname
|
||||
}
|
||||
|
||||
var defaultLocalURL string
|
||||
switch Protocol {
|
||||
case HTTPUnix:
|
||||
defaultLocalURL = "http://unix/"
|
||||
case FCGI:
|
||||
defaultLocalURL = AppURL
|
||||
case FCGIUnix:
|
||||
defaultLocalURL = AppURL
|
||||
case HTTP, HTTPS:
|
||||
defaultLocalURL = string(Protocol) + "://"
|
||||
if HTTPAddr == "0.0.0.0" {
|
||||
defaultLocalURL += net.JoinHostPort("localhost", HTTPPort) + "/"
|
||||
} else {
|
||||
defaultLocalURL += net.JoinHostPort(HTTPAddr, HTTPPort) + "/"
|
||||
}
|
||||
default:
|
||||
log.Fatal("Invalid PROTOCOL %q", Protocol)
|
||||
}
|
||||
LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(defaultLocalURL)
|
||||
LocalURL = strings.TrimRight(LocalURL, "/") + "/"
|
||||
LocalUseProxyProtocol = sec.Key("LOCAL_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol)
|
||||
RedirectOtherPort = sec.Key("REDIRECT_OTHER_PORT").MustBool(false)
|
||||
PortToRedirect = sec.Key("PORT_TO_REDIRECT").MustString("80")
|
||||
RedirectorUseProxyProtocol = sec.Key("REDIRECTOR_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol)
|
||||
if len(StaticRootPath) == 0 {
|
||||
StaticRootPath = AppWorkPath
|
||||
}
|
||||
StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(StaticRootPath)
|
||||
StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour)
|
||||
AppDataPath = sec.Key("APP_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data"))
|
||||
if !filepath.IsAbs(AppDataPath) {
|
||||
AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath))
|
||||
}
|
||||
if IsInTesting && HasInstallLock(rootCfg) {
|
||||
// FIXME: in testing, the "app data" directory is not correctly initialized before loading settings
|
||||
if _, err := os.Stat(AppDataPath); err != nil {
|
||||
_ = os.MkdirAll(AppDataPath, os.ModePerm)
|
||||
}
|
||||
}
|
||||
|
||||
appTempPathInternal = sec.Key("APP_TEMP_PATH").String()
|
||||
if appTempPathInternal != "" {
|
||||
if _, err := os.Stat(appTempPathInternal); err != nil {
|
||||
log.Fatal("APP_TEMP_PATH %q is not accessible: %v", appTempPathInternal, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: GOLANG-HTTP-TMPDIR: Some Golang packages (like "http") use os.TempDir() to create temporary files when uploading files.
|
||||
// So ideally we should set the TMPDIR environment variable to make them use our managed temp directory.
|
||||
// But there is no clear place to set it currently, for example: when running "install" page, the AppDataPath is not ready yet, then AppDataTempDir won't work
|
||||
|
||||
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
|
||||
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
|
||||
PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))
|
||||
if !filepath.IsAbs(PprofDataPath) {
|
||||
PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath)
|
||||
}
|
||||
checkOverlappedPath("[server].PPROF_DATA_PATH", PprofDataPath)
|
||||
|
||||
landingPage := sec.Key("LANDING_PAGE").MustString("home")
|
||||
switch landingPage {
|
||||
case "explore":
|
||||
LandingPageURL = LandingPageExplore
|
||||
case "organizations":
|
||||
LandingPageURL = LandingPageOrganizations
|
||||
case "login":
|
||||
LandingPageURL = LandingPageLogin
|
||||
case "", "home":
|
||||
LandingPageURL = LandingPageHome
|
||||
default:
|
||||
LandingPageURL = LandingPage(landingPage)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/structs"
|
||||
)
|
||||
|
||||
// enumerates all the types of captchas
|
||||
const (
|
||||
ImageCaptcha = "image"
|
||||
ReCaptcha = "recaptcha"
|
||||
HCaptcha = "hcaptcha"
|
||||
MCaptcha = "mcaptcha"
|
||||
CfTurnstile = "cfturnstile"
|
||||
)
|
||||
|
||||
// Service settings
|
||||
var Service = struct {
|
||||
DefaultUserVisibility string
|
||||
DefaultUserVisibilityMode structs.VisibleType
|
||||
AllowedUserVisibilityModes []string
|
||||
AllowedUserVisibilityModesSlice AllowedVisibility `ini:"-"`
|
||||
DefaultOrgVisibility string
|
||||
DefaultOrgVisibilityMode structs.VisibleType
|
||||
ActiveCodeLives int
|
||||
ResetPwdCodeLives int
|
||||
RegisterEmailConfirm bool
|
||||
RegisterManualConfirm bool
|
||||
EmailDomainAllowList []glob.Glob
|
||||
EmailDomainBlockList []glob.Glob
|
||||
DisableRegistration bool
|
||||
AllowOnlyInternalRegistration bool
|
||||
AllowOnlyExternalRegistration bool
|
||||
ShowRegistrationButton bool
|
||||
EnablePasswordSignInForm bool
|
||||
ShowMilestonesDashboardPage bool
|
||||
RequireSignInViewStrict bool
|
||||
BlockAnonymousAccessExpensive bool
|
||||
EnableNotifyMail bool
|
||||
EnableBasicAuth bool
|
||||
EnablePasskeyAuth bool
|
||||
EnableReverseProxyAuth bool
|
||||
EnableReverseProxyAuthAPI bool
|
||||
EnableReverseProxyAutoRegister bool
|
||||
EnableReverseProxyEmail bool
|
||||
EnableReverseProxyFullName bool
|
||||
EnableCaptcha bool
|
||||
RequireCaptchaForLogin bool
|
||||
RequireExternalRegistrationCaptcha bool
|
||||
RequireExternalRegistrationPassword bool
|
||||
CaptchaType string
|
||||
RecaptchaSecret string
|
||||
RecaptchaSitekey string
|
||||
RecaptchaURL string
|
||||
CfTurnstileSecret string
|
||||
CfTurnstileSitekey string
|
||||
HcaptchaSecret string
|
||||
HcaptchaSitekey string
|
||||
McaptchaSecret string
|
||||
McaptchaSitekey string
|
||||
McaptchaURL string
|
||||
DefaultKeepEmailPrivate bool
|
||||
DefaultAllowCreateOrganization bool
|
||||
DefaultUserIsRestricted bool
|
||||
EnableTimetracking bool
|
||||
DefaultEnableTimetracking bool
|
||||
DefaultEnableDependencies bool
|
||||
AllowCrossRepositoryDependencies bool
|
||||
DefaultAllowOnlyContributorsToTrackTime bool
|
||||
NoReplyAddress string
|
||||
UserLocationMapURL string
|
||||
EnableUserHeatmap bool
|
||||
AutoWatchNewRepos bool
|
||||
AutoWatchOnChanges bool
|
||||
DefaultOrgMemberVisible bool
|
||||
UserDeleteWithCommentsMaxTime time.Duration
|
||||
ValidSiteURLSchemes []string
|
||||
|
||||
// OpenID settings
|
||||
EnableOpenIDSignIn bool
|
||||
EnableOpenIDSignUp bool
|
||||
OpenIDWhitelist []*regexp.Regexp
|
||||
OpenIDBlacklist []*regexp.Regexp
|
||||
|
||||
// Explore page settings
|
||||
Explore struct {
|
||||
RequireSigninView bool `ini:"REQUIRE_SIGNIN_VIEW"`
|
||||
DisableUsersPage bool `ini:"DISABLE_USERS_PAGE"`
|
||||
DisableOrganizationsPage bool `ini:"DISABLE_ORGANIZATIONS_PAGE"`
|
||||
DisableCodePage bool `ini:"DISABLE_CODE_PAGE"`
|
||||
} `ini:"service.explore"`
|
||||
|
||||
QoS struct {
|
||||
Enabled bool
|
||||
MaxInFlightRequests int
|
||||
MaxWaitingRequests int
|
||||
TargetWaitTime time.Duration
|
||||
}
|
||||
}{
|
||||
AllowedUserVisibilityModesSlice: []bool{true, true, true},
|
||||
}
|
||||
|
||||
// AllowedVisibility store in a 3 item bool array what is allowed
|
||||
type AllowedVisibility []bool
|
||||
|
||||
// IsAllowedVisibility check if a AllowedVisibility allow a specific VisibleType
|
||||
func (a AllowedVisibility) IsAllowedVisibility(t structs.VisibleType) bool {
|
||||
if int(t) >= len(a) {
|
||||
return false
|
||||
}
|
||||
return a[t]
|
||||
}
|
||||
|
||||
// ToVisibleTypeSlice convert a AllowedVisibility into a VisibleType slice
|
||||
func (a AllowedVisibility) ToVisibleTypeSlice() (result []structs.VisibleType) {
|
||||
for i, v := range a {
|
||||
if v {
|
||||
result = append(result, structs.VisibleType(i))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func CompileEmailGlobList(sec ConfigSection, keys ...string) (globs []glob.Glob) {
|
||||
for _, key := range keys {
|
||||
list := sec.Key(key).Strings(",")
|
||||
for _, s := range list {
|
||||
if g, err := glob.Compile(s); err == nil {
|
||||
globs = append(globs, g)
|
||||
} else {
|
||||
log.Error("Skip invalid email allow/block list expression %q: %v", s, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return globs
|
||||
}
|
||||
|
||||
func loadServiceFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("service")
|
||||
Service.ActiveCodeLives = sec.Key("ACTIVE_CODE_LIVE_MINUTES").MustInt(180)
|
||||
Service.ResetPwdCodeLives = sec.Key("RESET_PASSWD_CODE_LIVE_MINUTES").MustInt(180)
|
||||
Service.DisableRegistration = sec.Key("DISABLE_REGISTRATION").MustBool()
|
||||
Service.AllowOnlyInternalRegistration = sec.Key("ALLOW_ONLY_INTERNAL_REGISTRATION").MustBool()
|
||||
Service.AllowOnlyExternalRegistration = sec.Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").MustBool()
|
||||
if Service.AllowOnlyExternalRegistration && Service.AllowOnlyInternalRegistration {
|
||||
log.Warn("ALLOW_ONLY_INTERNAL_REGISTRATION and ALLOW_ONLY_EXTERNAL_REGISTRATION are true - disabling registration")
|
||||
Service.DisableRegistration = true
|
||||
}
|
||||
if !sec.Key("REGISTER_EMAIL_CONFIRM").MustBool() {
|
||||
Service.RegisterManualConfirm = sec.Key("REGISTER_MANUAL_CONFIRM").MustBool(false)
|
||||
} else {
|
||||
Service.RegisterManualConfirm = false
|
||||
}
|
||||
if sec.HasKey("EMAIL_DOMAIN_WHITELIST") {
|
||||
deprecatedSetting(rootCfg, "service", "EMAIL_DOMAIN_WHITELIST", "service", "EMAIL_DOMAIN_ALLOWLIST", "1.21")
|
||||
}
|
||||
Service.EmailDomainAllowList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_WHITELIST", "EMAIL_DOMAIN_ALLOWLIST")
|
||||
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
|
||||
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
|
||||
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
|
||||
|
||||
// boolean values are considered as "strict"
|
||||
var err error
|
||||
Service.RequireSignInViewStrict, err = sec.Key("REQUIRE_SIGNIN_VIEW").Bool()
|
||||
if s := sec.Key("REQUIRE_SIGNIN_VIEW").String(); err != nil && s != "" {
|
||||
// non-boolean value only supports "expensive" at the moment
|
||||
Service.BlockAnonymousAccessExpensive = s == "expensive"
|
||||
if !Service.BlockAnonymousAccessExpensive {
|
||||
log.Fatal("Invalid config option: REQUIRE_SIGNIN_VIEW = %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
|
||||
Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
|
||||
Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)
|
||||
Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
|
||||
Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool()
|
||||
Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool()
|
||||
Service.EnableReverseProxyEmail = sec.Key("ENABLE_REVERSE_PROXY_EMAIL").MustBool()
|
||||
Service.EnableReverseProxyFullName = sec.Key("ENABLE_REVERSE_PROXY_FULL_NAME").MustBool()
|
||||
Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool(false)
|
||||
Service.RequireCaptchaForLogin = sec.Key("REQUIRE_CAPTCHA_FOR_LOGIN").MustBool(false)
|
||||
Service.RequireExternalRegistrationCaptcha = sec.Key("REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA").MustBool(Service.EnableCaptcha)
|
||||
Service.RequireExternalRegistrationPassword = sec.Key("REQUIRE_EXTERNAL_REGISTRATION_PASSWORD").MustBool()
|
||||
Service.CaptchaType = sec.Key("CAPTCHA_TYPE").MustString(ImageCaptcha)
|
||||
Service.RecaptchaSecret = sec.Key("RECAPTCHA_SECRET").MustString("")
|
||||
Service.RecaptchaSitekey = sec.Key("RECAPTCHA_SITEKEY").MustString("")
|
||||
Service.RecaptchaURL = sec.Key("RECAPTCHA_URL").MustString("https://www.google.com/recaptcha/")
|
||||
Service.CfTurnstileSecret = sec.Key("CF_TURNSTILE_SECRET").MustString("")
|
||||
Service.CfTurnstileSitekey = sec.Key("CF_TURNSTILE_SITEKEY").MustString("")
|
||||
Service.HcaptchaSecret = sec.Key("HCAPTCHA_SECRET").MustString("")
|
||||
Service.HcaptchaSitekey = sec.Key("HCAPTCHA_SITEKEY").MustString("")
|
||||
Service.McaptchaURL = sec.Key("MCAPTCHA_URL").MustString("https://demo.mcaptcha.org/")
|
||||
Service.McaptchaSecret = sec.Key("MCAPTCHA_SECRET").MustString("")
|
||||
Service.McaptchaSitekey = sec.Key("MCAPTCHA_SITEKEY").MustString("")
|
||||
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
||||
Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
|
||||
Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false)
|
||||
Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true)
|
||||
if Service.EnableTimetracking {
|
||||
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
|
||||
}
|
||||
Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
|
||||
Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true)
|
||||
Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
|
||||
Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply." + Domain)
|
||||
Service.UserLocationMapURL = sec.Key("USER_LOCATION_MAP_URL").String()
|
||||
Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)
|
||||
Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true)
|
||||
Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false)
|
||||
modes := sec.Key("ALLOWED_USER_VISIBILITY_MODES").Strings(",")
|
||||
if len(modes) != 0 {
|
||||
Service.AllowedUserVisibilityModes = []string{}
|
||||
Service.AllowedUserVisibilityModesSlice = []bool{false, false, false}
|
||||
for _, sMode := range modes {
|
||||
if tp, ok := structs.VisibilityModes[sMode]; ok { // remove unsupported modes
|
||||
Service.AllowedUserVisibilityModes = append(Service.AllowedUserVisibilityModes, sMode)
|
||||
Service.AllowedUserVisibilityModesSlice[tp] = true
|
||||
} else {
|
||||
log.Warn("ALLOWED_USER_VISIBILITY_MODES %s is unsupported", sMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(Service.AllowedUserVisibilityModes) == 0 {
|
||||
Service.AllowedUserVisibilityModes = []string{"public", "limited", "private"}
|
||||
Service.AllowedUserVisibilityModesSlice = []bool{true, true, true}
|
||||
}
|
||||
|
||||
Service.DefaultUserVisibility = sec.Key("DEFAULT_USER_VISIBILITY").String()
|
||||
if Service.DefaultUserVisibility == "" {
|
||||
Service.DefaultUserVisibility = Service.AllowedUserVisibilityModes[0]
|
||||
} else if !Service.AllowedUserVisibilityModesSlice[structs.VisibilityModes[Service.DefaultUserVisibility]] {
|
||||
log.Warn("DEFAULT_USER_VISIBILITY %s is wrong or not in ALLOWED_USER_VISIBILITY_MODES, using first allowed", Service.DefaultUserVisibility)
|
||||
Service.DefaultUserVisibility = Service.AllowedUserVisibilityModes[0]
|
||||
}
|
||||
Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility]
|
||||
Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes))
|
||||
Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility]
|
||||
Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool()
|
||||
Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0)
|
||||
sec.Key("VALID_SITE_URL_SCHEMES").MustString("http,https")
|
||||
Service.ValidSiteURLSchemes = sec.Key("VALID_SITE_URL_SCHEMES").Strings(",")
|
||||
schemes := make([]string, 0, len(Service.ValidSiteURLSchemes))
|
||||
for _, scheme := range Service.ValidSiteURLSchemes {
|
||||
scheme = strings.ToLower(strings.TrimSpace(scheme))
|
||||
if scheme != "" {
|
||||
schemes = append(schemes, scheme)
|
||||
}
|
||||
}
|
||||
Service.ValidSiteURLSchemes = schemes
|
||||
|
||||
mustMapSetting(rootCfg, "service.explore", &Service.Explore)
|
||||
|
||||
loadOpenIDSetting(rootCfg)
|
||||
loadQosSetting(rootCfg)
|
||||
}
|
||||
|
||||
func loadOpenIDSetting(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("openid")
|
||||
Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock)
|
||||
Service.EnableOpenIDSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(!Service.DisableRegistration && Service.EnableOpenIDSignIn)
|
||||
pats := sec.Key("WHITELISTED_URIS").Strings(" ")
|
||||
if len(pats) != 0 {
|
||||
Service.OpenIDWhitelist = make([]*regexp.Regexp, len(pats))
|
||||
for i, p := range pats {
|
||||
Service.OpenIDWhitelist[i] = regexp.MustCompilePOSIX(p)
|
||||
}
|
||||
}
|
||||
pats = sec.Key("BLACKLISTED_URIS").Strings(" ")
|
||||
if len(pats) != 0 {
|
||||
Service.OpenIDBlacklist = make([]*regexp.Regexp, len(pats))
|
||||
for i, p := range pats {
|
||||
Service.OpenIDBlacklist[i] = regexp.MustCompilePOSIX(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadQosSetting(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("qos")
|
||||
Service.QoS.Enabled = sec.Key("ENABLED").MustBool(false)
|
||||
Service.QoS.MaxInFlightRequests = sec.Key("MAX_INFLIGHT").MustInt(4 * runtime.NumCPU())
|
||||
Service.QoS.MaxWaitingRequests = sec.Key("MAX_WAITING").MustInt(100)
|
||||
Service.QoS.TargetWaitTime = sec.Key("TARGET_WAIT_TIME").MustDuration(250 * time.Millisecond)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadServices(t *testing.T) {
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[service]
|
||||
EMAIL_DOMAIN_WHITELIST = d1, *.w
|
||||
EMAIL_DOMAIN_ALLOWLIST = d2, *.a
|
||||
EMAIL_DOMAIN_BLOCKLIST = d3, *.b
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
|
||||
match := func(globs []glob.Glob, s string) bool {
|
||||
for _, g := range globs {
|
||||
if g.Match(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
assert.True(t, match(Service.EmailDomainAllowList, "d1"))
|
||||
assert.True(t, match(Service.EmailDomainAllowList, "foo.w"))
|
||||
assert.True(t, match(Service.EmailDomainAllowList, "d2"))
|
||||
assert.True(t, match(Service.EmailDomainAllowList, "foo.a"))
|
||||
assert.False(t, match(Service.EmailDomainAllowList, "d3"))
|
||||
|
||||
assert.True(t, match(Service.EmailDomainBlockList, "d3"))
|
||||
assert.True(t, match(Service.EmailDomainBlockList, "foo.b"))
|
||||
assert.False(t, match(Service.EmailDomainBlockList, "d1"))
|
||||
}
|
||||
|
||||
func TestLoadServiceVisibilityModes(t *testing.T) {
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
kases := map[string]func(){
|
||||
`
|
||||
[service]
|
||||
DEFAULT_USER_VISIBILITY = public
|
||||
ALLOWED_USER_VISIBILITY_MODES = public,limited,private
|
||||
`: func() {
|
||||
assert.Equal(t, "public", Service.DefaultUserVisibility)
|
||||
assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode)
|
||||
assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes)
|
||||
},
|
||||
`
|
||||
[service]
|
||||
DEFAULT_USER_VISIBILITY = public
|
||||
`: func() {
|
||||
assert.Equal(t, "public", Service.DefaultUserVisibility)
|
||||
assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode)
|
||||
assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes)
|
||||
},
|
||||
`
|
||||
[service]
|
||||
DEFAULT_USER_VISIBILITY = limited
|
||||
`: func() {
|
||||
assert.Equal(t, "limited", Service.DefaultUserVisibility)
|
||||
assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultUserVisibilityMode)
|
||||
assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes)
|
||||
},
|
||||
`
|
||||
[service]
|
||||
ALLOWED_USER_VISIBILITY_MODES = public,limited,private
|
||||
`: func() {
|
||||
assert.Equal(t, "public", Service.DefaultUserVisibility)
|
||||
assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode)
|
||||
assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes)
|
||||
},
|
||||
`
|
||||
[service]
|
||||
DEFAULT_USER_VISIBILITY = public
|
||||
ALLOWED_USER_VISIBILITY_MODES = limited,private
|
||||
`: func() {
|
||||
assert.Equal(t, "limited", Service.DefaultUserVisibility)
|
||||
assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultUserVisibilityMode)
|
||||
assert.Equal(t, []string{"limited", "private"}, Service.AllowedUserVisibilityModes)
|
||||
},
|
||||
`
|
||||
[service]
|
||||
DEFAULT_USER_VISIBILITY = my_type
|
||||
ALLOWED_USER_VISIBILITY_MODES = limited,private
|
||||
`: func() {
|
||||
assert.Equal(t, "limited", Service.DefaultUserVisibility)
|
||||
assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultUserVisibilityMode)
|
||||
assert.Equal(t, []string{"limited", "private"}, Service.AllowedUserVisibilityModes)
|
||||
},
|
||||
`
|
||||
[service]
|
||||
DEFAULT_USER_VISIBILITY = public
|
||||
ALLOWED_USER_VISIBILITY_MODES = public, limit, privated
|
||||
`: func() {
|
||||
assert.Equal(t, "public", Service.DefaultUserVisibility)
|
||||
assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode)
|
||||
assert.Equal(t, []string{"public"}, Service.AllowedUserVisibilityModes)
|
||||
},
|
||||
}
|
||||
|
||||
for kase, fun := range kases {
|
||||
t.Run(kase, func(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(kase)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
fun()
|
||||
// reset
|
||||
Service.AllowedUserVisibilityModesSlice = []bool{true, true, true}
|
||||
Service.AllowedUserVisibilityModes = []string{}
|
||||
Service.DefaultUserVisibility = ""
|
||||
Service.DefaultUserVisibilityMode = structs.VisibleTypePublic
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadServiceRequireSignInView(t *testing.T) {
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[service]
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.False(t, Service.RequireSignInViewStrict)
|
||||
assert.False(t, Service.BlockAnonymousAccessExpensive)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[service]
|
||||
REQUIRE_SIGNIN_VIEW = true
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.True(t, Service.RequireSignInViewStrict)
|
||||
assert.False(t, Service.BlockAnonymousAccessExpensive)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[service]
|
||||
REQUIRE_SIGNIN_VIEW = expensive
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.False(t, Service.RequireSignInViewStrict)
|
||||
assert.True(t, Service.BlockAnonymousAccessExpensive)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// SessionConfig defines Session settings
|
||||
var SessionConfig = struct {
|
||||
OriginalProvider string
|
||||
Provider string
|
||||
// Provider configuration, it's corresponding to provider.
|
||||
ProviderConfig string
|
||||
// Cookie name to save session ID. Default is "MacaronSession".
|
||||
CookieName string
|
||||
// Cookie path to store. Default is "/".
|
||||
CookiePath string
|
||||
// GC interval time in seconds. Default is 3600.
|
||||
Gclifetime int64
|
||||
// Max life time in seconds. Default is whatever GC interval time is.
|
||||
Maxlifetime int64
|
||||
// Use HTTPS only. Default is false.
|
||||
Secure bool
|
||||
// Cookie domain name. Default is empty.
|
||||
Domain string
|
||||
// SameSite declares if your cookie should be restricted to a first-party or same-site context. Valid strings are "none", "lax", "strict". Default is "lax"
|
||||
SameSite http.SameSite
|
||||
}{
|
||||
CookieName: "i_like_gitea",
|
||||
Gclifetime: 86400,
|
||||
Maxlifetime: 86400,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
func loadSessionFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("session")
|
||||
SessionConfig.Provider = sec.Key("PROVIDER").In("memory",
|
||||
[]string{"memory", "file", "redis", "mysql", "postgres", "couchbase", "memcache", "db"})
|
||||
SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(filepath.Join(AppDataPath, "sessions")), "\" ")
|
||||
if SessionConfig.Provider == "file" && !filepath.IsAbs(SessionConfig.ProviderConfig) {
|
||||
SessionConfig.ProviderConfig = filepath.Join(AppWorkPath, SessionConfig.ProviderConfig)
|
||||
checkOverlappedPath("[session].PROVIDER_CONFIG", SessionConfig.ProviderConfig)
|
||||
}
|
||||
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
|
||||
// HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the Install page, so there is no CookiePath
|
||||
SessionConfig.CookiePath = util.IfZero(AppSubURL, "/")
|
||||
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
|
||||
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
|
||||
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
|
||||
SessionConfig.Domain = sec.Key("DOMAIN").String()
|
||||
samesiteString := sec.Key("SAME_SITE").In("lax", []string{"none", "lax", "strict"})
|
||||
switch strings.ToLower(samesiteString) {
|
||||
case "none":
|
||||
SessionConfig.SameSite = http.SameSiteNoneMode
|
||||
case "strict":
|
||||
SessionConfig.SameSite = http.SameSiteStrictMode
|
||||
default:
|
||||
SessionConfig.SameSite = http.SameSiteLaxMode
|
||||
}
|
||||
shadowConfig, err := json.Marshal(SessionConfig)
|
||||
if err != nil {
|
||||
log.Fatal("Can't shadow session config: %v", err)
|
||||
}
|
||||
SessionConfig.ProviderConfig = string(shadowConfig)
|
||||
SessionConfig.OriginalProvider = SessionConfig.Provider
|
||||
SessionConfig.Provider = "VirtualSession"
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/user"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// settings
|
||||
var (
|
||||
// AppVer is the version of the current build of Gitea. It is set in main.go from main.Version.
|
||||
AppVer string
|
||||
// AppBuiltWith represents a human-readable version go runtime build version and build tags. (See main.go formatBuiltWith().)
|
||||
AppBuiltWith string
|
||||
// AppStartTime store time gitea has started
|
||||
AppStartTime time.Time
|
||||
|
||||
CfgProvider ConfigProvider
|
||||
IsWindows bool
|
||||
|
||||
// IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for:
|
||||
// * Skip nonsense error logs during testing caused by unreliable code (TODO: this is only a temporary solution, we should make the test code more reliable)
|
||||
// * Panic in dev or testing mode to make the problem more obvious and easier to debug
|
||||
// * Mock some functions or options to make testing easier (eg: session store, time, URL detection, etc.)
|
||||
IsInTesting = false
|
||||
)
|
||||
|
||||
func init() {
|
||||
IsWindows = runtime.GOOS == "windows"
|
||||
if AppVer == "" {
|
||||
AppVer = "dev"
|
||||
}
|
||||
|
||||
// FIXME: the logger shouldn't be initialized here, the app entry should initialize the logger
|
||||
// By default set this logger at Info - we'll change it later, but we need to start with something.
|
||||
log.SetupStderrLogger(log.DEFAULT, "console-stderr", log.INFO)
|
||||
}
|
||||
|
||||
// IsRunUserMatchCurrentUser returns false if configured run user does not match
|
||||
// actual user that runs the app. The first return value is the actual user name.
|
||||
// This check is ignored under Windows since SSH remote login is not the main
|
||||
// method to login on Windows.
|
||||
func IsRunUserMatchCurrentUser(runUser string) (string, bool) {
|
||||
if IsWindows || SSH.StartBuiltinServer {
|
||||
return "", true
|
||||
}
|
||||
|
||||
currentUser := user.CurrentUsername()
|
||||
return currentUser, runUser == currentUser
|
||||
}
|
||||
|
||||
func IsInE2eTesting() bool {
|
||||
return os.Getenv("GITEA_TEST_E2E") == "true"
|
||||
}
|
||||
|
||||
// PrepareAppDataPath creates app data directory if necessary
|
||||
func PrepareAppDataPath() error {
|
||||
// FIXME: There are too many calls to MkdirAll in old code. It is incorrect.
|
||||
// For example, if someDir=/mnt/vol1/gitea-home/data, if the mount point /mnt/vol1 is not mounted when Gitea runs,
|
||||
// then gitea will make new empty directories in /mnt/vol1, all are stored in the root filesystem.
|
||||
// The correct behavior should be: creating parent directories is end users' duty. We only create sub-directories in existing parent directories.
|
||||
// For quickstart, the parent directories should be created automatically for first startup (eg: a flag or a check of INSTALL_LOCK).
|
||||
// Now we can take the first step to do correctly (using Mkdir) in other packages, and prepare the AppDataPath here, then make a refactor in future.
|
||||
|
||||
st, err := os.Stat(AppDataPath)
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(AppDataPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create the APP_DATA_PATH directory: %q, Error: %w", AppDataPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to use APP_DATA_PATH %q. Error: %w", AppDataPath, err)
|
||||
}
|
||||
|
||||
if !st.IsDir() /* also works for symlink */ {
|
||||
return fmt.Errorf("the APP_DATA_PATH %q is not a directory (or symlink to a directory) and can't be used", AppDataPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitCfgProvider(file string) {
|
||||
var err error
|
||||
if CfgProvider, err = NewConfigProviderFromFile(file); err != nil {
|
||||
log.Fatal("Unable to init config provider from %q: %v", file, err)
|
||||
}
|
||||
CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls
|
||||
}
|
||||
|
||||
func MustInstalled() {
|
||||
if !InstallLock {
|
||||
log.Fatal(`Unable to load config file for a installed Gitea instance, you should either use "--config" to set your config file (app.ini), or run "gitea web" command to install Gitea.`)
|
||||
}
|
||||
}
|
||||
|
||||
func LoadCommonSettings() {
|
||||
if err := loadCommonSettingsFrom(CfgProvider); err != nil {
|
||||
log.Fatal("Unable to load settings from config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// loadCommonSettingsFrom loads common configurations from a configuration provider.
|
||||
func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||
// a lot of logic depends on InstallLock value, so it must be loaded before any other settings
|
||||
InstallLock = HasInstallLock(cfg)
|
||||
|
||||
// WARNING: don't change the sequence except you know what you are doing.
|
||||
loadRunModeFrom(cfg)
|
||||
loadLogGlobalFrom(cfg)
|
||||
loadServerFrom(cfg)
|
||||
loadSSHFrom(cfg)
|
||||
|
||||
mustCurrentRunUserMatch(cfg) // it depends on the SSH config, only non-builtin SSH server requires this check
|
||||
|
||||
loadOAuth2From(cfg)
|
||||
loadSecurityFrom(cfg)
|
||||
if err := loadAttachmentFrom(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadLFSFrom(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
loadTimeFrom(cfg)
|
||||
loadRepositoryFrom(cfg)
|
||||
if err := loadAvatarsFrom(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadRepoAvatarFrom(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadPackagesFrom(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadActionsFrom(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
loadUIFrom(cfg)
|
||||
loadAdminFrom(cfg)
|
||||
loadAPIFrom(cfg)
|
||||
loadMetricsFrom(cfg)
|
||||
loadCamoFrom(cfg)
|
||||
loadI18nFrom(cfg)
|
||||
loadGitFrom(cfg)
|
||||
loadMirrorFrom(cfg)
|
||||
loadMarkupFrom(cfg)
|
||||
loadGlobalLockFrom(cfg)
|
||||
loadOtherFrom(cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadRunModeFrom(rootCfg ConfigProvider) {
|
||||
rootSec := rootCfg.Section("")
|
||||
mustNotRunAsRoot(rootSec)
|
||||
|
||||
runModeValue := os.Getenv("GITEA_RUN_MODE")
|
||||
runModeValue = util.IfZero(runModeValue, rootSec.Key("RUN_MODE").String())
|
||||
// non-dev mode is treated as prod mode, to protect users from accidentally running in dev mode if there is a typo in this value.
|
||||
IsProd = !strings.EqualFold(runModeValue, "dev") // TODO: can use case-sensitive comparing in the future
|
||||
RunMode = util.Iif(IsProd, "prod", "dev")
|
||||
|
||||
// there is a separate check: mustCurrentRunUserMatch (IsRunUserMatchCurrentUser)
|
||||
RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
|
||||
}
|
||||
|
||||
func mustNotRunAsRoot(rootSec ConfigSection) {
|
||||
if os.Getuid() != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
mustRunAsRoot := os.Getenv("SNAP") != "" && os.Getenv("SNAP_NAME") != "" // snap container runs the app as uid=0
|
||||
if mustRunAsRoot {
|
||||
return
|
||||
}
|
||||
|
||||
// The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches.
|
||||
// Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly.
|
||||
allowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") || // check gitea config
|
||||
optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value() // check gitea env var
|
||||
|
||||
if !allowRunAsRoot {
|
||||
// Special thanks to VLC which inspired the wording of this messaging.
|
||||
log.Fatal("Gitea is not supposed to be run as root. If you need to use privileged TCP ports please instead use `setcap` and the `cap_net_bind_service` permission.")
|
||||
}
|
||||
log.Warn("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.")
|
||||
}
|
||||
|
||||
// HasInstallLock checks the install-lock in ConfigProvider directly, because sometimes the config file is not loaded into setting variables yet.
|
||||
func HasInstallLock(rootCfg ConfigProvider) bool {
|
||||
return rootCfg.Section("security").Key("INSTALL_LOCK").MustBool(false)
|
||||
}
|
||||
|
||||
func mustCurrentRunUserMatch(rootCfg ConfigProvider) {
|
||||
// Does not check run user when the "InstallLock" is off.
|
||||
if HasInstallLock(rootCfg) {
|
||||
currentUser, match := IsRunUserMatchCurrentUser(RunUser)
|
||||
if !match {
|
||||
log.Fatal("Expect user '%s' (RUN_USER in app.ini) but current user is: %s", RunUser, currentUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LoadSettings initializes the settings for normal start up
|
||||
func LoadSettings() {
|
||||
initAllLoggers()
|
||||
|
||||
loadDBSetting(CfgProvider)
|
||||
loadServiceFrom(CfgProvider)
|
||||
loadOAuth2ClientFrom(CfgProvider)
|
||||
loadCacheFrom(CfgProvider)
|
||||
loadSessionFrom(CfgProvider)
|
||||
loadCorsFrom(CfgProvider)
|
||||
loadMailsFrom(CfgProvider)
|
||||
loadProxyFrom(CfgProvider)
|
||||
loadWebhookFrom(CfgProvider)
|
||||
loadMigrationsFrom(CfgProvider)
|
||||
loadIndexerFrom(CfgProvider)
|
||||
loadTaskFrom(CfgProvider)
|
||||
LoadQueueSettings()
|
||||
loadProjectFrom(CfgProvider)
|
||||
loadMimeTypeMapFrom(CfgProvider)
|
||||
loadFederationFrom(CfgProvider)
|
||||
}
|
||||
|
||||
// LoadSettingsForInstall initializes the settings for install
|
||||
func LoadSettingsForInstall() {
|
||||
loadDBSetting(CfgProvider)
|
||||
loadServiceFrom(CfgProvider)
|
||||
loadMailsFrom(CfgProvider)
|
||||
}
|
||||
|
||||
var configuredPaths = make(map[string]string)
|
||||
|
||||
func checkOverlappedPath(name, path string) {
|
||||
// TODO: some paths shouldn't overlap (storage.xxx.path), while some could (data path is the base path for storage path)
|
||||
if targetName, ok := configuredPaths[path]; ok && targetName != name {
|
||||
LogStartupProblem(1, log.ERROR, "Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name)
|
||||
}
|
||||
configuredPaths[path] = name
|
||||
}
|
||||
|
||||
func PanicInDevOrTesting(msg string, a ...any) {
|
||||
if !IsProd || IsInTesting {
|
||||
panic(fmt.Sprintf(msg, a...))
|
||||
}
|
||||
log.Error(msg, a...)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var SSH = struct {
|
||||
Disabled bool `ini:"DISABLE_SSH"`
|
||||
StartBuiltinServer bool `ini:"START_SSH_SERVER"`
|
||||
BuiltinServerUser string `ini:"BUILTIN_SSH_SERVER_USER"`
|
||||
UseProxyProtocol bool `ini:"SSH_SERVER_USE_PROXY_PROTOCOL"`
|
||||
Domain string `ini:"SSH_DOMAIN"`
|
||||
Port int `ini:"SSH_PORT"`
|
||||
User string `ini:"SSH_USER"`
|
||||
ListenHost string `ini:"SSH_LISTEN_HOST"`
|
||||
ListenPort int `ini:"SSH_LISTEN_PORT"`
|
||||
RootPath string `ini:"SSH_ROOT_PATH"`
|
||||
ServerCiphers []string `ini:"SSH_SERVER_CIPHERS"`
|
||||
ServerKeyExchanges []string `ini:"SSH_SERVER_KEY_EXCHANGES"`
|
||||
ServerMACs []string `ini:"SSH_SERVER_MACS"`
|
||||
ServerHostKeys []string `ini:"SSH_SERVER_HOST_KEYS"`
|
||||
AuthorizedKeysBackup bool `ini:"SSH_AUTHORIZED_KEYS_BACKUP"`
|
||||
AuthorizedPrincipalsBackup bool `ini:"SSH_AUTHORIZED_PRINCIPALS_BACKUP"`
|
||||
AuthorizedKeysCommandTemplate string `ini:"SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE"`
|
||||
AuthorizedKeysCommandTemplateTemplate *template.Template `ini:"-"`
|
||||
MinimumKeySizeCheck bool `ini:"-"`
|
||||
MinimumKeySizes map[string]int `ini:"-"`
|
||||
CreateAuthorizedKeysFile bool `ini:"SSH_CREATE_AUTHORIZED_KEYS_FILE"`
|
||||
CreateAuthorizedPrincipalsFile bool `ini:"SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE"`
|
||||
ExposeAnonymous bool `ini:"SSH_EXPOSE_ANONYMOUS"`
|
||||
AuthorizedPrincipalsAllow []string `ini:"SSH_AUTHORIZED_PRINCIPALS_ALLOW"`
|
||||
AuthorizedPrincipalsEnabled bool `ini:"-"`
|
||||
TrustedUserCAKeys []string `ini:"SSH_TRUSTED_USER_CA_KEYS"`
|
||||
TrustedUserCAKeysFile string `ini:"SSH_TRUSTED_USER_CA_KEYS_FILENAME"`
|
||||
TrustedUserCAKeysParsed []gossh.PublicKey `ini:"-"`
|
||||
PerWriteTimeout time.Duration `ini:"SSH_PER_WRITE_TIMEOUT"`
|
||||
PerWritePerKbTimeout time.Duration `ini:"SSH_PER_WRITE_PER_KB_TIMEOUT"`
|
||||
}{
|
||||
Disabled: false,
|
||||
StartBuiltinServer: false,
|
||||
Domain: "",
|
||||
Port: 22,
|
||||
MinimumKeySizeCheck: true,
|
||||
MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 3071},
|
||||
ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"},
|
||||
AuthorizedKeysCommandTemplate: "{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}",
|
||||
PerWriteTimeout: PerWriteTimeout,
|
||||
PerWritePerKbTimeout: PerWritePerKbTimeout,
|
||||
}
|
||||
|
||||
func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) {
|
||||
anything := false
|
||||
email := false
|
||||
username := false
|
||||
for _, value := range values {
|
||||
v := strings.ToLower(strings.TrimSpace(value))
|
||||
switch v {
|
||||
case "off":
|
||||
return []string{"off"}, false
|
||||
case "email":
|
||||
email = true
|
||||
case "username":
|
||||
username = true
|
||||
case "anything":
|
||||
anything = true
|
||||
}
|
||||
}
|
||||
if anything {
|
||||
return []string{"anything"}, true
|
||||
}
|
||||
|
||||
authorizedPrincipalsAllow := []string{}
|
||||
if username {
|
||||
authorizedPrincipalsAllow = append(authorizedPrincipalsAllow, "username")
|
||||
}
|
||||
if email {
|
||||
authorizedPrincipalsAllow = append(authorizedPrincipalsAllow, "email")
|
||||
}
|
||||
|
||||
return authorizedPrincipalsAllow, true
|
||||
}
|
||||
|
||||
func loadSSHFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("server")
|
||||
if len(SSH.Domain) == 0 {
|
||||
SSH.Domain = Domain
|
||||
}
|
||||
|
||||
homeDir, err := util.HomeDir()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to get home directory: %v", err)
|
||||
}
|
||||
homeDir = strings.ReplaceAll(homeDir, "\\", "/")
|
||||
|
||||
SSH.RootPath = filepath.Join(homeDir, ".ssh")
|
||||
|
||||
if err = sec.MapTo(&SSH); err != nil {
|
||||
log.Fatal("Failed to map SSH settings: %v", err)
|
||||
}
|
||||
|
||||
serverCiphers := sec.Key("SSH_SERVER_CIPHERS").Strings(",")
|
||||
SSH.ServerCiphers = util.Iif(len(serverCiphers) > 0, serverCiphers, nil)
|
||||
|
||||
serverKeyExchanges := sec.Key("SSH_SERVER_KEY_EXCHANGES").Strings(",")
|
||||
SSH.ServerKeyExchanges = util.Iif(len(serverKeyExchanges) > 0, serverKeyExchanges, nil)
|
||||
|
||||
serverMACs := sec.Key("SSH_SERVER_MACS").Strings(",")
|
||||
SSH.ServerMACs = util.Iif(len(serverMACs) > 0, serverMACs, nil)
|
||||
|
||||
for i, key := range SSH.ServerHostKeys {
|
||||
if !filepath.IsAbs(key) {
|
||||
SSH.ServerHostKeys[i] = filepath.Join(AppDataPath, key)
|
||||
}
|
||||
}
|
||||
|
||||
SSH.Port = sec.Key("SSH_PORT").MustInt(22)
|
||||
SSH.ListenPort = sec.Key("SSH_LISTEN_PORT").MustInt(SSH.Port)
|
||||
SSH.UseProxyProtocol = sec.Key("SSH_SERVER_USE_PROXY_PROTOCOL").MustBool(false)
|
||||
|
||||
// When disable SSH, start builtin server value is ignored.
|
||||
if SSH.Disabled {
|
||||
SSH.StartBuiltinServer = false
|
||||
}
|
||||
|
||||
SSH.TrustedUserCAKeysFile = sec.Key("SSH_TRUSTED_USER_CA_KEYS_FILENAME").MustString(filepath.Join(SSH.RootPath, "gitea-trusted-user-ca-keys.pem"))
|
||||
|
||||
for _, caKey := range SSH.TrustedUserCAKeys {
|
||||
pubKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(caKey))
|
||||
if err != nil {
|
||||
log.Fatal("Failed to parse TrustedUserCaKeys: %s %v", caKey, err)
|
||||
}
|
||||
|
||||
SSH.TrustedUserCAKeysParsed = append(SSH.TrustedUserCAKeysParsed, pubKey)
|
||||
}
|
||||
if len(SSH.TrustedUserCAKeys) > 0 {
|
||||
// Set the default as email,username otherwise we can leave it empty
|
||||
sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").MustString("username,email")
|
||||
} else {
|
||||
sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").MustString("off")
|
||||
}
|
||||
|
||||
SSH.AuthorizedPrincipalsAllow, SSH.AuthorizedPrincipalsEnabled = parseAuthorizedPrincipalsAllow(sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").Strings(","))
|
||||
|
||||
SSH.MinimumKeySizeCheck = sec.Key("MINIMUM_KEY_SIZE_CHECK").MustBool(SSH.MinimumKeySizeCheck)
|
||||
minimumKeySizes := rootCfg.Section("ssh.minimum_key_sizes").Keys()
|
||||
for _, key := range minimumKeySizes {
|
||||
if key.MustInt() != -1 {
|
||||
SSH.MinimumKeySizes[strings.ToLower(key.Name())] = key.MustInt()
|
||||
} else {
|
||||
delete(SSH.MinimumKeySizes, strings.ToLower(key.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
SSH.AuthorizedKeysBackup = sec.Key("SSH_AUTHORIZED_KEYS_BACKUP").MustBool(false)
|
||||
SSH.CreateAuthorizedKeysFile = sec.Key("SSH_CREATE_AUTHORIZED_KEYS_FILE").MustBool(true)
|
||||
|
||||
SSH.AuthorizedPrincipalsBackup = false
|
||||
SSH.CreateAuthorizedPrincipalsFile = false
|
||||
if SSH.AuthorizedPrincipalsEnabled {
|
||||
SSH.AuthorizedPrincipalsBackup = sec.Key("SSH_AUTHORIZED_PRINCIPALS_BACKUP").MustBool(true)
|
||||
SSH.CreateAuthorizedPrincipalsFile = sec.Key("SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE").MustBool(true)
|
||||
}
|
||||
|
||||
SSH.ExposeAnonymous = sec.Key("SSH_EXPOSE_ANONYMOUS").MustBool(false)
|
||||
SSH.AuthorizedKeysCommandTemplate = sec.Key("SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE").MustString(SSH.AuthorizedKeysCommandTemplate)
|
||||
|
||||
SSH.AuthorizedKeysCommandTemplateTemplate = template.Must(template.New("").Parse(SSH.AuthorizedKeysCommandTemplate))
|
||||
|
||||
SSH.PerWriteTimeout = sec.Key("SSH_PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout)
|
||||
SSH.PerWritePerKbTimeout = sec.Key("SSH_PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
|
||||
|
||||
// ensure parseRunModeSetting has been executed before this
|
||||
SSH.BuiltinServerUser = rootCfg.Section("server").Key("BUILTIN_SSH_SERVER_USER").MustString(RunUser)
|
||||
SSH.User = rootCfg.Section("server").Key("SSH_USER").MustString(SSH.BuiltinServerUser)
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StorageType is a type of Storage
|
||||
type StorageType string
|
||||
|
||||
const (
|
||||
// LocalStorageType is the type descriptor for local storage
|
||||
LocalStorageType StorageType = "local"
|
||||
// MinioStorageType is the type descriptor for minio storage
|
||||
MinioStorageType StorageType = "minio"
|
||||
// AzureBlobStorageType is the type descriptor for azure blob storage
|
||||
AzureBlobStorageType StorageType = "azureblob"
|
||||
)
|
||||
|
||||
var storageTypes = []StorageType{
|
||||
LocalStorageType,
|
||||
MinioStorageType,
|
||||
AzureBlobStorageType,
|
||||
}
|
||||
|
||||
// IsValidStorageType returns true if the given storage type is valid
|
||||
func IsValidStorageType(storageType StorageType) bool {
|
||||
return slices.Contains(storageTypes, storageType)
|
||||
}
|
||||
|
||||
// MinioStorageConfig represents the configuration for a minio storage
|
||||
type MinioStorageConfig struct {
|
||||
Endpoint string `ini:"MINIO_ENDPOINT" json:",omitempty"`
|
||||
AccessKeyID string `ini:"MINIO_ACCESS_KEY_ID" json:",omitempty"`
|
||||
SecretAccessKey string `ini:"MINIO_SECRET_ACCESS_KEY" json:",omitempty"`
|
||||
IamEndpoint string `ini:"MINIO_IAM_ENDPOINT" json:",omitempty"`
|
||||
Bucket string `ini:"MINIO_BUCKET" json:",omitempty"`
|
||||
Location string `ini:"MINIO_LOCATION" json:",omitempty"`
|
||||
BasePath string `ini:"MINIO_BASE_PATH" json:",omitempty"`
|
||||
UseSSL bool `ini:"MINIO_USE_SSL"`
|
||||
InsecureSkipVerify bool `ini:"MINIO_INSECURE_SKIP_VERIFY"`
|
||||
ChecksumAlgorithm string `ini:"MINIO_CHECKSUM_ALGORITHM" json:",omitempty"`
|
||||
ServeDirect bool `ini:"SERVE_DIRECT"`
|
||||
BucketLookUpType string `ini:"MINIO_BUCKET_LOOKUP_TYPE" json:",omitempty"`
|
||||
}
|
||||
|
||||
func (cfg *MinioStorageConfig) ToShadow() {
|
||||
if cfg.AccessKeyID != "" {
|
||||
cfg.AccessKeyID = "******"
|
||||
}
|
||||
if cfg.SecretAccessKey != "" {
|
||||
cfg.SecretAccessKey = "******"
|
||||
}
|
||||
}
|
||||
|
||||
// MinioStorageConfig represents the configuration for a minio storage
|
||||
type AzureBlobStorageConfig struct {
|
||||
Endpoint string `ini:"AZURE_BLOB_ENDPOINT" json:",omitempty"`
|
||||
AccountName string `ini:"AZURE_BLOB_ACCOUNT_NAME" json:",omitempty"`
|
||||
AccountKey string `ini:"AZURE_BLOB_ACCOUNT_KEY" json:",omitempty"`
|
||||
Container string `ini:"AZURE_BLOB_CONTAINER" json:",omitempty"`
|
||||
BasePath string `ini:"AZURE_BLOB_BASE_PATH" json:",omitempty"`
|
||||
ServeDirect bool `ini:"SERVE_DIRECT"`
|
||||
}
|
||||
|
||||
func (cfg *AzureBlobStorageConfig) ToShadow() {
|
||||
if cfg.AccountKey != "" {
|
||||
cfg.AccountKey = "******"
|
||||
}
|
||||
if cfg.AccountName != "" {
|
||||
cfg.AccountName = "******"
|
||||
}
|
||||
}
|
||||
|
||||
// Storage represents configuration of storages
|
||||
type Storage struct {
|
||||
Type StorageType // local or minio or azureblob
|
||||
Path string `json:",omitempty"` // for local type
|
||||
TemporaryPath string `json:",omitempty"`
|
||||
MinioConfig MinioStorageConfig // for minio type
|
||||
AzureBlobConfig AzureBlobStorageConfig // for azureblob type
|
||||
}
|
||||
|
||||
func (storage *Storage) ToShadowCopy() Storage {
|
||||
shadowStorage := *storage
|
||||
shadowStorage.MinioConfig.ToShadow()
|
||||
shadowStorage.AzureBlobConfig.ToShadow()
|
||||
return shadowStorage
|
||||
}
|
||||
|
||||
func (storage *Storage) ServeDirect() bool {
|
||||
return (storage.Type == MinioStorageType && storage.MinioConfig.ServeDirect) ||
|
||||
(storage.Type == AzureBlobStorageType && storage.AzureBlobConfig.ServeDirect)
|
||||
}
|
||||
|
||||
const storageSectionName = "storage"
|
||||
|
||||
func getDefaultStorageSection(rootCfg ConfigProvider) ConfigSection {
|
||||
storageSec := rootCfg.Section(storageSectionName)
|
||||
// Global Defaults
|
||||
storageSec.Key("STORAGE_TYPE").MustString("local")
|
||||
storageSec.Key("MINIO_ENDPOINT").MustString("localhost:9000")
|
||||
storageSec.Key("MINIO_ACCESS_KEY_ID").MustString("")
|
||||
storageSec.Key("MINIO_SECRET_ACCESS_KEY").MustString("")
|
||||
storageSec.Key("MINIO_BUCKET").MustString("gitea")
|
||||
storageSec.Key("MINIO_LOCATION").MustString("us-east-1")
|
||||
storageSec.Key("MINIO_USE_SSL").MustBool(false)
|
||||
storageSec.Key("MINIO_INSECURE_SKIP_VERIFY").MustBool(false)
|
||||
storageSec.Key("MINIO_CHECKSUM_ALGORITHM").MustString("default")
|
||||
storageSec.Key("MINIO_BUCKET_LOOKUP_TYPE").MustString("auto")
|
||||
storageSec.Key("AZURE_BLOB_ENDPOINT").MustString("")
|
||||
storageSec.Key("AZURE_BLOB_ACCOUNT_NAME").MustString("")
|
||||
storageSec.Key("AZURE_BLOB_ACCOUNT_KEY").MustString("")
|
||||
storageSec.Key("AZURE_BLOB_CONTAINER").MustString("gitea")
|
||||
return storageSec
|
||||
}
|
||||
|
||||
// getStorage will find target section and extra special section first and then read override
|
||||
// items from extra section
|
||||
func getStorage(rootCfg ConfigProvider, name, typ string, sec ConfigSection) (*Storage, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("no name for storage")
|
||||
}
|
||||
|
||||
targetSec, tp, err := getStorageTargetSection(rootCfg, name, typ, sec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
overrideSec := getStorageOverrideSection(rootCfg, sec, tp, name)
|
||||
|
||||
targetType := targetSec.Key("STORAGE_TYPE").String()
|
||||
switch targetType {
|
||||
case string(LocalStorageType):
|
||||
return getStorageForLocal(targetSec, overrideSec, tp, name)
|
||||
case string(MinioStorageType):
|
||||
return getStorageForMinio(targetSec, overrideSec, tp, name)
|
||||
case string(AzureBlobStorageType):
|
||||
return getStorageForAzureBlob(targetSec, overrideSec, tp, name)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported storage type %q", targetType)
|
||||
}
|
||||
}
|
||||
|
||||
type targetSecType int
|
||||
|
||||
const (
|
||||
targetSecIsTyp targetSecType = iota // target section is [storage.type] which the type from parameter
|
||||
targetSecIsStorage // target section is [storage]
|
||||
targetSecIsDefault // target section is the default value
|
||||
targetSecIsStorageWithName // target section is [storage.name]
|
||||
targetSecIsSec // target section is from the name seciont [name]
|
||||
)
|
||||
|
||||
func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetSecType, error) { //nolint:unparam // FIXME: targetSecType is always 0, wrong design?
|
||||
targetSec, err := rootCfg.GetSection(storageSectionName + "." + typ)
|
||||
if err != nil {
|
||||
if !IsValidStorageType(StorageType(typ)) {
|
||||
return nil, 0, fmt.Errorf("get section via storage type %q failed: %v", typ, err)
|
||||
}
|
||||
// if typ is a valid storage type, but there is no [storage.local] or [storage.minio] section
|
||||
// it's not an error
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
targetType := targetSec.Key("STORAGE_TYPE").String()
|
||||
if targetType == "" {
|
||||
if !IsValidStorageType(StorageType(typ)) {
|
||||
return nil, 0, fmt.Errorf("unknown storage type %q", typ)
|
||||
}
|
||||
targetSec.Key("STORAGE_TYPE").SetValue(typ)
|
||||
} else if !IsValidStorageType(StorageType(targetType)) {
|
||||
return nil, 0, fmt.Errorf("unknown storage type %q for section storage.%v", targetType, typ)
|
||||
}
|
||||
|
||||
return targetSec, targetSecIsTyp, nil
|
||||
}
|
||||
|
||||
func getStorageTargetSection(rootCfg ConfigProvider, name, typ string, sec ConfigSection) (ConfigSection, targetSecType, error) {
|
||||
// check typ first
|
||||
if typ == "" {
|
||||
if sec != nil { // check sec's type secondly
|
||||
typ = sec.Key("STORAGE_TYPE").String()
|
||||
if IsValidStorageType(StorageType(typ)) {
|
||||
if targetSec, _ := rootCfg.GetSection(storageSectionName + "." + typ); targetSec == nil {
|
||||
return sec, targetSecIsSec, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if typ != "" {
|
||||
targetSec, tp, err := getStorageSectionByType(rootCfg, typ)
|
||||
if targetSec != nil || err != nil {
|
||||
return targetSec, tp, err
|
||||
}
|
||||
}
|
||||
|
||||
// check storage name thirdly
|
||||
targetSec, _ := rootCfg.GetSection(storageSectionName + "." + name)
|
||||
if targetSec != nil {
|
||||
targetType := targetSec.Key("STORAGE_TYPE").String()
|
||||
switch targetType {
|
||||
case "":
|
||||
if targetSec.Key("PATH").String() == "" { // both storage type and path are empty, use default
|
||||
return getDefaultStorageSection(rootCfg), targetSecIsDefault, nil
|
||||
}
|
||||
|
||||
targetSec.Key("STORAGE_TYPE").SetValue("local")
|
||||
default:
|
||||
targetSec, tp, err := getStorageSectionByType(rootCfg, targetType)
|
||||
if targetSec != nil || err != nil {
|
||||
return targetSec, tp, err
|
||||
}
|
||||
}
|
||||
|
||||
return targetSec, targetSecIsStorageWithName, nil
|
||||
}
|
||||
|
||||
return getDefaultStorageSection(rootCfg), targetSecIsDefault, nil
|
||||
}
|
||||
|
||||
// getStorageOverrideSection override section will be read SERVE_DIRECT, PATH, MINIO_BASE_PATH, MINIO_BUCKET to override the targetsec when possible
|
||||
func getStorageOverrideSection(rootConfig ConfigProvider, sec ConfigSection, targetSecType targetSecType, name string) ConfigSection {
|
||||
if targetSecType == targetSecIsSec {
|
||||
return nil
|
||||
}
|
||||
|
||||
if sec != nil {
|
||||
return sec
|
||||
}
|
||||
|
||||
if targetSecType != targetSecIsStorageWithName {
|
||||
nameSec, _ := rootConfig.GetSection(storageSectionName + "." + name)
|
||||
return nameSec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStorageForLocal(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) {
|
||||
storage := Storage{
|
||||
Type: StorageType(targetSec.Key("STORAGE_TYPE").String()),
|
||||
}
|
||||
|
||||
targetPath := ConfigSectionKeyString(targetSec, "PATH", "")
|
||||
var fallbackPath string
|
||||
if targetPath == "" { // no path
|
||||
fallbackPath = filepath.Join(AppDataPath, name)
|
||||
} else {
|
||||
if tp == targetSecIsStorage || tp == targetSecIsDefault {
|
||||
fallbackPath = filepath.Join(targetPath, name)
|
||||
} else {
|
||||
fallbackPath = targetPath
|
||||
}
|
||||
if !filepath.IsAbs(fallbackPath) {
|
||||
fallbackPath = filepath.Join(AppDataPath, fallbackPath)
|
||||
}
|
||||
}
|
||||
|
||||
if overrideSec == nil { // no override section
|
||||
storage.Path = fallbackPath
|
||||
} else {
|
||||
storage.Path = ConfigSectionKeyString(overrideSec, "PATH", "")
|
||||
if storage.Path == "" { // overrideSec has no path
|
||||
storage.Path = fallbackPath
|
||||
} else if !filepath.IsAbs(storage.Path) {
|
||||
if targetPath == "" {
|
||||
storage.Path = filepath.Join(AppDataPath, storage.Path)
|
||||
} else {
|
||||
storage.Path = filepath.Join(targetPath, storage.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkOverlappedPath("[storage."+name+"].PATH", storage.Path)
|
||||
|
||||
return &storage, nil
|
||||
}
|
||||
|
||||
func getStorageForMinio(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl // duplicates azure setup
|
||||
var storage Storage
|
||||
storage.Type = StorageType(targetSec.Key("STORAGE_TYPE").String())
|
||||
if err := targetSec.MapTo(&storage.MinioConfig); err != nil {
|
||||
return nil, fmt.Errorf("map minio config failed: %v", err)
|
||||
}
|
||||
|
||||
var defaultPath string
|
||||
if storage.MinioConfig.BasePath != "" {
|
||||
if tp == targetSecIsStorage || tp == targetSecIsDefault {
|
||||
defaultPath = strings.TrimSuffix(storage.MinioConfig.BasePath, "/") + "/" + name + "/"
|
||||
} else {
|
||||
defaultPath = storage.MinioConfig.BasePath
|
||||
}
|
||||
}
|
||||
if defaultPath == "" {
|
||||
defaultPath = name + "/"
|
||||
}
|
||||
|
||||
if overrideSec != nil {
|
||||
storage.MinioConfig.ServeDirect = ConfigSectionKeyBool(overrideSec, "SERVE_DIRECT", storage.MinioConfig.ServeDirect)
|
||||
storage.MinioConfig.BasePath = ConfigSectionKeyString(overrideSec, "MINIO_BASE_PATH", defaultPath)
|
||||
storage.MinioConfig.Bucket = ConfigSectionKeyString(overrideSec, "MINIO_BUCKET", storage.MinioConfig.Bucket)
|
||||
} else {
|
||||
storage.MinioConfig.BasePath = defaultPath
|
||||
}
|
||||
return &storage, nil
|
||||
}
|
||||
|
||||
func getStorageForAzureBlob(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl // duplicates minio setup
|
||||
var storage Storage
|
||||
storage.Type = StorageType(targetSec.Key("STORAGE_TYPE").String())
|
||||
if err := targetSec.MapTo(&storage.AzureBlobConfig); err != nil {
|
||||
return nil, fmt.Errorf("map azure blob config failed: %v", err)
|
||||
}
|
||||
|
||||
var defaultPath string
|
||||
if storage.AzureBlobConfig.BasePath != "" {
|
||||
if tp == targetSecIsStorage || tp == targetSecIsDefault {
|
||||
defaultPath = strings.TrimSuffix(storage.AzureBlobConfig.BasePath, "/") + "/" + name + "/"
|
||||
} else {
|
||||
defaultPath = storage.AzureBlobConfig.BasePath
|
||||
}
|
||||
}
|
||||
if defaultPath == "" {
|
||||
defaultPath = name + "/"
|
||||
}
|
||||
|
||||
if overrideSec != nil {
|
||||
storage.AzureBlobConfig.ServeDirect = ConfigSectionKeyBool(overrideSec, "SERVE_DIRECT", storage.AzureBlobConfig.ServeDirect)
|
||||
storage.AzureBlobConfig.BasePath = ConfigSectionKeyString(overrideSec, "AZURE_BLOB_BASE_PATH", defaultPath)
|
||||
storage.AzureBlobConfig.Container = ConfigSectionKeyString(overrideSec, "AZURE_BLOB_CONTAINER", storage.AzureBlobConfig.Container)
|
||||
} else {
|
||||
storage.AzureBlobConfig.BasePath = defaultPath
|
||||
}
|
||||
return &storage, nil
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getStorageMultipleName(t *testing.T) {
|
||||
iniStr := `
|
||||
[lfs]
|
||||
MINIO_BUCKET = gitea-lfs
|
||||
|
||||
[attachment]
|
||||
MINIO_BUCKET = gitea-attachment
|
||||
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_BUCKET = gitea-storage
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadAttachmentFrom(cfg))
|
||||
assert.Equal(t, "gitea-attachment", Attachment.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "attachments/", Attachment.Storage.MinioConfig.BasePath)
|
||||
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
assert.Equal(t, "gitea-lfs", LFS.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "lfs/", LFS.Storage.MinioConfig.BasePath)
|
||||
|
||||
assert.NoError(t, loadAvatarsFrom(cfg))
|
||||
assert.Equal(t, "gitea-storage", Avatar.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "avatars/", Avatar.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_getStorageUseOtherNameAsType(t *testing.T) {
|
||||
iniStr := `
|
||||
[attachment]
|
||||
STORAGE_TYPE = lfs
|
||||
|
||||
[storage.lfs]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_BUCKET = gitea-storage
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadAttachmentFrom(cfg))
|
||||
assert.Equal(t, "gitea-storage", Attachment.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "attachments/", Attachment.Storage.MinioConfig.BasePath)
|
||||
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
assert.Equal(t, "gitea-storage", LFS.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "lfs/", LFS.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageType(t *testing.T) {
|
||||
iniStr := `
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadPackagesFrom(cfg))
|
||||
assert.EqualValues(t, "minio", Packages.Storage.Type)
|
||||
assert.Equal(t, "gitea", Packages.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "packages/", Packages.Storage.MinioConfig.BasePath)
|
||||
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
assert.EqualValues(t, "minio", RepoArchive.Storage.Type)
|
||||
assert.Equal(t, "gitea", RepoArchive.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
|
||||
|
||||
assert.NoError(t, loadActionsFrom(cfg))
|
||||
assert.EqualValues(t, "minio", Actions.LogStorage.Type)
|
||||
assert.Equal(t, "gitea", Actions.LogStorage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "actions_log/", Actions.LogStorage.MinioConfig.BasePath)
|
||||
|
||||
assert.EqualValues(t, "minio", Actions.ArtifactStorage.Type)
|
||||
assert.Equal(t, "gitea", Actions.ArtifactStorage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "actions_artifacts/", Actions.ArtifactStorage.MinioConfig.BasePath)
|
||||
|
||||
assert.NoError(t, loadAvatarsFrom(cfg))
|
||||
assert.EqualValues(t, "minio", Avatar.Storage.Type)
|
||||
assert.Equal(t, "gitea", Avatar.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "avatars/", Avatar.Storage.MinioConfig.BasePath)
|
||||
|
||||
assert.NoError(t, loadRepoAvatarFrom(cfg))
|
||||
assert.EqualValues(t, "minio", RepoAvatar.Storage.Type)
|
||||
assert.Equal(t, "gitea", RepoAvatar.Storage.MinioConfig.Bucket)
|
||||
assert.Equal(t, "repo-avatars/", RepoAvatar.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeAzureBlob(t *testing.T) {
|
||||
iniStr := `
|
||||
[storage]
|
||||
STORAGE_TYPE = azureblob
|
||||
`
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, loadPackagesFrom(cfg))
|
||||
assert.EqualValues(t, "azureblob", Packages.Storage.Type)
|
||||
assert.Equal(t, "gitea", Packages.Storage.AzureBlobConfig.Container)
|
||||
assert.Equal(t, "packages/", Packages.Storage.AzureBlobConfig.BasePath)
|
||||
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
assert.EqualValues(t, "azureblob", RepoArchive.Storage.Type)
|
||||
assert.Equal(t, "gitea", RepoArchive.Storage.AzureBlobConfig.Container)
|
||||
assert.Equal(t, "repo-archive/", RepoArchive.Storage.AzureBlobConfig.BasePath)
|
||||
|
||||
assert.NoError(t, loadActionsFrom(cfg))
|
||||
assert.EqualValues(t, "azureblob", Actions.LogStorage.Type)
|
||||
assert.Equal(t, "gitea", Actions.LogStorage.AzureBlobConfig.Container)
|
||||
assert.Equal(t, "actions_log/", Actions.LogStorage.AzureBlobConfig.BasePath)
|
||||
|
||||
assert.EqualValues(t, "azureblob", Actions.ArtifactStorage.Type)
|
||||
assert.Equal(t, "gitea", Actions.ArtifactStorage.AzureBlobConfig.Container)
|
||||
assert.Equal(t, "actions_artifacts/", Actions.ArtifactStorage.AzureBlobConfig.BasePath)
|
||||
|
||||
assert.NoError(t, loadAvatarsFrom(cfg))
|
||||
assert.EqualValues(t, "azureblob", Avatar.Storage.Type)
|
||||
assert.Equal(t, "gitea", Avatar.Storage.AzureBlobConfig.Container)
|
||||
assert.Equal(t, "avatars/", Avatar.Storage.AzureBlobConfig.BasePath)
|
||||
|
||||
assert.NoError(t, loadRepoAvatarFrom(cfg))
|
||||
assert.EqualValues(t, "azureblob", RepoAvatar.Storage.Type)
|
||||
assert.Equal(t, "gitea", RepoAvatar.Storage.AzureBlobConfig.Container)
|
||||
assert.Equal(t, "repo-avatars/", RepoAvatar.Storage.AzureBlobConfig.BasePath)
|
||||
}
|
||||
|
||||
type testLocalStoragePathCase struct {
|
||||
loader func(rootCfg ConfigProvider) error
|
||||
storagePtr **Storage
|
||||
expectedPath string
|
||||
}
|
||||
|
||||
func testLocalStoragePath(t *testing.T, appDataPath, iniStr string, cases []testLocalStoragePathCase) {
|
||||
cfg, err := NewConfigProviderFromData(iniStr)
|
||||
assert.NoError(t, err)
|
||||
AppDataPath = appDataPath
|
||||
for _, c := range cases {
|
||||
assert.NoError(t, c.loader(cfg))
|
||||
storage := *c.storagePtr
|
||||
|
||||
assert.EqualValues(t, "local", storage.Type)
|
||||
assert.True(t, filepath.IsAbs(storage.Path))
|
||||
assert.Equal(t, filepath.Clean(c.expectedPath), filepath.Clean(storage.Path))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeLocal(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage]
|
||||
STORAGE_TYPE = local
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadAttachmentFrom, &Attachment.Storage, "/appdata/attachments"},
|
||||
{loadLFSFrom, &LFS.Storage, "/appdata/lfs"},
|
||||
{loadActionsFrom, &Actions.ArtifactStorage, "/appdata/actions_artifacts"},
|
||||
{loadPackagesFrom, &Packages.Storage, "/appdata/packages"},
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/appdata/repo-archive"},
|
||||
{loadActionsFrom, &Actions.LogStorage, "/appdata/actions_log"},
|
||||
{loadAvatarsFrom, &Avatar.Storage, "/appdata/avatars"},
|
||||
{loadRepoAvatarFrom, &RepoAvatar.Storage, "/appdata/repo-avatars"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeLocalPath(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage]
|
||||
STORAGE_TYPE = local
|
||||
PATH = /data/gitea
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadAttachmentFrom, &Attachment.Storage, "/data/gitea/attachments"},
|
||||
{loadLFSFrom, &LFS.Storage, "/data/gitea/lfs"},
|
||||
{loadActionsFrom, &Actions.ArtifactStorage, "/data/gitea/actions_artifacts"},
|
||||
{loadPackagesFrom, &Packages.Storage, "/data/gitea/packages"},
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/repo-archive"},
|
||||
{loadActionsFrom, &Actions.LogStorage, "/data/gitea/actions_log"},
|
||||
{loadAvatarsFrom, &Avatar.Storage, "/data/gitea/avatars"},
|
||||
{loadRepoAvatarFrom, &RepoAvatar.Storage, "/data/gitea/repo-avatars"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeLocalRelativePath(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage]
|
||||
STORAGE_TYPE = local
|
||||
PATH = storages
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadAttachmentFrom, &Attachment.Storage, "/appdata/storages/attachments"},
|
||||
{loadLFSFrom, &LFS.Storage, "/appdata/storages/lfs"},
|
||||
{loadActionsFrom, &Actions.ArtifactStorage, "/appdata/storages/actions_artifacts"},
|
||||
{loadPackagesFrom, &Packages.Storage, "/appdata/storages/packages"},
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/appdata/storages/repo-archive"},
|
||||
{loadActionsFrom, &Actions.LogStorage, "/appdata/storages/actions_log"},
|
||||
{loadAvatarsFrom, &Avatar.Storage, "/appdata/storages/avatars"},
|
||||
{loadRepoAvatarFrom, &RepoAvatar.Storage, "/appdata/storages/repo-avatars"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeLocalPathOverride(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage]
|
||||
STORAGE_TYPE = local
|
||||
PATH = /data/gitea
|
||||
|
||||
[repo-archive]
|
||||
PATH = /data/gitea/the-archives-dir
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadAttachmentFrom, &Attachment.Storage, "/data/gitea/attachments"},
|
||||
{loadLFSFrom, &LFS.Storage, "/data/gitea/lfs"},
|
||||
{loadActionsFrom, &Actions.ArtifactStorage, "/data/gitea/actions_artifacts"},
|
||||
{loadPackagesFrom, &Packages.Storage, "/data/gitea/packages"},
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/the-archives-dir"},
|
||||
{loadActionsFrom, &Actions.LogStorage, "/data/gitea/actions_log"},
|
||||
{loadAvatarsFrom, &Avatar.Storage, "/data/gitea/avatars"},
|
||||
{loadRepoAvatarFrom, &RepoAvatar.Storage, "/data/gitea/repo-avatars"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeLocalPathOverrideEmpty(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage]
|
||||
STORAGE_TYPE = local
|
||||
PATH = /data/gitea
|
||||
|
||||
[repo-archive]
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadAttachmentFrom, &Attachment.Storage, "/data/gitea/attachments"},
|
||||
{loadLFSFrom, &LFS.Storage, "/data/gitea/lfs"},
|
||||
{loadActionsFrom, &Actions.ArtifactStorage, "/data/gitea/actions_artifacts"},
|
||||
{loadPackagesFrom, &Packages.Storage, "/data/gitea/packages"},
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/repo-archive"},
|
||||
{loadActionsFrom, &Actions.LogStorage, "/data/gitea/actions_log"},
|
||||
{loadAvatarsFrom, &Avatar.Storage, "/data/gitea/avatars"},
|
||||
{loadRepoAvatarFrom, &RepoAvatar.Storage, "/data/gitea/repo-avatars"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeLocalRelativePathOverride(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage]
|
||||
STORAGE_TYPE = local
|
||||
PATH = /data/gitea
|
||||
|
||||
[repo-archive]
|
||||
PATH = the-archives-dir
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadAttachmentFrom, &Attachment.Storage, "/data/gitea/attachments"},
|
||||
{loadLFSFrom, &LFS.Storage, "/data/gitea/lfs"},
|
||||
{loadActionsFrom, &Actions.ArtifactStorage, "/data/gitea/actions_artifacts"},
|
||||
{loadPackagesFrom, &Packages.Storage, "/data/gitea/packages"},
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/the-archives-dir"},
|
||||
{loadActionsFrom, &Actions.LogStorage, "/data/gitea/actions_log"},
|
||||
{loadAvatarsFrom, &Avatar.Storage, "/data/gitea/avatars"},
|
||||
{loadRepoAvatarFrom, &RepoAvatar.Storage, "/data/gitea/repo-avatars"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeLocalPathOverride3(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage.repo-archive]
|
||||
STORAGE_TYPE = local
|
||||
PATH = /data/gitea/archives
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadAttachmentFrom, &Attachment.Storage, "/appdata/attachments"},
|
||||
{loadLFSFrom, &LFS.Storage, "/appdata/lfs"},
|
||||
{loadActionsFrom, &Actions.ArtifactStorage, "/appdata/actions_artifacts"},
|
||||
{loadPackagesFrom, &Packages.Storage, "/appdata/packages"},
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/archives"},
|
||||
{loadActionsFrom, &Actions.LogStorage, "/appdata/actions_log"},
|
||||
{loadAvatarsFrom, &Avatar.Storage, "/appdata/avatars"},
|
||||
{loadRepoAvatarFrom, &RepoAvatar.Storage, "/appdata/repo-avatars"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeLocalPathOverride3_5(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage.repo-archive]
|
||||
STORAGE_TYPE = local
|
||||
PATH = a-relative-path
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadAttachmentFrom, &Attachment.Storage, "/appdata/attachments"},
|
||||
{loadLFSFrom, &LFS.Storage, "/appdata/lfs"},
|
||||
{loadActionsFrom, &Actions.ArtifactStorage, "/appdata/actions_artifacts"},
|
||||
{loadPackagesFrom, &Packages.Storage, "/appdata/packages"},
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/appdata/a-relative-path"},
|
||||
{loadActionsFrom, &Actions.LogStorage, "/appdata/actions_log"},
|
||||
{loadAvatarsFrom, &Avatar.Storage, "/appdata/avatars"},
|
||||
{loadRepoAvatarFrom, &RepoAvatar.Storage, "/appdata/repo-avatars"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeLocalPathOverride4(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage.repo-archive]
|
||||
STORAGE_TYPE = local
|
||||
PATH = /data/gitea/archives
|
||||
|
||||
[repo-archive]
|
||||
PATH = /tmp/gitea/archives
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadAttachmentFrom, &Attachment.Storage, "/appdata/attachments"},
|
||||
{loadLFSFrom, &LFS.Storage, "/appdata/lfs"},
|
||||
{loadActionsFrom, &Actions.ArtifactStorage, "/appdata/actions_artifacts"},
|
||||
{loadPackagesFrom, &Packages.Storage, "/appdata/packages"},
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/tmp/gitea/archives"},
|
||||
{loadActionsFrom, &Actions.LogStorage, "/appdata/actions_log"},
|
||||
{loadAvatarsFrom, &Avatar.Storage, "/appdata/avatars"},
|
||||
{loadRepoAvatarFrom, &RepoAvatar.Storage, "/appdata/repo-avatars"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeLocalPathOverride5(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage.repo-archive]
|
||||
STORAGE_TYPE = local
|
||||
PATH = /data/gitea/archives
|
||||
|
||||
[repo-archive]
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadAttachmentFrom, &Attachment.Storage, "/appdata/attachments"},
|
||||
{loadLFSFrom, &LFS.Storage, "/appdata/lfs"},
|
||||
{loadActionsFrom, &Actions.ArtifactStorage, "/appdata/actions_artifacts"},
|
||||
{loadPackagesFrom, &Packages.Storage, "/appdata/packages"},
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/archives"},
|
||||
{loadActionsFrom, &Actions.LogStorage, "/appdata/actions_log"},
|
||||
{loadAvatarsFrom, &Avatar.Storage, "/appdata/avatars"},
|
||||
{loadRepoAvatarFrom, &RepoAvatar.Storage, "/appdata/repo-avatars"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageInheritStorageTypeLocalPathOverride72(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[repo-archive]
|
||||
STORAGE_TYPE = local
|
||||
PATH = archives
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/appdata/archives"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration20(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[repo-archive]
|
||||
STORAGE_TYPE = my_storage
|
||||
PATH = archives
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Error(t, loadRepoArchiveFrom(cfg))
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration21(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage.repo-archive]
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/appdata/repo-archive"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration22(t *testing.T) {
|
||||
testLocalStoragePath(t, "/appdata", `
|
||||
[storage.repo-archive]
|
||||
PATH = archives
|
||||
`, []testLocalStoragePathCase{
|
||||
{loadRepoArchiveFrom, &RepoArchive.Storage, "/appdata/archives"},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration23(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[repo-archive]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ACCESS_KEY_ID = my_access_key
|
||||
MINIO_SECRET_ACCESS_KEY = my_secret_key
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = getStorage(cfg, "", "", nil)
|
||||
assert.Error(t, err)
|
||||
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
cp := RepoArchive.Storage.ToShadowCopy()
|
||||
assert.Equal(t, "******", cp.MinioConfig.AccessKeyID)
|
||||
assert.Equal(t, "******", cp.MinioConfig.SecretAccessKey)
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration24(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[repo-archive]
|
||||
STORAGE_TYPE = my_archive
|
||||
|
||||
[storage.my_archive]
|
||||
; unsupported, storage type should be defined explicitly
|
||||
PATH = archives
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, loadRepoArchiveFrom(cfg))
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration25(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[repo-archive]
|
||||
STORAGE_TYPE = my_archive
|
||||
|
||||
[storage.my_archive]
|
||||
; unsupported, storage type should be known type
|
||||
STORAGE_TYPE = unknown // should be local or minio
|
||||
PATH = archives
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, loadRepoArchiveFrom(cfg))
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration26(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[repo-archive]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ACCESS_KEY_ID = my_access_key
|
||||
MINIO_SECRET_ACCESS_KEY = my_secret_key
|
||||
; wrong configuration
|
||||
MINIO_USE_SSL = abc
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
// assert.Error(t, loadRepoArchiveFrom(cfg))
|
||||
// FIXME: this should return error but now ini package's MapTo() doesn't check type
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration27(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[storage.repo-archive]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ACCESS_KEY_ID = my_access_key
|
||||
MINIO_SECRET_ACCESS_KEY = my_secret_key
|
||||
MINIO_USE_SSL = true
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
assert.Equal(t, "my_access_key", RepoArchive.Storage.MinioConfig.AccessKeyID)
|
||||
assert.Equal(t, "my_secret_key", RepoArchive.Storage.MinioConfig.SecretAccessKey)
|
||||
assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL)
|
||||
assert.Equal(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration28(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ACCESS_KEY_ID = my_access_key
|
||||
MINIO_SECRET_ACCESS_KEY = my_secret_key
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_BASE_PATH = /prefix
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
assert.Equal(t, "my_access_key", RepoArchive.Storage.MinioConfig.AccessKeyID)
|
||||
assert.Equal(t, "my_secret_key", RepoArchive.Storage.MinioConfig.SecretAccessKey)
|
||||
assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL)
|
||||
assert.Equal(t, "/prefix/repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_IAM_ENDPOINT = 127.0.0.1
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_BASE_PATH = /prefix
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
assert.Equal(t, "127.0.0.1", RepoArchive.Storage.MinioConfig.IamEndpoint)
|
||||
assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL)
|
||||
assert.Equal(t, "/prefix/repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ACCESS_KEY_ID = my_access_key
|
||||
MINIO_SECRET_ACCESS_KEY = my_secret_key
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_BASE_PATH = /prefix
|
||||
|
||||
[lfs]
|
||||
MINIO_BASE_PATH = /lfs
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
assert.Equal(t, "my_access_key", LFS.Storage.MinioConfig.AccessKeyID)
|
||||
assert.Equal(t, "my_secret_key", LFS.Storage.MinioConfig.SecretAccessKey)
|
||||
assert.True(t, LFS.Storage.MinioConfig.UseSSL)
|
||||
assert.Equal(t, "/lfs", LFS.Storage.MinioConfig.BasePath)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ACCESS_KEY_ID = my_access_key
|
||||
MINIO_SECRET_ACCESS_KEY = my_secret_key
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_BASE_PATH = /prefix
|
||||
|
||||
[storage.lfs]
|
||||
MINIO_BASE_PATH = /lfs
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
assert.Equal(t, "my_access_key", LFS.Storage.MinioConfig.AccessKeyID)
|
||||
assert.Equal(t, "my_secret_key", LFS.Storage.MinioConfig.SecretAccessKey)
|
||||
assert.True(t, LFS.Storage.MinioConfig.UseSSL)
|
||||
assert.Equal(t, "/lfs", LFS.Storage.MinioConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration29(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[repo-archive]
|
||||
STORAGE_TYPE = azureblob
|
||||
AZURE_BLOB_ACCOUNT_NAME = my_account_name
|
||||
AZURE_BLOB_ACCOUNT_KEY = my_account_key
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
// assert.Error(t, loadRepoArchiveFrom(cfg))
|
||||
// FIXME: this should return error but now ini package's MapTo() doesn't check type
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration30(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[storage.repo-archive]
|
||||
STORAGE_TYPE = azureblob
|
||||
AZURE_BLOB_ACCOUNT_NAME = my_account_name
|
||||
AZURE_BLOB_ACCOUNT_KEY = my_account_key
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
assert.Equal(t, "my_account_name", RepoArchive.Storage.AzureBlobConfig.AccountName)
|
||||
assert.Equal(t, "my_account_key", RepoArchive.Storage.AzureBlobConfig.AccountKey)
|
||||
assert.Equal(t, "repo-archive/", RepoArchive.Storage.AzureBlobConfig.BasePath)
|
||||
}
|
||||
|
||||
func Test_getStorageConfiguration31(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[storage]
|
||||
STORAGE_TYPE = azureblob
|
||||
AZURE_BLOB_ACCOUNT_NAME = my_account_name
|
||||
AZURE_BLOB_ACCOUNT_KEY = my_account_key
|
||||
AZURE_BLOB_BASE_PATH = /prefix
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
assert.Equal(t, "my_account_name", RepoArchive.Storage.AzureBlobConfig.AccountName)
|
||||
assert.Equal(t, "my_account_key", RepoArchive.Storage.AzureBlobConfig.AccountKey)
|
||||
assert.Equal(t, "/prefix/repo-archive/", RepoArchive.Storage.AzureBlobConfig.BasePath)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[storage]
|
||||
STORAGE_TYPE = azureblob
|
||||
AZURE_BLOB_ACCOUNT_NAME = my_account_name
|
||||
AZURE_BLOB_ACCOUNT_KEY = my_account_key
|
||||
AZURE_BLOB_BASE_PATH = /prefix
|
||||
|
||||
[lfs]
|
||||
AZURE_BLOB_BASE_PATH = /lfs
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
assert.Equal(t, "my_account_name", LFS.Storage.AzureBlobConfig.AccountName)
|
||||
assert.Equal(t, "my_account_key", LFS.Storage.AzureBlobConfig.AccountKey)
|
||||
assert.Equal(t, "/lfs", LFS.Storage.AzureBlobConfig.BasePath)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[storage]
|
||||
STORAGE_TYPE = azureblob
|
||||
AZURE_BLOB_ACCOUNT_NAME = my_account_name
|
||||
AZURE_BLOB_ACCOUNT_KEY = my_account_key
|
||||
AZURE_BLOB_BASE_PATH = /prefix
|
||||
|
||||
[storage.lfs]
|
||||
AZURE_BLOB_BASE_PATH = /lfs
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadLFSFrom(cfg))
|
||||
assert.Equal(t, "my_account_name", LFS.Storage.AzureBlobConfig.AccountName)
|
||||
assert.Equal(t, "my_account_key", LFS.Storage.AzureBlobConfig.AccountKey)
|
||||
assert.Equal(t, "/lfs", LFS.Storage.AzureBlobConfig.BasePath)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
||||
// if these are removed, the warning will not be shown
|
||||
// - will need to set default for [queue.task] LENGTH to 1000 though
|
||||
func loadTaskFrom(rootCfg ConfigProvider) {
|
||||
taskSec := rootCfg.Section("task")
|
||||
queueTaskSec := rootCfg.Section("queue.task")
|
||||
|
||||
deprecatedSetting(rootCfg, "task", "QUEUE_TYPE", "queue.task", "TYPE", "v1.19.0")
|
||||
deprecatedSetting(rootCfg, "task", "QUEUE_CONN_STR", "queue.task", "CONN_STR", "v1.19.0")
|
||||
deprecatedSetting(rootCfg, "task", "QUEUE_LENGTH", "queue.task", "LENGTH", "v1.19.0")
|
||||
|
||||
switch taskSec.Key("QUEUE_TYPE").MustString("channel") {
|
||||
case "channel":
|
||||
queueTaskSec.Key("TYPE").MustString("persistable-channel")
|
||||
queueTaskSec.Key("CONN_STR").MustString(taskSec.Key("QUEUE_CONN_STR").MustString(""))
|
||||
case "redis":
|
||||
queueTaskSec.Key("TYPE").MustString("redis")
|
||||
queueTaskSec.Key("CONN_STR").MustString(taskSec.Key("QUEUE_CONN_STR").MustString("addrs=127.0.0.1:6379 db=0"))
|
||||
}
|
||||
queueTaskSec.Key("LENGTH").MustInt(taskSec.Key("QUEUE_LENGTH").MustInt(1000))
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/auth/password/hash"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
)
|
||||
|
||||
var giteaTestSourceRoot *string // intentionally use a pointer to make sure the uninitialized access panics
|
||||
|
||||
func GetGiteaTestSourceRoot() string {
|
||||
return *giteaTestSourceRoot
|
||||
}
|
||||
|
||||
func detectGiteaTestRoot() string {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
giteaRoot := filepath.Dir(filepath.Dir(filepath.Dir(filename)))
|
||||
fixturesDir := filepath.Join(giteaRoot, "models", "fixtures")
|
||||
if _, err := os.Stat(fixturesDir); err != nil {
|
||||
panic("in gitea source code directory, fixtures directory not found: " + fixturesDir)
|
||||
}
|
||||
return giteaRoot
|
||||
}
|
||||
|
||||
func SetupGiteaTestEnv() {
|
||||
if giteaTestSourceRoot != nil {
|
||||
return // already initialized
|
||||
}
|
||||
|
||||
IsInTesting = true
|
||||
|
||||
log.OsExiter = func(code int) {
|
||||
if code != 0 {
|
||||
// Non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens:
|
||||
// * Show a full stacktrace for more details.
|
||||
// * If the "log.Fatal" is abused in tests, should fix.
|
||||
panic(fmt.Errorf("non-zero exit code during testing: %d", code))
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
initGiteaRoot := func() string {
|
||||
giteaRoot := os.Getenv("GITEA_TEST_ROOT")
|
||||
if giteaRoot == "" {
|
||||
giteaRoot = detectGiteaTestRoot()
|
||||
}
|
||||
giteaTestSourceRoot = &giteaRoot
|
||||
return giteaRoot
|
||||
}
|
||||
giteaRoot := initGiteaRoot()
|
||||
|
||||
initGiteaPaths := func() {
|
||||
// need to load assets (options, public) from the source code directory for testing
|
||||
StaticRootPath = giteaRoot
|
||||
// during testing, the AppPath must point to the pre-built Gitea binary in the source root
|
||||
// it needs to be called by git hooks
|
||||
AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "")
|
||||
}
|
||||
|
||||
initGiteaConf := func() string {
|
||||
// giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF"
|
||||
giteaConf := os.Getenv("GITEA_TEST_CONF")
|
||||
if giteaConf == "" {
|
||||
// if no GITEA_TEST_CONF, then it is in unit test, use a temp (non-existing / empty) config file
|
||||
// do not really use such config file, the test can run concurrently, using the same config file will cause data-race between tests
|
||||
giteaConf = "custom/conf/app-test-tmp.ini"
|
||||
customConfBuiltin = filepath.Join(AppWorkPath, giteaConf)
|
||||
CustomConf = customConfBuiltin
|
||||
_ = os.Remove(CustomConf)
|
||||
} else {
|
||||
// CustomConf must be absolute path to make tests pass.
|
||||
// At the moment, GITEA_TEST_CONF is always in Gitea's source root
|
||||
CustomConf = filepath.Join(giteaRoot, giteaConf)
|
||||
}
|
||||
return giteaConf
|
||||
}
|
||||
|
||||
cleanUpEnv := func() {
|
||||
// also unset unnecessary env vars for testing (only keep "GITEA_TEST_*" ones)
|
||||
UnsetUnnecessaryEnvVars()
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, "GIT_") || (strings.HasPrefix(env, "GITEA_") && !strings.HasPrefix(env, "GITEA_TEST_")) {
|
||||
k, _, _ := strings.Cut(env, "=")
|
||||
_ = os.Unsetenv(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initWorkPathAndConfig := func() {
|
||||
// init paths and config system for testing
|
||||
getTestEnv := func(key string) string { return "" }
|
||||
InitWorkPathAndCommonConfig(getTestEnv, ArgWorkPathAndCustomConf{CustomConf: CustomConf})
|
||||
|
||||
if err := PrepareAppDataPath(); err != nil {
|
||||
log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
|
||||
}
|
||||
|
||||
// register the dummy hash algorithm function used in the test fixtures
|
||||
_ = hash.Register("dummy", hash.NewDummyHasher)
|
||||
PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
||||
}
|
||||
|
||||
initGiteaPaths()
|
||||
giteaConf := initGiteaConf()
|
||||
cleanUpEnv()
|
||||
initWorkPathAndConfig()
|
||||
|
||||
if RepoRootPath == "" || AppDataPath == "" {
|
||||
panic("SetupGiteaTestEnv failed, paths are not initialized")
|
||||
}
|
||||
|
||||
// TODO: some git repo hooks (test fixtures) still use these env variables, need to be refactored in the future
|
||||
_ = os.Setenv("GITEA_ROOT", giteaRoot)
|
||||
_ = os.Setenv("GITEA_CONF", giteaConf) // test fixture git hooks use "$GITEA_ROOT/$GITEA_CONF" in their scripts
|
||||
}
|
||||
|
||||
func PrepareIntegrationTestConfig() error {
|
||||
giteaTestRoot := detectGiteaTestRoot()
|
||||
isInCI := os.Getenv("CI") != ""
|
||||
testDatabase := os.Getenv("GITEA_TEST_DATABASE")
|
||||
if testDatabase == "" {
|
||||
if isInCI {
|
||||
return errors.New("GITEA_TEST_DATABASE environment variable not set")
|
||||
}
|
||||
// for local development, default to sqlite. CI needs to explicitly set a database to avoid unexpected results
|
||||
testDatabase = "sqlite"
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Environment variable GITEA_TEST_DATABASE not set - defaulting to %s\n", testDatabase)
|
||||
}
|
||||
|
||||
_ = os.Setenv("GITEA_TEST_ROOT", giteaTestRoot)
|
||||
_ = os.Setenv("GITEA_TEST_CONF", filepath.Join("tests", testDatabase+".ini"))
|
||||
|
||||
workPath := filepath.Join(giteaTestRoot, "tests/integration/gitea-integration-"+testDatabase)
|
||||
if err := os.MkdirAll(workPath, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
confFile := filepath.Join(giteaTestRoot, "tests", testDatabase+".ini")
|
||||
tmplBuf, err := os.ReadFile(confFile + ".tmpl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpl := string(tmplBuf)
|
||||
envVars, err := shellquote.Split(os.Getenv("MAKEFILE_VARS"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envVarMap := map[string]string{
|
||||
"TEST_WORK_PATH": workPath,
|
||||
"TEST_LOGGER": "test,file",
|
||||
}
|
||||
for _, env := range append(os.Environ(), envVars...) {
|
||||
k, v, _ := strings.Cut(env, "=")
|
||||
k = strings.TrimSpace(k)
|
||||
v = strings.TrimSpace(v)
|
||||
envVarMap[k] = v
|
||||
}
|
||||
for k, v := range envVarMap {
|
||||
tmpl = strings.ReplaceAll(tmpl, fmt.Sprintf("{{%s}}", k), v)
|
||||
}
|
||||
err = os.WriteFile(confFile, []byte(tmpl), 0o644)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// DefaultUILocation is the location on the UI, so that we can display the time on UI.
|
||||
var DefaultUILocation = time.Local
|
||||
|
||||
func loadTimeFrom(rootCfg ConfigProvider) {
|
||||
zone := rootCfg.Section("time").Key("DEFAULT_UI_LOCATION").String()
|
||||
if zone != "" {
|
||||
var err error
|
||||
DefaultUILocation, err = time.LoadLocation(zone)
|
||||
if err != nil {
|
||||
log.Fatal("Load time zone failed: %v", err)
|
||||
}
|
||||
}
|
||||
if DefaultUILocation == nil {
|
||||
DefaultUILocation = time.Local
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// UI settings
|
||||
var UI = struct {
|
||||
ExplorePagingNum int
|
||||
SitemapPagingNum int
|
||||
IssuePagingNum int
|
||||
RepoSearchPagingNum int
|
||||
MembersPagingNum int
|
||||
FeedMaxCommitNum int
|
||||
FeedPagingNum int
|
||||
PackagesPagingNum int
|
||||
GraphMaxCommitNum int
|
||||
CodeCommentLines int
|
||||
ReactionMaxUserNum int
|
||||
MaxDisplayFileSize int64
|
||||
ShowUserEmail bool
|
||||
DefaultTheme string
|
||||
Themes []string
|
||||
FileIconTheme string
|
||||
FolderIconTheme string
|
||||
Reactions []string
|
||||
ReactionsLookup container.Set[string] `ini:"-"`
|
||||
CustomEmojis []string
|
||||
CustomEmojisMap map[string]string `ini:"-"`
|
||||
EnabledEmojis []string
|
||||
EnabledEmojisSet container.Set[string] `ini:"-"`
|
||||
SearchRepoDescription bool
|
||||
OnlyShowRelevantRepos bool
|
||||
ExploreDefaultSort string `ini:"EXPLORE_PAGING_DEFAULT_SORT"`
|
||||
PreferredTimestampTense string
|
||||
|
||||
AmbiguousUnicodeDetection bool
|
||||
|
||||
// TODO: DefaultShowFullName is introduced by https://github.com/go-gitea/gitea/pull/6710
|
||||
// But there are still many edge cases:
|
||||
// * Many places still use "username", not respecting this setting
|
||||
// * Many places use "Full Name" if it is not empty, cause inconsistent UI for users who have set their full name but some others don't
|
||||
// * Even if DefaultShowFullName=false, many places still need to show the full name
|
||||
// For most cases, either "username" or "username (Full Name)" should be used and are good enough.
|
||||
// Only in very few cases (e.g.: unimportant lists, narrow layout), "username" or "Full Name" can be used.
|
||||
DefaultShowFullName bool
|
||||
|
||||
Notification struct {
|
||||
MinTimeout time.Duration
|
||||
TimeoutStep time.Duration
|
||||
MaxTimeout time.Duration
|
||||
EventSourceUpdateTime time.Duration
|
||||
} `ini:"ui.notification"`
|
||||
|
||||
SVG struct {
|
||||
Enabled bool `ini:"ENABLE_RENDER"`
|
||||
} `ini:"ui.svg"`
|
||||
|
||||
CSV struct {
|
||||
MaxFileSize int64
|
||||
MaxRows int
|
||||
} `ini:"ui.csv"`
|
||||
|
||||
Admin struct {
|
||||
UserPagingNum int
|
||||
RepoPagingNum int
|
||||
NoticePagingNum int
|
||||
OrgPagingNum int
|
||||
} `ini:"ui.admin"`
|
||||
User struct {
|
||||
RepoPagingNum int
|
||||
OrgPagingNum int
|
||||
} `ini:"ui.user"`
|
||||
Meta struct {
|
||||
Author string
|
||||
Description string
|
||||
Keywords string
|
||||
} `ini:"ui.meta"`
|
||||
}{
|
||||
ExplorePagingNum: 20,
|
||||
SitemapPagingNum: 20,
|
||||
IssuePagingNum: 20,
|
||||
RepoSearchPagingNum: 20,
|
||||
MembersPagingNum: 20,
|
||||
FeedMaxCommitNum: 5,
|
||||
FeedPagingNum: 20,
|
||||
PackagesPagingNum: 20,
|
||||
GraphMaxCommitNum: 100,
|
||||
CodeCommentLines: 4,
|
||||
ReactionMaxUserNum: 10,
|
||||
MaxDisplayFileSize: 8388608,
|
||||
DefaultTheme: `gitea-auto`,
|
||||
FileIconTheme: `material`,
|
||||
FolderIconTheme: `basic`,
|
||||
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
|
||||
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
|
||||
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},
|
||||
ExploreDefaultSort: "recentupdate",
|
||||
PreferredTimestampTense: "mixed",
|
||||
|
||||
AmbiguousUnicodeDetection: true,
|
||||
|
||||
Notification: struct {
|
||||
MinTimeout time.Duration
|
||||
TimeoutStep time.Duration
|
||||
MaxTimeout time.Duration
|
||||
EventSourceUpdateTime time.Duration
|
||||
}{
|
||||
MinTimeout: 10 * time.Second,
|
||||
TimeoutStep: 10 * time.Second,
|
||||
MaxTimeout: 60 * time.Second,
|
||||
EventSourceUpdateTime: 10 * time.Second,
|
||||
},
|
||||
SVG: struct {
|
||||
Enabled bool `ini:"ENABLE_RENDER"`
|
||||
}{
|
||||
Enabled: true,
|
||||
},
|
||||
CSV: struct {
|
||||
MaxFileSize int64
|
||||
MaxRows int
|
||||
}{
|
||||
MaxFileSize: 524288,
|
||||
MaxRows: 2500,
|
||||
},
|
||||
Admin: struct {
|
||||
UserPagingNum int
|
||||
RepoPagingNum int
|
||||
NoticePagingNum int
|
||||
OrgPagingNum int
|
||||
}{
|
||||
UserPagingNum: 50,
|
||||
RepoPagingNum: 50,
|
||||
NoticePagingNum: 25,
|
||||
OrgPagingNum: 50,
|
||||
},
|
||||
User: struct {
|
||||
RepoPagingNum int
|
||||
OrgPagingNum int
|
||||
}{
|
||||
RepoPagingNum: 15,
|
||||
OrgPagingNum: 15,
|
||||
},
|
||||
Meta: struct {
|
||||
Author string
|
||||
Description string
|
||||
Keywords string
|
||||
}{
|
||||
Author: "Gitea - Git with a cup of tea",
|
||||
Description: "Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go",
|
||||
Keywords: "go,git,self-hosted,gitea",
|
||||
},
|
||||
}
|
||||
|
||||
func loadUIFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "ui", &UI)
|
||||
sec := rootCfg.Section("ui")
|
||||
UI.ShowUserEmail = sec.Key("SHOW_USER_EMAIL").MustBool(true)
|
||||
UI.DefaultShowFullName = sec.Key("DEFAULT_SHOW_FULL_NAME").MustBool(false)
|
||||
UI.SearchRepoDescription = sec.Key("SEARCH_REPO_DESCRIPTION").MustBool(true)
|
||||
|
||||
if UI.PreferredTimestampTense != "mixed" && UI.PreferredTimestampTense != "absolute" {
|
||||
log.Fatal("ui.PREFERRED_TIMESTAMP_TENSE must be either 'mixed' or 'absolute'")
|
||||
}
|
||||
|
||||
// OnlyShowRelevantRepos=false is important for many private/enterprise instances,
|
||||
// because many private repositories do not have "description/topic", users just want to search by their names.
|
||||
UI.OnlyShowRelevantRepos = sec.Key("ONLY_SHOW_RELEVANT_REPOS").MustBool(false)
|
||||
|
||||
UI.ReactionsLookup = make(container.Set[string])
|
||||
for _, reaction := range UI.Reactions {
|
||||
UI.ReactionsLookup.Add(reaction)
|
||||
}
|
||||
UI.CustomEmojisMap = make(map[string]string)
|
||||
for _, emoji := range UI.CustomEmojis {
|
||||
UI.CustomEmojisMap[emoji] = ":" + emoji + ":"
|
||||
}
|
||||
UI.EnabledEmojisSet = container.SetOf(UI.EnabledEmojis...)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// Webhook settings
|
||||
var Webhook = struct {
|
||||
QueueLength int
|
||||
DeliverTimeout int
|
||||
SkipTLSVerify bool
|
||||
AllowedHostList string
|
||||
Types []string
|
||||
PagingNum int
|
||||
ProxyURL string
|
||||
ProxyURLFixed *url.URL
|
||||
ProxyHosts []string
|
||||
}{
|
||||
QueueLength: 1000,
|
||||
DeliverTimeout: 5,
|
||||
SkipTLSVerify: false,
|
||||
PagingNum: 10,
|
||||
ProxyURL: "",
|
||||
ProxyHosts: []string{},
|
||||
}
|
||||
|
||||
func loadWebhookFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("webhook")
|
||||
Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000)
|
||||
Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
|
||||
Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
|
||||
Webhook.AllowedHostList = sec.Key("ALLOWED_HOST_LIST").MustString("")
|
||||
Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork", "packagist"}
|
||||
Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
|
||||
Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("")
|
||||
if Webhook.ProxyURL != "" {
|
||||
var err error
|
||||
Webhook.ProxyURLFixed, err = url.Parse(Webhook.ProxyURL)
|
||||
if err != nil {
|
||||
log.Error("Webhook PROXY_URL is not valid")
|
||||
Webhook.ProxyURL = ""
|
||||
}
|
||||
}
|
||||
Webhook.ProxyHosts = sec.Key("PROXY_HOSTS").Strings(",")
|
||||
}
|
||||
Reference in New Issue
Block a user