Compare commits

...

6 Commits

9 changed files with 175 additions and 137 deletions

View File

@ -38,7 +38,7 @@ You can also provide a comma-delimited string of alphanumeric patterns to exclud
Filenames matching any of these patterns will not be served. Filenames matching any of these patterns will not be served.
You can also combine these two parameters, with exclusions taking priority over inclusions. You can combine these two parameters, with exclusions taking priority over inclusions.
Both filtering parameters ignore the file extension and full path; they only compare against the bare filename. Both filtering parameters ignore the file extension and full path; they only compare against the bare filename.
@ -77,7 +77,7 @@ Enjoy!
## Sorting ## Sorting
You can specify a sorting pattern via the `sort=` query parameter, assuming the `-s|--sort` flag is enabled. You can specify a sorting direction via the `sort=` query parameter, assuming the `-s|--sort` flag is enabled.
A value of `sort=asc` means files will be served in ascending order (lowest-numbered to highest). A value of `sort=asc` means files will be served in ascending order (lowest-numbered to highest).
@ -95,7 +95,7 @@ For `sort=desc`, the highest-numbered file will be served instead.
If any other (or no) value is provided, the selected file will be random. If any other (or no) value is provided, the selected file will be random.
Note: These patterns require sequentially-numbered files matching the following pattern: `filename###.extension`. Note: These options require sequentially-numbered files matching the following pattern: `filename[0-9]*.extension`.
## Themes ## Themes
The `--code` handler provides syntax highlighting via [alecthomas/chroma](https://github.com/alecthomas/chroma). The `--code` handler provides syntax highlighting via [alecthomas/chroma](https://github.com/alecthomas/chroma).

View File

@ -109,9 +109,10 @@ func (cache *fileCache) Export(path string) error {
cache.mutex.RUnlock() cache.mutex.RUnlock()
if Verbose { if Verbose {
fmt.Printf("%s | CACHE: Exported %d entries in %s\n", fmt.Printf("%s | CACHE: Exported %d entries to %s in %s\n",
time.Now().Format(logDate), time.Now().Format(logDate),
length, length,
path,
time.Since(startTime), time.Since(startTime),
) )
} }
@ -146,9 +147,10 @@ func (cache *fileCache) Import(path string) error {
} }
if Verbose { if Verbose {
fmt.Printf("%s | CACHE: Imported %d entries in %s\n", fmt.Printf("%s | CACHE: Imported %d entries from %s in %s\n",
time.Now().Format(logDate), time.Now().Format(logDate),
length, length,
path,
time.Since(startTime), time.Since(startTime),
) )
} }
@ -174,7 +176,7 @@ func serveCacheClear(args []string, cache *fileCache, formats *types.Types, erro
} }
func registerCacheHandlers(mux *httprouter.Router, args []string, cache *fileCache, formats *types.Types, errorChannel chan<- error) error { func registerCacheHandlers(mux *httprouter.Router, args []string, cache *fileCache, formats *types.Types, errorChannel chan<- error) error {
register(mux, Prefix+"/clear_cache", serveCacheClear(args, cache, formats, errorChannel)) registerHandler(mux, Prefix+"/clear_cache", serveCacheClear(args, cache, formats, errorChannel))
return nil return nil
} }

View File

