fork of whitequark.org/git-pages with mods for tangled
at main 7.0 kB view raw
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}