A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go

more lint fixes. enable autofix

evan.jarrett.net d4b88b51 56dd5222

verified
Changed files
+178 -38
cmd
credential-helper
pkg
+9 -2
.golangci.yml
··· 1 # golangci-lint configuration for ATCR 2 # See: https://golangci-lint.run/usage/configuration/ 3 version: "2" 4 linters: 5 settings: 6 staticcheck: ··· 25 linters: 26 - errcheck 27 28 - # TODO: fix issues and remove these paths one by one 29 - 30 formatters: 31 enable: 32 - gofmt 33 - goimports
··· 1 # golangci-lint configuration for ATCR 2 # See: https://golangci-lint.run/usage/configuration/ 3 version: "2" 4 + 5 + issues: 6 + fix: true 7 + 8 linters: 9 settings: 10 staticcheck: ··· 29 linters: 30 - errcheck 31 32 formatters: 33 enable: 34 - gofmt 35 - goimports 36 + settings: 37 + gofmt: 38 + rewrite-rules: 39 + - pattern: 'interface{}' 40 + replacement: 'any'
+24 -7
cmd/credential-helper/main.go
··· 11 "os/exec" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "time" 16 ) ··· 385 } 386 387 var tokenResult DeviceTokenResponse 388 - json.NewDecoder(tokenResp.Body).Decode(&tokenResult) 389 tokenResp.Body.Close() 390 391 if tokenResult.Error == "authorization_pending" { ··· 767 // Compare each part 768 for i := range min(len(newParts), len(curParts)) { 769 newNum := 0 770 curNum := 0 771 - fmt.Sscanf(newParts[i], "%d", &newNum) 772 - fmt.Sscanf(curParts[i], "%d", &curNum) 773 774 if newNum > curNum { 775 return true ··· 881 // Install new binary 882 if err := copyFile(binaryPath, currentPath); err != nil { 883 // Try to restore backup 884 - os.Rename(backupPath, currentPath) 885 return fmt.Errorf("failed to install new binary: %w", err) 886 } 887 ··· 889 if err := os.Chmod(currentPath, 0755); err != nil { 890 // Try to restore backup 891 os.Remove(currentPath) 892 - os.Rename(backupPath, currentPath) 893 return fmt.Errorf("failed to set permissions: %w", err) 894 } 895 ··· 1047 1048 // Ensure directory exists 1049 dir := filepath.Dir(path) 1050 - os.MkdirAll(dir, 0700) 1051 1052 - os.WriteFile(path, data, 0600) 1053 }
··· 11 "os/exec" 12 "path/filepath" 13 "runtime" 14 + "strconv" 15 "strings" 16 "time" 17 ) ··· 386 } 387 388 var tokenResult DeviceTokenResponse 389 + if err := json.NewDecoder(tokenResp.Body).Decode(&tokenResult); err != nil { 390 + fmt.Fprintf(os.Stderr, "\nFailed to decode response: %v\n", err) 391 + tokenResp.Body.Close() 392 + continue 393 + } 394 tokenResp.Body.Close() 395 396 if tokenResult.Error == "authorization_pending" { ··· 772 // Compare each part 773 for i := range min(len(newParts), len(curParts)) { 774 newNum := 0 775 + if parsed, err := strconv.Atoi(newParts[i]); err == nil { 776 + newNum = parsed 777 + } 778 curNum := 0 779 + if parsed, err := strconv.Atoi(curParts[i]); err == nil { 780 + curNum = parsed 781 + } 782 783 if newNum > curNum { 784 return true ··· 890 // Install new binary 891 if err := copyFile(binaryPath, currentPath); err != nil { 892 // Try to restore backup 893 + if renameErr := os.Rename(backupPath, currentPath); renameErr != nil { 894 + fmt.Fprintf(os.Stderr, "Warning: failed to restore backup: %v\n", renameErr) 895 + } 896 return fmt.Errorf("failed to install new binary: %w", err) 897 } 898 ··· 900 if err := os.Chmod(currentPath, 0755); err != nil { 901 // Try to restore backup 902 os.Remove(currentPath) 903 + if renameErr := os.Rename(backupPath, currentPath); renameErr != nil { 904 + fmt.Fprintf(os.Stderr, "Warning: failed to restore backup: %v\n", renameErr) 905 + } 906 return fmt.Errorf("failed to set permissions: %w", err) 907 } 908 ··· 1060 1061 // Ensure directory exists 1062 dir := filepath.Dir(path) 1063 + if err := os.MkdirAll(dir, 0700); err != nil { 1064 + return 1065 + } 1066 1067 + if err := os.WriteFile(path, data, 0600); err != nil { 1068 + return // Cache write failed, non-critical 1069 + } 1070 }
+15 -13
pkg/appview/handlers/manifest_health.go
··· 2 3 import ( 4 "context" 5 "net/http" 6 "net/url" 7 "time" ··· 12 // ManifestHealthHandler handles HTMX polling for manifest health status 13 type ManifestHealthHandler struct { 14 HealthChecker *holdhealth.Checker 15 } 16 17 func (h *ManifestHealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ··· 61 func (h *ManifestHealthHandler) renderBadge(w http.ResponseWriter, endpoint string, reachable, pending bool) { 62 w.Header().Set("Content-Type", "text/html") 63 64 - if pending { 65 - // Still checking - render badge with HTMX retry after 3 seconds 66 - retryURL := "/api/manifest-health?endpoint=" + url.QueryEscape(endpoint) 67 - w.Write([]byte(`<span class="checking-badge" 68 - hx-get="` + retryURL + `" 69 - hx-trigger="load delay:3s" 70 - hx-swap="outerHTML"><i data-lucide="refresh-ccw"></i> Checking...</span>`)) 71 - } else if !reachable { 72 - // Unreachable - render offline badge 73 - w.Write([]byte(`<span class="offline-badge"><i data-lucide="triangle-alert"></i> Offline</span>`)) 74 - } else { 75 - // Reachable - no badge (empty response) 76 - w.Write([]byte(``)) 77 } 78 }
··· 2 3 import ( 4 "context" 5 + "html/template" 6 + "log/slog" 7 "net/http" 8 "net/url" 9 "time" ··· 14 // ManifestHealthHandler handles HTMX polling for manifest health status 15 type ManifestHealthHandler struct { 16 HealthChecker *holdhealth.Checker 17 + Templates *template.Template 18 } 19 20 func (h *ManifestHealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ··· 64 func (h *ManifestHealthHandler) renderBadge(w http.ResponseWriter, endpoint string, reachable, pending bool) { 65 w.Header().Set("Content-Type", "text/html") 66 67 + data := struct { 68 + Pending bool 69 + Reachable bool 70 + RetryURL string 71 + }{ 72 + Pending: pending, 73 + Reachable: reachable, 74 + RetryURL: url.QueryEscape(endpoint), 75 + } 76 + 77 + if err := h.Templates.ExecuteTemplate(w, "health-badge", data); err != nil { 78 + slog.Warn("Failed to render health badge", "error", err) 79 } 80 }
+1 -3
pkg/appview/handlers/opengraph.go
··· 211 } 212 213 // Draw package icon with description-sized text 214 - if err := card.DrawIcon("package", int(layout.TextX), int(textY)-int(ogcard.FontDescription), int(ogcard.FontDescription), ogcard.ColorMuted); err != nil { 215 - slog.Warn("Failed to draw package icon", "error", err) 216 - } 217 card.DrawText(repoText, layout.TextX+42, textY, ogcard.FontDescription, ogcard.ColorMuted, ogcard.AlignLeft, false) 218 219 // ATCR branding (bottom right)
··· 211 } 212 213 // Draw package icon with description-sized text 214 + card.DrawIcon("package", int(layout.TextX), int(textY)-int(ogcard.FontDescription), int(ogcard.FontDescription), ogcard.ColorMuted) 215 card.DrawText(repoText, layout.TextX+42, textY, ogcard.FontDescription, ogcard.ColorMuted, ogcard.AlignLeft, false) 216 217 // ATCR branding (bottom right)
+8 -1
pkg/appview/handlers/settings.go
··· 73 // UpdateDefaultHoldHandler handles updating the default hold 74 type UpdateDefaultHoldHandler struct { 75 Refresher *oauth.Refresher 76 } 77 78 func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ··· 105 } 106 107 w.Header().Set("Content-Type", "text/html") 108 - w.Write([]byte(`<div class="success"><i data-lucide="check"></i> Default hold updated successfully!</div>`)) 109 }
··· 73 // UpdateDefaultHoldHandler handles updating the default hold 74 type UpdateDefaultHoldHandler struct { 75 Refresher *oauth.Refresher 76 + Templates *template.Template 77 } 78 79 func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ··· 106 } 107 108 w.Header().Set("Content-Type", "text/html") 109 + if err := h.Templates.ExecuteTemplate(w, "alert", map[string]string{ 110 + "Class": "success", 111 + "Icon": "check", 112 + "Message": "Default hold updated successfully!", 113 + }); err != nil { 114 + slog.Warn("Failed to render alert", "error", err) 115 + } 116 }
+7 -5
pkg/appview/ogcard/card.go
··· 9 _ "image/jpeg" // Register JPEG decoder for image.Decode 10 "image/png" 11 "io" 12 "net/http" 13 "time" 14 ··· 118 draw.Draw(c.img, rect, &image.Uniform{col}, image.Point{}, draw.Over) 119 } 120 121 - // DrawText draws text at the specified position 122 - func (c *Card) DrawText(text string, x, y float64, size float64, col color.Color, align int, bold bool) error { 123 f := regularFont 124 if bold { 125 f = boldFont 126 } 127 if f == nil { 128 - return nil // No font loaded 129 } 130 131 ctx := freetype.NewContext() ··· 152 } 153 154 pt := freetype.Pt(int(x), int(y)) 155 - _, err := ctx.DrawString(text, pt) 156 - return err 157 } 158 159 // MeasureText returns the width of text in pixels
··· 9 _ "image/jpeg" // Register JPEG decoder for image.Decode 10 "image/png" 11 "io" 12 + "log/slog" 13 "net/http" 14 "time" 15 ··· 119 draw.Draw(c.img, rect, &image.Uniform{col}, image.Point{}, draw.Over) 120 } 121 122 + // DrawText draws text at the specified position. 123 + func (c *Card) DrawText(text string, x, y float64, size float64, col color.Color, align int, bold bool) { 124 f := regularFont 125 if bold { 126 f = boldFont 127 } 128 if f == nil { 129 + return // No font loaded 130 } 131 132 ctx := freetype.NewContext() ··· 153 } 154 155 pt := freetype.Pt(int(x), int(y)) 156 + if _, err := ctx.DrawString(text, pt); err != nil { 157 + slog.Warn("Failed to draw text", "text", text, "error", err) 158 + } 159 } 160 161 // MeasureText returns the width of text in pixels
+7 -6
pkg/appview/ogcard/icons.go
··· 6 "image" 7 "image/color" 8 "image/draw" 9 "strings" 10 11 "github.com/srwiley/oksvg" ··· 28 "package": `<path d="M16.5 9.4l-9-5.19M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.27 6.96L12 12.01l8.73-5.05M12 22.08V12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`, 29 } 30 31 - // DrawIcon draws a Lucide icon at the specified position with the given size and color 32 - func (c *Card) DrawIcon(name string, x, y, size int, col color.Color) error { 33 path, ok := iconPaths[name] 34 if !ok { 35 - return fmt.Errorf("unknown icon: %s", name) 36 } 37 38 // Build full SVG with color ··· 45 // Parse SVG 46 icon, err := oksvg.ReadIconStream(bytes.NewReader([]byte(svg))) 47 if err != nil { 48 - return fmt.Errorf("failed to parse icon SVG: %w", err) 49 } 50 51 // Create target image for the icon ··· 63 // Draw icon onto card 64 rect := image.Rect(x, y, x+size, y+size) 65 draw.Draw(c.img, rect, iconImg, image.Point{}, draw.Over) 66 - 67 - return nil 68 }
··· 6 "image" 7 "image/color" 8 "image/draw" 9 + "log/slog" 10 "strings" 11 12 "github.com/srwiley/oksvg" ··· 29 "package": `<path d="M16.5 9.4l-9-5.19M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.27 6.96L12 12.01l8.73-5.05M12 22.08V12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`, 30 } 31 32 + // DrawIcon draws a Lucide icon at the specified position with the given size and color. 33 + func (c *Card) DrawIcon(name string, x, y, size int, col color.Color) { 34 path, ok := iconPaths[name] 35 if !ok { 36 + slog.Warn("Unknown icon", "name", name) 37 + return 38 } 39 40 // Build full SVG with color ··· 47 // Parse SVG 48 icon, err := oksvg.ReadIconStream(bytes.NewReader([]byte(svg))) 49 if err != nil { 50 + slog.Warn("Failed to parse icon SVG", "name", name, "error", err) 51 + return 52 } 53 54 // Create target image for the icon ··· 66 // Draw icon onto card 67 rect := image.Rect(x, y, x+size, y+size) 68 draw.Draw(c.img, rect, iconImg, image.Point{}, draw.Over) 69 }
+2
pkg/appview/routes/routes.go
··· 146 // Manifest health check API endpoint (HTMX polling) 147 router.Get("/api/manifest-health", (&uihandlers.ManifestHealthHandler{ 148 HealthChecker: deps.HealthChecker, 149 }).ServeHTTP) 150 151 router.Get("/u/{handle}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( ··· 196 197 r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{ 198 Refresher: deps.Refresher, 199 }).ServeHTTP) 200 201 r.Delete("/api/images/{repository}/tags/{tag}", (&uihandlers.DeleteTagHandler{
··· 146 // Manifest health check API endpoint (HTMX polling) 147 router.Get("/api/manifest-health", (&uihandlers.ManifestHealthHandler{ 148 HealthChecker: deps.HealthChecker, 149 + Templates: deps.Templates, 150 }).ServeHTTP) 151 152 router.Get("/u/{handle}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( ··· 197 198 r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{ 199 Refresher: deps.Refresher, 200 + Templates: deps.Templates, 201 }).ServeHTTP) 202 203 r.Delete("/api/images/{repository}/tags/{tag}", (&uihandlers.DeleteTagHandler{
+3
pkg/appview/templates/partials/alert.html
···
··· 1 + {{ define "alert" }} 2 + <div class="{{ .Class }}"><i data-lucide="{{ .Icon }}"></i> {{ .Message }}</div> 3 + {{ end }}
+10
pkg/appview/templates/partials/health-badge.html
···
··· 1 + {{ define "health-badge" }} 2 + {{ if .Pending }} 3 + <span class="checking-badge" 4 + hx-get="/api/manifest-health?endpoint={{ .RetryURL }}" 5 + hx-trigger="load delay:3s" 6 + hx-swap="outerHTML"><i data-lucide="refresh-ccw"></i> Checking...</span> 7 + {{ else if not .Reachable }} 8 + <span class="offline-badge"><i data-lucide="triangle-alert"></i> Offline</span> 9 + {{ end }} 10 + {{ end }}
+91
pkg/appview/ui_test.go
··· 551 "install.html", 552 "manifest-modal", 553 "push-list.html", 554 } 555 556 for _, name := range expectedTemplates { ··· 680 t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput) 681 } 682 }) 683 } 684 } 685
··· 551 "install.html", 552 "manifest-modal", 553 "push-list.html", 554 + "health-badge", 555 + "alert", 556 } 557 558 for _, name := range expectedTemplates { ··· 682 t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput) 683 } 684 }) 685 + } 686 + } 687 + 688 + func TestTemplateExecution_HealthBadge(t *testing.T) { 689 + tmpl, err := Templates() 690 + if err != nil { 691 + t.Fatalf("Templates() error = %v", err) 692 + } 693 + 694 + tests := []struct { 695 + name string 696 + data map[string]any 697 + expectInOutput string 698 + expectMissing string 699 + }{ 700 + { 701 + name: "pending state", 702 + data: map[string]any{ 703 + "Pending": true, 704 + "Reachable": false, 705 + "RetryURL": "http%3A%2F%2Fexample.com", 706 + }, 707 + expectInOutput: "checking-badge", 708 + expectMissing: "offline-badge", 709 + }, 710 + { 711 + name: "offline state", 712 + data: map[string]any{ 713 + "Pending": false, 714 + "Reachable": false, 715 + "RetryURL": "", 716 + }, 717 + expectInOutput: "offline-badge", 718 + expectMissing: "checking-badge", 719 + }, 720 + { 721 + name: "online state - empty output", 722 + data: map[string]any{ 723 + "Pending": false, 724 + "Reachable": true, 725 + "RetryURL": "", 726 + }, 727 + expectMissing: "badge", 728 + }, 729 + } 730 + 731 + for _, tt := range tests { 732 + t.Run(tt.name, func(t *testing.T) { 733 + buf := new(bytes.Buffer) 734 + err := tmpl.ExecuteTemplate(buf, "health-badge", tt.data) 735 + if err != nil { 736 + t.Fatalf("Failed to execute template: %v", err) 737 + } 738 + 739 + output := buf.String() 740 + if tt.expectInOutput != "" && !strings.Contains(output, tt.expectInOutput) { 741 + t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput) 742 + } 743 + if tt.expectMissing != "" && strings.Contains(output, tt.expectMissing) { 744 + t.Errorf("Template output %q should not contain %q", output, tt.expectMissing) 745 + } 746 + }) 747 + } 748 + } 749 + 750 + func TestTemplateExecution_Alert(t *testing.T) { 751 + tmpl, err := Templates() 752 + if err != nil { 753 + t.Fatalf("Templates() error = %v", err) 754 + } 755 + 756 + data := map[string]string{ 757 + "Class": "success", 758 + "Icon": "check", 759 + "Message": "Operation completed!", 760 + } 761 + 762 + buf := new(bytes.Buffer) 763 + err = tmpl.ExecuteTemplate(buf, "alert", data) 764 + if err != nil { 765 + t.Fatalf("Failed to execute template: %v", err) 766 + } 767 + 768 + output := buf.String() 769 + expectedParts := []string{"success", "check", "Operation completed!"} 770 + for _, expected := range expectedParts { 771 + if !strings.Contains(output, expected) { 772 + t.Errorf("Template output %q does not contain expected %q", output, expected) 773 + } 774 } 775 } 776
+1 -1
pkg/hold/admin/auth.go
··· 87 } 88 89 // renderTemplate renders a template with the given data 90 - func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data interface{}) { 91 w.Header().Set("Content-Type", "text/html; charset=utf-8") 92 93 if err := ui.templates.ExecuteTemplate(w, name, data); err != nil {
··· 87 } 88 89 // renderTemplate renders a template with the given data 90 + func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data any) { 91 w.Header().Set("Content-Type", "text/html; charset=utf-8") 92 93 if err := ui.templates.ExecuteTemplate(w, name, data); err != nil {