Uniform all temporary directories and allow customizing temp path (#32352)

This PR uniform all temporary directory usage so that it will be easier
to manage.

Relate to #31792 

- [x] Added a new setting to allow users to configure the global
temporary directory.
- [x] Move all temporary files and directories to be placed under
os.Temp()/gitea.
- [x] `setting.Repository.Local.LocalCopyPath` now will be
`setting.TempPath/local-repo` and the customized path is removed.
```diff
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;[repository.local]
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;
-;; Path for local repository copy. Defaults to  TEMP_PATH + `local-repo`, this is deprecated and cannot be changed
-;LOCAL_COPY_PATH = local-repo
```

- [x] `setting.Repository.Upload.TempPath` now will be
`settting.TempPath/uploads` and the customized path is removed.
```diff
;[repository.upload]
-;;
-;; Path for uploads. Defaults to TEMP_PATH + `uploads`
-;TEMP_PATH = uploads
```

- [x] `setting.Packages.ChunkedUploadPath` now will be
`settting.TempPath/package-upload` and the customized path is removed.
```diff
;[packages]
-;;
-;; Path for chunked uploads. Defaults it's `package-upload` under `TEMP_PATH` unless it's an absolute path.
-;CHUNKED_UPLOAD_PATH = package-upload
```

- [x] `setting.SSH.KeyTestPath` now will be
`settting.TempPath/ssh_key_test` and the customized path is removed.
```diff
[server]
-;;
-;; Directory to create temporary files in when testing public keys using ssh-keygen,
-;; default is the system temporary directory.
-;SSH_KEY_TEST_PATH =
```

TODO:
- [ ] setting.PprofDataPath haven't been changed because it may need to
be kept until somebody read it but temp path may be clean up any time.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Lunny Xiao
2025-04-08 09:15:28 -07:00
committed by GitHub
parent fd7c364ca6
commit 32b97b3ce8
45 changed files with 361 additions and 418 deletions

View File

@ -11,7 +11,7 @@ import (
"os"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/setting"
)
// BlamePart represents block of blame - continuous lines with one sha
@ -29,12 +29,13 @@ type BlameReader struct {
bufferedReader *bufio.Reader
done chan error
lastSha *string
ignoreRevsFile *string
ignoreRevsFile string
objectFormat ObjectFormat
cleanupFuncs []func()
}
func (r *BlameReader) UsesIgnoreRevs() bool {
return r.ignoreRevsFile != nil
return r.ignoreRevsFile != ""
}
// NextPart returns next part of blame (sequential code lines with the same commit)
@ -122,36 +123,37 @@ func (r *BlameReader) Close() error {
r.bufferedReader = nil
_ = r.reader.Close()
_ = r.output.Close()
if r.ignoreRevsFile != nil {
_ = util.Remove(*r.ignoreRevsFile)
for _, cleanup := range r.cleanupFuncs {
if cleanup != nil {
cleanup()
}
}
return err
}
// CreateBlameReader creates reader for given repository, commit and file
func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) {
var ignoreRevsFile *string
if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
}
cmd := NewCommandNoGlobals("blame", "--porcelain")
if ignoreRevsFile != nil {
// Possible improvement: use --ignore-revs-file /dev/stdin on unix
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
cmd.AddOptionValues("--ignore-revs-file", *ignoreRevsFile)
}
cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
reader, stdout, err := os.Pipe()
if err != nil {
if ignoreRevsFile != nil {
_ = util.Remove(*ignoreRevsFile)
}
return nil, err
}
done := make(chan error, 1)
cmd := NewCommandNoGlobals("blame", "--porcelain")
var ignoreRevsFileName string
var ignoreRevsFileCleanup func() // TODO: maybe it should check the returned err in a defer func to make sure the cleanup could always be executed correctly
if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
ignoreRevsFileName, ignoreRevsFileCleanup = tryCreateBlameIgnoreRevsFile(commit)
if ignoreRevsFileName != "" {
// Possible improvement: use --ignore-revs-file /dev/stdin on unix
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
cmd.AddOptionValues("--ignore-revs-file", ignoreRevsFileName)
}
}
cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
done := make(chan error, 1)
go func() {
stderr := bytes.Buffer{}
// TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
@ -169,40 +171,44 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath
}()
bufferedReader := bufio.NewReader(reader)
return &BlameReader{
output: stdout,
reader: reader,
bufferedReader: bufferedReader,
done: done,
ignoreRevsFile: ignoreRevsFile,
ignoreRevsFile: ignoreRevsFileName,
objectFormat: objectFormat,
cleanupFuncs: []func(){ignoreRevsFileCleanup},
}, nil
}
func tryCreateBlameIgnoreRevsFile(commit *Commit) *string {
func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func()) {
entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs")
if err != nil {
return nil
log.Error("Unable to get .git-blame-ignore-revs file: GetTreeEntryByPath: %v", err)
return "", nil
}
r, err := entry.Blob().DataAsync()
if err != nil {
return nil
log.Error("Unable to get .git-blame-ignore-revs file data: DataAsync: %v", err)
return "", nil
}
defer r.Close()
f, err := os.CreateTemp("", "gitea_git-blame-ignore-revs")
f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs")
if err != nil {
return nil
log.Error("Unable to get .git-blame-ignore-revs file data: CreateTempFileRandom: %v", err)
return "", nil
}
filename := f.Name()
_, err = io.Copy(f, r)
_ = f.Close()
if err != nil {
_ = util.Remove(f.Name())
return nil
cleanup()
log.Error("Unable to get .git-blame-ignore-revs file data: Copy: %v", err)
return "", nil
}
return util.ToPointer(f.Name())
return filename, cleanup
}

