Compare commits
6 Commits
7942ea85b5
...
6fea978459
Author | SHA1 | Date |
---|---|---|
Seednode | 6fea978459 | |
Seednode | c0265d2a6e | |
Seednode | f872d0a27e | |
Seednode | d18dc9a41c | |
Seednode | c996f3fc53 | |
Seednode | a29390aa76 |
12
README.md
12
README.md
|
@ -78,17 +78,16 @@ 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.
|
||||||
|
|
||||||
|
@ -113,10 +112,11 @@ 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
188
cmd/debug.go
|
@ -1,188 +0,0 @@
|
||||||
/*
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
|
28
cmd/files.go
28
cmd/files.go
|
@ -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 *Index, registeredFormats *formats.SupportedFormats) (string, error) {
|
func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexes, index *FileIndex, 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,8 +272,10 @@ 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 _, extension := range registeredFormats.Extensions() {
|
for _, format := range registeredFormats.Extensions {
|
||||||
fileName = fmt.Sprintf("%s%.3d%s", p.base, p.number, extension)
|
for _, extension := range format.Extensions {
|
||||||
|
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 {
|
||||||
|
@ -310,7 +312,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,
|
||||||
|
@ -333,7 +335,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)
|
||||||
|
@ -390,7 +392,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)
|
||||||
|
@ -419,7 +421,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)
|
||||||
|
@ -442,8 +444,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 *Index, types *formats.SupportedFormats) ([]string, bool) {
|
func fileList(paths []string, filters *Filters, sort string, index *FileIndex, 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -496,7 +498,7 @@ func fileList(paths []string, filters *Filters, sort string, index *Index, types
|
||||||
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(),
|
||||||
|
@ -507,7 +509,7 @@ func fileList(paths []string, filters *Filters, sort string, index *Index, types
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cache && filters.IsEmpty() {
|
if Cache && filters.IsEmpty() {
|
||||||
index.setIndex(fileList)
|
index.setIndex(fileList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -556,7 +558,7 @@ func prepareDirectories(files *Files, sort string) []string {
|
||||||
return directories
|
return directories
|
||||||
}
|
}
|
||||||
|
|
||||||
func pickFile(args []string, filters *Filters, sort string, index *Index, registeredFormats *formats.SupportedFormats) (string, error) {
|
func pickFile(args []string, filters *Filters, sort string, index *FileIndex, 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)
|
||||||
|
|
199
cmd/index.go
199
cmd/index.go
|
@ -6,21 +6,29 @@ 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 Index struct {
|
type FileIndex struct {
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
list []string
|
list []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Index) Index() []string {
|
func (i *FileIndex) Index() []string {
|
||||||
i.mutex.RLock()
|
i.mutex.RLock()
|
||||||
val := i.list
|
val := i.list
|
||||||
i.mutex.RUnlock()
|
i.mutex.RUnlock()
|
||||||
|
@ -28,7 +36,7 @@ func (i *Index) Index() []string {
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Index) Remove(path string) {
|
func (i *FileIndex) 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)
|
||||||
|
@ -52,25 +60,25 @@ func (i *Index) Remove(path string) {
|
||||||
i.mutex.Unlock()
|
i.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Index) setIndex(val []string) {
|
func (i *FileIndex) setIndex(val []string) {
|
||||||
i.mutex.Lock()
|
i.mutex.Lock()
|
||||||
i.list = val
|
i.list = val
|
||||||
i.mutex.Unlock()
|
i.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Index) generateCache(args []string, registeredFormats *formats.SupportedFormats) {
|
func (i *FileIndex) 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 *Index) IsEmpty() bool {
|
func (i *FileIndex) IsEmpty() bool {
|
||||||
i.mutex.RLock()
|
i.mutex.RLock()
|
||||||
length := len(i.list)
|
length := len(i.list)
|
||||||
i.mutex.RUnlock()
|
i.mutex.RUnlock()
|
||||||
|
@ -78,7 +86,7 @@ func (i *Index) IsEmpty() bool {
|
||||||
return length == 0
|
return length == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Index) Export(path string) error {
|
func (i *FileIndex) 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
|
||||||
|
@ -102,7 +110,7 @@ func (i *Index) Export(path string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Index) Import(path string) error {
|
func (i *FileIndex) 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
|
||||||
|
@ -130,7 +138,7 @@ func (i *Index) Import(path string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveCacheClear(args []string, index *Index, registeredFormats *formats.SupportedFormats) httprouter.Handle {
|
func serveCacheClear(args []string, index *FileIndex, 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)
|
||||||
|
|
||||||
|
@ -139,3 +147,172 @@ func serveCacheClear(args []string, index *Index, registeredFormats *formats.Sup
|
||||||
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
110
cmd/root.go
110
cmd/root.go
|
@ -12,45 +12,52 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version string = "0.69.4"
|
ReleaseVersion string = "0.70.3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
all bool
|
All bool
|
||||||
audio bool
|
Audio bool
|
||||||
bind string
|
Bind string
|
||||||
cache bool
|
Cache bool
|
||||||
cacheFile string
|
CacheFile string
|
||||||
debug bool
|
Filtering bool
|
||||||
filtering bool
|
Flash bool
|
||||||
images bool
|
Images bool
|
||||||
maximumFileCount uint64
|
Index bool
|
||||||
minimumFileCount uint64
|
MaximumFileCount uint64
|
||||||
pageLength uint64
|
MinimumFileCount uint64
|
||||||
port uint16
|
PageLength uint64
|
||||||
profile bool
|
Port uint16
|
||||||
recursive bool
|
Profile bool
|
||||||
refreshInterval string
|
Recursive bool
|
||||||
russian bool
|
RefreshInterval string
|
||||||
sorting bool
|
Russian bool
|
||||||
statistics bool
|
Sorting bool
|
||||||
statisticsFile string
|
Statistics bool
|
||||||
text bool
|
StatisticsFile string
|
||||||
verbose bool
|
Text bool
|
||||||
version bool
|
Verbose bool
|
||||||
videos bool
|
Version 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) {
|
||||||
if debug {
|
// enable image support if no other flags are passed, to retain backwards compatibility
|
||||||
|
// to be replaced with MarkFlagsOneRequired on next spf13/cobra update
|
||||||
|
if !(All || Audio || Flash || Images || Text || Videos) {
|
||||||
|
Images = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if Index {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -75,29 +82,30 @@ 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(&debug, "debug", "d", false, "expose debug endpoint")
|
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(&images, "images", true, "enable support for image files")
|
rootCmd.Flags().BoolVar(&Images, "images", false, "enable support for image files")
|
||||||
rootCmd.Flags().Uint64Var(&maximumFileCount, "maximum-files", 1<<64-1, "skip directories with file counts above this value")
|
rootCmd.Flags().BoolVarP(&Index, "index", "i", false, "expose index endpoints")
|
||||||
rootCmd.Flags().Uint64Var(&minimumFileCount, "minimum-files", 1, "skip directories with file counts below this value")
|
rootCmd.Flags().Uint64Var(&MaximumFileCount, "maximum-files", 1<<64-1, "skip directories with file counts above this value")
|
||||||
rootCmd.Flags().Uint64Var(&pageLength, "page-length", 0, "pagination length for statistics and debug pages")
|
rootCmd.Flags().Uint64Var(&MinimumFileCount, "minimum-files", 1, "skip directories with file counts below this value")
|
||||||
rootCmd.Flags().Uint16VarP(&port, "port", "p", 8080, "port to listen on")
|
rootCmd.Flags().Uint64Var(&PageLength, "page-length", 0, "pagination length for statistics and debug pages")
|
||||||
rootCmd.Flags().BoolVar(&profile, "profile", false, "register net/http/pprof handlers")
|
rootCmd.Flags().Uint16VarP(&Port, "port", "p", 8080, "port to listen on")
|
||||||
rootCmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "recurse into subdirectories")
|
rootCmd.Flags().BoolVar(&Profile, "profile", false, "register net/http/pprof handlers")
|
||||||
rootCmd.Flags().StringVar(&refreshInterval, "refresh-interval", "", "force refresh interval equal to this duration (minimum 500ms)")
|
rootCmd.Flags().BoolVarP(&Recursive, "recursive", "r", false, "recurse into subdirectories")
|
||||||
rootCmd.Flags().BoolVar(&russian, "russian", false, "remove selected images after serving")
|
rootCmd.Flags().StringVar(&RefreshInterval, "refresh-interval", "", "force refresh interval equal to this duration (minimum 500ms)")
|
||||||
rootCmd.Flags().BoolVarP(&sorting, "sort", "s", false, "enable sorting")
|
rootCmd.Flags().BoolVar(&Russian, "russian", false, "remove selected images after serving")
|
||||||
rootCmd.Flags().BoolVar(&statistics, "stats", false, "expose stats endpoint")
|
rootCmd.Flags().BoolVarP(&Sorting, "sort", "s", false, "enable sorting")
|
||||||
rootCmd.Flags().StringVar(&statisticsFile, "stats-file", "", "path to optional persistent stats file")
|
rootCmd.Flags().BoolVar(&Statistics, "stats", false, "expose stats endpoint")
|
||||||
rootCmd.Flags().BoolVar(&text, "text", false, "enable support for text files")
|
rootCmd.Flags().StringVar(&StatisticsFile, "stats-file", "", "path to optional persistent stats file")
|
||||||
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "log accessed files to stdout")
|
rootCmd.Flags().BoolVar(&Text, "text", false, "enable support for text files")
|
||||||
rootCmd.Flags().BoolVarP(&version, "version", "V", false, "display version and exit")
|
rootCmd.Flags().BoolVarP(&Verbose, "verbose", "v", false, "log accessed files to stdout")
|
||||||
rootCmd.Flags().BoolVar(&videos, "video", false, "enable support for video files")
|
rootCmd.Flags().BoolVarP(&Version, "version", "V", false, "display version and exit")
|
||||||
|
rootCmd.Flags().BoolVar(&Videos, "video", false, "enable support for video files")
|
||||||
|
|
||||||
rootCmd.Flags().SetInterspersed(true)
|
rootCmd.Flags().SetInterspersed(true)
|
||||||
|
|
||||||
|
@ -109,5 +117,5 @@ func init() {
|
||||||
})
|
})
|
||||||
|
|
||||||
rootCmd.SetVersionTemplate("roulette v{{.Version}}\n")
|
rootCmd.SetVersionTemplate("roulette v{{.Version}}\n")
|
||||||
rootCmd.Version = Version
|
rootCmd.Version = ReleaseVersion
|
||||||
}
|
}
|
||||||
|
|
10
cmd/stats.go
10
cmd/stats.go
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
cmd/uri.go
10
cmd/uri.go
|
@ -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("&")
|
||||||
}
|
}
|
||||||
|
|
81
cmd/web.go
81
cmd/web.go
|
@ -37,7 +37,7 @@ const (
|
||||||
Timeout time.Duration = 10 * time.Second
|
Timeout time.Duration = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveStaticFile(paths []string, stats *ServeStats, index *Index) httprouter.Handle {
|
func serveStaticFile(paths []string, stats *ServeStats, index *FileIndex) 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 *Index) httprouter
|
||||||
|
|
||||||
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 *Index) httprouter
|
||||||
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 *Index) httprouter
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if statistics {
|
if Statistics {
|
||||||
stats.incrementCounter(filePath, startTime, fileSize)
|
stats.incrementCounter(filePath, startTime, fileSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveRoot(paths []string, Regexes *Regexes, index *Index, registeredFormats *formats.SupportedFormats) httprouter.Handle {
|
func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, 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 *Index, registeredFormats
|
||||||
|
|
||||||
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 *Index, registeredFormats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveMedia(paths []string, Regexes *Regexes, index *Index, registeredFormats *formats.SupportedFormats) httprouter.Handle {
|
func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, 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 *Index, registeredFormat
|
||||||
|
|
||||||
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 *Index, registeredFormat
|
||||||
|
|
||||||
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", Version))
|
data := []byte(fmt.Sprintf("roulette v%s\n", ReleaseVersion))
|
||||||
|
|
||||||
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,21 +314,30 @@ func ServePage(args []string) error {
|
||||||
return errors.New("invalid bind address provided")
|
return errors.New("invalid bind address provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
registeredFormats := &formats.SupportedFormats{}
|
mux := httprouter.New()
|
||||||
|
|
||||||
if audio || all {
|
registeredFormats := &formats.SupportedFormats{
|
||||||
|
Extensions: make(map[string]*formats.SupportedFormat),
|
||||||
|
MimeTypes: make(map[string]*formats.SupportedFormat),
|
||||||
|
}
|
||||||
|
|
||||||
|
if Audio || All {
|
||||||
registeredFormats.Add(formats.RegisterAudioFormats())
|
registeredFormats.Add(formats.RegisterAudioFormats())
|
||||||
}
|
}
|
||||||
|
|
||||||
if images || all {
|
if Flash || 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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,13 +350,11 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := httprouter.New()
|
index := &FileIndex{
|
||||||
|
|
||||||
index := &Index{
|
|
||||||
mutex: sync.RWMutex{},
|
mutex: sync.RWMutex{},
|
||||||
list: []string{},
|
list: []string{},
|
||||||
}
|
}
|
||||||
|
@ -358,7 +365,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,
|
||||||
|
@ -387,11 +394,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
|
||||||
}
|
}
|
||||||
|
@ -404,19 +411,19 @@ func ServePage(args []string) error {
|
||||||
mux.GET("/clear_cache", serveCacheClear(args, index, registeredFormats))
|
mux.GET("/clear_cache", serveCacheClear(args, index, registeredFormats))
|
||||||
}
|
}
|
||||||
|
|
||||||
if debug {
|
if Index {
|
||||||
mux.GET("/html/", serveDebugHtml(args, index, false))
|
mux.GET("/html/", serveIndexHtml(args, index, false))
|
||||||
if pageLength != 0 {
|
if PageLength != 0 {
|
||||||
mux.GET("/html/:page", serveDebugHtml(args, index, true))
|
mux.GET("/html/:page", serveIndexHtml(args, index, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
mux.GET("/json", serveDebugJson(args, index))
|
mux.GET("/json", serveIndexJson(args, index))
|
||||||
if pageLength != 0 {
|
if PageLength != 0 {
|
||||||
mux.GET("/json/:page", serveDebugJson(args, index))
|
mux.GET("/json/:page", serveIndexJson(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)
|
||||||
|
@ -424,9 +431,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)
|
||||||
|
@ -434,14 +441,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
func RegisterAudioFormats() *SupportedFormat {
|
func RegisterAudioFormats() *SupportedFormat {
|
||||||
return &SupportedFormat{
|
return &SupportedFormat{
|
||||||
Css: ``,
|
Css: ``,
|
||||||
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
|
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||||
return fmt.Sprintf(`<title>%s</title>`, fileName)
|
return fmt.Sprintf(`<title>%s</title>`, fileName)
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ type Dimensions struct {
|
||||||
|
|
||||||
func RegisterImageFormats() *SupportedFormat {
|
func RegisterImageFormats() *SupportedFormat {
|
||||||
return &SupportedFormat{
|
return &SupportedFormat{
|
||||||
Css: ``,
|
Css: ``,
|
||||||
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
|
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||||
dimensions, err := ImageDimensions(filePath)
|
dimensions, err := ImageDimensions(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -39,7 +39,6 @@ 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`,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SupportedFormat struct {
|
type SupportedFormat struct {
|
||||||
|
@ -20,46 +21,27 @@ type SupportedFormat struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedFormats struct {
|
type SupportedFormats struct {
|
||||||
types []*SupportedFormat
|
Extensions map[string]*SupportedFormat
|
||||||
|
MimeTypes map[string]*SupportedFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SupportedFormats) Add(t *SupportedFormat) {
|
func (s *SupportedFormats) Add(t *SupportedFormat) {
|
||||||
s.types = append(s.types, t)
|
for _, v := range t.Extensions {
|
||||||
}
|
_, exists := s.Extensions[v]
|
||||||
|
if !exists {
|
||||||
func (s *SupportedFormats) Extensions() []string {
|
s.Extensions[v] = t
|
||||||
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
|
for _, v := range t.MimeTypes {
|
||||||
|
_, exists := s.Extensions[v]
|
||||||
|
if !exists {
|
||||||
|
s.MimeTypes[v] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FileType(path string, types *SupportedFormats) (bool, *SupportedFormat, string, error) {
|
func FileType(path string, registeredFormats *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):
|
||||||
|
@ -74,12 +56,16 @@ func FileType(path string, types *SupportedFormats) (bool, *SupportedFormat, str
|
||||||
|
|
||||||
mimeType := http.DetectContentType(head)
|
mimeType := http.DetectContentType(head)
|
||||||
|
|
||||||
for _, v := range types.MimeTypes() {
|
// try identifying files by mime types first
|
||||||
if mimeType == v {
|
fileType, exists := registeredFormats.MimeTypes[mimeType]
|
||||||
fileType := types.Type(mimeType)
|
if exists {
|
||||||
|
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
|
||||||
|
|
Loading…
Reference in New Issue