Compare commits
3 Commits
dfbceb4bcd
...
1144b11ca3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1144b11ca3 | ||
|
|
aad9fa1b4a | ||
|
|
1a84c04314 |
@@ -55,7 +55,7 @@ func main() {
|
|||||||
slog.Warn("ensure bucket failed", "error", err)
|
slog.Warn("ensure bucket failed", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
healthH := handler.NewHealthHandler(pool)
|
healthH := handler.NewHealthHandler(pool, store)
|
||||||
nodeRepo := repository.NewNodeRepository(pool)
|
nodeRepo := repository.NewNodeRepository(pool)
|
||||||
nodeH := handler.NewNodeHandler(cfg, nodeRepo, store)
|
nodeH := handler.NewNodeHandler(cfg, nodeRepo, store)
|
||||||
go runTrashPurger(ctx, nodeRepo, store)
|
go runTrashPurger(ctx, nodeRepo, store)
|
||||||
@@ -67,6 +67,7 @@ func main() {
|
|||||||
|
|
||||||
r.Get("/healthz", healthH.Healthz)
|
r.Get("/healthz", healthH.Healthz)
|
||||||
r.Get("/readyz", healthH.Readyz)
|
r.Get("/readyz", healthH.Readyz)
|
||||||
|
r.Get("/health/detail", healthH.Detail)
|
||||||
|
|
||||||
mountFilesAPI := func(r chi.Router) {
|
mountFilesAPI := func(r chi.Router) {
|
||||||
r.Get("/nodes", nodeH.List)
|
r.Get("/nodes", nodeH.List)
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -3,7 +3,7 @@ module files-service
|
|||||||
go 1.25.7
|
go 1.25.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.estateliga.work/admin/portal-common v0.2.0
|
gitea.estateliga.work/admin/portal-common v0.3.0
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgx/v5 v5.9.1
|
github.com/jackc/pgx/v5 v5.9.1
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,5 +1,5 @@
|
|||||||
gitea.estateliga.work/admin/portal-common v0.2.0 h1:TwSxTDwSWnPJUGuCfjSy1f++MxvDIZ+HCUNMC3EFNcE=
|
gitea.estateliga.work/admin/portal-common v0.3.0 h1:xpr9UeLXk5pCcNXcTVGZzJZr0Ni7An7DV0OkuYv9qVM=
|
||||||
gitea.estateliga.work/admin/portal-common v0.2.0/go.mod h1:C860q6g38KVMsv+mKv6k1Vm7smVRCycl+N6r63TElnk=
|
gitea.estateliga.work/admin/portal-common v0.3.0/go.mod h1:C860q6g38KVMsv+mKv6k1Vm7smVRCycl+N6r63TElnk=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"files-service/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HealthHandler struct {
|
type HealthHandler struct {
|
||||||
pool *pgxpool.Pool
|
pool *pgxpool.Pool
|
||||||
|
store *storage.Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHealthHandler(pool *pgxpool.Pool) *HealthHandler {
|
func NewHealthHandler(pool *pgxpool.Pool, store *storage.Storage) *HealthHandler {
|
||||||
return &HealthHandler{pool: pool}
|
return &HealthHandler{pool: pool, store: store}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HealthHandler) Healthz(w http.ResponseWriter, _ *http.Request) {
|
func (h *HealthHandler) Healthz(w http.ResponseWriter, _ *http.Request) {
|
||||||
@@ -25,3 +32,75 @@ func (h *HealthHandler) Readyz(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ready"})
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ready"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HealthHandler) Detail(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
components := []componentProbe{
|
||||||
|
h.probePostgres(ctx),
|
||||||
|
h.probeStorage(ctx),
|
||||||
|
h.probeTrash(ctx),
|
||||||
|
h.probeStorageMetadata(ctx),
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"components": components})
|
||||||
|
}
|
||||||
|
|
||||||
|
type componentProbe struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
LatencyMs int64 `json:"latency_ms"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthHandler) probePostgres(ctx context.Context) componentProbe {
|
||||||
|
start := time.Now()
|
||||||
|
if err := h.pool.Ping(ctx); err != nil {
|
||||||
|
return componentProbe{Name: "postgres", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
|
||||||
|
}
|
||||||
|
return componentProbe{Name: "postgres", Status: "ok", LatencyMs: time.Since(start).Milliseconds()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthHandler) probeStorage(ctx context.Context) componentProbe {
|
||||||
|
start := time.Now()
|
||||||
|
if err := h.store.Check(ctx); err != nil {
|
||||||
|
return componentProbe{Name: "minio_storage", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
|
||||||
|
}
|
||||||
|
return componentProbe{Name: "minio_storage", Status: "ok", LatencyMs: time.Since(start).Milliseconds()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthHandler) probeTrash(ctx context.Context) componentProbe {
|
||||||
|
start := time.Now()
|
||||||
|
var due int
|
||||||
|
if err := h.pool.QueryRow(ctx, `
|
||||||
|
SELECT COUNT(*)::int
|
||||||
|
FROM files_nodes
|
||||||
|
WHERE purge_after IS NOT NULL
|
||||||
|
AND purge_after <= now()
|
||||||
|
`).Scan(&due); err != nil {
|
||||||
|
return componentProbe{Name: "trash_purger", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
|
||||||
|
}
|
||||||
|
if due > 0 {
|
||||||
|
return componentProbe{Name: "trash_purger", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: "purge_due=" + strconv.Itoa(due)}
|
||||||
|
}
|
||||||
|
return componentProbe{Name: "trash_purger", Status: "ok", LatencyMs: time.Since(start).Milliseconds()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthHandler) probeStorageMetadata(ctx context.Context) componentProbe {
|
||||||
|
start := time.Now()
|
||||||
|
var activeObjects int
|
||||||
|
if err := h.pool.QueryRow(ctx, `
|
||||||
|
SELECT COUNT(*)::int
|
||||||
|
FROM files_nodes
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
AND storage_key IS NOT NULL
|
||||||
|
`).Scan(&activeObjects); err != nil {
|
||||||
|
return componentProbe{Name: "storage_metadata", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
|
||||||
|
}
|
||||||
|
return componentProbe{
|
||||||
|
Name: "storage_metadata",
|
||||||
|
Status: "ok",
|
||||||
|
LatencyMs: time.Since(start).Milliseconds(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||||
@@ -26,19 +25,3 @@ func decodeJSON(r *http.Request, v any) error {
|
|||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
return json.NewDecoder(r.Body).Decode(v)
|
return json.NewDecoder(r.Body).Decode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func csvHeader(r *http.Request, key string) []string {
|
|
||||||
raw := r.Header.Get(key)
|
|
||||||
if raw == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
parts := strings.Split(raw, ",")
|
|
||||||
out := make([]string, 0, len(parts))
|
|
||||||
for _, p := range parts {
|
|
||||||
p = strings.TrimSpace(p)
|
|
||||||
if p != "" {
|
|
||||||
out = append(out, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -763,9 +763,9 @@ func allowedOfficeFormat(format string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func subordinates(r *http.Request) []string {
|
func subordinates(r *http.Request) []string {
|
||||||
ids := csvHeader(r, "X-User-Subordinates")
|
ids := commonmw.HeaderCSV(r, "X-User-Subordinates")
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
ids = csvHeader(r, "X-User-Subordinate-Ids")
|
ids = commonmw.HeaderCSV(r, "X-User-Subordinate-Ids")
|
||||||
}
|
}
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,20 @@ func (s *Storage) EnsureBucket(ctx context.Context) error {
|
|||||||
return s.client.MakeBucket(ctx, s.cfg.Bucket, minio.MakeBucketOptions{})
|
return s.client.MakeBucket(ctx, s.cfg.Bucket, minio.MakeBucketOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Check(ctx context.Context) error {
|
||||||
|
if !s.Configured() {
|
||||||
|
return errors.New("storage not configured")
|
||||||
|
}
|
||||||
|
exists, err := s.client.BucketExists(ctx, s.cfg.Bucket)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("check bucket: %w", err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("bucket %q does not exist", s.cfg.Bucket)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func GenerateKey(ownerID, filename string) string {
|
func GenerateKey(ownerID, filename string) string {
|
||||||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
||||||
if !AllowedExtension(ext) {
|
if !AllowedExtension(ext) {
|
||||||
|
|||||||
Reference in New Issue
Block a user