mirror of
https://github.com/langgenius/dify.git
synced 2026-08-24 12:32:54 -04:00
174 lines
4.4 KiB
Go
174 lines
4.4 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"hash"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/langgenius/dify/dify-agent-runtime/internal/snapshot"
|
|
)
|
|
|
|
const (
|
|
TrailerSnapshotStatus = "X-Snapshot-Status"
|
|
TrailerSnapshotSha256 = "X-Snapshot-Sha256"
|
|
TrailerSnapshotBytes = "X-Snapshot-Bytes"
|
|
SnapshotStatusOK = "ok"
|
|
|
|
maxSaveRequestBytes = 64 << 10
|
|
)
|
|
|
|
type SaveRequest struct {
|
|
Excludes []string `json:"excludes"`
|
|
}
|
|
|
|
func decodeSaveExcludes(body io.Reader) ([]string, error) {
|
|
var req SaveRequest
|
|
if err := json.NewDecoder(io.LimitReader(body, maxSaveRequestBytes)).Decode(&req); err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return req.Excludes, nil
|
|
}
|
|
|
|
// snapshotHandlers serves the native Home snapshot endpoints.
|
|
//
|
|
// SIZE CONTRACT: neither endpoint imposes a size limit. Consumers MUST bound
|
|
// the streams in their own logic.
|
|
type snapshotHandlers struct {
|
|
config *Config
|
|
gate sync.Mutex
|
|
}
|
|
|
|
func newSnapshotHandlers(config *Config) *snapshotHandlers {
|
|
return &snapshotHandlers{config: config}
|
|
}
|
|
|
|
// resolveHome fails loudly when $HOME is unset or not a directory — never
|
|
// degrade to a guessed or relative path.
|
|
func (h *snapshotHandlers) resolveHome() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
info, err := os.Stat(home)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !info.IsDir() {
|
|
return "", errors.New("home is not a directory")
|
|
}
|
|
return home, nil
|
|
}
|
|
|
|
func (h *snapshotHandlers) handleSnapshotSave() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
excludes, err := decodeSaveExcludes(r.Body)
|
|
if err != nil {
|
|
writeError(w, 400, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
|
|
if !h.gate.TryLock() {
|
|
writeError(w, 409, "snapshot_busy", "another snapshot operation is in progress")
|
|
return
|
|
}
|
|
defer h.gate.Unlock()
|
|
|
|
home, err := h.resolveHome()
|
|
if err != nil {
|
|
writeError(w, 500, "home_unavailable", err.Error())
|
|
return
|
|
}
|
|
rc := http.NewResponseController(w)
|
|
if err := rc.SetWriteDeadline(time.Now().Add(h.config.SnapshotTimeout)); err != nil {
|
|
log.Printf("WARN snapshot save: set write deadline: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), h.config.SnapshotTimeout)
|
|
defer cancel()
|
|
|
|
w.Header().Set("Trailer", TrailerSnapshotStatus+", "+TrailerSnapshotSha256+", "+TrailerSnapshotBytes)
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
|
|
hcw := &hashCountWriter{w: w, h: sha256.New()}
|
|
if err := snapshot.SaveHome(ctx, hcw, home, excludes); err != nil {
|
|
log.Printf("ERROR snapshot save: %v", err)
|
|
if hcw.n == 0 {
|
|
w.Header().Del("Trailer")
|
|
writeError(w, 500, "snapshot_failed", err.Error())
|
|
return
|
|
}
|
|
panic(http.ErrAbortHandler)
|
|
}
|
|
w.Header().Set(TrailerSnapshotStatus, SnapshotStatusOK)
|
|
w.Header().Set(TrailerSnapshotSha256, hex.EncodeToString(hcw.h.Sum(nil)))
|
|
w.Header().Set(TrailerSnapshotBytes, strconv.FormatInt(hcw.n, 10))
|
|
}
|
|
}
|
|
|
|
func (h *snapshotHandlers) handleSnapshotRestore() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !h.gate.TryLock() {
|
|
writeError(w, 409, "snapshot_busy", "another snapshot operation is in progress")
|
|
return
|
|
}
|
|
defer h.gate.Unlock()
|
|
|
|
home, err := h.resolveHome()
|
|
if err != nil {
|
|
writeError(w, 500, "home_unavailable", err.Error())
|
|
return
|
|
}
|
|
|
|
rc := http.NewResponseController(w)
|
|
if err := rc.SetReadDeadline(time.Now().Add(h.config.SnapshotTimeout)); err != nil {
|
|
log.Printf("WARN snapshot restore: set read deadline: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), h.config.SnapshotTimeout)
|
|
defer cancel()
|
|
|
|
result, err := snapshot.RestoreHome(ctx, r.Body, home)
|
|
if err != nil {
|
|
log.Printf("ERROR snapshot restore: %v", err)
|
|
if errors.Is(err, snapshot.ErrMalformed) {
|
|
writeError(w, 400, "archive_malformed", err.Error())
|
|
return
|
|
}
|
|
writeError(w, 500, "restore_failed", err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, RestoreResponse{
|
|
Entries: result.Entries,
|
|
BytesWritten: result.BytesWritten,
|
|
})
|
|
}
|
|
}
|
|
|
|
// hashCountWriter passes bytes through while hashing and counting them, so
|
|
// success trailers can carry the digest without buffering anything.
|
|
type hashCountWriter struct {
|
|
w io.Writer
|
|
h hash.Hash
|
|
n int64
|
|
}
|
|
|
|
func (hc *hashCountWriter) Write(p []byte) (int, error) {
|
|
n, err := hc.w.Write(p)
|
|
hc.n += int64(n)
|
|
_, _ = hc.h.Write(p[:n])
|
|
return n, err
|
|
}
|