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
|
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
|
||||||
|
|
64
frontend/package-lock.json
generated
64
frontend/package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { 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
135
main.go
|
@ -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 == "" {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue