Ver código fonte

conf: overhaul settings (#5953)

* Overhaul cache settings

* Overhaul HTTP settings

* conf: overhaul more settings

* log: make LGTM happy

* travis: upload report to Codecov

* Add codecov.yml
ᴜɴᴋɴᴡᴏɴ 4 anos atrás
pai
commit
17ae0ed3ee

+ 4 - 1
.travis.yml

@@ -14,4 +14,7 @@ before_install:
 
 script:
   - go build -v -tags "pam"
-  - go test -v -cover -race ./...
+  - go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
+
+after_success:
+  - bash <(curl -s https://codecov.io/bash)

+ 1 - 0
CHANGELOG.md

@@ -52,6 +52,7 @@ All notable changes to Gogs are documented in this file.
 - Configuration option `[repository] MIRROR_QUEUE_LENGTH`
 - Configuration option `[repository] PULL_REQUEST_QUEUE_LENGTH`
 - Configuration option `[session] ENABLE_SET_COOKIE`
+- Configuration option `[release.attachment] PATH`
 
 ---
 

+ 2 - 0
codecov.yml

@@ -0,0 +1,2 @@
+comment:
+  layout: 'diff, files'

+ 56 - 60
conf/app.ini

@@ -244,19 +244,63 @@ MAX_LIFE_TIME = 86400
 ; The cookie name for CSRF token.
 CSRF_COOKIE_NAME = _csrf
 
-; Attachment settings for releases
-[release.attachment]
-; Whether attachments are enabled. Defaults to `true`
+[cache]
+; The cache adapter, either "memory", "redis", or "memcache".
+ADAPTER = memory
+; For "memory" only, GC interval in seconds.
+INTERVAL = 60
+; For "redis" and "memcache", connection host address:
+; - redis: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180
+; - memcache: `127.0.0.1:11211`
+HOST =
+
+[http]
+; The value for "Access-Control-Allow-Origin" header, default is not to present.
+ACCESS_CONTROL_ALLOW_ORIGIN =
+
+[attachment]
+; Whether to enabled upload attachments in general.
 ENABLED = true
-; Path for attachments. Defaults to `data/attachments`
+; The path to store attachments on the file system.
 PATH = data/attachments
-; One or more allowed types, e.g. image/jpeg|image/png
+; File types that are allowed to be uploaded, e.g. "image/jpeg|image/png". Leave empty to allow any file type.
+ALLOWED_TYPES = image/jpeg|image/png
+; The maximum size of each file in MB.
+MAX_SIZE = 4
+; The maximum number of files per upload.
+MAX_FILES = 5
+
+[release.attachment]
+; Whether to enabled upload attachments for releases.
+ENABLED = true
+; File types that are allowed to be uploaded, e.g. "image/jpeg|image/png". Leave empty to allow any file type.
 ALLOWED_TYPES = */*
-; Max size of each file. Defaults to 32MB
+; The maximum size of each file in MB.
 MAX_SIZE = 32
-; Max number of files per upload. Defaults to 10
+; The maximum number of files per upload.
 MAX_FILES = 10
 
+[time]
+; Specifies the format for fully outputed dates.
+; Values should be one of the following:
+; ANSIC, UnixDate, RubyDate, RFC822, RFC822Z, RFC850, RFC1123, RFC1123Z, RFC3339, RFC3339Nano, Kitchen, Stamp, StampMilli, StampMicro and StampNano.
+; For more information about the format see http://golang.org/pkg/time/#pkg-constants.
+FORMAT = RFC1123
+
+[picture]
+; The path to store user avatars on the file system.
+AVATAR_UPLOAD_PATH = data/avatars
+; The path to store repository avatars on the file system.
+REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars
+; Chinese users can use a custom avatar source, such as http://cn.gravatar.com/avatar/.
+GRAVATAR_SOURCE = gravatar
+; Whether to disable Gravatar, this value will be forced to be true in offline mode.
+DISABLE_GRAVATAR = false
+; Whether to enable federated avatar lookup uses DNS to discover avatar associated
+; with emails, see https://www.libravatar.org for details.
+; This value will be forced to be false in offline mode or when Gravatar is disbaled.
+ENABLE_FEDERATED_AVATAR = false
+
 [markdown]
 ; Enable hard line break extension
 ENABLE_HARD_LINE_BREAK = false
@@ -274,10 +318,6 @@ DASHES = true
 LATEX_DASHES = true
 ANGLED_QUOTES = true
 
-[http]
-; Value for Access-Control-Allow-Origin header, default is not to present
-ACCESS_CONTROL_ALLOW_ORIGIN =
-
 [admin]
 ; Disable regular (non-admin) users to create organizations
 DISABLE_REGULAR_ORG_CREATION = false
@@ -294,50 +334,6 @@ SKIP_TLS_VERIFY = false
 ; Number of history information in each page
 PAGING_NUM = 10
 
-[cache]
-; Either "memory", "redis", or "memcache", default is "memory"
-ADAPTER = memory
-; For "memory" only, GC interval in seconds, default is 60
-INTERVAL = 60
-; For "redis" and "memcache", connection host address
-; redis: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180
-; memcache: `127.0.0.1:11211`
-HOST =
-
-[picture]
-; Path to store user uploaded avatars
-AVATAR_UPLOAD_PATH = data/avatars
-; Path to store repository uploaded avatars
-REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars
-; Chinese users can choose "duoshuo"
-; or a custom avatar source, like: http://cn.gravatar.com/avatar/
-GRAVATAR_SOURCE = gravatar
-; This value will be forced to be true in offline mode.
-DISABLE_GRAVATAR = false
-; Federated avatar lookup uses DNS to discover avatar associated
-; with emails, see https://www.libravatar.org
-; This value will be forced to be false in offline mode or Gravatar is disbaled.
-ENABLE_FEDERATED_AVATAR = false
-
-; Attachment settings for issues
-[attachment]
-; Whether attachments are enabled. Defaults to `true`
-ENABLED = true
-; Path for attachments. Defaults to `data/attachments`
-PATH = data/attachments
-; One or more allowed types, e.g. image/jpeg|image/png
-ALLOWED_TYPES = image/jpeg|image/png
-; Max size of each file. Defaults to 4MB
-MAX_SIZE = 4
-; Max number of files per upload. Defaults to 5
-MAX_FILES = 5
-
-[time]
-; Specifies the format for fully outputed dates. Defaults to RFC1123
-; Special supported values are ANSIC, UnixDate, RubyDate, RFC822, RFC822Z, RFC850, RFC1123, RFC1123Z, RFC3339, RFC3339Nano, Kitchen, Stamp, StampMilli, StampMicro and StampNano
-; For more information about the format see http://golang.org/pkg/time/#pkg-constants
-FORMAT =
-
 ; General settings of loggers
 [log]
 ROOT_PATH =
@@ -446,7 +442,7 @@ PULL = 300
 GC = 60
 
 [mirror]
-; Default interval in hours between each check
+; The default interval in hours for fetching updates.
 DEFAULT_INTERVAL = 8
 
 [api]
@@ -491,6 +487,10 @@ ENABLE_BASIC_AUTH = false
 BASIC_AUTH_USERNAME =
 BASIC_AUTH_PASSWORD =
 
+; Extension mapping to highlight class
+; e.g. .toml=ini
+[highlight.mapping]
+
 [i18n]
 LANGS = en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR,gl-ES,uk-UA,en-GB,hu-HU,sk-SK,id-ID,fa-IR,vi-VN,pt-PT
 NAMES = English,简体中文,繁體中文(香港),繁體中文(臺灣),Deutsch,français,Nederlands,latviešu,русский,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어,galego,українська,English (United Kingdom),Magyar,Slovenčina,Indonesian,Persian,Vietnamese,Português
@@ -528,10 +528,6 @@ fa-IR = fa
 vi-VN = vi
 pt-PT = pt
 
-; Extension mapping to highlight class
-; e.g. .toml=ini
-[highlight.mapping]
-
 [other]
 SHOW_FOOTER_BRANDING = false
 ; Show time of template execution in the footer

+ 7 - 8
conf/locale/locale_en-US.ini

@@ -1276,11 +1276,15 @@ config.session.gc_interval = GC interval
 config.session.max_life_time = Max life time
 config.session.csrf_cookie_name = CSRF cookie
 
-config.log_file_root_path = Log File Root Path
+config.cache_config = Cache configuration
+config.cache.adapter = Adapter
+config.cache.interval = GC interval
+config.cache.host = Host
 
-config.http_config = HTTP Configuration
-config.http_access_control_allow_origin = Access Control Allow Origin
+config.http_config = HTTP configuration
+config.http.access_control_allow_origin = Access control allow origin
 
+config.log_file_root_path = Log File Root Path
 
 config.webhook_config = Webhook Configuration
 config.queue_length = Queue Length
@@ -1290,11 +1294,6 @@ config.skip_tls_verify = Skip TLS Verify
 config.oauth_config = OAuth Configuration
 config.oauth_enabled = Enabled
 
-config.cache_config = Cache Configuration
-config.cache_adapter = Cache Adapter
-config.cache_interval = Cache Interval
-config.cache_conn = Cache Connection
-
 config.picture_config = Picture Configuration
 config.picture_service = Picture Service
 config.disable_gravatar = Disable Gravatar

Diferenças do arquivo suprimidas por serem muito extensas
+ 2 - 2
internal/assets/conf/conf_gen.go


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 1
internal/assets/templates/templates_gen.go


+ 1 - 1
internal/cmd/import.go

@@ -66,7 +66,7 @@ func runImportLocale(c *cli.Context) error {
 	escapedQuotes := []byte(`\"`)
 	regularQuotes := []byte(`"`)
 	// Cut out en-US.
-	for _, lang := range conf.Langs[1:] {
+	for _, lang := range conf.I18n.Langs[1:] {
 		name := fmt.Sprintf("locale_%s.ini", lang)
 		source := filepath.Join(c.String("source"), name)
 		target := filepath.Join(c.String("target"), name)

+ 1 - 1
internal/cmd/serv.go

@@ -68,7 +68,7 @@ func setup(c *cli.Context, logPath string, connectDB bool) {
 
 	err = log.NewFile(log.FileConfig{
 		Level:    level,
-		Filename: filepath.Join(conf.LogRootPath, logPath),
+		Filename: filepath.Join(conf.Log.RootPath, logPath),
 		FileRotationConfig: log.FileRotationConfig{
 			Rotate:  true,
 			Daily:   true,

+ 7 - 7
internal/cmd/web.go

@@ -92,14 +92,14 @@ func newMacaron() *macaron.Macaron {
 	))
 
 	m.Use(macaron.Static(
-		conf.AvatarUploadPath,
+		conf.Picture.AvatarUploadPath,
 		macaron.StaticOptions{
 			Prefix:      db.USER_AVATAR_URL_PREFIX,
 			SkipLogging: conf.Server.DisableRouterLog,
 		},
 	))
 	m.Use(macaron.Static(
-		conf.RepositoryAvatarUploadPath,
+		conf.Picture.RepositoryAvatarUploadPath,
 		macaron.StaticOptions{
 			Prefix:      db.REPO_AVATAR_URL_PREFIX,
 			SkipLogging: conf.Server.DisableRouterLog,
@@ -129,15 +129,15 @@ func newMacaron() *macaron.Macaron {
 		SubURL:          conf.Server.Subpath,
 		Files:           localeFiles,
 		CustomDirectory: filepath.Join(conf.CustomDir(), "conf", "locale"),
-		Langs:           conf.Langs,
-		Names:           conf.Names,
+		Langs:           conf.I18n.Langs,
+		Names:           conf.I18n.Names,
 		DefaultLang:     "en-US",
 		Redirect:        true,
 	}))
 	m.Use(cache.Cacher(cache.Options{
-		Adapter:       conf.CacheAdapter,
-		AdapterConfig: conf.CacheConn,
-		Interval:      conf.CacheInterval,
+		Adapter:       conf.Cache.Adapter,
+		AdapterConfig: conf.Cache.Host,
+		Interval:      conf.Cache.Interval,
 	}))
 	m.Use(captcha.Captchaer(captcha.Options{
 		SubURL: conf.Server.Subpath,

+ 98 - 404
internal/conf/conf.go

@@ -9,7 +9,6 @@ import (
 	"net/mail"
 	"net/url"
 	"os"
-	"path"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -18,13 +17,12 @@ import (
 	_ "github.com/go-macaron/cache/memcache"
 	_ "github.com/go-macaron/cache/redis"
 	_ "github.com/go-macaron/session/redis"
+	"github.com/gogs/go-libravatar"
 	"github.com/mcuadros/go-version"
 	"github.com/pkg/errors"
 	"gopkg.in/ini.v1"
 	log "unknwon.dev/clog/v2"
 
-	"github.com/gogs/go-libravatar"
-
 	"gogs.io/gogs/internal/assets/conf"
 	"gogs.io/gogs/internal/osutil"
 )
@@ -184,18 +182,18 @@ func Init(customConf string) error {
 	Repository.Root = ensureAbs(Repository.Root)
 	Repository.Upload.TempPath = ensureAbs(Repository.Upload.TempPath)
 
-	// *******************************
+	// *****************************
 	// ----- Database settings -----
-	// *******************************
+	// *****************************
 
 	if err = File.Section("database").MapTo(&Database); err != nil {
 		return errors.Wrap(err, "mapping [database] section")
 	}
 	Database.Path = ensureAbs(Database.Path)
 
-	// *******************************
+	// *****************************
 	// ----- Security settings -----
-	// *******************************
+	// *****************************
 
 	if err = File.Section("security").MapTo(&Security); err != nil {
 		return errors.Wrap(err, "mapping [security] section")
@@ -245,37 +243,40 @@ func Init(customConf string) error {
 		return errors.Wrap(err, "mapping [service] section")
 	}
 
-	// ***********************************
+	// *************************
 	// ----- User settings -----
-	// ***********************************
+	// *************************
 
 	if err = File.Section("user").MapTo(&User); err != nil {
 		return errors.Wrap(err, "mapping [user] section")
 	}
 
-	// ***********************************
+	// ****************************
 	// ----- Session settings -----
-	// ***********************************
+	// ****************************
 
 	if err = File.Section("session").MapTo(&Session); err != nil {
 		return errors.Wrap(err, "mapping [session] section")
 	}
 
-	handleDeprecated()
+	// *******************************
+	// ----- Attachment settings -----
+	// *******************************
+
+	if err = File.Section("attachment").MapTo(&Attachment); err != nil {
+		return errors.Wrap(err, "mapping [attachment] section")
+	}
+	Attachment.Path = ensureAbs(Attachment.Path)
 
-	// TODO
+	// *************************
+	// ----- Time settings -----
+	// *************************
 
-	sec := File.Section("attachment")
-	AttachmentPath = sec.Key("PATH").MustString(filepath.Join(Server.AppDataPath, "attachments"))
-	if !filepath.IsAbs(AttachmentPath) {
-		AttachmentPath = path.Join(workDir, AttachmentPath)
+	if err = File.Section("time").MapTo(&Time); err != nil {
+		return errors.Wrap(err, "mapping [time] section")
 	}
-	AttachmentAllowedTypes = strings.Replace(sec.Key("ALLOWED_TYPES").MustString("image/jpeg,image/png"), "|", ",", -1)
-	AttachmentMaxSize = sec.Key("MAX_SIZE").MustInt64(4)
-	AttachmentMaxFiles = sec.Key("MAX_FILES").MustInt(5)
-	AttachmentEnabled = sec.Key("ENABLED").MustBool(true)
 
-	TimeFormat = map[string]string{
+	Time.FormatLayout = map[string]string{
 		"ANSIC":       time.ANSIC,
 		"UnixDate":    time.UnixDate,
 		"RubyDate":    time.RubyDate,
@@ -291,89 +292,104 @@ func Init(customConf string) error {
 		"StampMilli":  time.StampMilli,
 		"StampMicro":  time.StampMicro,
 		"StampNano":   time.StampNano,
-	}[File.Section("time").Key("FORMAT").MustString("RFC1123")]
-
-	sec = File.Section("picture")
-	AvatarUploadPath = sec.Key("AVATAR_UPLOAD_PATH").MustString(filepath.Join(Server.AppDataPath, "avatars"))
-	if !filepath.IsAbs(AvatarUploadPath) {
-		AvatarUploadPath = path.Join(workDir, AvatarUploadPath)
+	}[Time.Format]
+	if Time.FormatLayout == "" {
+		return fmt.Errorf("unrecognized '[time] FORMAT': %s", Time.Format)
 	}
-	RepositoryAvatarUploadPath = sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").MustString(filepath.Join(Server.AppDataPath, "repo-avatars"))
-	if !filepath.IsAbs(RepositoryAvatarUploadPath) {
-		RepositoryAvatarUploadPath = path.Join(workDir, RepositoryAvatarUploadPath)
+
+	// ****************************
+	// ----- Picture settings -----
+	// ****************************
+
+	if err = File.Section("picture").MapTo(&Picture); err != nil {
+		return errors.Wrap(err, "mapping [picture] section")
 	}
-	switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source {
-	case "duoshuo":
-		GravatarSource = "http://gravatar.duoshuo.com/avatar/"
+	Picture.AvatarUploadPath = ensureAbs(Picture.AvatarUploadPath)
+	Picture.RepositoryAvatarUploadPath = ensureAbs(Picture.RepositoryAvatarUploadPath)
+
+	switch Picture.GravatarSource {
 	case "gravatar":
-		GravatarSource = "https://secure.gravatar.com/avatar/"
+		Picture.GravatarSource = "https://secure.gravatar.com/avatar/"
 	case "libravatar":
-		GravatarSource = "https://seccdn.libravatar.org/avatar/"
-	default:
-		GravatarSource = source
+		Picture.GravatarSource = "https://seccdn.libravatar.org/avatar/"
 	}
-	DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool()
-	EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(true)
+
 	if Server.OfflineMode {
-		DisableGravatar = true
-		EnableFederatedAvatar = false
+		Picture.DisableGravatar = true
+		Picture.EnableFederatedAvatar = false
 	}
-	if DisableGravatar {
-		EnableFederatedAvatar = false
+	if Picture.DisableGravatar {
+		Picture.EnableFederatedAvatar = false
 	}
+	if Picture.EnableFederatedAvatar {
+		gravatarURL, err := url.Parse(Picture.GravatarSource)
+		if err != nil {
+			return errors.Wrapf(err, "parse Gravatar source %q", Picture.GravatarSource)
+		}
 
-	if EnableFederatedAvatar {
-		LibravatarService = libravatar.New()
-		parts := strings.Split(GravatarSource, "/")
-		if len(parts) >= 3 {
-			if parts[0] == "https:" {
-				LibravatarService.SetUseHTTPS(true)
-				LibravatarService.SetSecureFallbackHost(parts[2])
-			} else {
-				LibravatarService.SetUseHTTPS(false)
-				LibravatarService.SetFallbackHost(parts[2])
-			}
+		Picture.LibravatarService = libravatar.New()
+		if gravatarURL.Scheme == "https" {
+			Picture.LibravatarService.SetUseHTTPS(true)
+			Picture.LibravatarService.SetSecureFallbackHost(gravatarURL.Host)
+		} else {
+			Picture.LibravatarService.SetUseHTTPS(false)
+			Picture.LibravatarService.SetFallbackHost(gravatarURL.Host)
 		}
 	}
 
-	if err = File.Section("http").MapTo(&HTTP); err != nil {
-		log.Fatal("Failed to map HTTP settings: %v", err)
+	// ***************************
+	// ----- Mirror settings -----
+	// ***************************
+
+	if err = File.Section("mirror").MapTo(&Mirror); err != nil {
+		return errors.Wrap(err, "mapping [mirror] section")
+	}
+
+	if Mirror.DefaultInterval <= 0 {
+		Mirror.DefaultInterval = 8
+	}
+
+	// *************************
+	// ----- I18n settings -----
+	// *************************
+
+	I18n = new(i18n)
+	if err = File.Section("i18n").MapTo(I18n); err != nil {
+		return errors.Wrap(err, "mapping [i18n] section")
+	}
+	I18n.dateLangs = File.Section("i18n.datelang").KeysHash()
+
+	handleDeprecated()
+
+	if err = File.Section("cache").MapTo(&Cache); err != nil {
+		return errors.Wrap(err, "mapping [cache] section")
+	} else if err = File.Section("http").MapTo(&HTTP); err != nil {
+		return errors.Wrap(err, "mapping [http] section")
+	} else if err = File.Section("release").MapTo(&Release); err != nil {
+		return errors.Wrap(err, "mapping [release] section")
 	} else if err = File.Section("webhook").MapTo(&Webhook); err != nil {
-		log.Fatal("Failed to map Webhook settings: %v", err)
-	} else if err = File.Section("release.attachment").MapTo(&Release.Attachment); err != nil {
-		log.Fatal("Failed to map Release.Attachment settings: %v", err)
+		return errors.Wrap(err, "mapping [webhook] section")
 	} else if err = File.Section("markdown").MapTo(&Markdown); err != nil {
-		log.Fatal("Failed to map Markdown settings: %v", err)
+		return errors.Wrap(err, "mapping [markdown] section")
 	} else if err = File.Section("smartypants").MapTo(&Smartypants); err != nil {
-		log.Fatal("Failed to map Smartypants settings: %v", err)
+		return errors.Wrap(err, "mapping [smartypants] section")
 	} else if err = File.Section("admin").MapTo(&Admin); err != nil {
-		log.Fatal("Failed to map Admin settings: %v", err)
+		return errors.Wrap(err, "mapping [admin] section")
 	} else if err = File.Section("cron").MapTo(&Cron); err != nil {
-		log.Fatal("Failed to map Cron settings: %v", err)
+		return errors.Wrap(err, "mapping [cron] section")
 	} else if err = File.Section("git").MapTo(&Git); err != nil {
-		log.Fatal("Failed to map Git settings: %v", err)
-	} else if err = File.Section("mirror").MapTo(&Mirror); err != nil {
-		log.Fatal("Failed to map Mirror settings: %v", err)
+		return errors.Wrap(err, "mapping [git] section")
 	} else if err = File.Section("api").MapTo(&API); err != nil {
-		log.Fatal("Failed to map API settings: %v", err)
+		return errors.Wrap(err, "mapping [api] section")
 	} else if err = File.Section("ui").MapTo(&UI); err != nil {
-		log.Fatal("Failed to map UI settings: %v", err)
+		return errors.Wrap(err, "mapping [ui] section")
 	} else if err = File.Section("prometheus").MapTo(&Prometheus); err != nil {
-		log.Fatal("Failed to map Prometheus settings: %v", err)
+		return errors.Wrap(err, "mapping [prometheus] section")
+	} else if err = File.Section("other").MapTo(&Other); err != nil {
+		return errors.Wrap(err, "mapping [other] section")
 	}
 
-	if Mirror.DefaultInterval <= 0 {
-		Mirror.DefaultInterval = 24
-	}
-
-	Langs = File.Section("i18n").Key("LANGS").Strings(",")
-	Names = File.Section("i18n").Key("NAMES").Strings(",")
-	dateLangs = File.Section("i18n.datelang").KeysHash()
-
-	ShowFooterBranding = File.Section("other").Key("SHOW_FOOTER_BRANDING").MustBool()
-	ShowFooterTemplateLoadTime = File.Section("other").Key("SHOW_FOOTER_TEMPLATE_LOAD_TIME").MustBool()
-
-	HasRobotsTxt = osutil.IsFile(path.Join(CustomDir(), "robots.txt"))
+	HasRobotsTxt = osutil.IsFile(filepath.Join(CustomDir(), "robots.txt"))
 	return nil
 }
 
@@ -384,325 +400,3 @@ func MustInit(customConf string) {
 		panic(err)
 	}
 }
-
-// TODO
-
-var (
-	HTTP struct {
-		AccessControlAllowOrigin string
-	}
-
-	// Database settings
-	UseSQLite3    bool
-	UseMySQL      bool
-	UsePostgreSQL bool
-	UseMSSQL      bool
-
-	// Webhook settings
-	Webhook struct {
-		Types          []string
-		QueueLength    int
-		DeliverTimeout int
-		SkipTLSVerify  bool `ini:"SKIP_TLS_VERIFY"`
-		PagingNum      int
-	}
-
-	// Release settigns
-	Release struct {
-		Attachment struct {
-			Enabled      bool
-			TempPath     string
-			AllowedTypes []string `delim:"|"`
-			MaxSize      int64
-			MaxFiles     int
-		} `ini:"-"`
-	}
-
-	// Markdown sttings
-	Markdown struct {
-		EnableHardLineBreak bool
-		CustomURLSchemes    []string `ini:"CUSTOM_URL_SCHEMES"`
-		FileExtensions      []string
-	}
-
-	// Smartypants settings
-	Smartypants struct {
-		Enabled      bool
-		Fractions    bool
-		Dashes       bool
-		LatexDashes  bool
-		AngledQuotes bool
-	}
-
-	// Admin settings
-	Admin struct {
-		DisableRegularOrgCreation bool
-	}
-
-	// Picture settings
-	AvatarUploadPath           string
-	RepositoryAvatarUploadPath string
-	GravatarSource             string
-	DisableGravatar            bool
-	EnableFederatedAvatar      bool
-	LibravatarService          *libravatar.Libravatar
-
-	// Log settings
-	LogRootPath string
-	LogModes    []string
-	LogConfigs  []interface{}
-
-	// Attachment settings
-	AttachmentPath         string
-	AttachmentAllowedTypes string
-	AttachmentMaxSize      int64
-	AttachmentMaxFiles     int
-	AttachmentEnabled      bool
-
-	// Time settings
-	TimeFormat string
-
-	// Cache settings
-	CacheAdapter  string
-	CacheInterval int
-	CacheConn     string
-
-	// Cron tasks
-	Cron struct {
-		UpdateMirror struct {
-			Enabled    bool
-			RunAtStart bool
-			Schedule   string
-		} `ini:"cron.update_mirrors"`
-		RepoHealthCheck struct {
-			Enabled    bool
-			RunAtStart bool
-			Schedule   string
-			Timeout    time.Duration
-			Args       []string `delim:" "`
-		} `ini:"cron.repo_health_check"`
-		CheckRepoStats struct {
-			Enabled    bool
-			RunAtStart bool
-			Schedule   string
-		} `ini:"cron.check_repo_stats"`
-		RepoArchiveCleanup struct {
-			Enabled    bool
-			RunAtStart bool
-			Schedule   string
-			OlderThan  time.Duration
-		} `ini:"cron.repo_archive_cleanup"`
-	}
-
-	// Git settings
-	Git struct {
-		Version                  string `ini:"-"`
-		DisableDiffHighlight     bool
-		MaxGitDiffLines          int
-		MaxGitDiffLineCharacters int
-		MaxGitDiffFiles          int
-		GCArgs                   []string `ini:"GC_ARGS" delim:" "`
-		Timeout                  struct {
-			Migrate int
-			Mirror  int
-			Clone   int
-			Pull    int
-			GC      int `ini:"GC"`
-		} `ini:"git.timeout"`
-	}
-
-	// Mirror settings
-	Mirror struct {
-		DefaultInterval int
-	}
-
-	// API settings
-	API struct {
-		MaxResponseItems int
-	}
-
-	// UI settings
-	UI struct {
-		ExplorePagingNum   int
-		IssuePagingNum     int
-		FeedMaxCommitNum   int
-		ThemeColorMetaTag  string
-		MaxDisplayFileSize int64
-
-		Admin struct {
-			UserPagingNum   int
-			RepoPagingNum   int
-			NoticePagingNum int
-			OrgPagingNum    int
-		} `ini:"ui.admin"`
-		User struct {
-			RepoPagingNum     int
-			NewsFeedPagingNum int
-			CommitsPagingNum  int
-		} `ini:"ui.user"`
-	}
-
-	// Prometheus settings
-	Prometheus struct {
-		Enabled           bool
-		EnableBasicAuth   bool
-		BasicAuthUsername string
-		BasicAuthPassword string
-	}
-
-	// I18n settings
-	Langs     []string
-	Names     []string
-	dateLangs map[string]string
-
-	// Highlight settings are loaded in modules/template/hightlight.go
-
-	// Other settings
-	ShowFooterBranding         bool
-	ShowFooterTemplateLoadTime bool
-
-	// Global setting objects
-	HasRobotsTxt bool
-)
-
-// DateLang transforms standard language locale name to corresponding value in datetime plugin.
-func DateLang(lang string) string {
-	name, ok := dateLangs[lang]
-	if ok {
-		return name
-	}
-	return "en"
-}
-
-// InitLogging initializes the logging service of the application.
-func InitLogging() {
-	LogRootPath = File.Section("log").Key("ROOT_PATH").MustString(filepath.Join(WorkDir(), "log"))
-
-	// Because we always create a console logger as the primary logger at init time,
-	// we need to remove it in case the user doesn't configure to use it after the
-	// logging service is initalized.
-	hasConsole := false
-
-	// Iterate over [log.*] sections to initialize individual logger.
-	LogModes = strings.Split(File.Section("log").Key("MODE").MustString("console"), ",")
-	LogConfigs = make([]interface{}, len(LogModes))
-	levelMappings := map[string]log.Level{
-		"trace": log.LevelTrace,
-		"info":  log.LevelInfo,
-		"warn":  log.LevelWarn,
-		"error": log.LevelError,
-		"fatal": log.LevelFatal,
-	}
-
-	type config struct {
-		Buffer int64
-		Config interface{}
-	}
-	for i, mode := range LogModes {
-		mode = strings.ToLower(strings.TrimSpace(mode))
-		secName := "log." + mode
-		sec, err := File.GetSection(secName)
-		if err != nil {
-			log.Fatal("Missing configuration section [%s] for %q logger", secName, mode)
-			return
-		}
-
-		level := levelMappings[strings.ToLower(sec.Key("LEVEL").MustString("trace"))]
-		buffer := sec.Key("BUFFER_LEN").MustInt64(100)
-		c := new(config)
-		switch mode {
-		case log.DefaultConsoleName:
-			hasConsole = true
-			c = &config{
-				Buffer: buffer,
-				Config: log.ConsoleConfig{
-					Level: level,
-				},
-			}
-			err = log.NewConsole(c.Buffer, c.Config)
-
-		case log.DefaultFileName:
-			logPath := filepath.Join(LogRootPath, "gogs.log")
-			logDir := filepath.Dir(logPath)
-			err = os.MkdirAll(logDir, os.ModePerm)
-			if err != nil {
-				log.Fatal("Failed to create log directory %q: %v", logDir, err)
-				return
-			}
-
-			c = &config{
-				Buffer: buffer,
-				Config: log.FileConfig{
-					Level:    level,
-					Filename: logPath,
-					FileRotationConfig: log.FileRotationConfig{
-						Rotate:   sec.Key("LOG_ROTATE").MustBool(true),
-						Daily:    sec.Key("DAILY_ROTATE").MustBool(true),
-						MaxSize:  1 << uint(sec.Key("MAX_SIZE_SHIFT").MustInt(28)),
-						MaxLines: sec.Key("MAX_LINES").MustInt64(1000000),
-						MaxDays:  sec.Key("MAX_DAYS").MustInt64(7),
-					},
-				},
-			}
-			err = log.NewFile(c.Buffer, c.Config)
-
-		case log.DefaultSlackName:
-			c = &config{
-				Buffer: buffer,
-				Config: log.SlackConfig{
-					Level: level,
-					URL:   sec.Key("URL").String(),
-				},
-			}
-			err = log.NewSlack(c.Buffer, c.Config)
-
-		case log.DefaultDiscordName:
-			c = &config{
-				Buffer: buffer,
-				Config: log.DiscordConfig{
-					Level:    level,
-					URL:      sec.Key("URL").String(),
-					Username: sec.Key("USERNAME").String(),
-				},
-			}
-
-		default:
-			continue
-		}
-
-		if err != nil {
-			log.Fatal("Failed to init %s logger: %v", mode, err)
-			return
-		}
-		LogConfigs[i] = c
-
-		log.Trace("Log mode: %s (%s)", strings.Title(mode), strings.Title(strings.ToLower(level.String())))
-	}
-
-	if !hasConsole {
-		log.Remove(log.DefaultConsoleName)
-	}
-}
-
-func newCacheService() {
-	CacheAdapter = File.Section("cache").Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache"})
-	switch CacheAdapter {
-	case "memory":
-		CacheInterval = File.Section("cache").Key("INTERVAL").MustInt(60)
-	case "redis", "memcache":
-		CacheConn = strings.Trim(File.Section("cache").Key("HOST").String(), "\" ")
-	default:
-		log.Fatal("Unrecognized cache adapter %q", CacheAdapter)
-		return
-	}
-
-	log.Trace("Cache service is enabled")
-}
-
-func NewServices() {
-	newCacheService()
-}
-
-// HookMode indicates whether program starts as Git server-side hook callback.
-// All operations should be done synchronously to prevent program exits before finishing.
-var HookMode bool

+ 131 - 0
internal/conf/log.go

@@ -0,0 +1,131 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package conf
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+
+	log "unknwon.dev/clog/v2"
+)
+
+// Log settings
+var Log struct {
+	RootPath string
+	Modes    []string
+	Configs  []interface{}
+}
+
+// InitLogging initializes the logging service of the application.
+func InitLogging() {
+	Log.RootPath = File.Section("log").Key("ROOT_PATH").MustString(filepath.Join(WorkDir(), "log"))
+
+	// Because we always create a console logger as the primary logger at init time,
+	// we need to remove it in case the user doesn't configure to use it after the
+	// logging service is initalized.
+	hasConsole := false
+
+	// Iterate over [log.*] sections to initialize individual logger.
+	Log.Modes = strings.Split(File.Section("log").Key("MODE").MustString("console"), ",")
+	Log.Configs = make([]interface{}, 0, len(Log.Modes))
+	levelMappings := map[string]log.Level{
+		"trace": log.LevelTrace,
+		"info":  log.LevelInfo,
+		"warn":  log.LevelWarn,
+		"error": log.LevelError,
+		"fatal": log.LevelFatal,
+	}
+
+	type config struct {
+		Buffer int64
+		Config interface{}
+	}
+	for _, mode := range Log.Modes {
+		mode = strings.ToLower(strings.TrimSpace(mode))
+		secName := "log." + mode
+		sec, err := File.GetSection(secName)
+		if err != nil {
+			log.Fatal("Missing configuration section [%s] for %q logger", secName, mode)
+			return
+		}
+
+		level := levelMappings[strings.ToLower(sec.Key("LEVEL").MustString("trace"))]
+		buffer := sec.Key("BUFFER_LEN").MustInt64(100)
+		var c *config
+		switch mode {
+		case log.DefaultConsoleName:
+			hasConsole = true
+			c = &config{
+				Buffer: buffer,
+				Config: log.ConsoleConfig{
+					Level: level,
+				},
+			}
+			err = log.NewConsole(c.Buffer, c.Config)
+
+		case log.DefaultFileName:
+			logPath := filepath.Join(Log.RootPath, "gogs.log")
+			logDir := filepath.Dir(logPath)
+			err = os.MkdirAll(logDir, os.ModePerm)
+			if err != nil {
+				log.Fatal("Failed to create log directory %q: %v", logDir, err)
+				return
+			}
+
+			c = &config{
+				Buffer: buffer,
+				Config: log.FileConfig{
+					Level:    level,
+					Filename: logPath,
+					FileRotationConfig: log.FileRotationConfig{
+						Rotate:   sec.Key("LOG_ROTATE").MustBool(true),
+						Daily:    sec.Key("DAILY_ROTATE").MustBool(true),
+						MaxSize:  1 << uint(sec.Key("MAX_SIZE_SHIFT").MustInt(28)),
+						MaxLines: sec.Key("MAX_LINES").MustInt64(1000000),
+						MaxDays:  sec.Key("MAX_DAYS").MustInt64(7),
+					},
+				},
+			}
+			err = log.NewFile(c.Buffer, c.Config)
+
+		case log.DefaultSlackName:
+			c = &config{
+				Buffer: buffer,
+				Config: log.SlackConfig{
+					Level: level,
+					URL:   sec.Key("URL").String(),
+				},
+			}
+			err = log.NewSlack(c.Buffer, c.Config)
+
+		case log.DefaultDiscordName:
+			c = &config{
+				Buffer: buffer,
+				Config: log.DiscordConfig{
+					Level:    level,
+					URL:      sec.Key("URL").String(),
+					Username: sec.Key("USERNAME").String(),
+				},
+			}
+			err = log.NewDiscord(c.Buffer, c.Config)
+
+		default:
+			continue
+		}
+
+		if err != nil {
+			log.Fatal("Failed to init %s logger: %v", mode, err)
+			return
+		}
+
+		Log.Configs = append(Log.Configs, c)
+		log.Trace("Log mode: %s (%s)", strings.Title(mode), strings.Title(strings.ToLower(level.String())))
+	}
+
+	if !hasConsole {
+		log.Remove(log.DefaultConsoleName)
+	}
+}

+ 206 - 0
internal/conf/static.go

@@ -7,6 +7,9 @@ package conf
 import (
 	"net/url"
 	"os"
+	"time"
+
+	"github.com/gogs/go-libravatar"
 )
 
 // ℹ️ README: This file contains static values that should only be set at initialization time.
@@ -227,8 +230,199 @@ var (
 		// Deprecated: Use MaxLifeTime instead, will be removed in 0.13.
 		SessionLifeTime int64
 	}
+
+	// Cache settings
+	Cache struct {
+		Adapter  string
+		Interval int
+		Host     string
+	}
+
+	// HTTP settings
+	HTTP struct {
+		AccessControlAllowOrigin string
+	}
+
+	// Attachment settings
+	Attachment struct {
+		Enabled      bool
+		Path         string
+		AllowedTypes []string `delim:"|"`
+		MaxSize      int64
+		MaxFiles     int
+	}
+
+	// Release settigns
+	Release struct {
+		Attachment struct {
+			Enabled      bool
+			AllowedTypes []string `delim:"|"`
+			MaxSize      int64
+			MaxFiles     int
+		} `ini:"release.attachment"`
+	}
+
+	// Time settings
+	Time struct {
+		Format string
+
+		// Derived from other static values
+		FormatLayout string `ini:"-"` // Actual layout of the Format.
+	}
+
+	// Picture settings
+	Picture struct {
+		AvatarUploadPath           string
+		RepositoryAvatarUploadPath string
+		GravatarSource             string
+		DisableGravatar            bool
+		EnableFederatedAvatar      bool
+
+		// Derived from other static values
+		LibravatarService *libravatar.Libravatar `ini:"-"` // Initialized client for federated avatar.
+	}
+
+	// Mirror settings
+	Mirror struct {
+		DefaultInterval int
+	}
+
+	// I18n settings
+	I18n *i18n
+
+	// Webhook settings
+	Webhook struct {
+		Types          []string
+		QueueLength    int
+		DeliverTimeout int
+		SkipTLSVerify  bool `ini:"SKIP_TLS_VERIFY"`
+		PagingNum      int
+	}
+
+	// Markdown sttings
+	Markdown struct {
+		EnableHardLineBreak bool
+		CustomURLSchemes    []string `ini:"CUSTOM_URL_SCHEMES"`
+		FileExtensions      []string
+	}
+
+	// Smartypants settings
+	Smartypants struct {
+		Enabled      bool
+		Fractions    bool
+		Dashes       bool
+		LatexDashes  bool
+		AngledQuotes bool
+	}
+
+	// Admin settings
+	Admin struct {
+		DisableRegularOrgCreation bool
+	}
+
+	// Cron tasks
+	Cron struct {
+		UpdateMirror struct {
+			Enabled    bool
+			RunAtStart bool
+			Schedule   string
+		} `ini:"cron.update_mirrors"`
+		RepoHealthCheck struct {
+			Enabled    bool
+			RunAtStart bool
+			Schedule   string
+			Timeout    time.Duration
+			Args       []string `delim:" "`
+		} `ini:"cron.repo_health_check"`
+		CheckRepoStats struct {
+			Enabled    bool
+			RunAtStart bool
+			Schedule   string
+		} `ini:"cron.check_repo_stats"`
+		RepoArchiveCleanup struct {
+			Enabled    bool
+			RunAtStart bool
+			Schedule   string
+			OlderThan  time.Duration
+		} `ini:"cron.repo_archive_cleanup"`
+	}
+
+	// Git settings
+	Git struct {
+		Version                  string `ini:"-"`
+		DisableDiffHighlight     bool
+		MaxGitDiffLines          int
+		MaxGitDiffLineCharacters int
+		MaxGitDiffFiles          int
+		GCArgs                   []string `ini:"GC_ARGS" delim:" "`
+		Timeout                  struct {
+			Migrate int
+			Mirror  int
+			Clone   int
+			Pull    int
+			GC      int `ini:"GC"`
+		} `ini:"git.timeout"`
+	}
+
+	// API settings
+	API struct {
+		MaxResponseItems int
+	}
+
+	// UI settings
+	UI struct {
+		ExplorePagingNum   int
+		IssuePagingNum     int
+		FeedMaxCommitNum   int
+		ThemeColorMetaTag  string
+		MaxDisplayFileSize int64
+
+		Admin struct {
+			UserPagingNum   int
+			RepoPagingNum   int
+			NoticePagingNum int
+			OrgPagingNum    int
+		} `ini:"ui.admin"`
+		User struct {
+			RepoPagingNum     int
+			NewsFeedPagingNum int
+			CommitsPagingNum  int
+		} `ini:"ui.user"`
+	}
+
+	// Prometheus settings
+	Prometheus struct {
+		Enabled           bool
+		EnableBasicAuth   bool
+		BasicAuthUsername string
+		BasicAuthPassword string
+	}
+
+	// Other settings
+	Other struct {
+		ShowFooterBranding         bool
+		ShowFooterTemplateLoadTime bool
+	}
+
+	// Global setting
+	HasRobotsTxt bool
 )
 
+type i18n struct {
+	Langs     []string `delim:","`
+	Names     []string `delim:","`
+	dateLangs map[string]string
+}
+
+// DateLang transforms standard language locale name to corresponding value in datetime plugin.
+func (i *i18n) DateLang(lang string) string {
+	name, ok := i.dateLangs[lang]
+	if ok {
+		return name
+	}
+	return "en"
+}
+
 // handleDeprecated transfers deprecated values to the new ones when set.
 func handleDeprecated() {
 	if App.AppName != "" {
@@ -294,3 +488,15 @@ func handleDeprecated() {
 		Session.SessionLifeTime = 0
 	}
 }
+
+// HookMode indicates whether program starts as Git server-side hook callback.
+// All operations should be done synchronously to prevent program exits before finishing.
+var HookMode bool
+
+// Indicates which database backend is currently being used.
+var (
+	UseSQLite3    bool
+	UseMySQL      bool
+	UsePostgreSQL bool
+	UseMSSQL      bool
+)

+ 2 - 2
internal/context/context.go

@@ -312,7 +312,7 @@ func Contexter() macaron.Handler {
 
 		// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
 		if c.Req.Method == "POST" && strings.Contains(c.Req.Header.Get("Content-Type"), "multipart/form-data") {
-			if err := c.Req.ParseMultipartForm(conf.AttachmentMaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
+			if err := c.Req.ParseMultipartForm(conf.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
 				c.ServerError("ParseMultipartForm", err)
 				return
 			}
@@ -324,7 +324,7 @@ func Contexter() macaron.Handler {
 		log.Trace("CSRF Token: %v", c.Data["CSRFToken"])
 
 		c.Data["ShowRegistrationButton"] = !conf.Auth.DisableRegistration
-		c.Data["ShowFooterBranding"] = conf.ShowFooterBranding
+		c.Data["ShowFooterBranding"] = conf.Other.ShowFooterBranding
 
 		c.renderNoticeBanner()
 

+ 1 - 1
internal/db/attachment.go

@@ -44,7 +44,7 @@ func (a *Attachment) AfterSet(colName string, _ xorm.Cell) {
 
 // AttachmentLocalPath returns where attachment is stored in local file system based on given UUID.
 func AttachmentLocalPath(uuid string) string {
-	return path.Join(conf.AttachmentPath, uuid[0:1], uuid[1:2], uuid)
+	return path.Join(conf.Attachment.Path, uuid[0:1], uuid[1:2], uuid)
 }
 
 // LocalPath returns where attachment is stored in local file system.

+ 1 - 1
internal/db/migrations/v15.go

@@ -39,7 +39,7 @@ func generateAndMigrateGitHooks(x *xorm.Engine) (err error) {
 	)
 
 	// Cleanup old update.log and http.log files.
-	_ = filepath.Walk(conf.LogRootPath, func(path string, info os.FileInfo, err error) error {
+	_ = filepath.Walk(conf.Log.RootPath, func(path string, info os.FileInfo, err error) error {
 		if !info.IsDir() &&
 			(strings.HasPrefix(filepath.Base(path), "update.log") ||
 				strings.HasPrefix(filepath.Base(path), "http.log")) {

+ 1 - 1
internal/db/models.go

@@ -172,7 +172,7 @@ func SetEngine() (err error) {
 	// WARNING: for serv command, MUST remove the output to os.stdout,
 	// so use log file to instead print to stdout.
 	sec := conf.File.Section("log.xorm")
-	logger, err := log.NewFileWriter(path.Join(conf.LogRootPath, "xorm.log"),
+	logger, err := log.NewFileWriter(path.Join(conf.Log.RootPath, "xorm.log"),
 		log.FileRotationConfig{
 			Rotate:  sec.Key("ROTATE").MustBool(true),
 			Daily:   sec.Key("ROTATE_DAILY").MustBool(true),

+ 2 - 2
internal/db/repo.go

@@ -296,7 +296,7 @@ func (repo *Repository) HTMLURL() string {
 
 // CustomAvatarPath returns repository custom avatar file path.
 func (repo *Repository) CustomAvatarPath() string {
-	return filepath.Join(conf.RepositoryAvatarUploadPath, com.ToStr(repo.ID))
+	return filepath.Join(conf.Picture.RepositoryAvatarUploadPath, com.ToStr(repo.ID))
 }
 
 // RelAvatarLink returns relative avatar link to the site domain,
@@ -327,7 +327,7 @@ func (repo *Repository) UploadAvatar(data []byte) error {
 		return fmt.Errorf("decode image: %v", err)
 	}
 
-	_ = os.MkdirAll(conf.RepositoryAvatarUploadPath, os.ModePerm)
+	_ = os.MkdirAll(conf.Picture.RepositoryAvatarUploadPath, os.ModePerm)
 	fw, err := os.Create(repo.CustomAvatarPath())
 	if err != nil {
 		return fmt.Errorf("create custom avatar directory: %v", err)

+ 3 - 3
internal/db/user.go

@@ -216,7 +216,7 @@ func (u *User) GenerateActivateCode() string {
 
 // CustomAvatarPath returns user custom avatar file path.
 func (u *User) CustomAvatarPath() string {
-	return filepath.Join(conf.AvatarUploadPath, com.ToStr(u.ID))
+	return filepath.Join(conf.Picture.AvatarUploadPath, com.ToStr(u.ID))
 }
 
 // GenerateRandomAvatar generates a random avatar for user.
@@ -262,7 +262,7 @@ func (u *User) RelAvatarLink() string {
 			return defaultImgUrl
 		}
 		return fmt.Sprintf("%s/%s/%d", conf.Server.Subpath, USER_AVATAR_URL_PREFIX, u.ID)
-	case conf.DisableGravatar:
+	case conf.Picture.DisableGravatar:
 		if !com.IsExist(u.CustomAvatarPath()) {
 			if err := u.GenerateRandomAvatar(); err != nil {
 				log.Error("GenerateRandomAvatar: %v", err)
@@ -341,7 +341,7 @@ func (u *User) UploadAvatar(data []byte) error {
 		return fmt.Errorf("decode image: %v", err)
 	}
 
-	_ = os.MkdirAll(conf.AvatarUploadPath, os.ModePerm)
+	_ = os.MkdirAll(conf.Picture.AvatarUploadPath, os.ModePerm)
 	fw, err := os.Create(u.CustomAvatarPath())
 	if err != nil {
 		return fmt.Errorf("create custom avatar directory: %v", err)

+ 15 - 16
internal/route/admin/admin.go

@@ -206,37 +206,36 @@ func Config(c *context.Context) {
 	c.Data["Auth"] = conf.Auth
 	c.Data["User"] = conf.User
 	c.Data["Session"] = conf.Session
-
-	c.Data["LogRootPath"] = conf.LogRootPath
-
+	c.Data["Cache"] = conf.Cache
 	c.Data["HTTP"] = conf.HTTP
 
-	c.Data["Webhook"] = conf.Webhook
-
-	c.Data["CacheAdapter"] = conf.CacheAdapter
-	c.Data["CacheInterval"] = conf.CacheInterval
-	c.Data["CacheConn"] = conf.CacheConn
-
-	c.Data["DisableGravatar"] = conf.DisableGravatar
-	c.Data["EnableFederatedAvatar"] = conf.EnableFederatedAvatar
+	// TODO
+	c.Data["Attachment"] = conf.Attachment
+	c.Data["Release"] = conf.Release
+	c.Data["Time"] = conf.Time
+	c.Data["Picture"] = conf.Picture
+	c.Data["Mirror"] = conf.Mirror
 
+	// ???
+	c.Data["Webhook"] = conf.Webhook
 	c.Data["Git"] = conf.Git
 
+	c.Data["LogRootPath"] = conf.Log.RootPath
 	type logger struct {
 		Mode, Config string
 	}
-	loggers := make([]*logger, len(conf.LogModes))
-	for i := range conf.LogModes {
+	loggers := make([]*logger, len(conf.Log.Modes))
+	for i := range conf.Log.Modes {
 		loggers[i] = &logger{
-			Mode: strings.Title(conf.LogModes[i]),
+			Mode: strings.Title(conf.Log.Modes[i]),
 		}
 
-		result, _ := jsoniter.MarshalIndent(conf.LogConfigs[i], "", "  ")
+		result, _ := jsoniter.MarshalIndent(conf.Log.Configs[i], "", "  ")
 		loggers[i].Config = string(result)
 	}
 	c.Data["Loggers"] = loggers
 
-	c.HTML(200, CONFIG)
+	c.Success(CONFIG)
 }
 
 func Monitor(c *context.Context) {

+ 4 - 5
internal/route/install.go

@@ -59,7 +59,7 @@ func GlobalInit(customConf string) error {
 	log.Trace("Work directory: %s", conf.WorkDir())
 	log.Trace("Custom path: %s", conf.CustomDir())
 	log.Trace("Custom config: %s", conf.CustomConf)
-	log.Trace("Log path: %s", conf.LogRootPath)
+	log.Trace("Log path: %s", conf.Log.RootPath)
 	log.Trace("Build time: %s", conf.BuildTime)
 	log.Trace("Build commit: %s", conf.BuildCommit)
 
@@ -67,7 +67,6 @@ func GlobalInit(customConf string) error {
 		log.Trace("Email service is enabled")
 	}
 
-	conf.NewServices()
 	email.NewContext()
 
 	if conf.Security.InstallLock {
@@ -172,7 +171,7 @@ func Install(c *context.Context) {
 	f.UseBuiltinSSHServer = conf.SSH.StartBuiltinServer
 	f.HTTPPort = conf.Server.HTTPPort
 	f.AppUrl = conf.Server.ExternalURL
-	f.LogRootPath = conf.LogRootPath
+	f.LogRootPath = conf.Log.RootPath
 
 	// E-mail service settings
 	if conf.Email.Enabled {
@@ -185,8 +184,8 @@ func Install(c *context.Context) {
 
 	// Server and other services settings
 	f.OfflineMode = conf.Server.OfflineMode
-	f.DisableGravatar = conf.DisableGravatar
-	f.EnableFederatedAvatar = conf.EnableFederatedAvatar
+	f.DisableGravatar = conf.Picture.DisableGravatar
+	f.EnableFederatedAvatar = conf.Picture.EnableFederatedAvatar
 	f.DisableRegistration = conf.Auth.DisableRegistration
 	f.EnableCaptcha = conf.Auth.EnableRegistrationCaptcha
 	f.RequireSignInView = conf.Auth.RequireSigninView

+ 12 - 12
internal/route/repo/issue.go

@@ -256,10 +256,10 @@ func Pulls(c *context.Context) {
 
 func renderAttachmentSettings(c *context.Context) {
 	c.Data["RequireDropzone"] = true
-	c.Data["IsAttachmentEnabled"] = conf.AttachmentEnabled
-	c.Data["AttachmentAllowedTypes"] = conf.AttachmentAllowedTypes
-	c.Data["AttachmentMaxSize"] = conf.AttachmentMaxSize
-	c.Data["AttachmentMaxFiles"] = conf.AttachmentMaxFiles
+	c.Data["IsAttachmentEnabled"] = conf.Attachment.Enabled
+	c.Data["AttachmentAllowedTypes"] = conf.Attachment.AllowedTypes
+	c.Data["AttachmentMaxSize"] = conf.Attachment.MaxSize
+	c.Data["AttachmentMaxFiles"] = conf.Attachment.MaxFiles
 }
 
 func RetrieveRepoMilestonesAndAssignees(c *context.Context, repo *db.Repository) {
@@ -429,7 +429,7 @@ func NewIssuePost(c *context.Context, f form.NewIssue) {
 	}
 
 	var attachments []string
-	if conf.AttachmentEnabled {
+	if conf.Attachment.Enabled {
 		attachments = f.Files
 	}
 
@@ -493,12 +493,12 @@ func uploadAttachment(c *context.Context, allowedTypes []string) {
 }
 
 func UploadIssueAttachment(c *context.Context) {
-	if !conf.AttachmentEnabled {
+	if !conf.Attachment.Enabled {
 		c.NotFound()
 		return
 	}
 
-	uploadAttachment(c, strings.Split(conf.AttachmentAllowedTypes, ","))
+	uploadAttachment(c, conf.Attachment.AllowedTypes)
 }
 
 func viewIssue(c *context.Context, isPullList bool) {
@@ -845,7 +845,7 @@ func NewComment(c *context.Context, f form.CreateComment) {
 	}
 
 	var attachments []string
-	if conf.AttachmentEnabled {
+	if conf.Attachment.Enabled {
 		attachments = f.Files
 	}
 
@@ -1130,7 +1130,7 @@ func NewMilestone(c *context.Context) {
 	c.Data["PageIsIssueList"] = true
 	c.Data["PageIsMilestones"] = true
 	c.Data["RequireDatetimepicker"] = true
-	c.Data["DateLang"] = conf.DateLang(c.Locale.Language())
+	c.Data["DateLang"] = conf.I18n.DateLang(c.Locale.Language())
 	c.HTML(200, MILESTONE_NEW)
 }
 
@@ -1139,7 +1139,7 @@ func NewMilestonePost(c *context.Context, f form.CreateMilestone) {
 	c.Data["PageIsIssueList"] = true
 	c.Data["PageIsMilestones"] = true
 	c.Data["RequireDatetimepicker"] = true
-	c.Data["DateLang"] = conf.DateLang(c.Locale.Language())
+	c.Data["DateLang"] = conf.I18n.DateLang(c.Locale.Language())
 
 	if c.HasError() {
 		c.HTML(200, MILESTONE_NEW)
@@ -1175,7 +1175,7 @@ func EditMilestone(c *context.Context) {
 	c.Data["PageIsMilestones"] = true
 	c.Data["PageIsEditMilestone"] = true
 	c.Data["RequireDatetimepicker"] = true
-	c.Data["DateLang"] = conf.DateLang(c.Locale.Language())
+	c.Data["DateLang"] = conf.I18n.DateLang(c.Locale.Language())
 
 	m, err := db.GetMilestoneByRepoID(c.Repo.Repository.ID, c.ParamsInt64(":id"))
 	if err != nil {
@@ -1199,7 +1199,7 @@ func EditMilestonePost(c *context.Context, f form.CreateMilestone) {
 	c.Data["PageIsMilestones"] = true
 	c.Data["PageIsEditMilestone"] = true
 	c.Data["RequireDatetimepicker"] = true
-	c.Data["DateLang"] = conf.DateLang(c.Locale.Language())
+	c.Data["DateLang"] = conf.I18n.DateLang(c.Locale.Language())
 
 	if c.HasError() {
 		c.HTML(200, MILESTONE_NEW)

+ 1 - 1
internal/route/repo/pull.go

@@ -677,7 +677,7 @@ func CompareAndPullRequestPost(c *context.Context, f form.NewIssue) {
 		return
 	}
 
-	if conf.AttachmentEnabled {
+	if conf.Attachment.Enabled {
 		attachments = f.Files
 	}
 

+ 2 - 2
internal/template/template.go

@@ -58,10 +58,10 @@ func FuncMap() []template.FuncMap {
 				return conf.Server.Domain
 			},
 			"DisableGravatar": func() bool {
-				return conf.DisableGravatar
+				return conf.Picture.DisableGravatar
 			},
 			"ShowFooterTemplateLoadTime": func() bool {
-				return conf.ShowFooterTemplateLoadTime
+				return conf.Other.ShowFooterTemplateLoadTime
 			},
 			"LoadTimes": func(startTime time.Time) string {
 				return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"

+ 5 - 5
internal/tool/tool.go

@@ -190,16 +190,16 @@ func HashEmail(email string) string {
 // which includes app sub-url as prefix. However, it is possible
 // to return full URL if user enables Gravatar-like service.
 func AvatarLink(email string) (url string) {
-	if conf.EnableFederatedAvatar && conf.LibravatarService != nil &&
+	if conf.Picture.EnableFederatedAvatar && conf.Picture.LibravatarService != nil &&
 		strings.Contains(email, "@") {
 		var err error
-		url, err = conf.LibravatarService.FromEmail(email)
+		url, err = conf.Picture.LibravatarService.FromEmail(email)
 		if err != nil {
 			log.Warn("AvatarLink.LibravatarService.FromEmail [%s]: %v", email, err)
 		}
 	}
-	if len(url) == 0 && !conf.DisableGravatar {
-		url = conf.GravatarSource + HashEmail(email) + "?d=identicon"
+	if len(url) == 0 && !conf.Picture.DisableGravatar {
+		url = conf.Picture.GravatarSource + HashEmail(email) + "?d=identicon"
 	}
 	if len(url) == 0 {
 		url = conf.Server.Subpath + "/img/avatar_default.png"
@@ -360,7 +360,7 @@ func RawTimeSince(t time.Time, lang string) string {
 
 // TimeSince calculates the time interval and generate user-friendly string.
 func TimeSince(t time.Time, lang string) template.HTML {
-	return template.HTML(fmt.Sprintf(`<span class="time-since" title="%s">%s</span>`, t.Format(conf.TimeFormat), timeSince(t, lang)))
+	return template.HTML(fmt.Sprintf(`<span class="time-since" title="%s">%s</span>`, t.Format(conf.Time.FormatLayout), timeSince(t, lang)))
 }
 
 // Subtract deals with subtraction of all types of number.

+ 45 - 26
templates/admin/config.tmpl

@@ -62,11 +62,6 @@
 
 						<dt>{{.i18n.Tr "admin.config.server.landing_url"}}</dt>
 						<dd><code>{{.Server.LandingURL}}</code></dd>
-
-						<div class="ui divider"></div>
-
-						<dt>{{.i18n.Tr "admin.config.log_file_root_path"}}</dt>
-						<dd><code>{{.LogRootPath}}</code></dd>
 					</dl>
 				</div>
 
@@ -121,7 +116,13 @@
 						<dt>{{.i18n.Tr "admin.config.repo.script_type"}}</dt>
 						<dd><code>{{.Repository.ScriptType}}</code></dd>
 						<dt>{{.i18n.Tr "admin.config.repo.ansi_chatset"}}</dt>
-						<dd>{{if .Repository.ANSICharset}}{{.Repository.AnsiCharset}}{{else}}{{.i18n.Tr "admin.config.not_set"}}{{end}}</dd>
+						<dd>
+							{{if .Repository.ANSICharset}}
+								{{.Repository.AnsiCharset}}
+							{{else}}
+								<i>{{.i18n.Tr "admin.config.not_set"}}</i>
+							{{end}}
+						</dd>
 						<dt>{{.i18n.Tr "admin.config.repo.force_private"}}</dt>
 						<dd><i class="fa fa{{if .Repository.ForcePrivate}}-check{{end}}-square-o"></i></dd>
 						<dt>{{.i18n.Tr "admin.config.repo.max_creation_limit"}}</dt>
@@ -224,7 +225,13 @@
 							<dt>{{.i18n.Tr "admin.config.email.disable_helo"}}</dt>
 							<dd><i class="fa fa{{if .Email.DisableHELO}}-check{{end}}-square-o"></i></dd>
 							<dt>{{.i18n.Tr "admin.config.email.helo_hostname"}}</dt>
-							<dd>{{if .Email.HELOHostname}}{{.Email.HELOHostname}}{{else}}{{.i18n.Tr "admin.config.not_set"}}{{end}}</dd>
+							<dd>
+								{{if .Email.HELOHostname}}
+									{{.Email.HELOHostname}}
+								{{else}}
+									<i>{{.i18n.Tr "admin.config.not_set"}}</i>
+								{{end}}
+							</dd>
 
 							<div class="ui divider"></div>
 
@@ -323,13 +330,34 @@
 					</dl>
 				</div>
 
-				<!-- HTTP Configuration -->
+				{{/* Cache settings */}}
+				<h4 class="ui top attached header">
+					{{.i18n.Tr "admin.config.cache_config"}}
+				</h4>
+				<div class="ui attached table segment">
+					<dl class="dl-horizontal admin-dl-horizontal">
+						<dt>{{.i18n.Tr "admin.config.cache.adapter"}}</dt>
+						<dd>{{.Cache.Adapter}}</dd>
+						<dt>{{.i18n.Tr "admin.config.cache.interval"}}</dt>
+						<dd>{{.Cache.Interval}} {{.i18n.Tr "tool.raw_seconds"}}</dd>
+						<dt>{{.i18n.Tr "admin.config.cache.host"}}</dt>
+						<dd>
+							{{if .CacheConn}}
+								<code>{{.CacheConn}}</code>
+							{{else}}
+								<i>{{.i18n.Tr "admin.config.not_set"}}</i>
+							{{end}}
+						</dd>
+					</dl>
+				</div>
+
+				{{/* HTTP settings */}}
 				<h4 class="ui top attached header">
 					{{.i18n.Tr "admin.config.http_config"}}
 				</h4>
 				<div class="ui attached table segment">
 					<dl class="dl-horizontal admin-dl-horizontal">
-						<dt>{{.i18n.Tr "admin.config.http_access_control_allow_origin"}}</dt>
+						<dt>{{.i18n.Tr "admin.config.http.access_control_allow_origin"}}</dt>
 						<dd>
 							{{if .HTTP.AccessControlAllowOrigin}}
 								<code>{{.HTTP.AccessControlAllowOrigin}}</code>
@@ -354,22 +382,6 @@
 					</dl>
 				</div>
 
-				<h4 class="ui top attached header">
-					{{.i18n.Tr "admin.config.cache_config"}}
-				</h4>
-				<div class="ui attached table segment">
-					<dl class="dl-horizontal admin-dl-horizontal">
-						<dt>{{.i18n.Tr "admin.config.cache_adapter"}}</dt>
-						<dd>{{.CacheAdapter}}</dd>
-						<dt>{{.i18n.Tr "admin.config.cache_interval"}}</dt>
-						<dd>{{.CacheInterval}} {{.i18n.Tr "tool.raw_seconds"}}</dd>
-						{{if .CacheConn}}
-						<dt>{{.i18n.Tr "admin.config.cache_conn"}}</dt>
-						<dd><code>{{.CacheConn}}</code></dd>
-						{{end}}
-					</dl>
-				</div>
-
 				<h4 class="ui top attached header">
 					{{.i18n.Tr "admin.config.picture_config"}}
 				</h4>
@@ -415,7 +427,14 @@
 				<h4 class="ui top attached header">
 					{{.i18n.Tr "admin.config.log_config"}}
 				</h4>
-				<div class="ui attached log-config segment">
+				<div class="ui attached log-config table segment">
+					<dl class="dl-horizontal admin-dl-horizontal">
+						<dt>{{.i18n.Tr "admin.config.log_file_root_path"}}</dt>
+						<dd><code>{{.LogRootPath}}</code></dd>
+					</dl>
+
+					<div class="ui divider"></div>
+
 					<table class="ui very basic table">
 						{{range .Loggers}}
 							<tr>

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff