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.
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.
@ -77,7 +77,7 @@ Enjoy!
## 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).
@ -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.
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
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()
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),
length,
path,
time.Since(startTime),
)
}
@ -146,9 +147,10 @@ func (cache *fileCache) Import(path string) error {
}
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),
length,
path,
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 {
register(mux, Prefix+"/clear_cache", serveCacheClear(args, cache, formats, errorChannel))
registerHandler(mux, Prefix+"/clear_cache", serveCacheClear(args, cache, formats, errorChannel))
return nil
}

View File

@ -40,20 +40,6 @@ type scanStatsChannels struct {
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 {
const unit = 1000
@ -85,65 +71,45 @@ func kill(path string, cache *fileCache) error {
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) {
path, err := pickFile(list)
if err != nil {
return "", err
}
splitPath, err := split(path, regexes)
if err != nil {
return "", err
}
splitPath.number = 1
switch {
case sortOrder == "asc":
path, err = tryExtensions(splitPath, formats)
if sortOrder == "asc" || sortOrder == "desc" {
splitPath, length, err := split(path, regexes)
if err != nil {
return "", err
}
case sortOrder == "desc":
for {
splitPath.increment()
switch {
case sortOrder == "asc":
splitPath.number = fmt.Sprintf("%0*d", length, 1)
path, err = tryExtensions(splitPath, formats)
if err != nil {
return "", err
}
if path == "" {
splitPath.decrement()
case sortOrder == "desc":
for {
splitPath.increment()
path, err = tryExtensions(splitPath, formats)
if err != nil {
return "", err
}
break
if path == "" {
splitPath.decrement()
path, err = tryExtensions(splitPath, formats)
if err != nil {
return "", err
}
break
}
}
}
}
@ -151,8 +117,8 @@ func newFile(list []string, sortOrder string, regexes *regexes, formats *types.T
return path, nil
}
func nextFile(path, sortOrder string, regexes *regexes, formats *types.Types) (string, error) {
splitPath, err := split(path, regexes)
func nextFile(filePath, sortOrder string, regexes *regexes, formats *types.Types) (string, error) {
splitPath, _, err := split(filePath, regexes)
if err != nil {
return "", err
}
@ -166,27 +132,27 @@ func nextFile(path, sortOrder string, regexes *regexes, formats *types.Types) (s
return "", nil
}
fileName, err := tryExtensions(splitPath, formats)
path, err := tryExtensions(splitPath, formats)
if err != nil {
return "", err
}
return fileName, err
return path, err
}
func tryExtensions(splitPath *splitPath, formats *types.Types) (string, error) {
var fileName string
var path string
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 {
return "", err
}
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)
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:
for {
select {
case p := <-fileChannel:
list = append(list, p)
case s := <-statsChannels.filesMatched:
stats.filesMatched = stats.filesMatched + s
case s := <-statsChannels.filesSkipped:
stats.filesSkipped = stats.filesSkipped + s
case s := <-statsChannels.directoriesMatched:
stats.directoriesMatched = stats.directoriesMatched + s
case s := <-statsChannels.directoriesSkipped:
stats.directoriesSkipped = stats.directoriesSkipped + s
case e := <-errorChannel:
return []string{}, e
case path := <-fileChannel:
list = append(list, path)
case stat := <-statsChannels.filesMatched:
stats.filesMatched = stats.filesMatched + stat
case stat := <-statsChannels.filesSkipped:
stats.filesSkipped = stats.filesSkipped + stat
case stat := <-statsChannels.directoriesMatched:
stats.directoriesMatched = stats.directoriesMatched + stat
case stat := <-statsChannels.directoriesSkipped:
stats.directoriesSkipped = stats.directoriesSkipped + stat
case err := <-errorChannel:
return []string{}, err
case <-done:
break Poll
}
@ -432,7 +398,7 @@ Poll:
}
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),
stats.filesMatched,
stats.filesMatched+stats.filesSkipped,
@ -543,12 +509,12 @@ func validatePaths(args []string, formats *types.Types) ([]string, error) {
pathMatches := (args[i] == path)
hasSupportedFiles, err := pathHasSupportedFiles(path, formats)
hasSupportedFiles, err := hasSupportedFiles(path, formats)
if err != nil {
return nil, err
}
var addPath bool = false
var addPath = false
switch {
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) {
if Cache {
register(mux, Prefix+"/html/", serveIndexHtml(args, cache, false))
registerHandler(mux, Prefix+"/html", serveIndexHtml(args, cache, false))
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 {
register(mux, Prefix+"/json/:page", serveIndexJson(args, cache, errorChannel))
registerHandler(mux, Prefix+"/json/:page", serveIndexJson(args, cache, errorChannel))
}
}
register(mux, Prefix+"/available_extensions", serveAvailableExtensions())
register(mux, Prefix+"/enabled_extensions", serveEnabledExtensions(formats))
register(mux, Prefix+"/available_mime_types", serveAvailableMimeTypes())
register(mux, Prefix+"/enabled_mime_types", serveEnabledMimeTypes(formats))
registerHandler(mux, Prefix+"/available_extensions", serveAvailableExtensions())
registerHandler(mux, Prefix+"/enabled_extensions", serveEnabledExtensions(formats))
registerHandler(mux, Prefix+"/available_mime_types", serveAvailableMimeTypes())
registerHandler(mux, Prefix+"/enabled_mime_types", serveEnabledMimeTypes(formats))
}

View File

@ -5,15 +5,29 @@ Copyright © 2023 Seednode <seednode@seedno.de>
package cmd
import (
"fmt"
"net/http"
"net/http/pprof"
"time"
"github.com/julienschmidt/httprouter"
)
func registerProfileHandlers(mux *httprouter.Router) {
mux.HandlerFunc("GET", Prefix+"/debug/pprof/", pprof.Index)
mux.HandlerFunc("GET", Prefix+"/debug/pprof/cmdline", pprof.Cmdline)
mux.HandlerFunc("GET", Prefix+"/debug/pprof/profile", pprof.Profile)
mux.HandlerFunc("GET", Prefix+"/debug/pprof/symbol", pprof.Symbol)
mux.HandlerFunc("GET", Prefix+"/debug/pprof/trace", pprof.Trace)
func registerProfileHandler(mux *httprouter.Router, verb, path string, handler http.HandlerFunc) {
mux.HandlerFunc(verb, path, handler)
if Handlers {
fmt.Printf("%s | SERVE: Registered handler for %s\n",
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 (
ReleaseVersion string = "0.96.4"
ReleaseVersion string = "1.1.0"
)
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, ",")
for i := 0; i < len(params); i++ {
switch {
case regexes.alphanumeric.MatchString(params[i]) && CaseSensitive:
results = append(results, params[i])
case regexes.alphanumeric.MatchString(params[i]):
results = append(results, strings.ToLower(params[i]))
}
results = append(results, params[i])
}
return results

View File

@ -37,7 +37,7 @@ const (
logDate string = `2006-01-02T15:04:05.000-07:00`
sourcePrefix string = `/source`
mediaPrefix string = `/view`
RedirectStatusCode int = http.StatusSeeOther
redirectStatusCode int = http.StatusSeeOther
timeout time.Duration = 10 * time.Second
)
@ -231,7 +231,7 @@ func serveRoot(paths []string, regexes *regexes, cache *fileCache, formats *type
preparePath(path),
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)
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,
)
http.Redirect(w, r, newUrl, RedirectStatusCode)
http.Redirect(w, r, newUrl, redirectStatusCode)
}
}
@ -440,30 +443,22 @@ func ServePage(args []string) error {
return ErrNoMediaFound
}
listenHost := net.JoinHostPort(Bind, strconv.Itoa(Port))
if Verbose {
fmt.Printf("%s | SERVE: Listening on %s...\n",
time.Now().Format(logDate),
listenHost,
)
regexes := &regexes{
filename: regexp.MustCompile(`(.+?)([0-9]*)(\..+)`),
alphanumeric: regexp.MustCompile(`^[A-z0-9]*$`),
}
if !strings.HasSuffix(Prefix, "/") {
Prefix = Prefix + "/"
}
listenHost := net.JoinHostPort(Bind, strconv.Itoa(Port))
cache := &fileCache{
mutex: sync.RWMutex{},
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()
srv := &http.Server{
@ -476,29 +471,25 @@ func ServePage(args []string) error {
mux.PanicHandler = serverErrorHandler()
if !strings.HasSuffix(Prefix, "/") {
Prefix = Prefix + "/"
}
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, "/")
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 {
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")
}
err = importCache(paths, cache, formats)
if err != nil {
return err
}
go func() {
for err := range errorChannel {
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()
if !errors.Is(err, http.ErrServerClosed) {
return err