From 3cb2e81ec7fca367da4dbbebb57d44acd43cd341 Mon Sep 17 00:00:00 2001 From: Seednode Date: Tue, 24 Jan 2023 12:03:26 -0600 Subject: [PATCH] Rearranged functions into alphabetical order --- cmd/files.go | 565 ++++++++++++++++++++++---------------------- cmd/version.go | 2 +- cmd/web.go | 622 ++++++++++++++++++++++++------------------------- 3 files changed, 594 insertions(+), 595 deletions(-) diff --git a/cmd/files.go b/cmd/files.go index 749b777..11f00fb 100644 --- a/cmd/files.go +++ b/cmd/files.go @@ -49,6 +49,20 @@ func (f *Files) append(directory, path string) { f.mutex.Unlock() } +type Path struct { + base string + number int + extension string +} + +func (p *Path) increment() { + p.number = p.number + 1 +} + +func (p *Path) decrement() { + p.number = p.number - 1 +} + type ScanStats struct { filesMatched uint64 filesSkipped uint64 @@ -83,78 +97,7 @@ func (s *ScanStats) DirectoriesMatched() uint64 { return atomic.LoadUint64(&s.directoriesMatched) } -type Path struct { - base string - number int - extension string -} - -func (p *Path) increment() { - p.number = p.number + 1 -} - -func (p *Path) decrement() { - p.number = p.number - 1 -} - -func contains(s []string, e string) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} - -func humanReadableSize(bytes int) string { - const unit = 1000 - - if bytes < unit { - return fmt.Sprintf("%d B", bytes) - } - - div, exp := int64(unit), 0 - - for n := bytes / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - - return fmt.Sprintf("%.1f %cB", - 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" { - path = fmt.Sprintf("/%s", filepath.ToSlash(path)) - } - - return path -} - func appendPath(directory, path string, files *Files, stats *ScanStats, shouldCache bool) error { - // If caching, only check image types once, during the initial scan, to speed up future pickFile() calls if shouldCache { image, err := isImage(path) if err != nil { @@ -226,6 +169,151 @@ func appendPaths(path string, files *Files, filters *Filters, stats *ScanStats) return nil } +func cleanFilename(filename string) string { + return filename[:len(filename)-(len(filepath.Ext(filename))+3)] +} + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +func fileExists(path string) (bool, error) { + _, err := os.Stat(path) + switch { + case err == nil: + return true, nil + case errors.Is(err, os.ErrNotExist): + return false, nil + default: + return false, err + } +} + +func fileList(paths []string, filters *Filters, sort string, index *Index) ([]string, bool) { + if Cache && filters.IsEmpty() && !index.IsEmpty() { + return index.Index(), true + } + + var fileList []string + + files := &Files{ + mutex: sync.Mutex{}, + list: make(map[string][]string), + } + + stats := &ScanStats{ + filesMatched: 0, + filesSkipped: 0, + directoriesMatched: 0, + } + + concurrency := &Concurrency{ + DirectoryScans: make(chan int, maxDirectoryScans), + FileScans: make(chan int, maxFileScans), + } + + var wg sync.WaitGroup + + startTime := time.Now() + + for i := 0; i < len(paths); i++ { + wg.Add(1) + concurrency.DirectoryScans <- 1 + + go func(i int) { + defer func() { + <-concurrency.DirectoryScans + wg.Done() + }() + + err := scanPath(paths[i], files, filters, stats, concurrency) + if err != nil { + fmt.Println(err) + } + }(i) + } + + wg.Wait() + + fileList = prepareDirectories(files, sort) + + if Verbose { + fmt.Printf("%s | Indexed %d/%d files across %d directories in %s\n", + time.Now().Format(logDate), + stats.FilesMatched(), + stats.FilesTotal(), + stats.DirectoriesMatched(), + time.Since(startTime), + ) + } + + if Cache && filters.IsEmpty() { + index.setIndex(fileList) + } + + return fileList, false +} + +func humanReadableSize(bytes int) string { + const unit = 1000 + + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + + div, exp := int64(unit), 0 + + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + + return fmt.Sprintf("%.1f %cB", + 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 isImage(path string) (bool, error) { + file, err := os.Open(path) + switch { + case errors.Is(err, os.ErrNotExist): + return false, nil + case err != nil: + return false, err + } + defer file.Close() + + head := make([]byte, 261) + file.Read(head) + + return filetype.IsImage(head), nil +} + func newFile(paths []string, filters *Filters, sortOrder string, regexes *Regexes, index *Index) (string, error) { filePath, err := pickFile(paths, filters, sortOrder, index) if err != nil { @@ -293,58 +381,34 @@ func nextFile(filePath, sortOrder string, regexes *Regexes) (string, error) { return fileName, err } -func splitPath(path string, regexes *Regexes) (*Path, error) { - p := Path{} - var err error +func normalizePaths(args []string) ([]string, error) { + var paths []string - split := regexes.filename.FindAllStringSubmatch(path, -1) + fmt.Println("Paths:") - if len(split) < 1 || len(split[0]) < 3 { - return &Path{}, nil - } - - p.base = split[0][1] - - p.number, err = strconv.Atoi(split[0][2]) - - if err != nil { - return &Path{}, err - } - - p.extension = split[0][3] - - return &p, nil -} - -func tryExtensions(p *Path) (string, error) { - var fileName string - - for _, extension := range extensions { - fileName = fmt.Sprintf("%s%.3d%s", p.base, p.number, extension) - - exists, err := fileExists(fileName) + for i := 0; i < len(args); i++ { + path, err := filepath.EvalSymlinks(args[i]) if err != nil { - return "", err + return nil, err } - if exists { - return fileName, nil + absolutePath, err := filepath.Abs(path) + if err != nil { + return nil, err } + + if (args[i]) != absolutePath { + fmt.Printf("%s (resolved to %s)\n", args[i], absolutePath) + } else { + fmt.Printf("%s\n", args[i]) + } + + paths = append(paths, absolutePath) } - return "", nil -} + fmt.Println() -func fileExists(path string) (bool, error) { - _, err := os.Stat(path) - switch { - case err == nil: - return true, nil - case errors.Is(err, os.ErrNotExist): - return false, nil - default: - return false, err - } + return paths, nil } func pathIsValid(filePath string, paths []string) bool { @@ -371,23 +435,91 @@ func pathIsValid(filePath string, paths []string) bool { } } -func isImage(path string) (bool, error) { - file, err := os.Open(path) - switch { - case errors.Is(err, os.ErrNotExist): - return false, nil - case err != nil: - return false, err +func pickFile(args []string, filters *Filters, sortOrder string, index *Index) (string, error) { + fileList, fromCache := fileList(args, filters, sortOrder, index) + + fileCount := len(fileList) + if fileCount == 0 { + return "", errNoImagesFound } - defer file.Close() - head := make([]byte, 261) - file.Read(head) + r := rand.Intn(fileCount - 1) - return filetype.IsImage(head), nil + for i := 0; i < fileCount; i++ { + if r >= (fileCount - 1) { + r = 0 + } else { + r++ + } + + filePath := fileList[r] + + if !fromCache { + isImage, err := isImage(filePath) + if err != nil { + return "", err + } + + if isImage { + return filePath, nil + } + + continue + } + + return filePath, nil + } + + return "", errNoImagesFound } -func getFiles(path string, files *Files, filters *Filters, stats *ScanStats, concurrency *Concurrency) error { +func prepareDirectories(files *Files, sort string) []string { + directories := []string{} + + keys := make([]string, len(files.list)) + + i := 0 + for k := range files.list { + keys[i] = k + i++ + } + + if sort == "asc" || sort == "desc" { + for i := 0; i < len(keys); i++ { + directories = append(directories, prepareDirectory(files.list[keys[i]])...) + } + } else { + for i := 0; i < len(keys); i++ { + directories = append(directories, files.list[keys[i]]...) + } + } + + return directories +} + +func prepareDirectory(directory []string) []string { + _, first := filepath.Split(directory[0]) + first = cleanFilename(first) + + _, last := filepath.Split(directory[len(directory)-1]) + last = cleanFilename(last) + + if first == last { + return append([]string{}, directory[0]) + } else { + return directory + } +} + +func preparePath(path string) string { + if runtime.GOOS == "windows" { + path = fmt.Sprintf("/%s", filepath.ToSlash(path)) + } + + return path +} + +func scanPath(path string, files *Files, filters *Filters, stats *ScanStats, concurrency *Concurrency) error { var wg sync.WaitGroup err := filepath.WalkDir(path, func(p string, info os.DirEntry, err error) error { @@ -429,177 +561,44 @@ func getFiles(path string, files *Files, filters *Filters, stats *ScanStats, con return nil } -func fileList(paths []string, filters *Filters, sort string, index *Index) ([]string, bool) { - if Cache && filters.IsEmpty() && !index.IsEmpty() { - return index.Index(), true +func splitPath(path string, regexes *Regexes) (*Path, error) { + p := Path{} + var err error + + split := regexes.filename.FindAllStringSubmatch(path, -1) + + if len(split) < 1 || len(split[0]) < 3 { + return &Path{}, nil } - var fileList []string + p.base = split[0][1] - files := &Files{ - mutex: sync.Mutex{}, - list: make(map[string][]string), + p.number, err = strconv.Atoi(split[0][2]) + + if err != nil { + return &Path{}, err } - stats := &ScanStats{ - filesMatched: 0, - filesSkipped: 0, - directoriesMatched: 0, - } + p.extension = split[0][3] - concurrency := &Concurrency{ - DirectoryScans: make(chan int, maxDirectoryScans), - FileScans: make(chan int, maxFileScans), - } - - var wg sync.WaitGroup - - startTime := time.Now() - - for i := 0; i < len(paths); i++ { - wg.Add(1) - concurrency.DirectoryScans <- 1 - - go func(i int) { - defer func() { - <-concurrency.DirectoryScans - wg.Done() - }() - - err := getFiles(paths[i], files, filters, stats, concurrency) - if err != nil { - fmt.Println(err) - } - }(i) - } - - wg.Wait() - - fileList = prepareDirectories(files, sort) - - if Verbose { - fmt.Printf("%s | Indexed %d/%d files across %d directories in %s\n", - time.Now().Format(logDate), - stats.FilesMatched(), - stats.FilesTotal(), - stats.DirectoriesMatched(), - time.Since(startTime), - ) - } - - if Cache && filters.IsEmpty() { - index.setIndex(fileList) - } - - return fileList, false + return &p, nil } -func cleanFilename(filename string) string { - return filename[:len(filename)-(len(filepath.Ext(filename))+3)] -} +func tryExtensions(p *Path) (string, error) { + var fileName string -func prepareDirectory(directory []string) []string { - _, first := filepath.Split(directory[0]) - first = cleanFilename(first) + for _, extension := range extensions { + fileName = fmt.Sprintf("%s%.3d%s", p.base, p.number, extension) - _, last := filepath.Split(directory[len(directory)-1]) - last = cleanFilename(last) - - if first == last { - return append([]string{}, directory[0]) - } else { - return directory - } -} - -func prepareDirectories(files *Files, sort string) []string { - directories := []string{} - - keys := make([]string, len(files.list)) - - i := 0 - for k := range files.list { - keys[i] = k - i++ - } - - if sort == "asc" || sort == "desc" { - for i := 0; i < len(keys); i++ { - directories = append(directories, prepareDirectory(files.list[keys[i]])...) - } - } else { - for i := 0; i < len(keys); i++ { - directories = append(directories, files.list[keys[i]]...) - } - } - - return directories -} - -func pickFile(args []string, filters *Filters, sort string, index *Index) (string, error) { - fileList, fromCache := fileList(args, filters, sort, index) - - fileCount := len(fileList) - if fileCount == 0 { - return "", errNoImagesFound - } - - r := rand.Intn(fileCount - 1) - - for i := 0; i < fileCount; i++ { - if r >= (fileCount - 1) { - r = 0 - } else { - r++ - } - - filePath := fileList[r] - - if !fromCache { - isImage, err := isImage(filePath) - if err != nil { - return "", err - } - - if isImage { - return filePath, nil - } - - continue - } - - return filePath, nil - } - - return "", errNoImagesFound -} - -func normalizePaths(args []string) ([]string, error) { - var paths []string - - fmt.Println("Paths:") - - for i := 0; i < len(args); i++ { - path, err := filepath.EvalSymlinks(args[i]) + exists, err := fileExists(fileName) if err != nil { - return nil, err + return "", err } - absolutePath, err := filepath.Abs(path) - if err != nil { - return nil, err + if exists { + return fileName, nil } - - if (args[i]) != absolutePath { - fmt.Printf("%s (resolved to %s)\n", args[i], absolutePath) - } else { - fmt.Printf("%s\n", args[i]) - } - - paths = append(paths, absolutePath) } - fmt.Println() - - return paths, nil + return "", nil } diff --git a/cmd/version.go b/cmd/version.go index 5eb1301..23cf362 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra" ) -var Version = "0.34.1" +var Version = "0.35.0" func init() { rootCmd.AddCommand(versionCmd) diff --git a/cmd/web.go b/cmd/web.go index 7bf33b9..5f3d8f8 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -30,12 +30,6 @@ const ( redirectStatusCode int = http.StatusSeeOther ) -type Regexes struct { - alphanumeric *regexp.Regexp - filename *regexp.Regexp - units *regexp.Regexp -} - type Filters struct { includes []string excludes []string @@ -96,6 +90,12 @@ func (i *Index) IsEmpty() bool { return length == 0 } +type Regexes struct { + alphanumeric *regexp.Regexp + filename *regexp.Regexp + units *regexp.Regexp +} + type ServeStats struct { mutex sync.RWMutex list []string @@ -155,278 +155,6 @@ type timesServed struct { times []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(404) - w.Header().Add("Content-Type", "text/html") - - var htmlBody strings.Builder - htmlBody.WriteString(``) - htmlBody.WriteString(``) - htmlBody.WriteString(`Not Found`) - htmlBody.WriteString(`404 page not found`) - - _, err := io.WriteString(w, gohtml.Format(htmlBody.String())) - if err != nil { - return err - } - - return nil -} - -func refreshInterval(r *http.Request, regexes *Regexes) (int64, string) { - refreshInterval := r.URL.Query().Get("refresh") - - if !regexes.units.MatchString(refreshInterval) { - return 0, "0ms" - } - - duration, err := time.ParseDuration(refreshInterval) - if err != nil { - return 0, "0ms" - } - - return duration.Milliseconds(), refreshInterval -} - -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 Filter { - queryParams.WriteString("include=") - if filters.HasIncludes() { - queryParams.WriteString(filters.Includes()) - } - - queryParams.WriteString("&exclude=") - if filters.HasExcludes() { - queryParams.WriteString(filters.Excludes()) - } - - hasParams = true - } - - if Sort { - 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(prefix) - 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 html(w http.ResponseWriter, r *http.Request, filePath string, dimensions *Dimensions, filters *Filters, regexes *Regexes) error { - fileName := filepath.Base(filePath) - - w.Header().Add("Content-Type", "text/html") - - sortOrder := sortOrder(r) - - refreshTimer, refreshInterval := refreshInterval(r, regexes) - - queryParams := generateQueryParams(filters, sortOrder, refreshInterval) - - var htmlBody strings.Builder - htmlBody.WriteString(``) - htmlBody.WriteString(``) - htmlBody.WriteString(fmt.Sprintf(`%s (%dx%d)`, - fileName, - dimensions.width, - dimensions.height)) - htmlBody.WriteString(``) - if refreshInterval != "0ms" { - htmlBody.WriteString(fmt.Sprintf("", - queryParams, - refreshTimer)) - } - htmlBody.WriteString(fmt.Sprintf(`Roulette selected: %s`, - queryParams, - generateFilePath(filePath), - dimensions.width, - dimensions.height, - fileName)) - htmlBody.WriteString(``) - - _, err := io.WriteString(w, gohtml.Format(htmlBody.String())) - if err != nil { - return err - } - - return nil -} - -func staticFile(w http.ResponseWriter, r *http.Request, paths []string, stats *ServeStats) error { - prefixedFilePath, err := stripQueryParams(r.URL.Path) - if err != nil { - return err - } - - filePath, err := filepath.EvalSymlinks(strings.TrimPrefix(prefixedFilePath, prefix)) - if err != nil { - return err - } - - if !pathIsValid(filePath, paths) { - notFound(w, r, filePath) - - return nil - } - - exists, err := fileExists(filePath) - if err != nil { - return err - } - - if !exists { - notFound(w, r, filePath) - - return nil - } - - startTime := time.Now() - - buf, err := os.ReadFile(filePath) - if err != nil { - return err - } - - w.Write(buf) - - fileSize := humanReadableSize(len(buf)) - - if Verbose { - fmt.Printf("%s | Served %s (%s) to %s in %s\n", - startTime.Format(logDate), - filePath, - fileSize, - realIP(r), - time.Since(startTime).Round(time.Microsecond), - ) - } - - if Debug { - stats.incrementCounter(filePath, startTime, fileSize) - } - - return nil -} - func cacheHandler(args []string, index *Index) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { index.generateCache(args) @@ -436,38 +164,7 @@ func cacheHandler(args []string, index *Index) http.HandlerFunc { } } -func statsHandler(args []string, stats *ServeStats) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - startTime := time.Now() - - response, err := stats.ListImages() - if err != nil { - fmt.Println(err) - } - - 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), - ) - } - } -} - -func staticFileHandler(paths []string, stats *ServeStats) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - err := staticFile(w, r, paths, stats) - if err != nil { - fmt.Println(err) - } - } -} +func doNothing(http.ResponseWriter, *http.Request) {} func htmlHandler(paths []string, regexes *Regexes, index *Index) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -556,7 +253,48 @@ func htmlHandler(paths []string, regexes *Regexes, index *Index) http.HandlerFun } } -func doNothing(http.ResponseWriter, *http.Request) {} +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(404) + w.Header().Add("Content-Type", "text/html") + + var htmlBody strings.Builder + htmlBody.WriteString(``) + htmlBody.WriteString(``) + htmlBody.WriteString(`Not Found`) + htmlBody.WriteString(`404 page not found`) + + _, err := io.WriteString(w, gohtml.Format(htmlBody.String())) + if err != nil { + return err + } + + return nil +} + +func refreshInterval(r *http.Request, regexes *Regexes) (int64, string) { + refreshInterval := r.URL.Query().Get("refresh") + + if !regexes.units.MatchString(refreshInterval) { + return 0, "0ms" + } + + duration, err := time.ParseDuration(refreshInterval) + if err != nil { + return 0, "0ms" + } + + return duration.Milliseconds(), refreshInterval +} func ServePage(args []string) error { fmt.Printf("roulette v%s\n\n", Version) @@ -608,3 +346,265 @@ func ServePage(args []string) error { return nil } + +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 generateFilePath(filePath string) string { + var htmlBody strings.Builder + + htmlBody.WriteString(prefix) + if runtime.GOOS == "windows" { + htmlBody.WriteString(`/`) + } + htmlBody.WriteString(filePath) + + return htmlBody.String() +} + +func generateQueryParams(filters *Filters, sortOrder, refreshInterval string) string { + var hasParams bool + + var queryParams strings.Builder + + queryParams.WriteString("?") + + if Filter { + queryParams.WriteString("include=") + if filters.HasIncludes() { + queryParams.WriteString(filters.Includes()) + } + + queryParams.WriteString("&exclude=") + if filters.HasExcludes() { + queryParams.WriteString(filters.Excludes()) + } + + hasParams = true + } + + if Sort { + 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 html(w http.ResponseWriter, r *http.Request, filePath string, dimensions *Dimensions, filters *Filters, regexes *Regexes) error { + fileName := filepath.Base(filePath) + + w.Header().Add("Content-Type", "text/html") + + sortOrder := sortOrder(r) + + refreshTimer, refreshInterval := refreshInterval(r, regexes) + + queryParams := generateQueryParams(filters, sortOrder, refreshInterval) + + var htmlBody strings.Builder + htmlBody.WriteString(``) + htmlBody.WriteString(``) + htmlBody.WriteString(fmt.Sprintf(`%s (%dx%d)`, + fileName, + dimensions.width, + dimensions.height)) + htmlBody.WriteString(``) + if refreshInterval != "0ms" { + htmlBody.WriteString(fmt.Sprintf("", + queryParams, + refreshTimer)) + } + htmlBody.WriteString(fmt.Sprintf(`Roulette selected: %s`, + queryParams, + generateFilePath(filePath), + dimensions.width, + dimensions.height, + fileName)) + htmlBody.WriteString(``) + + _, err := io.WriteString(w, gohtml.Format(htmlBody.String())) + if err != nil { + return err + } + + return nil +} + +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 refererToUri(referer string) string { + parts := strings.SplitAfterN(referer, "/", 4) + + if len(parts) < 4 { + return "" + } + + return "/" + parts[3] +} + +func staticFile(w http.ResponseWriter, r *http.Request, paths []string, stats *ServeStats) error { + prefixedFilePath, err := stripQueryParams(r.URL.Path) + if err != nil { + return err + } + + filePath, err := filepath.EvalSymlinks(strings.TrimPrefix(prefixedFilePath, prefix)) + if err != nil { + return err + } + + if !pathIsValid(filePath, paths) { + notFound(w, r, filePath) + + return nil + } + + exists, err := fileExists(filePath) + if err != nil { + return err + } + + if !exists { + notFound(w, r, filePath) + + return nil + } + + startTime := time.Now() + + buf, err := os.ReadFile(filePath) + if err != nil { + return err + } + + w.Write(buf) + + fileSize := humanReadableSize(len(buf)) + + if Verbose { + fmt.Printf("%s | Served %s (%s) to %s in %s\n", + startTime.Format(logDate), + filePath, + fileSize, + realIP(r), + time.Since(startTime).Round(time.Microsecond), + ) + } + + if Debug { + stats.incrementCounter(filePath, startTime, fileSize) + } + + return nil +} + +func staticFileHandler(paths []string, stats *ServeStats) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + err := staticFile(w, r, paths, stats) + if err != nil { + fmt.Println(err) + } + } +} + +func statsHandler(args []string, stats *ServeStats) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + startTime := time.Now() + + response, err := stats.ListImages() + if err != nil { + fmt.Println(err) + } + + 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), + ) + } + } +} + +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 +}