This commit is contained in:
lubiana 2025-07-25 21:15:45 +02:00
parent 84ef53ae05
commit 21471c69ba
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk
6 changed files with 179 additions and 29 deletions

View file

@ -30,7 +30,7 @@ COPY . .
COPY --from=frontend-builder /app/dist /app/frontend/dist COPY --from=frontend-builder /app/dist /app/frontend/dist
# Build the binary with embedded static files # Build the binary with embedded static files
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o gocheck . RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o gocheck .
# Stage 2: Create minimal runtime container # Stage 2: Create minimal runtime container
FROM alpine:latest FROM alpine:latest

View file

@ -27,7 +27,8 @@
"globals": "^16.3.0", "globals": "^16.3.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.35.1", "typescript-eslint": "^8.35.1",
"vite": "^7.0.4" "vite": "^7.0.4",
"vite-plugin-compression2": "^2.2.0"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@ -824,6 +825,42 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/pluginutils": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz",
"integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.45.1", "version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz",
@ -2362,6 +2399,13 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/esutils": { "node_modules/esutils": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@ -3544,6 +3588,13 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/tar-mini": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/tar-mini/-/tar-mini-0.2.0.tgz",
"integrity": "sha512-+qfUHz700DWnRutdUsxRRVZ38G1Qr27OetwaMYTdg8hcPxf46U0S1Zf76dQMWRBmusOt2ZCK5kbIaiLkoGO7WQ==",
"dev": true,
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.14", "version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@ -3747,6 +3798,17 @@
} }
} }
}, },
"node_modules/vite-plugin-compression2": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/vite-plugin-compression2/-/vite-plugin-compression2-2.2.0.tgz",
"integrity": "sha512-7BZlU2mBHbqoBGy0ARkn3tv/7LC/2h8ewVDpG/cyH8iSzLw6E/yH6P4oBOEvchkQxNpl+B5W6rFR5fdSwfDhMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"tar-mini": "^0.2.0"
}
},
"node_modules/vite/node_modules/fdir": { "node_modules/vite/node_modules/fdir": {
"version": "6.4.6", "version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",

View file

@ -29,6 +29,7 @@
"globals": "^16.3.0", "globals": "^16.3.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.35.1", "typescript-eslint": "^8.35.1",
"vite": "^7.0.4" "vite": "^7.0.4",
"vite-plugin-compression2": "^2.2.0"
} }
} }

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,10 +1,11 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc' import react from '@vitejs/plugin-react-swc'
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
import { compression } from 'vite-plugin-compression2'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss(), compression()],
server: { server: {
host: '0.0.0.0', // Allow external access for Docker host: '0.0.0.0', // Allow external access for Docker
port: 5173, port: 5173,

135
main.go
View file

@ -14,6 +14,8 @@ import (
"sync" "sync"
"time" "time"
"path"
"github.com/google/uuid" "github.com/google/uuid"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
@ -486,6 +488,114 @@ func deleteItem(uuid string, id int) error {
return err return err
} }
// ==== Static File Server with Compression Support ====
// compressionFileServer serves static files with compression support
func compressionFileServer(fsys fs.FS) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if this is a UUID path (starts with / followed by 36 characters)
isUUIDPath := len(r.URL.Path) == 37 && r.URL.Path[0] == '/' &&
!strings.Contains(r.URL.Path[1:], "/")
if r.URL.Path == "/" || isUUIDPath {
// Serve index.html at root or for routes with /{uuid}
content, err := fs.ReadFile(fsys, "frontend/dist/index.html")
if err != nil {
http.Error(w, "Not found", 404)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(content)
return
}
// Get the requested file path relative to frontend/dist
filePath := strings.TrimPrefix(r.URL.Path, "/")
if filePath == "" {
filePath = "index.html"
}
fullPath := "frontend/dist/" + filePath
// Check if client accepts compression
acceptEncoding := r.Header.Get("Accept-Encoding")
acceptsGzip := strings.Contains(acceptEncoding, "gzip")
acceptsBrotli := strings.Contains(acceptEncoding, "br")
// Try to serve compressed version if client supports it
if acceptsBrotli {
if compressedContent, err := fs.ReadFile(fsys, fullPath+".br"); err == nil {
w.Header().Set("Content-Encoding", "br")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(compressedContent)))
w.Header().Set("Vary", "Accept-Encoding")
// Set appropriate content type based on file extension
setContentType(w, filePath)
w.Write(compressedContent)
return
}
}
if acceptsGzip {
if compressedContent, err := fs.ReadFile(fsys, fullPath+".gz"); err == nil {
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(compressedContent)))
w.Header().Set("Vary", "Accept-Encoding")
// Set appropriate content type based on file extension
setContentType(w, filePath)
w.Write(compressedContent)
return
}
}
// Fall back to uncompressed file
if content, err := fs.ReadFile(fsys, fullPath); err == nil {
w.Header().Set("Vary", "Accept-Encoding")
// Set appropriate content type based on file extension
setContentType(w, filePath)
w.Write(content)
return
}
// File not found
http.NotFound(w, r)
})
}
// setContentType sets the appropriate Content-Type header based on file extension
func setContentType(w http.ResponseWriter, filePath string) {
ext := strings.ToLower(path.Ext(filePath))
switch ext {
case ".html":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
case ".css":
w.Header().Set("Content-Type", "text/css; charset=utf-8")
case ".js":
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
case ".json":
w.Header().Set("Content-Type", "application/json; charset=utf-8")
case ".svg":
w.Header().Set("Content-Type", "image/svg+xml")
case ".png":
w.Header().Set("Content-Type", "image/png")
case ".jpg", ".jpeg":
w.Header().Set("Content-Type", "image/jpeg")
case ".gif":
w.Header().Set("Content-Type", "image/gif")
case ".ico":
w.Header().Set("Content-Type", "image/x-icon")
case ".woff":
w.Header().Set("Content-Type", "font/woff")
case ".woff2":
w.Header().Set("Content-Type", "font/woff2")
case ".ttf":
w.Header().Set("Content-Type", "font/ttf")
case ".eot":
w.Header().Set("Content-Type", "application/vnd.ms-fontobject")
default:
// Default to octet-stream for unknown types
w.Header().Set("Content-Type", "application/octet-stream")
}
}
// ==== SSE Broadcast Logic ==== // ==== SSE Broadcast Logic ====
func broadcast(uuid string, msg interface{}) { func broadcast(uuid string, msg interface{}) {
@ -894,30 +1004,7 @@ func main() {
}) })
// Serve static files from embedded filesystem (register last) // Serve static files from embedded filesystem (register last)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.Handle("/", compressionFileServer(staticFiles))
// Check if this is a UUID path (starts with / followed by 36 characters)
isUUIDPath := len(r.URL.Path) == 37 && r.URL.Path[0] == '/' &&
!strings.Contains(r.URL.Path[1:], "/")
if r.URL.Path == "/" || isUUIDPath {
// Serve index.html at root or for routes with /{uuid}
content, err := staticFiles.ReadFile("frontend/dist/index.html")
if err != nil {
http.Error(w, "Not found", 404)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(content)
} else {
// Serve other static files from the static/ subdirectory
subFS, err := fs.Sub(staticFiles, "frontend/dist")
if err != nil {
http.Error(w, "Not found", 404)
return
}
http.StripPrefix("/", http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
}
})
port := strings.TrimSpace(os.Getenv("PORT")) port := strings.TrimSpace(os.Getenv("PORT"))
if port == "" { if port == "" {