diff --git a/frontend/src/components/files/ListingItem.vue b/frontend/src/components/files/ListingItem.vue index 8c70d54d..96aa9dad 100644 --- a/frontend/src/components/files/ListingItem.vue +++ b/frontend/src/components/files/ListingItem.vue @@ -1,19 +1,23 @@ diff --git a/frontend/src/components/files/Preview.vue b/frontend/src/components/files/Preview.vue index 75190daa..c96fc7bb 100644 --- a/frontend/src/components/files/Preview.vue +++ b/frontend/src/components/files/Preview.vue @@ -1,7 +1,13 @@ diff --git a/frontend/src/css/listing.css b/frontend/src/css/listing.css index 48c78f10..a97fd6b8 100644 --- a/frontend/src/css/listing.css +++ b/frontend/src/css/listing.css @@ -1,6 +1,6 @@ #listing h2 { margin: 0 0 0 0.5em; - font-size: .9em; + font-size: 0.9em; color: rgba(0, 0, 0, 0.38); font-weight: 500; } @@ -10,7 +10,7 @@ overflow: hidden; } -#listing>div { +#listing > div { display: flex; flex-wrap: wrap; justify-content: flex-start; @@ -22,7 +22,7 @@ display: flex; flex-wrap: nowrap; color: #6f6f6f; - transition: .1s ease background, .1s ease opacity; + transition: 0.1s ease background, 0.1s ease opacity; align-items: center; cursor: pointer; } @@ -52,6 +52,13 @@ vertical-align: bottom; } +#listing .item img { + width: 4em; + height: 4em; + margin-right: 0.1em; + vertical-align: bottom; +} + .message { text-align: center; font-size: 2em; @@ -64,7 +71,7 @@ .message i { font-size: 2.5em; - margin-bottom: .2em; + margin-bottom: 0.2em; display: block; } @@ -75,14 +82,14 @@ #listing.mosaic .item { width: calc(33% - 1em); - margin: .5em; + margin: 0.5em; padding: 0.5em; border-radius: 0.2em; - box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.12); } #listing.mosaic .item:hover { - box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24) !important; } #listing.mosaic .header { @@ -116,7 +123,7 @@ display: none; } -#listing .item[aria-selected=true] { +#listing .item[aria-selected="true"] { background: var(--blue) !important; color: #fff !important; } @@ -129,6 +136,11 @@ font-size: 2em; } +#listing.list .item div:first-of-type img { + width: 2em; + height: 2em; +} + #listing.list .item div:last-of-type { width: calc(100% - 3em); display: flex; @@ -151,19 +163,19 @@ #listing.list .header i { font-size: 1.5em; vertical-align: middle; - margin-left: .2em; + margin-left: 0.2em; } #listing.list .item.header { display: flex !important; background: #fafafa; z-index: 999; - padding: .85em; + padding: 0.85em; border: 0; border-bottom: 1px solid rgba(0, 0, 0, 0.1); } -#listing.list .item.header>div:first-child { +#listing.list .item.header > div:first-child { width: 0; } @@ -175,7 +187,7 @@ color: inherit; } -#listing.list .item.header>div:first-child { +#listing.list .item.header > div:first-child { width: 0; } @@ -193,7 +205,7 @@ #listing.list .header i { opacity: 0; - transition: .1s ease all; + transition: 0.1s ease all; } #listing.list .header p:hover i, @@ -215,7 +227,7 @@ height: 4em; padding: 0.5em 0.5em 0.5em 1em; justify-content: space-between; - transition: .2s ease bottom; + transition: 0.2s ease bottom; } #listing #multiple-selection.active { diff --git a/go.mod b/go.mod index 043493cc..522ac272 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1 github.com/mholt/archiver v3.1.1+incompatible github.com/mitchellh/go-homedir v1.1.0 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/nwaples/rardecode v1.0.0 // indirect github.com/pelletier/go-toml v1.6.0 github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1 // indirect diff --git a/go.sum b/go.sum index 271709a8..c86002a4 100644 --- a/go.sum +++ b/go.sum @@ -141,6 +141,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs= diff --git a/http/http.go b/http/http.go index 20828f44..fc8d5929 100644 --- a/http/http.go +++ b/http/http.go @@ -59,6 +59,7 @@ func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler, api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT") api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET") + api.PathPrefix("/compress").Handler(monkey(compressHandler, "/api/compress")).Methods("GET") api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET") api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET") diff --git a/http/preview.go b/http/preview.go new file mode 100644 index 00000000..05666703 --- /dev/null +++ b/http/preview.go @@ -0,0 +1,126 @@ +package http + +import ( + "bytes" + "errors" + "fmt" + "github.com/filebrowser/filebrowser/v2/files" + "github.com/nfnt/resize" + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" + "mime" + "net/http" + "net/url" +) + +var compressHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + if !d.user.Perm.Download { + return http.StatusAccepted, nil + } + + file, err := files.NewFileInfo(files.FileOptions{ + Fs: d.user.Fs, + Path: r.URL.Path, + Modify: d.user.Perm.Modify, + Expand: true, + Checker: d, + }) + if err != nil { + return errToStatus(err), err + } + + if file.IsDir || file.Type != "image" { + return http.StatusNotFound, nil + } + + return compressFileHandler(w, r, file) +}) + +func compressFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) { + fd, err := file.Fs.Open(file.Path) + if err != nil { + return http.StatusInternalServerError, err + } + defer fd.Close() + + if r.URL.Query().Get("inline") == "true" { + w.Header().Set("Content-Disposition", "inline") + } else { + // As per RFC6266 section 4.3 + w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name)) + } + + buf, err := compressImageHandler(file, fd) + if err != nil { + return errToStatus(err), err + } + w.Header().Add("Content-Length", fmt.Sprintf("%d", buf.Len())) + w.Header().Add("Content-Type", mime.TypeByExtension(file.Extension)) + io.Copy(w, buf) + return 0, nil +} + +func compressImageHandler(file *files.FileInfo, fd io.Reader) (*bytes.Buffer, error) { + var ( + buf *bytes.Buffer + m image.Image + err error + ) + + switch file.Extension { + case ".jpg", ".jpeg": + buf, m, err = compressImage(jpeg.Decode, fd) + if err != nil { + return nil, err + } + err = jpeg.Encode(buf, m, nil) + break + case ".png": + buf, m, err = compressImage(png.Decode, fd) + if err != nil { + return nil, err + } + err = png.Encode(buf, m) + break + case ".gif": + buf, m, err = compressImage(gif.Decode, fd) + if err != nil { + return nil, err + } + err = gif.Encode(buf, m, nil) + break + default: + return nil, errors.New("extension is not supported") + } + if err != nil { + return nil, err + } + return buf, nil +} + +const maxSize = 1080 + +func compressImage(decode func(r io.Reader) (image.Image, error), fd io.Reader) (*bytes.Buffer, image.Image, error) { + img, err := decode(fd) + if err != nil { + return nil, nil, err + } + buf := bytes.NewBuffer([]byte{}) + width := img.Bounds().Dx() + height := img.Bounds().Dy() + if width > maxSize && width > height { + width = maxSize + height = 0 + } else if height > maxSize && height > width { + width = 0 + height = maxSize + } else { + width = 0 + height = 0 + } + m := resize.Resize(uint(width), uint(height), img, resize.Lanczos3) + return buf, m, nil +}