View File

@ -7,10 +7,13 @@ import (
"context"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestReadingBlameOutputSha256(t *testing.T) {
setting.AppDataPath = t.TempDir()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()

View File

@ -7,10 +7,13 @@ import (
"context"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestReadingBlameOutput(t *testing.T) {
setting.AppDataPath = t.TempDir()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()

View File

@ -10,18 +10,19 @@ import (
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/tempdir"
"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"
)
func testRun(m *testing.M) error {
gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home")
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
if err != nil {
return fmt.Errorf("unable to create temp dir: %w", err)
}
defer util.RemoveAll(gitHomePath)
defer cleanup()
setting.Git.HomePath = gitHomePath
if err = InitFull(context.Background()); err != nil {

View File

@ -18,6 +18,7 @@ import (
"time"
"code.gitea.io/gitea/modules/proxy"
"code.gitea.io/gitea/modules/setting"
)
// GPGSettings represents the default GPG settings for this repository
@ -266,11 +267,11 @@ func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch
// CreateBundle create bundle content to the target path
func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error {
tmp, err := os.MkdirTemp(os.TempDir(), "gitea-bundle")
tmp, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-bundle")
if err != nil {
return err
}
defer os.RemoveAll(tmp)
defer cleanup()
env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects"))
_, _, err = NewCommand("init", "--bare").RunStdString(ctx, &RunOpts{Dir: tmp, Env: env})

View File

@ -10,8 +10,7 @@ import (
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/setting"
)
// ReadTreeToIndex reads a treeish to the index
@ -59,26 +58,18 @@ func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (tmpIndexFilena
}
}()
removeDirFn := func(dir string) func() { // it can't use the return value "tmpDir" directly because it is empty when error occurs
return func() {
if err := util.RemoveAll(dir); err != nil {
log.Error("failed to remove tmp index dir: %v", err)
}
}
}
tmpDir, err = os.MkdirTemp("", "index")
tmpDir, cancel, err = setting.AppDataTempDir("git-repo-content").MkdirTempRandom("index")
if err != nil {
return "", "", nil, err
}
tmpIndexFilename = filepath.Join(tmpDir, ".tmp-index")
cancel = removeDirFn(tmpDir)
err = repo.ReadTreeToIndex(treeish, tmpIndexFilename)
if err != nil {
return "", "", cancel, err
}
return tmpIndexFilename, tmpDir, cancel, err
return tmpIndexFilename, tmpDir, cancel, nil
}
// EmptyIndex empties the index

View File

@ -9,11 +9,14 @@ import (
"path/filepath"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRepository_GetLanguageStats(t *testing.T) {
setting.AppDataPath = t.TempDir()
repoPath := filepath.Join(testReposDir, "language_stats_repo")
gitRepo, err := openRepositoryWithDefaultContext(repoPath)
require.NoError(t, err)

View File

@ -12,11 +12,9 @@ import (
"runtime"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
// RegisterRenderers registers all supported third part renderers according settings
@ -88,16 +86,11 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
if p.IsInputFile {
// write to temp file
f, err := os.CreateTemp("", "gitea_input")
f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("gitea_input")
if err != nil {
return fmt.Errorf("%s create temp file when rendering %s failed: %w", p.Name(), p.Command, err)
}
tmpPath := f.Name()
defer func() {
if err := util.Remove(tmpPath); err != nil {
log.Warn("Unable to remove temporary file: %s: Error: %v", tmpPath, err)
}
}()
defer cleanup()
_, err = io.Copy(f, input)
if err != nil {

View File

@ -6,6 +6,7 @@ package packages
import (
"io"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util/filebuffer"
)
@ -34,11 +35,11 @@ func NewHashedBuffer() (*HashedBuffer, error) {
// NewHashedBufferWithSize creates a hashed buffer with a specific memory size
func NewHashedBufferWithSize(maxMemorySize int) (*HashedBuffer, error) {
b, err := filebuffer.New(maxMemorySize)
tempDir, err := setting.AppDataTempDir("package-hashed-buffer").MkdirAllSub("")
if err != nil {
return nil, err
}
b := filebuffer.New(maxMemorySize, tempDir)
hash := NewMultiHasher()
combinedWriter := io.MultiWriter(b, hash)

View File

@ -9,10 +9,13 @@ import (
"strings"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestHashedBuffer(t *testing.T) {
setting.AppDataPath = t.TempDir()
cases := []struct {
MaxMemorySize int
Data string

View File

@ -9,6 +9,8 @@ import (
"encoding/base64"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
@ -17,6 +19,7 @@ fgAA3AEAAAQAAAAjU3RyaW5ncwAAAADgAQAABAAAACNVUwDkAQAAMAAAACNHVUlEAAAAFAIAACgB
AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`
func TestExtractPortablePdb(t *testing.T) {
setting.AppDataPath = t.TempDir()
createArchive := func(name string, content []byte) []byte {
var buf bytes.Buffer
archive := zip.NewWriter(&buf)

View File

@ -4,41 +4,19 @@
package repository
import (
"context"
"fmt"
"os"
"path/filepath"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
// LocalCopyPath returns the local repository temporary copy path.
func LocalCopyPath() string {
if filepath.IsAbs(setting.Repository.Local.LocalCopyPath) {
return setting.Repository.Local.LocalCopyPath
}
return filepath.Join(setting.AppDataPath, setting.Repository.Local.LocalCopyPath)
}
// CreateTemporaryPath creates a temporary path
func CreateTemporaryPath(prefix string) (string, error) {
if err := os.MkdirAll(LocalCopyPath(), os.ModePerm); err != nil {
log.Error("Unable to create localcopypath directory: %s (%v)", LocalCopyPath(), err)
return "", fmt.Errorf("Failed to create localcopypath directory %s: %w", LocalCopyPath(), err)
}
basePath, err := os.MkdirTemp(LocalCopyPath(), prefix+".git")
func CreateTemporaryPath(prefix string) (string, context.CancelFunc, error) {
basePath, cleanup, err := setting.AppDataTempDir("local-repo").MkdirTempRandom(prefix + ".git")
if err != nil {
log.Error("Unable to create temporary directory: %s-*.git (%v)", prefix, err)
return "", fmt.Errorf("Failed to create dir %s-*.git: %w", prefix, err)
return "", nil, fmt.Errorf("failed to create dir %s-*.git: %w", prefix, err)
}
return basePath, nil
}
// RemoveTemporaryPath removes the temporary path
func RemoveTemporaryPath(basePath string) error {
if _, err := os.Stat(basePath); !os.IsNotExist(err) {
return util.RemoveAll(basePath)
}
return nil
return basePath, cleanup, nil
}

View File

@ -6,8 +6,6 @@ package setting
import (
"fmt"
"math"
"os"
"path/filepath"
"github.com/dustin/go-humanize"
)
@ -67,14 +65,10 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
return err
}
Packages.ChunkedUploadPath = filepath.ToSlash(sec.Key("CHUNKED_UPLOAD_PATH").MustString("tmp/package-upload"))
if !filepath.IsAbs(Packages.ChunkedUploadPath) {
Packages.ChunkedUploadPath = filepath.ToSlash(filepath.Join(AppDataPath, Packages.ChunkedUploadPath))
}
if HasInstallLock(rootCfg) {
if err := os.MkdirAll(Packages.ChunkedUploadPath, os.ModePerm); err != nil {
return fmt.Errorf("unable to create chunked upload directory: %s (%v)", Packages.ChunkedUploadPath, err)
Packages.ChunkedUploadPath, err = AppDataTempDir("package-upload").MkdirAllSub("")
if err != nil {
return fmt.Errorf("unable to create chunked upload directory: %w", err)
}
}

View File

@ -11,6 +11,7 @@ import (
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/tempdir"
)
var (
@ -196,3 +197,18 @@ func InitWorkPathAndCfgProvider(getEnvFn func(name string) string, args ArgWorkP
CustomPath = tmpCustomPath.Value
CustomConf = tmpCustomConf.Value
}
// 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)
}

View File

@ -62,17 +62,11 @@ var (
// Repository upload settings
Upload struct {
Enabled bool
TempPath string
AllowedTypes string
FileMaxSize int64
MaxFiles int
} `ini:"-"`
// Repository local settings
Local struct {
LocalCopyPath string
} `ini:"-"`
// Pull request settings
PullRequest struct {
WorkInProgressPrefixes []string
@ -181,25 +175,16 @@ var (
// Repository upload settings
Upload: struct {
Enabled bool
TempPath string
AllowedTypes string
FileMaxSize int64
MaxFiles int
}{
Enabled: true,
TempPath: "data/tmp/uploads",
AllowedTypes: "",
FileMaxSize: 50,
MaxFiles: 5,
},
// Repository local settings
Local: struct {
LocalCopyPath string
}{
LocalCopyPath: "tmp/local-repo",
},
// Pull request settings
PullRequest: struct {
WorkInProgressPrefixes []string
@ -308,8 +293,6 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
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.local").MapTo(&Repository.Local); err != nil {
log.Fatal("Failed to map Repository.Local 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)
}
@ -361,10 +344,6 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
}
}
if !filepath.IsAbs(Repository.Upload.TempPath) {
Repository.Upload.TempPath = filepath.Join(AppWorkPath, Repository.Upload.TempPath)
}
if err := loadRepoArchiveFrom(rootCfg); err != nil {
log.Fatal("loadRepoArchiveFrom: %v", err)
}

View File

@ -7,6 +7,7 @@ import (
"encoding/base64"
"net"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
@ -59,6 +60,8 @@ var (
// AssetVersion holds a opaque value that is used for cache-busting assets
AssetVersion string
appTempPathInternal string // the temporary path for the app, it is only an internal variable, do not use it, always use AppDataTempDir
Protocol Scheme
UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"`
ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"`
@ -330,6 +333,19 @@ func loadServerFrom(rootCfg ConfigProvider) {
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)
}
}
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)

View File

@ -4,7 +4,6 @@
package setting
import (
"os"
"path/filepath"
"strings"
"text/template"
@ -31,8 +30,6 @@ var SSH = struct {
ServerKeyExchanges []string `ini:"SSH_SERVER_KEY_EXCHANGES"`
ServerMACs []string `ini:"SSH_SERVER_MACS"`
ServerHostKeys []string `ini:"SSH_SERVER_HOST_KEYS"`
KeyTestPath string `ini:"SSH_KEY_TEST_PATH"`
KeygenPath string `ini:"SSH_KEYGEN_PATH"`
AuthorizedKeysBackup bool `ini:"SSH_AUTHORIZED_KEYS_BACKUP"`
AuthorizedPrincipalsBackup bool `ini:"SSH_AUTHORIZED_PRINCIPALS_BACKUP"`
AuthorizedKeysCommandTemplate string `ini:"SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE"`
@ -57,7 +54,6 @@ var SSH = struct {
ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"},
ServerKeyExchanges: []string{"curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"},
ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"},
KeygenPath: "",
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"},
@ -123,7 +119,6 @@ func loadSSHFrom(rootCfg ConfigProvider) {
if len(serverMACs) > 0 {
SSH.ServerMACs = serverMACs
}
SSH.KeyTestPath = os.TempDir()
if err = sec.MapTo(&SSH); err != nil {
log.Fatal("Failed to map SSH settings: %v", err)
}
@ -133,7 +128,6 @@ func loadSSHFrom(rootCfg ConfigProvider) {
}
}
SSH.KeygenPath = sec.Key("SSH_KEYGEN_PATH").String()
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)

View File

@ -32,11 +32,6 @@ func Init() error {
builtinUnused()
// FIXME: why 0o644 for a directory .....
if err := os.MkdirAll(setting.SSH.KeyTestPath, 0o644); err != nil {
return fmt.Errorf("failed to create directory %q for ssh key test: %w", setting.SSH.KeyTestPath, err)
}
if len(setting.SSH.TrustedUserCAKeys) > 0 && setting.SSH.AuthorizedPrincipalsEnabled {
caKeysFileName := setting.SSH.TrustedUserCAKeysFile
caKeysFileDir := filepath.Dir(caKeysFileName)

112
modules/tempdir/tempdir.go Normal file
View File

@ -0,0 +1,112 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package tempdir
import (
"os"
"path/filepath"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
type TempDir struct {
// base is the base directory for temporary files, it must exist before accessing and won't be created automatically.
// for example: base="/system-tmpdir", sub="gitea-tmp"
base, sub string
}
func (td *TempDir) JoinPath(elems ...string) string {
return filepath.Join(append([]string{td.base, td.sub}, elems...)...)
}
// MkdirAllSub works like os.MkdirAll, but the base directory must exist
func (td *TempDir) MkdirAllSub(dir string) (string, error) {
if _, err := os.Stat(td.base); err != nil {
return "", err
}
full := filepath.Join(td.base, td.sub, dir)
if err := os.MkdirAll(full, os.ModePerm); err != nil {
return "", err
}
return full, nil
}
func (td *TempDir) prepareDirWithPattern(elems ...string) (dir, pattern string, err error) {
if _, err = os.Stat(td.base); err != nil {
return "", "", err
}
dir, pattern = filepath.Split(filepath.Join(append([]string{td.base, td.sub}, elems...)...))
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return "", "", err
}
return dir, pattern, nil
}
// MkdirTempRandom works like os.MkdirTemp, the last path field is the "pattern"
func (td *TempDir) MkdirTempRandom(elems ...string) (string, func(), error) {
dir, pattern, err := td.prepareDirWithPattern(elems...)
if err != nil {
return "", nil, err
}
dir, err = os.MkdirTemp(dir, pattern)
if err != nil {
return "", nil, err
}
return dir, func() {
if err := util.RemoveAll(dir); err != nil {
log.Error("Failed to remove temp directory %s: %v", dir, err)
}
}, nil
}
// CreateTempFileRandom works like os.CreateTemp, the last path field is the "pattern"
func (td *TempDir) CreateTempFileRandom(elems ...string) (*os.File, func(), error) {
dir, pattern, err := td.prepareDirWithPattern(elems...)
if err != nil {
return nil, nil, err
}
f, err := os.CreateTemp(dir, pattern)
if err != nil {
return nil, nil, err
}
filename := f.Name()
return f, func() {
_ = f.Close()
if err := util.Remove(filename); err != nil {
log.Error("Unable to remove temporary file: %s: Error: %v", filename, err)
}
}, err
}
func (td *TempDir) RemoveOutdated(d time.Duration) {
var remove func(path string)
remove = func(path string) {
entries, _ := os.ReadDir(path)
for _, entry := range entries {
full := filepath.Join(path, entry.Name())
if entry.IsDir() {
remove(full)
_ = os.Remove(full)
continue
}
info, err := entry.Info()
if err == nil && time.Since(info.ModTime()) > d {
_ = os.Remove(full)
}
}
}
remove(td.JoinPath(""))
}
// New create a new TempDir instance, "base" must be an existing directory,
// "sub" could be a multi-level directory and will be created if not exist
func New(base, sub string) *TempDir {
return &TempDir{base: base, sub: sub}
}
func OsTempDir(sub string) *TempDir {
return New(os.TempDir(), sub)
}

View File

@ -0,0 +1,75 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package tempdir
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestTempDir(t *testing.T) {
base := t.TempDir()
t.Run("Create", func(t *testing.T) {
td := New(base, "sub1/sub2") // make sure the sub dir supports "/" in the path
assert.Equal(t, filepath.Join(base, "sub1", "sub2"), td.JoinPath())
assert.Equal(t, filepath.Join(base, "sub1", "sub2/test"), td.JoinPath("test"))
t.Run("MkdirTempRandom", func(t *testing.T) {
s, cleanup, err := td.MkdirTempRandom("foo")
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(s, filepath.Join(base, "sub1/sub2", "foo")))
_, err = os.Stat(s)
assert.NoError(t, err)
cleanup()
_, err = os.Stat(s)
assert.ErrorIs(t, err, os.ErrNotExist)
})
t.Run("CreateTempFileRandom", func(t *testing.T) {
f, cleanup, err := td.CreateTempFileRandom("foo", "bar")
filename := f.Name()
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(filename, filepath.Join(base, "sub1/sub2", "foo", "bar")))
_, err = os.Stat(filename)
assert.NoError(t, err)
cleanup()
_, err = os.Stat(filename)
assert.ErrorIs(t, err, os.ErrNotExist)
})
t.Run("RemoveOutDated", func(t *testing.T) {
fa1, _, err := td.CreateTempFileRandom("dir-a", "f1")
assert.NoError(t, err)
fa2, _, err := td.CreateTempFileRandom("dir-a", "f2")
assert.NoError(t, err)
_ = os.Chtimes(fa2.Name(), time.Now().Add(-time.Hour), time.Now().Add(-time.Hour))
fb1, _, err := td.CreateTempFileRandom("dir-b", "f1")
assert.NoError(t, err)
_ = os.Chtimes(fb1.Name(), time.Now().Add(-time.Hour), time.Now().Add(-time.Hour))
_, _, _ = fa1.Close(), fa2.Close(), fb1.Close()
td.RemoveOutdated(time.Minute)
_, err = os.Stat(fa1.Name())
assert.NoError(t, err)
_, err = os.Stat(fa2.Name())
assert.ErrorIs(t, err, os.ErrNotExist)
_, err = os.Stat(fb1.Name())
assert.ErrorIs(t, err, os.ErrNotExist)
})
})
t.Run("BaseNotExist", func(t *testing.T) {
td := New(filepath.Join(base, "not-exist"), "sub")
_, _, err := td.MkdirTempRandom("foo")
assert.ErrorIs(t, err, os.ErrNotExist)
})
}

View File

@ -56,7 +56,7 @@ var testMetas = map[string]string{
}
func TestMain(m *testing.M) {
unittest.InitSettings()
unittest.InitSettingsForTesting()
if err := git.InitSimple(context.Background()); err != nil {
log.Fatal("git init failed, err: %v", err)
}

View File

@ -7,16 +7,10 @@ import (
"bytes"
"errors"
"io"
"math"
"os"
)
var (
// ErrInvalidMemorySize occurs if the memory size is not in a valid range
ErrInvalidMemorySize = errors.New("Memory size must be greater 0 and lower math.MaxInt32")
// ErrWriteAfterRead occurs if Write is called after a read operation
ErrWriteAfterRead = errors.New("Write is unsupported after a read operation")
)
var ErrWriteAfterRead = errors.New("write is unsupported after a read operation") // occurs if Write is called after a read operation
type readAtSeeker interface {
io.ReadSeeker
@ -30,34 +24,17 @@ type FileBackedBuffer struct {
maxMemorySize int64
size int64
buffer bytes.Buffer
tempDir string
file *os.File
reader readAtSeeker
}
// New creates a file backed buffer with a specific maximum memory size
func New(maxMemorySize int) (*FileBackedBuffer, error) {
if maxMemorySize < 0 || maxMemorySize > math.MaxInt32 {
return nil, ErrInvalidMemorySize
}
func New(maxMemorySize int, tempDir string) *FileBackedBuffer {
return &FileBackedBuffer{
maxMemorySize: int64(maxMemorySize),
}, nil
}
// CreateFromReader creates a file backed buffer and copies the provided reader data into it.
func CreateFromReader(r io.Reader, maxMemorySize int) (*FileBackedBuffer, error) {
b, err := New(maxMemorySize)
if err != nil {
return nil, err
tempDir: tempDir,
}
_, err = io.Copy(b, r)
if err != nil {
return nil, err
}
return b, nil
}
// Write implements io.Writer
@ -73,7 +50,7 @@ func (b *FileBackedBuffer) Write(p []byte) (int, error) {
n, err = b.file.Write(p)
} else {
if b.size+int64(len(p)) > b.maxMemorySize {
b.file, err = os.CreateTemp("", "gitea-buffer-")
b.file, err = os.CreateTemp(b.tempDir, "gitea-buffer-")
if err != nil {
return 0, err
}
@ -148,7 +125,7 @@ func (b *FileBackedBuffer) Seek(offset int64, whence int) (int64, error) {
func (b *FileBackedBuffer) Close() error {
if b.file != nil {
err := b.file.Close()
os.Remove(b.file.Name())
_ = os.Remove(b.file.Name())
b.file = nil
return err
}

View File

@ -21,7 +21,8 @@ func TestFileBackedBuffer(t *testing.T) {
}
for _, c := range cases {
buf, err := CreateFromReader(strings.NewReader(c.Data), c.MaxMemorySize)
buf := New(c.MaxMemorySize, t.TempDir())
_, err := io.Copy(buf, strings.NewReader(c.Data))
assert.NoError(t, err)
assert.EqualValues(t, len(c.Data), buf.Size())