feat: establish NekoNest Cloud control and relay
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
package attachmentstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/klarkxy/nekonest/relaycore"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
root string
|
||||
mu sync.RWMutex
|
||||
usedBytes int64
|
||||
fileCount int
|
||||
maxBytes int64
|
||||
maxFiles int
|
||||
}
|
||||
|
||||
func New(root string) (*Store, error) {
|
||||
return NewWithLimits(root, 1<<30, 10_000)
|
||||
}
|
||||
|
||||
func NewWithLimits(root string, maxBytes int64, maxFiles int) (*Store, error) {
|
||||
root = filepath.Clean(strings.TrimSpace(root))
|
||||
if !filepath.IsAbs(root) {
|
||||
return nil, fmt.Errorf("attachment root must be absolute")
|
||||
}
|
||||
if err := os.MkdirAll(root, 0o700); err != nil {
|
||||
return nil, fmt.Errorf("create attachment root: %w", err)
|
||||
}
|
||||
if err := os.Chmod(root, 0o700); err != nil {
|
||||
return nil, fmt.Errorf("secure attachment root: %w", err)
|
||||
}
|
||||
info, err := os.Lstat(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
||||
return nil, fmt.Errorf("attachment root must be a real directory")
|
||||
}
|
||||
if maxBytes <= 0 || maxFiles <= 0 {
|
||||
return nil, fmt.Errorf("attachment limits must be positive")
|
||||
}
|
||||
store := &Store{root: root, maxBytes: maxBytes, maxFiles: maxFiles}
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.Type()&os.ModeSymlink != 0 {
|
||||
return nil, fmt.Errorf("attachment store contains a symbolic link")
|
||||
}
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".bin") {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil || !info.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("inspect attachment payload %q", entry.Name())
|
||||
}
|
||||
store.usedBytes += info.Size()
|
||||
store.fileCount++
|
||||
}
|
||||
if store.usedBytes > maxBytes || store.fileCount > maxFiles {
|
||||
return nil, fmt.Errorf("attachment store already exceeds configured quota")
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func validID(id string) bool {
|
||||
if len(id) != 32 {
|
||||
return false
|
||||
}
|
||||
for _, char := range id {
|
||||
if !strings.ContainsRune("0123456789abcdef", char) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Store) paths(id string) (string, string, error) {
|
||||
if !validID(id) {
|
||||
return "", "", fmt.Errorf("invalid attachment id")
|
||||
}
|
||||
payload := filepath.Join(s.root, id+".bin")
|
||||
metadata := filepath.Join(s.root, id+".json")
|
||||
if filepath.Dir(payload) != s.root || filepath.Dir(metadata) != s.root {
|
||||
return "", "", fmt.Errorf("attachment path escaped root")
|
||||
}
|
||||
return payload, metadata, nil
|
||||
}
|
||||
|
||||
func (s *Store) Put(ctx context.Context, attachment relaycore.Attachment, body []byte) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if int64(len(body)) > s.maxBytes-s.usedBytes || s.fileCount >= s.maxFiles {
|
||||
return fmt.Errorf("attachment quota exceeded")
|
||||
}
|
||||
payloadPath, metadataPath, err := s.paths(attachment.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metadata, err := json.Marshal(attachment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := os.OpenFile(payloadPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create attachment payload: %w", err)
|
||||
}
|
||||
removePayload := true
|
||||
defer func() {
|
||||
_ = payload.Close()
|
||||
if removePayload {
|
||||
_ = os.Remove(payloadPath)
|
||||
}
|
||||
}()
|
||||
if _, err := payload.Write(body); err != nil {
|
||||
return fmt.Errorf("write attachment payload: %w", err)
|
||||
}
|
||||
if err := payload.Sync(); err != nil {
|
||||
return fmt.Errorf("sync attachment payload: %w", err)
|
||||
}
|
||||
if err := payload.Close(); err != nil {
|
||||
return fmt.Errorf("close attachment payload: %w", err)
|
||||
}
|
||||
meta, err := os.OpenFile(metadataPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create attachment metadata: %w", err)
|
||||
}
|
||||
if _, err := meta.Write(metadata); err != nil {
|
||||
_ = meta.Close()
|
||||
_ = os.Remove(metadataPath)
|
||||
return fmt.Errorf("write attachment metadata: %w", err)
|
||||
}
|
||||
if err := meta.Sync(); err != nil {
|
||||
_ = meta.Close()
|
||||
_ = os.Remove(metadataPath)
|
||||
return fmt.Errorf("sync attachment metadata: %w", err)
|
||||
}
|
||||
if err := meta.Close(); err != nil {
|
||||
_ = os.Remove(metadataPath)
|
||||
return fmt.Errorf("close attachment metadata: %w", err)
|
||||
}
|
||||
removePayload = false
|
||||
s.usedBytes += int64(len(body))
|
||||
s.fileCount++
|
||||
return nil
|
||||
}
|
||||
|
||||
func rejectLink(path string) error {
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("attachment artifact is not a regular file")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) Get(ctx context.Context, id string) (relaycore.Attachment, io.ReadCloser, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return relaycore.Attachment{}, nil, err
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
payloadPath, metadataPath, err := s.paths(id)
|
||||
if err != nil {
|
||||
return relaycore.Attachment{}, nil, err
|
||||
}
|
||||
if err := rejectLink(metadataPath); err != nil {
|
||||
return relaycore.Attachment{}, nil, err
|
||||
}
|
||||
metadata, err := os.ReadFile(metadataPath)
|
||||
if err != nil {
|
||||
return relaycore.Attachment{}, nil, err
|
||||
}
|
||||
var attachment relaycore.Attachment
|
||||
if err := json.Unmarshal(metadata, &attachment); err != nil || attachment.ID != id {
|
||||
return relaycore.Attachment{}, nil, fmt.Errorf("invalid attachment metadata")
|
||||
}
|
||||
if err := rejectLink(payloadPath); err != nil {
|
||||
return relaycore.Attachment{}, nil, err
|
||||
}
|
||||
body, err := os.Open(payloadPath)
|
||||
if err != nil {
|
||||
return relaycore.Attachment{}, nil, err
|
||||
}
|
||||
return attachment, body, nil
|
||||
}
|
||||
|
||||
func (s *Store) Delete(_ context.Context, id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
payloadPath, metadataPath, err := s.paths(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var size int64
|
||||
existed := false
|
||||
if info, statErr := os.Lstat(payloadPath); statErr == nil && info.Mode().IsRegular() && info.Mode()&os.ModeSymlink == 0 {
|
||||
size = info.Size()
|
||||
existed = true
|
||||
}
|
||||
var joined error
|
||||
for _, path := range []string{metadataPath, payloadPath} {
|
||||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
joined = errors.Join(joined, err)
|
||||
}
|
||||
}
|
||||
if joined == nil && existed {
|
||||
s.usedBytes -= size
|
||||
if s.usedBytes < 0 {
|
||||
s.usedBytes = 0
|
||||
}
|
||||
if s.fileCount > 0 {
|
||||
s.fileCount--
|
||||
}
|
||||
}
|
||||
return joined
|
||||
}
|
||||
Reference in New Issue
Block a user