- tmux som sanningskälla (agentoberoende, överlever omstart via adoption) - API: sessions, prompt, question/answer, keys, mode, config, shares, notify, events - frågedetektor testad mot riktiga agy 1.1.9-dumpar + mock - integrationstest: 26 tester i isolerad debian-container (scripts/run-integration.sh) - e2e-verifierad mot riktig agy (trust-fråga -> svar -> prompt) - .gitea/workflows/helmd-release.yaml -> rullande helmd-latest (x64+arm64) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
2.9 KiB
Go
124 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Share is a file the agent published for the clients to view
|
|
// (reports, screenshots, generated pages...).
|
|
type Share struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Note string `json:"note,omitempty"`
|
|
Session string `json:"session,omitempty"`
|
|
Size int64 `json:"size"`
|
|
Mime string `json:"mime"`
|
|
Created time.Time `json:"created"`
|
|
}
|
|
|
|
type ShareStore struct {
|
|
dir string
|
|
cfg *Config
|
|
}
|
|
|
|
func NewShareStore(cfg *Config) (*ShareStore, error) {
|
|
if err := os.MkdirAll(cfg.ShareDir, 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
return &ShareStore{dir: cfg.ShareDir, cfg: cfg}, nil
|
|
}
|
|
|
|
func (s *ShareStore) metaPath(id string) string { return filepath.Join(s.dir, id+".meta.json") }
|
|
func (s *ShareStore) filePath(id, name string) string {
|
|
return filepath.Join(s.dir, id+"_"+filepath.Base(name))
|
|
}
|
|
|
|
// Add stores content under a fresh id and writes a metadata sidecar.
|
|
func (s *ShareStore) Add(name, note, session string, r io.Reader) (*Share, error) {
|
|
name = filepath.Base(name)
|
|
if !s.cfg.extAllowed(name) {
|
|
return nil, fmt.Errorf("filtyp tillåts inte: %s (tillåtna: %s)",
|
|
filepath.Ext(name), strings.Join(s.cfg.ShareExts, " "))
|
|
}
|
|
idb := make([]byte, 6)
|
|
if _, err := rand.Read(idb); err != nil {
|
|
return nil, err
|
|
}
|
|
id := hex.EncodeToString(idb)
|
|
f, err := os.Create(s.filePath(id, name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
size, err := io.Copy(f, r)
|
|
f.Close()
|
|
if err != nil {
|
|
os.Remove(s.filePath(id, name))
|
|
return nil, err
|
|
}
|
|
sh := &Share{
|
|
ID: id,
|
|
Name: name,
|
|
Note: note,
|
|
Session: session,
|
|
Size: size,
|
|
Mime: mime.TypeByExtension(strings.ToLower(filepath.Ext(name))),
|
|
Created: time.Now(),
|
|
}
|
|
meta, _ := json.MarshalIndent(sh, "", " ")
|
|
if err := os.WriteFile(s.metaPath(id), meta, 0o644); err != nil {
|
|
return nil, err
|
|
}
|
|
return sh, nil
|
|
}
|
|
|
|
func (s *ShareStore) Get(id string) (*Share, error) {
|
|
data, err := os.ReadFile(s.metaPath(id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var sh Share
|
|
if err := json.Unmarshal(data, &sh); err != nil {
|
|
return nil, err
|
|
}
|
|
return &sh, nil
|
|
}
|
|
|
|
func (s *ShareStore) List() []*Share {
|
|
matches, _ := filepath.Glob(filepath.Join(s.dir, "*.meta.json"))
|
|
var out []*Share
|
|
for _, m := range matches {
|
|
data, err := os.ReadFile(m)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var sh Share
|
|
if json.Unmarshal(data, &sh) == nil {
|
|
out = append(out, &sh)
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Created.After(out[j].Created) })
|
|
return out
|
|
}
|
|
|
|
func (s *ShareStore) Delete(id string) error {
|
|
sh, err := s.Get(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
os.Remove(s.filePath(sh.ID, sh.Name))
|
|
return os.Remove(s.metaPath(id))
|
|
}
|
|
|
|
// Path returns the on-disk path for serving.
|
|
func (s *ShareStore) Path(sh *Share) string { return s.filePath(sh.ID, sh.Name) }
|