From af884010d19f8ce213cf402e008db6858368d743 Mon Sep 17 00:00:00 2001 From: Noah Hsu Date: Thu, 11 Aug 2022 20:32:17 +0800 Subject: [PATCH] feat: local storage image thumbnail --- drivers/local/driver.go | 57 ++++++++++++++++++++++++++++++++------ drivers/local/meta.go | 1 + drivers/virtual/driver.go | 2 +- go.mod | 2 ++ go.sum | 5 ++++ internal/driver/driver.go | 2 +- internal/fs/copy.go | 2 +- internal/fs/list.go | 4 ++- internal/model/args.go | 5 ++++ internal/model/obj.go | 4 +-- internal/model/object.go | 37 +++++++++++++++++-------- internal/operations/fs.go | 10 +++---- pkg/utils/path.go | 24 ++++++++++++++++ pkg/utils/path_test.go | 7 +++++ server/common/base.go | 20 +++++++------ server/handles/down.go | 7 ++++- server/handles/fsmanage.go | 6 +++- server/handles/fsread.go | 39 ++++++++++++++------------ server/webdav/webdav.go | 5 +++- 19 files changed, 178 insertions(+), 61 deletions(-) create mode 100644 pkg/utils/path_test.go diff --git a/drivers/local/driver.go b/drivers/local/driver.go index 12e5615b8bc..c42efc473a5 100644 --- a/drivers/local/driver.go +++ b/drivers/local/driver.go @@ -1,17 +1,25 @@ package local import ( + "bytes" "context" + "io" "io/ioutil" + "net/http" "os" + stdpath "path" "path/filepath" + "strconv" "strings" + "github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/operations" "github.com/alist-org/alist/v3/pkg/utils" + "github.com/alist-org/alist/v3/server/common" + "github.com/disintegration/imaging" "github.com/pkg/errors" ) @@ -54,7 +62,7 @@ func (d *Local) GetAddition() driver.Additional { return d.Addition } -func (d *Local) List(ctx context.Context, dir model.Obj) ([]model.Obj, error) { +func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { fullPath := dir.GetID() rawFiles, err := ioutil.ReadDir(fullPath) if err != nil { @@ -65,11 +73,22 @@ func (d *Local) List(ctx context.Context, dir model.Obj) ([]model.Obj, error) { if strings.HasPrefix(f.Name(), ".") { continue } - file := model.Object{ - Name: f.Name(), - Modified: f.ModTime(), - Size: f.Size(), - IsFolder: f.IsDir(), + thumb := "" + if d.Thumbnail && utils.GetFileType(f.Name()) == conf.IMAGE { + thumb = common.GetApiUrl(nil) + stdpath.Join("/d", args.ReqPath, f.Name()) + thumb = utils.EncodePath(thumb, true) + thumb += "?type=thumb" + } + file := model.ObjectThumbnail{ + Object: model.Object{ + Name: f.Name(), + Modified: f.ModTime(), + Size: f.Size(), + IsFolder: f.IsDir(), + }, + Thumbnail: model.Thumbnail{ + Thumbnail: thumb, + }, } files = append(files, &file) } @@ -93,8 +112,30 @@ func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) { func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { fullPath := file.GetID() - link := model.Link{ - FilePath: &fullPath, + var link model.Link + if args.Type == "thumb" && utils.Ext(file.GetName()) != "svg" { + imgData, err := ioutil.ReadFile(fullPath) + if err != nil { + return nil, err + } + srcBuf := bytes.NewBuffer(imgData) + image, err := imaging.Decode(srcBuf) + if err != nil { + return nil, err + } + thumbImg := imaging.Resize(image, 144, 0, imaging.Lanczos) + var buf bytes.Buffer + err = imaging.Encode(&buf, thumbImg, imaging.PNG) + if err != nil { + return nil, err + } + size := buf.Len() + link.Data = io.NopCloser(&buf) + link.Header = http.Header{ + "Content-Length": []string{strconv.Itoa(size)}, + } + } else { + link.FilePath = &fullPath } return &link, nil } diff --git a/drivers/local/meta.go b/drivers/local/meta.go index 08f963be871..4ab3bd58951 100644 --- a/drivers/local/meta.go +++ b/drivers/local/meta.go @@ -7,6 +7,7 @@ import ( type Addition struct { driver.RootFolderPath + Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"` } var config = driver.Config{ diff --git a/drivers/virtual/driver.go b/drivers/virtual/driver.go index 88cfc413f13..a59cde65420 100644 --- a/drivers/virtual/driver.go +++ b/drivers/virtual/driver.go @@ -43,7 +43,7 @@ func (d *Virtual) GetAddition() driver.Additional { return d.Addition } -func (d *Virtual) List(ctx context.Context, dir model.Obj) ([]model.Obj, error) { +func (d *Virtual) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { var res []model.Obj for i := 0; i < d.NumFile; i++ { res = append(res, model.Object{ diff --git a/go.mod b/go.mod index 33e3ad7db5f..81762b396c5 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect + github.com/disintegration/imaging v1.6.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect @@ -53,6 +54,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/ugorji/go/codec v1.2.7 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum index ac27701f534..f3c3d629d43 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -230,6 +232,9 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU= +golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= diff --git a/internal/driver/driver.go b/internal/driver/driver.go index 42cedd94c09..b91c91f69cb 100644 --- a/internal/driver/driver.go +++ b/internal/driver/driver.go @@ -32,7 +32,7 @@ type Reader interface { // List files in the path // if identify files by path, need to set ID with path,like path.Join(dir.GetID(), obj.GetName()) // if identify files by id, need to set ID with corresponding id - List(ctx context.Context, dir model.Obj) ([]model.Obj, error) + List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) // Link get url/filepath/reader of file Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) } diff --git a/internal/fs/copy.go b/internal/fs/copy.go index 5916675846c..22e8361b4b5 100644 --- a/internal/fs/copy.go +++ b/internal/fs/copy.go @@ -51,7 +51,7 @@ func copyBetween2Storages(t *task.Task[uint64], srcStorage, dstStorage driver.Dr } if srcObj.IsDir() { t.SetStatus("src object is dir, listing objs") - objs, err := operations.List(t.Ctx, srcStorage, srcObjPath) + objs, err := operations.List(t.Ctx, srcStorage, srcObjPath, model.ListArgs{}) if err != nil { return errors.WithMessagef(err, "failed list src [%s] objs", srcObjPath) } diff --git a/internal/fs/list.go b/internal/fs/list.go index d7f3b56da59..566a6daf48e 100644 --- a/internal/fs/list.go +++ b/internal/fs/list.go @@ -24,7 +24,9 @@ func list(ctx context.Context, path string) ([]model.Obj, error) { } return nil, errors.WithMessage(err, "failed get storage") } - objs, err := operations.List(ctx, storage, actualPath) + objs, err := operations.List(ctx, storage, actualPath, model.ListArgs{ + ReqPath: path, + }) if err != nil { log.Errorf("%+v", err) if len(virtualFiles) != 0 { diff --git a/internal/model/args.go b/internal/model/args.go index 1f027334255..b3655476e71 100644 --- a/internal/model/args.go +++ b/internal/model/args.go @@ -6,9 +6,14 @@ import ( "time" ) +type ListArgs struct { + ReqPath string +} + type LinkArgs struct { IP string Header http.Header + Type string } type Link struct { diff --git a/internal/model/obj.go b/internal/model/obj.go index 0c26a5a03d9..60e3de5fad8 100644 --- a/internal/model/obj.go +++ b/internal/model/obj.go @@ -28,8 +28,8 @@ type URL interface { URL() string } -type Thumbnail interface { - Thumbnail() string +type Thumb interface { + Thumb() string } type SetID interface { diff --git a/internal/model/object.go b/internal/model/object.go index 37bc696a222..dc9816e4220 100644 --- a/internal/model/object.go +++ b/internal/model/object.go @@ -10,26 +10,39 @@ type Object struct { IsFolder bool } -func (f Object) GetName() string { - return f.Name +func (o Object) GetName() string { + return o.Name } -func (f Object) GetSize() int64 { - return f.Size +func (o Object) GetSize() int64 { + return o.Size } -func (f Object) ModTime() time.Time { - return f.Modified +func (o Object) ModTime() time.Time { + return o.Modified } -func (f Object) IsDir() bool { - return f.IsFolder +func (o Object) IsDir() bool { + return o.IsFolder } -func (f Object) GetID() string { - return f.ID +func (o Object) GetID() string { + return o.ID } -func (f *Object) SetID(id string) { - f.ID = id +func (o *Object) SetID(id string) { + o.ID = id +} + +type Thumbnail struct { + Thumbnail string +} + +func (t Thumbnail) Thumb() string { + return t.Thumbnail +} + +type ObjectThumbnail struct { + Object + Thumbnail } diff --git a/internal/operations/fs.go b/internal/operations/fs.go index 962562b5b47..0d58d971857 100644 --- a/internal/operations/fs.go +++ b/internal/operations/fs.go @@ -28,7 +28,7 @@ func ClearCache(storage driver.Driver, path string) { } // List files in storage, not contains virtual file -func List(ctx context.Context, storage driver.Driver, path string, refresh ...bool) ([]model.Obj, error) { +func List(ctx context.Context, storage driver.Driver, path string, args model.ListArgs, refresh ...bool) ([]model.Obj, error) { path = utils.StandardizePath(path) log.Debugf("operations.List %s", path) dir, err := Get(ctx, storage, path) @@ -39,7 +39,7 @@ func List(ctx context.Context, storage driver.Driver, path string, refresh ...bo return nil, errors.WithStack(errs.NotFolder) } if storage.Config().NoCache { - return storage.List(ctx, dir) + return storage.List(ctx, dir, args) } key := stdpath.Join(storage.GetStorage().MountPath, path) if len(refresh) == 0 || !refresh[0] { @@ -48,7 +48,7 @@ func List(ctx context.Context, storage driver.Driver, path string, refresh ...bo } } files, err, _ := filesG.Do(key, func() ([]model.Obj, error) { - files, err := storage.List(ctx, dir) + files, err := storage.List(ctx, dir, args) if err != nil { return nil, errors.WithMessage(err, "failed to list files") } @@ -99,7 +99,7 @@ func Get(ctx context.Context, storage driver.Driver, path string) (model.Obj, er } // not root folder dir, name := stdpath.Split(path) - files, err := List(ctx, storage, dir) + files, err := List(ctx, storage, dir, model.ListArgs{}) if err != nil { return nil, errors.WithMessage(err, "failed get parent list") } @@ -148,7 +148,7 @@ func Link(ctx context.Context, storage driver.Driver, path string, args model.Li return link, file, err } -// other api +// Other api func Other(ctx context.Context, storage driver.Driver, args model.FsOtherArgs) (interface{}, error) { obj, err := Get(ctx, storage, args.Path) if err != nil { diff --git a/pkg/utils/path.go b/pkg/utils/path.go index 30a0dbd77fe..be09a86b368 100644 --- a/pkg/utils/path.go +++ b/pkg/utils/path.go @@ -1,6 +1,7 @@ package utils import ( + "net/url" stdpath "path" "path/filepath" "runtime" @@ -36,3 +37,26 @@ func Ext(path string) string { } return ext } + +func EncodePath(path string, all ...bool) string { + seg := strings.Split(path, "/") + toReplace := []struct { + Src string + Dst string + }{ + {Src: "%", Dst: "%25"}, + {"%", "%25"}, + {"?", "%3F"}, + {"#", "%23"}, + } + for i := range seg { + if len(all) > 0 && all[0] { + seg[i] = url.PathEscape(seg[i]) + } else { + for j := range toReplace { + seg[i] = strings.ReplaceAll(seg[i], toReplace[j].Src, toReplace[j].Dst) + } + } + } + return strings.Join(seg, "/") +} diff --git a/pkg/utils/path_test.go b/pkg/utils/path_test.go new file mode 100644 index 00000000000..4eb2085087f --- /dev/null +++ b/pkg/utils/path_test.go @@ -0,0 +1,7 @@ +package utils + +import "testing" + +func TestEncodePath(t *testing.T) { + t.Log(EncodePath("http://localhost:5244/d/123#.png")) +} diff --git a/server/common/base.go b/server/common/base.go index 6172a1d3be4..46df102c754 100644 --- a/server/common/base.go +++ b/server/common/base.go @@ -9,15 +9,17 @@ import ( "github.com/alist-org/alist/v3/internal/setting" ) -func GetBaseUrl(r *http.Request) string { - baseUrl := setting.GetByKey(conf.ApiUrl) +func GetApiUrl(r *http.Request) string { + api := setting.GetByKey(conf.ApiUrl) protocol := "http" - if r.TLS != nil { - protocol = "https" + if r != nil { + if r.TLS != nil { + protocol = "https" + } + if api == "" { + api = fmt.Sprintf("%s://%s", protocol, r.Host) + } } - if baseUrl == "" { - baseUrl = fmt.Sprintf("%s://%s", protocol, r.Host) - } - strings.TrimSuffix(baseUrl, "/") - return baseUrl + strings.TrimSuffix(api, "/") + return api } diff --git a/server/handles/down.go b/server/handles/down.go index 3e9d8ab4e08..86217b2c87f 100644 --- a/server/handles/down.go +++ b/server/handles/down.go @@ -30,6 +30,7 @@ func Down(c *gin.Context) { link, _, err := fs.Link(c, rawPath, model.LinkArgs{ IP: c.ClientIP(), Header: c.Request.Header, + Type: c.Query("type"), }) if err != nil { common.ErrorResp(c, err, 500) @@ -52,13 +53,17 @@ func Proxy(c *gin.Context) { if downProxyUrl != "" { _, ok := c.GetQuery("d") if ok { - URL := fmt.Sprintf("%s%s?sign=%s", strings.Split(downProxyUrl, "\n")[0], rawPath, sign.Sign(filename)) + URL := fmt.Sprintf("%s%s?sign=%s", + strings.Split(downProxyUrl, "\n")[0], + utils.EncodePath(rawPath), + sign.Sign(filename)) c.Redirect(302, URL) return } } link, file, err := fs.Link(c, rawPath, model.LinkArgs{ Header: c.Request.Header, + Type: c.Query("type"), }) if err != nil { common.ErrorResp(c, err, 500) diff --git a/server/handles/fsmanage.go b/server/handles/fsmanage.go index 27601ddd2a2..c8f6af8fbea 100644 --- a/server/handles/fsmanage.go +++ b/server/handles/fsmanage.go @@ -11,6 +11,7 @@ import ( "github.com/alist-org/alist/v3/internal/fs" "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/sign" + "github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/server/common" "github.com/gin-gonic/gin" ) @@ -245,7 +246,10 @@ func Link(c *gin.Context) { } if storage.Config().OnlyLocal { common.SuccessResp(c, model.Link{ - URL: fmt.Sprintf("%s/p%s?d&sign=%s", common.GetBaseUrl(c.Request), req.Path, sign.Sign(stdpath.Base(rawPath))), + URL: fmt.Sprintf("%s/p%s?d&sign=%s", + common.GetApiUrl(c.Request), + utils.EncodePath(req.Path), + sign.Sign(stdpath.Base(rawPath))), }) return } diff --git a/server/handles/fsread.go b/server/handles/fsread.go index a4d79eb70f8..899752bc4ff 100644 --- a/server/handles/fsread.go +++ b/server/handles/fsread.go @@ -30,13 +30,13 @@ type DirReq struct { } type ObjResp struct { - Name string `json:"name"` - Size int64 `json:"size"` - IsDir bool `json:"is_dir"` - Modified time.Time `json:"modified"` - Sign string `json:"sign"` - Thumbnail string `json:"thumbnail"` - Type int `json:"type"` + Name string `json:"name"` + Size int64 `json:"size"` + IsDir bool `json:"is_dir"` + Modified time.Time `json:"modified"` + Sign string `json:"sign"` + Thumb string `json:"thumb"` + Type int `json:"type"` } type FsListResp struct { @@ -169,22 +169,22 @@ func pagination(objs []model.Obj, req *common.PageReq) (int, []model.Obj) { func toObjResp(objs []model.Obj) []ObjResp { var resp []ObjResp for _, obj := range objs { - thumbnail := "" - if t, ok := obj.(model.Thumbnail); ok { - thumbnail = t.Thumbnail() + thumb := "" + if t, ok := obj.(model.Thumb); ok { + thumb = t.Thumb() } tp := conf.FOLDER if !obj.IsDir() { tp = utils.GetFileType(obj.GetName()) } resp = append(resp, ObjResp{ - Name: obj.GetName(), - Size: obj.GetSize(), - IsDir: obj.IsDir(), - Modified: obj.ModTime(), - Sign: common.Sign(obj), - Thumbnail: thumbnail, - Type: tp, + Name: obj.GetName(), + Size: obj.GetSize(), + IsDir: obj.IsDir(), + Modified: obj.ModTime(), + Sign: common.Sign(obj), + Thumb: thumb, + Type: tp, }) } return resp @@ -248,7 +248,10 @@ func FsGet(c *gin.Context) { if storage.GetStorage().DownProxyUrl != "" { rawURL = fmt.Sprintf("%s%s?sign=%s", strings.Split(storage.GetStorage().DownProxyUrl, "\n")[0], req.Path, sign.Sign(obj.GetName())) } else { - rawURL = fmt.Sprintf("%s/p%s?sign=%s", common.GetBaseUrl(c.Request), req.Path, sign.Sign(obj.GetName())) + rawURL = fmt.Sprintf("%s/p%s?sign=%s", + common.GetApiUrl(c.Request), + utils.EncodePath(req.Path), + sign.Sign(obj.GetName())) } } else { // if storage is not proxy, use raw url by fs.Link diff --git a/server/webdav/webdav.go b/server/webdav/webdav.go index 2e16616faa8..f43f500c065 100644 --- a/server/webdav/webdav.go +++ b/server/webdav/webdav.go @@ -228,7 +228,10 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta return http.StatusInternalServerError, err } } else if storage.Config().MustProxy() || storage.GetStorage().WebdavProxy() { - u := fmt.Sprintf("%s/p%s?sign=%s", common.GetBaseUrl(r), reqPath, sign.Sign(path.Base(reqPath))) + u := fmt.Sprintf("%s/p%s?sign=%s", + common.GetApiUrl(r), + utils.EncodePath(reqPath), + sign.Sign(path.Base(reqPath))) http.Redirect(w, r, u, 302) } else { link, _, err := fs.Link(ctx, reqPath, model.LinkArgs{IP: utils.ClientIP(r)})