Compare commits

..

No commits in common. "83809696ac3072ed32d4b64d8da4440fbcc39a95" and "6fc21236a7a31e1e1b1ede5fd299e095293a03cd" have entirely different histories.

6 changed files with 119 additions and 160 deletions

View File

@ -20,7 +20,7 @@ Dockerfile available [here](https://git.seedno.de/seednode/roulette/raw/branch/m
An example instance with most features enabled can be found [here](https://nature.seedno.de/). An example instance with most features enabled can be found [here](https://nature.seedno.de/).
## Admin prefix ## Admin prefix
You can restrict access to certain functionality (the REST API and profiling endpoints) by prepending a secret string to the paths. You can restrict access to certain functionality by prepending a secret string to the paths.
For example, providing the `--admin-prefix=abc123` flag will register the index rebuild path as `/abc123/index/rebuild`. For example, providing the `--admin-prefix=abc123` flag will register the index rebuild path as `/abc123/index/rebuild`.
@ -44,21 +44,6 @@ The restricted paths are:
While this might thwart very basic attacks, the proper solution for most use cases would likely be to add authentication via a reverse proxy. While this might thwart very basic attacks, the proper solution for most use cases would likely be to add authentication via a reverse proxy.
## API
If the `--api` flag is passed, a number of REST endpoints are registered.
The first of these—`/index/`—responds to GET requests with the contents of the index, in JSON format.
The second—`/index/rebuild`—responds to POST requests by rebuilding the index.
This can prove useful when confirming whether the index is generated successfully, or whether a given file is in the index.
The remaining four endpoints respond to GET requests with information about the registered file types:
- `/extensions/available`
- `/extensions/enabled`
- `/types/available`
- `/types/enabled`
## Filtering ## Filtering
You can provide a comma-delimited string of alphanumeric patterns to match via the `include=` query parameter, assuming the `-f|--filter` flag is enabled. You can provide a comma-delimited string of alphanumeric patterns to match via the `include=` query parameter, assuming the `-f|--filter` flag is enabled.
@ -82,11 +67,24 @@ This will slightly increase the delay before the application begins responding t
The index can be regenerated at any time by accessing the `/index/rebuild` endpoint. The index can be regenerated at any time by accessing the `/index/rebuild` endpoint.
Automatic index rebuilds can be enabled via the `--index-interval` flag, which accepts [time.Duration](https://pkg.go.dev/time#ParseDuration) strings.
If `--index-file` is set, the index will be loaded from the specified file on start, and written to the file whenever it is re-generated. If `--index-file` is set, the index will be loaded from the specified file on start, and written to the file whenever it is re-generated.
The index file consists of [zstd](https://facebook.github.io/zstd/)-compressed [gobs](https://pkg.go.dev/encoding/gob). The index file consists of (optionally compressed) [gobs](https://pkg.go.dev/encoding/gob).
The compression format can be specified via the `--compression` flag.
Supported formats are `none`, `zlib`, and `zstd`.
Optionally, `--compression-fast` can be used to use the fastest instead of the best compression mode.
## Info
If the `-i|--info` flag is passed, five additional endpoints are registered.
The first of these—`/index/`—returns the contents of the index, in JSON format.
This can prove useful when confirming whether the index is generated successfully, or whether a given file is in the index.
The remaining four endpoints—`/extensions/available`, `/extensions/enabled`, `/types/available` and `/types/enabled`—return information about the registered file types.
## Refresh ## Refresh
If the `--refresh` flag is passed and a positive-value `refresh=<integer><unit>` query parameter is provided, the page will reload after that interval. If the `--refresh` flag is passed and a positive-value `refresh=<integer><unit>` query parameter is provided, the page will reload after that interval.
@ -104,7 +102,7 @@ If the `--russian` flag is passed, everything functions exactly as you would exp
That is, files will be deleted after being served. This is not a joke, you *will* lose data. That is, files will be deleted after being served. This is not a joke, you *will* lose data.
This uses [os.Remove()](https://pkg.go.dev/os#Remove) and checks to ensure the specified file is inside one of the paths passed to `roulette`. This uses `os.Remove()` and checks to ensure the specified file is inside one of the paths passed to `roulette`.
That said, this has not been tested to any real extent, so only pass this flag on systems you don't care about. That said, this has not been tested to any real extent, so only pass this flag on systems you don't care about.
@ -149,7 +147,6 @@ Flags:
--admin-prefix string string to prepend to administrative paths --admin-prefix string string to prepend to administrative paths
-a, --all enable all supported file types -a, --all enable all supported file types
--allow-empty allow specifying paths containing no supported files --allow-empty allow specifying paths containing no supported files
--api expose REST API
--audio enable support for audio files --audio enable support for audio files
--binary-prefix use IEC binary prefixes instead of SI decimal prefixes --binary-prefix use IEC binary prefixes instead of SI decimal prefixes
-b, --bind string address to bind to (default "0.0.0.0") -b, --bind string address to bind to (default "0.0.0.0")
@ -165,11 +162,12 @@ Flags:
--flash enable support for shockwave flash files (via ruffle.rs) --flash enable support for shockwave flash files (via ruffle.rs)
--fun add a bit of excitement to your day --fun add a bit of excitement to your day
-h, --help help for roulette -h, --help help for roulette
--ignore string filename used to indicate directory should be skipped --ignore skip all directories containing a specified filename
--ignore-file string filename used to indicate directory should be skipped (default ".roulette-ignore")
--images enable support for image files --images enable support for image files
--index generate index of supported file paths at startup --index generate index of supported file paths at startup
--index-file string path to optional persistent index file --index-file string path to optional persistent index file
--index-interval string interval at which to regenerate index (e.g. "5m" or "1h") -i, --info expose informational endpoints
--max-file-count int skip directories with file counts above this value (default 2147483647) --max-file-count int skip directories with file counts above this value (default 2147483647)
--min-file-count int skip directories with file counts below this value --min-file-count int skip directories with file counts below this value
-p, --port int port to listen on (default 8080) -p, --port int port to listen on (default 8080)

View File

@ -260,7 +260,7 @@ func walkPath(path string, fileChannel chan<- string, wg1 *sync.WaitGroup, stats
if !node.IsDir() { if !node.IsDir() {
files++ files++
if Ignore != "" && node.Name() == Ignore { if Ignore && node.Name() == IgnoreFile {
skipDir = true skipDir = true
} }
} }

View File

@ -7,10 +7,12 @@ package cmd
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"net/http"
"os" "os"
"sync" "sync"
"time" "time"
"github.com/julienschmidt/httprouter"
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
"seedno.de/seednode/roulette/types" "seedno.de/seednode/roulette/types"
) )
@ -186,43 +188,31 @@ func (index *fileIndex) Import(path string, errorChannel chan<- error) {
} }
} }
func registerIndexInterval(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, quit <-chan struct{}, errorChannel chan<- error) { func serveIndexRebuild(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) httprouter.Handle {
interval, err := time.ParseDuration(IndexInterval) return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
startTime := time.Now()
index.clear()
fileList(args, &filters{}, "", index, formats, encoder, errorChannel)
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
_, err := w.Write([]byte("Ok\n"))
if err != nil { if err != nil {
errorChannel <- err errorChannel <- err
return return
} }
ticker := time.NewTicker(interval)
go func() {
for {
select {
case <-ticker.C:
startTime := time.Now()
rebuildIndex(args, index, formats, encoder, errorChannel)
if Verbose { if Verbose {
fmt.Printf("%s | INDEX: Automatic rebuild took %s\n", fmt.Printf("%s | SERVE: Index rebuild requested by %s took %s\n",
startTime.Format(logDate), startTime.Format(logDate),
realIP(r),
time.Since(startTime).Round(time.Microsecond), time.Since(startTime).Round(time.Microsecond),
) )
} }
case <-quit:
ticker.Stop()
return
} }
}
}()
}
func rebuildIndex(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) {
index.clear()
fileList(args, &filters{}, "", index, formats, encoder, errorChannel)
} }
func importIndex(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) { func importIndex(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) {

View File

@ -13,40 +13,9 @@ import (
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/klauspost/compress/zstd"
"seedno.de/seednode/roulette/types" "seedno.de/seednode/roulette/types"
) )
func serveExtensions(formats types.Types, available bool, errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
startTime := time.Now()
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
var extensions string
if available {
extensions = types.SupportedFormats.GetExtensions()
} else {
extensions = formats.GetExtensions()
}
written, err := w.Write([]byte(extensions))
if err != nil {
errorChannel <- err
}
if Verbose {
fmt.Printf("%s | SERVE: Registered extension list (%s) to %s in %s\n",
startTime.Format(logDate),
humanReadableSize(written),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}
func serveIndex(args []string, index *fileIndex, errorChannel chan<- error) httprouter.Handle { func serveIndex(args []string, index *fileIndex, errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
startTime := time.Now() startTime := time.Now()
@ -86,24 +55,29 @@ func serveIndex(args []string, index *fileIndex, errorChannel chan<- error) http
} }
} }
func serveIndexRebuild(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) httprouter.Handle { func serveExtensions(formats types.Types, available bool, errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
startTime := time.Now() startTime := time.Now()
rebuildIndex(args, index, formats, encoder, errorChannel)
w.Header().Set("Content-Type", "text/plain;charset=UTF-8") w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
_, err := w.Write([]byte("Ok\n")) var extensions string
if available {
extensions = types.SupportedFormats.GetExtensions()
} else {
extensions = formats.GetExtensions()
}
written, err := w.Write([]byte(extensions))
if err != nil { if err != nil {
errorChannel <- err errorChannel <- err
return
} }
if Verbose { if Verbose {
fmt.Printf("%s | SERVE: Index rebuild requested by %s took %s\n", fmt.Printf("%s | SERVE: Registered extension list (%s) to %s in %s\n",
startTime.Format(logDate), startTime.Format(logDate),
humanReadableSize(written),
realIP(r), realIP(r),
time.Since(startTime).Round(time.Microsecond), time.Since(startTime).Round(time.Microsecond),
) )
@ -141,10 +115,9 @@ func serveMediaTypes(formats types.Types, available bool, errorChannel chan<- er
} }
} }
func registerAPIHandlers(mux *httprouter.Router, args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) { func registerInfoHandlers(mux *httprouter.Router, args []string, index *fileIndex, formats types.Types, errorChannel chan<- error) {
if Index { if Index {
mux.GET(Prefix+AdminPrefix+"/index", serveIndex(args, index, errorChannel)) mux.GET(Prefix+AdminPrefix+"/index", serveIndex(args, index, errorChannel))
mux.POST(Prefix+AdminPrefix+"/index/rebuild", serveIndexRebuild(args, index, formats, encoder, errorChannel))
} }
mux.GET(Prefix+AdminPrefix+"/extensions/available", serveExtensions(formats, true, errorChannel)) mux.GET(Prefix+AdminPrefix+"/extensions/available", serveExtensions(formats, true, errorChannel))

View File

@ -17,14 +17,13 @@ import (
const ( const (
AllowedCharacters string = `^[A-z0-9.\-_]+$` AllowedCharacters string = `^[A-z0-9.\-_]+$`
ReleaseVersion string = "7.0.0" ReleaseVersion string = "6.3.1"
) )
var ( var (
AdminPrefix string AdminPrefix string
All bool All bool
AllowEmpty bool AllowEmpty bool
API bool
Audio bool Audio bool
BinaryPrefix bool BinaryPrefix bool
Bind string Bind string
@ -39,11 +38,12 @@ var (
Filtering bool Filtering bool
Flash bool Flash bool
Fun bool Fun bool
Ignore string Ignore bool
IgnoreFile string
Images bool Images bool
Index bool Index bool
IndexFile string IndexFile string
IndexInterval string Info bool
MaxFileCount int MaxFileCount int
MinFileCount int MinFileCount int
Port int Port int
@ -83,7 +83,7 @@ var (
return ErrInvalidPort return ErrInvalidPort
case Concurrency < 1: case Concurrency < 1:
return ErrInvalidConcurrency return ErrInvalidConcurrency
case Ignore != "" && !regexp.MustCompile(AllowedCharacters).MatchString(Ignore): case Ignore && !regexp.MustCompile(AllowedCharacters).MatchString(IgnoreFile):
return ErrInvalidIgnoreFile return ErrInvalidIgnoreFile
case AdminPrefix != "" && !regexp.MustCompile(AllowedCharacters).MatchString(AdminPrefix): case AdminPrefix != "" && !regexp.MustCompile(AllowedCharacters).MatchString(AdminPrefix):
return ErrInvalidAdminPrefix return ErrInvalidAdminPrefix
@ -117,7 +117,6 @@ func init() {
rootCmd.Flags().StringVar(&AdminPrefix, "admin-prefix", "", "string to prepend to administrative paths") rootCmd.Flags().StringVar(&AdminPrefix, "admin-prefix", "", "string to prepend to administrative paths")
rootCmd.Flags().BoolVarP(&All, "all", "a", false, "enable all supported file types") rootCmd.Flags().BoolVarP(&All, "all", "a", false, "enable all supported file types")
rootCmd.Flags().BoolVar(&AllowEmpty, "allow-empty", false, "allow specifying paths containing no supported files") rootCmd.Flags().BoolVar(&AllowEmpty, "allow-empty", false, "allow specifying paths containing no supported files")
rootCmd.Flags().BoolVar(&API, "api", false, "expose REST API")
rootCmd.Flags().BoolVar(&Audio, "audio", false, "enable support for audio files") rootCmd.Flags().BoolVar(&Audio, "audio", false, "enable support for audio files")
rootCmd.Flags().BoolVar(&BinaryPrefix, "binary-prefix", false, "use IEC binary prefixes instead of SI decimal prefixes") rootCmd.Flags().BoolVar(&BinaryPrefix, "binary-prefix", false, "use IEC binary prefixes instead of SI decimal prefixes")
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")
@ -132,11 +131,12 @@ func init() {
rootCmd.Flags().BoolVarP(&Filtering, "filter", "f", false, "enable filtering") rootCmd.Flags().BoolVarP(&Filtering, "filter", "f", false, "enable filtering")
rootCmd.Flags().BoolVar(&Flash, "flash", false, "enable support for shockwave flash files (via ruffle.rs)") rootCmd.Flags().BoolVar(&Flash, "flash", false, "enable support for shockwave flash files (via ruffle.rs)")
rootCmd.Flags().BoolVar(&Fun, "fun", false, "add a bit of excitement to your day") rootCmd.Flags().BoolVar(&Fun, "fun", false, "add a bit of excitement to your day")
rootCmd.Flags().StringVar(&Ignore, "ignore", "", "filename used to indicate directory should be skipped") rootCmd.Flags().BoolVar(&Ignore, "ignore", false, "skip all directories containing a specified filename")
rootCmd.Flags().StringVar(&IgnoreFile, "ignore-file", ".roulette-ignore", "filename used to indicate directory should be skipped")
rootCmd.Flags().BoolVar(&Images, "images", false, "enable support for image files") rootCmd.Flags().BoolVar(&Images, "images", false, "enable support for image files")
rootCmd.Flags().BoolVar(&Index, "index", false, "generate index of supported file paths at startup") rootCmd.Flags().BoolVar(&Index, "index", false, "generate index of supported file paths at startup")
rootCmd.Flags().StringVar(&IndexFile, "index-file", "", "path to optional persistent index file") rootCmd.Flags().StringVar(&IndexFile, "index-file", "", "path to optional persistent index file")
rootCmd.Flags().StringVar(&IndexInterval, "index-interval", "", "interval at which to regenerate index (e.g. \"5m\" or \"1h\")") rootCmd.Flags().BoolVarP(&Info, "info", "i", false, "expose informational endpoints")
rootCmd.Flags().IntVar(&MaxFileCount, "max-file-count", math.MaxInt32, "skip directories with file counts above this value") rootCmd.Flags().IntVar(&MaxFileCount, "max-file-count", math.MaxInt32, "skip directories with file counts above this value")
rootCmd.Flags().IntVar(&MinFileCount, "min-file-count", 0, "skip directories with file counts below this value") rootCmd.Flags().IntVar(&MinFileCount, "min-file-count", 0, "skip directories with file counts below this value")
rootCmd.Flags().IntVarP(&Port, "port", "p", 8080, "port to listen on") rootCmd.Flags().IntVarP(&Port, "port", "p", 8080, "port to listen on")

View File

@ -52,6 +52,21 @@ func newPage(title, body string) string {
return htmlBody.String() return htmlBody.String()
} }
func noFiles(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
w.Write([]byte("No files found in the specified path(s).\n"))
if Verbose {
fmt.Printf("%s | SERVE: Empty path notification to %s\n",
startTime.Format(logDate),
r.RemoteAddr,
)
}
}
func serveStaticFile(paths []string, index *fileIndex, errorChannel chan<- error) httprouter.Handle { func serveStaticFile(paths []string, index *fileIndex, errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
prefix := Prefix + sourcePrefix prefix := Prefix + sourcePrefix
@ -206,18 +221,7 @@ func serveRoot(paths []string, index *fileIndex, filename *regexp.Regexp, format
path, err = newFile(list, sortOrder, filename, formats) path, err = newFile(list, sortOrder, filename, formats)
switch { switch {
case path == "": case path == "":
startTime := time.Now() noFiles(w, r)
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
w.Write([]byte("No files found in the specified path(s).\n"))
if Verbose {
fmt.Printf("%s | SERVE: Empty path notification to %s\n",
startTime.Format(logDate),
r.RemoteAddr,
)
}
return return
case err != nil && err == ErrNoMediaFound: case err != nil && err == ErrNoMediaFound:
@ -454,10 +458,9 @@ func redirectRoot() httprouter.Handle {
} }
func ServePage(args []string) error { func ServePage(args []string) error {
var err error
timeZone := os.Getenv("TZ") timeZone := os.Getenv("TZ")
if timeZone != "" { if timeZone != "" {
var err error
time.Local, err = time.LoadLocation(timeZone) time.Local, err = time.LoadLocation(timeZone)
if err != nil { if err != nil {
return err return err
@ -581,19 +584,14 @@ func ServePage(args []string) error {
mux.GET(Prefix+"/version", serveVersion(errorChannel)) mux.GET(Prefix+"/version", serveVersion(errorChannel))
quit := make(chan struct{})
defer close(quit)
if API {
registerAPIHandlers(mux, args, index, formats, encoder, errorChannel)
}
if Index { if Index {
importIndex(paths, index, formats, encoder, errorChannel) mux.GET(Prefix+AdminPrefix+"/index/rebuild", serveIndexRebuild(args, index, formats, encoder, errorChannel))
if IndexInterval != "" { importIndex(paths, index, formats, encoder, errorChannel)
registerIndexInterval(args, index, formats, encoder, quit, errorChannel)
} }
if Info {
registerInfoHandlers(mux, args, index, formats, errorChannel)
} }
if Profile { if Profile {