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 `--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 ## Statistics
@ -113,13 +117,13 @@ Flags:
-c, --cache generate directory cache at startup -c, --cache generate directory cache at startup
--cache-file string path to optional persistent cache file --cache-file string path to optional persistent cache file
-f, --filter enable filtering -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 -h, --help help for roulette
--images enable support for image files (default true) --images enable support for image files
-i, --index expose index endpoints -i, --index expose index endpoints
--maximum-files uint skip directories with file counts above this value (default 18446744073709551615) --maximum-files uint32 skip directories with file counts above this value (default 4294967295)
--minimum-files uint 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 uint pagination length for statistics and debug pages --page-length uint32 pagination length for statistics and debug pages
-p, --port uint16 port to listen on (default 8080) -p, --port uint16 port to listen on (default 8080)
--profile register net/http/pprof handlers --profile register net/http/pprof handlers
-r, --recursive recurse into subdirectories -r, --recursive recurse into subdirectories

View File

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

View File

@ -11,6 +11,7 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -20,7 +21,7 @@ import (
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
"github.com/yosssi/gohtml" "github.com/yosssi/gohtml"
"seedno.de/seednode/roulette/formats" "seedno.de/seednode/roulette/types"
) )
type FileIndex struct { type FileIndex struct {
@ -66,12 +67,12 @@ func (i *FileIndex) setIndex(val []string) {
i.mutex.Unlock() 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.mutex.Lock()
i.list = []string{} i.list = []string{}
i.mutex.Unlock() i.mutex.Unlock()
fileList(args, &Filters{}, "", i, registeredFormats) fileList(args, &Filters{}, "", i, formats)
if Cache && CacheFile != "" { if Cache && CacheFile != "" {
i.Export(CacheFile) i.Export(CacheFile)
@ -138,9 +139,9 @@ func (i *FileIndex) Import(path string) error {
return nil 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) { 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") 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 ( const (
ReleaseVersion string = "0.70.3" ReleaseVersion string = "0.75.0"
) )
var ( var (
@ -25,9 +25,9 @@ var (
Flash bool Flash bool
Images bool Images bool
Index bool Index bool
MaximumFileCount uint64 MaximumFileCount uint32
MinimumFileCount uint64 MinimumFileCount uint32
PageLength uint64 PageLength uint32
Port uint16 Port uint16
Profile bool Profile bool
Recursive bool Recursive bool
@ -45,13 +45,7 @@ var (
Use: "roulette <path> [path]...", Use: "roulette <path> [path]...",
Short: "Serves random media from the specified directories.", Short: "Serves random media from the specified directories.",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
PreRun: func(cmd *cobra.Command, args []string) { PreRunE: func(cmd *cobra.Command, args []string) error {
// 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
}
if Index { if Index {
cmd.MarkFlagRequired("cache") cmd.MarkFlagRequired("cache")
} }
@ -59,9 +53,11 @@ var (
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 {
log.Fatal(ErrIncorrectRefreshInterval) return ErrIncorrectRefreshInterval
} }
} }
return nil
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
err := ServePage(args) 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(&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(&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().Uint32Var(&MaximumFileCount, "maximum-files", 1<<32-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().Uint32Var(&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(&PageLength, "page-length", 0, "pagination length for statistics and debug pages")
rootCmd.Flags().Uint16VarP(&Port, "port", "p", 8080, "port to listen on") rootCmd.Flags().Uint16VarP(&Port, "port", "p", 8080, "port to listen on")
rootCmd.Flags().BoolVar(&Profile, "profile", false, "register net/http/pprof handlers") rootCmd.Flags().BoolVar(&Profile, "profile", false, "register net/http/pprof handlers")
rootCmd.Flags().BoolVarP(&Recursive, "recursive", "r", false, "recurse into subdirectories") rootCmd.Flags().BoolVarP(&Recursive, "recursive", "r", false, "recurse into subdirectories")

View File

@ -26,7 +26,7 @@ import (
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/yosssi/gohtml" "github.com/yosssi/gohtml"
"seedno.de/seednode/roulette/formats" "seedno.de/seednode/roulette/types"
) )
const ( 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) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
refererUri, err := stripQueryParams(refererToUri(r.Referer())) refererUri, err := stripQueryParams(refererToUri(r.Referer()))
if err != nil { if err != nil {
@ -152,7 +152,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFor
var filePath string var filePath string
if refererUri != "" { if refererUri != "" {
filePath, err = nextFile(strippedRefererUri, sortOrder, Regexes, registeredFormats) filePath, err = nextFile(strippedRefererUri, sortOrder, Regexes, formats)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -174,7 +174,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFor
break loop break loop
} }
filePath, err = newFile(paths, filters, sortOrder, Regexes, index, registeredFormats) filePath, err = newFile(paths, filters, sortOrder, Regexes, index, formats)
switch { switch {
case err != nil && err == ErrNoMediaFound: case err != nil && err == ErrNoMediaFound:
notFound(w, r, filePath) 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) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
filters := &Filters{ filters := &Filters{
includes: splitQueryParams(r.URL.Query().Get("include"), Regexes), includes: splitQueryParams(r.URL.Query().Get("include"), Regexes),
@ -229,7 +229,7 @@ func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, registeredFo
return return
} }
registered, fileType, mimeType, err := formats.FileType(filePath, registeredFormats) registered, fileType, mimeType, err := types.FileType(filePath, formats)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -257,12 +257,7 @@ func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, registeredFo
var htmlBody strings.Builder var htmlBody strings.Builder
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`) htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
htmlBody.WriteString(FaviconHtml) htmlBody.WriteString(FaviconHtml)
htmlBody.WriteString(`<style>html,body{margin:0;padding:0;height:100%;}`) htmlBody.WriteString(fmt.Sprintf(`<style>%s</style>`, fileType.Css()))
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((fileType.Title(queryParams, fileUri, filePath, fileName, mimeType))) htmlBody.WriteString((fileType.Title(queryParams, fileUri, filePath, fileName, mimeType)))
htmlBody.WriteString(`</head><body>`) htmlBody.WriteString(`</head><body>`)
if refreshInterval != "0ms" { if refreshInterval != "0ms" {
@ -316,32 +311,34 @@ func ServePage(args []string) error {
mux := httprouter.New() mux := httprouter.New()
registeredFormats := &formats.SupportedFormats{ formats := &types.Types{
Extensions: make(map[string]*formats.SupportedFormat), Extensions: make(map[string]string),
MimeTypes: make(map[string]*formats.SupportedFormat), MimeTypes: make(map[string]types.Type),
} }
if Audio || All { if Audio || All {
registeredFormats.Add(formats.RegisterAudioFormats()) formats.Add(types.Audio{})
} }
if Flash || All { if Flash || All {
registeredFormats.Add(formats.RegisterFlashFormats()) formats.Add(types.Flash{})
}
if Images || All {
registeredFormats.Add(formats.RegisterImageFormats())
} }
if Text || All { if Text || All {
registeredFormats.Add(formats.RegisterTextFormats()) formats.Add(types.Text{})
} }
if Videos || All { 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 { if err != nil {
return err return err
} }
@ -382,13 +379,13 @@ func ServePage(args []string) error {
mux.PanicHandler = serverErrorHandler() mux.PanicHandler = serverErrorHandler()
mux.GET("/", serveRoot(paths, regexes, index, registeredFormats)) mux.GET("/", serveRoot(paths, regexes, index, formats))
mux.GET("/favicons/*favicon", serveFavicons()) mux.GET("/favicons/*favicon", serveFavicons())
mux.GET("/favicon.ico", 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)) mux.GET(SourcePrefix+"/*static", serveStaticFile(paths, stats, index))
@ -405,10 +402,10 @@ func ServePage(args []string) error {
} }
if !skipIndex { 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 { if Index {
@ -421,6 +418,10 @@ 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("/mime_types", serveMimeTypes(formats))
} }
if Profile { 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
}