shrink
This commit is contained in:
parent
84ef53ae05
commit
21471c69ba
6 changed files with 179 additions and 29 deletions
|
@ -30,7 +30,7 @@ COPY . .
|
|||
COPY --from=frontend-builder /app/dist /app/frontend/dist
|
||||
|
||||
# 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
|
||||
FROM alpine:latest
|
||||
|
|
64
frontend/package-lock.json
generated
64
frontend/package-lock.json
generated
|
@ -27,7 +27,8 @@
|
|||
"globals": "^16.3.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.35.1",
|
||||
"vite": "^7.0.4"
|
||||
"vite": "^7.0.4",
|
||||
"vite-plugin-compression2": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
|
@ -824,6 +825,42 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "4.45.1",
|
||||
"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_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": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
|
@ -3544,6 +3588,13 @@
|
|||
"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": {
|
||||
"version": "0.2.14",
|
||||
"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": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"globals": "^16.3.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.35.1",
|
||||
"vite": "^7.0.4"
|
||||
"vite": "^7.0.4",
|
||||
"vite-plugin-compression2": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -1,10 +1,11 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { compression } from 'vite-plugin-compression2'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
plugins: [react(), tailwindcss(), compression()],
|
||||
server: {
|
||||
host: '0.0.0.0', // Allow external access for Docker
|
||||
port: 5173,
|
||||
|
|
135
main.go
135
main.go
|
@ -14,6 +14,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"path"
|
||||
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
@ -486,6 +488,114 @@ func deleteItem(uuid string, id int) error {
|
|||
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 ====
|
||||
|
||||
func broadcast(uuid string, msg interface{}) {
|
||||
|
@ -894,30 +1004,7 @@ func main() {
|
|||
})
|
||||
|
||||
// Serve static files from embedded filesystem (register last)
|
||||
http.HandleFunc("/", 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 := 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)
|
||||
}
|
||||
})
|
||||
http.Handle("/", compressionFileServer(staticFiles))
|
||||
|
||||
port := strings.TrimSpace(os.Getenv("PORT"))
|
||||
if port == "" {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue