feat: add image thumbnails support (#980)
* set max image preview size to 1080x1080pxpull/1014/head
							parent
							
								
									4c20772e11
								
							
						
					
					
						commit
						6b0d49b1fc
					
				|  | @ -13,7 +13,8 @@ | |||
|   :aria-label="name" | ||||
|   :aria-selected="isSelected"> | ||||
|     <div> | ||||
|       <i class="material-icons">{{ icon }}</i> | ||||
|       <img v-if="type==='image'" :src="thumbnailUrl"> | ||||
|       <i v-else class="material-icons">{{ icon }}</i> | ||||
|     </div> | ||||
| 
 | ||||
|     <div> | ||||
|  | @ -30,6 +31,7 @@ | |||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { baseURL } from '@/utils/constants' | ||||
| import { mapMutations, mapGetters, mapState } from 'vuex' | ||||
| import filesize from 'filesize' | ||||
| import moment from 'moment' | ||||
|  | @ -44,7 +46,7 @@ export default { | |||
|   }, | ||||
|   props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'], | ||||
|   computed: { | ||||
|     ...mapState(['selected', 'req', 'user']), | ||||
|     ...mapState(['selected', 'req', 'user', 'jwt']), | ||||
|     ...mapGetters(['selectedCount']), | ||||
|     isSelected () { | ||||
|       return (this.selected.indexOf(this.index) !== -1) | ||||
|  | @ -69,6 +71,10 @@ export default { | |||
|       } | ||||
| 
 | ||||
|       return true | ||||
|     }, | ||||
|     thumbnailUrl () { | ||||
|       const path = this.url.replace(/^\/files\//, '') | ||||
|       return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true` | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  |  | |||
|  | @ -86,8 +86,14 @@ export default { | |||
|     download () { | ||||
|       return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}` | ||||
|     }, | ||||
|     previewUrl () { | ||||
|       if (this.req.type === 'image') { | ||||
|         return `${baseURL}/api/preview/big${this.req.path}?auth=${this.jwt}` | ||||
|       } | ||||
|       return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}` | ||||
|     }, | ||||
|     raw () { | ||||
|       return `${this.download}&inline=true` | ||||
|       return `${this.previewUrl}&inline=true` | ||||
|     } | ||||
|   }, | ||||
|   async mounted () { | ||||
|  |  | |||
|  | @ -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; | ||||
|  | @ -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; | ||||
|  |  | |||
							
								
								
									
										1
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										1
									
								
								go.mod
								
								
								
								
							|  | @ -8,6 +8,7 @@ require ( | |||
| 	github.com/caddyserver/caddy v1.0.3 | ||||
| 	github.com/daaku/go.zipexe v1.0.1 // indirect | ||||
| 	github.com/dgrijalva/jwt-go v3.2.0+incompatible | ||||
| 	github.com/disintegration/imaging v1.6.2 | ||||
| 	github.com/dsnet/compress v0.0.1 // indirect | ||||
| 	github.com/golang/snappy v0.0.1 // indirect | ||||
| 	github.com/gorilla/mux v1.7.3 | ||||
|  |  | |||
							
								
								
									
										4
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										4
									
								
								go.sum
								
								
								
								
							|  | @ -43,6 +43,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs | |||
| github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= | ||||
| github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | ||||
| github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= | ||||
| 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/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= | ||||
| github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= | ||||
| github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= | ||||
|  | @ -239,6 +241,8 @@ golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACk | |||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= | ||||
| golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| 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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | ||||
| golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||
| golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8= | ||||
|  |  | |||
|  | @ -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("/preview/{size}/{path:.*}").Handler(monkey(previewHandler, "/api/preview")).Methods("GET") | ||||
| 	api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET") | ||||
| 	api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET") | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,94 @@ | |||
| package http | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"image" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/disintegration/imaging" | ||||
| 	"github.com/gorilla/mux" | ||||
| 
 | ||||
| 	"github.com/filebrowser/filebrowser/v2/files" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	sizeThumb = "thumb" | ||||
| 	sizeBig   = "big" | ||||
| ) | ||||
| 
 | ||||
| type imageProcessor func(src image.Image) (image.Image, error) | ||||
| 
 | ||||
| var previewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { | ||||
| 	if !d.user.Perm.Download { | ||||
| 		return http.StatusAccepted, nil | ||||
| 	} | ||||
| 	vars := mux.Vars(r) | ||||
| 	size := vars["size"] | ||||
| 	if size != sizeBig && size != sizeThumb { | ||||
| 		return http.StatusNotImplemented, nil | ||||
| 	} | ||||
| 
 | ||||
| 	file, err := files.NewFileInfo(files.FileOptions{ | ||||
| 		Fs:      d.user.Fs, | ||||
| 		Path:    "/" + vars["path"], | ||||
| 		Modify:  d.user.Perm.Modify, | ||||
| 		Expand:  true, | ||||
| 		Checker: d, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return errToStatus(err), err | ||||
| 	} | ||||
| 
 | ||||
| 	setContentDisposition(w, r, file) | ||||
| 
 | ||||
| 	switch file.Type { | ||||
| 	case "image": | ||||
| 		return handleImagePreview(w, r, file, size) | ||||
| 	default: | ||||
| 		return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", file.Type) | ||||
| 	} | ||||
| }) | ||||
| 
 | ||||
| func handleImagePreview(w http.ResponseWriter, r *http.Request, file *files.FileInfo, size string) (int, error) { | ||||
| 	format, err := imaging.FormatFromExtension(file.Extension) | ||||
| 	if err != nil { | ||||
| 		// Unsupported extensions directly return the raw data
 | ||||
| 		if err == imaging.ErrUnsupportedFormat { | ||||
| 			return rawFileHandler(w, r, file) | ||||
| 		} | ||||
| 		return errToStatus(err), err | ||||
| 	} | ||||
| 
 | ||||
| 	var imgProcessor imageProcessor | ||||
| 	switch size { | ||||
| 	case sizeBig: | ||||
| 		imgProcessor = func(img image.Image) (image.Image, error) { | ||||
| 			return imaging.Fit(img, 1080, 1080, imaging.Lanczos), nil | ||||
| 		} | ||||
| 	case sizeThumb: | ||||
| 		imgProcessor = func(img image.Image) (image.Image, error) { | ||||
| 			return imaging.Thumbnail(img, 128, 128, imaging.Box), nil | ||||
| 		} | ||||
| 	default: | ||||
| 		return http.StatusBadRequest, fmt.Errorf("unsupported preview size %s", size) | ||||
| 	} | ||||
| 
 | ||||
| 	fd, err := file.Fs.Open(file.Path) | ||||
| 	if err != nil { | ||||
| 		return errToStatus(err), err | ||||
| 	} | ||||
| 	defer fd.Close() | ||||
| 
 | ||||
| 	img, err := imaging.Decode(fd, imaging.AutoOrientation(true)) | ||||
| 	if err != nil { | ||||
| 		return errToStatus(err), err | ||||
| 	} | ||||
| 	img, err = imgProcessor(img) | ||||
| 	if err != nil { | ||||
| 		return errToStatus(err), err | ||||
| 	} | ||||
| 	if imaging.Encode(w, img, format) != nil { | ||||
| 		return errToStatus(err), err | ||||
| 	} | ||||
| 	return 0, nil | ||||
| } | ||||
							
								
								
									
										16
									
								
								http/raw.go
								
								
								
								
							
							
						
						
									
										16
									
								
								http/raw.go
								
								
								
								
							|  | @ -58,6 +58,15 @@ func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func setContentDisposition(w http.ResponseWriter, r *http.Request, file *files.FileInfo) { | ||||
| 	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)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { | ||||
| 	if !d.user.Perm.Download { | ||||
| 		return http.StatusAccepted, nil | ||||
|  | @ -168,12 +177,7 @@ func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo | |||
| 	} | ||||
| 	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)) | ||||
| 	} | ||||
| 	setContentDisposition(w, r, file) | ||||
| 
 | ||||
| 	http.ServeContent(w, r, file.Name, file.ModTime, fd) | ||||
| 	return 0, nil | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 monkeyWie
						monkeyWie