Compare commits

...

5 Commits

11 changed files with 165 additions and 333 deletions

View File

@ -37,8 +37,7 @@ The restricted paths are:
- `/debug/pprof/trace` - `/debug/pprof/trace`
- `/extensions/available` - `/extensions/available`
- `/extensions/enabled` - `/extensions/enabled`
- `/index/html` - `/index/`
- `/index/json`
- `/index/rebuild` - `/index/rebuild`
- `/types/available` - `/types/available`
- `/types/enabled` - `/types/enabled`
@ -64,7 +63,7 @@ Both filtering parameters ignore the file extension and full path; they only com
If the `--ignore` flag is passed, any directory containing a file named `.roulette-ignore` (configurable with `--ignore-file`) will be skipped during the scanning stage. If the `--ignore` flag is passed, any directory containing a file named `.roulette-ignore` (configurable with `--ignore-file`) will be skipped during the scanning stage.
## Indexing ## Indexing
If the `-i|--indexing` flag is passed, all specified paths will be indexed on start. If the `-i|--index` flag is passed, all specified paths will be indexed on start.
This will slightly increase the delay before the application begins responding to requests, but should significantly speed up subsequent requests. This will slightly increase the delay before the application begins responding to requests, but should significantly speed up subsequent requests.
@ -81,11 +80,9 @@ Supported formats are `none`, `zlib`, and `zstd`.
Optionally, `--compression-fast` can be used to use the fastest instead of the best compression mode. Optionally, `--compression-fast` can be used to use the fastest instead of the best compression mode.
## Info ## Info
If the `-i|--info` flag is passed, six additional endpoints are registered. If the `-i|--info` flag is passed, five additional endpoints are registered.
The first of these—`/index/html` and `/index/json`—return the contents of the index, in HTML and JSON formats respectively. The first of these—`/index/`—returns the contents of the index, in JSON format.
If `--page-length` is also set, these can be viewed in paginated form by appending a page number, e.g. `/index/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. This can prove useful when confirming whether the index is generated successfully, or whether a given file is in the index.
@ -166,7 +163,6 @@ Flags:
-f, --filter enable filtering -f, --filter enable filtering
--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
--handlers display registered handlers (for debugging)
-h, --help help for roulette -h, --help help for roulette
--ignore skip all directories containing a specified filename --ignore skip all directories containing a specified filename
--ignore-file string filename used to indicate directory to be skipped (default ".roulette-ignore") --ignore-file string filename used to indicate directory to be skipped (default ".roulette-ignore")
@ -176,7 +172,6 @@ Flags:
-i, --info expose informational endpoints -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
--page-length int pagination length for info pages
-p, --port int port to listen on (default 8080) -p, --port int port to listen on (default 8080)
--prefix string root path for http handlers (for reverse proxying) (default "/") --prefix string root path for http handlers (for reverse proxying) (default "/")
--profile register net/http/pprof handlers --profile register net/http/pprof handlers

View File

@ -5,7 +5,6 @@ Copyright © 2024 Seednode <seednode@seedno.de>
package cmd package cmd
import ( import (
"bytes"
"embed" "embed"
"net/http" "net/http"
"strconv" "strconv"
@ -36,12 +35,7 @@ func serveFavicons(errorChannel chan<- error) httprouter.Handle {
return return
} }
err = w.Header().Write(bytes.NewBufferString("Content-Length: " + strconv.Itoa(len(data)))) w.Header().Set("Content-Length", strconv.Itoa(len(data)))
if err != nil {
errorChannel <- err
return
}
_, err = w.Write(data) _, err = w.Write(data)
if err != nil { if err != nil {

View File

@ -25,10 +25,6 @@ import (
"seedno.de/seednode/roulette/types" "seedno.de/seednode/roulette/types"
) )
var (
filename = regexp.MustCompile(`(.+?)([0-9]*)(\..+)`)
)
type scanStats struct { type scanStats struct {
filesMatched chan int filesMatched chan int
filesSkipped chan int filesSkipped chan int
@ -79,14 +75,14 @@ func kill(path string, index *fileIndex) error {
return nil return nil
} }
func newFile(list []string, sortOrder string, formats types.Types) (string, error) { func newFile(list []string, sortOrder string, filename *regexp.Regexp, formats types.Types) (string, error) {
path, err := pickFile(list) path, err := pickFile(list)
if err != nil { if err != nil {
return "", err return "", err
} }
if sortOrder == "asc" || sortOrder == "desc" { if sortOrder == "asc" || sortOrder == "desc" {
splitPath, err := split(path) splitPath, err := split(path, filename)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -125,8 +121,8 @@ func newFile(list []string, sortOrder string, formats types.Types) (string, erro
return path, nil return path, nil
} }
func nextFile(filePath, sortOrder string, formats types.Types) (string, error) { func nextFile(filePath, sortOrder string, filename *regexp.Regexp, formats types.Types) (string, error) {
splitPath, err := split(filePath) splitPath, err := split(filePath, filename)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -543,26 +539,33 @@ func validatePaths(args []string, formats types.Types) ([]string, error) {
switch { switch {
case pathMatches && hasSupportedFiles: case pathMatches && hasSupportedFiles:
if Verbose {
fmt.Printf("%s | PATHS: Added %s\n", fmt.Printf("%s | PATHS: Added %s\n",
time.Now().Format(logDate), time.Now().Format(logDate),
args[i], args[i],
) )
}
paths = append(paths, path) paths = append(paths, path)
case !pathMatches && hasSupportedFiles: case !pathMatches && hasSupportedFiles:
if Verbose {
fmt.Printf("%s | PATHS: Added %s [resolved to %s]\n", fmt.Printf("%s | PATHS: Added %s [resolved to %s]\n",
time.Now().Format(logDate), time.Now().Format(logDate),
args[i], args[i],
path, path,
) )
}
paths = append(paths, path) paths = append(paths, path)
case pathMatches && !hasSupportedFiles: case pathMatches && !hasSupportedFiles:
if Verbose {
fmt.Printf("%s | PATHS: Skipped %s (No supported files found)\n", fmt.Printf("%s | PATHS: Skipped %s (No supported files found)\n",
time.Now().Format(logDate), time.Now().Format(logDate),
args[i], args[i],
) )
}
case !pathMatches && !hasSupportedFiles: case !pathMatches && !hasSupportedFiles:
if Verbose {
fmt.Printf("%s | PATHS: Skipped %s [resolved to %s] (No supported files found)\n", fmt.Printf("%s | PATHS: Skipped %s [resolved to %s] (No supported files found)\n",
time.Now().Format(logDate), time.Now().Format(logDate),
args[i], args[i],
@ -570,6 +573,7 @@ func validatePaths(args []string, formats types.Types) ([]string, error) {
) )
} }
} }
}
return paths, nil return paths, nil
} }

View File

@ -9,7 +9,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"runtime"
"sync" "sync"
"time" "time"
@ -213,8 +212,6 @@ func serveIndexRebuild(args []string, index *fileIndex, formats types.Types, enc
time.Since(startTime).Round(time.Microsecond), time.Since(startTime).Round(time.Microsecond),
) )
} }
runtime.GC()
} }
} }

View File

@ -7,194 +7,28 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/yosssi/gohtml"
"seedno.de/seednode/roulette/types" "seedno.de/seednode/roulette/types"
) )
func paginateIndex(page int, fileCount int, ending bool) string { func serveIndex(args []string, index *fileIndex, errorChannel chan<- error) httprouter.Handle {
var firstPage int = 1
var lastPage int
if fileCount%PageLength == 0 {
lastPage = fileCount / PageLength
} else {
lastPage = (fileCount / PageLength) + 1
}
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 / PageLength
}
var html strings.Builder
if ending {
html.WriteString("<tr><td style=\"border-bottom:none;\">")
} else {
html.WriteString("<tr><td>")
}
html.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '%s%s/index/html/%d';\">First</button>",
Prefix,
AdminPrefix,
firstPage))
html.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '%s%s/index/html/%d';\"%s>Prev</button>",
Prefix,
AdminPrefix,
prevPage,
prevStatus))
html.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '%s%s/index/html/%d';\"%s>Next</button>",
Prefix,
AdminPrefix,
nextPage,
nextStatus))
html.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '%s%s/index/html/%d';\">Last</button>",
Prefix,
AdminPrefix,
lastPage))
html.WriteString("</td></tr>\n")
return html.String()
}
func serveIndexHtml(args []string, index *fileIndex, shouldPaginate bool) 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()
w.Header().Set("Content-Type", "text/html")
indexDump := index.List() indexDump := index.List()
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) * PageLength)
stopIndex = (startIndex + PageLength)
}
if startIndex > (fileCount - 1) {
indexDump = []string{}
}
if stopIndex > fileCount {
stopIndex = fileCount
}
sort.SliceStable(indexDump, func(p, q int) bool { sort.SliceStable(indexDump, func(p, q int) bool {
return strings.ToLower(indexDump[p]) < strings.ToLower(indexDump[q]) 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:none;}td{border-bottom:1px solid black;}td{white-space:nowrap;padding:.5em}</style>`)
htmlBody.WriteString(fmt.Sprintf("<title>Index contains %d files</title></head><body><table>", fileCount))
if shouldPaginate && !DisableButtons {
htmlBody.WriteString(paginateIndex(page, fileCount, false))
}
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\">%s</a></td></tr>\n", Prefix, mediaPrefix, v, shouldSort, v))
}
}
if shouldPaginate && !DisableButtons {
htmlBody.WriteString(paginateIndex(page, fileCount, true))
}
htmlBody.WriteString(`</table></body></html>`)
written, err := io.WriteString(w, gohtml.Format(htmlBody.String()))
if err != nil {
return
}
if Verbose {
fmt.Printf("%s | SERVE: HTML index page (%s) to %s in %s\n",
startTime.Format(logDate),
humanReadableSize(written),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}
func serveIndexJson(args []string, index *fileIndex, errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
startTime := time.Now()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
indexedFiles := index.List() response, err := json.MarshalIndent(indexDump, "", " ")
fileCount := len(indexedFiles)
sort.SliceStable(indexedFiles, func(p, q int) bool {
return strings.ToLower(indexedFiles[p]) < strings.ToLower(indexedFiles[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) * PageLength)
stopIndex = (startIndex + PageLength)
}
if startIndex > (fileCount - 1) {
indexedFiles = []string{}
}
if stopIndex > fileCount {
stopIndex = fileCount
}
response, err := json.MarshalIndent(indexedFiles[startIndex:stopIndex], "", " ")
if err != nil { if err != nil {
errorChannel <- err errorChannel <- err
@ -203,6 +37,8 @@ func serveIndexJson(args []string, index *fileIndex, errorChannel chan<- error)
return return
} }
response = append(response, []byte("\n")...)
written, err := w.Write(response) written, err := w.Write(response)
if err != nil { if err != nil {
errorChannel <- err errorChannel <- err
@ -219,35 +55,21 @@ func serveIndexJson(args []string, index *fileIndex, errorChannel chan<- error)
} }
} }
func serveAvailableExtensions(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()
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
written, err := w.Write([]byte(types.SupportedFormats.GetExtensions())) var extensions string
if err != nil {
errorChannel <- err if available {
extensions = types.SupportedFormats.GetExtensions()
} else {
extensions = formats.GetExtensions()
} }
if Verbose { written, err := w.Write([]byte(extensions))
fmt.Printf("%s | SERVE: Available extension list (%s) to %s in %s\n",
startTime.Format(logDate),
humanReadableSize(written),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}
func serveEnabledExtensions(formats types.Types, 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")
written, err := w.Write([]byte(formats.GetExtensions()))
if err != nil { if err != nil {
errorChannel <- err errorChannel <- err
} }
@ -263,13 +85,21 @@ func serveEnabledExtensions(formats types.Types, errorChannel chan<- error) http
} }
} }
func serveAvailableMediaTypes(errorChannel chan<- error) httprouter.Handle { func serveMediaTypes(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()
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
written, err := w.Write([]byte(types.SupportedFormats.GetMediaTypes())) var mediaTypes string
if available {
mediaTypes = types.SupportedFormats.GetMediaTypes()
} else {
mediaTypes = formats.GetMediaTypes()
}
written, err := w.Write([]byte(mediaTypes))
if err != nil { if err != nil {
errorChannel <- err errorChannel <- err
} }
@ -285,43 +115,13 @@ func serveAvailableMediaTypes(errorChannel chan<- error) httprouter.Handle {
} }
} }
func serveEnabledMediaTypes(formats types.Types, 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")
written, err := w.Write([]byte(formats.GetMediaTypes()))
if err != nil {
errorChannel <- err
}
if Verbose {
fmt.Printf("%s | SERVE: Registered media type list (%s) to %s in %s\n",
startTime.Format(logDate),
humanReadableSize(written),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}
func registerInfoHandlers(mux *httprouter.Router, args []string, index *fileIndex, formats types.Types, errorChannel chan<- error) { func registerInfoHandlers(mux *httprouter.Router, args []string, index *fileIndex, formats types.Types, errorChannel chan<- error) {
if Index { if Index {
registerHandler(mux, Prefix+AdminPrefix+"/index/html", serveIndexHtml(args, index, false)) mux.GET(Prefix+AdminPrefix+"/index", serveIndex(args, index, errorChannel))
if PageLength != 0 {
registerHandler(mux, Prefix+AdminPrefix+"/index/html/:page", serveIndexHtml(args, index, true))
} }
registerHandler(mux, Prefix+AdminPrefix+"/index/json", serveIndexJson(args, index, errorChannel)) mux.GET(Prefix+AdminPrefix+"/extensions/available", serveExtensions(formats, true, errorChannel))
if PageLength != 0 { mux.GET(Prefix+AdminPrefix+"/extensions/enabled", serveExtensions(formats, false, errorChannel))
registerHandler(mux, Prefix+AdminPrefix+"/index/json/:page", serveIndexJson(args, index, errorChannel)) mux.GET(Prefix+AdminPrefix+"/types/available", serveMediaTypes(formats, true, errorChannel))
} mux.GET(Prefix+AdminPrefix+"/types/enabled", serveMediaTypes(formats, false, errorChannel))
}
registerHandler(mux, Prefix+AdminPrefix+"/extensions/available", serveAvailableExtensions(errorChannel))
registerHandler(mux, Prefix+AdminPrefix+"/extensions/enabled", serveEnabledExtensions(formats, errorChannel))
registerHandler(mux, Prefix+AdminPrefix+"/types/available", serveAvailableMediaTypes(errorChannel))
registerHandler(mux, Prefix+AdminPrefix+"/types/enabled", serveEnabledMediaTypes(formats, errorChannel))
} }

View File

@ -17,7 +17,7 @@ import (
const ( const (
AllowedCharacters string = `^[A-z0-9.\-_]+$` AllowedCharacters string = `^[A-z0-9.\-_]+$`
ReleaseVersion string = "5.4.3" ReleaseVersion string = "6.1.1"
) )
var ( var (
@ -38,7 +38,6 @@ var (
Filtering bool Filtering bool
Flash bool Flash bool
Fun bool Fun bool
Handlers bool
Ignore bool Ignore bool
IgnoreFile string IgnoreFile string
Images bool Images bool
@ -47,12 +46,10 @@ var (
Info bool Info bool
MaxFileCount int MaxFileCount int
MinFileCount int MinFileCount int
PageLength int
Port int Port int
Prefix string Prefix string
Profile bool Profile bool
Recursive bool Recursive bool
Redact bool
Refresh bool Refresh bool
Russian bool Russian bool
Sorting bool Sorting bool
@ -126,7 +123,7 @@ func init() {
rootCmd.Flags().BoolVar(&CaseSensitive, "case-sensitive", false, "use case-sensitive matching for filters") rootCmd.Flags().BoolVar(&CaseSensitive, "case-sensitive", false, "use case-sensitive matching for filters")
rootCmd.Flags().BoolVar(&Code, "code", false, "enable support for source code files") rootCmd.Flags().BoolVar(&Code, "code", false, "enable support for source code files")
rootCmd.Flags().StringVar(&CodeTheme, "code-theme", "solarized-dark256", "theme for source code syntax highlighting") rootCmd.Flags().StringVar(&CodeTheme, "code-theme", "solarized-dark256", "theme for source code syntax highlighting")
rootCmd.Flags().IntVar(&Concurrency, "concurrency", 10240, "maximum concurrency for scan threads") rootCmd.Flags().IntVar(&Concurrency, "concurrency", 1024, "maximum concurrency for scan threads")
rootCmd.Flags().BoolVarP(&Debug, "debug", "d", false, "display even more verbose logs") rootCmd.Flags().BoolVarP(&Debug, "debug", "d", false, "display even more verbose logs")
rootCmd.Flags().BoolVar(&DisableButtons, "disable-buttons", false, "disable first/prev/next/last buttons") rootCmd.Flags().BoolVar(&DisableButtons, "disable-buttons", false, "disable first/prev/next/last buttons")
rootCmd.Flags().BoolVar(&ExitOnError, "exit-on-error", false, "shut down webserver on error, instead of just printing error") rootCmd.Flags().BoolVar(&ExitOnError, "exit-on-error", false, "shut down webserver on error, instead of just printing error")
@ -134,21 +131,18 @@ 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().BoolVar(&Handlers, "handlers", false, "display registered handlers (for debugging)")
rootCmd.Flags().BoolVar(&Ignore, "ignore", false, "skip all directories containing a specified filename") 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 to be skipped") 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().BoolVarP(&Info, "info", "i", false, "expose informational endpoints") 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().IntVar(&PageLength, "page-length", 0, "pagination length for info pages")
rootCmd.Flags().IntVarP(&Port, "port", "p", 8080, "port to listen on") rootCmd.Flags().IntVarP(&Port, "port", "p", 8080, "port to listen on")
rootCmd.Flags().StringVar(&Prefix, "prefix", "/", "root path for http handlers (for reverse proxying)") rootCmd.Flags().StringVar(&Prefix, "prefix", "/", "root path for http handlers (for reverse proxying)")
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().BoolVar(&Redact, "redact", false, "redact admin prefix in log output")
rootCmd.Flags().BoolVar(&Refresh, "refresh", false, "enable automatic page refresh via query parameter") rootCmd.Flags().BoolVar(&Refresh, "refresh", false, "enable automatic page refresh via query parameter")
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")

View File

@ -6,6 +6,7 @@ package cmd
import ( import (
"fmt" "fmt"
"regexp"
"sort" "sort"
"strings" "strings"
@ -38,7 +39,7 @@ func (splitPath *splitPath) decrement() string {
return fmt.Sprintf("%0*d", len(splitPath.number), asInt-1) return fmt.Sprintf("%0*d", len(splitPath.number), asInt-1)
} }
func split(path string) (*splitPath, error) { func split(path string, filename *regexp.Regexp) (*splitPath, error) {
split := filename.FindAllStringSubmatch(path, -1) split := filename.FindAllStringSubmatch(path, -1)
if len(split) < 1 || len(split[0]) < 3 { if len(split) < 1 || len(split[0]) < 3 {
@ -54,8 +55,8 @@ func split(path string) (*splitPath, error) {
return p, nil return p, nil
} }
func getRange(path string, index *fileIndex) (string, string, error) { func getRange(path string, index *fileIndex, filename *regexp.Regexp) (string, string, error) {
splitPath, err := split(path) splitPath, err := split(path, filename)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@ -70,7 +71,7 @@ func getRange(path string, index *fileIndex) (string, string, error) {
Loop: Loop:
for _, val := range list { for _, val := range list {
splitVal, err := split(val) splitVal, err := split(val, filename)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@ -94,8 +95,8 @@ func pathUrlEscape(path string) string {
return strings.Replace(path, `'`, `%27`, -1) return strings.Replace(path, `'`, `%27`, -1)
} }
func paginateSorted(path, first, last, queryParams string, formats types.Types) (string, error) { func paginate(path, first, last, queryParams string, filename *regexp.Regexp, formats types.Types) (string, error) {
split, err := split(path) split, err := split(path, filename)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -5,7 +5,6 @@ Copyright © 2024 Seednode <seednode@seedno.de>
package cmd package cmd
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -13,6 +12,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@ -32,8 +32,6 @@ import (
"seedno.de/seednode/roulette/types/video" "seedno.de/seednode/roulette/types/video"
) )
var ()
const ( const (
logDate string = `2006-01-02T15:04:05.000-07:00` logDate string = `2006-01-02T15:04:05.000-07:00`
sourcePrefix string = `/source` sourcePrefix string = `/source`
@ -171,7 +169,7 @@ func serveStaticFile(paths []string, index *fileIndex, errorChannel chan<- error
} }
} }
func serveRoot(paths []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) httprouter.Handle { func serveRoot(paths []string, index *fileIndex, filename *regexp.Regexp, formats types.Types, encoder *zstd.Encoder, 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) {
refererUri, err := stripQueryParams(refererToUri(r.Referer())) refererUri, err := stripQueryParams(refererToUri(r.Referer()))
if err != nil { if err != nil {
@ -196,7 +194,7 @@ func serveRoot(paths []string, index *fileIndex, formats types.Types, encoder *z
var path string var path string
if refererUri != "" { if refererUri != "" {
path, err = nextFile(strippedRefererUri, sortOrder, formats) path, err = nextFile(strippedRefererUri, sortOrder, filename, formats)
if err != nil { if err != nil {
errorChannel <- err errorChannel <- err
@ -220,7 +218,7 @@ func serveRoot(paths []string, index *fileIndex, formats types.Types, encoder *z
break loop break loop
} }
path, err = newFile(list, sortOrder, formats) path, err = newFile(list, sortOrder, filename, formats)
switch { switch {
case path == "": case path == "":
noFiles(w, r) noFiles(w, r)
@ -251,7 +249,7 @@ func serveRoot(paths []string, index *fileIndex, formats types.Types, encoder *z
} }
} }
func serveMedia(paths []string, index *fileIndex, formats types.Types, errorChannel chan<- error) httprouter.Handle { func serveMedia(paths []string, index *fileIndex, filename *regexp.Regexp, formats types.Types, 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()
@ -314,7 +312,7 @@ func serveMedia(paths []string, index *fileIndex, formats types.Types, errorChan
return return
} }
mediaType := format.MediaType(path) mediaType := format.MediaType(filepath.Ext(path))
fileUri := Prefix + generateFileUri(path) fileUri := Prefix + generateFileUri(path)
@ -347,7 +345,7 @@ func serveMedia(paths []string, index *fileIndex, formats types.Types, errorChan
var first, last string var first, last string
if Index && sortOrder != "" { if Index && sortOrder != "" {
first, last, err = getRange(path, index) first, last, err = getRange(path, index, filename)
if err != nil { if err != nil {
errorChannel <- err errorChannel <- err
@ -358,7 +356,7 @@ func serveMedia(paths []string, index *fileIndex, formats types.Types, errorChan
} }
if Index && !DisableButtons && sortOrder != "" { if Index && !DisableButtons && sortOrder != "" {
paginate, err := paginateSorted(path, first, last, queryParams, formats) paginated, err := paginate(path, first, last, queryParams, filename, formats)
if err != nil { if err != nil {
errorChannel <- err errorChannel <- err
@ -367,7 +365,7 @@ func serveMedia(paths []string, index *fileIndex, formats types.Types, errorChan
return return
} }
htmlBody.WriteString(paginate) htmlBody.WriteString(paginated)
} }
if refreshInterval != "0ms" { if refreshInterval != "0ms" {
@ -388,7 +386,7 @@ func serveMedia(paths []string, index *fileIndex, formats types.Types, errorChan
formattedPage := gohtml.Format(htmlBody.String()) formattedPage := gohtml.Format(htmlBody.String())
written, err := io.WriteString(w, formattedPage) written, err := io.WriteString(w, formattedPage+"\n")
if err != nil { if err != nil {
errorChannel <- err errorChannel <- err
@ -426,12 +424,7 @@ func serveVersion(errorChannel chan<- error) httprouter.Handle {
data := []byte(fmt.Sprintf("roulette v%s\n", ReleaseVersion)) data := []byte(fmt.Sprintf("roulette v%s\n", ReleaseVersion))
err := w.Header().Write(bytes.NewBufferString("Content-Length: " + strconv.Itoa(len(data)))) w.Header().Set("Content-Length", strconv.Itoa(len(data)))
if err != nil {
errorChannel <- err
return
}
written, err := w.Write(data) written, err := w.Write(data)
if err != nil { if err != nil {
@ -451,21 +444,6 @@ func serveVersion(errorChannel chan<- error) httprouter.Handle {
} }
} }
func registerHandler(mux *httprouter.Router, path string, handle httprouter.Handle) {
mux.GET(path, handle)
if Redact && AdminPrefix != "" {
path = strings.ReplaceAll(path, AdminPrefix, "/<admin_prefix>")
}
if Handlers {
fmt.Printf("%s | SERVE: Registered handler for %s\n",
time.Now().Format(logDate),
path,
)
}
}
func redirectRoot() httprouter.Handle { func redirectRoot() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
newUrl := fmt.Sprintf("http://%s%s", newUrl := fmt.Sprintf("http://%s%s",
@ -539,10 +517,6 @@ func ServePage(args []string) error {
return ErrNoMediaFound return ErrNoMediaFound
} }
if !strings.HasSuffix(Prefix, "/") {
Prefix = Prefix + "/"
}
listenHost := net.JoinHostPort(Bind, strconv.Itoa(Port)) listenHost := net.JoinHostPort(Bind, strconv.Itoa(Port))
index := &fileIndex{ index := &fileIndex{
@ -584,26 +558,32 @@ func ServePage(args []string) error {
return err return err
} }
registerHandler(mux, Prefix, serveRoot(paths, index, formats, encoder, errorChannel)) filename := regexp.MustCompile(`(.+?)([0-9]*)(\..+)`)
if !strings.HasSuffix(Prefix, "/") {
Prefix = Prefix + "/"
}
mux.GET(Prefix, serveRoot(paths, index, filename, formats, encoder, errorChannel))
Prefix = strings.TrimSuffix(Prefix, "/") Prefix = strings.TrimSuffix(Prefix, "/")
if Prefix != "" { if Prefix != "" {
registerHandler(mux, "/", redirectRoot()) mux.GET("/", redirectRoot())
} }
registerHandler(mux, Prefix+"/favicons/*favicon", serveFavicons(errorChannel)) mux.GET(Prefix+"/favicons/*favicon", serveFavicons(errorChannel))
registerHandler(mux, Prefix+"/favicon.ico", serveFavicons(errorChannel)) mux.GET(Prefix+"/favicon.ico", serveFavicons(errorChannel))
registerHandler(mux, Prefix+mediaPrefix+"/*media", serveMedia(paths, index, formats, errorChannel)) mux.GET(Prefix+mediaPrefix+"/*media", serveMedia(paths, index, filename, formats, errorChannel))
registerHandler(mux, Prefix+sourcePrefix+"/*static", serveStaticFile(paths, index, errorChannel)) mux.GET(Prefix+sourcePrefix+"/*static", serveStaticFile(paths, index, errorChannel))
registerHandler(mux, Prefix+"/version", serveVersion(errorChannel)) mux.GET(Prefix+"/version", serveVersion(errorChannel))
if Index { if Index {
registerHandler(mux, Prefix+AdminPrefix+"/index/rebuild", serveIndexRebuild(args, index, formats, encoder, errorChannel)) mux.GET(Prefix+AdminPrefix+"/index/rebuild", serveIndexRebuild(args, index, formats, encoder, errorChannel))
importIndex(paths, index, formats, encoder, errorChannel) importIndex(paths, index, formats, encoder, errorChannel)
} }

48
docker/Dockerfile.debug Normal file
View File

@ -0,0 +1,48 @@
# set app name
ARG app=roulette
# create build stage
ARG TAG
FROM --platform=$BUILDPLATFORM golang:$TAG AS build
ARG app
# install dependencies
RUN apk add --update-cache git upx
# clone
RUN git clone https://git.seedno.de/seednode/$app /src/$app
# build and compress the binary
WORKDIR /src/$app
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 \
GOOS=$TARGETOS \
GOARCH=$TARGETARCH \
go build -trimpath -ldflags "-s -w" -o $app \
&& upx --best --lzma $app \
&& chmod 500 $app
# set up final stage
FROM --platform=$BUILDPLATFORM alpine:latest
ARG app
# copy in user info
COPY --chown=root:root --chmod=0400 passwd /etc/passwd
# run as nonroot
USER root
# copy in binary
COPY --from=build --chown=root:root --chmod=0005 /src/$app/$app /$app
# copy in time zone info
COPY --from=build --chown=root:root --chmod=0004 /usr/local/go/lib/time/zoneinfo.zip /
# load time zone info
ENV ZONEINFO=/zoneinfo.zip
# listen on an unprivileged port
EXPOSE 8080
# run application
ENTRYPOINT ["/bin/ash"]

View File

@ -37,3 +37,19 @@ docker buildx build --platform "${platforms}" \
$(if [ "${LATEST}" == "yes" ]; then echo "-t ${registry}/${image_name}:latest"; fi) \ $(if [ "${LATEST}" == "yes" ]; then echo "-t ${registry}/${image_name}:latest"; fi) \
-f Dockerfile . \ -f Dockerfile . \
--push --push
# copy debug image to local image repository
docker buildx build \
--build-arg TAG="${tag}" \
-t "${registry}/${image_name}:${image_version}-debug" \
$(if [ "${LATEST}" == "yes" ]; then echo "-t ${registry}/${image_name}:debug"; fi) \
-f Dockerfile.debug . \
--load
# push debug image to remote registry
docker buildx build --platform "${platforms}" \
--build-arg TAG="${tag}" \
-t "${registry}/${image_name}:${image_version}-debug" \
$(if [ "${LATEST}" == "yes" ]; then echo "-t ${registry}/${image_name}:debug"; fi) \
-f Dockerfile.debug . \
--push

View File

@ -99,10 +99,13 @@ func (t Types) GetMediaTypes() string {
for _, j := range t { for _, j := range t {
extensions := j.Extensions() extensions := j.Extensions()
for _, v := range extensions { for _, v := range extensions {
if v != "" {
mediaTypes = append(mediaTypes, v) mediaTypes = append(mediaTypes, v)
} }
} }
}
mediaTypes = removeDuplicateStr(mediaTypes) mediaTypes = removeDuplicateStr(mediaTypes)