mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 09:14:16 +00:00
161 lines
5.1 KiB
Go
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)
|
|
}
|
|
}
|