Total restructure, just look at the diffs at this point

This commit is contained in:
Seednode 2023-09-12 23:35:17 -05:00
parent cbf7218453
commit d7bc6e2451
11 changed files with 361 additions and 240 deletions

View File

@ -78,13 +78,17 @@ The cache can be regenerated at any time by accessing the `/clear_cache` endpoin
If `--cache-file` is set, the cache will be loaded from the specified file on start, and written to the file whenever it is re-generated. If `--cache-file` is set, the cache will be loaded from the specified file on start, and written to the file whenever it is re-generated.
If the `-i|--index` flag is passed, four additional endpoints are registered. ## Info
If the `-i|--info` flag is passed, six additional endpoints are registered.
The first of these—`/html` and `/json`—return the contents of the index, in HTML and JSON formats respectively. The first of these—`/html` and `/json`—return the contents of the index, in HTML and JSON formats respectively.
If `--page-length` is also set, these can be viewed in paginated form by appending `/n`, e.g. `/html/5` for the fifth page.
This can prove useful when confirming whether the index is generated successfully, or whether a given file is in the index. This can prove useful when confirming whether the index is generated successfully, or whether a given file is in the index.
The other two endpoints—`/extensions` and `/mime_types`—return the registered file types. The remaining four endpoints—`/available_extensions`, `/enabled_extensions`, `/available_mime_types` and `/enabled_mime_types`—return information about the registered file types.
## Statistics ## Statistics
@ -92,6 +96,8 @@ If the `--stats` flag is passed, an additional endpoint, `/stats`, is registered
When accessed, this endpoint returns a JSON document listing every file served, along with the number of times it has been served, its filesize, and timestamps of when it was served. When accessed, this endpoint returns a JSON document listing every file served, along with the number of times it has been served, its filesize, and timestamps of when it was served.
If `--page-length` is also set, this can be viewed in paginated form by appending `/n`, e.g. `/stats/5` for the fifth page.
## Russian ## Russian
If the `--russian` flag is passed, everything functions exactly as you would expect. If the `--russian` flag is passed, everything functions exactly as you would expect.
@ -111,7 +117,7 @@ Usage:
roulette <path> [path]... [flags] roulette <path> [path]... [flags]
Flags: Flags:
--all enable all supported file types -a, --all enable all supported file types
--audio enable support for audio files --audio enable support for audio files
-b, --bind string address to bind to (default "0.0.0.0") -b, --bind string address to bind to (default "0.0.0.0")
-c, --cache generate directory cache at startup -c, --cache generate directory cache at startup
@ -120,7 +126,7 @@ Flags:
--flash enable support for shockwave flash files (via ruffle.rs) --flash enable support for shockwave flash files (via ruffle.rs)
-h, --help help for roulette -h, --help help for roulette
--images enable support for image files --images enable support for image files
-i, --index expose index endpoints -i, --info expose informational endpoints
--maximum-files uint32 skip directories with file counts above this value (default 4294967295) --maximum-files uint32 skip directories with file counts above this value (default 4294967295)
--minimum-files uint32 skip directories with file counts below this value (default 1) --minimum-files uint32 skip directories with file counts below this value (default 1)
--page-length uint32 pagination length for statistics and debug pages --page-length uint32 pagination length for statistics and debug pages
@ -133,7 +139,7 @@ Flags:
--stats expose stats endpoint --stats expose stats endpoint
--stats-file string path to optional persistent stats file --stats-file string path to optional persistent stats file
--text enable support for text files --text enable support for text files
-v, --verbose log accessed files to stdout -v, --verbose log accessed files and other information to stdout
-V, --version display version and exit -V, --version display version and exit
--video enable support for video files --video enable support for video files
``` ```

141
cmd/cache.go Normal file
View File

