Compare commits

..

No commits in common. "6fea978459faf555c03c18099502055b4b1b85cb" and "7942ea85b515076145771560552fbc2a982bbf9c" have entirely different histories.

14 changed files with 361 additions and 388 deletions

View File

@ -78,16 +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, two additional endpoints—`/html` and `/json`—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.
## Statistics ## Statistics
If the `--stats` flag is passed, an additional endpoint, `/stats`, is registered. 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.
## Debug
If the `-d|--debug` flag is passed, two additional endpoints—`/html` and `/json`—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.
## 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.
@ -112,11 +113,10 @@ Flags:
-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
--cache-file string path to optional persistent cache file --cache-file string path to optional persistent cache file
-d, --debug expose debug endpoint
-f, --filter enable filtering -f, --filter enable filtering
--flash enable support for shockwave flash files (via ruffle.rs) (default true)
-h, --help help for roulette -h, --help help for roulette
--images enable support for image files (default true) --images enable support for image files (default true)
-i, --index expose index endpoints
--maximum-files uint skip directories with file counts above this value (default 18446744073709551615) --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) --minimum-files uint skip directories with file counts below this value (default 1)
--page-length uint pagination length for statistics and debug pages --page-length uint pagination length for statistics and debug pages

188
cmd/debug.go Normal file
View File

@ -0,0 +1,188 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package cmd
import (
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/julienschmidt/httprouter"
"github.com/yosssi/gohtml"
)
func serveDebugHtml(args []string, index *Index, paginate bool) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Set("Content-Type", "text/html")
startTime := time.Now()
indexDump := index.Index()
fileCount := len(indexDump)
var startIndex, stopIndex int
page, err := strconv.Atoi(p.ByName("page"))
if err != nil || page <= 0 {
startIndex = 0
stopIndex = fileCount
} else {
startIndex = ((page - 1) * int(pageLength))
stopIndex = (startIndex + int(pageLength))
}
if startIndex > (fileCount - 1) {
indexDump = []string{}
}
if stopIndex > fileCount {
stopIndex = fileCount
}
sort.SliceStable(indexDump, func(p, q int) bool {
return strings.ToLower(indexDump[p]) < strings.ToLower(indexDump[q])
})
var htmlBody strings.Builder
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
htmlBody.WriteString(FaviconHtml)
htmlBody.WriteString(`<style>a{text-decoration:none;height:100%;width:100%;color:inherit;cursor:pointer}`)
htmlBody.WriteString(`table,td,tr{border:1px solid black;border-collapse:collapse}td{white-space:nowrap;padding:.5em}</style>`)
htmlBody.WriteString(fmt.Sprintf("<title>Index contains %d files</title></head><body><table>", fileCount))
if len(indexDump) > 0 {
for _, v := range indexDump[startIndex:stopIndex] {
var shouldSort = ""
if sorting {
shouldSort = "?sort=asc"
}
htmlBody.WriteString(fmt.Sprintf("<tr><td><a href=\"%s%s%s\">%s</a></td></tr>\n", MediaPrefix, v, shouldSort, v))
}
}
if pageLength != 0 {
var firstPage int = 1
var lastPage int
if fileCount%int(pageLength) == 0 {
lastPage = fileCount / int(pageLength)
} else {
lastPage = (fileCount / int(pageLength)) + 1
}
if paginate {
var prevStatus, nextStatus string = "", ""
if page <= 1 {
prevStatus = " disabled"
}
if page >= lastPage {
nextStatus = " disabled"
}
prevPage := page - 1
if prevPage < 1 {
prevPage = 1
}
nextPage := page + 1
if nextPage > lastPage {
nextPage = fileCount / int(pageLength)
}
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\">First</button>",
firstPage))
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\"%s>Prev</button>",
prevPage,
prevStatus))
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\"%s>Next</button>",
nextPage,
nextStatus))
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\">Last</button>",
lastPage))
}
}
htmlBody.WriteString(`</table></body></html>`)
b, err := io.WriteString(w, gohtml.Format(htmlBody.String()))
if err != nil {
return
}
if verbose {
fmt.Printf("%s | Served HTML debug page (%s) to %s in %s\n",
startTime.Format(LogDate),
humanReadableSize(b),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}
func serveDebugJson(args []string, index *Index) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
startTime := time.Now()
indexDump := index.Index()
fileCount := len(indexDump)
sort.SliceStable(indexDump, func(p, q int) bool {
return strings.ToLower(indexDump[p]) < strings.ToLower(indexDump[q])
})
var startIndex, stopIndex int
page, err := strconv.Atoi(p.ByName("page"))
if err != nil || page <= 0 {
startIndex = 0
stopIndex = fileCount
} else {
startIndex = ((page - 1) * int(pageLength))
stopIndex = (startIndex + int(pageLength))
}
if startIndex > (fileCount - 1) {
indexDump = []string{}
}
if stopIndex > fileCount {
stopIndex = fileCount
}
response, err := json.MarshalIndent(indexDump[startIndex:stopIndex], "", " ")
if err != nil {
fmt.Println(err)
serverError(w, r, nil)
return
}
w.Write(response)
if verbose {
fmt.Printf("%s | Served JSON debug page (%s) to %s in %s\n",
startTime.Format(LogDate),
humanReadableSize(len(response)),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}

View File

@ -35,7 +35,7 @@ func newErrorPage(title, body string) string {
func notFound(w http.ResponseWriter, r *http.Request, filePath string) error { func notFound(w http.ResponseWriter, r *http.Request, filePath string) error {
startTime := time.Now() startTime := time.Now()
if Verbose { if verbose {
fmt.Printf("%s | Unavailable file %s requested by %s\n", fmt.Printf("%s | Unavailable file %s requested by %s\n",
startTime.Format(LogDate), startTime.Format(LogDate),
filePath, filePath,
@ -57,7 +57,7 @@ func notFound(w http.ResponseWriter, r *http.Request, filePath string) error {
func serverError(w http.ResponseWriter, r *http.Request, i interface{}) { func serverError(w http.ResponseWriter, r *http.Request, i interface{}) {
startTime := time.Now() startTime := time.Now()
if Verbose { if verbose {
fmt.Printf("%s | Invalid request for %s from %s\n", fmt.Printf("%s | Invalid request for %s from %s\n",
startTime.Format(LogDate), startTime.Format(LogDate),
r.URL.Path, r.URL.Path,

View File

@ -128,7 +128,7 @@ func appendPath(directory, path string, files *Files, stats *ScanStats, register
} }
func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats, types *formats.SupportedFormats) error { func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats, types *formats.SupportedFormats) error {
shouldCache := Cache && filters.IsEmpty() shouldCache := cache && filters.IsEmpty()
absolutePath, err := filepath.Abs(path) absolutePath, err := filepath.Abs(path)
if err != nil { if err != nil {
@ -180,7 +180,7 @@ 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 *Index, registeredFormats *formats.SupportedFormats) (string, error) {
filePath, err := pickFile(paths, filters, sortOrder, index, registeredFormats) filePath, err := pickFile(paths, filters, sortOrder, index, registeredFormats)
if err != nil { if err != nil {
return "", nil return "", nil
@ -272,10 +272,8 @@ func splitPath(path string, Regexes *Regexes) (*Path, error) {
func tryExtensions(p *Path, registeredFormats *formats.SupportedFormats) (string, error) { func tryExtensions(p *Path, registeredFormats *formats.SupportedFormats) (string, error) {
var fileName string var fileName string
for _, format := range registeredFormats.Extensions { for _, extension := range registeredFormats.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 {
@ -312,7 +310,7 @@ func pathIsValid(filePath string, paths []string) bool {
} }
switch { switch {
case Verbose && !matchesPrefix: case verbose && !matchesPrefix:
fmt.Printf("%s | Error: Failed to serve file outside specified path(s): %s\n", fmt.Printf("%s | Error: Failed to serve file outside specified path(s): %s\n",
time.Now().Format(LogDate), time.Now().Format(LogDate),
filePath, filePath,
@ -335,7 +333,7 @@ func pathHasSupportedFiles(path string, registeredFormats *formats.SupportedForm
} }
switch { switch {
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 := formats.FileType(p, registeredFormats)
@ -392,7 +390,7 @@ func scanPath(path string, files *Files, filters *Filters, stats *ScanStats, con
} }
switch { switch {
case !Recursive && info.IsDir() && p != path: case !recursive && info.IsDir() && p != path:
return filepath.SkipDir return filepath.SkipDir
case !info.IsDir(): case !info.IsDir():
wg.Add(1) wg.Add(1)
@ -421,7 +419,7 @@ func scanPath(path string, files *Files, filters *Filters, stats *ScanStats, con
fmt.Println(err) fmt.Println(err)
} }
if files > 0 && (files < MinimumFileCount) || (files > MaximumFileCount) { if files > 0 && (files < minimumFileCount) || (files > maximumFileCount) {
// This count will not otherwise include the parent directory itself, so increment by one // This count will not otherwise include the parent directory itself, so increment by one
stats.directoriesSkipped.Add(directories + 1) stats.directoriesSkipped.Add(directories + 1)
stats.filesSkipped.Add(files) stats.filesSkipped.Add(files)
@ -444,8 +442,8 @@ 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 *Index, types *formats.SupportedFormats) ([]string, bool) {
if Cache && filters.IsEmpty() && !index.IsEmpty() { if cache && filters.IsEmpty() && !index.IsEmpty() {
return index.Index(), true return index.Index(), true
} }
@ -498,7 +496,7 @@ func fileList(paths []string, filters *Filters, sort string, index *FileIndex, t
return []string{}, false return []string{}, false
} }
if Verbose { if verbose {
fmt.Printf("%s | Indexed %d/%d files across %d/%d directories in %s\n", fmt.Printf("%s | Indexed %d/%d files across %d/%d directories in %s\n",
time.Now().Format(LogDate), time.Now().Format(LogDate),
stats.filesMatched.Load(), stats.filesMatched.Load(),
@ -509,7 +507,7 @@ func fileList(paths []string, filters *Filters, sort string, index *FileIndex, t
) )
} }
if Cache && filters.IsEmpty() { if cache && filters.IsEmpty() {
index.setIndex(fileList) index.setIndex(fileList)
} }
@ -558,7 +556,7 @@ 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 *Index, registeredFormats *formats.SupportedFormats) (string, error) {
fileList, fromCache := fileList(args, filters, sort, index, registeredFormats) fileList, fromCache := fileList(args, filters, sort, index, registeredFormats)
fileCount := len(fileList) fileCount := len(fileList)

View File

@ -6,29 +6,21 @@ package cmd
import ( import (
"encoding/gob" "encoding/gob"
"encoding/json"
"fmt"
"io"
"net/http" "net/http"
"os" "os"
"sort"
"strconv"
"strings"
"sync" "sync"
"time"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
"github.com/yosssi/gohtml"
"seedno.de/seednode/roulette/formats" "seedno.de/seednode/roulette/formats"
) )
type FileIndex struct { type Index struct {
mutex sync.RWMutex mutex sync.RWMutex
list []string list []string
} }
func (i *FileIndex) Index() []string { func (i *Index) Index() []string {
i.mutex.RLock() i.mutex.RLock()
val := i.list val := i.list
i.mutex.RUnlock() i.mutex.RUnlock()
@ -36,7 +28,7 @@ func (i *FileIndex) Index() []string {
return val return val
} }
func (i *FileIndex) Remove(path string) { func (i *Index) Remove(path string) {
i.mutex.RLock() i.mutex.RLock()
tempIndex := make([]string, len(i.list)) tempIndex := make([]string, len(i.list))
copy(tempIndex, i.list) copy(tempIndex, i.list)
@ -60,25 +52,25 @@ func (i *FileIndex) Remove(path string) {
i.mutex.Unlock() i.mutex.Unlock()
} }
func (i *FileIndex) setIndex(val []string) { func (i *Index) setIndex(val []string) {
i.mutex.Lock() i.mutex.Lock()
i.list = val i.list = val
i.mutex.Unlock() i.mutex.Unlock()
} }
func (i *FileIndex) generateCache(args []string, registeredFormats *formats.SupportedFormats) { func (i *Index) generateCache(args []string, registeredFormats *formats.SupportedFormats) {
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, registeredFormats)
if Cache && CacheFile != "" { if cache && cacheFile != "" {
i.Export(CacheFile) i.Export(cacheFile)
} }
} }
func (i *FileIndex) IsEmpty() bool { func (i *Index) IsEmpty() bool {
i.mutex.RLock() i.mutex.RLock()
length := len(i.list) length := len(i.list)
i.mutex.RUnlock() i.mutex.RUnlock()
@ -86,7 +78,7 @@ func (i *FileIndex) IsEmpty() bool {
return length == 0 return length == 0
} }
func (i *FileIndex) Export(path string) error { func (i *Index) Export(path string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil { if err != nil {
return err return err
@ -110,7 +102,7 @@ func (i *FileIndex) Export(path string) error {
return nil return nil
} }
func (i *FileIndex) Import(path string) error { func (i *Index) Import(path string) error {
file, err := os.OpenFile(path, os.O_RDONLY, 0600) file, err := os.OpenFile(path, os.O_RDONLY, 0600)
if err != nil { if err != nil {
return err return err
@ -138,7 +130,7 @@ 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 *Index, registeredFormats *formats.SupportedFormats) 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, registeredFormats)
@ -147,172 +139,3 @@ func serveCacheClear(args []string, index *FileIndex, registeredFormats *formats
w.Write([]byte("Ok")) w.Write([]byte("Ok"))
} }
} }
func serveIndexHtml(args []string, index *FileIndex, paginate bool) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Set("Content-Type", "text/html")
startTime := time.Now()
indexDump := index.Index()
fileCount := len(indexDump)
var startIndex, stopIndex int
page, err := strconv.Atoi(p.ByName("page"))
if err != nil || page <= 0 {
startIndex = 0
stopIndex = fileCount
} else {
startIndex = ((page - 1) * int(PageLength))
stopIndex = (startIndex + int(PageLength))
}
if startIndex > (fileCount - 1) {
indexDump = []string{}
}
if stopIndex > fileCount {
stopIndex = fileCount
}
sort.SliceStable(indexDump, func(p, q int) bool {
return strings.ToLower(indexDump[p]) < strings.ToLower(indexDump[q])
})
var htmlBody strings.Builder
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
htmlBody.WriteString(FaviconHtml)
htmlBody.WriteString(`<style>a{text-decoration:none;height:100%;width:100%;color:inherit;cursor:pointer}`)
htmlBody.WriteString(`table,td,tr{border:1px solid black;border-collapse:collapse}td{white-space:nowrap;padding:.5em}</style>`)
htmlBody.WriteString(fmt.Sprintf("<title>Index contains %d files</title></head><body><table>", fileCount))
if len(indexDump) > 0 {
for _, v := range indexDump[startIndex:stopIndex] {
var shouldSort = ""
if Sorting {
shouldSort = "?sort=asc"
}
htmlBody.WriteString(fmt.Sprintf("<tr><td><a href=\"%s%s%s\">%s</a></td></tr>\n", MediaPrefix, v, shouldSort, v))
}
}
if PageLength != 0 {
var firstPage int = 1
var lastPage int
if fileCount%int(PageLength) == 0 {
lastPage = fileCount / int(PageLength)
} else {
lastPage = (fileCount / int(PageLength)) + 1
}
if paginate {
var prevStatus, nextStatus string = "", ""
if page <= 1 {
prevStatus = " disabled"
}
if page >= lastPage {
nextStatus = " disabled"
}
prevPage := page - 1
if prevPage < 1 {
prevPage = 1
}
nextPage := page + 1
if nextPage > lastPage {
nextPage = fileCount / int(PageLength)
}
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\">First</button>",
firstPage))
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\"%s>Prev</button>",
prevPage,
prevStatus))
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\"%s>Next</button>",
nextPage,
nextStatus))
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\">Last</button>",
lastPage))
}
}
htmlBody.WriteString(`</table></body></html>`)
b, err := io.WriteString(w, gohtml.Format(htmlBody.String()))
if err != nil {
return
}
if Verbose {
fmt.Printf("%s | Served HTML index page (%s) to %s in %s\n",
startTime.Format(LogDate),
humanReadableSize(b),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}
func serveIndexJson(args []string, index *FileIndex) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
startTime := time.Now()
indexDump := index.Index()
fileCount := len(indexDump)
sort.SliceStable(indexDump, func(p, q int) bool {
return strings.ToLower(indexDump[p]) < strings.ToLower(indexDump[q])
})
var startIndex, stopIndex int
page, err := strconv.Atoi(p.ByName("page"))
if err != nil || page <= 0 {
startIndex = 0
stopIndex = fileCount
} else {
startIndex = ((page - 1) * int(PageLength))
stopIndex = (startIndex + int(PageLength))
}
if startIndex > (fileCount - 1) {
indexDump = []string{}
}
if stopIndex > fileCount {
stopIndex = fileCount
}
response, err := json.MarshalIndent(indexDump[startIndex:stopIndex], "", " ")
if err != nil {
fmt.Println(err)
serverError(w, r, nil)
return
}
w.Write(response)
if Verbose {
fmt.Printf("%s | Served JSON index page (%s) to %s in %s\n",
startTime.Format(LogDate),
humanReadableSize(len(response)),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}

View File

@ -12,52 +12,45 @@ import (
) )
const ( const (
ReleaseVersion string = "0.70.3" Version string = "0.69.4"
) )
var ( var (
All bool all bool
Audio bool audio bool
Bind string bind string
Cache bool cache bool
CacheFile string cacheFile string
Filtering bool debug bool
Flash bool filtering bool
Images bool images bool
Index bool maximumFileCount uint64
MaximumFileCount uint64 minimumFileCount uint64
MinimumFileCount uint64 pageLength uint64
PageLength uint64 port uint16
Port uint16 profile bool
Profile bool recursive bool
Recursive bool refreshInterval string
RefreshInterval string russian bool
Russian bool sorting bool
Sorting bool statistics bool
Statistics bool statisticsFile string
StatisticsFile string text bool
Text bool verbose bool
Verbose bool version bool
Version bool videos bool
Videos bool
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
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) { PreRun: func(cmd *cobra.Command, args []string) {
// enable image support if no other flags are passed, to retain backwards compatibility if debug {
// to be replaced with MarkFlagsOneRequired on next spf13/cobra update
if !(All || Audio || Flash || Images || Text || Videos) {
Images = true
}
if Index {
cmd.MarkFlagRequired("cache") 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 {
log.Fatal(ErrIncorrectRefreshInterval) log.Fatal(ErrIncorrectRefreshInterval)
} }
@ -82,30 +75,29 @@ func Execute() {
} }
func init() { func init() {
rootCmd.Flags().BoolVar(&All, "all", false, "enable all supported file types") rootCmd.Flags().BoolVar(&all, "all", 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")
rootCmd.Flags().StringVar(&CacheFile, "cache-file", "", "path to optional persistent cache file") rootCmd.Flags().StringVar(&cacheFile, "cache-file", "", "path to optional persistent cache file")
rootCmd.Flags().BoolVarP(&Filtering, "filter", "f", false, "enable filtering") rootCmd.Flags().BoolVarP(&debug, "debug", "d", false, "expose debug endpoint")
rootCmd.Flags().BoolVar(&Flash, "flash", false, "enable support for shockwave flash files (via ruffle.rs)") rootCmd.Flags().BoolVarP(&filtering, "filter", "f", false, "enable filtering")
rootCmd.Flags().BoolVar(&Images, "images", false, "enable support for image files") rootCmd.Flags().BoolVar(&images, "images", true, "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(&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(&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().Uint64Var(&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") rootCmd.Flags().StringVar(&refreshInterval, "refresh-interval", "", "force refresh interval equal to this duration (minimum 500ms)")
rootCmd.Flags().StringVar(&RefreshInterval, "refresh-interval", "", "force refresh interval equal to this duration (minimum 500ms)") rootCmd.Flags().BoolVar(&russian, "russian", false, "remove selected images after serving")
rootCmd.Flags().BoolVar(&Russian, "russian", false, "remove selected images after serving") rootCmd.Flags().BoolVarP(&sorting, "sort", "s", false, "enable sorting")
rootCmd.Flags().BoolVarP(&Sorting, "sort", "s", false, "enable sorting") 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 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")
rootCmd.Flags().SetInterspersed(true) rootCmd.Flags().SetInterspersed(true)
@ -117,5 +109,5 @@ func init() {
}) })
rootCmd.SetVersionTemplate("roulette v{{.Version}}\n") rootCmd.SetVersionTemplate("roulette v{{.Version}}\n")
rootCmd.Version = ReleaseVersion rootCmd.Version = Version
} }

View File

@ -118,8 +118,8 @@ func (s *ServeStats) ListFiles(page int) ([]byte, error) {
startIndex = 0 startIndex = 0
stopIndex = len(stats.List) - 1 stopIndex = len(stats.List) - 1
} else { } else {
startIndex = ((page - 1) * int(PageLength)) startIndex = ((page - 1) * int(pageLength))
stopIndex = (startIndex + int(PageLength)) stopIndex = (startIndex + int(pageLength))
} }
if startIndex > len(stats.List)-1 { if startIndex > len(stats.List)-1 {
@ -231,7 +231,7 @@ func serveStats(args []string, stats *ServeStats) httprouter.Handle {
w.Write(response) w.Write(response)
if Verbose { if verbose {
fmt.Printf("%s | Served statistics page (%s) to %s in %s\n", fmt.Printf("%s | Served statistics page (%s) to %s in %s\n",
startTime.Format(LogDate), startTime.Format(LogDate),
humanReadableSize(len(response)), humanReadableSize(len(response)),
@ -240,8 +240,8 @@ func serveStats(args []string, stats *ServeStats) httprouter.Handle {
) )
} }
if StatisticsFile != "" { if statisticsFile != "" {
stats.Export(StatisticsFile) stats.Export(statisticsFile)
} }
} }
} }

View File

@ -13,13 +13,13 @@ import (
"time" "time"
) )
func refreshInterval(r *http.Request) (int64, string) { func RefreshInterval(r *http.Request) (int64, string) {
var interval string var interval string
if RefreshInterval == "" { if refreshInterval == "" {
interval = r.URL.Query().Get("refresh") interval = r.URL.Query().Get("refresh")
} else { } else {
interval = RefreshInterval interval = refreshInterval
} }
duration, err := time.ParseDuration(interval) duration, err := time.ParseDuration(interval)
@ -68,7 +68,7 @@ func generateQueryParams(filters *Filters, sortOrder, refreshInterval string) st
queryParams.WriteString("?") queryParams.WriteString("?")
if Filtering { if filtering {
queryParams.WriteString("include=") queryParams.WriteString("include=")
if filters.HasIncludes() { if filters.HasIncludes() {
queryParams.WriteString(filters.Includes()) queryParams.WriteString(filters.Includes())
@ -82,7 +82,7 @@ func generateQueryParams(filters *Filters, sortOrder, refreshInterval string) st
hasParams = true hasParams = true
} }
if Sorting { if sorting {
if hasParams { if hasParams {
queryParams.WriteString("&") queryParams.WriteString("&")
} }

View File

@ -37,7 +37,7 @@ const (
Timeout time.Duration = 10 * time.Second Timeout time.Duration = 10 * time.Second
) )
func serveStaticFile(paths []string, stats *ServeStats, index *FileIndex) httprouter.Handle { func serveStaticFile(paths []string, stats *ServeStats, index *Index) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
path := strings.TrimPrefix(r.URL.Path, SourcePrefix) path := strings.TrimPrefix(r.URL.Path, SourcePrefix)
@ -95,7 +95,7 @@ func serveStaticFile(paths []string, stats *ServeStats, index *FileIndex) httpro
fileSize := humanReadableSize(len(buf)) fileSize := humanReadableSize(len(buf))
if Russian { if russian {
err = os.Remove(filePath) err = os.Remove(filePath)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -105,12 +105,12 @@ func serveStaticFile(paths []string, stats *ServeStats, index *FileIndex) httpro
return return
} }
if Cache { if cache {
index.Remove(filePath) index.Remove(filePath)
} }
} }
if Verbose { if verbose {
fmt.Printf("%s | Served %s (%s) to %s in %s\n", fmt.Printf("%s | Served %s (%s) to %s in %s\n",
startTime.Format(LogDate), startTime.Format(LogDate),
filePath, filePath,
@ -120,14 +120,14 @@ func serveStaticFile(paths []string, stats *ServeStats, index *FileIndex) httpro
) )
} }
if Statistics { if statistics {
stats.incrementCounter(filePath, startTime, fileSize) stats.incrementCounter(filePath, startTime, fileSize)
} }
} }
} }
func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFormats *formats.SupportedFormats) httprouter.Handle { func serveRoot(paths []string, Regexes *Regexes, index *Index, registeredFormats *formats.SupportedFormats) 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 {
@ -147,7 +147,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFor
sortOrder := SortOrder(r) sortOrder := SortOrder(r)
_, refreshInterval := refreshInterval(r) _, refreshInterval := RefreshInterval(r)
var filePath string var filePath string
@ -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 *Index, registeredFormats *formats.SupportedFormats) 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),
@ -250,7 +250,7 @@ func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, registeredFo
w.Header().Add("Content-Type", "text/html") w.Header().Add("Content-Type", "text/html")
refreshTimer, refreshInterval := refreshInterval(r) refreshTimer, refreshInterval := RefreshInterval(r)
queryParams := generateQueryParams(filters, sortOrder, refreshInterval) queryParams := generateQueryParams(filters, sortOrder, refreshInterval)
@ -286,7 +286,7 @@ func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, registeredFo
func serveVersion() httprouter.Handle { func serveVersion() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
data := []byte(fmt.Sprintf("roulette v%s\n", ReleaseVersion)) data := []byte(fmt.Sprintf("roulette v%s\n", Version))
w.Header().Write(bytes.NewBufferString("Content-Length: " + strconv.Itoa(len(data)))) w.Header().Write(bytes.NewBufferString("Content-Length: " + strconv.Itoa(len(data))))
@ -304,7 +304,7 @@ func ServePage(args []string) error {
} }
} }
bindHost, err := net.LookupHost(Bind) bindHost, err := net.LookupHost(bind)
if err != nil { if err != nil {
return err return err
} }
@ -314,30 +314,21 @@ func ServePage(args []string) error {
return errors.New("invalid bind address provided") return errors.New("invalid bind address provided")
} }
mux := httprouter.New() registeredFormats := &formats.SupportedFormats{}
registeredFormats := &formats.SupportedFormats{ if audio || all {
Extensions: make(map[string]*formats.SupportedFormat),
MimeTypes: make(map[string]*formats.SupportedFormat),
}
if Audio || All {
registeredFormats.Add(formats.RegisterAudioFormats()) registeredFormats.Add(formats.RegisterAudioFormats())
} }
if Flash || All { if images || all {
registeredFormats.Add(formats.RegisterFlashFormats())
}
if Images || All {
registeredFormats.Add(formats.RegisterImageFormats()) registeredFormats.Add(formats.RegisterImageFormats())
} }
if Text || All { if text || all {
registeredFormats.Add(formats.RegisterTextFormats()) registeredFormats.Add(formats.RegisterTextFormats())
} }
if Videos || All { if videos || all {
registeredFormats.Add(formats.RegisterVideoFormats()) registeredFormats.Add(formats.RegisterVideoFormats())
} }
@ -350,11 +341,13 @@ func ServePage(args []string) error {
return ErrNoMediaFound return ErrNoMediaFound
} }
if Russian { if russian {
fmt.Printf("WARNING! Files *will* be deleted after serving!\n\n") fmt.Printf("WARNING! Files *will* be deleted after serving!\n\n")
} }
index := &FileIndex{ mux := httprouter.New()
index := &Index{
mutex: sync.RWMutex{}, mutex: sync.RWMutex{},
list: []string{}, list: []string{},
} }
@ -365,7 +358,7 @@ func ServePage(args []string) error {
} }
srv := &http.Server{ srv := &http.Server{
Addr: net.JoinHostPort(Bind, strconv.Itoa(int(Port))), Addr: net.JoinHostPort(bind, strconv.Itoa(int(port))),
Handler: mux, Handler: mux,
IdleTimeout: 10 * time.Minute, IdleTimeout: 10 * time.Minute,
ReadTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second,
@ -394,11 +387,11 @@ func ServePage(args []string) error {
mux.GET("/version", serveVersion()) mux.GET("/version", serveVersion())
if Cache { if cache {
skipIndex := false skipIndex := false
if CacheFile != "" { if cacheFile != "" {
err := index.Import(CacheFile) err := index.Import(cacheFile)
if err == nil { if err == nil {
skipIndex = true skipIndex = true
} }
@ -411,19 +404,19 @@ func ServePage(args []string) error {
mux.GET("/clear_cache", serveCacheClear(args, index, registeredFormats)) mux.GET("/clear_cache", serveCacheClear(args, index, registeredFormats))
} }
if Index { if debug {
mux.GET("/html/", serveIndexHtml(args, index, false)) mux.GET("/html/", serveDebugHtml(args, index, false))
if PageLength != 0 { if pageLength != 0 {
mux.GET("/html/:page", serveIndexHtml(args, index, true)) mux.GET("/html/:page", serveDebugHtml(args, index, true))
} }
mux.GET("/json", serveIndexJson(args, index)) mux.GET("/json", serveDebugJson(args, index))
if PageLength != 0 { if pageLength != 0 {
mux.GET("/json/:page", serveIndexJson(args, index)) mux.GET("/json/:page", serveDebugJson(args, index))
} }
} }
if Profile { if profile {
mux.HandlerFunc("GET", "/debug/pprof/", pprof.Index) mux.HandlerFunc("GET", "/debug/pprof/", pprof.Index)
mux.HandlerFunc("GET", "/debug/pprof/cmdline", pprof.Cmdline) mux.HandlerFunc("GET", "/debug/pprof/cmdline", pprof.Cmdline)
mux.HandlerFunc("GET", "/debug/pprof/profile", pprof.Profile) mux.HandlerFunc("GET", "/debug/pprof/profile", pprof.Profile)
@ -431,9 +424,9 @@ func ServePage(args []string) error {
mux.HandlerFunc("GET", "/debug/pprof/trace", pprof.Trace) mux.HandlerFunc("GET", "/debug/pprof/trace", pprof.Trace)
} }
if Statistics { if statistics {
if StatisticsFile != "" { if statisticsFile != "" {
stats.Import(StatisticsFile) stats.Import(statisticsFile)
gracefulShutdown := make(chan os.Signal, 1) gracefulShutdown := make(chan os.Signal, 1)
signal.Notify(gracefulShutdown, syscall.SIGINT, syscall.SIGTERM) signal.Notify(gracefulShutdown, syscall.SIGINT, syscall.SIGTERM)
@ -441,14 +434,14 @@ func ServePage(args []string) error {
go func() { go func() {
<-gracefulShutdown <-gracefulShutdown
stats.Export(StatisticsFile) stats.Export(statisticsFile)
os.Exit(0) os.Exit(0)
}() }()
} }
mux.GET("/stats", serveStats(args, stats)) mux.GET("/stats", serveStats(args, stats))
if PageLength != 0 { if pageLength != 0 {
mux.GET("/stats/:page", serveStats(args, stats)) mux.GET("/stats/:page", serveStats(args, stats))
} }
} }

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

@ -39,6 +39,7 @@ func RegisterTextFormats() *SupportedFormat {
}, },
MimeTypes: []string{ MimeTypes: []string{
`application/json`, `application/json`,
`application/octet-stream`,
`application/xml`, `application/xml`,
`text/css`, `text/css`,
`text/csv`, `text/csv`,

View File

@ -8,7 +8,6 @@ import (
"errors" "errors"
"net/http" "net/http"
"os" "os"
"path/filepath"
) )
type SupportedFormat struct { type SupportedFormat struct {
@ -21,27 +20,46 @@ type SupportedFormat struct {
} }
type SupportedFormats struct { type SupportedFormats struct {
Extensions map[string]*SupportedFormat types []*SupportedFormat
MimeTypes map[string]*SupportedFormat
} }
func (s *SupportedFormats) Add(t *SupportedFormat) { func (s *SupportedFormats) Add(t *SupportedFormat) {
for _, v := range t.Extensions { s.types = append(s.types, t)
_, 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) { func (s *SupportedFormats) Extensions() []string {
var extensions []string
for _, t := range s.types {
extensions = append(extensions, t.Extensions...)
}
return extensions
}
func (s *SupportedFormats) MimeTypes() []string {
var mimeTypes []string
for _, t := range s.types {
mimeTypes = append(mimeTypes, t.MimeTypes...)
}
return mimeTypes
}
func (s *SupportedFormats) Type(mimeType string) *SupportedFormat {
for i := range s.types {
for _, m := range s.types[i].MimeTypes {
if mimeType == m {
return s.types[i]
}
}
}
return nil
}
func FileType(path string, types *SupportedFormats) (bool, *SupportedFormat, string, 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):
@ -56,16 +74,12 @@ func FileType(path string, registeredFormats *SupportedFormats) (bool, *Supporte
mimeType := http.DetectContentType(head) mimeType := http.DetectContentType(head)
// try identifying files by mime types first for _, v := range types.MimeTypes() {
fileType, exists := registeredFormats.MimeTypes[mimeType] if mimeType == v {
if exists { fileType := types.Type(mimeType)
return fileType.Validate(path), fileType, mimeType, nil 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 return false, nil, "", nil