Compare commits
9 Commits
6fea978459
...
cbf7218453
Author | SHA1 | Date |
---|---|---|
Seednode | cbf7218453 | |
Seednode | c77a151a24 | |
Seednode | 62a8bde8ea | |
Seednode | a7daf81754 | |
Seednode | 57bcd40c29 | |
Seednode | cc00a1b63b | |
Seednode | 3b4f8f7499 | |
Seednode | e0d4b3882b | |
Seednode | 732d944036 |
18
README.md
18
README.md
|
@ -78,9 +78,13 @@ 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 the `-i|--index` flag is passed, two additional endpoints—`/html` and `/json`—are registered.
|
||||
If the `-i|--index` flag is passed, four additional endpoints 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.
|
||||
The first of these—`/html` and `/json`—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.
|
||||
|
||||
The other two endpoints—`/extensions` and `/mime_types`—return the registered file types.
|
||||
|
||||
## Statistics
|
||||
|
||||
|
@ -113,13 +117,13 @@ Flags:
|
|||
-c, --cache generate directory cache at startup
|
||||
--cache-file string path to optional persistent cache file
|
||||
-f, --filter enable filtering
|
||||
--flash enable support for shockwave flash files (via ruffle.rs) (default true)
|
||||
--flash enable support for shockwave flash files (via ruffle.rs)
|
||||
-h, --help help for roulette
|
||||
--images enable support for image files (default true)
|
||||
--images enable support for image files
|
||||
-i, --index expose index endpoints
|
||||
--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)
|
||||
--page-length uint pagination length for statistics and debug pages
|
||||
--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 statistics and debug pages
|
||||
-p, --port uint16 port to listen on (default 8080)
|
||||
--profile register net/http/pprof handlers
|
||||
-r, --recursive recurse into subdirectories
|
||||
|
|
78
cmd/files.go
78
cmd/files.go
|
@ -20,7 +20,7 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"seedno.de/seednode/roulette/formats"
|
||||
"seedno.de/seednode/roulette/types"
|
||||
)
|
||||
|
||||
type maxConcurrency int
|
||||
|
@ -53,10 +53,10 @@ func (f *Files) Append(directory, path string) {
|
|||
}
|
||||
|
||||
type ScanStats struct {
|
||||
filesMatched atomic.Uint64
|
||||
filesSkipped atomic.Uint64
|
||||
directoriesMatched atomic.Uint64
|
||||
directoriesSkipped atomic.Uint64
|
||||
filesMatched atomic.Uint32
|
||||
filesSkipped atomic.Uint32
|
||||
directoriesMatched atomic.Uint32
|
||||
directoriesSkipped atomic.Uint32
|
||||
}
|
||||
|
||||
type Path struct {
|
||||
|
@ -108,9 +108,9 @@ func preparePath(path string) string {
|
|||
return MediaPrefix + path
|
||||
}
|
||||
|
||||
func appendPath(directory, path string, files *Files, stats *ScanStats, registeredFormats *formats.SupportedFormats, shouldCache bool) error {
|
||||
func appendPath(directory, path string, files *Files, stats *ScanStats, formats *types.Types, shouldCache bool) error {
|
||||
if shouldCache {
|
||||
registered, _, _, err := formats.FileType(path, registeredFormats)
|
||||
registered, _, _, err := types.FileType(path, formats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ func appendPath(directory, path string, files *Files, stats *ScanStats, register
|
|||
return nil
|
||||
}
|
||||
|
||||
func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats, types *formats.SupportedFormats) error {
|
||||
func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats, formats *types.Types) error {
|
||||
shouldCache := Cache && filters.IsEmpty()
|
||||
|
||||
absolutePath, err := filepath.Abs(path)
|
||||
|
@ -158,7 +158,7 @@ func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats,
|
|||
filename,
|
||||
filters.includes[i],
|
||||
) {
|
||||
err := appendPath(directory, path, files, stats, types, shouldCache)
|
||||
err := appendPath(directory, path, files, stats, formats, shouldCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -172,7 +172,7 @@ func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats,
|
|||
return nil
|
||||
}
|
||||
|
||||
err = appendPath(directory, path, files, stats, types, shouldCache)
|
||||
err = appendPath(directory, path, files, stats, formats, shouldCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -180,8 +180,8 @@ func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats,
|
|||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexes, index *FileIndex, formats *types.Types) (string, error) {
|
||||
filePath, err := pickFile(paths, filters, sortOrder, index, formats)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
|
|||
|
||||
switch {
|
||||
case sortOrder == "asc":
|
||||
filePath, err = tryExtensions(path, registeredFormats)
|
||||
filePath, err = tryExtensions(path, formats)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
|
|||
for {
|
||||
path.increment()
|
||||
|
||||
filePath, err = tryExtensions(path, registeredFormats)
|
||||
filePath, err = tryExtensions(path, formats)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
|
|||
if filePath == "" {
|
||||
path.decrement()
|
||||
|
||||
filePath, err = tryExtensions(path, registeredFormats)
|
||||
filePath, err = tryExtensions(path, formats)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
|
|||
return filePath, nil
|
||||
}
|
||||
|
||||
func nextFile(filePath, sortOrder string, Regexes *Regexes, registeredFormats *formats.SupportedFormats) (string, error) {
|
||||
func nextFile(filePath, sortOrder string, Regexes *Regexes, formats *types.Types) (string, error) {
|
||||
path, err := splitPath(filePath, Regexes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -239,7 +239,7 @@ func nextFile(filePath, sortOrder string, Regexes *Regexes, registeredFormats *f
|
|||
return "", nil
|
||||
}
|
||||
|
||||
fileName, err := tryExtensions(path, registeredFormats)
|
||||
fileName, err := tryExtensions(path, formats)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -269,13 +269,11 @@ func splitPath(path string, Regexes *Regexes) (*Path, error) {
|
|||
return &p, nil
|
||||
}
|
||||
|
||||
func tryExtensions(p *Path, registeredFormats *formats.SupportedFormats) (string, error) {
|
||||
func tryExtensions(p *Path, formats *types.Types) (string, error) {
|
||||
var fileName string
|
||||
|
||||
for _, format := range registeredFormats.Extensions {
|
||||
for _, extension := range format.Extensions {
|
||||
fileName = fmt.Sprintf("%s%.3d%s", p.base, p.number, extension)
|
||||
}
|
||||
for extension := range formats.Extensions {
|
||||
fileName = fmt.Sprintf("%s%.3d%s", p.base, p.number, extension)
|
||||
|
||||
exists, err := fileExists(fileName)
|
||||
if err != nil {
|
||||
|
@ -326,7 +324,7 @@ func pathIsValid(filePath string, paths []string) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func pathHasSupportedFiles(path string, registeredFormats *formats.SupportedFormats) (bool, error) {
|
||||
func pathHasSupportedFiles(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 {
|
||||
|
@ -338,7 +336,7 @@ func pathHasSupportedFiles(path string, registeredFormats *formats.SupportedForm
|
|||
case !Recursive && info.IsDir() && p != path:
|
||||
return filepath.SkipDir
|
||||
case !info.IsDir():
|
||||
registered, _, _, err := formats.FileType(p, registeredFormats)
|
||||
registered, _, _, err := types.FileType(p, formats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -363,9 +361,9 @@ func pathHasSupportedFiles(path string, registeredFormats *formats.SupportedForm
|
|||
}
|
||||
}
|
||||
|
||||
func pathCount(path string) (uint64, uint64, error) {
|
||||
var directories uint64 = 0
|
||||
var files uint64 = 0
|
||||
func pathCount(path string) (uint32, uint32, error) {
|
||||
var directories uint32 = 0
|
||||
var files uint32 = 0
|
||||
|
||||
nodes, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
|
@ -383,7 +381,7 @@ func pathCount(path string) (uint64, uint64, error) {
|
|||
return files, directories, nil
|
||||
}
|
||||
|
||||
func scanPath(path string, files *Files, filters *Filters, stats *ScanStats, concurrency *Concurrency, types *formats.SupportedFormats) error {
|
||||
func scanPath(path string, files *Files, filters *Filters, stats *ScanStats, concurrency *Concurrency, formats *types.Types) error {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
err := filepath.WalkDir(path, func(p string, info os.DirEntry, err error) error {
|
||||
|
@ -410,7 +408,7 @@ func scanPath(path string, files *Files, filters *Filters, stats *ScanStats, con
|
|||
fmt.Println(err)
|
||||
}
|
||||
|
||||
err = appendPaths(path, files, filters, stats, types)
|
||||
err = appendPaths(path, files, filters, stats, formats)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
@ -444,7 +442,7 @@ func scanPath(path string, files *Files, filters *Filters, stats *ScanStats, con
|
|||
return nil
|
||||
}
|
||||
|
||||
func fileList(paths []string, filters *Filters, sort string, index *FileIndex, types *formats.SupportedFormats) ([]string, bool) {
|
||||
func fileList(paths []string, filters *Filters, sort string, index *FileIndex, formats *types.Types) ([]string, bool) {
|
||||
if Cache && filters.IsEmpty() && !index.IsEmpty() {
|
||||
return index.Index(), true
|
||||
}
|
||||
|
@ -457,10 +455,10 @@ func fileList(paths []string, filters *Filters, sort string, index *FileIndex, t
|
|||
}
|
||||
|
||||
stats := &ScanStats{
|
||||
filesMatched: atomic.Uint64{},
|
||||
filesSkipped: atomic.Uint64{},
|
||||
directoriesMatched: atomic.Uint64{},
|
||||
directoriesSkipped: atomic.Uint64{},
|
||||
filesMatched: atomic.Uint32{},
|
||||
filesSkipped: atomic.Uint32{},
|
||||
directoriesMatched: atomic.Uint32{},
|
||||
directoriesSkipped: atomic.Uint32{},
|
||||
}
|
||||
|
||||
concurrency := &Concurrency{
|
||||
|
@ -483,7 +481,7 @@ func fileList(paths []string, filters *Filters, sort string, index *FileIndex, t
|
|||
wg.Done()
|
||||
}()
|
||||
|
||||
err := scanPath(paths[i], files, filters, stats, concurrency, types)
|
||||
err := scanPath(paths[i], files, filters, stats, concurrency, formats)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
@ -558,8 +556,8 @@ func prepareDirectories(files *Files, sort string) []string {
|
|||
return directories
|
||||
}
|
||||
|
||||
func pickFile(args []string, filters *Filters, sort string, index *FileIndex, registeredFormats *formats.SupportedFormats) (string, error) {
|
||||
fileList, fromCache := fileList(args, filters, sort, index, registeredFormats)
|
||||
func pickFile(args []string, filters *Filters, sort string, index *FileIndex, formats *types.Types) (string, error) {
|
||||
fileList, fromCache := fileList(args, filters, sort, index, formats)
|
||||
|
||||
fileCount := len(fileList)
|
||||
if fileCount < 1 {
|
||||
|
@ -586,7 +584,7 @@ func pickFile(args []string, filters *Filters, sort string, index *FileIndex, re
|
|||
filePath := fileList[val]
|
||||
|
||||
if !fromCache {
|
||||
registered, _, _, err := formats.FileType(filePath, registeredFormats)
|
||||
registered, _, _, err := types.FileType(filePath, formats)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -618,7 +616,7 @@ func normalizePath(path string) (string, error) {
|
|||
return absolutePath, nil
|
||||
}
|
||||
|
||||
func normalizePaths(args []string, types *formats.SupportedFormats) ([]string, error) {
|
||||
func normalizePaths(args []string, formats *types.Types) ([]string, error) {
|
||||
var paths []string
|
||||
|
||||
var pathList strings.Builder
|
||||
|
@ -632,7 +630,7 @@ func normalizePaths(args []string, types *formats.SupportedFormats) ([]string, e
|
|||
|
||||
pathMatches := (args[i] == path)
|
||||
|
||||
hasSupportedFiles, err := pathHasSupportedFiles(path, types)
|
||||
hasSupportedFiles, err := pathHasSupportedFiles(path, formats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
87
cmd/index.go
87
cmd/index.go
|
@ -11,6 +11,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -20,7 +21,7 @@ import (
|
|||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/yosssi/gohtml"
|
||||
"seedno.de/seednode/roulette/formats"
|
||||
"seedno.de/seednode/roulette/types"
|
||||
)
|
||||
|
||||
type FileIndex struct {
|
||||
|
@ -66,12 +67,12 @@ func (i *FileIndex) setIndex(val []string) {
|
|||
i.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (i *FileIndex) generateCache(args []string, registeredFormats *formats.SupportedFormats) {
|
||||
func (i *FileIndex) generateCache(args []string, formats *types.Types) {
|
||||
i.mutex.Lock()
|
||||
i.list = []string{}
|
||||
i.mutex.Unlock()
|
||||
|
||||
fileList(args, &Filters{}, "", i, registeredFormats)
|
||||
fileList(args, &Filters{}, "", i, formats)
|
||||
|
||||
if Cache && CacheFile != "" {
|
||||
i.Export(CacheFile)
|
||||
|
@ -138,9 +139,9 @@ func (i *FileIndex) Import(path string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func serveCacheClear(args []string, index *FileIndex, registeredFormats *formats.SupportedFormats) httprouter.Handle {
|
||||
func serveCacheClear(args []string, index *FileIndex, formats *types.Types) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
index.generateCache(args, registeredFormats)
|
||||
index.generateCache(args, formats)
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
|
@ -316,3 +317,79 @@ func serveIndexJson(args []string, index *FileIndex) httprouter.Handle {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveExtensions(formats *types.Types) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
var output strings.Builder
|
||||
|
||||
extensions := make([]string, len(formats.Extensions))
|
||||
|
||||
i := 0
|
||||
|
||||
for k := range formats.Extensions {
|
||||
extensions[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
slices.Sort(extensions)
|
||||
|
||||
for _, v := range extensions {
|
||||
output.WriteString(v + "\n")
|
||||
}
|
||||
|
||||
response := []byte(output.String())
|
||||
|
||||
w.Write(response)
|
||||
|
||||
if Verbose {
|
||||
fmt.Printf("%s | Served registered extensions list (%s) to %s in %s\n",
|
||||
startTime.Format(LogDate),
|
||||
humanReadableSize(len(response)),
|
||||
realIP(r),
|
||||
time.Since(startTime).Round(time.Microsecond),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveMimeTypes(formats *types.Types) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
var output strings.Builder
|
||||
|
||||
mimeTypes := make([]string, len(formats.MimeTypes))
|
||||
|
||||
i := 0
|
||||
|
||||
for k := range formats.MimeTypes {
|
||||
mimeTypes[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
slices.Sort(mimeTypes)
|
||||
|
||||
for _, v := range mimeTypes {
|
||||
output.WriteString(v + "\n")
|
||||
}
|
||||
|
||||
response := []byte(output.String())
|
||||
|
||||
w.Write(response)
|
||||
|
||||
if Verbose {
|
||||
fmt.Printf("%s | Served registered MIME types list (%s) to %s in %s\n",
|
||||
startTime.Format(LogDate),
|
||||
humanReadableSize(len(response)),
|
||||
realIP(r),
|
||||
time.Since(startTime).Round(time.Microsecond),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
26
cmd/root.go
26
cmd/root.go
|
@ -12,7 +12,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
ReleaseVersion string = "0.70.3"
|
||||
ReleaseVersion string = "0.75.0"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -25,9 +25,9 @@ var (
|
|||
Flash bool
|
||||
Images bool
|
||||
Index bool
|
||||
MaximumFileCount uint64
|
||||
MinimumFileCount uint64
|
||||
PageLength uint64
|
||||
MaximumFileCount uint32
|
||||
MinimumFileCount uint32
|
||||
PageLength uint32
|
||||
Port uint16
|
||||
Profile bool
|
||||
Recursive bool
|
||||
|
@ -45,13 +45,7 @@ var (
|
|||
Use: "roulette <path> [path]...",
|
||||
Short: "Serves random media from the specified directories.",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
// 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
|
||||
}
|
||||
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if Index {
|
||||
cmd.MarkFlagRequired("cache")
|
||||
}
|
||||
|
@ -59,9 +53,11 @@ var (
|
|||
if RefreshInterval != "" {
|
||||
interval, err := time.ParseDuration(RefreshInterval)
|
||||
if err != nil || interval < 500*time.Millisecond {
|
||||
log.Fatal(ErrIncorrectRefreshInterval)
|
||||
return ErrIncorrectRefreshInterval
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := ServePage(args)
|
||||
|
@ -91,9 +87,9 @@ func init() {
|
|||
rootCmd.Flags().BoolVar(&Flash, "flash", false, "enable support for shockwave flash files (via ruffle.rs)")
|
||||
rootCmd.Flags().BoolVar(&Images, "images", false, "enable support for image files")
|
||||
rootCmd.Flags().BoolVarP(&Index, "index", "i", false, "expose index endpoints")
|
||||
rootCmd.Flags().Uint64Var(&MaximumFileCount, "maximum-files", 1<<64-1, "skip directories with file counts above this value")
|
||||
rootCmd.Flags().Uint64Var(&MinimumFileCount, "minimum-files", 1, "skip directories with file counts below this value")
|
||||
rootCmd.Flags().Uint64Var(&PageLength, "page-length", 0, "pagination length for statistics and debug pages")
|
||||
rootCmd.Flags().Uint32Var(&MaximumFileCount, "maximum-files", 1<<32-1, "skip directories with file counts above this value")
|
||||
rootCmd.Flags().Uint32Var(&MinimumFileCount, "minimum-files", 1, "skip directories with file counts below this value")
|
||||
rootCmd.Flags().Uint32Var(&PageLength, "page-length", 0, "pagination length for statistics and debug pages")
|
||||
rootCmd.Flags().Uint16VarP(&Port, "port", "p", 8080, "port to listen on")
|
||||
rootCmd.Flags().BoolVar(&Profile, "profile", false, "register net/http/pprof handlers")
|
||||
rootCmd.Flags().BoolVarP(&Recursive, "recursive", "r", false, "recurse into subdirectories")
|
||||
|
|
57
cmd/web.go
57
cmd/web.go
|
@ -26,7 +26,7 @@ import (
|
|||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/yosssi/gohtml"
|
||||
"seedno.de/seednode/roulette/formats"
|
||||
"seedno.de/seednode/roulette/types"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -127,7 +127,7 @@ func serveStaticFile(paths []string, stats *ServeStats, index *FileIndex) httpro
|
|||
}
|
||||
}
|
||||
|
||||
func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFormats *formats.SupportedFormats) httprouter.Handle {
|
||||
func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, formats *types.Types) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
refererUri, err := stripQueryParams(refererToUri(r.Referer()))
|
||||
if err != nil {
|
||||
|
@ -152,7 +152,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFor
|
|||
var filePath string
|
||||
|
||||
if refererUri != "" {
|
||||
filePath, err = nextFile(strippedRefererUri, sortOrder, Regexes, registeredFormats)
|
||||
filePath, err = nextFile(strippedRefererUri, sortOrder, Regexes, formats)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
|
@ -174,7 +174,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFor
|
|||
break loop
|
||||
}
|
||||
|
||||
filePath, err = newFile(paths, filters, sortOrder, Regexes, index, registeredFormats)
|
||||
filePath, err = newFile(paths, filters, sortOrder, Regexes, index, formats)
|
||||
switch {
|
||||
case err != nil && err == ErrNoMediaFound:
|
||||
notFound(w, r, filePath)
|
||||
|
@ -200,7 +200,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *FileIndex, registeredFor
|
|||
}
|
||||
}
|
||||
|
||||
func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, registeredFormats *formats.SupportedFormats) httprouter.Handle {
|
||||
func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, formats *types.Types) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
filters := &Filters{
|
||||
includes: splitQueryParams(r.URL.Query().Get("include"), Regexes),
|
||||
|
@ -229,7 +229,7 @@ func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, registeredFo
|
|||
return
|
||||
}
|
||||
|
||||
registered, fileType, mimeType, err := formats.FileType(filePath, registeredFormats)
|
||||
registered, fileType, mimeType, err := types.FileType(filePath, formats)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
|
@ -257,12 +257,7 @@ func serveMedia(paths []string, Regexes *Regexes, index *FileIndex, registeredFo
|
|||
var htmlBody strings.Builder
|
||||
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
|
||||
htmlBody.WriteString(FaviconHtml)
|
||||
htmlBody.WriteString(`<style>html,body{margin:0;padding:0;height:100%;}`)
|
||||
htmlBody.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
|
||||
htmlBody.WriteString(`img{margin:auto;display:block;max-width:97%;max-height:97%;object-fit:scale-down;`)
|
||||
htmlBody.WriteString(`position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}`)
|
||||
htmlBody.WriteString(fileType.Css)
|
||||
htmlBody.WriteString(`</style>`)
|
||||
htmlBody.WriteString(fmt.Sprintf(`<style>%s</style>`, fileType.Css()))
|
||||
htmlBody.WriteString((fileType.Title(queryParams, fileUri, filePath, fileName, mimeType)))
|
||||
htmlBody.WriteString(`</head><body>`)
|
||||
if refreshInterval != "0ms" {
|
||||
|
@ -316,32 +311,34 @@ func ServePage(args []string) error {
|
|||
|
||||
mux := httprouter.New()
|
||||
|
||||
registeredFormats := &formats.SupportedFormats{
|
||||
Extensions: make(map[string]*formats.SupportedFormat),
|
||||
MimeTypes: make(map[string]*formats.SupportedFormat),
|
||||
formats := &types.Types{
|
||||
Extensions: make(map[string]string),
|
||||
MimeTypes: make(map[string]types.Type),
|
||||
}
|
||||
|
||||
if Audio || All {
|
||||
registeredFormats.Add(formats.RegisterAudioFormats())
|
||||
formats.Add(types.Audio{})
|
||||
}
|
||||
|
||||
if Flash || All {
|
||||
registeredFormats.Add(formats.RegisterFlashFormats())
|
||||
}
|
||||
|
||||
if Images || All {
|
||||
registeredFormats.Add(formats.RegisterImageFormats())
|
||||
formats.Add(types.Flash{})
|
||||
}
|
||||
|
||||
if Text || All {
|
||||
registeredFormats.Add(formats.RegisterTextFormats())
|
||||
formats.Add(types.Text{})
|
||||
}
|
||||
|
||||
if Videos || All {
|
||||
registeredFormats.Add(formats.RegisterVideoFormats())
|
||||
formats.Add(types.Video{})
|
||||
}
|
||||
|
||||
paths, err := normalizePaths(args, registeredFormats)
|
||||
// enable image support if no other flags are passed, to retain backwards compatibility
|
||||
// to be replaced with rootCmd.MarkFlagsOneRequired on next spf13/cobra update
|
||||
if Images || All || len(formats.Extensions) == 0 {
|
||||
formats.Add(types.Images{})
|
||||
}
|
||||
|
||||
paths, err := normalizePaths(args, formats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -382,13 +379,13 @@ func ServePage(args []string) error {
|
|||
|
||||
mux.PanicHandler = serverErrorHandler()
|
||||
|
||||
mux.GET("/", serveRoot(paths, regexes, index, registeredFormats))
|
||||
mux.GET("/", serveRoot(paths, regexes, index, formats))
|
||||
|
||||
mux.GET("/favicons/*favicon", serveFavicons())
|
||||
|
||||
mux.GET("/favicon.ico", serveFavicons())
|
||||
|
||||
mux.GET(MediaPrefix+"/*media", serveMedia(paths, regexes, index, registeredFormats))
|
||||
mux.GET(MediaPrefix+"/*media", serveMedia(paths, regexes, index, formats))
|
||||
|
||||
mux.GET(SourcePrefix+"/*static", serveStaticFile(paths, stats, index))
|
||||
|
||||
|
@ -405,10 +402,10 @@ func ServePage(args []string) error {
|
|||
}
|
||||
|
||||
if !skipIndex {
|
||||
index.generateCache(args, registeredFormats)
|
||||
index.generateCache(args, formats)
|
||||
}
|
||||
|
||||
mux.GET("/clear_cache", serveCacheClear(args, index, registeredFormats))
|
||||
mux.GET("/clear_cache", serveCacheClear(args, index, formats))
|
||||
}
|
||||
|
||||
if Index {
|
||||
|
@ -421,6 +418,10 @@ func ServePage(args []string) error {
|
|||
if PageLength != 0 {
|
||||
mux.GET("/json/:page", serveIndexJson(args, index))
|
||||
}
|
||||
|
||||
mux.GET("/extensions", serveExtensions(formats))
|
||||
|
||||
mux.GET("/mime_types", serveMimeTypes(formats))
|
||||
}
|
||||
|
||||
if Profile {
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package formats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func RegisterAudioFormats() *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 {
|
||||
return fmt.Sprintf(`<a href="/%s"><audio controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the audio tag.</audio></a>`,
|
||||
queryParams,
|
||||
fileUri,
|
||||
mime,
|
||||
fileName)
|
||||
},
|
||||
Extensions: []string{
|
||||
`.mp3`,
|
||||
`.ogg`,
|
||||
`.oga`,
|
||||
`.wav`,
|
||||
},
|
||||
MimeTypes: []string{
|
||||
`audio/mpeg`,
|
||||
`audio/ogg`,
|
||||
`audio/wav`,
|
||||
},
|
||||
Validate: func(filePath string) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package formats
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"os"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
type Dimensions struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
func RegisterImageFormats() *SupportedFormat {
|
||||
return &SupportedFormat{
|
||||
Css: ``,
|
||||
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||
dimensions, err := ImageDimensions(filePath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<title>%s (%dx%d)</title>`,
|
||||
fileName,
|
||||
dimensions.Width,
|
||||
dimensions.Height)
|
||||
},
|
||||
Body: func(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||
dimensions, err := ImageDimensions(filePath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<a href="/%s"><img src="%s" width="%d" height="%d" type="%s" alt="Roulette selected: %s"></a>`,
|
||||
queryParams,
|
||||
fileUri,
|
||||
dimensions.Width,
|
||||
dimensions.Height,
|
||||
mime,
|
||||
fileName)
|
||||
},
|
||||
Extensions: []string{
|
||||
`.apng`,
|
||||
`.avif`,
|
||||
`.bmp`,
|
||||
`.gif`,
|
||||
`.jpg`,
|
||||
`.jpeg`,
|
||||
`.jfif`,
|
||||
`.pjp`,
|
||||
`.pjpeg`,
|
||||
`.png`,
|
||||
`.svg`,
|
||||
`.webp`,
|
||||
},
|
||||
MimeTypes: []string{
|
||||
`image/apng`,
|
||||
`image/avif`,
|
||||
`image/bmp`,
|
||||
`image/gif`,
|
||||
`image/jpeg`,
|
||||
`image/png`,
|
||||
`image/svg+xml`,
|
||||
`image/webp`,
|
||||
},
|
||||
Validate: func(filePath string) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ImageDimensions(path string) (*Dimensions, error) {
|
||||
file, err := os.Open(path)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
fmt.Printf("File %s does not exist\n", path)
|
||||
return &Dimensions{}, nil
|
||||
case err != nil:
|
||||
fmt.Printf("File %s open returned error: %s\n", path, err)
|
||||
return &Dimensions{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
decodedConfig, _, err := image.DecodeConfig(file)
|
||||
switch {
|
||||
case errors.Is(err, image.ErrFormat):
|
||||
fmt.Printf("File %s has invalid image format\n", path)
|
||||
return &Dimensions{Width: 0, Height: 0}, nil
|
||||
case err != nil:
|
||||
fmt.Printf("File %s decode returned error: %s\n", path, err)
|
||||
return &Dimensions{}, err
|
||||
}
|
||||
|
||||
return &Dimensions{Width: decodedConfig.Width, Height: decodedConfig.Height}, nil
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package formats
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func RegisterTextFormats() *SupportedFormat {
|
||||
return &SupportedFormat{
|
||||
Css: `pre{margin:.5rem;}`,
|
||||
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 {
|
||||
body, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
body = []byte{}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<a href="/%s"><pre>%s</pre></a>`,
|
||||
queryParams,
|
||||
body)
|
||||
},
|
||||
Extensions: []string{
|
||||
`.css`,
|
||||
`.csv`,
|
||||
`.html`,
|
||||
`.js`,
|
||||
`.json`,
|
||||
`.md`,
|
||||
`.txt`,
|
||||
`.xml`,
|
||||
},
|
||||
MimeTypes: []string{
|
||||
`application/json`,
|
||||
`application/xml`,
|
||||
`text/css`,
|
||||
`text/csv`,
|
||||
`text/javascript`,
|
||||
`text/plain`,
|
||||
`text/plain; charset=utf-8`,
|
||||
},
|
||||
Validate: func(path string) bool {
|
||||
file, err := os.Open(path)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return false
|
||||
case err != nil:
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
head := make([]byte, 512)
|
||||
file.Read(head)
|
||||
|
||||
return utf8.Valid(head)
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package formats
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type SupportedFormat struct {
|
||||
Css string
|
||||
Title func(queryParams, fileUri, filePath, fileName, mime string) string
|
||||
Body func(queryParams, fileUri, filePath, fileName, mime string) string
|
||||
Extensions []string
|
||||
MimeTypes []string
|
||||
Validate func(filePath string) bool
|
||||
}
|
||||
|
||||
type SupportedFormats struct {
|
||||
Extensions map[string]*SupportedFormat
|
||||
MimeTypes map[string]*SupportedFormat
|
||||
}
|
||||
|
||||
func (s *SupportedFormats) Add(t *SupportedFormat) {
|
||||
for _, v := range t.Extensions {
|
||||
_, exists := s.Extensions[v]
|
||||
if !exists {
|
||||
s.Extensions[v] = t
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range t.MimeTypes {
|
||||
_, exists := s.Extensions[v]
|
||||
if !exists {
|
||||
s.MimeTypes[v] = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FileType(path string, registeredFormats *SupportedFormats) (bool, *SupportedFormat, string, error) {
|
||||
file, err := os.Open(path)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return false, nil, "", nil
|
||||
case err != nil:
|
||||
return false, nil, "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
head := make([]byte, 512)
|
||||
file.Read(head)
|
||||
|
||||
mimeType := http.DetectContentType(head)
|
||||
|
||||
// try identifying files by mime types first
|
||||
fileType, exists := registeredFormats.MimeTypes[mimeType]
|
||||
if exists {
|
||||
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
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package formats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func RegisterVideoFormats() *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 {
|
||||
return fmt.Sprintf(`<a href="/%s"><video controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the video tag.</video></a>`,
|
||||
queryParams,
|
||||
fileUri,
|
||||
mime,
|
||||
fileName)
|
||||
},
|
||||
Extensions: []string{
|
||||
`.mp4`,
|
||||
`.ogm`,
|
||||
`.ogv`,
|
||||
`.webm`,
|
||||
},
|
||||
MimeTypes: []string{
|
||||
`video/mp4`,
|
||||
`video/ogg`,
|
||||
`video/webm`,
|
||||
},
|
||||
Validate: func(filePath string) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Audio struct{}
|
||||
|
||||
func (t Audio) Css() string {
|
||||
var css strings.Builder
|
||||
|
||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
||||
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
|
||||
|
||||
return css.String()
|
||||
}
|
||||
|
||||
func (t Audio) Title(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||
return fmt.Sprintf(`<title>%s</title>`, fileName)
|
||||
}
|
||||
|
||||
func (t Audio) Body(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||
return fmt.Sprintf(`<a href="/%s"><audio controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the audio tag.</audio></a>`,
|
||||
queryParams,
|
||||
fileUri,
|
||||
mime,
|
||||
fileName)
|
||||
}
|
||||
|
||||
func (t Audio) Extensions() map[string]string {
|
||||
return map[string]string{
|
||||
`.mp3`: `audio/mpeg`,
|
||||
`.ogg`: `audio/ogg`,
|
||||
`.oga`: `audio/ogg`,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Audio) MimeTypes() []string {
|
||||
return []string{
|
||||
`application/ogg`,
|
||||
`audio/mp3`,
|
||||
`audio/mpeg`,
|
||||
`audio/mpeg3`,
|
||||
`audio/ogg`,
|
||||
`audio/x-mpeg-3`,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Audio) Validate(filePath string) bool {
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Flash struct{}
|
||||
|
||||
func (t Flash) Css() string {
|
||||
var css strings.Builder
|
||||
|
||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
||||
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
|
||||
|
||||
return css.String()
|
||||
}
|
||||
|
||||
func (t Flash) Title(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||
return fmt.Sprintf(`<title>%s</title>`, fileName)
|
||||
}
|
||||
|
||||
func (t Flash) Body(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()
|
||||
}
|
||||
|
||||
func (t Flash) Extensions() map[string]string {
|
||||
return map[string]string{
|
||||
`.swf`: `application/x-shockwave-flash`,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Flash) MimeTypes() []string {
|
||||
return []string{
|
||||
`application/x-shockwave-flash`,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Flash) Validate(filePath string) bool {
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
type Dimensions struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
type Images struct{}
|
||||
|
||||
func (t Images) Css() string {
|
||||
var css strings.Builder
|
||||
|
||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
||||
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
|
||||
css.WriteString(`img{margin:auto;display:block;max-width:97%;max-height:97%;`)
|
||||
css.WriteString(`object-fit:scale-down;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}`)
|
||||
|
||||
return css.String()
|
||||
}
|
||||
|
||||
func (t Images) Title(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||
dimensions, err := ImageDimensions(filePath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<title>%s (%dx%d)</title>`,
|
||||
fileName,
|
||||
dimensions.Width,
|
||||
dimensions.Height)
|
||||
}
|
||||
|
||||
func (t Images) Body(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||
dimensions, err := ImageDimensions(filePath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<a href="/%s"><img src="%s" width="%d" height="%d" type="%s" alt="Roulette selected: %s"></a>`,
|
||||
queryParams,
|
||||
fileUri,
|
||||
dimensions.Width,
|
||||
dimensions.Height,
|
||||
mime,
|
||||
fileName)
|
||||
}
|
||||
|
||||
func (t Images) Extensions() map[string]string {
|
||||
return map[string]string{
|
||||
`.apng`: `image/apng`,
|
||||
`.avif`: `image/avif`,
|
||||
`.bmp`: `image/bmp`,
|
||||
`.gif`: `image/gif`,
|
||||
`.jpg`: `image/jpeg`,
|
||||
`.jpeg`: `image/jpeg`,
|
||||
`.jfif`: `image/jpeg`,
|
||||
`.pjp`: `image/jpeg`,
|
||||
`.pjpeg`: `image/jpeg`,
|
||||
`.png`: `image/png`,
|
||||
`.svg`: `image/svg+xml`,
|
||||
`.webp`: `image/webp`,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Images) MimeTypes() []string {
|
||||
return []string{
|
||||
`image/apng`,
|
||||
`image/avif`,
|
||||
`image/bmp`,
|
||||
`image/gif`,
|
||||
`image/jpeg`,
|
||||
`image/png`,
|
||||
`image/svg+xml`,
|
||||
`image/webp`,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Images) Validate(filePath string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func ImageDimensions(path string) (*Dimensions, error) {
|
||||
file, err := os.Open(path)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
fmt.Printf("File %s does not exist\n", path)
|
||||
return &Dimensions{}, nil
|
||||
case err != nil:
|
||||
fmt.Printf("File %s open returned error: %s\n", path, err)
|
||||
return &Dimensions{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
decodedConfig, _, err := image.DecodeConfig(file)
|
||||
switch {
|
||||
case errors.Is(err, image.ErrFormat):
|
||||
fmt.Printf("File %s has invalid image format\n", path)
|
||||
return &Dimensions{Width: 0, Height: 0}, nil
|
||||
case err != nil:
|
||||
fmt.Printf("File %s decode returned error: %s\n", path, err)
|
||||
return &Dimensions{}, err
|
||||
}
|
||||
|
||||
return &Dimensions{Width: decodedConfig.Width, Height: decodedConfig.Height}, nil
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type Text struct{}
|
||||
|
||||
func (t Text) Css() string {
|
||||
var css strings.Builder
|
||||
|
||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
||||
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;overflow:hidden;}`)
|
||||
css.WriteString(`textarea{border:none;caret-color:transparent;outline:none;margin:.5rem;`)
|
||||
css.WriteString(`height:99%;width:99%;white-space:pre;overflow:auto;}`)
|
||||
|
||||
return css.String()
|
||||
}
|
||||
|
||||
func (t Text) Title(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||
return fmt.Sprintf(`<title>%s</title>`, fileName)
|
||||
}
|
||||
|
||||
func (t Text) Body(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||
body, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
body = []byte{}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<a href="/%s"><textarea autofocus readonly>%s</textarea></a>`,
|
||||
queryParams,
|
||||
body)
|
||||
}
|
||||
|
||||
func (t Text) Extensions() map[string]string {
|
||||
return map[string]string{
|
||||
`.css`: `text/css`,
|
||||
`.csv`: `text/csv`,
|
||||
`.htm`: `text/html`,
|
||||
`.html`: `text/html`,
|
||||
`.js`: `text/javascript`,
|
||||
`.json`: `application/json`,
|
||||
`.md`: `text/markdown`,
|
||||
`.txt`: `text/plain`,
|
||||
`.xml`: `application/xml`,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Text) MimeTypes() []string {
|
||||
return []string{
|
||||
`application/json`,
|
||||
`application/xml`,
|
||||
`text/css`,
|
||||
`text/csv`,
|
||||
`text/html`,
|
||||
`text/javascript`,
|
||||
`text/plain`,
|
||||
`text/plain; charset=utf-8`,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Text) Validate(filePath string) bool {
|
||||
file, err := os.Open(filePath)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return false
|
||||
case err != nil:
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
head := make([]byte, 512)
|
||||
file.Read(head)
|
||||
|
||||
return utf8.Valid(head)
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Type interface {
|
||||
Css() string
|
||||
Title(queryParams, fileUri, filePath, fileName, mime string) string
|
||||
Body(queryParams, fileUri, filePath, fileName, mime string) string
|
||||
Extensions() map[string]string
|
||||
MimeTypes() []string
|
||||
Validate(filePath string) bool
|
||||
}
|
||||
|
||||
type Types struct {
|
||||
Extensions map[string]string
|
||||
MimeTypes map[string]Type
|
||||
}
|
||||
|
||||
func (s *Types) Add(t Type) {
|
||||
for k, v := range t.Extensions() {
|
||||
_, exists := s.Extensions[k]
|
||||
if !exists {
|
||||
s.Extensions[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range t.MimeTypes() {
|
||||
_, exists := s.Extensions[v]
|
||||
if !exists {
|
||||
s.MimeTypes[v] = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FileType(path string, registeredFormats *Types) (bool, Type, string, error) {
|
||||
file, err := os.Open(path)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return false, nil, "", nil
|
||||
case err != nil:
|
||||
return false, nil, "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
head := make([]byte, 512)
|
||||
file.Read(head)
|
||||
|
||||
mimeType := http.DetectContentType(head)
|
||||
|
||||
// try identifying files by mime types first
|
||||
fileType, exists := registeredFormats.MimeTypes[mimeType]
|
||||
if exists {
|
||||
return fileType.Validate(path), fileType, mimeType, nil
|
||||
}
|
||||
|
||||
// if mime type detection fails, use the file extension
|
||||
mimeType, exists = registeredFormats.Extensions[filepath.Ext(path)]
|
||||
if exists {
|
||||
fileType, exists := registeredFormats.MimeTypes[mimeType]
|
||||
|
||||
if exists {
|
||||
return fileType.Validate(path), fileType, mimeType, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil, "", nil
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Video struct{}
|
||||
|
||||
func (t Video) Css() string {
|
||||
var css strings.Builder
|
||||
|
||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
||||
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
|
||||
css.WriteString(`video{margin:auto;display:block;max-width:97%;max-height:97%;`)
|
||||
css.WriteString(`object-fit:scale-down;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}`)
|
||||
|
||||
return css.String()
|
||||
}
|
||||
|
||||
func (t Video) Title(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||
return fmt.Sprintf(`<title>%s</title>`, fileName)
|
||||
}
|
||||
|
||||
func (t Video) Body(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||
return fmt.Sprintf(`<a href="/%s"><video controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the video tag.</video></a>`,
|
||||
queryParams,
|
||||
fileUri,
|
||||
mime,
|
||||
fileName)
|
||||
}
|
||||
|
||||
func (t Video) Extensions() map[string]string {
|
||||
return map[string]string{
|
||||
`.mp4`: `video/mp4`,
|
||||
`.ogm`: `video/ogg`,
|
||||
`.ogv`: `video/ogg`,
|
||||
`.webm`: `video/webm`,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Video) MimeTypes() []string {
|
||||
return []string{
|
||||
`video/mp4`,
|
||||
`video/ogg`,
|
||||
`video/webm`,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Video) Validate(filePath string) bool {
|
||||
return true
|
||||
}
|
Loading…
Reference in New Issue