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 } defer f.Close() util.DoChunks(f, 4, 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") }