// Package storageagent — внутренний сервис монтирования хранилищ (см. docs/library-and-storage.md). // В M0 — каркас: проверка здоровья, аутентификация межсервисных запросов, список точек монтирования. package storageagent import ( "crypto/subtle" "encoding/json" "log/slog" "net/http" "os" "path/filepath" "strings" "github.com/go-chi/chi/v5" "pipu/api/internal/platform/httpx" ) // Mount — состояние точки монтирования источника. type Mount struct { SourceID string `json:"source_id"` Path string `json:"path"` } // NewRouter возвращает роутер агента. token — общий секрет INTERNAL_TOKEN; // при пустом token все запросы к /internal отклоняются. func NewRouter(log *slog.Logger, token, libraryRoot string) http.Handler { if log == nil { log = slog.New(slog.DiscardHandler) } r := chi.NewRouter() r.NotFound(httpx.NotFound) r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) }) r.Route("/internal", func(r chi.Router) { r.Use(requireToken(token)) r.Get("/mounts", func(w http.ResponseWriter, r *http.Request) { mounts, err := listMounts(libraryRoot) if err != nil { log.ErrorContext(r.Context(), "list mounts", "err", err) httpx.WriteProblem(w, http.StatusInternalServerError, "storage.list_failed", "cannot list mounts") return } writeJSON(w, http.StatusOK, map[string]any{"mounts": mounts}) }) }) return r } func requireToken(token string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { got, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ") if !ok || token == "" || subtle.ConstantTimeCompare([]byte(got), []byte(token)) != 1 { httpx.WriteProblem(w, http.StatusUnauthorized, "auth.invalid_internal_token", "invalid internal token") return } next.ServeHTTP(w, r) }) } } // listMounts перечисляет подкаталоги корня библиотеки (каждый — точка монтирования источника). func listMounts(root string) ([]Mount, error) { entries, err := os.ReadDir(root) if err != nil { if os.IsNotExist(err) { return []Mount{}, nil } return nil, err } mounts := make([]Mount, 0, len(entries)) for _, e := range entries { if e.IsDir() { mounts = append(mounts, Mount{SourceID: e.Name(), Path: filepath.Join(root, e.Name())}) } } return mounts, nil } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) }