Split source into more files and fixed bug where images wouldn't display due to swapped parameter order
This commit is contained in:
parent
951ff50f88
commit
86184f26bf
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/yosssi/gohtml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func serveDebugHtml(args []string, index *Index, paginate bool) httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
indexDump := index.Index()
|
||||||
|
|
||||||
|
fileCount := len(indexDump)
|
||||||
|
|
||||||
|
var startIndex, stopIndex int
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(p.ByName("page"))
|
||||||
|
if err != nil || page <= 0 {
|
||||||
|
startIndex = 0
|
||||||
|
stopIndex = fileCount
|
||||||
|
} else {
|
||||||
|
startIndex = ((page - 1) * int(pageLength))
|
||||||
|
stopIndex = (startIndex + int(pageLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
if startIndex > (fileCount - 1) {
|
||||||
|
indexDump = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stopIndex > fileCount {
|
||||||
|
stopIndex = fileCount
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(indexDump, func(p, q int) bool {
|
||||||
|
return strings.ToLower(indexDump[p]) < strings.ToLower(indexDump[q])
|
||||||
|
})
|
||||||
|
|
||||||
|
var htmlBody strings.Builder
|
||||||
|
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
|
||||||
|
htmlBody.WriteString(FaviconHtml)
|
||||||
|
htmlBody.WriteString(`<style>a{text-decoration:none;height:100%;width:100%;color:inherit;cursor:pointer}`)
|
||||||
|
htmlBody.WriteString(`table,td,tr{border:1px solid black;border-collapse:collapse}td{white-space:nowrap;padding:.5em}</style>`)
|
||||||
|
htmlBody.WriteString(fmt.Sprintf("<title>Index contains %d files</title></head><body><table>", fileCount))
|
||||||
|
if len(indexDump) > 0 {
|
||||||
|
for _, v := range indexDump[startIndex:stopIndex] {
|
||||||
|
var shouldSort = ""
|
||||||
|
|
||||||
|
if sorting {
|
||||||
|
shouldSort = "?sort=asc"
|
||||||
|
}
|
||||||
|
htmlBody.WriteString(fmt.Sprintf("<tr><td><a href=\"%s%s%s\">%s</a></td></tr>\n", MediaPrefix, v, shouldSort, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pageLength != 0 {
|
||||||
|
nextPage := page + 1
|
||||||
|
|
||||||
|
if nextPage > (fileCount/int(pageLength)) && fileCount%int(pageLength) == 0 {
|
||||||
|
nextPage = fileCount / int(pageLength)
|
||||||
|
} else if nextPage > (fileCount / int(pageLength)) {
|
||||||
|
nextPage = (fileCount / int(pageLength)) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPage := page - 1
|
||||||
|
if prevPage < 1 {
|
||||||
|
prevPage = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if paginate {
|
||||||
|
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\">Prev</button>", prevPage))
|
||||||
|
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\">Next</button>", nextPage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody.WriteString(`</table></body></html>`)
|
||||||
|
|
||||||
|
b, err := io.WriteString(w, gohtml.Format(htmlBody.String()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("%s | Served HTML debug page (%s) to %s in %s\n",
|
||||||
|
startTime.Format(LogDate),
|
||||||
|
humanReadableSize(b),
|
||||||
|
realIP(r),
|
||||||
|
time.Since(startTime).Round(time.Microsecond),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveDebugJson(args []string, index *Index) httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
indexDump := index.Index()
|
||||||
|
|
||||||
|
fileCount := len(indexDump)
|
||||||
|
|
||||||
|
sort.SliceStable(indexDump, func(p, q int) bool {
|
||||||
|
return strings.ToLower(indexDump[p]) < strings.ToLower(indexDump[q])
|
||||||
|
})
|
||||||
|
|
||||||
|
var startIndex, stopIndex int
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(p.ByName("page"))
|
||||||
|
if err != nil || page <= 0 {
|
||||||
|
startIndex = 0
|
||||||
|
stopIndex = fileCount
|
||||||
|
} else {
|
||||||
|
startIndex = ((page - 1) * int(pageLength))
|
||||||
|
stopIndex = (startIndex + int(pageLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
if startIndex > (fileCount - 1) {
|
||||||
|
indexDump = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stopIndex > fileCount {
|
||||||
|
stopIndex = fileCount
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := json.MarshalIndent(indexDump[startIndex:stopIndex], "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
|
||||||
|
serverError(w, r, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(response)
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("%s | Served JSON debug page (%s) to %s in %s\n",
|
||||||
|
startTime.Format(LogDate),
|
||||||
|
humanReadableSize(len(response)),
|
||||||
|
realIP(r),
|
||||||
|
time.Since(startTime).Round(time.Microsecond),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yosssi/gohtml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newErrorPage(title, body string) string {
|
||||||
|
var htmlBody strings.Builder
|
||||||
|
|
||||||
|
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
|
||||||
|
htmlBody.WriteString(FaviconHtml)
|
||||||
|
htmlBody.WriteString(`<style>a{display:block;height:100%;width:100%;text-decoration:none;color:inherit;cursor:auto;}</style>`)
|
||||||
|
htmlBody.WriteString(fmt.Sprintf("<title>%s</title></head>", title))
|
||||||
|
htmlBody.WriteString(fmt.Sprintf("<body><a href=\"/\">%s</a></body></html>", body))
|
||||||
|
|
||||||
|
return htmlBody.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func notFound(w http.ResponseWriter, r *http.Request, filePath string) error {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("%s | Unavailable file %s requested by %s\n",
|
||||||
|
startTime.Format(LogDate),
|
||||||
|
filePath,
|
||||||
|
r.RemoteAddr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Header().Add("Content-Type", "text/html")
|
||||||
|
|
||||||
|
_, err := io.WriteString(w, gohtml.Format(newErrorPage("Not Found", "404 Page not found")))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverError(w http.ResponseWriter, r *http.Request, i interface{}) {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("%s | Invalid request for %s from %s\n",
|
||||||
|
startTime.Format(LogDate),
|
||||||
|
r.URL.Path,
|
||||||
|
r.RemoteAddr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Header().Add("Content-Type", "text/html")
|
||||||
|
|
||||||
|
io.WriteString(w, gohtml.Format(newErrorPage("Server Error", "500 Internal Server Error")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverErrorHandler() func(http.ResponseWriter, *http.Request, interface{}) {
|
||||||
|
return serverError
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"embed"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed favicons/*
|
||||||
|
var favicons embed.FS
|
||||||
|
|
||||||
|
const (
|
||||||
|
FaviconHtml string = `<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/favicons/site.webmanifest">
|
||||||
|
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">`
|
||||||
|
)
|
||||||
|
|
||||||
|
func serveFavicons() httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
fname := strings.TrimPrefix(r.URL.Path, "/")
|
||||||
|
|
||||||
|
data, err := favicons.ReadFile(fname)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Write(bytes.NewBufferString("Content-Length: " + strconv.Itoa(len(data))))
|
||||||
|
|
||||||
|
w.Write(data)
|
||||||
|
}
|
||||||
|
}
|
84
cmd/files.go
84
cmd/files.go
|
@ -7,10 +7,6 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
_ "image/gif"
|
|
||||||
_ "image/jpeg"
|
|
||||||
_ "image/png"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
@ -23,8 +19,6 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "golang.org/x/image/bmp"
|
|
||||||
_ "golang.org/x/image/webp"
|
|
||||||
"seedno.de/seednode/roulette/formats"
|
"seedno.de/seednode/roulette/formats"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,27 +37,8 @@ type Concurrency struct {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNoMediaFound = errors.New("no supported media formats found which match all criteria")
|
ErrNoMediaFound = errors.New("no supported media formats found which match all criteria")
|
||||||
Extensions = [12]string{
|
|
||||||
".bmp",
|
|
||||||
".gif",
|
|
||||||
".jpeg",
|
|
||||||
".jpg",
|
|
||||||
".mp3",
|
|
||||||
".mp4",
|
|
||||||
".ogg",
|
|
||||||
".ogv",
|
|
||||||
".png",
|
|
||||||
".wav",
|
|
||||||
".webm",
|
|
||||||
".webp",
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Dimensions struct {
|
|
||||||
width int
|
|
||||||
height int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Files struct {
|
type Files struct {
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
list map[string][]string
|
list map[string][]string
|
||||||
|
@ -123,27 +98,6 @@ func humanReadableSize(bytes int) string {
|
||||||
float64(bytes)/float64(div), "KMGTPE"[exp])
|
float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageDimensions(path string) (*Dimensions, error) {
|
|
||||||
file, err := os.Open(path)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, os.ErrNotExist):
|
|
||||||
return &Dimensions{}, nil
|
|
||||||
case err != nil:
|
|
||||||
return &Dimensions{}, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
myImage, _, err := image.DecodeConfig(file)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, image.ErrFormat):
|
|
||||||
return &Dimensions{width: 0, height: 0}, nil
|
|
||||||
case err != nil:
|
|
||||||
return &Dimensions{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Dimensions{width: myImage.Width, height: myImage.Height}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func preparePath(path string) string {
|
func preparePath(path string) string {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return fmt.Sprintf("%s/%s", MediaPrefix, filepath.ToSlash(path))
|
return fmt.Sprintf("%s/%s", MediaPrefix, filepath.ToSlash(path))
|
||||||
|
@ -154,12 +108,12 @@ func preparePath(path string) string {
|
||||||
|
|
||||||
func appendPath(directory, path string, files *Files, stats *ScanStats, registeredFormats *formats.SupportedFormats, shouldCache bool) error {
|
func appendPath(directory, path string, files *Files, stats *ScanStats, registeredFormats *formats.SupportedFormats, shouldCache bool) error {
|
||||||
if shouldCache {
|
if shouldCache {
|
||||||
supported, _, _, err := formats.FileType(path, registeredFormats)
|
registered, _, _, err := formats.FileType(path, registeredFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !supported {
|
if !registered {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,8 +178,8 @@ func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexes, index *Index, types *formats.SupportedFormats) (string, error) {
|
func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexes, index *Index, registeredFormats *formats.SupportedFormats) (string, error) {
|
||||||
filePath, err := pickFile(paths, filters, sortOrder, index, types)
|
filePath, err := pickFile(paths, filters, sortOrder, index, registeredFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
@ -239,7 +193,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case sortOrder == "asc":
|
case sortOrder == "asc":
|
||||||
filePath, err = tryExtensions(path)
|
filePath, err = tryExtensions(path, registeredFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -247,7 +201,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
|
||||||
for {
|
for {
|
||||||
path.increment()
|
path.increment()
|
||||||
|
|
||||||
filePath, err = tryExtensions(path)
|
filePath, err = tryExtensions(path, registeredFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -255,7 +209,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
|
||||||
if filePath == "" {
|
if filePath == "" {
|
||||||
path.decrement()
|
path.decrement()
|
||||||
|
|
||||||
filePath, err = tryExtensions(path)
|
filePath, err = tryExtensions(path, registeredFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -268,7 +222,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe
|
||||||
return filePath, nil
|
return filePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextFile(filePath, sortOrder string, Regexes *Regexes) (string, error) {
|
func nextFile(filePath, sortOrder string, Regexes *Regexes, registeredFormats *formats.SupportedFormats) (string, error) {
|
||||||
path, err := splitPath(filePath, Regexes)
|
path, err := splitPath(filePath, Regexes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -283,7 +237,7 @@ func nextFile(filePath, sortOrder string, Regexes *Regexes) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName, err := tryExtensions(path)
|
fileName, err := tryExtensions(path, registeredFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -313,10 +267,10 @@ func splitPath(path string, Regexes *Regexes) (*Path, error) {
|
||||||
return &p, nil
|
return &p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tryExtensions(p *Path) (string, error) {
|
func tryExtensions(p *Path, registeredFormats *formats.SupportedFormats) (string, error) {
|
||||||
var fileName string
|
var fileName string
|
||||||
|
|
||||||
for _, extension := range Extensions {
|
for _, extension := range registeredFormats.Extensions() {
|
||||||
fileName = fmt.Sprintf("%s%.3d%s", p.base, p.number, extension)
|
fileName = fmt.Sprintf("%s%.3d%s", p.base, p.number, extension)
|
||||||
|
|
||||||
exists, err := fileExists(fileName)
|
exists, err := fileExists(fileName)
|
||||||
|
@ -368,8 +322,8 @@ func pathIsValid(filePath string, paths []string) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathHasSupportedFiles(path string, types *formats.SupportedFormats) (bool, error) {
|
func pathHasSupportedFiles(path string, registeredFormats *formats.SupportedFormats) (bool, error) {
|
||||||
hasSupportedFiles := 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 {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -380,13 +334,13 @@ func pathHasSupportedFiles(path string, types *formats.SupportedFormats) (bool,
|
||||||
case !recursive && info.IsDir() && p != path:
|
case !recursive && info.IsDir() && p != path:
|
||||||
return filepath.SkipDir
|
return filepath.SkipDir
|
||||||
case !info.IsDir():
|
case !info.IsDir():
|
||||||
supported, _, _, err := formats.FileType(p, types)
|
registered, _, _, err := formats.FileType(p, registeredFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if supported {
|
if registered {
|
||||||
hasSupportedFiles <- true
|
hasRegisteredFiles <- true
|
||||||
return filepath.SkipAll
|
return filepath.SkipAll
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -398,7 +352,7 @@ func pathHasSupportedFiles(path string, types *formats.SupportedFormats) (bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-hasSupportedFiles:
|
case <-hasRegisteredFiles:
|
||||||
return true, nil
|
return true, nil
|
||||||
default:
|
default:
|
||||||
return false, nil
|
return false, nil
|
||||||
|
@ -628,12 +582,12 @@ func pickFile(args []string, filters *Filters, sort string, index *Index, regist
|
||||||
filePath := fileList[val]
|
filePath := fileList[val]
|
||||||
|
|
||||||
if !fromCache {
|
if !fromCache {
|
||||||
supported, _, _, err := formats.FileType(filePath, registeredFormats)
|
registered, _, _, err := formats.FileType(filePath, registeredFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if supported {
|
if registered {
|
||||||
return filePath, nil
|
return filePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type Filters struct {
|
||||||
|
includes []string
|
||||||
|
excludes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) IsEmpty() bool {
|
||||||
|
return !(f.HasIncludes() || f.HasExcludes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) HasIncludes() bool {
|
||||||
|
return len(f.includes) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) Includes() string {
|
||||||
|
return strings.Join(f.includes, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) HasExcludes() bool {
|
||||||
|
return len(f.excludes) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) Excludes() string {
|
||||||
|
return strings.Join(f.excludes, ",")
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
|
"seedno.de/seednode/roulette/formats"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Index struct {
|
||||||
|
mutex sync.RWMutex
|
||||||
|
list []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Index) Index() []string {
|
||||||
|
i.mutex.RLock()
|
||||||
|
val := i.list
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Index) Remove(path string) {
|
||||||
|
i.mutex.RLock()
|
||||||
|
tempIndex := make([]string, len(i.list))
|
||||||
|
copy(tempIndex, i.list)
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
|
||||||
|
var position int
|
||||||
|
|
||||||
|
for k, v := range tempIndex {
|
||||||
|
if path == v {
|
||||||
|
position = k
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tempIndex[position] = tempIndex[len(tempIndex)-1]
|
||||||
|
|
||||||
|
i.mutex.Lock()
|
||||||
|
i.list = make([]string, len(tempIndex)-1)
|
||||||
|
copy(i.list, tempIndex[:len(tempIndex)-1])
|
||||||
|
i.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Index) setIndex(val []string) {
|
||||||
|
i.mutex.Lock()
|
||||||
|
i.list = val
|
||||||
|
i.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Index) generateCache(args []string, registeredFormats *formats.SupportedFormats) {
|
||||||
|
i.mutex.Lock()
|
||||||
|
i.list = []string{}
|
||||||
|
i.mutex.Unlock()
|
||||||
|
|
||||||
|
fileList(args, &Filters{}, "", i, registeredFormats)
|
||||||
|
|
||||||
|
if cache && cacheFile != "" {
|
||||||
|
i.Export(cacheFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Index) IsEmpty() bool {
|
||||||
|
i.mutex.RLock()
|
||||||
|
length := len(i.list)
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
|
||||||
|
return length == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Index) Export(path string) error {
|
||||||
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
z, err := zstd.NewWriter(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer z.Close()
|
||||||
|
|
||||||
|
enc := gob.NewEncoder(z)
|
||||||
|
|
||||||
|
i.mutex.RLock()
|
||||||
|
|
||||||
|
enc.Encode(&i.list)
|
||||||
|
|
||||||
|
i.mutex.RUnlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Index) Import(path string) error {
|
||||||
|
file, err := os.OpenFile(path, os.O_RDONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
z, err := zstd.NewReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer z.Close()
|
||||||
|
|
||||||
|
dec := gob.NewDecoder(z)
|
||||||
|
|
||||||
|
i.mutex.Lock()
|
||||||
|
|
||||||
|
err = dec.Decode(&i.list)
|
||||||
|
|
||||||
|
i.mutex.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveCacheClear(args []string, index *Index, registeredFormats *formats.SupportedFormats) httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
index.generateCache(args, registeredFormats)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
|
||||||
|
w.Write([]byte("Ok"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version string = "0.64.0"
|
Version string = "0.65.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -0,0 +1,247 @@
|
||||||
|
/*
|
||||||
|
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServeStats struct {
|
||||||
|
mutex sync.RWMutex
|
||||||
|
list []string
|
||||||
|
count map[string]uint32
|
||||||
|
size map[string]string
|
||||||
|
times map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type exportedServeStats struct {
|
||||||
|
List []string
|
||||||
|
Count map[string]uint32
|
||||||
|
Size map[string]string
|
||||||
|
Times map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServeStats) incrementCounter(file string, timestamp time.Time, filesize string) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
|
||||||
|
s.count[file]++
|
||||||
|
|
||||||
|
s.times[file] = append(s.times[file], timestamp.Format(LogDate))
|
||||||
|
|
||||||
|
_, exists := s.size[file]
|
||||||
|
if !exists {
|
||||||
|
s.size[file] = filesize
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains(s.list, file) {
|
||||||
|
s.list = append(s.list, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServeStats) toExported() *exportedServeStats {
|
||||||
|
stats := &exportedServeStats{
|
||||||
|
List: make([]string, len(s.list)),
|
||||||
|
Count: make(map[string]uint32),
|
||||||
|
Size: make(map[string]string),
|
||||||
|
Times: make(map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.RLock()
|
||||||
|
|
||||||
|
copy(stats.List, s.list)
|
||||||
|
|
||||||
|
for k, v := range s.count {
|
||||||
|
stats.Count[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.size {
|
||||||
|
stats.Size[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.times {
|
||||||
|
stats.Times[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.RUnlock()
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServeStats) toImported(stats *exportedServeStats) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
|
||||||
|
s.list = make([]string, len(stats.List))
|
||||||
|
|
||||||
|
copy(s.list, stats.List)
|
||||||
|
|
||||||
|
for k, v := range stats.Count {
|
||||||
|
s.count[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range stats.Size {
|
||||||
|
s.size[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range stats.Times {
|
||||||
|
s.times[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServeStats) ListFiles(page int) ([]byte, error) {
|
||||||
|
stats := s.toExported()
|
||||||
|
|
||||||
|
sort.SliceStable(stats.List, func(p, q int) bool {
|
||||||
|
return strings.ToLower(stats.List[p]) < strings.ToLower(stats.List[q])
|
||||||
|
})
|
||||||
|
|
||||||
|
var startIndex, stopIndex int
|
||||||
|
|
||||||
|
if page == -1 {
|
||||||
|
startIndex = 0
|
||||||
|
stopIndex = len(stats.List) - 1
|
||||||
|
} else {
|
||||||
|
startIndex = ((page - 1) * int(pageLength))
|
||||||
|
stopIndex = (startIndex + int(pageLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
if startIndex > len(stats.List)-1 {
|
||||||
|
return []byte("{}"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if stopIndex > len(stats.List)-1 {
|
||||||
|
stopIndex = len(stats.List) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
a := make([]timesServed, stopIndex-startIndex)
|
||||||
|
|
||||||
|
for k, v := range stats.List[startIndex:stopIndex] {
|
||||||
|
a[k] = timesServed{v, stats.Count[v], stats.Size[v], stats.Times[v]}
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := json.MarshalIndent(a, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServeStats) Export(path string) error {
|
||||||
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
z, err := zstd.NewWriter(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer z.Close()
|
||||||
|
|
||||||
|
enc := gob.NewEncoder(z)
|
||||||
|
|
||||||
|
stats := s.toExported()
|
||||||
|
|
||||||
|
err = enc.Encode(&stats)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServeStats) Import(path string) error {
|
||||||
|
file, err := os.OpenFile(path, os.O_RDONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
z, err := zstd.NewReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer z.Close()
|
||||||
|
|
||||||
|
dec := gob.NewDecoder(z)
|
||||||
|
|
||||||
|
stats := &exportedServeStats{
|
||||||
|
List: []string{},
|
||||||
|
Count: make(map[string]uint32),
|
||||||
|
Size: make(map[string]string),
|
||||||
|
Times: make(map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dec.Decode(stats)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.toImported(stats)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type timesServed struct {
|
||||||
|
File string
|
||||||
|
Served uint32
|
||||||
|
Size string
|
||||||
|
Times []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveStats(args []string, stats *ServeStats) httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(p.ByName("page"))
|
||||||
|
if err != nil || page == 0 {
|
||||||
|
page = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := stats.ListFiles(page)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
|
||||||
|
serverError(w, r, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
w.Write(response)
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("%s | Served statistics page (%s) to %s in %s\n",
|
||||||
|
startTime.Format(LogDate),
|
||||||
|
humanReadableSize(len(response)),
|
||||||
|
realIP(r),
|
||||||
|
time.Since(startTime).Round(time.Microsecond),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if statisticsFile != "" {
|
||||||
|
stats.Export(statisticsFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
/*
|
||||||
|
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RefreshInterval(r *http.Request) (int64, string) {
|
||||||
|
var interval string
|
||||||
|
|
||||||
|
if refreshInterval == "" {
|
||||||
|
interval = r.URL.Query().Get("refresh")
|
||||||
|
} else {
|
||||||
|
interval = refreshInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, err := time.ParseDuration(interval)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err != nil || duration == 0:
|
||||||
|
return 0, "0ms"
|
||||||
|
case duration < 500*time.Millisecond:
|
||||||
|
return 500, "500ms"
|
||||||
|
default:
|
||||||
|
return duration.Milliseconds(), interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SortOrder(r *http.Request) string {
|
||||||
|
sortOrder := r.URL.Query().Get("sort")
|
||||||
|
if sortOrder == "asc" || sortOrder == "desc" {
|
||||||
|
return sortOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitQueryParams(query string, Regexes *Regexes) []string {
|
||||||
|
results := []string{}
|
||||||
|
|
||||||
|
if query == "" {
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
params := strings.Split(query, ",")
|
||||||
|
|
||||||
|
for i := 0; i < len(params); i++ {
|
||||||
|
if Regexes.alphanumeric.MatchString(params[i]) {
|
||||||
|
results = append(results, strings.ToLower(params[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateQueryParams(filters *Filters, sortOrder, refreshInterval string) string {
|
||||||
|
var hasParams bool
|
||||||
|
|
||||||
|
var queryParams strings.Builder
|
||||||
|
|
||||||
|
queryParams.WriteString("?")
|
||||||
|
|
||||||
|
if filtering {
|
||||||
|
queryParams.WriteString("include=")
|
||||||
|
if filters.HasIncludes() {
|
||||||
|
queryParams.WriteString(filters.Includes())
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParams.WriteString("&exclude=")
|
||||||
|
if filters.HasExcludes() {
|
||||||
|
queryParams.WriteString(filters.Excludes())
|
||||||
|
}
|
||||||
|
|
||||||
|
hasParams = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if sorting {
|
||||||
|
if hasParams {
|
||||||
|
queryParams.WriteString("&")
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParams.WriteString(fmt.Sprintf("sort=%s", sortOrder))
|
||||||
|
|
||||||
|
hasParams = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasParams {
|
||||||
|
queryParams.WriteString("&")
|
||||||
|
}
|
||||||
|
queryParams.WriteString(fmt.Sprintf("refresh=%s", refreshInterval))
|
||||||
|
|
||||||
|
return queryParams.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripQueryParams(u string) (string, error) {
|
||||||
|
uri, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
uri.RawQuery = ""
|
||||||
|
|
||||||
|
escapedUri, err := url.QueryUnescape(uri.String())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return strings.TrimPrefix(escapedUri, "/"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return escapedUri, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateFileUri(path string) string {
|
||||||
|
var uri strings.Builder
|
||||||
|
|
||||||
|
uri.WriteString(SourcePrefix)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
uri.WriteString(`/`)
|
||||||
|
}
|
||||||
|
uri.WriteString(path)
|
||||||
|
|
||||||
|
return uri.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func refererToUri(referer string) string {
|
||||||
|
parts := strings.SplitAfterN(referer, "/", 4)
|
||||||
|
|
||||||
|
if len(parts) < 4 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/" + parts[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
func realIP(r *http.Request) string {
|
||||||
|
remoteAddr := strings.SplitAfter(r.RemoteAddr, ":")
|
||||||
|
|
||||||
|
if len(remoteAddr) < 1 {
|
||||||
|
return r.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
remotePort := remoteAddr[len(remoteAddr)-1]
|
||||||
|
|
||||||
|
cfIP := r.Header.Get("Cf-Connecting-Ip")
|
||||||
|
xRealIp := r.Header.Get("X-Real-Ip")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cfIP != "":
|
||||||
|
return cfIP + ":" + remotePort
|
||||||
|
case xRealIp != "":
|
||||||
|
return xRealIp + ":" + remotePort
|
||||||
|
default:
|
||||||
|
return r.RemoteAddr
|
||||||
|
}
|
||||||
|
}
|
813
cmd/web.go
813
cmd/web.go
|
@ -6,21 +6,16 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
|
||||||
"encoding/gob"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -30,28 +25,16 @@ import (
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"github.com/klauspost/compress/zstd"
|
|
||||||
"github.com/yosssi/gohtml"
|
"github.com/yosssi/gohtml"
|
||||||
"seedno.de/seednode/roulette/formats"
|
"seedno.de/seednode/roulette/formats"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed favicons/*
|
|
||||||
var favicons embed.FS
|
|
||||||
|
|
||||||
const (
|
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
|
||||||
|
|
||||||
FaviconHtml string = `<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png">
|
|
||||||
<link rel="manifest" href="/favicons/site.webmanifest">
|
|
||||||
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5">
|
|
||||||
<meta name="msapplication-TileColor" content="#da532c">
|
|
||||||
<meta name="theme-color" content="#ffffff">`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Regexes struct {
|
type Regexes struct {
|
||||||
|
@ -59,730 +42,6 @@ type Regexes struct {
|
||||||
filename *regexp.Regexp
|
filename *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
type Filters struct {
|
|
||||||
includes []string
|
|
||||||
excludes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Filters) IsEmpty() bool {
|
|
||||||
return !(f.HasIncludes() || f.HasExcludes())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Filters) HasIncludes() bool {
|
|
||||||
return len(f.includes) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Filters) Includes() string {
|
|
||||||
return strings.Join(f.includes, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Filters) HasExcludes() bool {
|
|
||||||
return len(f.excludes) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Filters) Excludes() string {
|
|
||||||
return strings.Join(f.excludes, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Index struct {
|
|
||||||
mutex sync.RWMutex
|
|
||||||
list []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Index) Index() []string {
|
|
||||||
i.mutex.RLock()
|
|
||||||
val := i.list
|
|
||||||
i.mutex.RUnlock()
|
|
||||||
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Index) Remove(path string) {
|
|
||||||
i.mutex.RLock()
|
|
||||||
tempIndex := make([]string, len(i.list))
|
|
||||||
copy(tempIndex, i.list)
|
|
||||||
i.mutex.RUnlock()
|
|
||||||
|
|
||||||
var position int
|
|
||||||
|
|
||||||
for k, v := range tempIndex {
|
|
||||||
if path == v {
|
|
||||||
position = k
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tempIndex[position] = tempIndex[len(tempIndex)-1]
|
|
||||||
|
|
||||||
i.mutex.Lock()
|
|
||||||
i.list = make([]string, len(tempIndex)-1)
|
|
||||||
copy(i.list, tempIndex[:len(tempIndex)-1])
|
|
||||||
i.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Index) setIndex(val []string) {
|
|
||||||
i.mutex.Lock()
|
|
||||||
i.list = val
|
|
||||||
i.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Index) generateCache(args []string, supportedFormats *formats.SupportedFormats) {
|
|
||||||
i.mutex.Lock()
|
|
||||||
i.list = []string{}
|
|
||||||
i.mutex.Unlock()
|
|
||||||
|
|
||||||
fileList(args, &Filters{}, "", i, supportedFormats)
|
|
||||||
|
|
||||||
if cache && cacheFile != "" {
|
|
||||||
i.Export(cacheFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Index) IsEmpty() bool {
|
|
||||||
i.mutex.RLock()
|
|
||||||
length := len(i.list)
|
|
||||||
i.mutex.RUnlock()
|
|
||||||
|
|
||||||
return length == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Index) Export(path string) error {
|
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
z, err := zstd.NewWriter(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer z.Close()
|
|
||||||
|
|
||||||
enc := gob.NewEncoder(z)
|
|
||||||
|
|
||||||
i.mutex.RLock()
|
|
||||||
|
|
||||||
enc.Encode(&i.list)
|
|
||||||
|
|
||||||
i.mutex.RUnlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Index) Import(path string) error {
|
|
||||||
file, err := os.OpenFile(path, os.O_RDONLY, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
z, err := zstd.NewReader(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer z.Close()
|
|
||||||
|
|
||||||
dec := gob.NewDecoder(z)
|
|
||||||
|
|
||||||
i.mutex.Lock()
|
|
||||||
|
|
||||||
err = dec.Decode(&i.list)
|
|
||||||
|
|
||||||
i.mutex.Unlock()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServeStats struct {
|
|
||||||
mutex sync.RWMutex
|
|
||||||
list []string
|
|
||||||
count map[string]uint32
|
|
||||||
size map[string]string
|
|
||||||
times map[string][]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type exportedServeStats struct {
|
|
||||||
List []string
|
|
||||||
Count map[string]uint32
|
|
||||||
Size map[string]string
|
|
||||||
Times map[string][]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServeStats) incrementCounter(file string, timestamp time.Time, filesize string) {
|
|
||||||
s.mutex.Lock()
|
|
||||||
|
|
||||||
s.count[file]++
|
|
||||||
|
|
||||||
s.times[file] = append(s.times[file], timestamp.Format(LogDate))
|
|
||||||
|
|
||||||
_, exists := s.size[file]
|
|
||||||
if !exists {
|
|
||||||
s.size[file] = filesize
|
|
||||||
}
|
|
||||||
|
|
||||||
if !contains(s.list, file) {
|
|
||||||
s.list = append(s.list, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServeStats) toExported() *exportedServeStats {
|
|
||||||
stats := &exportedServeStats{
|
|
||||||
List: make([]string, len(s.list)),
|
|
||||||
Count: make(map[string]uint32),
|
|
||||||
Size: make(map[string]string),
|
|
||||||
Times: make(map[string][]string),
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mutex.RLock()
|
|
||||||
|
|
||||||
copy(stats.List, s.list)
|
|
||||||
|
|
||||||
for k, v := range s.count {
|
|
||||||
stats.Count[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range s.size {
|
|
||||||
stats.Size[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range s.times {
|
|
||||||
stats.Times[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mutex.RUnlock()
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServeStats) toImported(stats *exportedServeStats) {
|
|
||||||
s.mutex.Lock()
|
|
||||||
|
|
||||||
s.list = make([]string, len(stats.List))
|
|
||||||
|
|
||||||
copy(s.list, stats.List)
|
|
||||||
|
|
||||||
for k, v := range stats.Count {
|
|
||||||
s.count[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range stats.Size {
|
|
||||||
s.size[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range stats.Times {
|
|
||||||
s.times[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServeStats) ListFiles(page int) ([]byte, error) {
|
|
||||||
stats := s.toExported()
|
|
||||||
|
|
||||||
sort.SliceStable(stats.List, func(p, q int) bool {
|
|
||||||
return strings.ToLower(stats.List[p]) < strings.ToLower(stats.List[q])
|
|
||||||
})
|
|
||||||
|
|
||||||
var startIndex, stopIndex int
|
|
||||||
|
|
||||||
if page == -1 {
|
|
||||||
startIndex = 0
|
|
||||||
stopIndex = len(stats.List) - 1
|
|
||||||
} else {
|
|
||||||
startIndex = ((page - 1) * int(pageLength))
|
|
||||||
stopIndex = (startIndex + int(pageLength))
|
|
||||||
}
|
|
||||||
|
|
||||||
if startIndex > len(stats.List)-1 {
|
|
||||||
return []byte("{}"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if stopIndex > len(stats.List)-1 {
|
|
||||||
stopIndex = len(stats.List) - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
a := make([]timesServed, stopIndex-startIndex)
|
|
||||||
|
|
||||||
for k, v := range stats.List[startIndex:stopIndex] {
|
|
||||||
a[k] = timesServed{v, stats.Count[v], stats.Size[v], stats.Times[v]}
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := json.MarshalIndent(a, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServeStats) Export(path string) error {
|
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
z, err := zstd.NewWriter(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer z.Close()
|
|
||||||
|
|
||||||
enc := gob.NewEncoder(z)
|
|
||||||
|
|
||||||
stats := s.toExported()
|
|
||||||
|
|
||||||
err = enc.Encode(&stats)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServeStats) Import(path string) error {
|
|
||||||
file, err := os.OpenFile(path, os.O_RDONLY, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
z, err := zstd.NewReader(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer z.Close()
|
|
||||||
|
|
||||||
dec := gob.NewDecoder(z)
|
|
||||||
|
|
||||||
stats := &exportedServeStats{
|
|
||||||
List: []string{},
|
|
||||||
Count: make(map[string]uint32),
|
|
||||||
Size: make(map[string]string),
|
|
||||||
Times: make(map[string][]string),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = dec.Decode(stats)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.toImported(stats)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type timesServed struct {
|
|
||||||
File string
|
|
||||||
Served uint32
|
|
||||||
Size string
|
|
||||||
Times []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newErrorPage(title, body string) string {
|
|
||||||
var htmlBody strings.Builder
|
|
||||||
|
|
||||||
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
|
|
||||||
htmlBody.WriteString(FaviconHtml)
|
|
||||||
htmlBody.WriteString(`<style>a{display:block;height:100%;width:100%;text-decoration:none;color:inherit;cursor:auto;}</style>`)
|
|
||||||
htmlBody.WriteString(fmt.Sprintf("<title>%s</title></head>", title))
|
|
||||||
htmlBody.WriteString(fmt.Sprintf("<body><a href=\"/\">%s</a></body></html>", body))
|
|
||||||
|
|
||||||
return htmlBody.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func notFound(w http.ResponseWriter, r *http.Request, filePath string) error {
|
|
||||||
startTime := time.Now()
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("%s | Unavailable file %s requested by %s\n",
|
|
||||||
startTime.Format(LogDate),
|
|
||||||
filePath,
|
|
||||||
r.RemoteAddr,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
w.Header().Add("Content-Type", "text/html")
|
|
||||||
|
|
||||||
_, err := io.WriteString(w, gohtml.Format(newErrorPage("Not Found", "404 Page not found")))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func serverError(w http.ResponseWriter, r *http.Request, i interface{}) {
|
|
||||||
startTime := time.Now()
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("%s | Invalid request for %s from %s\n",
|
|
||||||
startTime.Format(LogDate),
|
|
||||||
r.URL.Path,
|
|
||||||
r.RemoteAddr,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
w.Header().Add("Content-Type", "text/html")
|
|
||||||
|
|
||||||
io.WriteString(w, gohtml.Format(newErrorPage("Server Error", "500 Internal Server Error")))
|
|
||||||
}
|
|
||||||
|
|
||||||
func serverErrorHandler() func(http.ResponseWriter, *http.Request, interface{}) {
|
|
||||||
return serverError
|
|
||||||
}
|
|
||||||
|
|
||||||
func RefreshInterval(r *http.Request) (int64, string) {
|
|
||||||
var interval string
|
|
||||||
|
|
||||||
if refreshInterval == "" {
|
|
||||||
interval = r.URL.Query().Get("refresh")
|
|
||||||
} else {
|
|
||||||
interval = refreshInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
duration, err := time.ParseDuration(interval)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case err != nil || duration == 0:
|
|
||||||
return 0, "0ms"
|
|
||||||
case duration < 500*time.Millisecond:
|
|
||||||
return 500, "500ms"
|
|
||||||
default:
|
|
||||||
return duration.Milliseconds(), interval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SortOrder(r *http.Request) string {
|
|
||||||
sortOrder := r.URL.Query().Get("sort")
|
|
||||||
if sortOrder == "asc" || sortOrder == "desc" {
|
|
||||||
return sortOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitQueryParams(query string, Regexes *Regexes) []string {
|
|
||||||
results := []string{}
|
|
||||||
|
|
||||||
if query == "" {
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
params := strings.Split(query, ",")
|
|
||||||
|
|
||||||
for i := 0; i < len(params); i++ {
|
|
||||||
if Regexes.alphanumeric.MatchString(params[i]) {
|
|
||||||
results = append(results, strings.ToLower(params[i]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateQueryParams(filters *Filters, sortOrder, refreshInterval string) string {
|
|
||||||
var hasParams bool
|
|
||||||
|
|
||||||
var queryParams strings.Builder
|
|
||||||
|
|
||||||
queryParams.WriteString("?")
|
|
||||||
|
|
||||||
if filtering {
|
|
||||||
queryParams.WriteString("include=")
|
|
||||||
if filters.HasIncludes() {
|
|
||||||
queryParams.WriteString(filters.Includes())
|
|
||||||
}
|
|
||||||
|
|
||||||
queryParams.WriteString("&exclude=")
|
|
||||||
if filters.HasExcludes() {
|
|
||||||
queryParams.WriteString(filters.Excludes())
|
|
||||||
}
|
|
||||||
|
|
||||||
hasParams = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if sorting {
|
|
||||||
if hasParams {
|
|
||||||
queryParams.WriteString("&")
|
|
||||||
}
|
|
||||||
|
|
||||||
queryParams.WriteString(fmt.Sprintf("sort=%s", sortOrder))
|
|
||||||
|
|
||||||
hasParams = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasParams {
|
|
||||||
queryParams.WriteString("&")
|
|
||||||
}
|
|
||||||
queryParams.WriteString(fmt.Sprintf("refresh=%s", refreshInterval))
|
|
||||||
|
|
||||||
return queryParams.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripQueryParams(u string) (string, error) {
|
|
||||||
uri, err := url.Parse(u)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
uri.RawQuery = ""
|
|
||||||
|
|
||||||
escapedUri, err := url.QueryUnescape(uri.String())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return strings.TrimPrefix(escapedUri, "/"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return escapedUri, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateFilePath(filePath string) string {
|
|
||||||
var htmlBody strings.Builder
|
|
||||||
|
|
||||||
htmlBody.WriteString(SourcePrefix)
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
htmlBody.WriteString(`/`)
|
|
||||||
}
|
|
||||||
htmlBody.WriteString(filePath)
|
|
||||||
|
|
||||||
return htmlBody.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func refererToUri(referer string) string {
|
|
||||||
parts := strings.SplitAfterN(referer, "/", 4)
|
|
||||||
|
|
||||||
if len(parts) < 4 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return "/" + parts[3]
|
|
||||||
}
|
|
||||||
|
|
||||||
func realIP(r *http.Request) string {
|
|
||||||
remoteAddr := strings.SplitAfter(r.RemoteAddr, ":")
|
|
||||||
|
|
||||||
if len(remoteAddr) < 1 {
|
|
||||||
return r.RemoteAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
remotePort := remoteAddr[len(remoteAddr)-1]
|
|
||||||
|
|
||||||
cfIP := r.Header.Get("Cf-Connecting-Ip")
|
|
||||||
xRealIp := r.Header.Get("X-Real-Ip")
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case cfIP != "":
|
|
||||||
return cfIP + ":" + remotePort
|
|
||||||
case xRealIp != "":
|
|
||||||
return xRealIp + ":" + remotePort
|
|
||||||
default:
|
|
||||||
return r.RemoteAddr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveCacheClear(args []string, index *Index, supportedFormats *formats.SupportedFormats) httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
index.generateCache(args, supportedFormats)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
|
||||||
|
|
||||||
w.Write([]byte("Ok"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveStats(args []string, stats *ServeStats) httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
|
|
||||||
page, err := strconv.Atoi(p.ByName("page"))
|
|
||||||
if err != nil || page == 0 {
|
|
||||||
page = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := stats.ListFiles(page)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
|
|
||||||
serverError(w, r, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
w.Write(response)
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("%s | Served statistics page (%s) to %s in %s\n",
|
|
||||||
startTime.Format(LogDate),
|
|
||||||
humanReadableSize(len(response)),
|
|
||||||
realIP(r),
|
|
||||||
time.Since(startTime).Round(time.Microsecond),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if statisticsFile != "" {
|
|
||||||
stats.Export(statisticsFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveDebugHtml(args []string, index *Index, paginate bool) httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
|
|
||||||
indexDump := index.Index()
|
|
||||||
|
|
||||||
fileCount := len(indexDump)
|
|
||||||
|
|
||||||
var startIndex, stopIndex int
|
|
||||||
|
|
||||||
page, err := strconv.Atoi(p.ByName("page"))
|
|
||||||
if err != nil || page <= 0 {
|
|
||||||
startIndex = 0
|
|
||||||
stopIndex = fileCount
|
|
||||||
} else {
|
|
||||||
startIndex = ((page - 1) * int(pageLength))
|
|
||||||
stopIndex = (startIndex + int(pageLength))
|
|
||||||
}
|
|
||||||
|
|
||||||
if startIndex > (fileCount - 1) {
|
|
||||||
indexDump = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if stopIndex > fileCount {
|
|
||||||
stopIndex = fileCount
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(indexDump, func(p, q int) bool {
|
|
||||||
return strings.ToLower(indexDump[p]) < strings.ToLower(indexDump[q])
|
|
||||||
})
|
|
||||||
|
|
||||||
var htmlBody strings.Builder
|
|
||||||
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
|
|
||||||
htmlBody.WriteString(FaviconHtml)
|
|
||||||
htmlBody.WriteString(`<style>a{text-decoration:none;height:100%;width:100%;color:inherit;cursor:pointer}`)
|
|
||||||
htmlBody.WriteString(`table,td,tr{border:1px solid black;border-collapse:collapse}td{white-space:nowrap;padding:.5em}</style>`)
|
|
||||||
htmlBody.WriteString(fmt.Sprintf("<title>Index contains %d files</title></head><body><table>", fileCount))
|
|
||||||
if len(indexDump) > 0 {
|
|
||||||
for _, v := range indexDump[startIndex:stopIndex] {
|
|
||||||
var shouldSort = ""
|
|
||||||
|
|
||||||
if sorting {
|
|
||||||
shouldSort = "?sort=asc"
|
|
||||||
}
|
|
||||||
htmlBody.WriteString(fmt.Sprintf("<tr><td><a href=\"%s%s%s\">%s</a></td></tr>\n", MediaPrefix, v, shouldSort, v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pageLength != 0 {
|
|
||||||
nextPage := page + 1
|
|
||||||
|
|
||||||
if nextPage > (fileCount/int(pageLength)) && fileCount%int(pageLength) == 0 {
|
|
||||||
nextPage = fileCount / int(pageLength)
|
|
||||||
} else if nextPage > (fileCount / int(pageLength)) {
|
|
||||||
nextPage = (fileCount / int(pageLength)) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
prevPage := page - 1
|
|
||||||
if prevPage < 1 {
|
|
||||||
prevPage = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if paginate {
|
|
||||||
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\">Prev</button>", prevPage))
|
|
||||||
htmlBody.WriteString(fmt.Sprintf("<button onclick=\"window.location.href = '/html/%d';\">Next</button>", nextPage))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlBody.WriteString(`</table></body></html>`)
|
|
||||||
|
|
||||||
b, err := io.WriteString(w, gohtml.Format(htmlBody.String()))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("%s | Served HTML debug page (%s) to %s in %s\n",
|
|
||||||
startTime.Format(LogDate),
|
|
||||||
humanReadableSize(b),
|
|
||||||
realIP(r),
|
|
||||||
time.Since(startTime).Round(time.Microsecond),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveDebugJson(args []string, index *Index) httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
|
|
||||||
indexDump := index.Index()
|
|
||||||
|
|
||||||
fileCount := len(indexDump)
|
|
||||||
|
|
||||||
sort.SliceStable(indexDump, func(p, q int) bool {
|
|
||||||
return strings.ToLower(indexDump[p]) < strings.ToLower(indexDump[q])
|
|
||||||
})
|
|
||||||
|
|
||||||
var startIndex, stopIndex int
|
|
||||||
|
|
||||||
page, err := strconv.Atoi(p.ByName("page"))
|
|
||||||
if err != nil || page <= 0 {
|
|
||||||
startIndex = 0
|
|
||||||
stopIndex = fileCount
|
|
||||||
} else {
|
|
||||||
startIndex = ((page - 1) * int(pageLength))
|
|
||||||
stopIndex = (startIndex + int(pageLength))
|
|
||||||
}
|
|
||||||
|
|
||||||
if startIndex > (fileCount - 1) {
|
|
||||||
indexDump = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if stopIndex > fileCount {
|
|
||||||
stopIndex = fileCount
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := json.MarshalIndent(indexDump[startIndex:stopIndex], "", " ")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
|
|
||||||
serverError(w, r, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write(response)
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("%s | Served JSON debug page (%s) to %s in %s\n",
|
|
||||||
startTime.Format(LogDate),
|
|
||||||
humanReadableSize(len(response)),
|
|
||||||
realIP(r),
|
|
||||||
time.Since(startTime).Round(time.Microsecond),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveStaticFile(paths []string, stats *ServeStats, index *Index) httprouter.Handle {
|
func serveStaticFile(paths []string, stats *ServeStats, index *Index) httprouter.Handle {
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
path := strings.TrimPrefix(r.URL.Path, SourcePrefix)
|
path := strings.TrimPrefix(r.URL.Path, SourcePrefix)
|
||||||
|
@ -873,7 +132,7 @@ func serveStaticFile(paths []string, stats *ServeStats, index *Index) httprouter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveRoot(paths []string, Regexes *Regexes, index *Index, supportedFormats *formats.SupportedFormats) httprouter.Handle {
|
func serveRoot(paths []string, Regexes *Regexes, index *Index, registeredFormats *formats.SupportedFormats) httprouter.Handle {
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
refererUri, err := stripQueryParams(refererToUri(r.Referer()))
|
refererUri, err := stripQueryParams(refererToUri(r.Referer()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -898,7 +157,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *Index, supportedFormats
|
||||||
var filePath string
|
var filePath string
|
||||||
|
|
||||||
if refererUri != "" {
|
if refererUri != "" {
|
||||||
filePath, err = nextFile(strippedRefererUri, sortOrder, Regexes)
|
filePath, err = nextFile(strippedRefererUri, sortOrder, Regexes, registeredFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|
||||||
|
@ -920,7 +179,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *Index, supportedFormats
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath, err = newFile(paths, filters, sortOrder, Regexes, index, supportedFormats)
|
filePath, err = newFile(paths, filters, sortOrder, Regexes, index, registeredFormats)
|
||||||
switch {
|
switch {
|
||||||
case err != nil && err == ErrNoMediaFound:
|
case err != nil && err == ErrNoMediaFound:
|
||||||
notFound(w, r, filePath)
|
notFound(w, r, filePath)
|
||||||
|
@ -946,7 +205,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *Index, supportedFormats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveMedia(paths []string, Regexes *Regexes, index *Index, supportedFormats *formats.SupportedFormats) httprouter.Handle {
|
func serveMedia(paths []string, Regexes *Regexes, index *Index, registeredFormats *formats.SupportedFormats) httprouter.Handle {
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
filters := &Filters{
|
filters := &Filters{
|
||||||
includes: splitQueryParams(r.URL.Query().Get("include"), Regexes),
|
includes: splitQueryParams(r.URL.Query().Get("include"), Regexes),
|
||||||
|
@ -955,13 +214,13 @@ func serveMedia(paths []string, Regexes *Regexes, index *Index, supportedFormats
|
||||||
|
|
||||||
sortOrder := SortOrder(r)
|
sortOrder := SortOrder(r)
|
||||||
|
|
||||||
filePath := strings.TrimPrefix(r.URL.Path, MediaPrefix)
|
path := strings.TrimPrefix(r.URL.Path, MediaPrefix)
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
filePath = strings.TrimPrefix(filePath, "/")
|
path = strings.TrimPrefix(path, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
exists, err := fileExists(filePath)
|
exists, err := fileExists(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|
||||||
|
@ -970,12 +229,12 @@ func serveMedia(paths []string, Regexes *Regexes, index *Index, supportedFormats
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
notFound(w, r, filePath)
|
notFound(w, r, path)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
supported, fileType, mime, err := formats.FileType(filePath, supportedFormats)
|
registered, fileType, mime, err := formats.FileType(path, registeredFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|
||||||
|
@ -984,22 +243,15 @@ func serveMedia(paths []string, Regexes *Regexes, index *Index, supportedFormats
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !supported {
|
if !registered {
|
||||||
notFound(w, r, filePath)
|
notFound(w, r, path)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dimensions, err := imageDimensions(filePath)
|
fileUri := generateFileUri(path)
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
|
|
||||||
serverError(w, r, nil)
|
fileName := filepath.Base(path)
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileName := filepath.Base(filePath)
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/html")
|
w.Header().Add("Content-Type", "text/html")
|
||||||
|
|
||||||
|
@ -1007,8 +259,6 @@ func serveMedia(paths []string, Regexes *Regexes, index *Index, supportedFormats
|
||||||
|
|
||||||
queryParams := generateQueryParams(filters, sortOrder, refreshInterval)
|
queryParams := generateQueryParams(filters, sortOrder, refreshInterval)
|
||||||
|
|
||||||
path := generateFilePath(filePath)
|
|
||||||
|
|
||||||
var htmlBody strings.Builder
|
var htmlBody strings.Builder
|
||||||
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
|
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
|
||||||
htmlBody.WriteString(FaviconHtml)
|
htmlBody.WriteString(FaviconHtml)
|
||||||
|
@ -1016,14 +266,14 @@ func serveMedia(paths []string, Regexes *Regexes, index *Index, supportedFormats
|
||||||
htmlBody.WriteString(`a{display:block;height:100%;width:100%;text-decoration:none;}`)
|
htmlBody.WriteString(`a{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(`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%);}</style>`)
|
htmlBody.WriteString(`position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}</style>`)
|
||||||
htmlBody.WriteString((fileType.Title(queryParams, path, mime, fileName, dimensions.height, dimensions.width)))
|
htmlBody.WriteString((fileType.Title(queryParams, fileUri, path, fileName, mime)))
|
||||||
htmlBody.WriteString(`</head><body>`)
|
htmlBody.WriteString(`</head><body>`)
|
||||||
if refreshInterval != "0ms" {
|
if refreshInterval != "0ms" {
|
||||||
htmlBody.WriteString(fmt.Sprintf("<script>window.onload = function(){setInterval(function(){window.location.href = '/%s';}, %d);};</script>",
|
htmlBody.WriteString(fmt.Sprintf("<script>window.onload = function(){setInterval(function(){window.location.href = '/%s';}, %d);};</script>",
|
||||||
queryParams,
|
queryParams,
|
||||||
refreshTimer))
|
refreshTimer))
|
||||||
}
|
}
|
||||||
htmlBody.WriteString((fileType.Body(queryParams, path, mime, fileName, dimensions.height, dimensions.width)))
|
htmlBody.WriteString((fileType.Body(queryParams, fileUri, path, fileName, mime)))
|
||||||
htmlBody.WriteString(`</body></html>`)
|
htmlBody.WriteString(`</body></html>`)
|
||||||
|
|
||||||
_, err = io.WriteString(w, gohtml.Format(htmlBody.String()))
|
_, err = io.WriteString(w, gohtml.Format(htmlBody.String()))
|
||||||
|
@ -1037,21 +287,6 @@ func serveMedia(paths []string, Regexes *Regexes, index *Index, supportedFormats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveFavicons() httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
fname := strings.TrimPrefix(r.URL.Path, "/")
|
|
||||||
|
|
||||||
data, err := favicons.ReadFile(fname)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Write(bytes.NewBufferString("Content-Length: " + strconv.Itoa(len(data))))
|
|
||||||
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveVersion() httprouter.Handle {
|
func serveVersion() httprouter.Handle {
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
data := []byte(fmt.Sprintf("roulette v%s\n", Version))
|
data := []byte(fmt.Sprintf("roulette v%s\n", Version))
|
||||||
|
@ -1082,21 +317,21 @@ func ServePage(args []string) error {
|
||||||
return errors.New("invalid bind address provided")
|
return errors.New("invalid bind address provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
supportedFormats := &formats.SupportedFormats{}
|
registeredFormats := &formats.SupportedFormats{}
|
||||||
|
|
||||||
if audio {
|
if audio {
|
||||||
supportedFormats.Add(formats.RegisterAudioFormats())
|
registeredFormats.Add(formats.RegisterAudioFormats())
|
||||||
}
|
}
|
||||||
|
|
||||||
if images {
|
if images {
|
||||||
supportedFormats.Add(formats.RegisterImageFormats())
|
registeredFormats.Add(formats.RegisterImageFormats())
|
||||||
}
|
}
|
||||||
|
|
||||||
if videos {
|
if videos {
|
||||||
supportedFormats.Add(formats.RegisterVideoFormats())
|
registeredFormats.Add(formats.RegisterVideoFormats())
|
||||||
}
|
}
|
||||||
|
|
||||||
paths, err := normalizePaths(args, supportedFormats)
|
paths, err := normalizePaths(args, registeredFormats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1139,13 +374,13 @@ func ServePage(args []string) error {
|
||||||
|
|
||||||
mux.PanicHandler = serverErrorHandler()
|
mux.PanicHandler = serverErrorHandler()
|
||||||
|
|
||||||
mux.GET("/", serveRoot(paths, regexes, index, supportedFormats))
|
mux.GET("/", serveRoot(paths, regexes, index, registeredFormats))
|
||||||
|
|
||||||
mux.GET("/favicons/*favicon", serveFavicons())
|
mux.GET("/favicons/*favicon", serveFavicons())
|
||||||
|
|
||||||
mux.GET("/favicon.ico", serveFavicons())
|
mux.GET("/favicon.ico", serveFavicons())
|
||||||
|
|
||||||
mux.GET(MediaPrefix+"/*media", serveMedia(paths, regexes, index, supportedFormats))
|
mux.GET(MediaPrefix+"/*media", serveMedia(paths, regexes, index, registeredFormats))
|
||||||
|
|
||||||
mux.GET(SourcePrefix+"/*static", serveStaticFile(paths, stats, index))
|
mux.GET(SourcePrefix+"/*static", serveStaticFile(paths, stats, index))
|
||||||
|
|
||||||
|
@ -1162,10 +397,10 @@ func ServePage(args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !skipIndex {
|
if !skipIndex {
|
||||||
index.generateCache(args, supportedFormats)
|
index.generateCache(args, registeredFormats)
|
||||||
}
|
}
|
||||||
|
|
||||||
mux.GET("/clear_cache", serveCacheClear(args, index, supportedFormats))
|
mux.GET("/clear_cache", serveCacheClear(args, index, registeredFormats))
|
||||||
}
|
}
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
|
|
|
@ -12,13 +12,13 @@ import (
|
||||||
|
|
||||||
func RegisterAudioFormats() *SupportedFormat {
|
func RegisterAudioFormats() *SupportedFormat {
|
||||||
return &SupportedFormat{
|
return &SupportedFormat{
|
||||||
Title: func(queryParams, filePath, mime, fileName string, width, height int) string {
|
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||||
return fmt.Sprintf(`<title>%s</title>`, fileName)
|
return fmt.Sprintf(`<title>%s</title>`, fileName)
|
||||||
},
|
},
|
||||||
Body: func(queryParams, filePath, mime, fileName string, width, height int) string {
|
Body: func(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||||
return fmt.Sprintf(`<a href="/%s"><audio controls autoplay><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the audio tag.</audio></a>`,
|
return fmt.Sprintf(`<a href="/%s"><audio controls autoplay><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the audio tag.</audio></a>`,
|
||||||
queryParams,
|
queryParams,
|
||||||
filePath,
|
fileUri,
|
||||||
mime,
|
mime,
|
||||||
fileName)
|
fileName)
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,30 +5,48 @@ Copyright © 2023 Seednode <seednode@seedno.de>
|
||||||
package formats
|
package formats
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/h2non/filetype"
|
"github.com/h2non/filetype"
|
||||||
_ "golang.org/x/image/bmp"
|
_ "golang.org/x/image/bmp"
|
||||||
_ "golang.org/x/image/webp"
|
_ "golang.org/x/image/webp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Dimensions struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
func RegisterImageFormats() *SupportedFormat {
|
func RegisterImageFormats() *SupportedFormat {
|
||||||
return &SupportedFormat{
|
return &SupportedFormat{
|
||||||
Title: func(queryParams, filePath, mime, fileName string, width, height int) string {
|
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>`,
|
return fmt.Sprintf(`<title>%s (%dx%d)</title>`,
|
||||||
fileName,
|
fileName,
|
||||||
width,
|
dimensions.Width,
|
||||||
height)
|
dimensions.Height)
|
||||||
},
|
},
|
||||||
Body: func(queryParams, filePath, mime, fileName string, width, height int) string {
|
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>`,
|
return fmt.Sprintf(`<a href="/%s"><img src="%s" width="%d" height="%d" type="%s" alt="Roulette selected: %s"></a>`,
|
||||||
queryParams,
|
queryParams,
|
||||||
filePath,
|
fileUri,
|
||||||
width,
|
dimensions.Width,
|
||||||
height,
|
dimensions.Height,
|
||||||
mime,
|
mime,
|
||||||
fileName)
|
fileName)
|
||||||
},
|
},
|
||||||
|
@ -45,3 +63,28 @@ func RegisterImageFormats() *SupportedFormat {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
myImage, _, 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: myImage.Width, Height: myImage.Height}, nil
|
||||||
|
}
|
||||||
|
|
|
@ -13,11 +13,14 @@ import (
|
||||||
"github.com/h2non/filetype"
|
"github.com/h2non/filetype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type FormatFunction func(queryParams, fileUri, filePath, fileName, mime string) string
|
||||||
|
type ValidatorFunction func([]byte) bool
|
||||||
|
|
||||||
type SupportedFormat struct {
|
type SupportedFormat struct {
|
||||||
Title func(queryParams, filePath, mime, fileName string, width, height int) string
|
Title FormatFunction
|
||||||
Body func(queryParams, filePath, mime, fileName string, width, height int) string
|
Body FormatFunction
|
||||||
Extensions []string
|
Extensions []string
|
||||||
validator func([]byte) bool
|
validator ValidatorFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedFormats struct {
|
type SupportedFormats struct {
|
||||||
|
|
|
@ -12,13 +12,13 @@ import (
|
||||||
|
|
||||||
func RegisterVideoFormats() *SupportedFormat {
|
func RegisterVideoFormats() *SupportedFormat {
|
||||||
return &SupportedFormat{
|
return &SupportedFormat{
|
||||||
Title: func(queryParams, filePath, mime, fileName string, width, height int) string {
|
Title: func(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||||
return fmt.Sprintf(`<title>%s</title>`, fileName)
|
return fmt.Sprintf(`<title>%s</title>`, fileName)
|
||||||
},
|
},
|
||||||
Body: func(queryParams, filePath, mime, fileName string, width, height int) string {
|
Body: func(queryParams, fileUri, filePath, fileName, mime string) string {
|
||||||
return fmt.Sprintf(`<a href="/%s"><video controls autoplay><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the video tag.</video></a>`,
|
return fmt.Sprintf(`<a href="/%s"><video controls autoplay><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the video tag.</video></a>`,
|
||||||
queryParams,
|
queryParams,
|
||||||
filePath,
|
fileUri,
|
||||||
mime,
|
mime,
|
||||||
fileName)
|
fileName)
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue