A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
at refactor 143 lines 3.4 kB view raw
1package appview 2 3import ( 4 "embed" 5 "fmt" 6 "html/template" 7 "io/fs" 8 "net/http" 9 "strings" 10 "time" 11 12 "atcr.io/pkg/appview/licenses" 13) 14 15//go:generate curl -fsSL -o static/js/htmx.min.js https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js 16//go:generate curl -fsSL -o static/js/lucide.min.js https://unpkg.com/lucide@latest/dist/umd/lucide.min.js 17 18//go:embed templates/**/*.html 19var templatesFS embed.FS 20 21//go:embed static 22var staticFS embed.FS 23 24// Templates returns parsed templates with helper functions 25func Templates() (*template.Template, error) { 26 funcMap := template.FuncMap{ 27 "timeAgo": func(t time.Time) string { 28 duration := time.Since(t) 29 30 if duration < time.Minute { 31 return "just now" 32 } else if duration < time.Hour { 33 mins := int(duration.Minutes()) 34 if mins == 1 { 35 return "1 minute ago" 36 } 37 return fmt.Sprintf("%d minutes ago", mins) 38 } else if duration < 24*time.Hour { 39 hours := int(duration.Hours()) 40 if hours == 1 { 41 return "1 hour ago" 42 } 43 return fmt.Sprintf("%d hours ago", hours) 44 } else { 45 days := int(duration.Hours() / 24) 46 if days == 1 { 47 return "1 day ago" 48 } 49 return fmt.Sprintf("%d days ago", days) 50 } 51 }, 52 53 "humanizeBytes": func(bytes int64) string { 54 const unit = 1024 55 if bytes < unit { 56 return fmt.Sprintf("%d B", bytes) 57 } 58 div, exp := int64(unit), 0 59 for n := bytes / unit; n >= unit; n /= unit { 60 div *= unit 61 exp++ 62 } 63 return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) 64 }, 65 66 "truncateDigest": func(digest string, length int) string { 67 if len(digest) <= length { 68 return digest 69 } 70 return digest[:length] + "..." 71 }, 72 73 "firstChar": func(s string) string { 74 if len(s) == 0 { 75 return "?" 76 } 77 return string([]rune(s)[0]) 78 }, 79 80 "trimPrefix": func(prefix, s string) string { 81 if len(s) >= len(prefix) && s[:len(prefix)] == prefix { 82 return s[len(prefix):] 83 } 84 return s 85 }, 86 87 "sanitizeID": func(s string) string { 88 // Replace special CSS selector characters with dashes 89 // e.g., "sha256:abc123" becomes "sha256-abc123" 90 // e.g., "v0.0.2" becomes "v0-0-2" 91 s = strings.ReplaceAll(s, ":", "-") 92 s = strings.ReplaceAll(s, ".", "-") 93 return s 94 }, 95 96 "parseLicenses": func(licensesStr string) []licenses.LicenseInfo { 97 return licenses.ParseLicenses(licensesStr) 98 }, 99 } 100 101 tmpl := template.New("").Funcs(funcMap) 102 tmpl, err := tmpl.ParseFS(templatesFS, "templates/**/*.html") 103 if err != nil { 104 return nil, err 105 } 106 107 return tmpl, nil 108} 109 110// StaticHandler returns HTTP handler for static files 111func StaticHandler() http.Handler { 112 sub, err := fs.Sub(staticFS, "static") 113 if err != nil { 114 panic(err) 115 } 116 return http.FileServer(http.FS(sub)) 117} 118 119// StaticRootFiles returns list of root-level files in static directory (not subdirectories) 120func StaticRootFiles() ([]string, error) { 121 entries, err := staticFS.ReadDir("static") 122 if err != nil { 123 return nil, err 124 } 125 126 var files []string 127 for _, entry := range entries { 128 // Only include files, not directories 129 if !entry.IsDir() { 130 files = append(files, entry.Name()) 131 } 132 } 133 return files, nil 134} 135 136// StaticSubdir returns an fs.FS for a subdirectory within static/ 137func StaticSubdir(name string) http.Handler { 138 sub, err := fs.Sub(staticFS, "static/"+name) 139 if err != nil { 140 panic(err) 141 } 142 return http.FileServer(http.FS(sub)) 143}