diff --git a/cmd/debug.go b/cmd/debug.go new file mode 100644 index 0000000..25d9d8f --- /dev/null +++ b/cmd/debug.go @@ -0,0 +1,161 @@ +/* +Copyright © 2023 Seednode +*/ + +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(``) + htmlBody.WriteString(FaviconHtml) + htmlBody.WriteString(``) + htmlBody.WriteString(fmt.Sprintf("Index contains %d files", fileCount)) + if len(indexDump) > 0 { + for _, v := range indexDump[startIndex:stopIndex] { + var shouldSort = "" + + if sorting { + shouldSort = "?sort=asc" + } + htmlBody.WriteString(fmt.Sprintf("\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("", prevPage)) + htmlBody.WriteString(fmt.Sprintf("", nextPage)) + } + } + + htmlBody.WriteString(`
%s
`) + + 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), + ) + } + } +} diff --git a/cmd/errors.go b/cmd/errors.go new file mode 100644 index 0000000..c1faa30 --- /dev/null +++ b/cmd/errors.go @@ -0,0 +1,70 @@ +/* +Copyright © 2023 Seednode +*/ + +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(``) + htmlBody.WriteString(FaviconHtml) + htmlBody.WriteString(``) + htmlBody.WriteString(fmt.Sprintf("%s", title)) + htmlBody.WriteString(fmt.Sprintf("%s", 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 +} diff --git a/cmd/favicons.go b/cmd/favicons.go new file mode 100644 index 0000000..4d54056 --- /dev/null +++ b/cmd/favicons.go @@ -0,0 +1,43 @@ +/* +Copyright © 2023 Seednode +*/ + +package cmd + +import ( + "bytes" + "embed" + "net/http" + "strconv" + "strings" + + "github.com/julienschmidt/httprouter" +) + +//go:embed favicons/* +var favicons embed.FS + +const ( + FaviconHtml string = ` + + + + + + ` +) + +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) + } +} diff --git a/cmd/files.go b/cmd/files.go index 80b414c..4291eae 100644 --- a/cmd/files.go +++ b/cmd/files.go @@ -7,10 +7,6 @@ package cmd import ( "errors" "fmt" - "image" - _ "image/gif" - _ "image/jpeg" - _ "image/png" "math/big" "crypto/rand" @@ -23,8 +19,6 @@ import ( "sync/atomic" "time" - _ "golang.org/x/image/bmp" - _ "golang.org/x/image/webp" "seedno.de/seednode/roulette/formats" ) @@ -43,27 +37,8 @@ type Concurrency struct { var ( 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 { mutex sync.RWMutex list map[string][]string @@ -123,27 +98,6 @@ func humanReadableSize(bytes int) string { 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 { if runtime.GOOS == "windows" { 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 { if shouldCache { - supported, _, _, err := formats.FileType(path, registeredFormats) + registered, _, _, err := formats.FileType(path, registeredFormats) if err != nil { return err } - if !supported { + if !registered { return nil } } @@ -224,8 +178,8 @@ func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats, return nil } -func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexes, index *Index, types *formats.SupportedFormats) (string, error) { - filePath, err := pickFile(paths, filters, sortOrder, index, types) +func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexes, index *Index, registeredFormats *formats.SupportedFormats) (string, error) { + filePath, err := pickFile(paths, filters, sortOrder, index, registeredFormats) if err != nil { return "", nil } @@ -239,7 +193,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe switch { case sortOrder == "asc": - filePath, err = tryExtensions(path) + filePath, err = tryExtensions(path, registeredFormats) if err != nil { return "", err } @@ -247,7 +201,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe for { path.increment() - filePath, err = tryExtensions(path) + filePath, err = tryExtensions(path, registeredFormats) if err != nil { return "", err } @@ -255,7 +209,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe if filePath == "" { path.decrement() - filePath, err = tryExtensions(path) + filePath, err = tryExtensions(path, registeredFormats) if err != nil { return "", err } @@ -268,7 +222,7 @@ func newFile(paths []string, filters *Filters, sortOrder string, Regexes *Regexe 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) if err != nil { return "", err @@ -283,7 +237,7 @@ func nextFile(filePath, sortOrder string, Regexes *Regexes) (string, error) { return "", nil } - fileName, err := tryExtensions(path) + fileName, err := tryExtensions(path, registeredFormats) if err != nil { return "", err } @@ -313,10 +267,10 @@ func splitPath(path string, Regexes *Regexes) (*Path, error) { return &p, nil } -func tryExtensions(p *Path) (string, error) { +func tryExtensions(p *Path, registeredFormats *formats.SupportedFormats) (string, error) { var fileName string - for _, extension := range Extensions { + for _, extension := range registeredFormats.Extensions() { fileName = fmt.Sprintf("%s%.3d%s", p.base, p.number, extension) exists, err := fileExists(fileName) @@ -368,8 +322,8 @@ func pathIsValid(filePath string, paths []string) bool { } } -func pathHasSupportedFiles(path string, types *formats.SupportedFormats) (bool, error) { - hasSupportedFiles := make(chan bool, 1) +func pathHasSupportedFiles(path string, registeredFormats *formats.SupportedFormats) (bool, error) { + hasRegisteredFiles := make(chan bool, 1) err := filepath.WalkDir(path, func(p string, info os.DirEntry, err error) error { if err != nil { @@ -380,13 +334,13 @@ func pathHasSupportedFiles(path string, types *formats.SupportedFormats) (bool, case !recursive && info.IsDir() && p != path: return filepath.SkipDir case !info.IsDir(): - supported, _, _, err := formats.FileType(p, types) + registered, _, _, err := formats.FileType(p, registeredFormats) if err != nil { return err } - if supported { - hasSupportedFiles <- true + if registered { + hasRegisteredFiles <- true return filepath.SkipAll } } @@ -398,7 +352,7 @@ func pathHasSupportedFiles(path string, types *formats.SupportedFormats) (bool, } select { - case <-hasSupportedFiles: + case <-hasRegisteredFiles: return true, nil default: return false, nil @@ -628,12 +582,12 @@ func pickFile(args []string, filters *Filters, sort string, index *Index, regist filePath := fileList[val] if !fromCache { - supported, _, _, err := formats.FileType(filePath, registeredFormats) + registered, _, _, err := formats.FileType(filePath, registeredFormats) if err != nil { return "", err } - if supported { + if registered { return filePath, nil } diff --git a/cmd/filters.go b/cmd/filters.go new file mode 100644 index 0000000..ea2c55d --- /dev/null +++ b/cmd/filters.go @@ -0,0 +1,32 @@ +/* +Copyright © 2023 Seednode +*/ + +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, ",") +} diff --git a/cmd/index.go b/cmd/index.go new file mode 100644 index 0000000..d57c3f0 --- /dev/null +++ b/cmd/index.go @@ -0,0 +1,141 @@ +/* +Copyright © 2023 Seednode +*/ + +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")) + } +} diff --git a/cmd/root.go b/cmd/root.go index d6fce15..5022380 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,7 +17,7 @@ var ( ) const ( - Version string = "0.64.0" + Version string = "0.65.0" ) var ( diff --git a/cmd/stats.go b/cmd/stats.go new file mode 100644 index 0000000..4f48bdd --- /dev/null +++ b/cmd/stats.go @@ -0,0 +1,247 @@ +/* +Copyright © 2023 Seednode +*/ + +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) + } + } +} diff --git a/cmd/uri.go b/cmd/uri.go new file mode 100644 index 0000000..e51b5e0 --- /dev/null +++ b/cmd/uri.go @@ -0,0 +1,165 @@ +/* +Copyright © 2023 Seednode +*/ + +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 + } +} diff --git a/cmd/web.go b/cmd/web.go index 21064a6..6e443b3 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -6,21 +6,16 @@ package cmd import ( "bytes" - "embed" - "encoding/gob" - "encoding/json" "errors" "fmt" "io" "net" "net/http" - "net/url" "os" "os/signal" "path/filepath" "regexp" "runtime" - "sort" "strconv" "strings" "sync" @@ -30,28 +25,16 @@ import ( "net/http/pprof" "github.com/julienschmidt/httprouter" - "github.com/klauspost/compress/zstd" "github.com/yosssi/gohtml" "seedno.de/seednode/roulette/formats" ) -//go:embed favicons/* -var favicons embed.FS - const ( LogDate string = `2006-01-02T15:04:05.000-07:00` SourcePrefix string = `/source` MediaPrefix string = `/view` RedirectStatusCode int = http.StatusSeeOther Timeout time.Duration = 10 * time.Second - - FaviconHtml string = ` - - - - - - ` ) type Regexes struct { @@ -59,730 +42,6 @@ type Regexes struct { 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(``) - htmlBody.WriteString(FaviconHtml) - htmlBody.WriteString(``) - htmlBody.WriteString(fmt.Sprintf("%s", title)) - htmlBody.WriteString(fmt.Sprintf("%s", 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(``) - htmlBody.WriteString(FaviconHtml) - htmlBody.WriteString(``) - htmlBody.WriteString(fmt.Sprintf("Index contains %d files", fileCount)) - if len(indexDump) > 0 { - for _, v := range indexDump[startIndex:stopIndex] { - var shouldSort = "" - - if sorting { - shouldSort = "?sort=asc" - } - htmlBody.WriteString(fmt.Sprintf("\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("", prevPage)) - htmlBody.WriteString(fmt.Sprintf("", nextPage)) - } - } - - htmlBody.WriteString(`
%s
`) - - 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 { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 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) { refererUri, err := stripQueryParams(refererToUri(r.Referer())) if err != nil { @@ -898,7 +157,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *Index, supportedFormats var filePath string if refererUri != "" { - filePath, err = nextFile(strippedRefererUri, sortOrder, Regexes) + filePath, err = nextFile(strippedRefererUri, sortOrder, Regexes, registeredFormats) if err != nil { fmt.Println(err) @@ -920,7 +179,7 @@ func serveRoot(paths []string, Regexes *Regexes, index *Index, supportedFormats break loop } - filePath, err = newFile(paths, filters, sortOrder, Regexes, index, supportedFormats) + filePath, err = newFile(paths, filters, sortOrder, Regexes, index, registeredFormats) switch { case err != nil && err == ErrNoMediaFound: 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) { filters := &Filters{ includes: splitQueryParams(r.URL.Query().Get("include"), Regexes), @@ -955,13 +214,13 @@ func serveMedia(paths []string, Regexes *Regexes, index *Index, supportedFormats sortOrder := SortOrder(r) - filePath := strings.TrimPrefix(r.URL.Path, MediaPrefix) + path := strings.TrimPrefix(r.URL.Path, MediaPrefix) if runtime.GOOS == "windows" { - filePath = strings.TrimPrefix(filePath, "/") + path = strings.TrimPrefix(path, "/") } - exists, err := fileExists(filePath) + exists, err := fileExists(path) if err != nil { fmt.Println(err) @@ -970,12 +229,12 @@ func serveMedia(paths []string, Regexes *Regexes, index *Index, supportedFormats return } if !exists { - notFound(w, r, filePath) + notFound(w, r, path) return } - supported, fileType, mime, err := formats.FileType(filePath, supportedFormats) + registered, fileType, mime, err := formats.FileType(path, registeredFormats) if err != nil { fmt.Println(err) @@ -984,22 +243,15 @@ func serveMedia(paths []string, Regexes *Regexes, index *Index, supportedFormats return } - if !supported { - notFound(w, r, filePath) + if !registered { + notFound(w, r, path) return } - dimensions, err := imageDimensions(filePath) - if err != nil { - fmt.Println(err) + fileUri := generateFileUri(path) - serverError(w, r, nil) - - return - } - - fileName := filepath.Base(filePath) + fileName := filepath.Base(path) 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) - path := generateFilePath(filePath) - var htmlBody strings.Builder htmlBody.WriteString(``) 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(`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.Title(queryParams, path, mime, fileName, dimensions.height, dimensions.width))) + htmlBody.WriteString((fileType.Title(queryParams, fileUri, path, fileName, mime))) htmlBody.WriteString(``) if refreshInterval != "0ms" { htmlBody.WriteString(fmt.Sprintf("", queryParams, refreshTimer)) } - htmlBody.WriteString((fileType.Body(queryParams, path, mime, fileName, dimensions.height, dimensions.width))) + htmlBody.WriteString((fileType.Body(queryParams, fileUri, path, fileName, mime))) htmlBody.WriteString(``) _, 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 { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 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") } - supportedFormats := &formats.SupportedFormats{} + registeredFormats := &formats.SupportedFormats{} if audio { - supportedFormats.Add(formats.RegisterAudioFormats()) + registeredFormats.Add(formats.RegisterAudioFormats()) } if images { - supportedFormats.Add(formats.RegisterImageFormats()) + registeredFormats.Add(formats.RegisterImageFormats()) } if videos { - supportedFormats.Add(formats.RegisterVideoFormats()) + registeredFormats.Add(formats.RegisterVideoFormats()) } - paths, err := normalizePaths(args, supportedFormats) + paths, err := normalizePaths(args, registeredFormats) if err != nil { return err } @@ -1139,13 +374,13 @@ func ServePage(args []string) error { mux.PanicHandler = serverErrorHandler() - mux.GET("/", serveRoot(paths, regexes, index, supportedFormats)) + mux.GET("/", serveRoot(paths, regexes, index, registeredFormats)) mux.GET("/favicons/*favicon", 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)) @@ -1162,10 +397,10 @@ func ServePage(args []string) error { } 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 { diff --git a/formats/audio.go b/formats/audio.go index 23a8d90..a2ecd31 100644 --- a/formats/audio.go +++ b/formats/audio.go @@ -12,13 +12,13 @@ import ( func RegisterAudioFormats() *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(`%s`, fileName) }, - Body: func(queryParams, filePath, mime, fileName string, width, height int) string { + Body: func(queryParams, fileUri, filePath, fileName, mime string) string { return fmt.Sprintf(``, queryParams, - filePath, + fileUri, mime, fileName) }, diff --git a/formats/images.go b/formats/images.go index f3491a2..dbe3a66 100644 --- a/formats/images.go +++ b/formats/images.go @@ -5,30 +5,48 @@ Copyright © 2023 Seednode package formats import ( + "errors" "fmt" + "image" _ "image/gif" _ "image/jpeg" _ "image/png" + "os" "github.com/h2non/filetype" _ "golang.org/x/image/bmp" _ "golang.org/x/image/webp" ) +type Dimensions struct { + Width int + Height int +} + func RegisterImageFormats() *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(`%s (%dx%d)`, fileName, - width, - height) + dimensions.Width, + 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(`Roulette selected: %s`, queryParams, - filePath, - width, - height, + fileUri, + dimensions.Width, + dimensions.Height, mime, 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 +} diff --git a/formats/types.go b/formats/types.go index b8b0d6a..aa21f74 100644 --- a/formats/types.go +++ b/formats/types.go @@ -13,11 +13,14 @@ import ( "github.com/h2non/filetype" ) +type FormatFunction func(queryParams, fileUri, filePath, fileName, mime string) string +type ValidatorFunction func([]byte) bool + type SupportedFormat struct { - Title func(queryParams, filePath, mime, fileName string, width, height int) string - Body func(queryParams, filePath, mime, fileName string, width, height int) string + Title FormatFunction + Body FormatFunction Extensions []string - validator func([]byte) bool + validator ValidatorFunction } type SupportedFormats struct { diff --git a/formats/video.go b/formats/video.go index acea30d..330d54f 100644 --- a/formats/video.go +++ b/formats/video.go @@ -12,13 +12,13 @@ import ( func RegisterVideoFormats() *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(`%s`, fileName) }, - Body: func(queryParams, filePath, mime, fileName string, width, height int) string { + Body: func(queryParams, fileUri, filePath, fileName, mime string) string { return fmt.Sprintf(``, queryParams, - filePath, + fileUri, mime, fileName) },