Skip to content

Commit 6b0d49b

Browse files
authored
feat: add image thumbnails support (#980)
* set max image preview size to 1080x1080px
1 parent 4c20772 commit 6b0d49b

File tree

8 files changed

+137
-9
lines changed

8 files changed

+137
-9
lines changed

frontend/src/components/files/ListingItem.vue

+8-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
:aria-label="name"
1414
:aria-selected="isSelected">
1515
<div>
16-
<i class="material-icons">{{ icon }}</i>
16+
<img v-if="type==='image'" :src="thumbnailUrl">
17+
<i v-else class="material-icons">{{ icon }}</i>
1718
</div>
1819

1920
<div>
@@ -30,6 +31,7 @@
3031
</template>
3132

3233
<script>
34+
import { baseURL } from '@/utils/constants'
3335
import { mapMutations, mapGetters, mapState } from 'vuex'
3436
import filesize from 'filesize'
3537
import moment from 'moment'
@@ -44,7 +46,7 @@ export default {
4446
},
4547
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
4648
computed: {
47-
...mapState(['selected', 'req', 'user']),
49+
...mapState(['selected', 'req', 'user', 'jwt']),
4850
...mapGetters(['selectedCount']),
4951
isSelected () {
5052
return (this.selected.indexOf(this.index) !== -1)
@@ -69,6 +71,10 @@ export default {
6971
}
7072
7173
return true
74+
},
75+
thumbnailUrl () {
76+
const path = this.url.replace(/^\/files\//, '')
77+
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
7278
}
7379
},
7480
methods: {

frontend/src/components/files/Preview.vue

+7-1
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,14 @@ export default {
8686
download () {
8787
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
8888
},
89+
previewUrl () {
90+
if (this.req.type === 'image') {
91+
return `${baseURL}/api/preview/big${this.req.path}?auth=${this.jwt}`
92+
}
93+
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
94+
},
8995
raw () {
90-
return `${this.download}&inline=true`
96+
return `${this.previewUrl}&inline=true`
9197
}
9298
},
9399
async mounted () {

frontend/src/css/listing.css

+12
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@
5252
vertical-align: bottom;
5353
}
5454

55+
#listing .item img {
56+
width: 4em;
57+
height: 4em;
58+
margin-right: 0.1em;
59+
vertical-align: bottom;
60+
}
61+
5562
.message {
5663
text-align: center;
5764
font-size: 2em;
@@ -129,6 +136,11 @@
129136
font-size: 2em;
130137
}
131138

139+
#listing.list .item div:first-of-type img {
140+
width: 2em;
141+
height: 2em;
142+
}
143+
132144
#listing.list .item div:last-of-type {
133145
width: calc(100% - 3em);
134146
display: flex;

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/caddyserver/caddy v1.0.3
99
github.com/daaku/go.zipexe v1.0.1 // indirect
1010
github.com/dgrijalva/jwt-go v3.2.0+incompatible
11+
github.com/disintegration/imaging v1.6.2
1112
github.com/dsnet/compress v0.0.1 // indirect
1213
github.com/golang/snappy v0.0.1 // indirect
1314
github.com/gorilla/mux v1.7.3

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
4343
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
4444
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
4545
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
46+
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
47+
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
4648
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
4749
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
4850
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
239241
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
240242
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
241243
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
244+
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
245+
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
242246
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
243247
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
244248
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=

http/http.go

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler,
5959
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
6060

6161
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
62+
api.PathPrefix("/preview/{size}/{path:.*}").Handler(monkey(previewHandler, "/api/preview")).Methods("GET")
6263
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
6364
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")
6465

http/preview.go

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package http
2+
3+
import (
4+
"fmt"
5+
"image"
6+
"net/http"
7+
8+
"github.com/disintegration/imaging"
9+
"github.com/gorilla/mux"
10+
11+
"github.com/filebrowser/filebrowser/v2/files"
12+
)
13+
14+
const (
15+
sizeThumb = "thumb"
16+
sizeBig = "big"
17+
)
18+
19+
type imageProcessor func(src image.Image) (image.Image, error)
20+
21+
var previewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
22+
if !d.user.Perm.Download {
23+
return http.StatusAccepted, nil
24+
}
25+
vars := mux.Vars(r)
26+
size := vars["size"]
27+
if size != sizeBig && size != sizeThumb {
28+
return http.StatusNotImplemented, nil
29+
}
30+
31+
file, err := files.NewFileInfo(files.FileOptions{
32+
Fs: d.user.Fs,
33+
Path: "/" + vars["path"],
34+
Modify: d.user.Perm.Modify,
35+
Expand: true,
36+
Checker: d,
37+
})
38+
if err != nil {
39+
return errToStatus(err), err
40+
}
41+
42+
setContentDisposition(w, r, file)
43+
44+
switch file.Type {
45+
case "image":
46+
return handleImagePreview(w, r, file, size)
47+
default:
48+
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", file.Type)
49+
}
50+
})
51+
52+
func handleImagePreview(w http.ResponseWriter, r *http.Request, file *files.FileInfo, size string) (int, error) {
53+
format, err := imaging.FormatFromExtension(file.Extension)
54+
if err != nil {
55+
// Unsupported extensions directly return the raw data
56+
if err == imaging.ErrUnsupportedFormat {
57+
return rawFileHandler(w, r, file)
58+
}
59+
return errToStatus(err), err
60+
}
61+
62+
var imgProcessor imageProcessor
63+
switch size {
64+
case sizeBig:
65+
imgProcessor = func(img image.Image) (image.Image, error) {
66+
return imaging.Fit(img, 1080, 1080, imaging.Lanczos), nil
67+
}
68+
case sizeThumb:
69+
imgProcessor = func(img image.Image) (image.Image, error) {
70+
return imaging.Thumbnail(img, 128, 128, imaging.Box), nil
71+
}
72+
default:
73+
return http.StatusBadRequest, fmt.Errorf("unsupported preview size %s", size)
74+
}
75+
76+
fd, err := file.Fs.Open(file.Path)
77+
if err != nil {
78+
return errToStatus(err), err
79+
}
80+
defer fd.Close()
81+
82+
img, err := imaging.Decode(fd, imaging.AutoOrientation(true))
83+
if err != nil {
84+
return errToStatus(err), err
85+
}
86+
img, err = imgProcessor(img)
87+
if err != nil {
88+
return errToStatus(err), err
89+
}
90+
if imaging.Encode(w, img, format) != nil {
91+
return errToStatus(err), err
92+
}
93+
return 0, nil
94+
}

http/raw.go

+10-6
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
5858
}
5959
}
6060

61+
func setContentDisposition(w http.ResponseWriter, r *http.Request, file *files.FileInfo) {
62+
if r.URL.Query().Get("inline") == "true" {
63+
w.Header().Set("Content-Disposition", "inline")
64+
} else {
65+
// As per RFC6266 section 4.3
66+
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
67+
}
68+
}
69+
6170
var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
6271
if !d.user.Perm.Download {
6372
return http.StatusAccepted, nil
@@ -168,12 +177,7 @@ func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo
168177
}
169178
defer fd.Close()
170179

171-
if r.URL.Query().Get("inline") == "true" {
172-
w.Header().Set("Content-Disposition", "inline")
173-
} else {
174-
// As per RFC6266 section 4.3
175-
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
176-
}
180+
setContentDisposition(w, r, file)
177181

178182
http.ServeContent(w, r, file.Name, file.ModTime, fd)
179183
return 0, nil

0 commit comments

Comments
 (0)