@ -0,0 +1,141 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package cmd
import (
"encoding/gob"
"net/http"
"os"
"sync"
"github.com/julienschmidt/httprouter"
"github.com/klauspost/compress/zstd"
"seedno.de/seednode/roulette/types"
)
type FileIndex struct {
mutex sync.RWMutex
list []string
}
func (i *FileIndex) Index() []string {
i.mutex.RLock()
val := i.list
i.mutex.RUnlock()
return val
}
func (i *FileIndex) Remove(path string) {
i.mutex.RLock()
tempIndex := make([]string, len(i.list))
copy(tempIndex, i.list)
i.mutex.RUnlock()
var position int
for k, v := range tempIndex {
if path == v {
position = k
break
}
}
tempIndex[position] = tempIndex[len(tempIndex)-1]
i.mutex.Lock()
i.list = make([]string, len(tempIndex)-1)
copy(i.list, tempIndex[:len(tempIndex)-1])
i.mutex.Unlock()
}
func (i *FileIndex) setIndex(val []string) {
i.mutex.Lock()
i.list = val
i.mutex.Unlock()
}
func (i *FileIndex) generateCache(args []string, formats *types.Types) {
i.mutex.Lock()
i.list = []string{}
i.mutex.Unlock()
fileList(args, &Filters{}, "", i, formats)
if Cache && CacheFile != "" {
i.Export(CacheFile)
}
}
func (i *FileIndex) IsEmpty() bool {
i.mutex.RLock()
length := len(i.list)
i.mutex.RUnlock()
return length == 0
}
func (i *FileIndex) Export(path string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer file.Close()
z, err := zstd.NewWriter(file)
if err != nil {
return err
}
defer z.Close()
enc := gob.NewEncoder(z)
i.mutex.RLock()
enc.Encode(&i.list)
i.mutex.RUnlock()
return nil
}
func (i *FileIndex) Import(path string) error {
file, err := os.OpenFile(path, os.O_RDONLY, 0600)
if err != nil {
return err
}
defer file.Close()
z, err := zstd.NewReader(file)
if err != nil {
return err
}
defer z.Close()
dec := gob.NewDecoder(z)
i.mutex.Lock()
err = dec.Decode(&i.list)
i.mutex.Unlock()
if err != nil {
return err
}
return nil
}
func serveCacheClear(args []string, index *FileIndex, formats *types.Types) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
index.generateCache(args, formats)
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("Ok"))
}
}

View File

