feat(05-04): add attach delete lifecycle and static file handler
- DeleteForOwner removes system_files rows in-tx; blobs after commit - Soft-delete of an owner keeps rows and blobs - StaticHandler serves exact partition+disk_name keys and 404s traversal
This commit is contained in:
92
lagoon/attach/static.go
Normal file
92
lagoon/attach/static.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package attach
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gocloud.dev/blob"
|
||||
)
|
||||
|
||||
const defaultStaticContentType = "application/octet-stream"
|
||||
|
||||
// StaticHandler serves GET prefix/<partition>/<disk_name> from bucket.
|
||||
// The blob key is rebuilt from disk_name via PartitionDirectory; request
|
||||
// path segments never reach NewReader unvalidated (T-05-13).
|
||||
func StaticHandler(bucket *blob.Bucket, prefix string) http.Handler {
|
||||
prefix = strings.TrimSuffix(prefix, "/")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if bucket == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
rel, ok := stripStaticPrefix(r.URL.Path, prefix)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
diskName, ok := parsePublicBlobPath(rel)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
key := BlobKey(diskName)
|
||||
reader, err := bucket.NewReader(r.Context(), key, nil)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
ct := reader.ContentType()
|
||||
if ct == "" {
|
||||
ct = defaultStaticContentType
|
||||
}
|
||||
w.Header().Set("Content-Type", ct)
|
||||
if r.Method == http.MethodHead {
|
||||
return
|
||||
}
|
||||
_, _ = io.Copy(w, reader)
|
||||
})
|
||||
}
|
||||
|
||||
func stripStaticPrefix(path, prefix string) (string, bool) {
|
||||
if prefix == "" {
|
||||
return strings.TrimPrefix(path, "/"), true
|
||||
}
|
||||
if path == prefix {
|
||||
return "", false
|
||||
}
|
||||
if strings.HasPrefix(path, prefix+"/") {
|
||||
return path[len(prefix)+1:], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// parsePublicBlobPath accepts exactly 3 partition groups plus disk_name
|
||||
// whose PartitionDirectory matches those groups. Rejects "..", empty
|
||||
// segments, extra slashes, and mismatched partitions.
|
||||
func parsePublicBlobPath(p string) (string, bool) {
|
||||
if p == "" || strings.Contains(p, "\\") || strings.Contains(p, "..") || strings.Contains(p, "//") {
|
||||
return "", false
|
||||
}
|
||||
parts := strings.Split(p, "/")
|
||||
if len(parts) != 4 {
|
||||
return "", false
|
||||
}
|
||||
for _, part := range parts {
|
||||
if part == "" || part == "." || part == ".." {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
diskName := parts[3]
|
||||
got := strings.Join(parts[:3], "/")
|
||||
want := strings.TrimSuffix(PartitionDirectory(diskName), "/")
|
||||
if got != want {
|
||||
return "", false
|
||||
}
|
||||
return diskName, true
|
||||
}
|
||||
Reference in New Issue
Block a user