Compare commits

..

9 Commits

17 changed files with 616 additions and 452 deletions

View File

@ -78,9 +78,13 @@ 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 the `-i|--index` flag is passed, two additional endpoints—`/html` and `/json`are registered.
If the `-i|--index` flag is passed, four additional endpoints are registered.
When accessed, these endpoints return the contents of the index, in HTML and JSON formats respectively. This can prove useful when confirming whether the index is generated successfully, or whether a given file is in the index.
The first of these—`/html` and `/json`—return the contents of the index, in HTML and JSON formats respectively.
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.
## Statistics
@ -113,13 +117,13 @@ Flags:
-c, --cache generate directory cache at startup
--cache-file string path to optional persistent cache file
-f, --filter enable filtering
--flash enable support for shockwave flash files (via ruffle.rs) (default true)
--flash enable support for shockwave flash files (via ruffle.rs)
-h, --help help for roulette
--images enable support for image files (default true)
--images enable support for image files
-i, --index expose index endpoints
--maximum-files uint skip directories with file counts above this value (default 18446744073709551615)
--minimum-files uint skip directories with file counts below this value (default 1)
--page-length uint pagination length for statistics and debug pages
--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)
--page-length uint32 pagination length for statistics and debug pages
-p, --port uint16 port to listen on (default 8080)
--profile register net/http/pprof handlers
-r, --recursive recurse into subdirectories

View File