@ -5,150 +5,20 @@ Copyright © 2023 Seednode <seednode@seedno.de>
package cmd package cmd
import ( import (
"encoding/gob"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/klauspost/compress/zstd"
"github.com/yosssi/gohtml" "github.com/yosssi/gohtml"
"seedno.de/seednode/roulette/types" "seedno.de/seednode/roulette/types"
) )
type FileIndex struct {
mutex sync.RWMutex
list []string
}
func (i *FileIndex) Index() []string {
i.mutex.RLock()
val := i.list
i.mutex.RUnlock()
return val
}
func (i *FileIndex) Remove(path string) {
i.mutex.RLock()
tempIndex := make([]string, len(i.list))
copy(tempIndex, i.list)
i.mutex.RUnlock()
var position int
for k, v := range tempIndex {
if path == v {
position = k
break
}
}
tempIndex[position] = tempIndex[len(tempIndex)-1]
i.mutex.Lock()
i.list = make([]string, len(tempIndex)-1)
copy(i.list, tempIndex[:len(tempIndex)-1])
i.mutex.Unlock()
}
func (i *FileIndex) setIndex(val []string) {
i.mutex.Lock()
i.list = val
i.mutex.Unlock()
}
func (i *FileIndex) generateCache(args []string, formats *types.Types) {
i.mutex.Lock()
i.list = []string{}
i.mutex.Unlock()
fileList(args, &Filters{}, "", i, formats)
if Cache && CacheFile != "" {
i.Export(CacheFile)
}
}
func (i *FileIndex) IsEmpty() bool {
i.mutex.RLock()
length := len(i.list)
i.mutex.RUnlock()
return length == 0
}
func (i *FileIndex) Export(path string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer file.Close()
z, err := zstd.NewWriter(file)
if err != nil {
return err
}
defer z.Close()
enc := gob.NewEncoder(z)
i.mutex.RLock()
enc.Encode(&i.list)
i.mutex.RUnlock()
return nil
}
func (i *FileIndex) Import(path string) error {
file, err := os.OpenFile(path, os.O_RDONLY, 0600)
if err != nil {
return err
}
defer file.Close()
z, err := zstd.NewReader(file)
if err != nil {
return err
}
defer z.Close()
dec := gob.NewDecoder(z)
i.mutex.Lock()
err = dec.Decode(&i.list)
i.mutex.Unlock()
if err != nil {
return err
}
return nil
}
func serveCacheClear(args []string, index *FileIndex, formats *types.Types) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
index.generateCache(args, formats)
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("Ok"))
}
}
func serveIndexHtml(args []string, index *FileIndex, paginate bool) httprouter.Handle { func serveIndexHtml(args []string, index *FileIndex, paginate bool) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
@ -318,30 +188,34 @@ func serveIndexJson(args []string, index *FileIndex) httprouter.Handle {
} }
} }
func serveExtensions(formats *types.Types) httprouter.Handle { func serveAvailableExtensions() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
startTime := time.Now() startTime := time.Now()
var output strings.Builder response := []byte(types.SupportedFormats.GetExtensions())
extensions := make([]string, len(formats.Extensions)) w.Write(response)
i := 0 if Verbose {
fmt.Printf("%s | Served available extensions list (%s) to %s in %s\n",
for k := range formats.Extensions { startTime.Format(LogDate),
extensions[i] = k humanReadableSize(len(response)),
i++ realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
} }
slices.Sort(extensions)
for _, v := range extensions {
output.WriteString(v + "\n")
} }
}
response := []byte(output.String()) func serveEnabledExtensions(formats *types.Types) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Set("Content-Type", "text/plain")
startTime := time.Now()
response := []byte(formats.GetExtensions())
w.Write(response) w.Write(response)
@ -356,30 +230,34 @@ func serveExtensions(formats *types.Types) httprouter.Handle {
} }
} }
func serveMimeTypes(formats *types.Types) httprouter.Handle { func serveAvailableMimeTypes() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
startTime := time.Now() startTime := time.Now()
var output strings.Builder response := []byte(types.SupportedFormats.GetMimeTypes())
mimeTypes := make([]string, len(formats.MimeTypes)) w.Write(response)
i := 0 if Verbose {
fmt.Printf("%s | Served available MIME types list (%s) to %s in %s\n",
for k := range formats.MimeTypes { startTime.Format(LogDate),
mimeTypes[i] = k humanReadableSize(len(response)),
i++ realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
} }
slices.Sort(mimeTypes)
for _, v := range mimeTypes {
output.WriteString(v + "\n")
} }
}
response := []byte(output.String()) func serveEnabledMimeTypes(formats *types.Types) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Set("Content-Type", "text/plain")
startTime := time.Now()
response := []byte(formats.GetMimeTypes())
w.Write(response) w.Write(response)

View File

