mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-31 13:17:09 +00:00
152 lines
4.4 KiB
Go
152 lines
4.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"atcr.io/pkg/appview/middleware"
|
|
"atcr.io/pkg/appview/storage"
|
|
"atcr.io/pkg/atproto"
|
|
)
|
|
|
|
// StorageHandler handles the storage quota API endpoint
|
|
// Returns an HTML partial for HTMX to swap into the settings page
|
|
type StorageHandler struct {
|
|
BaseUIHandler
|
|
}
|
|
|
|
// QuotaStats mirrors the hold service response
|
|
type QuotaStats struct {
|
|
UserDID string `json:"userDid"`
|
|
UniqueBlobs int `json:"uniqueBlobs"`
|
|
TotalSize int64 `json:"totalSize"`
|
|
Limit *int64 `json:"limit,omitempty"` // nil = unlimited
|
|
Tier string `json:"tier,omitempty"` // e.g., "deckhand", "bosun", "owner"
|
|
}
|
|
|
|
func (h *StorageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
user := middleware.GetUser(r)
|
|
if user == nil {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Use hold_did query param if provided (for previewing other holds),
|
|
// otherwise fall back to the user's saved default hold from their profile.
|
|
holdDID := r.URL.Query().Get("hold_did")
|
|
if holdDID == "" {
|
|
client := atproto.NewClientWithSessionProvider(user.PDSEndpoint, user.DID, h.Refresher)
|
|
profile, err := storage.GetProfile(r.Context(), client)
|
|
if err != nil {
|
|
slog.Warn("Failed to get profile for storage quota", "did", user.DID, "error", err)
|
|
h.renderError(w, "Failed to load profile")
|
|
return
|
|
}
|
|
if profile == nil || profile.DefaultHold == "" {
|
|
h.renderNoHold(w)
|
|
return
|
|
}
|
|
holdDID = profile.DefaultHold
|
|
}
|
|
|
|
// Resolve hold URL from DID
|
|
holdURL := atproto.ResolveHoldURL(holdDID)
|
|
if holdURL == "" {
|
|
slog.Warn("Failed to resolve hold URL", "did", user.DID, "holdDid", holdDID)
|
|
h.renderError(w, "Failed to resolve hold service")
|
|
return
|
|
}
|
|
|
|
// Call the hold's quota endpoint
|
|
quotaURL := fmt.Sprintf("%s%s?userDid=%s", holdURL, atproto.HoldGetQuota, user.DID)
|
|
resp, err := http.Get(quotaURL)
|
|
if err != nil {
|
|
slog.Warn("Failed to fetch quota from hold", "did", user.DID, "holdURL", holdURL, "error", err)
|
|
h.renderError(w, "Failed to connect to hold service")
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
slog.Warn("Hold returned error for quota", "did", user.DID, "status", resp.StatusCode)
|
|
h.renderError(w, "Hold service returned an error")
|
|
return
|
|
}
|
|
|
|
var stats QuotaStats
|
|
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
|
|
slog.Warn("Failed to decode quota response", "did", user.DID, "error", err)
|
|
h.renderError(w, "Failed to parse quota data")
|
|
return
|
|
}
|
|
|
|
// Render the stats partial
|
|
h.renderStats(w, stats)
|
|
}
|
|
|
|
func (h *StorageHandler) renderStats(w http.ResponseWriter, stats QuotaStats) {
|
|
// Calculate usage percentage if limit exists
|
|
var usagePercent int
|
|
var hasLimit bool
|
|
var humanLimit string
|
|
|
|
if stats.Limit != nil && *stats.Limit > 0 {
|
|
hasLimit = true
|
|
humanLimit = humanizeBytes(*stats.Limit)
|
|
usagePercent = int(float64(stats.TotalSize) / float64(*stats.Limit) * 100)
|
|
if usagePercent > 100 {
|
|
usagePercent = 100
|
|
}
|
|
}
|
|
|
|
data := struct {
|
|
UniqueBlobs int
|
|
TotalSize int64
|
|
HumanSize string
|
|
HasLimit bool
|
|
HumanLimit string
|
|
UsagePercent int
|
|
Tier string
|
|
}{
|
|
UniqueBlobs: stats.UniqueBlobs,
|
|
TotalSize: stats.TotalSize,
|
|
HumanSize: humanizeBytes(stats.TotalSize),
|
|
HasLimit: hasLimit,
|
|
HumanLimit: humanLimit,
|
|
UsagePercent: usagePercent,
|
|
Tier: stats.Tier,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
if err := h.Templates.ExecuteTemplate(w, "storage_stats", data); err != nil {
|
|
slog.Error("Failed to render storage stats template", "error", err)
|
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *StorageHandler) renderError(w http.ResponseWriter, message string) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
fmt.Fprintf(w, `<div class="storage-error"><i data-lucide="alert-circle"></i> %s</div>`, message)
|
|
}
|
|
|
|
func (h *StorageHandler) renderNoHold(w http.ResponseWriter) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
fmt.Fprint(w, `<div class="storage-info"><i data-lucide="info"></i> No hold configured. Set a default hold above to see storage usage.</div>`)
|
|
}
|
|
|
|
// humanizeBytes converts bytes to human-readable format
|
|
func humanizeBytes(bytes int64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|