@ -20,7 +20,7 @@ import (
"sync/atomic"
"time"
"seedno.de/seednode/roulette/formats"
"seedno.de/seednode/roulette/types"
)
type maxConcurrency int
@ -53,10 +53,10 @@ func (f *Files) Append(directory, path string) {
}
type ScanStats struct {
filesMatched atomic.Uint64
filesSkipped atomic.Uint64
directoriesMatched atomic.Uint64
directoriesSkipped atomic.Uint64
filesMatched atomic.Uint32
filesSkipped atomic.Uint32
directoriesMatched atomic.Uint32
directoriesSkipped atomic.Uint32
}
type Path struct {
@ -108,9 +108,9 @@ func preparePath(path string) string {
return MediaPrefix + path
}
func appendPath(directory, path string, files *Files, stats *ScanStats, registeredFormats *formats.SupportedFormats, shouldCache bool) error {
func appendPath(directory, path string, files *Files, stats *ScanStats, formats *types.Types, shouldCache bool) error {
if shouldCache {
registered, _, _, err := formats.FileType(path, registeredFormats)
registered, _, _, err := types.FileType(path, formats)
if err != nil {
return err
}
@ -127,7 +127,7 @@ func appendPath(directory, path string, files *Files, stats *ScanStats, register
return nil
}
func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats, types *formats.SupportedFormats) error {
func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats, formats *types.Types) error {
shouldCache := Cache && filters.IsEmpty()
absolutePath, err := filepath.Abs(path)
@ -158,7 +158,7 @@ func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats,
filename,
filters.includes[i],
) {
err := appendPath(directory, path, files, stats, types, shouldCache)
err := appendPath(directory, path, files, stats, formats, shouldCache)
if err != nil {
return err
}
@ -172,7 +172,7 @@ func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats,
return nil
}
err = appendPath(directory, path, files, stats, types, shouldCache)
err = appendPath(directory, path, files, stats, formats, shouldCache)
if err != nil {
return err
}
@ -180,8 +180,8 @@ func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats,
return nil
}
func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexes, index *FileIndex, registeredFormats *formats.SupportedFormats) (string, error) {
filePath, err := pickFile(paths, filters, sortOrder, index, registeredFormats)
func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexes, index *FileIndex, formats *types.Types) (string, error) {
filePath, err := pickFile(paths, filters, sortOrder, index, formats)
if err != nil {
return "", nil
}
@ -195,7 +195,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
switch {
case sortOrder == "asc":
filePath, err = tryExtensions(path, registeredFormats)
filePath, err = tryExtensions(path, formats)
if err != nil {
return "", err
}
@ -203,7 +203,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
for {
path.increment()
filePath, err = tryExtensions(path, registeredFormats)
filePath, err = tryExtensions(path, formats)
if err != nil {
return "", err
}
@ -211,7 +211,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
if filePath == "" {
path.decrement()
filePath, err = tryExtensions(path, registeredFormats)
filePath, err = tryExtensions(path, formats)
if err != nil {
return "", err
}
@ -224,7 +224,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
return filePath, nil
}
func nextFile(filePath, sortOrder string, Regexes *Regexes, registeredFormats *formats.SupportedFormats) (string, error) {
func nextFile(filePath, sortOrder string, Regexes *Regexes, formats *types.Types) (string, error) {
path, err := splitPath(filePath, Regexes)
if err != nil {
return "", err
@ -239,7 +239,7 @@ func nextFile(filePath, sortOrder string, Regexes *Regexes, registeredFormats *f
return "", nil
}
fileName, err := tryExtensions(path, registeredFormats)
fileName, err := tryExtensions(path, formats)
if err != nil {
return "", err
}
@ -269,13 +269,11 @@ func splitPath(path string, Regexes *Regexes) (*Path, error) {
return &p, nil
}
func tryExtensions(p *Path, registeredFormats *formats.SupportedFormats) (string, error) {
func tryExtensions(p *Path, formats *types.Types) (string, error) {
var fileName string
for _, format := range registeredFormats.Extensions {
for _, extension := range format.Extensions {
fileName = fmt.Sprintf("%s%.3d%s", p.base, p.number, extension)
}
for extension := range formats.Extensions {
fileName = fmt.Sprintf("%s%.3d%s", p.base, p.number, extension)
exists, err := fileExists(fileName)
if err != nil {
@ -326,7 +324,7 @@ func pathIsValid(filePath string, paths []string) bool {
}
}
func pathHasSupportedFiles(path string, registeredFormats *formats.SupportedFormats) (bool, error) {
func pathHasSupportedFiles(path string, formats *types.Types) (bool, error) {
hasRegisteredFiles := make(chan bool, 1)
err := filepath.WalkDir(path, func(p string, info os.DirEntry, err error) error {
@ -338,7 +336,7 @@ func pathHasSupportedFiles(path string, registeredFormats *formats.SupportedForm
case !Recursive && info.IsDir() && p != path:
return filepath.SkipDir
case !info.IsDir():
registered, _, _, err := formats.FileType(p, registeredFormats)
registered, _, _, err := types.FileType(p, formats)
if err != nil {
return err
}
@ -363,9 +361,9 @@ func pathHasSupportedFiles(path string, registeredFormats *formats.SupportedForm
}
}
func pathCount(path string) (uint64, uint64, error) {
var directories uint64 = 0
var files uint64 = 0
func pathCount(path string) (uint32, uint32, error) {
var directories uint32 = 0
var files uint32 = 0
nodes, err := os.ReadDir(path)
if err != nil {
@ -383,7 +381,7 @@ func pathCount(path string) (uint64, uint64, error) {
return files, directories, nil
}
func scanPath(path string, files *Files, filters *Filters, stats *ScanStats, concurrency *Concurrency, types *formats.SupportedFormats) error {
func scanPath(path string, files *Files, filters *Filters, stats *ScanStats, concurrency *Concurrency, formats *types.Types) error {
var wg sync.WaitGroup
err := filepath.WalkDir(path, func(p string, info os.DirEntry, err error) error {
@ -410,7 +408,7 @@ func scanPath(path string, files *Files, filters *Filters, stats *ScanStats, con
fmt.Println(err)
}
err = appendPaths(path, files, filters, stats, types)
err = appendPaths(path, files, filters, stats, formats)
if err != nil {
fmt.Println(err)
}
@ -444,7 +442,7 @@ func scanPath(path string, files *Files, filters *Filters, stats *ScanStats, con
return nil
}
func fileList(paths []string, filters *Filters, sort string, index *FileIndex, types *formats.SupportedFormats) ([]string, bool) {
func fileList(paths []string, filters *Filters, sort string, index *FileIndex, formats *types.Types) ([]string, bool) {
if Cache && filters.IsEmpty() && !index.IsEmpty() {
return index.Index(), true
}
@ -457,10 +455,10 @@ func fileList(paths []string, filters *Filters, sort string, index *FileIndex, t
}
stats := &ScanStats{
filesMatched: atomic.Uint64{},
filesSkipped: atomic.Uint64{},
directoriesMatched: atomic.Uint64{},
directoriesSkipped: atomic.Uint64{},
filesMatched: atomic.Uint32{},
filesSkipped: atomic.Uint32{},
directoriesMatched: atomic.Uint32{},
directoriesSkipped: atomic.Uint32{},
}
concurrency := &Concurrency{
@ -483,7 +481,7 @@ func fileList(paths []string, filters *Filters, sort string, index *FileIndex, t
wg.Done()
}()
err := scanPath(paths[i], files, filters, stats, concurrency, types)
err := scanPath(paths[i], files, filters, stats, concurrency, formats)
if err != nil {
fmt.Println(err)
}
@ -558,8 +556,8 @@ func prepareDirectories(files *Files, sort string) []string {
return directories
}
func pickFile(args []string, filters *Filters, sort string, index *FileIndex, registeredFormats *formats.SupportedFormats) (string, error) {
fileList, fromCache := fileList(args, filters, sort, index, registeredFormats)
func pickFile(args []string, filters *Filters, sort string, index *FileIndex, formats *types.Types) (string, error) {
fileList, fromCache := fileList(args, filters, sort, index, formats)
fileCount := len(fileList)
if fileCount < 1 {
@ -586,7 +584,7 @@ func pickFile(args []string, filters *Filters, sort string, index *FileIndex, re
filePath := fileList[val]
if !fromCache {
registered, _, _, err := formats.FileType(filePath, registeredFormats)
registered, _, _, err := types.FileType(filePath, formats)
if err != nil {
return "", err
}
@ -618,7 +616,7 @@ func normalizePath(path string) (string, error) {
return absolutePath, nil
}
func normalizePaths(args []string, types *formats.SupportedFormats) ([]string, error) {
func normalizePaths(args []string, formats *types.Types) ([]string, error) {
var paths []string
var pathList strings.Builder
@ -632,7 +630,7 @@ func normalizePaths(args []string, types *formats.SupportedFormats) ([]string, e
pathMatches := (args[i] == path)
hasSupportedFiles, err := pathHasSupportedFiles(path, types)
hasSupportedFiles, err := pathHasSupportedFiles(path, formats)
if err != nil {
return nil, err
}

View File

@ -11,6 +11,7 @@ import (
"io"
"net/http"
"os"
"slices"
"sort"
"strconv"
"strings"
@ -20,7 +21,7 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/klauspost/compress/zstd"
"github.com/yosssi/gohtml"
"seedno.de/seednode/roulette/formats"
"seedno.de/seednode/roulette/types"
)
type FileIndex struct {
@ -66,12 +67,12 @@ func (i *FileIndex) setIndex(val []string) {
i.mutex.Unlock()
}
func (i *FileIndex) generateCache(args []string, registeredFormats *formats.SupportedFormats) {
func (i *FileIndex) generateCache(args []string, formats *types.Types) {
i.mutex.Lock()
i.list = []string{}
i.mutex.Unlock()
fileList(args, &Filters{}, "", i, registeredFormats)
fileList(args, &Filters{}, "", i, formats)
if Cache && CacheFile != "" {
i.Export(CacheFile)
@ -138,9 +139,9 @@ func (i *FileIndex) Import(path string) error {
return nil
}
func serveCacheClear(args []string, index *FileIndex, registeredFormats *formats.SupportedFormats) httprouter.Handle {
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, registeredFormats)
index.generateCache(args, formats)
w.Header().Set("Content-Type", "text/plain")
@ -316,3 +317,79 @@ func serveIndexJson(args []string, index *FileIndex) httprouter.Handle {
}
}
}
func serveExtensions(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()
var output strings.Builder
extensions := make([]string, len(formats.Extensions))
i := 0
for k := range formats.Extensions {
extensions[i] = k
i++
}
slices.Sort(extensions)
for _, v := range extensions {
output.WriteString(v + "\n")
}
response := []byte(output.String())
w.Write(response)
if Verbose {
fmt.Printf("%s | Served registered extensions list (%s) to %s in %s\n",
startTime.Format(LogDate),
humanReadableSize(len(response)),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}
func serveMimeTypes(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()
var output strings.Builder
mimeTypes := make([]string, len(formats.MimeTypes))
i := 0
for k := range formats.MimeTypes {
mimeTypes[i] = k
i++
}
slices.Sort(mimeTypes)
for _, v := range mimeTypes {
output.WriteString(v + "\n")
}
response := []byte(output.String())
w.Write(response)
if Verbose {
fmt.Printf("%s | Served registered MIME types list (%s) to %s in %s\n",
startTime.Format(LogDate),
humanReadableSize(len(response)),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}

View File

@ -12,7 +12,7 @@ import (
)
const (
ReleaseVersion string = "0.70.3"
ReleaseVersion string = "0.75.0"
)
var (
@ -25,9 +25,9 @@ var (
Flash bool
Images bool
Index bool
MaximumFileCount uint64
MinimumFileCount uint64
PageLength uint64
MaximumFileCount uint32
MinimumFileCount uint32
PageLength uint32
Port uint16
Profile bool
Recursive bool
@ -45,13 +45,7 @@ var (
Use: "roulette <path> [path]...",
Short: "Serves random media from the specified directories.",
Args: cobra.MinimumNArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
// enable image support if no other flags are passed, to retain backwards compatibility
// to be replaced with MarkFlagsOneRequired on next spf13/cobra update
if !(All || Audio || Flash || Images || Text || Videos) {
Images = true
}
PreRunE: func(cmd *cobra.Command, args []string) error {
if Index {
cmd.MarkFlagRequired("cache")
}
@ -59,9 +53,11 @@ var (
if RefreshInterval != "" {
interval, err := time.ParseDuration(RefreshInterval)
if err != nil || interval < 500*time.Millisecond {
log.Fatal(ErrIncorrectRefreshInterval)
return ErrIncorrectRefreshInterval
}
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
err := ServePage(args)
@ -91,9 +87,9 @@ func init() {
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().BoolVarP(&Index, "index", "i", false, "expose index endpoints")
rootCmd.Flags().Uint64Var(&MaximumFileCount, "maximum-files", 1<<64-1, "skip directories with file counts above this value")
rootCmd.Flags().Uint64Var(&MinimumFileCount, "minimum-files", 1, "skip directories with file counts below this value")
rootCmd.Flags().Uint64Var(&PageLength, "page-length", 0, "pagination length for statistics and debug pages")
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(&PageLength, "page-length", 0, "pagination length for statistics and debug pages")
rootCmd.Flags().Uint16VarP(&Port, "port", "p", 8080, "port to listen on")
rootCmd.Flags().BoolVar(&Profile, "profile", false, "register net/http/pprof handlers")
rootCmd.Flags().BoolVarP(&Recursive, "recursive", "r", false, "recurse into subdirectories")

View File

@ -26,7 +26,7 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/yosssi/gohtml"
"seedno.de/seednode/roulette/formats"
"seedno.de/seednode/roulette/types"
)
const (
@ -127,7 +127,7 @@ func serveStaticFile(paths []string, stats *ServeStats, index *FileIndex) httpro
}
}
func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFormats *formats.SupportedFormats) httprouter.Handle {
func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, formats *types.Types) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
refererUri, err := stripQueryParams(refererToUri(r.Referer()))
if err != nil {
@ -152,7 +152,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFor
var filePath string
if refererUri != "" {
filePath, err = nextFile(strippedRefererUri, sortOrder, Regexes, registeredFormats)
filePath, err = nextFile(strippedRefererUri, sortOrder, Regexes, formats)
if err != nil {
fmt.Println(err)
@ -174,7 +174,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFor
break loop
}
filePath, err = newFile(paths, filters, sortOrder, Regexes, index, registeredFormats)
filePath, err = newFile(paths, filters, sortOrder, Regexes, index, formats)
switch {
case err != nil && err == ErrNoMediaFound:
notFound(w, r, filePath)
@ -200,7 +200,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFor
}
}
func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, registeredFormats *formats.SupportedFormats) httprouter.Handle {
func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, formats *types.Types) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
filters := &Filters{
includes: splitQueryParams(r.URL.Query().Get("include"), Regexes),
@ -229,7 +229,7 @@ func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, registeredFo
return
}
registered, fileType, mimeType, err := formats.FileType(filePath, registeredFormats)
registered, fileType, mimeType, err := types.FileType(filePath, formats)
if err != nil {
fmt.Println(err)
@ -257,12 +257,7 @@ func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, registeredFo
var htmlBody strings.Builder
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
htmlBody.WriteString(FaviconHtml)
htmlBody.WriteString(`<style>html,body{margin:0;padding:0;height:100%;}`)
htmlBody.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
htmlBody.WriteString(`img{margin:auto;display:block;max-width:97%;max-height:97%;object-fit:scale-down;`)
htmlBody.WriteString(`position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}`)
htmlBody.WriteString(fileType.Css)
htmlBody.WriteString(`</style>`)
htmlBody.WriteString(fmt.Sprintf(`<style>%s</style>`, fileType.Css()))
htmlBody.WriteString((fileType.Title(queryParams, fileUri, filePath, fileName, mimeType)))
htmlBody.WriteString(`</head><body>`)
if refreshInterval != "0ms" {
@ -316,32 +311,34 @@ func ServePage(args []string) error {
mux := httprouter.New()
registeredFormats := &formats.SupportedFormats{
Extensions: make(map[string]*formats.SupportedFormat),
MimeTypes: make(map[string]*formats.SupportedFormat),
formats := &types.Types{
Extensions: make(map[string]string),
MimeTypes: make(map[string]types.Type),
}
if Audio || All {
registeredFormats.Add(formats.RegisterAudioFormats())
formats.Add(types.Audio{})
}
if Flash || All {
registeredFormats.Add(formats.RegisterFlashFormats())
}
if Images || All {
registeredFormats.Add(formats.RegisterImageFormats())
formats.Add(types.Flash{})
}
if Text || All {
registeredFormats.Add(formats.RegisterTextFormats())
formats.Add(types.Text{})
}
if Videos || All {
registeredFormats.Add(formats.RegisterVideoFormats())
formats.Add(types.Video{})
}
paths, err := normalizePaths(args, registeredFormats)
// enable image support if no other flags are passed, to retain backwards compatibility
// to be replaced with rootCmd.MarkFlagsOneRequired on next spf13/cobra update
if Images || All || len(formats.Extensions) == 0 {
formats.Add(types.Images{})
}
paths, err := normalizePaths(args, formats)
if err != nil {
return err
}
@ -382,13 +379,13 @@ func ServePage(args []string) error {
mux.PanicHandler = serverErrorHandler()
mux.GET("/", serveRoot(paths, regexes, index, registeredFormats))
mux.GET("/", serveRoot(paths, regexes, index, formats))
mux.GET("/favicons/*favicon", serveFavicons())
mux.GET("/favicon.ico", serveFavicons())
mux.GET(MediaPrefix+"/*media", serveMedia(paths, regexes, index, registeredFormats))
mux.GET(MediaPrefix+"/*media", serveMedia(paths, regexes, index, formats))
mux.GET(SourcePrefix+"/*static", serveStaticFile(paths, stats, index))
@ -405,10 +402,10 @@ func ServePage(args []string) error {
}
if !skipIndex {
index.generateCache(args, registeredFormats)
index.generateCache(args, formats)
}
mux.GET("/clear_cache", serveCacheClear(args, index, registeredFormats))
mux.GET("/clear_cache", serveCacheClear(args, index, formats))
}
if Index {
@ -421,6 +418,10 @@ func ServePage(args []string) error {
if PageLength != 0 {
mux.GET("/json/:page", serveIndexJson(args, index))
}
mux.GET("/extensions", serveExtensions(formats))
mux.GET("/mime_types", serveMimeTypes(formats))
}
if Profile {

View File

@ -1,39 +0,0 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package formats
import (
"fmt"
)
func RegisterAudioFormats() *SupportedFormat {
return &SupportedFormat{
Css: ``,
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName)
},
Body: func(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>`,
queryParams,
fileUri,
mime,
fileName)
},
Extensions: []string{
`.mp3`,
`.ogg`,
`.oga`,
`.wav`,
},
MimeTypes: []string{
`audio/mpeg`,
`audio/ogg`,
`audio/wav`,
},
Validate: func(filePath string) bool {
return true
},
}
}

View File

@ -1,36 +0,0 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package formats
import (
"fmt"
"strings"
)
func RegisterFlashFormats() *SupportedFormat {
return &SupportedFormat{
Css: ``,
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName)
},
Body: func(queryParams, fileUri, filePath, fileName, mime string) string {
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(`<br /><button onclick=\"window.location.href = '/%s';\">Next</button>`, queryParams))
return html.String()
},
Extensions: []string{
`.swf`,
},
MimeTypes: []string{
`application/x-shockwave-flash`,
},
Validate: func(filePath string) bool {
return true
},
}
}

View File

@ -1,106 +0,0 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package formats
import (
"errors"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/webp"
)
type Dimensions struct {
Width int
Height int
}
func RegisterImageFormats() *SupportedFormat {
return &SupportedFormat{
Css: ``,
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
dimensions, err := ImageDimensions(filePath)
if err != nil {
fmt.Println(err)
}
return fmt.Sprintf(`<title>%s (%dx%d)</title>`,
fileName,
dimensions.Width,
dimensions.Height)
},
Body: func(queryParams, fileUri, filePath, fileName, mime string) string {
dimensions, err := ImageDimensions(filePath)
if err != nil {
fmt.Println(err)
}
return fmt.Sprintf(`<a href="/%s"><img src="%s" width="%d" height="%d" type="%s" alt="Roulette selected: %s"></a>`,
queryParams,
fileUri,
dimensions.Width,
dimensions.Height,
mime,
fileName)
},
Extensions: []string{
`.apng`,
`.avif`,
`.bmp`,
`.gif`,
`.jpg`,
`.jpeg`,
`.jfif`,
`.pjp`,
`.pjpeg`,
`.png`,
`.svg`,
`.webp`,
},
MimeTypes: []string{
`image/apng`,
`image/avif`,
`image/bmp`,
`image/gif`,
`image/jpeg`,
`image/png`,
`image/svg+xml`,
`image/webp`,
},
Validate: func(filePath string) bool {
return true
},
}
}
func ImageDimensions(path string) (*Dimensions, error) {
file, err := os.Open(path)
switch {
case errors.Is(err, os.ErrNotExist):
fmt.Printf("File %s does not exist\n", path)
return &Dimensions{}, nil
case err != nil:
fmt.Printf("File %s open returned error: %s\n", path, err)
return &Dimensions{}, err
}
defer file.Close()
decodedConfig, _, err := image.DecodeConfig(file)
switch {
case errors.Is(err, image.ErrFormat):
fmt.Printf("File %s has invalid image format\n", path)
return &Dimensions{Width: 0, Height: 0}, nil
case err != nil:
fmt.Printf("File %s decode returned error: %s\n", path, err)
return &Dimensions{}, err
}
return &Dimensions{Width: decodedConfig.Width, Height: decodedConfig.Height}, nil
}

View File

@ -1,65 +0,0 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package formats
import (
"errors"
"fmt"
"os"
"unicode/utf8"
)
func RegisterTextFormats() *SupportedFormat {
return &SupportedFormat{
Css: `pre{margin:.5rem;}`,
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName)
},
Body: func(queryParams, fileUri, filePath, fileName, mime string) string {
body, err := os.ReadFile(filePath)
if err != nil {
body = []byte{}
}
return fmt.Sprintf(`<a href="/%s"><pre>%s</pre></a>`,
queryParams,
body)
},
Extensions: []string{
`.css`,
`.csv`,
`.html`,
`.js`,
`.json`,
`.md`,
`.txt`,
`.xml`,
},
MimeTypes: []string{
`application/json`,
`application/xml`,
`text/css`,
`text/csv`,
`text/javascript`,
`text/plain`,
`text/plain; charset=utf-8`,
},
Validate: func(path string) bool {
file, err := os.Open(path)
switch {
case errors.Is(err, os.ErrNotExist):
return false
case err != nil:
return false
}
defer file.Close()
head := make([]byte, 512)
file.Read(head)
return utf8.Valid(head)
},
}
}

View File

@ -1,72 +0,0 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package formats
import (
"errors"
"net/http"
"os"
"path/filepath"
)
type SupportedFormat struct {
Css string
Title func(queryParams, fileUri, filePath, fileName, mime string) string
Body func(queryParams, fileUri, filePath, fileName, mime string) string
Extensions []string
MimeTypes []string
Validate func(filePath string) bool
}
type SupportedFormats struct {
Extensions map[string]*SupportedFormat
MimeTypes map[string]*SupportedFormat
}
func (s *SupportedFormats) Add(t *SupportedFormat) {
for _, v := range t.Extensions {
_, exists := s.Extensions[v]
if !exists {
s.Extensions[v] = t
}
}
for _, v := range t.MimeTypes {
_, exists := s.Extensions[v]
if !exists {
s.MimeTypes[v] = t
}
}
}
func FileType(path string, registeredFormats *SupportedFormats) (bool, *SupportedFormat, string, error) {
file, err := os.Open(path)
switch {
case errors.Is(err, os.ErrNotExist):
return false, nil, "", nil
case err != nil:
return false, nil, "", err
}
defer file.Close()
head := make([]byte, 512)
file.Read(head)
mimeType := http.DetectContentType(head)
// try identifying files by mime types first
fileType, exists := registeredFormats.MimeTypes[mimeType]
if exists {
return fileType.Validate(path), fileType, mimeType, nil
}
// if mime type detection fails, use the file extension
fileType, exists = registeredFormats.Extensions[filepath.Ext(path)]
if exists {
return fileType.Validate(path), fileType, mimeType, nil
}
return false, nil, "", nil
}

View File

@ -1,39 +0,0 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package formats
import (
"fmt"
)
func RegisterVideoFormats() *SupportedFormat {
return &SupportedFormat{
Css: ``,
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName)
},
Body: func(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>`,
queryParams,
fileUri,
mime,
fileName)
},
Extensions: []string{
`.mp4`,
`.ogm`,
`.ogv`,
`.webm`,
},
MimeTypes: []string{
`video/mp4`,
`video/ogg`,
`video/webm`,
},
Validate: func(filePath string) bool {
return true
},
}
}

56
types/audio.go Normal file
View File

@ -0,0 +1,56 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package types
import (
"fmt"
"strings"
)
type Audio struct{}
func (t Audio) Css() string {
var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
return css.String()
}
func (t Audio) Title(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName)
}
func (t Audio) 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>`,
queryParams,
fileUri,
mime,
fileName)
}
func (t Audio) Extensions() map[string]string {
return map[string]string{
`.mp3`: `audio/mpeg`,
`.ogg`: `audio/ogg`,
`.oga`: `audio/ogg`,
}
}
func (t Audio) MimeTypes() []string {
return []string{
`application/ogg`,
`audio/mp3`,
`audio/mpeg`,
`audio/mpeg3`,
`audio/ogg`,
`audio/x-mpeg-3`,
}
}
func (t Audio) Validate(filePath string) bool {
return true
}

50
types/flash.go Normal file
View File

@ -0,0 +1,50 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package types
import (
"fmt"
"strings"
)
type Flash struct{}
func (t Flash) Css() string {
var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
return css.String()
}
func (t Flash) Title(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName)
}
func (t Flash) Body(queryParams, fileUri, filePath, fileName, mime string) string {
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(`<br /><button onclick="window.location.href = '/%s';">Next</button>`, queryParams))
return html.String()
}
func (t Flash) Extensions() map[string]string {
return map[string]string{
`.swf`: `application/x-shockwave-flash`,
}
}
func (t Flash) MimeTypes() []string {
return []string{
`application/x-shockwave-flash`,
}
}
func (t Flash) Validate(filePath string) bool {
return true
}

123
types/images.go Normal file
View File

@ -0,0 +1,123 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package types
import (
"errors"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
"strings"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/webp"
)
type Dimensions struct {
Width int
Height int
}
type Images struct{}
func (t Images) Css() string {
var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
css.WriteString(`img{margin:auto;display:block;max-width:97%;max-height:97%;`)
css.WriteString(`object-fit:scale-down;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}`)
return css.String()
}
func (t Images) Title(queryParams, fileUri, filePath, fileName, mime string) string {
dimensions, err := ImageDimensions(filePath)
if err != nil {
fmt.Println(err)
}
return fmt.Sprintf(`<title>%s (%dx%d)</title>`,
fileName,
dimensions.Width,
dimensions.Height)
}
func (t Images) Body(queryParams, fileUri, filePath, fileName, mime string) string {
dimensions, err := ImageDimensions(filePath)
if err != nil {
fmt.Println(err)
}
return fmt.Sprintf(`<a href="/%s"><img src="%s" width="%d" height="%d" type="%s" alt="Roulette selected: %s"></a>`,
queryParams,
fileUri,
dimensions.Width,
dimensions.Height,
mime,
fileName)
}
func (t Images) Extensions() map[string]string {
return map[string]string{
`.apng`: `image/apng`,
`.avif`: `image/avif`,
`.bmp`: `image/bmp`,
`.gif`: `image/gif`,
`.jpg`: `image/jpeg`,
`.jpeg`: `image/jpeg`,
`.jfif`: `image/jpeg`,
`.pjp`: `image/jpeg`,
`.pjpeg`: `image/jpeg`,
`.png`: `image/png`,
`.svg`: `image/svg+xml`,
`.webp`: `image/webp`,
}
}
func (t Images) MimeTypes() []string {
return []string{
`image/apng`,
`image/avif`,
`image/bmp`,
`image/gif`,
`image/jpeg`,
`image/png`,
`image/svg+xml`,
`image/webp`,
}
}
func (t Images) Validate(filePath string) bool {
return true
}
func ImageDimensions(path string) (*Dimensions, error) {
file, err := os.Open(path)
switch {
case errors.Is(err, os.ErrNotExist):
fmt.Printf("File %s does not exist\n", path)
return &Dimensions{}, nil
case err != nil:
fmt.Printf("File %s open returned error: %s\n", path, err)
return &Dimensions{}, err
}
defer file.Close()
decodedConfig, _, err := image.DecodeConfig(file)
switch {
case errors.Is(err, image.ErrFormat):
fmt.Printf("File %s has invalid image format\n", path)
return &Dimensions{Width: 0, Height: 0}, nil
case err != nil:
fmt.Printf("File %s decode returned error: %s\n", path, err)
return &Dimensions{}, err
}
return &Dimensions{Width: decodedConfig.Width, Height: decodedConfig.Height}, nil
}

84
types/text.go Normal file
View File

@ -0,0 +1,84 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package types
import (
"errors"
"fmt"
"os"
"strings"
"unicode/utf8"
)
type Text struct{}
func (t Text) Css() string {
var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;overflow:hidden;}`)
css.WriteString(`textarea{border:none;caret-color:transparent;outline:none;margin:.5rem;`)
css.WriteString(`height:99%;width:99%;white-space:pre;overflow:auto;}`)
return css.String()
}
func (t Text) Title(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName)
}
func (t Text) Body(queryParams, fileUri, filePath, fileName, mime string) string {
body, err := os.ReadFile(filePath)
if err != nil {
body = []byte{}
}
return fmt.Sprintf(`<a href="/%s"><textarea autofocus readonly>%s</textarea></a>`,
queryParams,
body)
}
func (t Text) Extensions() map[string]string {
return map[string]string{
`.css`: `text/css`,
`.csv`: `text/csv`,
`.htm`: `text/html`,
`.html`: `text/html`,
`.js`: `text/javascript`,
`.json`: `application/json`,
`.md`: `text/markdown`,
`.txt`: `text/plain`,
`.xml`: `application/xml`,
}
}
func (t Text) MimeTypes() []string {
return []string{
`application/json`,
`application/xml`,
`text/css`,
`text/csv`,
`text/html`,
`text/javascript`,
`text/plain`,
`text/plain; charset=utf-8`,
}
}
func (t Text) Validate(filePath string) bool {
file, err := os.Open(filePath)
switch {
case errors.Is(err, os.ErrNotExist):
return false
case err != nil:
return false
}
defer file.Close()
head := make([]byte, 512)
file.Read(head)
return utf8.Valid(head)
}

76
types/types.go Normal file
View File

@ -0,0 +1,76 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package types
import (
"errors"
"net/http"
"os"
"path/filepath"
)
type Type interface {
Css() string
Title(queryParams, fileUri, filePath, fileName, mime string) string
Body(queryParams, fileUri, filePath, fileName, mime string) string
Extensions() map[string]string
MimeTypes() []string
Validate(filePath string) bool
}
type Types struct {
Extensions map[string]string
MimeTypes map[string]Type
}
func (s *Types) Add(t Type) {
for k, v := range t.Extensions() {
_, exists := s.Extensions[k]
if !exists {
s.Extensions[k] = v
}
}
for _, v := range t.MimeTypes() {
_, exists := s.Extensions[v]
if !exists {
s.MimeTypes[v] = t
}
}
}
func FileType(path string, registeredFormats *Types) (bool, Type, string, error) {
file, err := os.Open(path)
switch {
case errors.Is(err, os.ErrNotExist):
return false, nil, "", nil
case err != nil:
return false, nil, "", err
}
defer file.Close()
head := make([]byte, 512)
file.Read(head)
mimeType := http.DetectContentType(head)
// try identifying files by mime types first
fileType, exists := registeredFormats.MimeTypes[mimeType]
if exists {
return fileType.Validate(path), fileType, mimeType, nil
}
// if mime type detection fails, use the file extension
mimeType, exists = registeredFormats.Extensions[filepath.Ext(path)]
if exists {
fileType, exists := registeredFormats.MimeTypes[mimeType]
if exists {
return fileType.Validate(path), fileType, mimeType, nil
}
}
return false, nil, "", nil
}

56
types/video.go Normal file
View File

@ -0,0 +1,56 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package types
import (
"fmt"
"strings"
)
type Video struct{}
func (t Video) Css() string {
var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
css.WriteString(`video{margin:auto;display:block;max-width:97%;max-height:97%;`)
css.WriteString(`object-fit:scale-down;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}`)
return css.String()
}
func (t Video) Title(queryParams, fileUri, filePath, fileName, mime string) string {
return fmt.Sprintf(`<title>%s</title>`, fileName)
}
func (t Video) 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>`,
queryParams,
fileUri,
mime,
fileName)
}
func (t Video) Extensions() map[string]string {
return map[string]string{
`.mp4`: `video/mp4`,
`.ogm`: `video/ogg`,
`.ogv`: `video/ogg`,
`.webm`: `video/webm`,
}
}
func (t Video) MimeTypes() []string {
return []string{
`video/mp4`,
`video/ogg`,
`video/webm`,
}
}
func (t Video) Validate(filePath string) bool {
return true
}