records/server/server.go

266 lines
6.7 KiB
Go

package server
import (
"fmt"
"log"
"os"
"regexp"
"slices"
"strings"
"net/http"
"net/url"
"path/filepath"
"git.baxters.nz/jeremy/records/musicindex"
"git.baxters.nz/jeremy/records/util"
)
var pagesInitialised = false
var indexPage IndexPage
var artistsPages map[string]ArtistsPage
var albumsPages map[string]AlbumsPage
// mapping of artist names to sort-by strings to pages
var artistPages map[string]map[string]ArtistPage
// mapping of artist names to album names to pages
var albumPages map[string]map[string]AlbumPage
func doIndexPage(w http.ResponseWriter, req *http.Request) {
writePage(indexPage, w)
}
func doArtistsPage(w http.ResponseWriter, req *http.Request) {
by := req.URL.Query().Get("by")
if slices.Contains(ArtistSortOptions(), by) {
writePage(artistsPages[by], w)
return
}
writePage(artistsPages["name"], w)
}
func doAlbumsPage(w http.ResponseWriter, req *http.Request) {
by := req.URL.Query().Get("by")
if slices.Contains(AlbumSortOptions(), by) {
writePage(albumsPages[by], w)
return
}
writePage(albumsPages["name"], w)
}
func detectTextType(fileName string) string {
if strings.HasSuffix(fileName, ".html") {
return "text/html"
}
if strings.HasSuffix(fileName, ".css") {
return "text/css"
}
return "text/plain"
}
func serveStaticFiles(w http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.RequestURI, "/static/templates/") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
contents, err := staticFS.ReadFile(req.RequestURI[1:])
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
contentType := http.DetectContentType(contents)
if strings.HasPrefix(contentType, "text/plain") {
contentType = detectTextType(filepath.Base(req.RequestURI))
}
w.Header().Add("Content-Type", contentType)
w.Write(contents)
}
func serveMediaDirectory(w http.ResponseWriter, req *http.Request) {
path, err := url.QueryUnescape("/" + strings.SplitN(req.RequestURI[1:], "/", 2)[1])
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
filePath := musicindex.MediaDirectory() + path
if strings.Contains(path, "/..") || strings.Contains(path, "/.") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
stat, err := os.Stat(filePath)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
if stat.IsDir() {
http.Error(w, "is a directory; maybe you are looking for " + path, http.StatusForbidden)
return
}
f, err := os.Open(filePath)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
util.DoChunks(f, func (buf []byte) { w.Write(buf) })
}
func handleArtistAlbumPage(w http.ResponseWriter, req *http.Request) {
// test artist
rx, err := regexp.Compile("^/([^/?]+)/?(\\?.*)?$")
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
if (rx.MatchString(req.RequestURI)) {
artist, err := url.QueryUnescape(rx.FindStringSubmatch(req.RequestURI)[1])
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if musicindex.ArtistExists(artist) {
by := req.URL.Query().Get("by")
if slices.Contains(AlbumSortOptions(), by) {
writePage(artistPages[artist][by], w)
return
}
writePage(artistPages[artist]["name"], w)
return
}
// artist not found
http.Error(w, "not found", http.StatusNotFound)
return
}
// test album tarball
rx, err = regexp.Compile("^/([^/?]+)/([^/?]+)\\.tar\\.gz$")
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
if (rx.MatchString(req.RequestURI)) {
captures := rx.FindStringSubmatch(req.RequestURI)
artist, err := url.QueryUnescape(captures[1])
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
albumName, err := url.QueryUnescape(captures[2])
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if musicindex.ArtistExists(artist) {
album, ok := musicindex.FindArtist(artist).Albums[albumName]
if ok {
contents, err := os.ReadFile(album.Tarball)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Content-Disposition", `attachment; filename="` +
strings.ReplaceAll(album.Name, `"`, `'`) + `.tar.gz"`)
w.Header().Set("Content-Length",
fmt.Sprintf("%d", album.TarballSize))
w.Write(contents)
return
}
}
// artist or album not found
http.Error(w, "not found", http.StatusNotFound)
return
}
// test album
rx, err = regexp.Compile("^/([^/?]+)/([^/?]+)/?(\\?.*)?$")
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
if (rx.MatchString(req.RequestURI)) {
captures := rx.FindStringSubmatch(req.RequestURI)
artist, err := url.QueryUnescape(captures[1])
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
albumName, err := url.QueryUnescape(captures[2])
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if musicindex.ArtistExists(artist) {
album, ok := musicindex.FindArtist(artist).Albums[albumName]
if ok {
writePage(albumPages[artist][album.Name], w)
return
}
}
// artist or album not found
http.Error(w, "not found", http.StatusNotFound)
return
}
// illegal URI format
http.Error(w, "not found", http.StatusNotFound)
}
func RunOn(address string) {
if !pagesInitialised {
indexPage = MakeIndexPage()
artistsPages = make(map[string]ArtistsPage)
for _, v := range ArtistSortOptions() {
artistsPages[v] = MakeArtistsPage(v)
}
albumsPages = make(map[string]AlbumsPage)
for _, v := range AlbumSortOptions() {
albumsPages[v] = MakeAlbumsPage(v)
}
artistPages = make(map[string]map[string]ArtistPage)
for _, artist := range musicindex.Artists() {
artistPages[artist] = make(map[string]ArtistPage)
for _, v := range AlbumSortOptions() {
artistPages[artist][v] = MakeArtistPage(v, artist)
}
}
albumPages = make(map[string]map[string]AlbumPage)
for _, artist := range musicindex.Artists() {
albumPages[artist] = make(map[string]AlbumPage)
for _, album := range musicindex.FindArtist(artist).Albums {
albumPages[artist][album.Name] = MakeAlbumPage(album)
}
}
pagesInitialised = true
}
http.HandleFunc("/{$}", doIndexPage)
http.HandleFunc("/index/artists", doArtistsPage)
http.HandleFunc("/index/albums", doAlbumsPage)
http.HandleFunc("/static/", serveStaticFiles)
http.HandleFunc("/media/", serveMediaDirectory)
http.HandleFunc("/", handleArtistAlbumPage)
log.Printf("listening on %s\n", address)
err := http.ListenAndServe(address, nil)
util.Die("%s", err.Error())
}
func Run() {
RunOn(":8000")
}