@ -40,20 +40,6 @@ type scanStatsChannels struct {
directoriesSkipped chan int directoriesSkipped chan int
} }
type splitPath struct {
base string
number int
extension string
}
func (splitPath *splitPath) increment() {
splitPath.number = splitPath.number + 1
}
func (splitPath *splitPath) decrement() {
splitPath.number = splitPath.number - 1
}
func humanReadableSize(bytes int) string { func humanReadableSize(bytes int) string {
const unit = 1000 const unit = 1000
@ -85,43 +71,22 @@ func kill(path string, cache *fileCache) error {
return nil return nil
} }
func split(path string, regexes *regexes) (*splitPath, error) {
p := splitPath{}
var err error
split := regexes.filename.FindAllStringSubmatch(path, -1)
if len(split) < 1 || len(split[0]) < 3 {
return &splitPath{}, nil
}
p.base = split[0][1]
p.number, err = strconv.Atoi(split[0][2])
if err != nil {
return &splitPath{}, err
}
p.extension = split[0][3]
return &p, nil
}
func newFile(list []string, sortOrder string, regexes *regexes, formats *types.Types) (string, error) { func newFile(list []string, sortOrder string, regexes *regexes, formats *types.Types) (string, error) {
path, err := pickFile(list) path, err := pickFile(list)
if err != nil { if err != nil {
return "", err return "", err
} }
splitPath, err := split(path, regexes) if sortOrder == "asc" || sortOrder == "desc" {
splitPath, length, err := split(path, regexes)
if err != nil { if err != nil {
return "", err return "", err
} }
splitPath.number = 1
switch { switch {
case sortOrder == "asc": case sortOrder == "asc":
splitPath.number = fmt.Sprintf("%0*d", length, 1)
path, err = tryExtensions(splitPath, formats) path, err = tryExtensions(splitPath, formats)
if err != nil { if err != nil {
return "", err return "", err
@ -147,12 +112,13 @@ func newFile(list []string, sortOrder string, regexes *regexes, formats *types.T
} }
} }
} }
}
return path, nil return path, nil
} }
func nextFile(path, sortOrder string, regexes *regexes, formats *types.Types) (string, error) { func nextFile(filePath, sortOrder string, regexes *regexes, formats *types.Types) (string, error) {
splitPath, err := split(path, regexes) splitPath, _, err := split(filePath, regexes)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -166,27 +132,27 @@ func nextFile(path, sortOrder string, regexes *regexes, formats *types.Types) (s
return "", nil return "", nil
} }
fileName, err := tryExtensions(splitPath, formats) path, err := tryExtensions(splitPath, formats)
if err != nil { if err != nil {
return "", err return "", err
} }
return fileName, err return path, err
} }
func tryExtensions(splitPath *splitPath, formats *types.Types) (string, error) { func tryExtensions(splitPath *splitPath, formats *types.Types) (string, error) {
var fileName string var path string
for extension := range formats.Extensions { for extension := range formats.Extensions {
fileName = fmt.Sprintf("%s%.3d%s", splitPath.base, splitPath.number, extension) path = fmt.Sprintf("%s%s%s", splitPath.base, splitPath.number, extension)
exists, err := fileExists(fileName) exists, err := fileExists(path)
if err != nil { if err != nil {
return "", err return "", err
} }
if exists { if exists {
return fileName, nil return path, nil
} }
} }
@ -229,7 +195,7 @@ func pathIsValid(path string, paths []string) bool {
} }
} }
func pathHasSupportedFiles(path string, formats *types.Types) (bool, error) { func hasSupportedFiles(path string, formats *types.Types) (bool, error) {
hasRegisteredFiles := make(chan bool, 1) hasRegisteredFiles := make(chan bool, 1)
err := filepath.WalkDir(path, func(p string, info os.DirEntry, err error) error { err := filepath.WalkDir(path, func(p string, info os.DirEntry, err error) error {
@ -409,18 +375,18 @@ func scanPaths(paths []string, sort string, cache *fileCache, formats *types.Typ
Poll: Poll:
for { for {
select { select {
case p := <-fileChannel: case path := <-fileChannel:
list = append(list, p) list = append(list, path)
case s := <-statsChannels.filesMatched: case stat := <-statsChannels.filesMatched:
stats.filesMatched = stats.filesMatched + s stats.filesMatched = stats.filesMatched + stat
case s := <-statsChannels.filesSkipped: case stat := <-statsChannels.filesSkipped:
stats.filesSkipped = stats.filesSkipped + s stats.filesSkipped = stats.filesSkipped + stat
case s := <-statsChannels.directoriesMatched: case stat := <-statsChannels.directoriesMatched:
stats.directoriesMatched = stats.directoriesMatched + s stats.directoriesMatched = stats.directoriesMatched + stat
case s := <-statsChannels.directoriesSkipped: case stat := <-statsChannels.directoriesSkipped:
stats.directoriesSkipped = stats.directoriesSkipped + s stats.directoriesSkipped = stats.directoriesSkipped + stat
case e := <-errorChannel: case err := <-errorChannel:
return []string{}, e return []string{}, err
case <-done: case <-done:
break Poll break Poll
} }
@ -432,7 +398,7 @@ Poll:
} }
if Verbose { if Verbose {
fmt.Printf("%s | INDEX: %d/%d files across %d/%d directories in %s\n", fmt.Printf("%s | INDEX: Selected %d/%d files across %d/%d directories in %s\n",
time.Now().Format(logDate), time.Now().Format(logDate),
stats.filesMatched, stats.filesMatched,
stats.filesMatched+stats.filesSkipped, stats.filesMatched+stats.filesSkipped,
@ -543,12 +509,12 @@ func validatePaths(args []string, formats *types.Types) ([]string, error) {
pathMatches := (args[i] == path) pathMatches := (args[i] == path)
hasSupportedFiles, err := pathHasSupportedFiles(path, formats) hasSupportedFiles, err := hasSupportedFiles(path, formats)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var addPath bool = false var addPath = false
switch { switch {
case pathMatches && hasSupportedFiles: case pathMatches && hasSupportedFiles:

View File

@ -274,19 +274,19 @@ func serveEnabledMimeTypes(formats *types.Types) httprouter.Handle {
func registerInfoHandlers(mux *httprouter.Router, args []string, cache *fileCache, formats *types.Types, errorChannel chan<- error) { func registerInfoHandlers(mux *httprouter.Router, args []string, cache *fileCache, formats *types.Types, errorChannel chan<- error) {
if Cache { if Cache {
register(mux, Prefix+"/html/", serveIndexHtml(args, cache, false)) registerHandler(mux, Prefix+"/html", serveIndexHtml(args, cache, false))
if PageLength != 0 { if PageLength != 0 {
register(mux, Prefix+"/html/:page", serveIndexHtml(args, cache, true)) registerHandler(mux, Prefix+"/html/:page", serveIndexHtml(args, cache, true))
} }
register(mux, Prefix+"/json", serveIndexJson(args, cache, errorChannel)) registerHandler(mux, Prefix+"/json", serveIndexJson(args, cache, errorChannel))
if PageLength != 0 { if PageLength != 0 {
register(mux, Prefix+"/json/:page", serveIndexJson(args, cache, errorChannel)) registerHandler(mux, Prefix+"/json/:page", serveIndexJson(args, cache, errorChannel))
} }
} }
register(mux, Prefix+"/available_extensions", serveAvailableExtensions()) registerHandler(mux, Prefix+"/available_extensions", serveAvailableExtensions())
register(mux, Prefix+"/enabled_extensions", serveEnabledExtensions(formats)) registerHandler(mux, Prefix+"/enabled_extensions", serveEnabledExtensions(formats))
register(mux, Prefix+"/available_mime_types", serveAvailableMimeTypes()) registerHandler(mux, Prefix+"/available_mime_types", serveAvailableMimeTypes())
register(mux, Prefix+"/enabled_mime_types", serveEnabledMimeTypes(formats)) registerHandler(mux, Prefix+"/enabled_mime_types", serveEnabledMimeTypes(formats))
} }

View File

@ -5,15 +5,29 @@ Copyright © 2023 Seednode <seednode@seedno.de>
package cmd package cmd
import ( import (
"fmt"
"net/http"
"net/http/pprof" "net/http/pprof"
"time"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
) )
func registerProfileHandlers(mux *httprouter.Router) { func registerProfileHandler(mux *httprouter.Router, verb, path string, handler http.HandlerFunc) {
mux.HandlerFunc("GET", Prefix+"/debug/pprof/", pprof.Index) mux.HandlerFunc(verb, path, handler)
mux.HandlerFunc("GET", Prefix+"/debug/pprof/cmdline", pprof.Cmdline)
mux.HandlerFunc("GET", Prefix+"/debug/pprof/profile", pprof.Profile) if Handlers {
mux.HandlerFunc("GET", Prefix+"/debug/pprof/symbol", pprof.Symbol) fmt.Printf("%s | SERVE: Registered handler for %s\n",
mux.HandlerFunc("GET", Prefix+"/debug/pprof/trace", pprof.Trace) time.Now().Format(logDate),
path,
)
}
}
func registerProfileHandlers(mux *httprouter.Router) {
registerProfileHandler(mux, "GET", Prefix+"/debug/pprof/", pprof.Index)
registerProfileHandler(mux, "GET", Prefix+"/debug/pprof/cmdline", pprof.Cmdline)
registerProfileHandler(mux, "GET", Prefix+"/debug/pprof/profile", pprof.Profile)
registerProfileHandler(mux, "GET", Prefix+"/debug/pprof/symbol", pprof.Symbol)
registerProfileHandler(mux, "GET", Prefix+"/debug/pprof/trace", pprof.Trace)
} }

View File

@ -12,7 +12,7 @@ import (
) )
const ( const (
ReleaseVersion string = "0.96.4" ReleaseVersion string = "1.1.0"
) )
var ( var (

57
cmd/sort.go Normal file
View File

@ -0,0 +1,57 @@
/*
Copyright © 2023 Seednode <seednode@seedno.de>
*/
package cmd
import (
"fmt"
"strconv"
)
type splitPath struct {
base string
number string
extension string
}
func (splitPath *splitPath) increment() {
length := len(splitPath.number)
asInt, err := strconv.Atoi(splitPath.number)
if err != nil {
return
}
splitPath.number = fmt.Sprintf("%0*d", length, asInt+1)
}
func (splitPath *splitPath) decrement() {
length := len(splitPath.number)
asInt, err := strconv.Atoi(splitPath.number)
if err != nil {
return
}
splitPath.number = fmt.Sprintf("%0*d", length, asInt-1)
}
func split(path string, regexes *regexes) (*splitPath, int, error) {
p := splitPath{}
split := regexes.filename.FindAllStringSubmatch(path, -1)
if len(split) < 1 || len(split[0]) < 3 {
return &splitPath{}, 0, nil
}
p.base = split[0][1]
p.number = split[0][2]
p.extension = split[0][3]
return &p, len(p.number), nil
}

View File

@ -47,12 +47,7 @@ func splitQueryParams(query string, regexes *regexes) []string {
params := strings.Split(query, ",") params := strings.Split(query, ",")
for i := 0; i < len(params); i++ { for i := 0; i < len(params); i++ {
switch {
case regexes.alphanumeric.MatchString(params[i]) && CaseSensitive:
results = append(results, params[i]) results = append(results, params[i])
case regexes.alphanumeric.MatchString(params[i]):
results = append(results, strings.ToLower(params[i]))
}
} }
return results return results

View File

@ -37,7 +37,7 @@ 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`
mediaPrefix string = `/view` mediaPrefix string = `/view`
RedirectStatusCode int = http.StatusSeeOther redirectStatusCode int = http.StatusSeeOther
timeout time.Duration = 10 * time.Second timeout time.Duration = 10 * time.Second
) )
@ -231,7 +231,7 @@ func serveRoot(paths []string, regexes *regexes, cache *fileCache, formats *type
preparePath(path), preparePath(path),
queryParams, queryParams,
) )
http.Redirect(w, r, newUrl, RedirectStatusCode) http.Redirect(w, r, newUrl, redirectStatusCode)
} }
} }
@ -362,11 +362,14 @@ func serveVersion() httprouter.Handle {
} }
} }
func register(mux *httprouter.Router, path string, handle httprouter.Handle) { func registerHandler(mux *httprouter.Router, path string, handle httprouter.Handle) {
mux.GET(path, handle) mux.GET(path, handle)
if Handlers { if Handlers {
fmt.Printf("Registered handler for path %s\n", path) fmt.Printf("%s | SERVE: Registered handler for %s\n",
time.Now().Format(logDate),
path,
)
} }
} }
@ -377,7 +380,7 @@ func redirectRoot() httprouter.Handle {
Prefix, Prefix,
) )
http.Redirect(w, r, newUrl, RedirectStatusCode) http.Redirect(w, r, newUrl, redirectStatusCode)
} }
} }
@ -440,30 +443,22 @@ func ServePage(args []string) error {
return ErrNoMediaFound return ErrNoMediaFound
} }
listenHost := net.JoinHostPort(Bind, strconv.Itoa(Port)) regexes := &regexes{
filename: regexp.MustCompile(`(.+?)([0-9]*)(\..+)`),
if Verbose { alphanumeric: regexp.MustCompile(`^[A-z0-9]*$`),
fmt.Printf("%s | SERVE: Listening on %s...\n",
time.Now().Format(logDate),
listenHost,
)
} }
if !strings.HasSuffix(Prefix, "/") {
Prefix = Prefix + "/"
}
listenHost := net.JoinHostPort(Bind, strconv.Itoa(Port))
cache := &fileCache{ cache := &fileCache{
mutex: sync.RWMutex{}, mutex: sync.RWMutex{},
list: []string{}, list: []string{},
} }
err = importCache(paths, cache, formats)
if err != nil {
return err
}
regexes := &regexes{
filename: regexp.MustCompile(`(.+)([0-9]{3})(\..+)`),
alphanumeric: regexp.MustCompile(`^[A-z0-9]*$`),
}
mux := httprouter.New() mux := httprouter.New()
srv := &http.Server{ srv := &http.Server{
@ -476,29 +471,25 @@ func ServePage(args []string) error {
mux.PanicHandler = serverErrorHandler() mux.PanicHandler = serverErrorHandler()
if !strings.HasSuffix(Prefix, "/") {
Prefix = Prefix + "/"
}
errorChannel := make(chan error) errorChannel := make(chan error)
register(mux, Prefix, serveRoot(paths, regexes, cache, formats, errorChannel)) registerHandler(mux, Prefix, serveRoot(paths, regexes, cache, formats, errorChannel))
Prefix = strings.TrimSuffix(Prefix, "/") Prefix = strings.TrimSuffix(Prefix, "/")
if Prefix != "" { if Prefix != "" {
register(mux, "/", redirectRoot()) registerHandler(mux, "/", redirectRoot())
} }
register(mux, Prefix+"/favicons/*favicon", serveFavicons()) registerHandler(mux, Prefix+"/favicons/*favicon", serveFavicons())
register(mux, Prefix+"/favicon.ico", serveFavicons()) registerHandler(mux, Prefix+"/favicon.ico", serveFavicons())
register(mux, Prefix+mediaPrefix+"/*media", serveMedia(paths, regexes, cache, formats, errorChannel)) registerHandler(mux, Prefix+mediaPrefix+"/*media", serveMedia(paths, regexes, cache, formats, errorChannel))
register(mux, Prefix+sourcePrefix+"/*static", serveStaticFile(paths, cache, errorChannel)) registerHandler(mux, Prefix+sourcePrefix+"/*static", serveStaticFile(paths, cache, errorChannel))
register(mux, Prefix+"/version", serveVersion()) registerHandler(mux, Prefix+"/version", serveVersion())
if Cache { if Cache {
err = registerCacheHandlers(mux, args, cache, formats, errorChannel) err = registerCacheHandlers(mux, args, cache, formats, errorChannel)
@ -519,6 +510,11 @@ func ServePage(args []string) error {
fmt.Printf("WARNING! Files *will* be deleted after serving!\n\n") fmt.Printf("WARNING! Files *will* be deleted after serving!\n\n")
} }
err = importCache(paths, cache, formats)
if err != nil {
return err
}
go func() { go func() {
for err := range errorChannel { for err := range errorChannel {
fmt.Printf("%s | ERROR: %v\n", time.Now().Format(logDate), err) fmt.Printf("%s | ERROR: %v\n", time.Now().Format(logDate), err)
@ -531,6 +527,14 @@ func ServePage(args []string) error {
} }
}() }()
if Verbose {
fmt.Printf("%s | SERVE: Listening on http://%s%s/\n",
time.Now().Format(logDate),
listenHost,
Prefix,
)
}
err = srv.ListenAndServe() err = srv.ListenAndServe()
if !errors.Is(err, http.ErrServerClosed) { if !errors.Is(err, http.ErrServerClosed) {
return err return err