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 1 # golangci-lint configuration for ATCR 2 2 # See: https://golangci-lint.run/usage/configuration/ 3 3 version: "2" 4 + 5 + issues: 6 + fix: true 7 + 4 8 linters: 5 9 settings: 6 10 staticcheck: ··· 25 29 linters: 26 30 - errcheck 27 31 28 - # TODO: fix issues and remove these paths one by one 29 - 30 32 formatters: 31 33 enable: 32 34 - gofmt 33 35 - goimports 36 + settings: 37 + gofmt: 38 + rewrite-rules: 39 + - pattern: 'interface{}' 40 + replacement: 'any'
+24 -7
cmd/credential-helper/main.go
··· 11 11 "os/exec" 12 12 "path/filepath" 13 13 "runtime" 14 + "strconv" 14 15 "strings" 15 16 "time" 16 17 ) ··· 385 386 } 386 387 387 388 var tokenResult DeviceTokenResponse 388 - json.NewDecoder(tokenResp.Body).Decode(&tokenResult) 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 + } 389 394 tokenResp.Body.Close() 390 395 391 396 if tokenResult.Error == "authorization_pending" { ··· 767 772 // Compare each part 768 773 for i := range min(len(newParts), len(curParts)) { 769 774 newNum := 0 775 + if parsed, err := strconv.Atoi(newParts[i]); err == nil { 776 + newNum = parsed 777 + } 770 778 curNum := 0 771 - fmt.Sscanf(newParts[i], "%d", &newNum) 772 - fmt.Sscanf(curParts[i], "%d", &curNum) 779 + if parsed, err := strconv.Atoi(curParts[i]); err == nil { 780 + curNum = parsed 781 + } 773 782 774 783 if newNum > curNum { 775 784 return true ··· 881 890 // Install new binary 882 891 if err := copyFile(binaryPath, currentPath); err != nil { 883 892 // Try to restore backup 884 - os.Rename(backupPath, currentPath) 893 + if renameErr := os.Rename(backupPath, currentPath); renameErr != nil { 894 + fmt.Fprintf(os.Stderr, "Warning: failed to restore backup: %v\n", renameErr) 895 + } 885 896 return fmt.Errorf("failed to install new binary: %w", err) 886 897 } 887 898 ··· 889 900 if err := os.Chmod(currentPath, 0755); err != nil { 890 901 // Try to restore backup 891 902 os.Remove(currentPath) 892 - os.Rename(backupPath, currentPath) 903 + if renameErr := os.Rename(backupPath, currentPath); renameErr != nil { 904 + fmt.Fprintf(os.Stderr, "Warning: failed to restore backup: %v\n", renameErr) 905 + } 893 906 return fmt.Errorf("failed to set permissions: %w", err) 894 907 } 895 908 ··· 1047 1060 1048 1061 // Ensure directory exists 1049 1062 dir := filepath.Dir(path) 1050 - os.MkdirAll(dir, 0700) 1063 + if err := os.MkdirAll(dir, 0700); err != nil { 1064 + return 1065 + } 1051 1066 1052 - os.WriteFile(path, data, 0600) 1067 + if err := os.WriteFile(path, data, 0600); err != nil { 1068 + return // Cache write failed, non-critical 1069 + } 1053 1070 }
+15 -13
pkg/appview/handlers/manifest_health.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "html/template" 6 + "log/slog" 5 7 "net/http" 6 8 "net/url" 7 9 "time" ··· 12 14 // ManifestHealthHandler handles HTMX polling for manifest health status 13 15 type ManifestHealthHandler struct { 14 16 HealthChecker *holdhealth.Checker 17 + Templates *template.Template 15 18 } 16 19 17 20 func (h *ManifestHealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ··· 61 64 func (h *ManifestHealthHandler) renderBadge(w http.ResponseWriter, endpoint string, reachable, pending bool) { 62 65 w.Header().Set("Content-Type", "text/html") 63 66 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(``)) 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) 77 79 } 78 80 }
+1 -3
pkg/appview/handlers/opengraph.go
··· 211 211 } 212 212 213 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 - } 214 + card.DrawIcon("package", int(layout.TextX), int(textY)-int(ogcard.FontDescription), int(ogcard.FontDescription), ogcard.ColorMuted) 217 215 card.DrawText(repoText, layout.TextX+42, textY, ogcard.FontDescription, ogcard.ColorMuted, ogcard.AlignLeft, false) 218 216 219 217 // ATCR branding (bottom right)
+8 -1
pkg/appview/handlers/settings.go
··· 73 73 // UpdateDefaultHoldHandler handles updating the default hold 74 74 type UpdateDefaultHoldHandler struct { 75 75 Refresher *oauth.Refresher 76 + Templates *template.Template 76 77 } 77 78 78 79 func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ··· 105 106 } 106 107 107 108 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 + 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 + } 109 116 }
+7 -5
pkg/appview/ogcard/card.go
··· 9 9 _ "image/jpeg" // Register JPEG decoder for image.Decode 10 10 "image/png" 11 11 "io" 12 + "log/slog" 12 13 "net/http" 13 14 "time" 14 15 ··· 118 119 draw.Draw(c.img, rect, &image.Uniform{col}, image.Point{}, draw.Over) 119 120 } 120 121 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 { 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) { 123 124 f := regularFont 124 125 if bold { 125 126 f = boldFont 126 127 } 127 128 if f == nil { 128 - return nil // No font loaded 129 + return // No font loaded 129 130 } 130 131 131 132 ctx := freetype.NewContext() ··· 152 153 } 153 154 154 155 pt := freetype.Pt(int(x), int(y)) 155 - _, err := ctx.DrawString(text, pt) 156 - return err 156 + if _, err := ctx.DrawString(text, pt); err != nil { 157 + slog.Warn("Failed to draw text", "text", text, "error", err) 158 + } 157 159 } 158 160 159 161 // MeasureText returns the width of text in pixels
+7 -6
pkg/appview/ogcard/icons.go
··· 6 6 "image" 7 7 "image/color" 8 8 "image/draw" 9 + "log/slog" 9 10 "strings" 10 11 11 12 "github.com/srwiley/oksvg" ··· 28 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"/>`, 29 30 } 30 31 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 { 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) { 33 34 path, ok := iconPaths[name] 34 35 if !ok { 35 - return fmt.Errorf("unknown icon: %s", name) 36 + slog.Warn("Unknown icon", "name", name) 37 + return 36 38 } 37 39 38 40 // Build full SVG with color ··· 45 47 // Parse SVG 46 48 icon, err := oksvg.ReadIconStream(bytes.NewReader([]byte(svg))) 47 49 if err != nil { 48 - return fmt.Errorf("failed to parse icon SVG: %w", err) 50 + slog.Warn("Failed to parse icon SVG", "name", name, "error", err) 51 + return 49 52 } 50 53 51 54 // Create target image for the icon ··· 63 66 // Draw icon onto card 64 67 rect := image.Rect(x, y, x+size, y+size) 65 68 draw.Draw(c.img, rect, iconImg, image.Point{}, draw.Over) 66 - 67 - return nil 68 69 }
+2
pkg/appview/routes/routes.go
··· 146 146 // Manifest health check API endpoint (HTMX polling) 147 147 router.Get("/api/manifest-health", (&uihandlers.ManifestHealthHandler{ 148 148 HealthChecker: deps.HealthChecker, 149 + Templates: deps.Templates, 149 150 }).ServeHTTP) 150 151 151 152 router.Get("/u/{handle}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( ··· 196 197 197 198 r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{ 198 199 Refresher: deps.Refresher, 200 + Templates: deps.Templates, 199 201 }).ServeHTTP) 200 202 201 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 551 "install.html", 552 552 "manifest-modal", 553 553 "push-list.html", 554 + "health-badge", 555 + "alert", 554 556 } 555 557 556 558 for _, name := range expectedTemplates { ··· 680 682 t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput) 681 683 } 682 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 + } 683 774 } 684 775 } 685 776
+1 -1
pkg/hold/admin/auth.go
··· 87 87 } 88 88 89 89 // renderTemplate renders a template with the given data 90 - func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data interface{}) { 90 + func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data any) { 91 91 w.Header().Set("Content-Type", "text/html; charset=utf-8") 92 92 93 93 if err := ui.templates.ExecuteTemplate(w, name, data); err != nil {