add finishing touches, dockerfile
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
config.json
|
||||
.env
|
||||
@@ -2,3 +2,4 @@
|
||||
*.sh
|
||||
*.sqlite3
|
||||
.data
|
||||
internal/server/static
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
FROM node:24-alpine AS node-build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY frontend /build
|
||||
|
||||
RUN npm ci
|
||||
RUN npx vite build --outDir dist
|
||||
|
||||
FROM golang:1.26-alpine AS go-build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY . /build
|
||||
COPY --from=node-build /build/dist /build/internal/server/static
|
||||
|
||||
RUN go get
|
||||
RUN go build -o zampler
|
||||
|
||||
FROM alpine:3.24
|
||||
|
||||
COPY --from=go-build /build/zampler /usr/local/bin
|
||||
|
||||
ENTRYPOINT ["zampler"]
|
||||
CMD ["/data", "-l", "trace"]
|
||||
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
config.json
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"build": "vue-tsc -b && vite build --outDir ../internal/server/static",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useBookmarkStore, useFlagStore, useHistoryStore, useRemoteFileStore, useSearchStore } from './stores';
|
||||
import { useBookmarkStore, useConfigStore, useFlagStore, useHistoryStore, useRemoteFileStore, useSearchStore } from './stores';
|
||||
import FileGrid from '@/sections/FileGrid.vue';
|
||||
import SearchBar from '@/sections/SearchBar.vue';
|
||||
import AudioPlayer from '@/sections/AudioPlayer.vue';
|
||||
@@ -8,6 +8,7 @@ import BookmarkIcon from './icons/BookmarkIcon.vue';
|
||||
import ClockIcon from './icons/ClockIcon.vue';
|
||||
|
||||
const bookmarks = useBookmarkStore()
|
||||
const config = useConfigStore()
|
||||
const flags = useFlagStore()
|
||||
const history = useHistoryStore()
|
||||
const search = useSearchStore()
|
||||
@@ -28,7 +29,6 @@ const files = computed(() => {
|
||||
files = files.filter(file => file.path.toLowerCase().includes(v))
|
||||
}
|
||||
|
||||
console.log(flags.value.at(0))
|
||||
if (flags.value.includes("bookmark")) {
|
||||
const ids = bookmarks.files.map(file => file.id)
|
||||
files = files.filter(file => ids.includes(file.id))
|
||||
@@ -43,7 +43,10 @@ onMounted(() => {
|
||||
history.read()
|
||||
search.read()
|
||||
|
||||
remoteFiles.reload()
|
||||
config.reload().then(() => {
|
||||
remoteFiles.setServerUrl(config.value!.serverUrl)
|
||||
remoteFiles.reload()
|
||||
})
|
||||
|
||||
})
|
||||
</script>
|
||||
@@ -61,7 +64,7 @@ onMounted(() => {
|
||||
<div id="flags" style="grid-area: flags">
|
||||
<button type="button" v-on:click="flags.toggle('bookmark')" :data-active="flags.value.includes('bookmark')">
|
||||
<BookmarkIcon />
|
||||
<span>Bookmarked</span>
|
||||
<span>Bookmarks</span>
|
||||
</button>
|
||||
<button type="button" v-on:click="flags.toggle('history')" :data-active="flags.value.includes('history')">
|
||||
<ClockIcon />
|
||||
@@ -73,9 +76,9 @@ onMounted(() => {
|
||||
|
||||
<footer class="panel" id="bottom">
|
||||
<nav>
|
||||
<a href="https://code.aneur.in/zampler/zampler" target="_blank">Open Source</a>
|
||||
<a href="https://mit-license.org" target="_blank">© 2026 (MIT)</a>
|
||||
<a href="https://www.aneur.in" target="_blank">Aneurin</a>
|
||||
<a href="https://mit-license.org" target="_blank">© 2026 under MIT</a>
|
||||
<a href="https://code.aneur.in/zampler/zampler" target="_blank">Open Source</a>
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
Vendored
+4
@@ -1,4 +1,8 @@
|
||||
declare namespace dto {
|
||||
export interface Config {
|
||||
serverUrl: string
|
||||
}
|
||||
|
||||
export interface File {
|
||||
id: string
|
||||
path: string
|
||||
|
||||
+53
-10
@@ -5,6 +5,38 @@ export const useAudioPlayerStore = useFileStore('audio-player')
|
||||
|
||||
export const useBookmarkStore = useLocalFilesStore('bookmarks')
|
||||
|
||||
export const useConfigStore = defineStore('config', {
|
||||
state() {
|
||||
const busy = false
|
||||
let value: dto.Config | undefined
|
||||
|
||||
return {
|
||||
busy,
|
||||
value,
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
async reload() {
|
||||
if (this.busy) {
|
||||
return
|
||||
}
|
||||
|
||||
this.busy = true
|
||||
try {
|
||||
const res = await fetch('config.json')
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load config (${res.status} ${res.statusText})`)
|
||||
}
|
||||
|
||||
this.value = await res.json()
|
||||
} finally {
|
||||
this.busy = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export function useFileStore(key: string) {
|
||||
return defineStore(`file-${key}`, {
|
||||
state() {
|
||||
@@ -104,16 +136,14 @@ export function useLocalFilesStore(key: string) {
|
||||
|
||||
export const useRemoteFileStore = defineStore('files', {
|
||||
state() {
|
||||
const busy = false
|
||||
const files: dto.File[] = []
|
||||
|
||||
const serverUrl = import.meta.env.VITE_SERVER_URL || 'http://localhost:7777'
|
||||
const apiUrl = import.meta.env.VITE_API_URL || `${serverUrl}/api`
|
||||
const serverUrl = ''
|
||||
|
||||
return {
|
||||
apiUrl,
|
||||
busy,
|
||||
files,
|
||||
serverUrl,
|
||||
|
||||
files
|
||||
}
|
||||
},
|
||||
|
||||
@@ -125,12 +155,25 @@ export const useRemoteFileStore = defineStore('files', {
|
||||
|
||||
actions: {
|
||||
async reload() {
|
||||
const res = await fetch(`${this.apiUrl}/files`)
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to reload files from API (${res.status} ${res.statusText})`)
|
||||
if (this.busy) {
|
||||
return
|
||||
}
|
||||
|
||||
this.files = await res.json()
|
||||
try {
|
||||
this.busy = true
|
||||
const res = await fetch(`${this.serverUrl}/api/files`)
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to reload files from API (${res.status} ${res.statusText})`)
|
||||
}
|
||||
|
||||
this.files = await res.json()
|
||||
} finally {
|
||||
this.busy = false
|
||||
}
|
||||
},
|
||||
|
||||
setServerUrl(serverUrl: string) {
|
||||
this.serverUrl = serverUrl
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+7
-1
@@ -33,6 +33,8 @@ func (cli *CLI) Cmd() *cobra.Command {
|
||||
f.StringP("address", "a", "", "HTTP bind address")
|
||||
f.IntP("port", "p", 7777, "HTTP listen port")
|
||||
|
||||
f.StringP("url", "u", "", "frontend URL")
|
||||
|
||||
f.StringP("log-level", "l", "info", "logging level (trace, debug, info, warn, error)")
|
||||
|
||||
f.StringArrayP("extension", "x", []string{"wav"}, "allowed audio file extensions")
|
||||
@@ -58,6 +60,7 @@ func (cli *CLI) RunE(cmd *cobra.Command, rootDirs []string) error {
|
||||
extensions, _ := cmd.Flags().GetStringArray("extension")
|
||||
host, _ := cmd.Flags().GetString("host")
|
||||
port, _ := cmd.Flags().GetInt("port")
|
||||
url, _ := cmd.Flags().GetString("url")
|
||||
|
||||
if len(extensions) == 0 {
|
||||
return errors.New("at least one extension required")
|
||||
@@ -76,7 +79,7 @@ func (cli *CLI) RunE(cmd *cobra.Command, rootDirs []string) error {
|
||||
router.Use(middlewares.NewLog(cli.log).Middleware)
|
||||
|
||||
// Set up standard HTTP endpoints
|
||||
srv := server.NewServer(cli.db, cli.log)
|
||||
srv := server.NewServer(cli.db, cli.log, url)
|
||||
srv.ConfigureRouter(router)
|
||||
|
||||
// Set up API endpoints
|
||||
@@ -84,6 +87,9 @@ func (cli *CLI) RunE(cmd *cobra.Command, rootDirs []string) error {
|
||||
apiRouter := router.PathPrefix("/api/").Subrouter()
|
||||
api.ConfigureRouter(apiRouter)
|
||||
|
||||
// Set up static assets
|
||||
srv.ConfigureStaticAssets(router)
|
||||
|
||||
errch := make(chan error)
|
||||
go func() {
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package dto
|
||||
|
||||
type Config struct {
|
||||
ServerURL string `json:"serverUrl"`
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package dto
|
||||
|
||||
type File struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Hash string `json:"hash"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (l *Log) Middleware(next http.Handler) http.Handler {
|
||||
func (l *Log) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
u, _ := uuid.NewV7()
|
||||
|
||||
lw := &logWriter{
|
||||
lw := &LogWriter{
|
||||
id: u.String(),
|
||||
log: l.log.With().Logger(),
|
||||
next: w,
|
||||
@@ -39,9 +39,9 @@ func (l *Log) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
l.next.ServeHTTP(lw, req)
|
||||
}
|
||||
|
||||
// logWriter implements and wraps http.ResponseWriter to track and log each individual request.
|
||||
// LogWriter implements and wraps http.ResponseWriter to track and log each individual request.
|
||||
// This is only used internally by Log.
|
||||
type logWriter struct {
|
||||
type LogWriter struct {
|
||||
id string
|
||||
log zerolog.Logger
|
||||
next http.ResponseWriter
|
||||
@@ -50,11 +50,11 @@ type logWriter struct {
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w *logWriter) Header() http.Header {
|
||||
func (w *LogWriter) Header() http.Header {
|
||||
return w.next.Header()
|
||||
}
|
||||
|
||||
func (w *logWriter) Write(b []byte) (int, error) {
|
||||
func (w *LogWriter) Write(b []byte) (int, error) {
|
||||
bytes, err := w.next.Write(b)
|
||||
|
||||
if w.statusCode == 0 {
|
||||
@@ -66,12 +66,16 @@ func (w *logWriter) Write(b []byte) (int, error) {
|
||||
return bytes, err
|
||||
}
|
||||
|
||||
func (w *logWriter) WriteHeader(statusCode int) {
|
||||
func (w *LogWriter) WriteHeader(statusCode int) {
|
||||
w.statusCode = statusCode
|
||||
w.next.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (w *logWriter) logRequest(bytes int) {
|
||||
func (w *LogWriter) Written() bool {
|
||||
return w.statusCode > 0
|
||||
}
|
||||
|
||||
func (w *LogWriter) logRequest(bytes int) {
|
||||
var e *zerolog.Event
|
||||
|
||||
if w.statusCode >= 100 && w.statusCode < 400 {
|
||||
|
||||
@@ -1,28 +1,60 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.aneur.in/go/rest"
|
||||
"code.aneur.in/zampler/zampler/internal/dto"
|
||||
"code.aneur.in/zampler/zampler/internal/samples"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
db *samples.DB
|
||||
//go:embed static
|
||||
var staticFiles embed.FS
|
||||
|
||||
type Server struct {
|
||||
url string
|
||||
|
||||
db *samples.DB
|
||||
log zerolog.Logger
|
||||
|
||||
static http.Handler
|
||||
staticFiles fs.FS
|
||||
}
|
||||
|
||||
func (srv *Server) ConfigureRouter(r *mux.Router) {
|
||||
r.Path("/config.json").Methods(http.MethodGet, http.MethodOptions).HandlerFunc(srv.GetConfig)
|
||||
r.Path("/files/{path:.+}").Methods(http.MethodGet, http.MethodOptions).HandlerFunc(srv.ReadFileByPath)
|
||||
r.Path("/file/{id}").Methods(http.MethodGet, http.MethodOptions).HandlerFunc(srv.ReadFile)
|
||||
}
|
||||
|
||||
func NewServer(db *samples.DB, log zerolog.Logger) *Server {
|
||||
srv := &Server{
|
||||
db: db,
|
||||
func (srv *Server) ConfigureStaticAssets(r *mux.Router) {
|
||||
staticFiles, err := fs.Sub(staticFiles, "static")
|
||||
if err != nil {
|
||||
srv.log.Warn().Err(err).Msgf("Directory %q not found, not serving static assets", "internal/server/static")
|
||||
return
|
||||
}
|
||||
|
||||
srv.staticFiles = staticFiles
|
||||
srv.static = http.FileServerFS(staticFiles)
|
||||
r.PathPrefix("/").Methods(http.MethodGet, http.MethodOptions).Handler(srv.static)
|
||||
}
|
||||
|
||||
func (srv *Server) GetConfig(w http.ResponseWriter, req *http.Request) {
|
||||
rest.WriteResponseJSON(w, http.StatusOK, dto.Config{
|
||||
ServerURL: strings.TrimRight(srv.url, "/"),
|
||||
})
|
||||
}
|
||||
|
||||
func NewServer(db *samples.DB, log zerolog.Logger, url string) *Server {
|
||||
srv := &Server{
|
||||
url: url,
|
||||
|
||||
db: db,
|
||||
log: log.With().Str("namespace", "server.Server").Logger(),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user