Compare commits

..

No commits in common. "3ea2227a9fa3fdb6d007cf2248f1dcaa85c8342c" and "cbf721845300a1e4869d6c82d8a1edfb899632d6" have entirely different histories.

661 changed files with 242 additions and 361 deletions

View File

@ -8,6 +8,8 @@ A new file will be selected if you open `/` directly, or if you click on any dis
Browser history is preserved, so you can always go back to any previously displayed media.
Supported file types and extensions are listed in the corresponding source files in `formats/`.
Feature requests, code criticism, bug reports, general chit-chat, and unrelated angst accepted at `roulette@seedno.de`.
Static binary builds available [here](https://cdn.seedno.de/builds/roulette).
@ -76,17 +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.
## Info
If the `-i|--info` flag is passed, six additional endpoints are registered.
If the `-i|--index` flag is passed, four additional endpoints are registered.
The first of these—`/html` and `/json`—return the contents of the index, in HTML and JSON formats respectively.
If `--page-length` is also set, these can be viewed in paginated form by appending `/n`, e.g. `/html/5` for the fifth page.
This can prove useful when confirming whether the index is generated successfully, or whether a given file is in the index.
The remaining four endpoints—`/available_extensions`, `/enabled_extensions`, `/available_mime_types` and `/enabled_mime_types`—return information about the registered file types.
The other two endpoints—`/extensions` and `/mime_types`—return the registered file types.
## Statistics
@ -94,8 +92,6 @@ 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.
If `--page-length` is also set, this can be viewed in paginated form by appending `/n`, e.g. `/stats/5` for the fifth page.
## Russian
If the `--russian` flag is passed, everything functions exactly as you would expect.
@ -115,7 +111,7 @@ Usage:
roulette <path> [path]... [flags]
Flags:
-a, --all enable all supported file types
--all enable all supported file types
--audio enable support for audio files
-b, --bind string address to bind to (default "0.0.0.0")
-c, --cache generate directory cache at startup
@ -124,7 +120,7 @@ Flags:
--flash enable support for shockwave flash files (via ruffle.rs)
-h, --help help for roulette
--images enable support for image files
-i, --info expose informational endpoints
-i, --index expose index endpoints
--maximum-files uint32 skip directories with file counts above this value (default 4294967295)
--minimum-files uint32 skip directories with file counts below this value (default 1)
--page-length uint32 pagination length for statistics and debug pages
@ -137,7 +133,7 @@ Flags:
--stats expose stats endpoint
--stats-file string path to optional persistent stats file
--text enable support for text files
-v, --verbose log accessed files and other information to stdout
-v, --verbose log accessed files to stdout
-V, --version display version and exit
--video enable support for video files
```

View File

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

View File

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

View File

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

View File

@ -27,11 +27,6 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/yosssi/gohtml"
"seedno.de/seednode/roulette/types"
"seedno.de/seednode/roulette/types/audio"
"seedno.de/seednode/roulette/types/flash"
"seedno.de/seednode/roulette/types/images"
"seedno.de/seednode/roulette/types/text"
"seedno.de/seednode/roulette/types/video"
)
const (
@ -322,25 +317,25 @@ func ServePage(args []string) error {
}
if Audio || All {
formats.Add(audio.Format{})
formats.Add(types.Audio{})
}
if Flash || All {
formats.Add(flash.Format{})
formats.Add(types.Flash{})
}
if Text || All {
formats.Add(text.Format{})
formats.Add(types.Text{})
}
if Videos || All {
formats.Add(video.Format{})
formats.Add(types.Video{})
}
// 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(images.Format{})
formats.Add(types.Images{})
}
paths, err := normalizePaths(args, formats)
@ -413,8 +408,7 @@ func ServePage(args []string) error {
mux.GET("/clear_cache", serveCacheClear(args, index, formats))
}
if Info {
if Cache {
if Index {
mux.GET("/html/", serveIndexHtml(args, index, false))
if PageLength != 0 {
mux.GET("/html/:page", serveIndexHtml(args, index, true))
@ -424,12 +418,10 @@ func ServePage(args []string) error {
if PageLength != 0 {
mux.GET("/json/:page", serveIndexJson(args, index))
}
}
mux.GET("/available_extensions", serveAvailableExtensions())
mux.GET("/enabled_extensions", serveEnabledExtensions(formats))
mux.GET("/available_mime_types", serveAvailableMimeTypes())
mux.GET("/enabled_mime_types", serveEnabledMimeTypes(formats))
mux.GET("/extensions", serveExtensions(formats))
mux.GET("/mime_types", serveMimeTypes(formats))
}
if Profile {

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Some files were not shown because too many files have changed in this diff Show More