Files
at-container-registry/cmd/hold/main.go
T

161 lines
5.1 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"atcr.io/pkg/atproto"
"atcr.io/pkg/hold"
indigooauth "github.com/bluesky-social/indigo/atproto/auth/oauth"
// Import storage drivers
_ "github.com/distribution/distribution/v3/registry/storage/driver/filesystem"
_ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws"
)
func main() {
// Load configuration from environment variables
cfg, err := hold.LoadConfigFromEnv()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Create hold service
service, err := hold.NewHoldService(cfg)
if err != nil {
log.Fatalf("Failed to create hold service: %v", err)
}
// Setup HTTP routes
mux := http.NewServeMux()
mux.HandleFunc("/health", service.HealthHandler)
mux.HandleFunc("/register", service.HandleRegister)
mux.HandleFunc("/presigned-url", service.HandlePresignedURL)
mux.HandleFunc("/move", service.HandleMove)
// Multipart upload endpoints
mux.HandleFunc("/start-multipart", service.HandleStartMultipart)
mux.HandleFunc("/part-presigned-url", service.HandleGetPartURL)
mux.HandleFunc("/complete-multipart", service.HandleCompleteMultipart)
mux.HandleFunc("/abort-multipart", service.HandleAbortMultipart)
// Buffered multipart part upload endpoint (for when presigned URLs are disabled/unavailable)
mux.HandleFunc("/multipart-parts/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse URL: /multipart-parts/{uploadID}/{partNumber}
path := r.URL.Path[len("/multipart-parts/"):]
parts := strings.Split(path, "/")
if len(parts) != 2 {
http.Error(w, "invalid path format, expected /multipart-parts/{uploadID}/{partNumber}", http.StatusBadRequest)
return
}
uploadID := parts[0]
partNumber, err := strconv.Atoi(parts[1])
if err != nil {
http.Error(w, fmt.Sprintf("invalid part number: %v", err), http.StatusBadRequest)
return
}
// Get DID from query param
did := r.URL.Query().Get("did")
service.HandleMultipartPartUpload(w, r, uploadID, partNumber, did, service.MultipartMgr)
})
// Pre-register OAuth callback route (will be populated by auto-registration)
var oauthCallbackHandler http.HandlerFunc
mux.HandleFunc("/auth/oauth/callback", func(w http.ResponseWriter, r *http.Request) {
if oauthCallbackHandler != nil {
oauthCallbackHandler(w, r)
} else {
http.Error(w, "OAuth callback not initialized", http.StatusServiceUnavailable)
}
})
// OAuth client metadata endpoint for ATProto OAuth
// The hold service serves its metadata at /client-metadata.json
// This is referenced by its client ID URL
mux.HandleFunc("/client-metadata.json", func(w http.ResponseWriter, r *http.Request) {
// Create a temporary config to generate metadata (indigo provides this)
redirectURI := cfg.Server.PublicURL + "/auth/oauth/callback"
clientID := cfg.Server.PublicURL + "/client-metadata.json"
// Define scopes needed for hold registration and crew management
// Omit action parameter to allow all actions (create, update, delete)
scopes := []string{
"atproto",
fmt.Sprintf("repo:%s", atproto.HoldCollection),
fmt.Sprintf("repo:%s", atproto.HoldCrewCollection),
fmt.Sprintf("repo:%s", atproto.SailorProfileCollection),
}
config := indigooauth.NewPublicConfig(clientID, redirectURI, scopes)
metadata := config.ClientMetadata()
// Serve as JSON
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
json.NewEncoder(w).Encode(metadata)
})
mux.HandleFunc("/blobs/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet, http.MethodHead:
service.HandleProxyGet(w, r)
case http.MethodPut:
service.HandleProxyPut(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})
// Create server
server := &http.Server{
Addr: cfg.Server.Addr,
Handler: mux,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
}
// Start server in goroutine so we can do auto-registration after it's running
serverErr := make(chan error, 1)
go func() {
log.Printf("Starting hold service on %s", cfg.Server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
serverErr <- err
}
}()
// Give server a moment to start
time.Sleep(100 * time.Millisecond)
// Auto-register if owner DID is set (now that server is running)
if cfg.Registration.OwnerDID != "" {
if err := service.AutoRegister(&oauthCallbackHandler); err != nil {
log.Printf("WARNING: Auto-registration failed: %v", err)
log.Printf("You can register manually later using the /register endpoint")
} else {
log.Printf("Successfully registered hold service in PDS")
}
// Reconcile allow-all crew state
if err := service.ReconcileAllowAllCrew(&oauthCallbackHandler); err != nil {
log.Printf("WARNING: Failed to reconcile allow-all crew state: %v", err)
}
}
// Wait for server error or shutdown
if err := <-serverErr; err != nil {
log.Fatalf("Server failed: %v", err)
}
}