forked from
whitequark.org/git-pages
fork of whitequark.org/git-pages with mods for tangled
1package git_pages
2
3import (
4 "context"
5 "crypto/sha256"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "os"
11 "path/filepath"
12 "strings"
13 "time"
14)
15
16type FSBackend struct {
17 blobRoot *os.Root
18 siteRoot *os.Root
19}
20
21var _ Backend = (*FSBackend)(nil)
22
23func maybeCreateOpenRoot(dir string, name string) (*os.Root, error) {
24 dirName := filepath.Join(dir, name)
25
26 if err := os.Mkdir(dirName, 0o755); err != nil && !errors.Is(err, os.ErrExist) {
27 return nil, fmt.Errorf("mkdir: %w", err)
28 }
29
30 root, err := os.OpenRoot(dirName)
31 if err != nil {
32 return nil, fmt.Errorf("open: %w", err)
33 }
34
35 return root, nil
36}
37
38func createTempInRoot(root *os.Root, name string, data []byte) (string, error) {
39 tempFile, err := os.CreateTemp(root.Name(), name)
40 if err != nil {
41 return "", fmt.Errorf("mktemp: %w", err)
42 }
43 _, err = tempFile.Write(data)
44 tempFile.Close()
45 if err != nil {
46 return "", fmt.Errorf("write: %w", err)
47 }
48
49 tempPath, err := filepath.Rel(root.Name(), tempFile.Name())
50 if err != nil {
51 return "", fmt.Errorf("relpath: %w", err)
52 }
53
54 return tempPath, nil
55}
56
57func NewFSBackend(config *FSConfig) (*FSBackend, error) {
58 blobRoot, err := maybeCreateOpenRoot(config.Root, "blob")
59 if err != nil {
60 return nil, fmt.Errorf("blob: %w", err)
61 }
62 siteRoot, err := maybeCreateOpenRoot(config.Root, "site")
63 if err != nil {
64 return nil, fmt.Errorf("site: %w", err)
65 }
66 return &FSBackend{blobRoot, siteRoot}, nil
67}
68
69func (fs *FSBackend) Backend() Backend {
70 return fs
71}
72
73func (fs *FSBackend) HasFeature(ctx context.Context, feature BackendFeature) bool {
74 switch feature {
75 case FeatureCheckDomainMarker:
76 return true
77 default:
78 return false
79 }
80}
81
82func (fs *FSBackend) EnableFeature(ctx context.Context, feature BackendFeature) error {
83 switch feature {
84 case FeatureCheckDomainMarker:
85 return nil
86 default:
87 return fmt.Errorf("not implemented")
88 }
89}
90
91func (fs *FSBackend) GetBlob(
92 ctx context.Context, name string,
93) (
94 reader io.ReadSeeker, size uint64, mtime time.Time, err error,
95) {
96 blobPath := filepath.Join(splitBlobName(name)...)
97 stat, err := fs.blobRoot.Stat(blobPath)
98 if errors.Is(err, os.ErrNotExist) {
99 err = fmt.Errorf("%w: %s", ErrObjectNotFound, err.(*os.PathError).Path)
100 return
101 } else if err != nil {
102 err = fmt.Errorf("stat: %w", err)
103 return
104 }
105 file, err := fs.blobRoot.Open(blobPath)
106 if err != nil {
107 err = fmt.Errorf("open: %w", err)
108 return
109 }
110 return file, uint64(stat.Size()), stat.ModTime(), nil
111}
112
113func (fs *FSBackend) PutBlob(ctx context.Context, name string, data []byte) error {
114 blobPath := filepath.Join(splitBlobName(name)...)
115 blobDir := filepath.Dir(blobPath)
116
117 tempPath, err := createTempInRoot(fs.blobRoot, name, data)
118 if err != nil {
119 return err
120 }
121
122 if err := fs.blobRoot.Chmod(tempPath, 0o444); err != nil {
123 return fmt.Errorf("chmod: %w", err)
124 }
125
126again:
127 for {
128 if err := fs.blobRoot.MkdirAll(blobDir, 0o755); err != nil {
129 if errors.Is(err, os.ErrExist) {
130 // Handle the case where two `PutBlob()` calls race creating a common prefix
131 // of a blob directory. The `MkdirAll()` call that loses the TOCTTOU condition
132 // bails out, so we have to repeat it.
133 continue again
134 }
135 return fmt.Errorf("mkdir: %w", err)
136 }
137 break
138 }
139
140 if err := fs.blobRoot.Rename(tempPath, blobPath); err != nil {
141 return fmt.Errorf("rename: %w", err)
142 }
143
144 return nil
145}
146
147func (fs *FSBackend) DeleteBlob(ctx context.Context, name string) error {
148 blobPath := filepath.Join(splitBlobName(name)...)
149 return fs.blobRoot.Remove(blobPath)
150}
151
152func (b *FSBackend) ListManifests(ctx context.Context) (manifests []string, err error) {
153 err = fs.WalkDir(b.siteRoot.FS(), ".", func(path string, d fs.DirEntry, err error) error {
154 if strings.Count(path, "/") > 1 {
155 return fs.SkipDir
156 }
157 _, project, _ := strings.Cut(path, "/")
158 if project == "" || strings.HasPrefix(project, ".") && project != ".index" {
159 return nil
160 }
161 manifests = append(manifests, path)
162 return nil
163 })
164 return
165}
166
167func (fs *FSBackend) GetManifest(
168 ctx context.Context, name string, opts GetManifestOptions,
169) (
170 manifest *Manifest, mtime time.Time, err error,
171) {
172 stat, err := fs.siteRoot.Stat(name)
173 if errors.Is(err, os.ErrNotExist) {
174 err = fmt.Errorf("%w: %s", ErrObjectNotFound, err.(*os.PathError).Path)
175 return
176 } else if err != nil {
177 err = fmt.Errorf("stat: %w", err)
178 return
179 }
180 data, err := fs.siteRoot.ReadFile(name)
181 if err != nil {
182 err = fmt.Errorf("read: %w", err)
183 return
184 }
185 manifest, err = DecodeManifest(data)
186 if err != nil {
187 return
188 }
189 return manifest, stat.ModTime(), nil
190}
191
192func stagedManifestName(manifestData []byte) string {
193 return fmt.Sprintf(".%x", sha256.Sum256(manifestData))
194}
195
196func (fs *FSBackend) StageManifest(ctx context.Context, manifest *Manifest) error {
197 manifestData := EncodeManifest(manifest)
198
199 tempPath, err := createTempInRoot(fs.siteRoot, ".manifest", manifestData)
200 if err != nil {
201 return err
202 }
203
204 if err := fs.siteRoot.Rename(tempPath, stagedManifestName(manifestData)); err != nil {
205 return fmt.Errorf("rename: %w", err)
206 }
207
208 return nil
209}
210
211func domainFrozenMarkerName(domain string) string {
212 return filepath.Join(domain, ".frozen")
213}
214
215func (fs *FSBackend) checkDomainFrozen(ctx context.Context, domain string) error {
216 if _, err := fs.siteRoot.Stat(domainFrozenMarkerName(domain)); err == nil {
217 return ErrDomainFrozen
218 } else if !errors.Is(err, os.ErrNotExist) {
219 return fmt.Errorf("stat: %w", err)
220 } else {
221 return nil
222 }
223}
224
225func (fs *FSBackend) CommitManifest(ctx context.Context, name string, manifest *Manifest) error {
226 domain := filepath.Dir(name)
227 if err := fs.checkDomainFrozen(ctx, domain); err != nil {
228 return err
229 }
230
231 manifestData := EncodeManifest(manifest)
232 manifestHashName := stagedManifestName(manifestData)
233
234 if _, err := fs.siteRoot.Stat(manifestHashName); err != nil {
235 return fmt.Errorf("manifest not staged")
236 }
237
238 if err := fs.siteRoot.MkdirAll(domain, 0o755); err != nil {
239 return fmt.Errorf("mkdir: %w", err)
240 }
241
242 if err := fs.siteRoot.Rename(manifestHashName, name); err != nil {
243 return fmt.Errorf("rename: %w", err)
244 }
245
246 return nil
247}
248
249func (fs *FSBackend) DeleteManifest(ctx context.Context, name string) error {
250 domain := filepath.Dir(name)
251 if err := fs.checkDomainFrozen(ctx, domain); err != nil {
252 return err
253 }
254
255 err := fs.siteRoot.Remove(name)
256 if errors.Is(err, os.ErrNotExist) {
257 return nil
258 } else {
259 return err
260 }
261}
262
263func (fs *FSBackend) CheckDomain(ctx context.Context, domain string) (bool, error) {
264 _, err := fs.siteRoot.Stat(domain)
265 if errors.Is(err, os.ErrNotExist) {
266 return false, nil
267 } else if err == nil {
268 return true, nil
269 } else {
270 return false, err
271 }
272}
273
274func (fs *FSBackend) CreateDomain(ctx context.Context, domain string) error {
275 return nil // no-op
276}
277
278func (fs *FSBackend) FreezeDomain(ctx context.Context, domain string, freeze bool) error {
279 if freeze {
280 return fs.siteRoot.WriteFile(domainFrozenMarkerName(domain), []byte{}, 0o644)
281 } else {
282 err := fs.siteRoot.Remove(domainFrozenMarkerName(domain))
283 if errors.Is(err, os.ErrNotExist) {
284 return nil
285 } else {
286 return err
287 }
288 }
289}