Skip to content

Commit 5552eff

Browse files
GiteaBotChristopherHXwxiaoguang
authored
Fix artifacts v4 backend upload problems (#36805) (#36834)
Backport #36805 by @ChristopherHX * Use base64.RawURLEncoding to avoid equal sign * using the nodejs package they seem to get lost * Support uploads with unspecified length * Support uploads with a single named blockid * without requiring a blockmap Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: ChristopherHX <christopher.homberger@web.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
1 parent f44f7bf commit 5552eff

10 files changed

Lines changed: 555 additions & 250 deletions

File tree

modules/storage/local.go

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package storage
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"io"
1011
"net/url"
@@ -27,25 +28,32 @@ type LocalStorage struct {
2728

2829
// NewLocalStorage returns a local files
2930
func NewLocalStorage(ctx context.Context, config *setting.Storage) (ObjectStorage, error) {
31+
// prepare storage root path
3032
if !filepath.IsAbs(config.Path) {
31-
return nil, fmt.Errorf("LocalStorageConfig.Path should have been prepared by setting/storage.go and should be an absolute path, but not: %q", config.Path)
32-
}
33-
log.Info("Creating new Local Storage at %s", config.Path)
34-
if err := os.MkdirAll(config.Path, os.ModePerm); err != nil {
35-
return nil, err
33+
return nil, fmt.Errorf("LocalStorage config.Path should have been prepared by setting/storage.go and should be an absolute path, but not: %q", config.Path)
3634
}
35+
storageRoot := util.FilePathJoinAbs(config.Path)
3736

38-
if config.TemporaryPath == "" {
39-
config.TemporaryPath = filepath.Join(config.Path, "tmp")
37+
// prepare storage temporary path
38+
storageTmp := config.TemporaryPath
39+
if storageTmp == "" {
40+
storageTmp = filepath.Join(storageRoot, "tmp")
41+
}
42+
if !filepath.IsAbs(storageTmp) {
43+
return nil, fmt.Errorf("LocalStorage config.TemporaryPath should be an absolute path, but not: %q", config.TemporaryPath)
4044
}
41-
if !filepath.IsAbs(config.TemporaryPath) {
42-
return nil, fmt.Errorf("LocalStorageConfig.TemporaryPath should be an absolute path, but not: %q", config.TemporaryPath)
45+
storageTmp = util.FilePathJoinAbs(storageTmp)
46+
47+
// create the storage root if not exist
48+
log.Info("Creating new Local Storage at %s", storageRoot)
49+
if err := os.MkdirAll(storageRoot, os.ModePerm); err != nil {
50+
return nil, err
4351
}
4452

4553
return &LocalStorage{
4654
ctx: ctx,
47-
dir: config.Path,
48-
tmpdir: config.TemporaryPath,
55+
dir: storageRoot,
56+
tmpdir: storageTmp,
4957
}, nil
5058
}
5159

@@ -108,44 +116,60 @@ func (l *LocalStorage) Stat(path string) (os.FileInfo, error) {
108116
return os.Stat(l.buildLocalPath(path))
109117
}
110118

111-
// Delete delete a file
119+
func (l *LocalStorage) deleteEmptyParentDirs(localFullPath string) {
120+
for parent := filepath.Dir(localFullPath); len(parent) > len(l.dir); parent = filepath.Dir(parent) {
121+
if err := os.Remove(parent); err != nil {
122+
// since the target file has been deleted, parent dir error is not related to the file deletion itself.
123+
break
124+
}
125+
}
126+
}
127+
128+
// Delete deletes the file in storage and removes the empty parent directories (if possible)
112129
func (l *LocalStorage) Delete(path string) error {
113-
return util.Remove(l.buildLocalPath(path))
130+
localFullPath := l.buildLocalPath(path)
131+
err := util.Remove(localFullPath)
132+
l.deleteEmptyParentDirs(localFullPath)
133+
return err
114134
}
115135

116136
// URL gets the redirect URL to a file
117137
func (l *LocalStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) {
118138
return nil, ErrURLNotSupported
119139
}
120140

141+
func (l *LocalStorage) normalizeWalkError(err error) error {
142+
if errors.Is(err, os.ErrNotExist) {
143+
// ignore it because the file may be deleted during the walk, and we don't care about it
144+
return nil
145+
}
146+
return err
147+
}
148+
121149
// IterateObjects iterates across the objects in the local storage
122150
func (l *LocalStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error {
123151
dir := l.buildLocalPath(dirName)
124-
return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
125-
if err != nil {
152+
return filepath.WalkDir(dir, func(path string, d os.DirEntry, errWalk error) error {
153+
if err := l.ctx.Err(); err != nil {
126154
return err
127155
}
128-
select {
129-
case <-l.ctx.Done():
130-
return l.ctx.Err()
131-
default:
156+
if errWalk != nil {
157+
return l.normalizeWalkError(errWalk)
132158
}
133-
if path == l.dir {
134-
return nil
135-
}
136-
if d.IsDir() {
159+
if path == l.dir || d.IsDir() {
137160
return nil
138161
}
162+
139163
relPath, err := filepath.Rel(l.dir, path)
140164
if err != nil {
141-
return err
165+
return l.normalizeWalkError(err)
142166
}
143167
obj, err := os.Open(path)
144168
if err != nil {
145-
return err
169+
return l.normalizeWalkError(err)
146170
}
147171
defer obj.Close()
148-
return fn(relPath, obj)
172+
return fn(filepath.ToSlash(relPath), obj)
149173
})
150174
}
151175

modules/storage/local_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
package storage
55

66
import (
7+
"os"
8+
"strings"
79
"testing"
810

911
"code.gitea.io/gitea/modules/setting"
1012

1113
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
1215
)
1316

1417
func TestBuildLocalPath(t *testing.T) {
@@ -53,6 +56,49 @@ func TestBuildLocalPath(t *testing.T) {
5356
}
5457
}
5558

59+
func TestLocalStorageDelete(t *testing.T) {
60+
rootDir := t.TempDir()
61+
st, err := NewLocalStorage(t.Context(), &setting.Storage{Path: rootDir})
62+
require.NoError(t, err)
63+
64+
assertExists := func(t *testing.T, path string, exists bool) {
65+
_, err = os.Stat(rootDir + "/" + path)
66+
if exists {
67+
require.NoError(t, err)
68+
} else {
69+
require.ErrorIs(t, err, os.ErrNotExist)
70+
}
71+
}
72+
73+
_, err = st.Save("dir/sub1/1-a.txt", strings.NewReader(""), -1)
74+
require.NoError(t, err)
75+
_, err = st.Save("dir/sub1/1-b.txt", strings.NewReader(""), -1)
76+
require.NoError(t, err)
77+
_, err = st.Save("dir/sub2/2-a.txt", strings.NewReader(""), -1)
78+
require.NoError(t, err)
79+
80+
assertExists(t, "dir/sub1/1-a.txt", true)
81+
assertExists(t, "dir/sub1/1-b.txt", true)
82+
assertExists(t, "dir/sub2/2-a.txt", true)
83+
84+
require.NoError(t, st.Delete("dir/sub1/1-a.txt"))
85+
assertExists(t, "dir/sub1", true)
86+
assertExists(t, "dir/sub1/1-a.txt", false)
87+
assertExists(t, "dir/sub1/1-b.txt", true)
88+
assertExists(t, "dir/sub2/2-a.txt", true)
89+
90+
require.NoError(t, st.Delete("dir/sub1/1-b.txt"))
91+
assertExists(t, ".", true)
92+
assertExists(t, "dir/sub1", false)
93+
assertExists(t, "dir/sub1/1-a.txt", false)
94+
assertExists(t, "dir/sub1/1-b.txt", false)
95+
assertExists(t, "dir/sub2/2-a.txt", true)
96+
97+
require.NoError(t, st.Delete("dir/sub2/2-a.txt"))
98+
assertExists(t, ".", true)
99+
assertExists(t, "dir", false)
100+
}
101+
56102
func TestLocalStorageIterator(t *testing.T) {
57103
testStorageIterator(t, setting.LocalStorageType, &setting.Storage{Path: t.TempDir()})
58104
}

modules/storage/storage.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,12 @@ type ObjectStorage interface {
6868
Stat(path string) (os.FileInfo, error)
6969
Delete(path string) error
7070
URL(path, name, method string, reqParams url.Values) (*url.URL, error)
71-
IterateObjects(path string, iterator func(path string, obj Object) error) error
71+
72+
// IterateObjects calls the iterator function for each object in the storage with the given path as prefix
73+
// The "fullPath" argument in callback is the full path in this storage.
74+
// * IterateObjects("", ...): iterate all objects in this storage
75+
// * IterateObjects("sub-path", ...): iterate all objects with "sub-path" as prefix in this storage, the "fullPath" will be like "sub-path/xxx"
76+
IterateObjects(basePath string, iterator func(fullPath string, obj Object) error) error
7277
}
7378

7479
// Copy copies a file from source ObjectStorage to dest ObjectStorage

modules/util/path.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,21 @@ const filepathSeparator = string(os.PathSeparator)
7575
// {`/foo`, ``, `bar`} => `/foo/bar`
7676
// {`/foo`, `..`, `bar`} => `/foo/bar`
7777
func FilePathJoinAbs(base string, sub ...string) string {
78-
elems := make([]string, 1, len(sub)+1)
79-
8078
// POSIX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
8179
// to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
82-
if isOSWindows() {
83-
elems[0] = filepath.Clean(base)
84-
} else {
85-
elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", filepathSeparator))
80+
if !isOSWindows() {
81+
base = strings.ReplaceAll(base, "\\", filepathSeparator)
82+
}
83+
if !filepath.IsAbs(base) {
84+
// This shouldn't happen. If it is really necessary to handle relative paths, use filepath.Abs() to get absolute paths first
85+
panic(fmt.Sprintf("FilePathJoinAbs: %q (for path %v) is not absolute, do not guess a relative path based on current working directory", base, sub))
8686
}
87-
if !filepath.IsAbs(elems[0]) {
88-
// This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
89-
panic(fmt.Sprintf("FilePathJoinAbs: %q (for path %v) is not absolute, do not guess a relative path based on current working directory", elems[0], elems))
87+
if len(sub) == 0 {
88+
return filepath.Clean(base)
9089
}
90+
91+
elems := make([]string, 1, len(sub)+1)
92+
elems[0] = base
9193
for _, s := range sub {
9294
if s == "" {
9395
continue
@@ -98,7 +100,7 @@ func FilePathJoinAbs(base string, sub ...string) string {
98100
elems = append(elems, filepath.Clean(filepathSeparator+strings.ReplaceAll(s, "\\", filepathSeparator)))
99101
}
100102
}
101-
// the elems[0] must be an absolute path, just join them together
103+
// the elems[0] must be an absolute path, just join them together, and Join will also do Clean
102104
return filepath.Join(elems...)
103105
}
104106

routers/api/actions/artifacts.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
241241
}
242242

243243
// get upload file size
244-
fileRealTotalSize, contentLength := getUploadFileSize(ctx)
244+
fileRealTotalSize := getUploadFileSize(ctx)
245245

246246
// get artifact retention days
247247
expiredDays := setting.Actions.ArtifactRetentionDays
@@ -265,17 +265,17 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
265265
return
266266
}
267267

268-
// save chunk to storage, if success, return chunk stotal size
268+
// save chunk to storage, if success, return chunks total size
269269
// if artifact is not gzip when uploading, chunksTotalSize == fileRealTotalSize
270270
// if artifact is gzip when uploading, chunksTotalSize < fileRealTotalSize
271-
chunksTotalSize, err := saveUploadChunk(ar.fs, ctx, artifact, contentLength, runID)
271+
chunksTotalSize, err := saveUploadChunkV3GetTotalSize(ar.fs, ctx, artifact, runID)
272272
if err != nil {
273273
log.Error("Error save upload chunk: %v", err)
274274
ctx.HTTPError(http.StatusInternalServerError, "Error save upload chunk")
275275
return
276276
}
277277

278-
// update artifact size if zero or not match, over write artifact size
278+
// update artifact size if zero or not match, overwrite artifact size
279279
if artifact.FileSize == 0 ||
280280
artifact.FileCompressedSize == 0 ||
281281
artifact.FileSize != fileRealTotalSize ||

0 commit comments

Comments
 (0)