diff --git a/README.md b/README.md index e2c506a..dd01c32 100644 --- a/README.md +++ b/README.md @@ -112,33 +112,34 @@ Usage: roulette [path]... [flags] Flags: - -a, --all enable all supported file types - --audio enable support for audio files - -b, --bind string address to bind to (default "0.0.0.0") - -c, --cache generate directory cache at startup - --cache-file string path to optional persistent cache file - --code enable support for source code files - --code-theme string theme for source code syntax highlighting (default "solarized-dark256") - -f, --filter enable filtering - --flash enable support for shockwave flash files (via ruffle.rs) - --handlers display registered handlers (for debugging) - -h, --help help for roulette - --images enable support for image files - -i, --info expose informational endpoints - --maximum-files uint32 skip directories with file counts above this value (default 4294967295) - --minimum-files uint32 skip directories with file counts below this value (default 1) - --page-length uint32 pagination length for info pages - -p, --port uint16 port to listen on (default 8080) - --prefix string root path for http handlers (for reverse proxying) (default "/") - --profile register net/http/pprof handlers - -r, --recursive recurse into subdirectories - --refresh enable automatic page refresh via query parameter - --russian remove selected images after serving - -s, --sort enable sorting - --text enable support for text files - -v, --verbose log accessed files and other information to stdout - -V, --version display version and exit - --video enable support for video files + -a, --all enable all supported file types + --audio enable support for audio files + -b, --bind string address to bind to (default "0.0.0.0") + -c, --cache generate directory cache at startup + --cache-file string path to optional persistent cache file + --code enable support for source code files + --code-theme string theme for source code syntax highlighting (default "solarized-dark256") + --exit-on-error shut down webserver on error, instead of just printing the error + -f, --filter enable filtering + --flash enable support for shockwave flash files (via ruffle.rs) + --handlers display registered handlers (for debugging) + -h, --help help for roulette + --images enable support for image files + -i, --info expose informational endpoints + --maximum-files uint skip directories with file counts above this value (default 4294967295) + --minimum-files uint skip directories with file counts below this value (default 1) + --page-length uint32 pagination length for info pages + -p, --port uint16 port to listen on (default 8080) + --prefix string root path for http handlers (for reverse proxying) (default "/") + --profile register net/http/pprof handlers + -r, --recursive recurse into subdirectories + --refresh enable automatic page refresh via query parameter + --russian remove selected images after serving + -s, --sort enable sorting + --text enable support for text files + -v, --verbose log accessed files and other information to stdout + -V, --version display version and exit + --video enable support for video files ``` ## Building the Docker container diff --git a/cmd/cache.go b/cmd/cache.go index 6525359..cfbedce 100644 --- a/cmd/cache.go +++ b/cmd/cache.go @@ -6,7 +6,6 @@ package cmd import ( "encoding/gob" - "fmt" "net/http" "os" "sync" @@ -129,11 +128,11 @@ func (cache *fileCache) Import(path string) error { return nil } -func serveCacheClear(args []string, cache *fileCache, formats *types.Types) httprouter.Handle { +func serveCacheClear(args []string, cache *fileCache, formats *types.Types, errorChannel chan<- error) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { list, err := fileList(args, &filters{}, "", &fileCache{}, formats) if err != nil { - fmt.Println(err) + errorChannel <- err return } @@ -146,7 +145,7 @@ func serveCacheClear(args []string, cache *fileCache, formats *types.Types) http } } -func registerCacheHandlers(mux *httprouter.Router, args []string, cache *fileCache, formats *types.Types) error { +func registerCacheHandlers(mux *httprouter.Router, args []string, cache *fileCache, formats *types.Types, errorChannel chan<- error) error { skipIndex := false if CacheFile != "" { @@ -165,7 +164,7 @@ func registerCacheHandlers(mux *httprouter.Router, args []string, cache *fileCac cache.set(list) } - register(mux, Prefix+"/clear_cache", serveCacheClear(args, cache, formats)) + register(mux, Prefix+"/clear_cache", serveCacheClear(args, cache, formats, errorChannel)) return nil } diff --git a/cmd/favicons.go b/cmd/favicons.go index 5350529..a10136e 100644 --- a/cmd/favicons.go +++ b/cmd/favicons.go @@ -7,6 +7,7 @@ package cmd import ( "bytes" "embed" + "errors" "net/http" "strconv" "strings" @@ -27,12 +28,16 @@ const ( ` ) -func serveFavicons() httprouter.Handle { +func serveFavicons(errorChannel chan<- error) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + errorChannel <- errors.New("test") + fname := strings.TrimPrefix(r.URL.Path, "/") data, err := favicons.ReadFile(fname) if err != nil { + errorChannel <- err + return } diff --git a/cmd/info.go b/cmd/info.go index fa520fc..93959ea 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -133,7 +133,7 @@ func serveIndexHtml(args []string, cache *fileCache, paginate bool) httprouter.H } } -func serveIndexJson(args []string, index *fileCache) httprouter.Handle { +func serveIndexJson(args []string, index *fileCache, errorChannel chan<- error) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { w.Header().Set("Content-Type", "application/json") @@ -168,7 +168,7 @@ func serveIndexJson(args []string, index *fileCache) httprouter.Handle { response, err := json.MarshalIndent(cachedFiles[startIndex:stopIndex], "", " ") if err != nil { - fmt.Println(err) + errorChannel <- err serverError(w, r, nil) @@ -272,16 +272,16 @@ func serveEnabledMimeTypes(formats *types.Types) httprouter.Handle { } } -func registerInfoHandlers(mux *httprouter.Router, args []string, cache *fileCache, formats *types.Types) { +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)) if PageLength != 0 { register(mux, Prefix+"/html/:page", serveIndexHtml(args, cache, true)) } - register(mux, Prefix+"/json", serveIndexJson(args, cache)) + register(mux, Prefix+"/json", serveIndexJson(args, cache, errorChannel)) if PageLength != 0 { - register(mux, Prefix+"/json/:page", serveIndexJson(args, cache)) + register(mux, Prefix+"/json/:page", serveIndexJson(args, cache, errorChannel)) } } diff --git a/cmd/root.go b/cmd/root.go index 502837b..7639cf9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,7 +11,7 @@ import ( ) const ( - ReleaseVersion string = "0.88.1" + ReleaseVersion string = "0.89.0" ) var ( @@ -22,6 +22,7 @@ var ( CacheFile string Code bool CodeTheme string + ExitOnError bool Filtering bool Flash bool Handlers bool @@ -72,6 +73,7 @@ func init() { rootCmd.Flags().StringVar(&CacheFile, "cache-file", "", "path to optional persistent cache file") 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().BoolVar(&ExitOnError, "exit-on-error", false, "shut down webserver on error, instead of just printing the error") 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(&Handlers, "handlers", false, "display registered handlers (for debugging)") diff --git a/cmd/web.go b/cmd/web.go index e2477e7..38b6f7d 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -6,6 +6,7 @@ package cmd import ( "bytes" + "context" "errors" "fmt" "io" @@ -39,7 +40,7 @@ const ( timeout time.Duration = 10 * time.Second ) -func serveStaticFile(paths []string, cache *fileCache) httprouter.Handle { +func serveStaticFile(paths []string, cache *fileCache, errorChannel chan<- error) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { prefix := Prefix + sourcePrefix @@ -47,7 +48,7 @@ func serveStaticFile(paths []string, cache *fileCache) httprouter.Handle { prefixedFilePath, err := stripQueryParams(path) if err != nil { - fmt.Println(err) + errorChannel <- err serverError(w, r, nil) @@ -56,7 +57,7 @@ func serveStaticFile(paths []string, cache *fileCache) httprouter.Handle { filePath, err := filepath.EvalSymlinks(strings.TrimPrefix(prefixedFilePath, prefix)) if err != nil { - fmt.Println(err) + errorChannel <- err serverError(w, r, nil) @@ -71,6 +72,8 @@ func serveStaticFile(paths []string, cache *fileCache) httprouter.Handle { exists, err := fileExists(filePath) if err != nil { + errorChannel <- err + serverError(w, r, nil) return @@ -86,6 +89,8 @@ func serveStaticFile(paths []string, cache *fileCache) httprouter.Handle { buf, err := os.ReadFile(filePath) if err != nil { + errorChannel <- err + serverError(w, r, nil) return @@ -98,6 +103,8 @@ func serveStaticFile(paths []string, cache *fileCache) httprouter.Handle { if Russian { err = os.Remove(filePath) if err != nil { + errorChannel <- err + serverError(w, r, nil) return @@ -120,11 +127,11 @@ func serveStaticFile(paths []string, cache *fileCache) httprouter.Handle { } } -func serveRoot(paths []string, regexes *regexes, cache *fileCache, formats *types.Types) httprouter.Handle { +func serveRoot(paths []string, regexes *regexes, cache *fileCache, formats *types.Types, errorChannel chan<- error) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { refererUri, err := stripQueryParams(refererToUri(r.Referer())) if err != nil { - fmt.Println(err) + errorChannel <- err serverError(w, r, nil) @@ -147,7 +154,7 @@ func serveRoot(paths []string, regexes *regexes, cache *fileCache, formats *type if refererUri != "" { filePath, err = nextFile(strippedRefererUri, sortOrder, regexes, formats) if err != nil { - fmt.Println(err) + errorChannel <- err serverError(w, r, nil) @@ -174,7 +181,7 @@ func serveRoot(paths []string, regexes *regexes, cache *fileCache, formats *type return case err != nil: - fmt.Println(err) + errorChannel <- err serverError(w, r, nil) @@ -194,7 +201,7 @@ func serveRoot(paths []string, regexes *regexes, cache *fileCache, formats *type } } -func serveMedia(paths []string, regexes *regexes, formats *types.Types) httprouter.Handle { +func serveMedia(paths []string, regexes *regexes, formats *types.Types, errorChannel chan<- error) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { filters := &filters{ included: splitQueryParams(r.URL.Query().Get("include"), regexes), @@ -211,7 +218,7 @@ func serveMedia(paths []string, regexes *regexes, formats *types.Types) httprout exists, err := fileExists(path) if err != nil { - fmt.Println(err) + errorChannel <- err serverError(w, r, nil) @@ -252,19 +259,37 @@ func serveMedia(paths []string, regexes *regexes, formats *types.Types) httprout htmlBody.WriteString(``) htmlBody.WriteString(faviconHtml) htmlBody.WriteString(fmt.Sprintf(``, format.Css())) - htmlBody.WriteString((format.Title(rootUrl, fileUri, path, fileName, Prefix, mimeType))) + + title, err := format.Title(rootUrl, fileUri, path, fileName, Prefix, mimeType) + if err != nil { + errorChannel <- err + + serverError(w, r, nil) + + return + } + htmlBody.WriteString(title) htmlBody.WriteString(``) if refreshInterval != "0ms" { htmlBody.WriteString(fmt.Sprintf("", rootUrl, refreshTimer)) } - htmlBody.WriteString((format.Body(rootUrl, fileUri, path, fileName, Prefix, mimeType))) + + body, err := format.Body(rootUrl, fileUri, path, fileName, Prefix, mimeType) + if err != nil { + errorChannel <- err + + serverError(w, r, nil) + + return + } + htmlBody.WriteString(body) htmlBody.WriteString(``) _, err = io.WriteString(w, gohtml.Format(htmlBody.String())) if err != nil { - fmt.Println(err) + errorChannel <- err serverError(w, r, nil) @@ -387,7 +412,9 @@ func ServePage(args []string) error { Prefix = Prefix + "/" } - register(mux, Prefix, serveRoot(paths, regexes, cache, formats)) + errorChannel := make(chan error) + + register(mux, Prefix, serveRoot(paths, regexes, cache, formats, errorChannel)) Prefix = strings.TrimSuffix(Prefix, "/") @@ -395,22 +422,25 @@ func ServePage(args []string) error { register(mux, "/", redirectRoot()) } - register(mux, Prefix+"/favicons/*favicon", serveFavicons()) + register(mux, Prefix+"/favicons/*favicon", serveFavicons(errorChannel)) - register(mux, Prefix+"/favicon.ico", serveFavicons()) + register(mux, Prefix+"/favicon.ico", serveFavicons(errorChannel)) - register(mux, Prefix+mediaPrefix+"/*media", serveMedia(paths, regexes, formats)) + register(mux, Prefix+mediaPrefix+"/*media", serveMedia(paths, regexes, formats, errorChannel)) - register(mux, Prefix+sourcePrefix+"/*static", serveStaticFile(paths, cache)) + register(mux, Prefix+sourcePrefix+"/*static", serveStaticFile(paths, cache, errorChannel)) register(mux, Prefix+"/version", serveVersion()) if Cache { - registerCacheHandlers(mux, args, cache, formats) + err = registerCacheHandlers(mux, args, cache, formats, errorChannel) + if err != nil { + return err + } } if Info { - registerInfoHandlers(mux, args, cache, formats) + registerInfoHandlers(mux, args, cache, formats, errorChannel) } if Profile { @@ -421,6 +451,18 @@ func ServePage(args []string) error { fmt.Printf("WARNING! Files *will* be deleted after serving!\n\n") } + go func() { + for err := range errorChannel { + fmt.Printf("%s | Error: %v\n", time.Now().Format(logDate), err) + + if ExitOnError { + fmt.Printf("%s | Shutting down...\n", time.Now().Format(logDate)) + + srv.Shutdown(context.Background()) + } + } + }() + err = srv.ListenAndServe() if !errors.Is(err, http.ErrServerClosed) { return err diff --git a/types/audio/audio.go b/types/audio/audio.go index 9464775..a6a1a9f 100644 --- a/types/audio/audio.go +++ b/types/audio/audio.go @@ -22,16 +22,16 @@ func (t Format) Css() string { return css.String() } -func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { - return fmt.Sprintf(`%s`, fileName) +func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { + return fmt.Sprintf(`%s`, fileName), nil } -func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { +func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { return fmt.Sprintf(``, rootUrl, fileUri, mime, - fileName) + fileName), nil } func (t Format) Extensions() map[string]string { diff --git a/types/code/code.go b/types/code/code.go index 06b940b..6ebbf84 100644 --- a/types/code/code.go +++ b/types/code/code.go @@ -60,14 +60,14 @@ func (t Format) Css() string { return css.String() } -func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { - return fmt.Sprintf(`%s`, fileName) +func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { + return fmt.Sprintf(`%s`, fileName), nil } -func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { +func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { contents, err := os.ReadFile(filePath) if err != nil { - return "" + return "", err } contentString := string(contents) @@ -97,24 +97,24 @@ func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) iterator, err := lexer.Tokenise(nil, contentString) if err != nil { - return "" + return "", err } err = formatter.Format(w, style, iterator) if err != nil { - return "" + return "", err } w.Flush() b, err := io.ReadAll(r) if err != nil { - return "" + return "", err } return fmt.Sprintf(`%s`, rootUrl, - string(b)) + string(b)), nil } func (t Format) Extensions() map[string]string { diff --git a/types/flash/flash.go b/types/flash/flash.go index c12b4ce..daa17ae 100644 --- a/types/flash/flash.go +++ b/types/flash/flash.go @@ -22,17 +22,17 @@ func (t Format) Css() string { return css.String() } -func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { - return fmt.Sprintf(`%s`, fileName) +func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { + return fmt.Sprintf(`%s`, fileName), nil } -func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { +func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { var html strings.Builder html.WriteString(fmt.Sprintf(``, fileUri)) html.WriteString(fmt.Sprintf(`
`, rootUrl)) - return html.String() + return html.String(), nil } func (t Format) Extensions() map[string]string { diff --git a/types/images/images.go b/types/images/images.go index e1bdf75..15e41cc 100644 --- a/types/images/images.go +++ b/types/images/images.go @@ -37,22 +37,22 @@ func (t Format) Css() string { return css.String() } -func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { +func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { dimensions, err := ImageDimensions(filePath) if err != nil { - fmt.Println(err) + return "", err } return fmt.Sprintf(`%s (%dx%d)`, fileName, dimensions.width, - dimensions.height) + dimensions.height), nil } -func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { +func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { dimensions, err := ImageDimensions(filePath) if err != nil { - fmt.Println(err) + return "", err } return fmt.Sprintf(`Roulette selected: %s`, @@ -61,7 +61,7 @@ func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) dimensions.width, dimensions.height, mime, - fileName) + fileName), nil } func (t Format) Extensions() map[string]string { diff --git a/types/text/text.go b/types/text/text.go index 7096860..7aad421 100644 --- a/types/text/text.go +++ b/types/text/text.go @@ -27,11 +27,11 @@ func (t Format) Css() string { return css.String() } -func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { - return fmt.Sprintf(`%s`, fileName) +func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { + return fmt.Sprintf(`%s`, fileName), nil } -func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { +func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { body, err := os.ReadFile(filePath) if err != nil { body = []byte{} @@ -39,7 +39,7 @@ func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) return fmt.Sprintf(``, rootUrl, - body) + body), nil } func (t Format) Extensions() map[string]string { diff --git a/types/types.go b/types/types.go index f6f8e2f..f62e8c9 100644 --- a/types/types.go +++ b/types/types.go @@ -16,8 +16,8 @@ var SupportedFormats = &Types{ type Type interface { Css() string - Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) string - Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) string + Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) + Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) Extensions() map[string]string MimeType(string) string Validate(filePath string) bool diff --git a/types/video/video.go b/types/video/video.go index 808ec6c..5c02025 100644 --- a/types/video/video.go +++ b/types/video/video.go @@ -25,16 +25,16 @@ func (t Format) Css() string { return css.String() } -func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { - return fmt.Sprintf(`%s`, fileName) +func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { + return fmt.Sprintf(`%s`, fileName), nil } -func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) string { +func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { return fmt.Sprintf(``, rootUrl, fileUri, mime, - fileName) + fileName), nil } func (t Format) Extensions() map[string]string {