fix1
This commit is contained in:
161
internal/blobfs/blobfs.go
Normal file
161
internal/blobfs/blobfs.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package blobfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Meta struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ContentType string `json:"contentType"`
|
||||
SHA256 string `json:"sha256"`
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
Save(ctx context.Context, id int64, filename string, r io.Reader) (Meta, error)
|
||||
Open(ctx context.Context, id int64) (io.ReadSeekCloser, Meta, error)
|
||||
Stat(ctx context.Context, id int64) (Meta, bool, error)
|
||||
Delete(ctx context.Context, id int64) error
|
||||
}
|
||||
|
||||
type FS struct{ root string }
|
||||
|
||||
func New(root string) *FS { return &FS{root: root} }
|
||||
|
||||
func (fs *FS) dir(id int64) string { return filepath.Join(fs.root, "files", fmt.Sprintf("%d", id)) }
|
||||
func (fs *FS) metaPath(id int64) string { return filepath.Join(fs.dir(id), "meta.json") }
|
||||
func (fs *FS) blobPath(id int64, name string) string {
|
||||
return filepath.Join(fs.dir(id), "blob"+safeExt(name))
|
||||
}
|
||||
|
||||
func safeExt(name string) string {
|
||||
ext := filepath.Ext(name)
|
||||
if len(ext) > 16 { // unrealistisch lange Exts beschneiden
|
||||
ext = ext[:16]
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
func (fs *FS) Save(_ context.Context, id int64, filename string, r io.Reader) (Meta, error) {
|
||||
if strings.TrimSpace(filename) == "" {
|
||||
return Meta{}, errors.New("filename required")
|
||||
}
|
||||
if err := os.MkdirAll(fs.dir(id), 0o755); err != nil {
|
||||
return Meta{}, err
|
||||
}
|
||||
tmp := filepath.Join(fs.dir(id), "blob.tmp")
|
||||
out, err := os.Create(tmp)
|
||||
if err != nil {
|
||||
return Meta{}, err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
hasher := sha256.New()
|
||||
tee := io.TeeReader(r, hasher)
|
||||
|
||||
// Content-Type sniffen: ersten 512 Bytes puffern
|
||||
buf := make([]byte, 512)
|
||||
n, _ := io.ReadFull(tee, buf)
|
||||
if n > 0 {
|
||||
if _, err := out.Write(buf[:n]); err != nil {
|
||||
return Meta{}, err
|
||||
}
|
||||
}
|
||||
// Rest kopieren
|
||||
size := int64(n)
|
||||
written, err := io.Copy(out, tee)
|
||||
if err != nil {
|
||||
return Meta{}, err
|
||||
}
|
||||
size += written
|
||||
|
||||
// Hash
|
||||
sum := hex.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
ct := http.DetectContentType(buf[:n])
|
||||
if ct == "application/octet-stream" {
|
||||
// Versuch über Dateiendung
|
||||
if ext := filepath.Ext(filename); ext != "" {
|
||||
if byExt := mime.TypeByExtension(ext); byExt != "" {
|
||||
ct = byExt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finaler Ort
|
||||
final := fs.blobPath(id, filename)
|
||||
if err := os.Rename(tmp, final); err != nil {
|
||||
return Meta{}, err
|
||||
}
|
||||
|
||||
meta := Meta{Name: filename, Size: size, ContentType: ct, SHA256: sum}
|
||||
if err := writeJSON(fs.metaPath(id), meta); err != nil {
|
||||
return Meta{}, err
|
||||
}
|
||||
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func (fs *FS) Open(_ context.Context, id int64) (io.ReadSeekCloser, Meta, error) {
|
||||
meta, ok, err := fs.Stat(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, Meta{}, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, Meta{}, os.ErrNotExist
|
||||
}
|
||||
f, err := os.Open(fs.blobPath(id, meta.Name))
|
||||
return f, meta, err
|
||||
}
|
||||
|
||||
func (fs *FS) Stat(_ context.Context, id int64) (Meta, bool, error) {
|
||||
b, err := os.ReadFile(fs.metaPath(id))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return Meta{}, false, nil
|
||||
}
|
||||
return Meta{}, false, err
|
||||
}
|
||||
var m Meta
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return Meta{}, false, err
|
||||
}
|
||||
// Größe aus FS gegenprüfen (falls manipuliert)
|
||||
info, err := os.Stat(fs.blobPath(id, m.Name))
|
||||
if err == nil {
|
||||
m.Size = info.Size()
|
||||
}
|
||||
return m, true, nil
|
||||
}
|
||||
|
||||
func (fs *FS) Delete(_ context.Context, id int64) error {
|
||||
return os.RemoveAll(fs.dir(id))
|
||||
}
|
||||
|
||||
func writeJSON(path string, v any) error {
|
||||
tmp := path + ".tmp"
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.NewEncoder(f).Encode(v); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
Reference in New Issue
Block a user