File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/artifact/artifact.go
242 lines · 6.5kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
// Package artifact keeps the files attached to a release, outside git. Chapter 22.2.
133728e barerepo 1mo
2
package artifact
133728e barerepo 1mo
3
133728e barerepo 1mo
4
import (
133728e barerepo 1mo
5
"fmt"
133728e barerepo 1mo
6
"io"
133728e barerepo 1mo
7
"io/fs"
133728e barerepo 1mo
8
"os"
133728e barerepo 1mo
9
"path/filepath"
133728e barerepo 1mo
10
"sort"
133728e barerepo 1mo
11
"strings"
133728e barerepo 1mo
12
"time"
133728e barerepo 1mo
13
133728e barerepo 1mo
14
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
15
)
133728e barerepo 1mo
16
133728e barerepo 1mo
17
// File is one attached file, named and measured, which is what the releases page draws.
133728e barerepo 1mo
18
type File struct {
133728e barerepo 1mo
19
Name string
133728e barerepo 1mo
20
Size int64
133728e barerepo 1mo
21
}
133728e barerepo 1mo
22
133728e barerepo 1mo
23
// ValidName reports a file name safe to join to a path, which is the only check that matters here.
133728e barerepo 1mo
24
func ValidName(name string) bool {
133728e barerepo 1mo
25
if name == "" || len(name) > 128 || name == "." || name == ".." {
133728e barerepo 1mo
26
return false
133728e barerepo 1mo
27
}
133728e barerepo 1mo
28
if strings.ContainsAny(name, "/\\\x00") || strings.HasPrefix(name, ".") {
133728e barerepo 1mo
29
return false
133728e barerepo 1mo
30
}
133728e barerepo 1mo
31
for _, c := range name {
133728e barerepo 1mo
32
switch {
133728e barerepo 1mo
33
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
133728e barerepo 1mo
34
case c == '.' || c == '-' || c == '_' || c == '+':
133728e barerepo 1mo
35
default:
133728e barerepo 1mo
36
return false
133728e barerepo 1mo
37
}
133728e barerepo 1mo
38
}
133728e barerepo 1mo
39
return true
133728e barerepo 1mo
40
}
133728e barerepo 1mo
41
133728e barerepo 1mo
42
// dir resolves where one tag's files live, refusing anything that would leave the root.
133728e barerepo 1mo
43
func dir(root, owner, name, tag string) (string, error) {
133728e barerepo 1mo
44
if !gitx.ValidName(owner) || !gitx.ValidRepoName(name) || !gitx.ValidRef("refs/tags/"+tag) {
133728e barerepo 1mo
45
return "", fmt.Errorf("no such release")
133728e barerepo 1mo
46
}
133728e barerepo 1mo
47
absRoot, err := filepath.Abs(root)
133728e barerepo 1mo
48
if err != nil {
133728e barerepo 1mo
49
return "", err
133728e barerepo 1mo
50
}
133728e barerepo 1mo
51
// A tag may hold a slash, so it is one path element with the slash spelled out.
133728e barerepo 1mo
52
safe := strings.ReplaceAll(tag, "/", "%2F")
133728e barerepo 1mo
53
if safe == "." || safe == ".." || strings.HasPrefix(safe, ".") {
133728e barerepo 1mo
54
return "", fmt.Errorf("no such release")
133728e barerepo 1mo
55
}
133728e barerepo 1mo
56
out := filepath.Join(absRoot, owner, name, safe)
133728e barerepo 1mo
57
if !strings.HasPrefix(out, absRoot+string(os.PathSeparator)) {
133728e barerepo 1mo
58
return "", fmt.Errorf("no such release")
133728e barerepo 1mo
59
}
133728e barerepo 1mo
60
return out, nil
133728e barerepo 1mo
61
}
133728e barerepo 1mo
62
133728e barerepo 1mo
63
// Put writes one attached file, replacing what was there, so a rerun does not double the list.
133728e barerepo 1mo
64
func Put(root, owner, name, tag, file string, body io.Reader) (int64, error) {
133728e barerepo 1mo
65
if !ValidName(file) {
133728e barerepo 1mo
66
return 0, fmt.Errorf("%q is not a usable file name", file)
133728e barerepo 1mo
67
}
133728e barerepo 1mo
68
d, err := dir(root, owner, name, tag)
133728e barerepo 1mo
69
if err != nil {
133728e barerepo 1mo
70
return 0, err
133728e barerepo 1mo
71
}
133728e barerepo 1mo
72
if err := os.MkdirAll(d, 0o750); err != nil {
133728e barerepo 1mo
73
return 0, err
133728e barerepo 1mo
74
}
133728e barerepo 1mo
75
// A unique part file, so two uploads of one name cannot write into each other.
133728e barerepo 1mo
76
f, err := os.CreateTemp(d, "."+file+".*.part")
133728e barerepo 1mo
77
if err != nil {
133728e barerepo 1mo
78
return 0, err
133728e barerepo 1mo
79
}
133728e barerepo 1mo
80
tmp := f.Name()
133728e barerepo 1mo
81
n, err := io.Copy(f, body)
133728e barerepo 1mo
82
if cerr := f.Close(); err == nil {
133728e barerepo 1mo
83
err = cerr
133728e barerepo 1mo
84
}
133728e barerepo 1mo
85
if err != nil {
133728e barerepo 1mo
86
os.Remove(tmp)
133728e barerepo 1mo
87
return 0, err
133728e barerepo 1mo
88
}
133728e barerepo 1mo
89
// The rename is what makes a half written upload invisible to a reader.
133728e barerepo 1mo
90
if err := os.Rename(tmp, filepath.Join(d, file)); err != nil {
133728e barerepo 1mo
91
os.Remove(tmp)
133728e barerepo 1mo
92
return 0, err
133728e barerepo 1mo
93
}
133728e barerepo 1mo
94
return n, nil
133728e barerepo 1mo
95
}
133728e barerepo 1mo
96
133728e barerepo 1mo
97
// ListAll reads one repository's releases in a single pass, so a page with no files costs one stat.
133728e barerepo 1mo
98
func ListAll(root, owner, name string) (map[string][]File, error) {
133728e barerepo 1mo
99
d, err := repoDir(root, owner, name)
133728e barerepo 1mo
100
if err != nil {
133728e barerepo 1mo
101
return nil, err
133728e barerepo 1mo
102
}
133728e barerepo 1mo
103
tags, err := os.ReadDir(d)
133728e barerepo 1mo
104
if err != nil {
133728e barerepo 1mo
105
return nil, nil
133728e barerepo 1mo
106
}
133728e barerepo 1mo
107
out := make(map[string][]File, len(tags))
133728e barerepo 1mo
108
for _, t := range tags {
133728e barerepo 1mo
109
if !t.IsDir() {
133728e barerepo 1mo
110
continue
133728e barerepo 1mo
111
}
133728e barerepo 1mo
112
files, err := readFiles(filepath.Join(d, t.Name()))
133728e barerepo 1mo
113
if err != nil || len(files) == 0 {
133728e barerepo 1mo
114
continue
133728e barerepo 1mo
115
}
133728e barerepo 1mo
116
out[unescapeTag(t.Name())] = files
133728e barerepo 1mo
117
}
133728e barerepo 1mo
118
return out, nil
133728e barerepo 1mo
119
}
133728e barerepo 1mo
120
133728e barerepo 1mo
121
// readFiles names what is in one release's directory, skipping the part files an upload leaves.
133728e barerepo 1mo
122
func readFiles(d string) ([]File, error) {
133728e barerepo 1mo
123
entries, err := os.ReadDir(d)
133728e barerepo 1mo
124
if err != nil {
133728e barerepo 1mo
125
return nil, err
133728e barerepo 1mo
126
}
133728e barerepo 1mo
127
out := make([]File, 0, len(entries))
133728e barerepo 1mo
128
for _, e := range entries {
133728e barerepo 1mo
129
if e.IsDir() || !ValidName(e.Name()) {
133728e barerepo 1mo
130
continue
133728e barerepo 1mo
131
}
133728e barerepo 1mo
132
info, err := e.Info()
133728e barerepo 1mo
133
if err != nil {
133728e barerepo 1mo
134
continue
133728e barerepo 1mo
135
}
133728e barerepo 1mo
136
out = append(out, File{Name: e.Name(), Size: info.Size()})
133728e barerepo 1mo
137
}
133728e barerepo 1mo
138
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
133728e barerepo 1mo
139
return out, nil
133728e barerepo 1mo
140
}
133728e barerepo 1mo
141
133728e barerepo 1mo
142
func unescapeTag(dirName string) string { return strings.ReplaceAll(dirName, "%2F", "/") }
133728e barerepo 1mo
143
133728e barerepo 1mo
144
// Open reads one attached file back, and the caller closes it.
133728e barerepo 1mo
145
func Open(root, owner, name, tag, file string) (*os.File, os.FileInfo, error) {
133728e barerepo 1mo
146
if !ValidName(file) {
133728e barerepo 1mo
147
return nil, nil, fmt.Errorf("no such file")
133728e barerepo 1mo
148
}
133728e barerepo 1mo
149
d, err := dir(root, owner, name, tag)
133728e barerepo 1mo
150
if err != nil {
133728e barerepo 1mo
151
return nil, nil, err
133728e barerepo 1mo
152
}
133728e barerepo 1mo
153
f, err := os.Open(filepath.Join(d, file))
133728e barerepo 1mo
154
if err != nil {
133728e barerepo 1mo
155
return nil, nil, err
133728e barerepo 1mo
156
}
133728e barerepo 1mo
157
info, err := f.Stat()
133728e barerepo 1mo
158
if err != nil || info.IsDir() {
133728e barerepo 1mo
159
f.Close()
133728e barerepo 1mo
160
return nil, nil, fmt.Errorf("no such file")
133728e barerepo 1mo
161
}
133728e barerepo 1mo
162
return f, info, nil
133728e barerepo 1mo
163
}
133728e barerepo 1mo
164
133728e barerepo 1mo
165
// Move carries a repository's files to its new name, since nothing in git holds them. 22.2.
133728e barerepo 1mo
166
func Move(root, oldOwner, oldName, newOwner, newName string) error {
133728e barerepo 1mo
167
from, err := repoDir(root, oldOwner, oldName)
133728e barerepo 1mo
168
if err != nil {
133728e barerepo 1mo
169
return err
133728e barerepo 1mo
170
}
133728e barerepo 1mo
171
to, err := repoDir(root, newOwner, newName)
133728e barerepo 1mo
172
if err != nil {
133728e barerepo 1mo
173
return err
133728e barerepo 1mo
174
}
133728e barerepo 1mo
175
if _, err := os.Stat(from); err != nil {
133728e barerepo 1mo
176
// A repository with nothing attached has nothing to move, which is most of them.
133728e barerepo 1mo
177
return nil
133728e barerepo 1mo
178
}
133728e barerepo 1mo
179
if err := os.MkdirAll(filepath.Dir(to), 0o750); err != nil {
133728e barerepo 1mo
180
return err
133728e barerepo 1mo
181
}
133728e barerepo 1mo
182
return os.Rename(from, to)
133728e barerepo 1mo
183
}
133728e barerepo 1mo
184
133728e barerepo 1mo
185
// repoDir is where one repository's releases live, refusing anything that would leave the root.
133728e barerepo 1mo
186
func repoDir(root, owner, name string) (string, error) {
133728e barerepo 1mo
187
if !gitx.ValidName(owner) || !gitx.ValidRepoName(name) {
133728e barerepo 1mo
188
return "", fmt.Errorf("no such repository")
133728e barerepo 1mo
189
}
133728e barerepo 1mo
190
absRoot, err := filepath.Abs(root)
133728e barerepo 1mo
191
if err != nil {
133728e barerepo 1mo
192
return "", err
133728e barerepo 1mo
193
}
133728e barerepo 1mo
194
out := filepath.Join(absRoot, owner, name)
133728e barerepo 1mo
195
if !strings.HasPrefix(out, absRoot+string(os.PathSeparator)) {
133728e barerepo 1mo
196
return "", fmt.Errorf("no such repository")
133728e barerepo 1mo
197
}
133728e barerepo 1mo
198
return out, nil
133728e barerepo 1mo
199
}
133728e barerepo 1mo
200
133728e barerepo 1mo
201
// Forget drops every file a repository has, for a delete, since the blob store is not in git.
133728e barerepo 1mo
202
func Forget(root, owner, name string) error {
133728e barerepo 1mo
203
out, err := repoDir(root, owner, name)
133728e barerepo 1mo
204
if err != nil {
133728e barerepo 1mo
205
return err
133728e barerepo 1mo
206
}
133728e barerepo 1mo
207
return os.RemoveAll(out)
133728e barerepo 1mo
208
}
133728e barerepo 1mo
209
133728e barerepo 1mo
210
// PartWindow is how long one upload may be in flight before its temporary counts as abandoned.
133728e barerepo 1mo
211
const PartWindow = 24 * time.Hour
133728e barerepo 1mo
212
133728e barerepo 1mo
213
// SweepParts removes what a killed process left mid-copy, which no page lists and nothing else frees.
133728e barerepo 1mo
214
func SweepParts(root string, window time.Duration, now time.Time) (int, error) {
133728e barerepo 1mo
215
if window <= 0 {
133728e barerepo 1mo
216
return 0, nil
133728e barerepo 1mo
217
}
133728e barerepo 1mo
218
gone := 0
133728e barerepo 1mo
219
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
133728e barerepo 1mo
220
if err != nil || d.IsDir() {
133728e barerepo 1mo
221
return nil
133728e barerepo 1mo
222
}
222 of 242 lines · the whole file
history · rawbarerepo 0.1.0