diff --git a/go.mod b/go.mod index c238809..61287af 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module leech go 1.22.2 require ( + github.com/disintegration/imaging v1.6.2 github.com/dustin/go-humanize v1.0.1 github.com/gofiber/fiber/v2 v2.52.4 ) @@ -18,5 +19,6 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect golang.org/x/sys v0.15.0 // indirect ) diff --git a/go.sum b/go.sum index 9b33019..b3e21f3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM= @@ -23,7 +25,10 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/html/html.go b/html/html.go index 37ea297..bd8ff36 100644 --- a/html/html.go +++ b/html/html.go @@ -2,6 +2,7 @@ package html import ( "fmt" + "leech/thumbnail" "strings" "github.com/dustin/go-humanize" @@ -106,10 +107,7 @@ func FileListPage(req string, entries []Entry) string { if dirEntry.Size != -1 { formattedSize = humanize.Bytes(uint64(dirEntry.Size)) } - icon := "/assets/images/fileicon.png" - if dirEntry.IsDir { - icon = "/assets/images/diricon.png" - } + link := "/" + req + "/" + dirEntry.Name if dirEntry.Name == ".." { splitReq := strings.Split(req, "/") @@ -121,6 +119,14 @@ func FileListPage(req string, entries []Entry) string { if !dirEntry.IsDir { link = "/serve/" + req + "/" + dirEntry.Name } + icon := "/thumb/" + req + "/" + dirEntry.Name + + if !thumbnail.IsSupportedFileType(dirEntry.Name) { + icon = "/assets/images/fileicon.png" + } + if dirEntry.IsDir { + icon = "/assets/images/diricon.png" + } body += fmt.Sprintf(`
%s
%s
`, link, icon, dirEntry.Name, formattedSize) } return header + body + footer diff --git a/main.go b/main.go index 44079af..81cc596 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,8 @@ func main() { app.Static("/serve/"+dirName, dirToServe) } + app.Use("/thumb", route.HandleThumb) + app.Use("/", route.HandleList) app.Listen(config.Config.Host) diff --git a/route/route.go b/route/route.go index 8526716..ec5014e 100644 --- a/route/route.go +++ b/route/route.go @@ -1,9 +1,10 @@ package route import ( - "fmt" + "errors" "leech/config" "leech/html" + "leech/thumbnail" "net/url" "os" "path" @@ -38,7 +39,6 @@ func RecursivelyGetSize(completePath string) (int64, error) { func HandleList(c *fiber.Ctx) error { encodedReq := c.Path()[1:] - fmt.Println(encodedReq) req, err := url.QueryUnescape(encodedReq) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) @@ -68,7 +68,6 @@ func HandleList(c *fiber.Ctx) error { } else { pathSlice := strings.Split(req, "/") pathBase, ok := config.Config.ServeDirs[pathSlice[0]] - fmt.Println(pathSlice[0]) if ok { pathSlice[0] = pathBase completePath := path.Join(pathSlice...) @@ -101,3 +100,35 @@ func HandleList(c *fiber.Ctx) error { } } } + +func HandleThumb(c *fiber.Ctx) error { + encodedReq := c.Path()[1:] + req, err := url.QueryUnescape(encodedReq) + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) + } + c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) + pathSlice := strings.Split(req, "/") + pathSlice = pathSlice[1:len(pathSlice)] + pathBase, ok := config.Config.ServeDirs[pathSlice[0]] + if ok { + pathSlice[0] = pathBase + completePath := path.Join(pathSlice...) + if file_info, err := os.Stat(completePath); err == nil { + if file_info.IsDir() { + return c.Status(fiber.StatusUnauthorized).SendString("Sorry that's a directory!") + } + thumbnail.GetThumbnail(c, completePath) + } else if errors.Is(err, os.ErrNotExist) { + return c.Status(fiber.StatusNotFound).SendString("Sorry can't find that!") + } else { + // Schrodinger: file may or may not exist. See err for details. + + // Therefore, do *NOT* use !os.IsNotExist(err) to test for file existence + + } + } else { + return c.Status(fiber.StatusNotFound).SendString("Sorry can't find that!") + } + return nil +} diff --git a/thumbnail/thumbnail.go b/thumbnail/thumbnail.go new file mode 100644 index 0000000..233fdb3 --- /dev/null +++ b/thumbnail/thumbnail.go @@ -0,0 +1,79 @@ +package thumbnail + +import ( + "bytes" + "image" + "image/color" + "image/png" + "os" + "path/filepath" + "slices" + "sync" + + "github.com/disintegration/imaging" + "github.com/gofiber/fiber/v2" +) + +var SupportedFileTypes = []string{".png", ".PNG", ".jpg", ".JPG", ".jpeg", ".JPEG"} +var FileTypesMap = map[string]imaging.Format{ + ".png": imaging.PNG, + ".PNG": imaging.PNG, + ".jpg": imaging.JPEG, + ".JPG": imaging.JPEG, + ".jpeg": imaging.JPEG, + ".JPEG": imaging.JPEG, +} +var thumbnailSize = 24 + +var thumbnailCache = map[string][]byte{} +var thumbnailCacheMutex = &sync.RWMutex{} + +func IsSupportedFileType(completePath string) bool { + fileExt := filepath.Ext(completePath) + return slices.Contains(SupportedFileTypes, fileExt) +} + +func GetThumbnail(c *fiber.Ctx, completePath string) { + c.Set(fiber.HeaderContentType, "image") + thumbnailCacheMutex.RLock() + bytesThumb, ok := thumbnailCache[completePath] + thumbnailCacheMutex.RUnlock() + if ok { + c.Write(bytesThumb) + return + } + + fileExt := filepath.Ext(completePath) + if !slices.Contains(SupportedFileTypes, fileExt) { + return + } + f, err := os.Open(completePath) + if err != nil { + return + } + defer f.Close() + img, _, err := image.Decode(f) + if err != nil { + return + } + // load images and make 64x64 thumbnails of them + thumbnail := imaging.Thumbnail(img, thumbnailSize, thumbnailSize, imaging.CatmullRom) + // create a new blank image + dst := imaging.New(thumbnailSize, thumbnailSize, color.NRGBA{0, 0, 0, 0}) + // paste thumbnails into the new image side by side + dst = imaging.Paste(dst, thumbnail, image.Pt(0, 0)) + // save the combined image to buffer + var buf bytes.Buffer + if FileTypesMap[fileExt] == imaging.PNG { + err = png.Encode(&buf, dst) + } else { + err = imaging.Encode(&buf, dst, FileTypesMap[fileExt]) + } + if err != nil { + return + } + thumbnailCacheMutex.Lock() + thumbnailCache[completePath] = buf.Bytes() + thumbnailCacheMutex.Unlock() + c.Write(buf.Bytes()) +}