@ -12,7 +12,7 @@ import (
) )
const ( const (
ReleaseVersion string = "0.75.0" ReleaseVersion string = "0.76.0"
) )
var ( var (
@ -24,7 +24,7 @@ var (
Filtering bool Filtering bool
Flash bool Flash bool
Images bool Images bool
Index bool Info bool
MaximumFileCount uint32 MaximumFileCount uint32
MinimumFileCount uint32 MinimumFileCount uint32
PageLength uint32 PageLength uint32
@ -46,10 +46,6 @@ var (
Short: "Serves random media from the specified directories.", Short: "Serves random media from the specified directories.",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
if Index {
cmd.MarkFlagRequired("cache")
}
if RefreshInterval != "" { if RefreshInterval != "" {
interval, err := time.ParseDuration(RefreshInterval) interval, err := time.ParseDuration(RefreshInterval)
if err != nil || interval < 500*time.Millisecond { if err != nil || interval < 500*time.Millisecond {
@ -78,7 +74,7 @@ func Execute() {
} }
func init() { func init() {
rootCmd.Flags().BoolVar(&All, "all", false, "enable all supported file types") rootCmd.Flags().BoolVarP(&All, "all", "a", false, "enable all supported file types")
rootCmd.Flags().BoolVar(&Audio, "audio", false, "enable support for audio files") rootCmd.Flags().BoolVar(&Audio, "audio", false, "enable support for audio files")
rootCmd.Flags().StringVarP(&Bind, "bind", "b", "0.0.0.0", "address to bind to") rootCmd.Flags().StringVarP(&Bind, "bind", "b", "0.0.0.0", "address to bind to")
rootCmd.Flags().BoolVarP(&Cache, "cache", "c", false, "generate directory cache at startup") rootCmd.Flags().BoolVarP(&Cache, "cache", "c", false, "generate directory cache at startup")
@ -86,7 +82,7 @@ func init() {
rootCmd.Flags().BoolVarP(&Filtering, "filter", "f", false, "enable filtering") rootCmd.Flags().BoolVarP(&Filtering, "filter", "f", false, "enable filtering")
rootCmd.Flags().BoolVar(&Flash, "flash", false, "enable support for shockwave flash files (via ruffle.rs)") rootCmd.Flags().BoolVar(&Flash, "flash", false, "enable support for shockwave flash files (via ruffle.rs)")
rootCmd.Flags().BoolVar(&Images, "images", false, "enable support for image files") rootCmd.Flags().BoolVar(&Images, "images", false, "enable support for image files")
rootCmd.Flags().BoolVarP(&Index, "index", "i", false, "expose index endpoints") rootCmd.Flags().BoolVarP(&Info, "info", "i", false, "expose informational endpoints")
rootCmd.Flags().Uint32Var(&MaximumFileCount, "maximum-files", 1<<32-1, "skip directories with file counts above this value") rootCmd.Flags().Uint32Var(&MaximumFileCount, "maximum-files", 1<<32-1, "skip directories with file counts above this value")
rootCmd.Flags().Uint32Var(&MinimumFileCount, "minimum-files", 1, "skip directories with file counts below this value") rootCmd.Flags().Uint32Var(&MinimumFileCount, "minimum-files", 1, "skip directories with file counts below this value")
rootCmd.Flags().Uint32Var(&PageLength, "page-length", 0, "pagination length for statistics and debug pages") rootCmd.Flags().Uint32Var(&PageLength, "page-length", 0, "pagination length for statistics and debug pages")
@ -99,7 +95,7 @@ func init() {
rootCmd.Flags().BoolVar(&Statistics, "stats", false, "expose stats endpoint") rootCmd.Flags().BoolVar(&Statistics, "stats", false, "expose stats endpoint")
rootCmd.Flags().StringVar(&StatisticsFile, "stats-file", "", "path to optional persistent stats file") rootCmd.Flags().StringVar(&StatisticsFile, "stats-file", "", "path to optional persistent stats file")
rootCmd.Flags().BoolVar(&Text, "text", false, "enable support for text files") rootCmd.Flags().BoolVar(&Text, "text", false, "enable support for text files")
rootCmd.Flags().BoolVarP(&Verbose, "verbose", "v", false, "log accessed files to stdout") rootCmd.Flags().BoolVarP(&Verbose, "verbose", "v", false, "log accessed files and other information to stdout")
rootCmd.Flags().BoolVarP(&Version, "version", "V", false, "display version and exit") rootCmd.Flags().BoolVarP(&Version, "version", "V", false, "display version and exit")
rootCmd.Flags().BoolVar(&Videos, "video", false, "enable support for video files") rootCmd.Flags().BoolVar(&Videos, "video", false, "enable support for video files")

View File

@ -27,6 +27,11 @@ import (
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/yosssi/gohtml" "github.com/yosssi/gohtml"
"seedno.de/seednode/roulette/types" "seedno.de/seednode/roulette/types"
"seedno.de/seednode/roulette/types/audio"
"seedno.de/seednode/roulette/types/flash"
"seedno.de/seednode/roulette/types/images"
"seedno.de/seednode/roulette/types/text"
"seedno.de/seednode/roulette/types/video"
) )
const ( const (
@ -317,25 +322,25 @@ func ServePage(args []string) error {
} }
if Audio || All { if Audio || All {
formats.Add(types.Audio{}) formats.Add(audio.Format{})
} }
if Flash || All { if Flash || All {
formats.Add(types.Flash{}) formats.Add(flash.Format{})
} }
if Text || All { if Text || All {
formats.Add(types.Text{}) formats.Add(text.Format{})
} }
if Videos || All { if Videos || All {
formats.Add(types.Video{}) formats.Add(video.Format{})
} }
// enable image support if no other flags are passed, to retain backwards compatibility // enable image support if no other flags are passed, to retain backwards compatibility
// to be replaced with rootCmd.MarkFlagsOneRequired on next spf13/cobra update // to be replaced with rootCmd.MarkFlagsOneRequired on next spf13/cobra update
if Images || All || len(formats.Extensions) == 0 { if Images || All || len(formats.Extensions) == 0 {
formats.Add(types.Images{}) formats.Add(images.Format{})
} }
paths, err := normalizePaths(args, formats) paths, err := normalizePaths(args, formats)
@ -408,7 +413,8 @@ func ServePage(args []string) error {
mux.GET("/clear_cache", serveCacheClear(args, index, formats)) mux.GET("/clear_cache", serveCacheClear(args, index, formats))
} }
if Index { if Info {
if Cache {
mux.GET("/html/", serveIndexHtml(args, index, false)) mux.GET("/html/", serveIndexHtml(args, index, false))
if PageLength != 0 { if PageLength != 0 {
mux.GET("/html/:page", serveIndexHtml(args, index, true)) mux.GET("/html/:page", serveIndexHtml(args, index, true))
@ -418,10 +424,12 @@ func ServePage(args []string) error {
if PageLength != 0 { if PageLength != 0 {
mux.GET("/json/:page", serveIndexJson(args, index)) mux.GET("/json/:page", serveIndexJson(args, index))
} }
}
mux.GET("/extensions", serveExtensions(formats)) mux.GET("/available_extensions", serveAvailableExtensions())
mux.GET("/enabled_extensions", serveEnabledExtensions(formats))
mux.GET("/mime_types", serveMimeTypes(formats)) mux.GET("/available_mime_types", serveAvailableMimeTypes())
mux.GET("/enabled_mime_types", serveEnabledMimeTypes(formats))
} }
if Profile { if Profile {

View File

@ -2,16 +2,18 @@
Copyright © 2023 Seednode <seednode@seedno.de> Copyright © 2023 Seednode <seednode@seedno.de>
*/ */
package types package audio
import ( import (
"fmt" "fmt"
"strings" "strings"
"seedno.de/seednode/roulette/types"
) )
type Audio struct{} type Format struct{}
func (t Audio) Css() string { func (t Format) Css() string {
var css strings.Builder var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`) css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
@ -20,11 +22,11 @@ func (t Audio) Css() string {
return css.String() return css.String()
} }
func (t Audio) Title(queryParams, fileUri, filePath, fileName, mime string) string { func (t Format) Title(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName) return fmt.Sprintf(`<title>%s</title>`, fileName)
} }
func (t Audio) Body(queryParams, fileUri, filePath, fileName, mime string) string { func (t Format) Body(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<a href="/%s"><audio controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the audio tag.</audio></a>`, return fmt.Sprintf(`<a href="/%s"><audio controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the audio tag.</audio></a>`,
queryParams, queryParams,
fileUri, fileUri,
@ -32,7 +34,7 @@ func (t Audio) Body(queryParams, fileUri, filePath, fileName, mime string) strin
fileName) fileName)
} }
func (t Audio) Extensions() map[string]string { func (t Format) Extensions() map[string]string {
return map[string]string{ return map[string]string{
`.mp3`: `audio/mpeg`, `.mp3`: `audio/mpeg`,
`.ogg`: `audio/ogg`, `.ogg`: `audio/ogg`,
@ -40,7 +42,7 @@ func (t Audio) Extensions() map[string]string {
} }
} }
func (t Audio) MimeTypes() []string { func (t Format) MimeTypes() []string {
return []string{ return []string{
`application/ogg`, `application/ogg`,
`audio/mp3`, `audio/mp3`,
@ -51,6 +53,12 @@ func (t Audio) MimeTypes() []string {
} }
} }
func (t Audio) Validate(filePath string) bool { func (t Format) Validate(filePath string) bool {
return true return true
} }
func init() {
format := Format{}
types.Register(format)
}

View File

@ -2,16 +2,18 @@
Copyright © 2023 Seednode <seednode@seedno.de> Copyright © 2023 Seednode <seednode@seedno.de>
*/ */
package types package flash
import ( import (
"fmt" "fmt"
"strings" "strings"
"seedno.de/seednode/roulette/types"
) )
type Flash struct{} type Format struct{}
func (t Flash) Css() string { func (t Format) Css() string {
var css strings.Builder var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`) css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
@ -20,11 +22,11 @@ func (t Flash) Css() string {
return css.String() return css.String()
} }
func (t Flash) Title(queryParams, fileUri, filePath, fileName, mime string) string { func (t Format) Title(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName) return fmt.Sprintf(`<title>%s</title>`, fileName)
} }
func (t Flash) Body(queryParams, fileUri, filePath, fileName, mime string) string { func (t Format) Body(queryParams, fileUri, filePath, fileName, mime string) string {
var html strings.Builder var html strings.Builder
html.WriteString(fmt.Sprintf(`<script src="https://unpkg.com/@ruffle-rs/ruffle"></script><script>window.RufflePlayer.config = {autoplay:"on"};</script><embed src="%s"></embed>`, fileUri)) html.WriteString(fmt.Sprintf(`<script src="https://unpkg.com/@ruffle-rs/ruffle"></script><script>window.RufflePlayer.config = {autoplay:"on"};</script><embed src="%s"></embed>`, fileUri))
@ -33,18 +35,24 @@ func (t Flash) Body(queryParams, fileUri, filePath, fileName, mime string) strin
return html.String() return html.String()
} }
func (t Flash) Extensions() map[string]string { func (t Format) Extensions() map[string]string {
return map[string]string{ return map[string]string{
`.swf`: `application/x-shockwave-flash`, `.swf`: `application/x-shockwave-flash`,
} }
} }
func (t Flash) MimeTypes() []string { func (t Format) MimeTypes() []string {
return []string{ return []string{
`application/x-shockwave-flash`, `application/x-shockwave-flash`,
} }
} }
func (t Flash) Validate(filePath string) bool { func (t Format) Validate(filePath string) bool {
return true return true
} }
func init() {
format := Format{}
types.Register(format)
}

View File

@ -2,7 +2,7 @@
Copyright © 2023 Seednode <seednode@seedno.de> Copyright © 2023 Seednode <seednode@seedno.de>
*/ */
package types package images
import ( import (
"errors" "errors"
@ -16,16 +16,17 @@ import (
_ "golang.org/x/image/bmp" _ "golang.org/x/image/bmp"
_ "golang.org/x/image/webp" _ "golang.org/x/image/webp"
"seedno.de/seednode/roulette/types"
) )
type Dimensions struct { type dimensions struct {
Width int width int
Height int height int
} }
type Images struct{} type Format struct{}
func (t Images) Css() string { func (t Format) Css() string {
var css strings.Builder var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`) css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
@ -36,7 +37,7 @@ func (t Images) Css() string {
return css.String() return css.String()
} }
func (t Images) Title(queryParams, fileUri, filePath, fileName, mime string) string { func (t Format) Title(queryParams, fileUri, filePath, fileName, mime string) string {
dimensions, err := ImageDimensions(filePath) dimensions, err := ImageDimensions(filePath)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -44,11 +45,11 @@ func (t Images) Title(queryParams, fileUri, filePath, fileName, mime string) str
return fmt.Sprintf(`<title>%s (%dx%d)</title>`, return fmt.Sprintf(`<title>%s (%dx%d)</title>`,
fileName, fileName,
dimensions.Width, dimensions.width,
dimensions.Height) dimensions.height)
} }
func (t Images) Body(queryParams, fileUri, filePath, fileName, mime string) string { func (t Format) Body(queryParams, fileUri, filePath, fileName, mime string) string {
dimensions, err := ImageDimensions(filePath) dimensions, err := ImageDimensions(filePath)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -57,13 +58,13 @@ func (t Images) Body(queryParams, fileUri, filePath, fileName, mime string) stri
return fmt.Sprintf(`<a href="/%s"><img src="%s" width="%d" height="%d" type="%s" alt="Roulette selected: %s"></a>`, return fmt.Sprintf(`<a href="/%s"><img src="%s" width="%d" height="%d" type="%s" alt="Roulette selected: %s"></a>`,
queryParams, queryParams,
fileUri, fileUri,
dimensions.Width, dimensions.width,
dimensions.Height, dimensions.height,
mime, mime,
fileName) fileName)
} }
func (t Images) Extensions() map[string]string { func (t Format) Extensions() map[string]string {
return map[string]string{ return map[string]string{
`.apng`: `image/apng`, `.apng`: `image/apng`,
`.avif`: `image/avif`, `.avif`: `image/avif`,
@ -80,7 +81,7 @@ func (t Images) Extensions() map[string]string {
} }
} }
func (t Images) MimeTypes() []string { func (t Format) MimeTypes() []string {
return []string{ return []string{
`image/apng`, `image/apng`,
`image/avif`, `image/avif`,
@ -93,19 +94,19 @@ func (t Images) MimeTypes() []string {
} }
} }
func (t Images) Validate(filePath string) bool { func (t Format) Validate(filePath string) bool {
return true return true
} }
func ImageDimensions(path string) (*Dimensions, error) { func ImageDimensions(path string) (*dimensions, error) {
file, err := os.Open(path) file, err := os.Open(path)
switch { switch {
case errors.Is(err, os.ErrNotExist): case errors.Is(err, os.ErrNotExist):
fmt.Printf("File %s does not exist\n", path) fmt.Printf("File %s does not exist\n", path)
return &Dimensions{}, nil return &dimensions{}, nil
case err != nil: case err != nil:
fmt.Printf("File %s open returned error: %s\n", path, err) fmt.Printf("File %s open returned error: %s\n", path, err)
return &Dimensions{}, err return &dimensions{}, err
} }
defer file.Close() defer file.Close()
@ -113,11 +114,17 @@ func ImageDimensions(path string) (*Dimensions, error) {
switch { switch {
case errors.Is(err, image.ErrFormat): case errors.Is(err, image.ErrFormat):
fmt.Printf("File %s has invalid image format\n", path) fmt.Printf("File %s has invalid image format\n", path)
return &Dimensions{Width: 0, Height: 0}, nil return &dimensions{width: 0, height: 0}, nil
case err != nil: case err != nil:
fmt.Printf("File %s decode returned error: %s\n", path, err) fmt.Printf("File %s decode returned error: %s\n", path, err)
return &Dimensions{}, err return &dimensions{}, err
} }
return &Dimensions{Width: decodedConfig.Width, Height: decodedConfig.Height}, nil return &dimensions{width: decodedConfig.Width, height: decodedConfig.Height}, nil
}
func init() {
format := Format{}
types.Register(format)
} }

View File

@ -2,7 +2,7 @@
Copyright © 2023 Seednode <seednode@seedno.de> Copyright © 2023 Seednode <seednode@seedno.de>
*/ */
package types package text
import ( import (
"errors" "errors"
@ -10,11 +10,13 @@ import (
"os" "os"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"seedno.de/seednode/roulette/types"
) )
type Text struct{} type Format struct{}
func (t Text) Css() string { func (t Format) Css() string {
var css strings.Builder var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`) css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
@ -25,11 +27,11 @@ func (t Text) Css() string {
return css.String() return css.String()
} }
func (t Text) Title(queryParams, fileUri, filePath, fileName, mime string) string { func (t Format) Title(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName) return fmt.Sprintf(`<title>%s</title>`, fileName)
} }
func (t Text) Body(queryParams, fileUri, filePath, fileName, mime string) string { func (t Format) Body(queryParams, fileUri, filePath, fileName, mime string) string {
body, err := os.ReadFile(filePath) body, err := os.ReadFile(filePath)
if err != nil { if err != nil {
body = []byte{} body = []byte{}
@ -40,7 +42,7 @@ func (t Text) Body(queryParams, fileUri, filePath, fileName, mime string) string
body) body)
} }
func (t Text) Extensions() map[string]string { func (t Format) Extensions() map[string]string {
return map[string]string{ return map[string]string{
`.css`: `text/css`, `.css`: `text/css`,
`.csv`: `text/csv`, `.csv`: `text/csv`,
@ -54,7 +56,7 @@ func (t Text) Extensions() map[string]string {
} }
} }
func (t Text) MimeTypes() []string { func (t Format) MimeTypes() []string {
return []string{ return []string{
`application/json`, `application/json`,
`application/xml`, `application/xml`,
@ -67,7 +69,7 @@ func (t Text) MimeTypes() []string {
} }
} }
func (t Text) Validate(filePath string) bool { func (t Format) Validate(filePath string) bool {
file, err := os.Open(filePath) file, err := os.Open(filePath)
switch { switch {
case errors.Is(err, os.ErrNotExist): case errors.Is(err, os.ErrNotExist):
@ -82,3 +84,9 @@ func (t Text) Validate(filePath string) bool {
return utf8.Valid(head) return utf8.Valid(head)
} }
func init() {
format := Format{}
types.Register(format)
}

View File

@ -9,8 +9,15 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings"
) )
var SupportedFormats = &Types{
Extensions: make(map[string]string),
MimeTypes: make(map[string]Type),
}
type Type interface { type Type interface {
Css() string Css() string
Title(queryParams, fileUri, filePath, fileName, mime string) string Title(queryParams, fileUri, filePath, fileName, mime string) string
@ -74,3 +81,49 @@ func FileType(path string, registeredFormats *Types) (bool, Type, string, error)
return false, nil, "", nil return false, nil, "", nil
} }
func Register(t Type) {
SupportedFormats.Add(t)
}
func (t Types) GetExtensions() string {
var output strings.Builder
extensions := make([]string, len(t.Extensions))
i := 0
for k := range t.Extensions {
extensions[i] = k
i++
}
slices.Sort(extensions)
for _, v := range extensions {
output.WriteString(v + "\n")
}
return output.String()
}
func (t Types) GetMimeTypes() string {
var output strings.Builder
mimeTypes := make([]string, len(t.MimeTypes))
i := 0
for k := range t.MimeTypes {
mimeTypes[i] = k
i++
}
slices.Sort(mimeTypes)
for _, v := range mimeTypes {
output.WriteString(v + "\n")
}
return output.String()
}

View File

@ -2,16 +2,18 @@
Copyright © 2023 Seednode <seednode@seedno.de> Copyright © 2023 Seednode <seednode@seedno.de>
*/ */
package types package video
import ( import (
"fmt" "fmt"
"strings" "strings"
"seedno.de/seednode/roulette/types"
) )
type Video struct{} type Format struct{}
func (t Video) Css() string { func (t Format) Css() string {
var css strings.Builder var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`) css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
@ -22,11 +24,11 @@ func (t Video) Css() string {
return css.String() return css.String()
} }
func (t Video) Title(queryParams, fileUri, filePath, fileName, mime string) string { func (t Format) Title(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName) return fmt.Sprintf(`<title>%s</title>`, fileName)
} }
func (t Video) Body(queryParams, fileUri, filePath, fileName, mime string) string { func (t Format) Body(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<a href="/%s"><video controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the video tag.</video></a>`, return fmt.Sprintf(`<a href="/%s"><video controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the video tag.</video></a>`,
queryParams, queryParams,
fileUri, fileUri,
@ -34,7 +36,7 @@ func (t Video) Body(queryParams, fileUri, filePath, fileName, mime string) strin
fileName) fileName)
} }
func (t Video) Extensions() map[string]string { func (t Format) Extensions() map[string]string {
return map[string]string{ return map[string]string{
`.mp4`: `video/mp4`, `.mp4`: `video/mp4`,
`.ogm`: `video/ogg`, `.ogm`: `video/ogg`,
@ -43,7 +45,7 @@ func (t Video) Extensions() map[string]string {
} }
} }
func (t Video) MimeTypes() []string { func (t Format) MimeTypes() []string {
return []string{ return []string{
`video/mp4`, `video/mp4`,
`video/ogg`, `video/ogg`,
@ -51,6 +53,12 @@ func (t Video) MimeTypes() []string {
} }
} }
func (t Video) Validate(filePath string) bool { func (t Format) Validate(filePath string) bool {
return true return true
} }
func init() {
format := Format{}
types.Register(format)
}