A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

code clean up

+105 -112
+1 -1
cmd/appview/main.go
··· 8 8 _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" 9 9 10 10 // Register our custom middleware 11 - _ "atcr.io/pkg/middleware" 11 + _ "atcr.io/pkg/appview/middleware" 12 12 ) 13 13 14 14 func main() {
+12 -13
cmd/appview/serve.go
··· 19 19 sqlite3 "github.com/mattn/go-sqlite3" 20 20 "github.com/spf13/cobra" 21 21 22 + "atcr.io/pkg/appview/middleware" 22 23 "atcr.io/pkg/auth/oauth" 23 24 "atcr.io/pkg/auth/token" 24 - "atcr.io/pkg/middleware" 25 25 26 26 // UI components 27 27 "atcr.io/pkg/appview" 28 28 "atcr.io/pkg/appview/db" 29 29 uihandlers "atcr.io/pkg/appview/handlers" 30 30 "atcr.io/pkg/appview/jetstream" 31 - appmiddleware "atcr.io/pkg/appview/middleware" 32 31 "github.com/gorilla/mux" 33 32 ) 34 33 ··· 474 473 475 474 // Public routes (with optional auth for navbar) 476 475 // SECURITY: Public pages use read-only DB 477 - router.Handle("/", appmiddleware.OptionalAuth(sessionStore, database)( 476 + router.Handle("/", middleware.OptionalAuth(sessionStore, database)( 478 477 &uihandlers.HomeHandler{ 479 478 DB: readOnlyDB, 480 479 Templates: templates, ··· 482 481 }, 483 482 )).Methods("GET") 484 483 485 - router.Handle("/api/recent-pushes", appmiddleware.OptionalAuth(sessionStore, database)( 484 + router.Handle("/api/recent-pushes", middleware.OptionalAuth(sessionStore, database)( 486 485 &uihandlers.RecentPushesHandler{ 487 486 DB: readOnlyDB, 488 487 Templates: templates, ··· 491 490 )).Methods("GET") 492 491 493 492 // SECURITY: Search uses read-only DB to prevent writes and limit access to sensitive tables 494 - router.Handle("/search", appmiddleware.OptionalAuth(sessionStore, database)( 493 + router.Handle("/search", middleware.OptionalAuth(sessionStore, database)( 495 494 &uihandlers.SearchHandler{ 496 495 DB: readOnlyDB, 497 496 Templates: templates, ··· 499 498 }, 500 499 )).Methods("GET") 501 500 502 - router.Handle("/api/search-results", appmiddleware.OptionalAuth(sessionStore, database)( 501 + router.Handle("/api/search-results", middleware.OptionalAuth(sessionStore, database)( 503 502 &uihandlers.SearchResultsHandler{ 504 503 DB: readOnlyDB, 505 504 Templates: templates, ··· 508 507 )).Methods("GET") 509 508 510 509 // API route for repository stats (public, read-only) 511 - router.Handle("/api/stats/{handle}/{repository}", appmiddleware.OptionalAuth(sessionStore, database)( 510 + router.Handle("/api/stats/{handle}/{repository}", middleware.OptionalAuth(sessionStore, database)( 512 511 &uihandlers.GetStatsHandler{ 513 512 DB: readOnlyDB, 514 513 Directory: oauthApp.Directory(), ··· 516 515 )).Methods("GET") 517 516 518 517 // API routes for stars (require authentication) 519 - router.Handle("/api/stars/{handle}/{repository}", appmiddleware.RequireAuth(sessionStore, database)( 518 + router.Handle("/api/stars/{handle}/{repository}", middleware.RequireAuth(sessionStore, database)( 520 519 &uihandlers.StarRepositoryHandler{ 521 520 DB: database, // Needs write access 522 521 Directory: oauthApp.Directory(), ··· 524 523 }, 525 524 )).Methods("POST") 526 525 527 - router.Handle("/api/stars/{handle}/{repository}", appmiddleware.RequireAuth(sessionStore, database)( 526 + router.Handle("/api/stars/{handle}/{repository}", middleware.RequireAuth(sessionStore, database)( 528 527 &uihandlers.UnstarRepositoryHandler{ 529 528 DB: database, // Needs write access 530 529 Directory: oauthApp.Directory(), ··· 532 531 }, 533 532 )).Methods("DELETE") 534 533 535 - router.Handle("/api/stars/{handle}/{repository}", appmiddleware.OptionalAuth(sessionStore, database)( 534 + router.Handle("/api/stars/{handle}/{repository}", middleware.OptionalAuth(sessionStore, database)( 536 535 &uihandlers.CheckStarHandler{ 537 536 DB: readOnlyDB, // Read-only check 538 537 Directory: oauthApp.Directory(), ··· 540 539 }, 541 540 )).Methods("GET") 542 541 543 - router.Handle("/u/{handle}", appmiddleware.OptionalAuth(sessionStore, database)( 542 + router.Handle("/u/{handle}", middleware.OptionalAuth(sessionStore, database)( 544 543 &uihandlers.UserPageHandler{ 545 544 DB: readOnlyDB, 546 545 Templates: templates, ··· 548 547 }, 549 548 )).Methods("GET") 550 549 551 - router.Handle("/r/{handle}/{repository}", appmiddleware.OptionalAuth(sessionStore, database)( 550 + router.Handle("/r/{handle}/{repository}", middleware.OptionalAuth(sessionStore, database)( 552 551 &uihandlers.RepositoryPageHandler{ 553 552 DB: readOnlyDB, 554 553 Templates: templates, ··· 560 559 561 560 // Authenticated routes 562 561 authRouter := router.NewRoute().Subrouter() 563 - authRouter.Use(appmiddleware.RequireAuth(sessionStore, database)) 562 + authRouter.Use(middleware.RequireAuth(sessionStore, database)) 564 563 565 564 authRouter.Handle("/settings", &uihandlers.SettingsHandler{ 566 565 Templates: templates,
+7
pkg/auth/scope.go
··· 5 5 "strings" 6 6 ) 7 7 8 + // AccessEntry represents access permissions for a resource 9 + type AccessEntry struct { 10 + Type string `json:"type"` // "repository" 11 + Name string `json:"name,omitempty"` // e.g., "alice/myapp" 12 + Actions []string `json:"actions,omitempty"` // e.g., ["pull", "push"] 13 + } 14 + 8 15 // ParseScope parses Docker registry scope strings into AccessEntry structures 9 16 // Scope format: "repository:alice/myapp:pull,push" 10 17 // Multiple scopes can be provided
-8
pkg/auth/types.go
··· 1 - package auth 2 - 3 - // AccessEntry represents access permissions for a resource 4 - type AccessEntry struct { 5 - Type string `json:"type"` // "repository" 6 - Name string `json:"name,omitempty"` // e.g., "alice/myapp" 7 - Actions []string `json:"actions,omitempty"` // e.g., ["pull", "push"] 8 - }
+84
pkg/hold/handlers.go
··· 12 12 "atcr.io/pkg/atproto" 13 13 ) 14 14 15 + // PresignedURLOperation defines the type of presigned URL operation 16 + type PresignedURLOperation string 17 + 18 + const ( 19 + OperationGet PresignedURLOperation = "GET" 20 + OperationHead PresignedURLOperation = "HEAD" 21 + OperationPut PresignedURLOperation = "PUT" 22 + ) 23 + 24 + // PresignedURLRequest represents a request for a presigned URL (GET, HEAD, or PUT) 25 + type PresignedURLRequest struct { 26 + Operation PresignedURLOperation `json:"operation"` 27 + DID string `json:"did"` 28 + Digest string `json:"digest"` 29 + Size int64 `json:"size,omitempty"` // Only required for PUT operations 30 + } 31 + 32 + // PresignedURLResponse contains the presigned URL 33 + type PresignedURLResponse struct { 34 + URL string `json:"url"` 35 + ExpiresAt time.Time `json:"expires_at"` 36 + } 37 + 15 38 // HandlePresignedURL handles presigned URL requests (GET, HEAD, or PUT) 16 39 // Operation type is specified in the request body 17 40 func (s *HoldService) HandlePresignedURL(w http.ResponseWriter, r *http.Request) { ··· 232 255 w.WriteHeader(http.StatusCreated) 233 256 } 234 257 258 + // StartMultipartUploadRequest initiates a multipart upload 259 + type StartMultipartUploadRequest struct { 260 + DID string `json:"did"` 261 + Digest string `json:"digest"` 262 + } 263 + 264 + // StartMultipartUploadResponse contains the multipart upload ID 265 + type StartMultipartUploadResponse struct { 266 + UploadID string `json:"upload_id"` 267 + ExpiresAt time.Time `json:"expires_at"` 268 + } 269 + 235 270 // HandleStartMultipart initiates a multipart upload 236 271 func (s *HoldService) HandleStartMultipart(w http.ResponseWriter, r *http.Request) { 237 272 if r.Method != http.MethodPost { ··· 276 311 json.NewEncoder(w).Encode(resp) 277 312 } 278 313 314 + // GetPartURLRequest requests a presigned URL for a specific part 315 + type GetPartURLRequest struct { 316 + DID string `json:"did"` 317 + Digest string `json:"digest"` 318 + UploadID string `json:"upload_id"` 319 + PartNumber int `json:"part_number"` 320 + } 321 + 322 + // GetPartURLResponse contains the presigned URL for a part 323 + type GetPartURLResponse struct { 324 + URL string `json:"url"` 325 + ExpiresAt time.Time `json:"expires_at"` 326 + } 327 + 279 328 // HandleGetPartURL generates a presigned URL for uploading a specific part 280 329 func (s *HoldService) HandleGetPartURL(w http.ResponseWriter, r *http.Request) { 281 330 if r.Method != http.MethodPost { ··· 323 372 324 373 w.Header().Set("Content-Type", "application/json") 325 374 json.NewEncoder(w).Encode(resp) 375 + } 376 + 377 + // CompleteMultipartRequest completes a multipart upload 378 + type CompleteMultipartRequest struct { 379 + DID string `json:"did"` 380 + Digest string `json:"digest"` 381 + UploadID string `json:"upload_id"` 382 + Parts []CompletedPart `json:"parts"` 383 + } 384 + 385 + // CompletedPart represents an uploaded part with its ETag 386 + type CompletedPart struct { 387 + PartNumber int `json:"part_number"` 388 + ETag string `json:"etag"` 326 389 } 327 390 328 391 // HandleCompleteMultipart completes a multipart upload ··· 381 444 }) 382 445 } 383 446 447 + // AbortMultipartRequest aborts an in-progress upload 448 + type AbortMultipartRequest struct { 449 + DID string `json:"did"` 450 + Digest string `json:"digest"` 451 + UploadID string `json:"upload_id"` 452 + } 453 + 384 454 // HandleAbortMultipart aborts an in-progress multipart upload 385 455 func (s *HoldService) HandleAbortMultipart(w http.ResponseWriter, r *http.Request) { 386 456 if r.Method != http.MethodPost { ··· 425 495 json.NewEncoder(w).Encode(map[string]string{ 426 496 "status": "aborted", 427 497 }) 498 + } 499 + 500 + // RegisterRequest represents a request to register this hold in a user's PDS 501 + type RegisterRequest struct { 502 + DID string `json:"did"` 503 + AccessToken string `json:"access_token"` 504 + PDSEndpoint string `json:"pds_endpoint"` 505 + } 506 + 507 + // RegisterResponse contains the registration result 508 + type RegisterResponse struct { 509 + HoldURI string `json:"hold_uri"` 510 + CrewURI string `json:"crew_uri"` 511 + Message string `json:"message"` 428 512 } 429 513 430 514 // HandleRegister registers this hold service in a user's PDS (manual endpoint)
-89
pkg/hold/types.go
··· 1 - package hold 2 - 3 - import ( 4 - "time" 5 - ) 6 - 7 - // PresignedURLOperation defines the type of presigned URL operation 8 - type PresignedURLOperation string 9 - 10 - const ( 11 - OperationGet PresignedURLOperation = "GET" 12 - OperationHead PresignedURLOperation = "HEAD" 13 - OperationPut PresignedURLOperation = "PUT" 14 - ) 15 - 16 - // PresignedURLRequest represents a request for a presigned URL (GET, HEAD, or PUT) 17 - type PresignedURLRequest struct { 18 - Operation PresignedURLOperation `json:"operation"` 19 - DID string `json:"did"` 20 - Digest string `json:"digest"` 21 - Size int64 `json:"size,omitempty"` // Only required for PUT operations 22 - } 23 - 24 - // PresignedURLResponse contains the presigned URL 25 - type PresignedURLResponse struct { 26 - URL string `json:"url"` 27 - ExpiresAt time.Time `json:"expires_at"` 28 - } 29 - 30 - // StartMultipartUploadRequest initiates a multipart upload 31 - type StartMultipartUploadRequest struct { 32 - DID string `json:"did"` 33 - Digest string `json:"digest"` 34 - } 35 - 36 - // StartMultipartUploadResponse contains the multipart upload ID 37 - type StartMultipartUploadResponse struct { 38 - UploadID string `json:"upload_id"` 39 - ExpiresAt time.Time `json:"expires_at"` 40 - } 41 - 42 - // GetPartURLRequest requests a presigned URL for a specific part 43 - type GetPartURLRequest struct { 44 - DID string `json:"did"` 45 - Digest string `json:"digest"` 46 - UploadID string `json:"upload_id"` 47 - PartNumber int `json:"part_number"` 48 - } 49 - 50 - // GetPartURLResponse contains the presigned URL for a part 51 - type GetPartURLResponse struct { 52 - URL string `json:"url"` 53 - ExpiresAt time.Time `json:"expires_at"` 54 - } 55 - 56 - // CompleteMultipartRequest completes a multipart upload 57 - type CompleteMultipartRequest struct { 58 - DID string `json:"did"` 59 - Digest string `json:"digest"` 60 - UploadID string `json:"upload_id"` 61 - Parts []CompletedPart `json:"parts"` 62 - } 63 - 64 - // CompletedPart represents an uploaded part with its ETag 65 - type CompletedPart struct { 66 - PartNumber int `json:"part_number"` 67 - ETag string `json:"etag"` 68 - } 69 - 70 - // AbortMultipartRequest aborts an in-progress upload 71 - type AbortMultipartRequest struct { 72 - DID string `json:"did"` 73 - Digest string `json:"digest"` 74 - UploadID string `json:"upload_id"` 75 - } 76 - 77 - // RegisterRequest represents a request to register this hold in a user's PDS 78 - type RegisterRequest struct { 79 - DID string `json:"did"` 80 - AccessToken string `json:"access_token"` 81 - PDSEndpoint string `json:"pds_endpoint"` 82 - } 83 - 84 - // RegisterResponse contains the registration result 85 - type RegisterResponse struct { 86 - HoldURI string `json:"hold_uri"` 87 - CrewURI string `json:"crew_uri"` 88 - Message string `json:"message"` 89 - }
+1 -1
pkg/middleware/registry.go pkg/appview/middleware/registry.go
··· 14 14 "github.com/distribution/distribution/v3/registry/storage/driver" 15 15 "github.com/distribution/reference" 16 16 17 + "atcr.io/pkg/appview/storage" 17 18 "atcr.io/pkg/atproto" 18 19 "atcr.io/pkg/auth" 19 20 "atcr.io/pkg/auth/oauth" 20 - "atcr.io/pkg/storage" 21 21 ) 22 22 23 23 // Global refresher instance (set by main.go)
pkg/storage/hold_cache.go pkg/appview/storage/hold_cache.go
pkg/storage/proxy_blob_store.go pkg/appview/storage/proxy_blob_store.go
pkg/storage/routing_repository.go pkg/appview/storage/routing_repository.go