Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1529e796df | ||
|
|
d4b904b92b | ||
|
|
12d4177823 | ||
|
|
8142b32f38 | ||
|
|
c5abbb4e1c | ||
|
|
65ac73414f | ||
|
|
ede4213c8e | ||
|
|
b60d291490 | ||
|
|
b9ede79888 | ||
|
|
3d2cb838d1 | ||
|
|
778734419d | ||
|
|
be8683f556 | ||
|
|
c3450f4614 | ||
|
|
5881bc9ab0 | ||
|
|
a2fb499a20 | ||
|
|
411a928fea | ||
|
|
f5d02cdde9 | ||
|
|
c9340af8d0 | ||
|
|
a722bcc13f | ||
|
|
77fe3cfc60 | ||
|
|
470f93cefc | ||
|
|
92fde4dd12 | ||
|
|
95bc92955f | ||
|
|
f2f914221c | ||
|
|
c2d8038c63 | ||
|
|
cb8ac5ebf1 | ||
|
|
aa78e3ab1f | ||
|
|
bc00165094 | ||
|
|
94ef59602f | ||
|
|
14e2f84ceb | ||
|
|
f228fa5540 | ||
|
|
f2d2c1cbf8 | ||
|
|
d9be370e24 | ||
|
|
727c63b98e | ||
|
|
34dfb49b71 | ||
|
|
0b0a704d44 | ||
|
|
2d99d0bf13 | ||
|
|
1790df2090 | ||
|
|
10570ade44 | ||
|
|
43526d9d1a | ||
|
|
2636f876ab | ||
|
|
eed9da1471 | ||
|
|
9a2ebbabe2 | ||
|
|
716396a726 | ||
|
|
0727496601 | ||
|
|
194030fcfc | ||
|
|
b3b644527d | ||
|
|
7e5beeff46 | ||
|
|
a47b69bcec | ||
|
|
6ec6a23861 | ||
|
|
c9cc0d3d5d | ||
|
|
28d2b35718 | ||
|
|
b4f131be50 | ||
|
|
d0b359561f | ||
|
|
453636dfe2 | ||
|
|
b1605aa6d3 | ||
|
|
23503b80a4 | ||
|
|
0d69fbd9a3 | ||
|
|
0d665e528f | ||
|
|
de0b8bb7b2 | ||
|
|
84da110085 | ||
|
|
6b0d49b1fc |
@@ -21,9 +21,17 @@ jobs:
|
|||||||
root: .
|
root: .
|
||||||
paths:
|
paths:
|
||||||
- '*'
|
- '*'
|
||||||
|
test:
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.15.2
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
name: "Test"
|
||||||
|
command: go test ./...
|
||||||
build-go:
|
build-go:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.14.3
|
- image: circleci/golang:1.15.2
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: '~/project'
|
at: '~/project'
|
||||||
@@ -41,12 +49,12 @@ jobs:
|
|||||||
- '*'
|
- '*'
|
||||||
release:
|
release:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.14.3
|
- image: circleci/golang:1.15.2
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: '~/project'
|
at: '~/project'
|
||||||
- setup_remote_docker
|
- setup_remote_docker
|
||||||
- run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
- run: echo $DOCKER_PASSWORD | docker login --username $DOCKER_USERNAME --password-stdin
|
||||||
- run: curl -sL https://git.io/goreleaser | bash
|
- run: curl -sL https://git.io/goreleaser | bash
|
||||||
- run: docker logout
|
- run: docker logout
|
||||||
workflows:
|
workflows:
|
||||||
@@ -57,6 +65,10 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
only: /.*/
|
only: /.*/
|
||||||
|
- test:
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
- build-node:
|
- build-node:
|
||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
@@ -68,6 +80,7 @@ workflows:
|
|||||||
requires:
|
requires:
|
||||||
- build-node
|
- build-node
|
||||||
- lint
|
- lint
|
||||||
|
- test
|
||||||
- release:
|
- release:
|
||||||
context: deploy
|
context: deploy
|
||||||
requires:
|
requires:
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ dockers:
|
|||||||
goarm: ''
|
goarm: ''
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:alpine"
|
- "filebrowser/filebrowser:alpine"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-apline"
|
- "filebrowser/filebrowser:{{ .Tag }}-alpine"
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-alpine"
|
- "filebrowser/filebrowser:v{{ .Major }}-alpine"
|
||||||
extra_files:
|
extra_files:
|
||||||
- .docker.json
|
- .docker.json
|
||||||
|
|||||||
85
CHANGELOG.md
85
CHANGELOG.md
@@ -2,6 +2,91 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.7.0](https://github.com/filebrowser/filebrowser/compare/v2.6.2...v2.7.0) (2020-09-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add --socket-perm flag to control unix socket file permissions (closes [#1060](https://github.com/filebrowser/filebrowser/issues/1060)) ([65ac734](https://github.com/filebrowser/filebrowser/commit/65ac73414fadc4686c94803a93ff319e8f7ce9d1))
|
||||||
|
* preview mobile dropdown ([7787344](https://github.com/filebrowser/filebrowser/commit/778734419de314d4cb64d07109bbab73f8e2e42a))
|
||||||
|
* preview size button ([3d2cb83](https://github.com/filebrowser/filebrowser/commit/3d2cb838d111ee61047599f49e76de80c821f341))
|
||||||
|
* put selected files in the root of the archive (closes [#1065](https://github.com/filebrowser/filebrowser/issues/1065)) ([8142b32](https://github.com/filebrowser/filebrowser/commit/8142b32f3865eccd3331328e0d087f805d186ed5))
|
||||||
|
|
||||||
|
### [2.6.2](https://github.com/filebrowser/filebrowser/compare/v2.6.1...v2.6.2) (2020-08-05)
|
||||||
|
|
||||||
|
### [2.6.1](https://github.com/filebrowser/filebrowser/compare/v2.6.0...v2.6.1) (2020-07-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* delete cached previews when deleting file ([f5d02cd](https://github.com/filebrowser/filebrowser/commit/f5d02cdde97923b963878abf5a300393b9feb348))
|
||||||
|
* escape special characters in preview url (closes [#1002](https://github.com/filebrowser/filebrowser/issues/1002)) ([c9340af](https://github.com/filebrowser/filebrowser/commit/c9340af8d045671ad3338c5d2d887c335ab92de4))
|
||||||
|
|
||||||
|
## [2.6.0](https://github.com/filebrowser/filebrowser/compare/v2.5.0...v2.6.0) (2020-07-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add lazy load of image thumbnails ([bc00165](https://github.com/filebrowser/filebrowser/commit/bc001650944ae963b12b5b2538a68de7cd0d8f82))
|
||||||
|
* add param to disable img resizing ([aa78e3a](https://github.com/filebrowser/filebrowser/commit/aa78e3ab1fcae6f618e811ba4e315a7a209f9df2))
|
||||||
|
* cache resized images ([95bc929](https://github.com/filebrowser/filebrowser/commit/95bc92955f391ece22c40d9592f2a3e6e26907b9))
|
||||||
|
* limit image resize workers ([94ef596](https://github.com/filebrowser/filebrowser/commit/94ef59602fb50fc21b1164feda90a3b9aeb5e972))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* conflict handling on upload button ([f228fa5](https://github.com/filebrowser/filebrowser/commit/f228fa55408824618e9f0879da67c86d22b0d324))
|
||||||
|
* drop feedback ([f2d2c1c](https://github.com/filebrowser/filebrowser/commit/f2d2c1cbf85fba3edffb7b079f121ed3f0bc1e02))
|
||||||
|
* missing error message ([d9be370](https://github.com/filebrowser/filebrowser/commit/d9be370e2474b8070fa58db920c9481270cc4a48))
|
||||||
|
* parent verification on copy ([727c63b](https://github.com/filebrowser/filebrowser/commit/727c63b98e2964d0960d25914c296570f6c79478))
|
||||||
|
* path separator inconsistency on rename ([34dfb49](https://github.com/filebrowser/filebrowser/commit/34dfb49b719c948e709a4639b4af2c5cb73b3887))
|
||||||
|
|
||||||
|
## [2.5.0](https://github.com/filebrowser/filebrowser/compare/v2.4.0...v2.5.0) (2020-07-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add previewer title and loading indicator ([716396a](https://github.com/filebrowser/filebrowser/commit/716396a726329f0ba42fc34167dd07497c5bf47c))
|
||||||
|
* duplicate files in the same directory ([43526d9](https://github.com/filebrowser/filebrowser/commit/43526d9d1a8c837245e3f5059e0b4737583eeaeb))
|
||||||
|
* file copy, move and paste conflict checking ([eed9da1](https://github.com/filebrowser/filebrowser/commit/eed9da1471723ed3fbe6c00b1d6362b1c5fd8b04))
|
||||||
|
* rename option on replace prompt ([2636f87](https://github.com/filebrowser/filebrowser/commit/2636f876ab8f88eea6d9548de524ca2339eb0843))
|
||||||
|
* upload queue ([6ec6a23](https://github.com/filebrowser/filebrowser/commit/6ec6a2386173410f5cab9941dbf1bacb6b70ddd2))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* blinking previewer ([9a2ebba](https://github.com/filebrowser/filebrowser/commit/9a2ebbabe2e9f0c292701d33f36f9b7a457b1164))
|
||||||
|
* dark theme colors ([b3b6445](https://github.com/filebrowser/filebrowser/commit/b3b644527d5673e16e61d404ff58a3c7bd6b6637))
|
||||||
|
* directory conflict checking ([7e5beef](https://github.com/filebrowser/filebrowser/commit/7e5beeff464e75ab185c430cd96e7cc67209ccc1))
|
||||||
|
* prompt before closing window ([194030f](https://github.com/filebrowser/filebrowser/commit/194030fcfcf54a2cf5e2f8ececcbb4754474d8f8))
|
||||||
|
* remove incomplete uploaded files ([0727496](https://github.com/filebrowser/filebrowser/commit/0727496601a9918c8131c56f62419bfac7ac589a))
|
||||||
|
* reset clipboard after pasting cutted files ([10570ad](https://github.com/filebrowser/filebrowser/commit/10570ade442b573ebe00af08369e28b1b0688df6))
|
||||||
|
|
||||||
|
## [2.4.0](https://github.com/filebrowser/filebrowser/compare/v2.3.0...v2.4.0) (2020-07-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* full screen editor ([0d665e5](https://github.com/filebrowser/filebrowser/commit/0d665e528f880ceda0976ceed66070ac34de7969))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add preview bypass for .gif files ([#1012](https://github.com/filebrowser/filebrowser/issues/1012)) ([453636d](https://github.com/filebrowser/filebrowser/commit/453636dfe2bbf177c74617862eb763485d4774bf))
|
||||||
|
* prompt key shortcut conflict ([0d69fbd](https://github.com/filebrowser/filebrowser/commit/0d69fbd9a342aa2695859021df0c423e3ae4a4fa))
|
||||||
|
|
||||||
|
## [2.3.0](https://github.com/filebrowser/filebrowser/compare/v2.2.0...v2.3.0) (2020-06-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add image thumbnails support ([#980](https://github.com/filebrowser/filebrowser/issues/980)) ([6b0d49b](https://github.com/filebrowser/filebrowser/commit/6b0d49b1fc8bdce89576ba91cc0b8ec594fcd625))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* typo in image_templates (apline -> alpine) ([#1005](https://github.com/filebrowser/filebrowser/issues/1005)) ([84da110](https://github.com/filebrowser/filebrowser/commit/84da11008516a371fc0446d97863dc14d337aa25))
|
||||||
|
|
||||||
## [2.2.0](https://github.com/filebrowser/filebrowser/compare/v2.1.2...v2.2.0) (2020-06-22)
|
## [2.2.0](https://github.com/filebrowser/filebrowser/compare/v2.1.2...v2.2.0) (2020-06-22)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
38
cmd/root.go
38
cmd/root.go
@@ -14,13 +14,16 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
v "github.com/spf13/viper"
|
v "github.com/spf13/viper"
|
||||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/auth"
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/diskcache"
|
||||||
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/img"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
@@ -55,7 +58,12 @@ func addServerFlags(flags *pflag.FlagSet) {
|
|||||||
flags.StringP("key", "k", "", "tls key")
|
flags.StringP("key", "k", "", "tls key")
|
||||||
flags.StringP("root", "r", ".", "root to prepend to relative paths")
|
flags.StringP("root", "r", ".", "root to prepend to relative paths")
|
||||||
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
|
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
|
||||||
|
flags.Uint32("socket-perm", 0666, "unix socket file permissions")
|
||||||
flags.StringP("baseurl", "b", "", "base url")
|
flags.StringP("baseurl", "b", "", "base url")
|
||||||
|
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
|
||||||
|
flags.Int("img-processors", 4, "image processors count")
|
||||||
|
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
|
||||||
|
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -103,6 +111,24 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
quickSetup(cmd.Flags(), d)
|
quickSetup(cmd.Flags(), d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build img service
|
||||||
|
workersCount, err := cmd.Flags().GetInt("img-processors")
|
||||||
|
checkErr(err)
|
||||||
|
if workersCount < 1 {
|
||||||
|
log.Fatal("Image resize workers count could not be < 1")
|
||||||
|
}
|
||||||
|
imgSvc := img.New(workersCount)
|
||||||
|
|
||||||
|
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
||||||
|
cacheDir, err := cmd.Flags().GetString("cache-dir")
|
||||||
|
checkErr(err)
|
||||||
|
if cacheDir != "" {
|
||||||
|
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
|
||||||
|
log.Fatalf("can't make directory %s: %s", cacheDir, err)
|
||||||
|
}
|
||||||
|
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
server := getRunParams(cmd.Flags(), d.store)
|
server := getRunParams(cmd.Flags(), d.store)
|
||||||
setupLog(server.Log)
|
setupLog(server.Log)
|
||||||
|
|
||||||
@@ -118,6 +144,10 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
case server.Socket != "":
|
case server.Socket != "":
|
||||||
listener, err = net.Listen("unix", server.Socket)
|
listener, err = net.Listen("unix", server.Socket)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
|
||||||
|
checkErr(err)
|
||||||
|
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
|
||||||
|
checkErr(err)
|
||||||
case server.TLSKey != "" && server.TLSCert != "":
|
case server.TLSKey != "" && server.TLSCert != "":
|
||||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
|
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
@@ -132,7 +162,7 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||||
go cleanupHandler(listener, sigc)
|
go cleanupHandler(listener, sigc)
|
||||||
|
|
||||||
handler, err := fbhttp.NewHandler(d.store, server)
|
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -205,6 +235,12 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
|||||||
server.Socket = ""
|
server.Socket = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, disableThumbnails := getParamB(flags, "disable-thumbnails")
|
||||||
|
server.EnableThumbnails = !disableThumbnails
|
||||||
|
|
||||||
|
_, disablePreviewResize := getParamB(flags, "disable-preview-resize")
|
||||||
|
server.ResizePreview = !disablePreviewResize
|
||||||
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
diskcache/cache.go
Normal file
11
diskcache/cache.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Interface interface {
|
||||||
|
Store(ctx context.Context, key string, value []byte) error
|
||||||
|
Load(ctx context.Context, key string) (value []byte, exist bool, err error)
|
||||||
|
Delete(ctx context.Context, key string) error
|
||||||
|
}
|
||||||
110
diskcache/file_cache.go
Normal file
110
diskcache/file_cache.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha1" //nolint:gosec
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileCache struct {
|
||||||
|
fs afero.Fs
|
||||||
|
|
||||||
|
// granular locks
|
||||||
|
scopedLocks struct {
|
||||||
|
sync.Mutex
|
||||||
|
sync.Once
|
||||||
|
locks map[string]sync.Locker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(fs afero.Fs, root string) *FileCache {
|
||||||
|
return &FileCache{
|
||||||
|
fs: afero.NewBasePathFs(fs, root),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
|
||||||
|
mu := f.getScopedLocks(key)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
fileName := f.getFileName(key)
|
||||||
|
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
|
||||||
|
r, ok, err := f.open(key)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return nil, ok, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
value, err = ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return value, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) Delete(ctx context.Context, key string) error {
|
||||||
|
mu := f.getScopedLocks(key)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
fileName := f.getFileName(key)
|
||||||
|
if err := f.fs.Remove(fileName); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) open(key string) (afero.File, bool, error) {
|
||||||
|
fileName := f.getFileName(key)
|
||||||
|
file, err := f.fs.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getScopedLocks pull lock from the map if found or create a new one
|
||||||
|
func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) {
|
||||||
|
f.scopedLocks.Do(func() { f.scopedLocks.locks = map[string]sync.Locker{} })
|
||||||
|
|
||||||
|
f.scopedLocks.Lock()
|
||||||
|
lock, ok := f.scopedLocks.locks[key]
|
||||||
|
if !ok {
|
||||||
|
lock = &sync.Mutex{}
|
||||||
|
f.scopedLocks.locks[key] = lock
|
||||||
|
}
|
||||||
|
f.scopedLocks.Unlock()
|
||||||
|
|
||||||
|
return lock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) getFileName(key string) string {
|
||||||
|
hasher := sha1.New() //nolint:gosec
|
||||||
|
_, _ = hasher.Write([]byte(key))
|
||||||
|
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)
|
||||||
|
}
|
||||||
55
diskcache/file_cache_test.go
Normal file
55
diskcache/file_cache_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileCache(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
const (
|
||||||
|
key = "key"
|
||||||
|
value = "some text"
|
||||||
|
newValue = "new text"
|
||||||
|
cacheRoot = "/cache"
|
||||||
|
cachedFilePath = "a/62/a62f2225bf70bfaccbc7f1ef2a397836717377de"
|
||||||
|
)
|
||||||
|
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
cache := New(fs, "/cache")
|
||||||
|
|
||||||
|
// store new key
|
||||||
|
err := cache.Store(ctx, key, []byte(value))
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
|
||||||
|
|
||||||
|
// update existing key
|
||||||
|
err = cache.Store(ctx, key, []byte(newValue))
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
|
||||||
|
|
||||||
|
// delete key
|
||||||
|
err = cache.Delete(ctx, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
exists, err := afero.Exists(fs, filepath.Join(cacheRoot, cachedFilePath))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:golint
|
||||||
|
t.Helper()
|
||||||
|
// check actual file content
|
||||||
|
b, err := afero.ReadFile(fs, fileFullPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, wantValue, string(b))
|
||||||
|
|
||||||
|
// check cache content
|
||||||
|
b, ok, err := cache.Load(ctx, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, wantValue, string(b))
|
||||||
|
}
|
||||||
24
diskcache/noop_cache.go
Normal file
24
diskcache/noop_cache.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NoOp struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNoOp() *NoOp {
|
||||||
|
return &NoOp{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoOp) Store(ctx context.Context, key string, value []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoOp) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoOp) Delete(ctx context.Context, key string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -16,4 +16,5 @@ var (
|
|||||||
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
||||||
ErrPermissionDenied = errors.New("permission denied")
|
ErrPermissionDenied = errors.New("permission denied")
|
||||||
ErrInvalidRequestParams = errors.New("invalid request params")
|
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||||
|
ErrSourceIsParent = errors.New("source is parent")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package fileutils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
@@ -25,7 +27,7 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the destination file.
|
// Create the destination file.
|
||||||
dst, err := fs.Create(dest)
|
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -49,3 +51,59 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommonPrefix returns common directory path of provided files
|
||||||
|
func CommonPrefix(sep byte, paths ...string) string {
|
||||||
|
// Handle special cases.
|
||||||
|
switch len(paths) {
|
||||||
|
case 0:
|
||||||
|
return ""
|
||||||
|
case 1:
|
||||||
|
return path.Clean(paths[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note, we treat string as []byte, not []rune as is often
|
||||||
|
// done in Go. (And sep as byte, not rune). This is because
|
||||||
|
// most/all supported OS' treat paths as string of non-zero
|
||||||
|
// bytes. A filename may be displayed as a sequence of Unicode
|
||||||
|
// runes (typically encoded as UTF-8) but paths are
|
||||||
|
// not required to be valid UTF-8 or in any normalized form
|
||||||
|
// (e.g. "é" (U+00C9) and "é" (U+0065,U+0301) are different
|
||||||
|
// file names.
|
||||||
|
c := []byte(path.Clean(paths[0]))
|
||||||
|
|
||||||
|
// We add a trailing sep to handle the case where the
|
||||||
|
// common prefix directory is included in the path list
|
||||||
|
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
|
||||||
|
// path.Clean will have cleaned off trailing / separators with
|
||||||
|
// the exception of the root directory, "/" (in which case we
|
||||||
|
// make it "//", but this will get fixed up to "/" bellow).
|
||||||
|
c = append(c, sep)
|
||||||
|
|
||||||
|
// Ignore the first path since it's already in c
|
||||||
|
for _, v := range paths[1:] {
|
||||||
|
// Clean up each path before testing it
|
||||||
|
v = path.Clean(v) + string(sep)
|
||||||
|
|
||||||
|
// Find the first non-common byte and truncate c
|
||||||
|
if len(v) < len(c) {
|
||||||
|
c = c[:len(v)]
|
||||||
|
}
|
||||||
|
for i := 0; i < len(c); i++ {
|
||||||
|
if v[i] != c[i] {
|
||||||
|
c = c[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing non-separator characters and the final separator
|
||||||
|
for i := len(c) - 1; i >= 0; i-- {
|
||||||
|
if c[i] == sep {
|
||||||
|
c = c[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(c)
|
||||||
|
}
|
||||||
|
|||||||
46
fileutils/file_test.go
Normal file
46
fileutils/file_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package fileutils
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCommonPrefix(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
paths []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
"same lvl": {
|
||||||
|
paths: []string{
|
||||||
|
"/home/user/file1",
|
||||||
|
"/home/user/file2",
|
||||||
|
},
|
||||||
|
want: "/home/user",
|
||||||
|
},
|
||||||
|
"sub folder": {
|
||||||
|
paths: []string{
|
||||||
|
"/home/user/folder",
|
||||||
|
"/home/user/folder/file",
|
||||||
|
},
|
||||||
|
want: "/home/user/folder",
|
||||||
|
},
|
||||||
|
"relative path": {
|
||||||
|
paths: []string{
|
||||||
|
"/home/user/folder",
|
||||||
|
"/home/user/folder/../folder2",
|
||||||
|
},
|
||||||
|
want: "/home/user",
|
||||||
|
},
|
||||||
|
"no common path": {
|
||||||
|
paths: []string{
|
||||||
|
"/home/user/folder",
|
||||||
|
"/etc/file",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, tt := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
if got := CommonPrefix('/', tt.paths...); got != tt.want {
|
||||||
|
t.Errorf("CommonPrefix() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
5
frontend/package-lock.json
generated
5
frontend/package-lock.json
generated
@@ -13030,6 +13030,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.15.3.tgz",
|
||||||
"integrity": "sha512-PVNgo6yhOmacZVFjSapZ314oewwLyXHjJwAqjnaPN1GJAJd/dvsrShGzSiJuCX4Hc36G4epJvNXUwO8y7wEKew=="
|
"integrity": "sha512-PVNgo6yhOmacZVFjSapZ314oewwLyXHjJwAqjnaPN1GJAJd/dvsrShGzSiJuCX4Hc36G4epJvNXUwO8y7wEKew=="
|
||||||
},
|
},
|
||||||
|
"vue-lazyload": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-lazyload/-/vue-lazyload-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-uHnq0FTEeNmqnbBC2aRKlmtd9LofMZ6Q3mWvgfLa+i9vhxU8fDK+nGs9c1iVT85axSua/AUnMttIq3xPaU9G3A=="
|
||||||
|
},
|
||||||
"vue-loader": {
|
"vue-loader": {
|
||||||
"version": "15.8.3",
|
"version": "15.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.8.3.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"qrcode.vue": "^1.7.0",
|
"qrcode.vue": "^1.7.0",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-i18n": "^8.15.3",
|
"vue-i18n": "^8.15.3",
|
||||||
|
"vue-lazyload": "^1.3.3",
|
||||||
"vue-router": "^3.1.3",
|
"vue-router": "^3.1.3",
|
||||||
"vuex": "^3.1.2",
|
"vuex": "^3.1.2",
|
||||||
"vuex-router-sync": "^5.0.0"
|
"vuex-router-sync": "^5.0.0"
|
||||||
|
|||||||
@@ -13,18 +13,19 @@
|
|||||||
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
|
||||||
|
|
||||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
|
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
|
||||||
<meta name="theme-color" content="#2979ff">
|
<meta name="theme-color" content="#2979ff">
|
||||||
|
|
||||||
<!-- Add to home screen for Safari on iOS -->
|
<!-- Add to home screen for Safari on iOS/iPadOS -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="assets">
|
<meta name="apple-mobile-web-app-title" content="assets">
|
||||||
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png">
|
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png">
|
||||||
|
|
||||||
<!-- Add to home screen for Windows -->
|
<!-- Add to home screen for Windows -->
|
||||||
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/msapplication-icon-144x144.png">
|
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png">
|
||||||
<meta name="msapplication-TileColor" content="#2979ff">
|
<meta name="msapplication-TileColor" content="#2979ff">
|
||||||
|
|
||||||
<!-- Inject Some Variables and generate the manifest json -->
|
<!-- Inject Some Variables and generate the manifest json -->
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
:root {
|
:root {
|
||||||
--background: #121212;
|
--background: #141D24;
|
||||||
--surfacePrimary: #171819;
|
--surfacePrimary: #20292F;
|
||||||
--surfaceSecondary: #212528;
|
--surfaceSecondary: #3A4147;
|
||||||
--divider: rgba(255, 255, 255, 0.12);
|
--divider: rgba(255, 255, 255, 0.12);
|
||||||
--icon: #ffffff;
|
--icon: #ffffff;
|
||||||
--textPrimary: rgba(255, 255, 255, 0.87);
|
--textPrimary: rgba(255, 255, 255, 0.87);
|
||||||
@@ -16,7 +16,7 @@ body {
|
|||||||
#loading {
|
#loading {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
#loading .spinner div {
|
#loading .spinner div, #previewer .loading .spinner div {
|
||||||
background: var(--icon);
|
background: var(--icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,25 +30,34 @@ header {
|
|||||||
|
|
||||||
#search #input {
|
#search #input {
|
||||||
background: var(--surfaceSecondary);
|
background: var(--surfaceSecondary);
|
||||||
|
border-color: var(--surfacePrimary);
|
||||||
}
|
}
|
||||||
#search.active #input,
|
#search #input input::placeholder {
|
||||||
#search.active .boxes {
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
#search.active #input {
|
||||||
background: var(--surfacePrimary);
|
background: var(--surfacePrimary);
|
||||||
}
|
}
|
||||||
#search.active input {
|
#search.active input {
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
}
|
}
|
||||||
#search.active #result {
|
#search #result {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
}
|
}
|
||||||
#search.active .boxes h3 {
|
#search .boxes {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
#search .boxes h3 {
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
color: var(--textPrimary) !important;
|
color: var(--textPrimary) !important;
|
||||||
}
|
}
|
||||||
|
.action:hover {
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
|
}
|
||||||
.action i {
|
.action i {
|
||||||
color: var(--icon) !important;
|
color: var(--icon) !important;
|
||||||
}
|
}
|
||||||
@@ -93,6 +102,10 @@ nav > div {
|
|||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--surfacePrimary);
|
background: var(--surfacePrimary);
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
@@ -106,9 +119,23 @@ nav > div {
|
|||||||
.dashboard p label {
|
.dashboard p label {
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
}
|
}
|
||||||
|
.card#share ul li input,
|
||||||
|
.card#share ul li select,
|
||||||
.input {
|
.input {
|
||||||
background: var(--surfaceSecondary);
|
background: var(--surfaceSecondary);
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.input:hover,
|
||||||
|
.input:focus {
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
.input--red {
|
||||||
|
background: #73302D;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--green {
|
||||||
|
background: #147A41;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard #nav li,
|
.dashboard #nav li,
|
||||||
@@ -119,10 +146,35 @@ nav > div {
|
|||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list li:hover {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
.file-list li:before {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
.file-list li[aria-selected=true]:before {
|
||||||
|
color: var(--icon);
|
||||||
|
}
|
||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
background: var(--surfacePrimary);
|
background: var(--surfacePrimary);
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
}
|
}
|
||||||
|
.shell__result {
|
||||||
|
border-top: 1px solid var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container .bar {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 736px) {
|
@media (max-width: 736px) {
|
||||||
#file-selection {
|
#file-selection {
|
||||||
@@ -138,3 +190,11 @@ nav > div {
|
|||||||
background: var(--surfaceSecondary) !important;
|
background: var(--surfaceSecondary) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.share__box, .share__box__download {
|
||||||
|
background: var(--surfaceSecondary) !important;
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
.share__box__download {
|
||||||
|
border-bottom-color: var(--divider);
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ async function resourceAction (url, method, content) {
|
|||||||
const res = await fetchURL(`/api/resources${url}`, opts)
|
const res = await fetchURL(`/api/resources${url}`, opts)
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error(res.responseText)
|
throw new Error(await res.text())
|
||||||
} else {
|
} else {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -94,9 +94,6 @@ export async function post (url, content = '', overwrite = false, onupload) {
|
|||||||
request.upload.onprogress = onupload
|
request.upload.onprogress = onupload
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a message to user before closing the tab during file upload
|
|
||||||
window.onbeforeunload = () => "Files are being uploaded."
|
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
resolve(request.responseText)
|
resolve(request.responseText)
|
||||||
@@ -112,29 +109,28 @@ export async function post (url, content = '', overwrite = false, onupload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
request.send(content)
|
request.send(content)
|
||||||
// Upload is done no more message before closing the tab
|
})
|
||||||
}).finally(() => { window.onbeforeunload = null })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveCopy (items, copy = false) {
|
function moveCopy (items, copy = false, overwrite = false, rename = false) {
|
||||||
let promises = []
|
let promises = []
|
||||||
|
|
||||||
for (let item of items) {
|
for (let item of items) {
|
||||||
const from = removePrefix(item.from)
|
const from = removePrefix(item.from)
|
||||||
const to = encodeURIComponent(removePrefix(item.to))
|
const to = encodeURIComponent(removePrefix(item.to))
|
||||||
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}`
|
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}&override=${overwrite}&rename=${rename}`
|
||||||
promises.push(resourceAction(url, 'PATCH'))
|
promises.push(resourceAction(url, 'PATCH'))
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function move (items) {
|
export function move (items, overwrite = false, rename = false) {
|
||||||
return moveCopy(items)
|
return moveCopy(items, false, overwrite, rename)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copy (items) {
|
export function copy (items, overwrite = false, rename = false) {
|
||||||
return moveCopy(items, true)
|
return moveCopy(items, true, overwrite, rename)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checksum (url, algo) {
|
export async function checksum (url, algo) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header v-if="!isEditor && !isPreview">
|
||||||
<div>
|
<div>
|
||||||
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
||||||
<i class="material-icons">menu</i>
|
<i class="material-icons">menu</i>
|
||||||
@@ -13,10 +13,6 @@
|
|||||||
<i class="material-icons">search</i>
|
<i class="material-icons">search</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
|
|
||||||
<i class="material-icons">save</i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||||
<i class="material-icons">more_vert</i>
|
<i class="material-icons">more_vert</i>
|
||||||
</button>
|
</button>
|
||||||
@@ -112,6 +108,7 @@ export default {
|
|||||||
'selectedCount',
|
'selectedCount',
|
||||||
'isFiles',
|
'isFiles',
|
||||||
'isEditor',
|
'isEditor',
|
||||||
|
'isPreview',
|
||||||
'isListing',
|
'isListing',
|
||||||
'isLogged'
|
'isLogged'
|
||||||
]),
|
]),
|
||||||
@@ -129,9 +126,6 @@ export default {
|
|||||||
showUpload () {
|
showUpload () {
|
||||||
return this.isListing && this.user.perm.create
|
return this.isListing && this.user.perm.create
|
||||||
},
|
},
|
||||||
showSaveButton () {
|
|
||||||
return this.isEditor && this.user.perm.modify
|
|
||||||
},
|
|
||||||
showDownloadButton () {
|
showDownloadButton () {
|
||||||
return this.isFiles && this.user.perm.download
|
return this.isFiles && this.user.perm.download
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -136,12 +136,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener("keydown", event => {
|
|
||||||
if (event.keyCode === 27) {
|
|
||||||
this.closeHovers()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$refs.result.addEventListener('scroll', event => {
|
this.$refs.result.addEventListener('scroll', event => {
|
||||||
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
|
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
|
||||||
this.resultsCount += 50
|
this.resultsCount += 50
|
||||||
|
|||||||
22
frontend/src/components/buttons/PreviewSize.vue
Normal file
22
frontend/src/components/buttons/PreviewSize.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="$emit('change-size')">
|
||||||
|
<i class="material-icons">{{ this.icon }}</i>
|
||||||
|
<span>{{ $t('buttons.info') }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'preview-size-button',
|
||||||
|
props: [ 'size' ],
|
||||||
|
computed: {
|
||||||
|
icon () {
|
||||||
|
if (this.size) {
|
||||||
|
return 'photo_size_select_large'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'hd'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,11 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div id="editor-container">
|
||||||
|
<div class="bar">
|
||||||
|
<button @click="back" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close" class="action">
|
||||||
|
<i class="material-icons">close</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="title">
|
||||||
|
<span>{{ req.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="save" v-show="user.perm.modify" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" id="save-button" class="action">
|
||||||
|
<i class="material-icons">save</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="breadcrumbs">
|
||||||
|
<span><i class="material-icons">home</i></span>
|
||||||
|
|
||||||
|
<span v-for="(link, index) in breadcrumbs" :key="index">
|
||||||
|
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
|
||||||
|
<span>{{ link.name }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form id="editor"></form>
|
<form id="editor"></form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import { files as api } from '@/api'
|
import { files as api } from '@/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
import url from '@/utils/url'
|
||||||
|
|
||||||
import ace from 'ace-builds/src-min-noconflict/ace.js'
|
import ace from 'ace-builds/src-min-noconflict/ace.js'
|
||||||
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
|
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
|
||||||
@@ -14,27 +40,52 @@ import { theme } from '@/utils/constants'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'editor',
|
name: 'editor',
|
||||||
computed: {
|
|
||||||
...mapState(['req'])
|
|
||||||
},
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['req', 'user']),
|
||||||
|
breadcrumbs () {
|
||||||
|
let parts = this.$route.path.split('/')
|
||||||
|
|
||||||
|
if (parts[0] === '') {
|
||||||
|
parts.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts[parts.length - 1] === '') {
|
||||||
|
parts.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
let breadcrumbs = []
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
breadcrumbs.push({ name: decodeURIComponent(parts[i]) })
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbs.shift()
|
||||||
|
|
||||||
|
if (breadcrumbs.length > 3) {
|
||||||
|
while (breadcrumbs.length !== 4) {
|
||||||
|
breadcrumbs.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbs[0].name = '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
return breadcrumbs
|
||||||
|
}
|
||||||
|
},
|
||||||
created () {
|
created () {
|
||||||
window.addEventListener('keydown', this.keyEvent)
|
window.addEventListener('keydown', this.keyEvent)
|
||||||
document.getElementById('save-button').addEventListener('click', this.save)
|
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
window.removeEventListener('keydown', this.keyEvent)
|
window.removeEventListener('keydown', this.keyEvent)
|
||||||
document.getElementById('save-button').removeEventListener('click', this.save)
|
|
||||||
this.editor.destroy();
|
this.editor.destroy();
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
const fileContent = this.req.content || '';
|
const fileContent = this.req.content || '';
|
||||||
|
|
||||||
this.editor = ace.edit('editor', {
|
this.editor = ace.edit('editor', {
|
||||||
maxLines: 80,
|
|
||||||
minLines: 20,
|
|
||||||
value: fileContent,
|
value: fileContent,
|
||||||
showPrintMargin: false,
|
showPrintMargin: false,
|
||||||
readOnly: this.req.type === 'textImmutable',
|
readOnly: this.req.type === 'textImmutable',
|
||||||
@@ -48,6 +99,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
back () {
|
||||||
|
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||||
|
this.$router.push({ path: uri })
|
||||||
|
},
|
||||||
keyEvent (event) {
|
keyEvent (event) {
|
||||||
if (!event.ctrlKey && !event.metaKey) {
|
if (!event.ctrlKey && !event.metaKey) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,10 +10,12 @@
|
|||||||
@mouseup="mouseUp"
|
@mouseup="mouseUp"
|
||||||
@wheel="wheelMove"
|
@wheel="wheelMove"
|
||||||
>
|
>
|
||||||
<img :src="src" class="image-ex-img" ref="imgex" @load="setCenter">
|
<img :src="src" class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import throttle from 'lodash.throttle'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
src: String,
|
src: String,
|
||||||
@@ -50,7 +52,12 @@ export default {
|
|||||||
inDrag: false,
|
inDrag: false,
|
||||||
lastTouchDistance: 0,
|
lastTouchDistance: 0,
|
||||||
moveDisabled: false,
|
moveDisabled: false,
|
||||||
disabledTimer: null
|
disabledTimer: null,
|
||||||
|
imageLoaded: false,
|
||||||
|
position: {
|
||||||
|
center: { x: 0, y: 0 },
|
||||||
|
relative: { x: 0, y: 0 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -63,24 +70,54 @@ export default {
|
|||||||
if (getComputedStyle(container).height === "0px") {
|
if (getComputedStyle(container).height === "0px") {
|
||||||
container.style.height = "100%"
|
container.style.height = "100%"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.onResize)
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
window.removeEventListener('resize', this.onResize)
|
||||||
|
document.removeEventListener('mouseup', this.onMouseUp)
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
src: function () {
|
||||||
|
this.scale = 1
|
||||||
|
this.setZoom()
|
||||||
|
this.setCenter()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onLoad() {
|
||||||
|
let img = this.$refs.imgex
|
||||||
|
|
||||||
|
this.imageLoaded = true
|
||||||
|
|
||||||
|
if (img === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img.classList.remove('image-ex-img-center')
|
||||||
|
this.setCenter()
|
||||||
|
img.classList.add('image-ex-img-ready')
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', this.onMouseUp)
|
||||||
|
},
|
||||||
|
onMouseUp() {
|
||||||
|
this.inDrag = false
|
||||||
|
},
|
||||||
|
onResize: throttle(function() {
|
||||||
|
if (this.imageLoaded) {
|
||||||
|
this.setCenter()
|
||||||
|
this.doMove(this.position.relative.x, this.position.relative.y)
|
||||||
|
}
|
||||||
|
}, 100),
|
||||||
setCenter() {
|
setCenter() {
|
||||||
let container = this.$refs.container
|
let container = this.$refs.container
|
||||||
let img = this.$refs.imgex
|
let img = this.$refs.imgex
|
||||||
|
|
||||||
let rate = Math.min(
|
this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2)
|
||||||
container.clientWidth / img.clientWidth,
|
this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2)
|
||||||
container.clientHeight / img.clientHeight
|
|
||||||
)
|
img.style.left = this.position.center.x + 'px'
|
||||||
if (!this.autofill && rate > 1) {
|
img.style.top = this.position.center.y + 'px'
|
||||||
rate = 1
|
|
||||||
}
|
|
||||||
// height will be auto set
|
|
||||||
img.width = Math.floor(img.clientWidth * rate)
|
|
||||||
img.style.top = `${Math.floor((container.clientHeight - img.clientHeight) / 2)}px`
|
|
||||||
img.style.left = `${Math.floor((container.clientWidth - img.clientWidth) / 2)}px`
|
|
||||||
document.addEventListener('mouseup', () => this.inDrag = false )
|
|
||||||
},
|
},
|
||||||
mousedownStart(event) {
|
mousedownStart(event) {
|
||||||
this.lastX = null
|
this.lastX = null
|
||||||
@@ -159,8 +196,22 @@ export default {
|
|||||||
},
|
},
|
||||||
doMove(x, y) {
|
doMove(x, y) {
|
||||||
let style = this.$refs.imgex.style
|
let style = this.$refs.imgex.style
|
||||||
style.left = `${this.pxStringToNumber(style.left) + x}px`
|
let posX = this.pxStringToNumber(style.left) + x
|
||||||
style.top = `${this.pxStringToNumber(style.top) + y}px`
|
let posY = this.pxStringToNumber(style.top) + y
|
||||||
|
|
||||||
|
style.left = posX + 'px'
|
||||||
|
style.top = posY + 'px'
|
||||||
|
|
||||||
|
this.position.relative.x = Math.abs(this.position.center.x - posX)
|
||||||
|
this.position.relative.y = Math.abs(this.position.center.y - posY)
|
||||||
|
|
||||||
|
if (posX < this.position.center.x) {
|
||||||
|
this.position.relative.x = this.position.relative.x * -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (posY < this.position.center.y) {
|
||||||
|
this.position.relative.y = this.position.relative.y * -1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
wheelMove(event) {
|
wheelMove(event) {
|
||||||
this.scale += (event.wheelDeltaY / 100) * this.zoomStep
|
this.scale += (event.wheelDeltaY / 100) * this.zoomStep
|
||||||
@@ -185,9 +236,20 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-ex-img {
|
.image-ex-img {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-ex-img-center {
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
position: absolute;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-ex-img-ready {
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
position: absolute;
|
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,9 +8,7 @@
|
|||||||
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
|
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
|
||||||
</div>
|
</div>
|
||||||
<div v-else id="listing"
|
<div v-else id="listing"
|
||||||
:class="user.viewMode"
|
:class="user.viewMode">
|
||||||
@dragenter="dragEnter"
|
|
||||||
@dragend="dragEnd">
|
|
||||||
<div>
|
<div>
|
||||||
<div class="item header">
|
<div class="item header">
|
||||||
<div></div>
|
<div></div>
|
||||||
@@ -89,29 +87,22 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapMutations } from 'vuex'
|
import { mapState, mapMutations } from 'vuex'
|
||||||
import throttle from 'lodash.throttle'
|
|
||||||
import Item from './ListingItem'
|
import Item from './ListingItem'
|
||||||
import css from '@/utils/css'
|
import css from '@/utils/css'
|
||||||
import { users, files as api } from '@/api'
|
import { users, files as api } from '@/api'
|
||||||
import buttons from '@/utils/buttons'
|
import * as upload from '@/utils/upload'
|
||||||
import url from '@/utils/url'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'listing',
|
name: 'listing',
|
||||||
components: { Item },
|
components: { Item },
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
show: 50,
|
showLimit: 50,
|
||||||
uploading: {
|
dragCounter: 0
|
||||||
id: 0,
|
|
||||||
count: 0,
|
|
||||||
size: 0,
|
|
||||||
progress: []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['req', 'selected', 'user']),
|
...mapState(['req', 'selected', 'user', 'show']),
|
||||||
nameSorted () {
|
nameSorted () {
|
||||||
return (this.req.sorting.by === 'name')
|
return (this.req.sorting.by === 'name')
|
||||||
},
|
},
|
||||||
@@ -139,14 +130,14 @@ export default {
|
|||||||
return { dirs, files }
|
return { dirs, files }
|
||||||
},
|
},
|
||||||
dirs () {
|
dirs () {
|
||||||
return this.items.dirs.slice(0, this.show)
|
return this.items.dirs.slice(0, this.showLimit)
|
||||||
},
|
},
|
||||||
files () {
|
files () {
|
||||||
let show = this.show - this.items.dirs.length
|
let showLimit = this.showLimit - this.items.dirs.length
|
||||||
|
|
||||||
if (show < 0) show = 0
|
if (showLimit < 0) showLimit = 0
|
||||||
|
|
||||||
return this.items.files.slice(0, show)
|
return this.items.files.slice(0, showLimit)
|
||||||
},
|
},
|
||||||
nameIcon () {
|
nameIcon () {
|
||||||
if (this.nameSorted && !this.ascOrdered) {
|
if (this.nameSorted && !this.ascOrdered) {
|
||||||
@@ -179,6 +170,8 @@ export default {
|
|||||||
window.addEventListener('resize', this.resizeEvent)
|
window.addEventListener('resize', this.resizeEvent)
|
||||||
window.addEventListener('scroll', this.scrollEvent)
|
window.addEventListener('scroll', this.scrollEvent)
|
||||||
document.addEventListener('dragover', this.preventDefault)
|
document.addEventListener('dragover', this.preventDefault)
|
||||||
|
document.addEventListener('dragenter', this.dragEnter)
|
||||||
|
document.addEventListener('dragleave', this.dragLeave)
|
||||||
document.addEventListener('drop', this.drop)
|
document.addEventListener('drop', this.drop)
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
@@ -187,6 +180,8 @@ export default {
|
|||||||
window.removeEventListener('resize', this.resizeEvent)
|
window.removeEventListener('resize', this.resizeEvent)
|
||||||
window.removeEventListener('scroll', this.scrollEvent)
|
window.removeEventListener('scroll', this.scrollEvent)
|
||||||
document.removeEventListener('dragover', this.preventDefault)
|
document.removeEventListener('dragover', this.preventDefault)
|
||||||
|
document.removeEventListener('dragenter', this.dragEnter)
|
||||||
|
document.removeEventListener('dragleave', this.dragLeave)
|
||||||
document.removeEventListener('drop', this.drop)
|
document.removeEventListener('drop', this.drop)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -195,6 +190,10 @@ export default {
|
|||||||
return window.btoa(unescape(encodeURIComponent(name)))
|
return window.btoa(unescape(encodeURIComponent(name)))
|
||||||
},
|
},
|
||||||
keyEvent (event) {
|
keyEvent (event) {
|
||||||
|
if (this.show !== null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!event.ctrlKey && !event.metaKey) {
|
if (!event.ctrlKey && !event.metaKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -252,7 +251,8 @@ export default {
|
|||||||
|
|
||||||
this.$store.commit('updateClipboard', {
|
this.$store.commit('updateClipboard', {
|
||||||
key: key,
|
key: key,
|
||||||
items: items
|
items: items,
|
||||||
|
path: this.$route.path
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
paste (event) {
|
paste (event) {
|
||||||
@@ -265,23 +265,56 @@ export default {
|
|||||||
for (let item of this.$store.state.clipboard.items) {
|
for (let item of this.$store.state.clipboard.items) {
|
||||||
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
|
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
|
||||||
const to = this.$route.path + item.name
|
const to = this.$route.path + item.name
|
||||||
items.push({ from, to })
|
items.push({ from, to, name: item.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.$store.state.clipboard.key === 'x') {
|
let action = (overwrite, rename) => {
|
||||||
api.move(items).then(() => {
|
api.copy(items, overwrite, rename).then(() => {
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
}).catch(this.$showError)
|
}).catch(this.$showError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.state.clipboard.key === 'x') {
|
||||||
|
action = (overwrite, rename) => {
|
||||||
|
api.move(items, overwrite, rename).then(() => {
|
||||||
|
this.$store.commit('resetClipboard')
|
||||||
|
this.$store.commit('setReload', true)
|
||||||
|
}).catch(this.$showError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.state.clipboard.path == this.$route.path) {
|
||||||
|
action(false, true)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
api.copy(items).then(() => {
|
let conflict = upload.checkConflict(items, this.req.items)
|
||||||
this.$store.commit('setReload', true)
|
|
||||||
}).catch(this.$showError)
|
let overwrite = false
|
||||||
|
let rename = false
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit('showHover', {
|
||||||
|
prompt: 'replace-rename',
|
||||||
|
confirm: (event, option) => {
|
||||||
|
overwrite = option == 'overwrite'
|
||||||
|
rename = option == 'rename'
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
action(overwrite, rename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename)
|
||||||
},
|
},
|
||||||
resizeEvent () {
|
resizeEvent () {
|
||||||
// Update the columns size based on the window width.
|
// Update the columns size based on the window width.
|
||||||
@@ -292,10 +325,12 @@ export default {
|
|||||||
},
|
},
|
||||||
scrollEvent () {
|
scrollEvent () {
|
||||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||||
this.show += 50
|
this.showLimit += 50
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dragEnter () {
|
dragEnter () {
|
||||||
|
this.dragCounter++
|
||||||
|
|
||||||
// When the user starts dragging an item, put every
|
// When the user starts dragging an item, put every
|
||||||
// file on the listing with 50% opacity.
|
// file on the listing with 50% opacity.
|
||||||
let items = document.getElementsByClassName('item')
|
let items = document.getElementsByClassName('item')
|
||||||
@@ -304,11 +339,16 @@ export default {
|
|||||||
file.style.opacity = 0.5
|
file.style.opacity = 0.5
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
dragEnd () {
|
dragLeave () {
|
||||||
|
this.dragCounter--
|
||||||
|
|
||||||
|
if (this.dragCounter == 0) {
|
||||||
this.resetOpacity()
|
this.resetOpacity()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
drop: function (event) {
|
drop: async function (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
this.dragCounter = 0
|
||||||
this.resetOpacity()
|
this.resetOpacity()
|
||||||
|
|
||||||
let dt = event.dataTransfer
|
let dt = event.dataTransfer
|
||||||
@@ -327,65 +367,34 @@ export default {
|
|||||||
base = el.querySelector('.name').innerHTML + '/'
|
base = el.querySelector('.name').innerHTML + '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (base === '') {
|
let files = await upload.scanFiles(dt)
|
||||||
this.scanFiles(dt).then((result) => {
|
let path = this.$route.path + base
|
||||||
this.checkConflict(result, this.req.items, base)
|
let items = this.req.items
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.scanFiles(dt).then((result) => {
|
|
||||||
api.fetch(this.$route.path + base)
|
|
||||||
.then(req => {
|
|
||||||
this.checkConflict(result, req.items, base)
|
|
||||||
})
|
|
||||||
.catch(this.$showError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
checkConflict (files, items, base) {
|
|
||||||
if (typeof items === 'undefined' || items === null) {
|
|
||||||
items = []
|
|
||||||
}
|
|
||||||
|
|
||||||
let folder_upload = false
|
if (base !== '') {
|
||||||
if (files[0].fullPath !== undefined) {
|
try {
|
||||||
folder_upload = true
|
items = (await api.fetch(path)).items
|
||||||
}
|
} catch (error) {
|
||||||
|
this.$showError(error)
|
||||||
let conflict = false
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
let file = files[i]
|
|
||||||
let name = file.name
|
|
||||||
|
|
||||||
if (folder_upload) {
|
|
||||||
let dirs = file.fullPath.split("/")
|
|
||||||
if (dirs.length > 1) {
|
|
||||||
name = dirs[0]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = items.findIndex(function hasConflict (element) {
|
let conflict = upload.checkConflict(files, items)
|
||||||
return (element.name === this)
|
|
||||||
}, name)
|
|
||||||
|
|
||||||
if (res >= 0) {
|
|
||||||
conflict = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!conflict) {
|
|
||||||
this.handleFiles(files, base)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
this.$store.commit('showHover', {
|
this.$store.commit('showHover', {
|
||||||
prompt: 'replace',
|
prompt: 'replace',
|
||||||
confirm: (event) => {
|
confirm: (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.$store.commit('closeHovers')
|
this.$store.commit('closeHovers')
|
||||||
this.handleFiles(files, base, true)
|
upload.handleFiles(files, path, true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
upload.handleFiles(files, path)
|
||||||
},
|
},
|
||||||
uploadInput (event) {
|
uploadInput (event) {
|
||||||
this.$store.commit('closeHovers')
|
this.$store.commit('closeHovers')
|
||||||
@@ -400,7 +409,23 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkConflict(files, this.req.items, '')
|
let path = this.$route.path
|
||||||
|
let conflict = upload.checkConflict(files, this.req.items)
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit('showHover', {
|
||||||
|
prompt: 'replace',
|
||||||
|
confirm: (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
upload.handleFiles(files, path, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
upload.handleFiles(files, path)
|
||||||
},
|
},
|
||||||
resetOpacity () {
|
resetOpacity () {
|
||||||
let items = document.getElementsByClassName('item')
|
let items = document.getElementsByClassName('item')
|
||||||
@@ -409,145 +434,6 @@ export default {
|
|||||||
file.style.opacity = 1
|
file.style.opacity = 1
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
scanFiles(dt) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
let reading = 0
|
|
||||||
const contents = []
|
|
||||||
|
|
||||||
if (dt.items !== undefined) {
|
|
||||||
for (let item of dt.items) {
|
|
||||||
if (item.kind === "file" && typeof item.webkitGetAsEntry === "function") {
|
|
||||||
const entry = item.webkitGetAsEntry()
|
|
||||||
readEntry(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve(dt.files)
|
|
||||||
}
|
|
||||||
|
|
||||||
function readEntry(entry, directory = "") {
|
|
||||||
if (entry.isFile) {
|
|
||||||
reading++
|
|
||||||
entry.file(file => {
|
|
||||||
reading--
|
|
||||||
|
|
||||||
file.fullPath = `${directory}${file.name}`
|
|
||||||
contents.push(file)
|
|
||||||
|
|
||||||
if (reading === 0) {
|
|
||||||
resolve(contents)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (entry.isDirectory) {
|
|
||||||
const dir = {
|
|
||||||
isDir: true,
|
|
||||||
path: `${directory}${entry.name}`
|
|
||||||
}
|
|
||||||
|
|
||||||
contents.push(dir)
|
|
||||||
|
|
||||||
readReaderContent(entry.createReader(), `${directory}${entry.name}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readReaderContent(reader, directory) {
|
|
||||||
reading++
|
|
||||||
|
|
||||||
reader.readEntries(function (entries) {
|
|
||||||
reading--
|
|
||||||
if (entries.length > 0) {
|
|
||||||
for (const entry of entries) {
|
|
||||||
readEntry(entry, `${directory}/`)
|
|
||||||
}
|
|
||||||
|
|
||||||
readReaderContent(reader, `${directory}/`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reading === 0) {
|
|
||||||
resolve(contents)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
setProgress: throttle(function() {
|
|
||||||
if (this.uploading.count == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let sum = this.uploading.progress.reduce((acc, val) => acc + val)
|
|
||||||
this.$store.commit('setProgress', Math.ceil(sum / this.uploading.size * 100))
|
|
||||||
}, 100, {leading: false, trailing: true}),
|
|
||||||
handleFiles (files, base, overwrite = false) {
|
|
||||||
if (this.uploading.count == 0) {
|
|
||||||
buttons.loading('upload')
|
|
||||||
}
|
|
||||||
|
|
||||||
let promises = []
|
|
||||||
|
|
||||||
let onupload = (id) => (event) => {
|
|
||||||
this.uploading.progress[id] = event.loaded
|
|
||||||
this.setProgress()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
let file = files[i]
|
|
||||||
|
|
||||||
if (!file.isDir) {
|
|
||||||
let filename = (file.fullPath !== undefined) ? file.fullPath : file.name
|
|
||||||
let filenameEncoded = url.encodeRFC5987ValueChars(filename)
|
|
||||||
|
|
||||||
let id = this.uploading.id
|
|
||||||
|
|
||||||
this.uploading.size += file.size
|
|
||||||
this.uploading.id++
|
|
||||||
this.uploading.count++
|
|
||||||
|
|
||||||
let promise = api.post(this.$route.path + base + filenameEncoded, file, overwrite, throttle(onupload(id), 100)).finally(() => {
|
|
||||||
this.uploading.count--
|
|
||||||
})
|
|
||||||
|
|
||||||
promises.push(promise)
|
|
||||||
} else {
|
|
||||||
let uri = this.$route.path + base
|
|
||||||
let folders = file.path.split("/")
|
|
||||||
|
|
||||||
for (let i = 0; i < folders.length; i++) {
|
|
||||||
let folder = folders[i]
|
|
||||||
let folderEncoded = encodeURIComponent(folder)
|
|
||||||
uri += folderEncoded + "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
api.post(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let finish = () => {
|
|
||||||
if (this.uploading.count > 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons.success('upload')
|
|
||||||
|
|
||||||
this.$store.commit('setProgress', 0)
|
|
||||||
this.$store.commit('setReload', true)
|
|
||||||
|
|
||||||
this.uploading.id = 0
|
|
||||||
this.uploading.sizes = []
|
|
||||||
this.uploading.progress = []
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(promises)
|
|
||||||
.then(() => {
|
|
||||||
finish()
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
finish()
|
|
||||||
this.$showError(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
async sort (by) {
|
async sort (by) {
|
||||||
let asc = false
|
let asc = false
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
:aria-label="name"
|
:aria-label="name"
|
||||||
:aria-selected="isSelected">
|
:aria-selected="isSelected">
|
||||||
<div>
|
<div>
|
||||||
<i class="material-icons">{{ icon }}</i>
|
<img v-if="type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl">
|
||||||
|
<i v-else class="material-icons">{{ icon }}</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -30,10 +31,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { baseURL, enableThumbs } from '@/utils/constants'
|
||||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
import { mapMutations, mapGetters, mapState } from 'vuex'
|
||||||
import filesize from 'filesize'
|
import filesize from 'filesize'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { files as api } from '@/api'
|
import { files as api } from '@/api'
|
||||||
|
import * as upload from '@/utils/upload'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'item',
|
name: 'item',
|
||||||
@@ -44,7 +47,7 @@ export default {
|
|||||||
},
|
},
|
||||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['selected', 'req', 'user']),
|
...mapState(['selected', 'req', 'user', 'jwt']),
|
||||||
...mapGetters(['selectedCount']),
|
...mapGetters(['selectedCount']),
|
||||||
isSelected () {
|
isSelected () {
|
||||||
return (this.selected.indexOf(this.index) !== -1)
|
return (this.selected.indexOf(this.index) !== -1)
|
||||||
@@ -69,6 +72,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
},
|
||||||
|
thumbnailUrl () {
|
||||||
|
const path = this.url.replace(/^\/files\//, '')
|
||||||
|
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
|
||||||
|
},
|
||||||
|
isThumbsEnabled () {
|
||||||
|
return enableThumbs
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -104,26 +114,61 @@ export default {
|
|||||||
|
|
||||||
el.style.opacity = 1
|
el.style.opacity = 1
|
||||||
},
|
},
|
||||||
drop: function (event) {
|
drop: async function (event) {
|
||||||
if (!this.canDrop) return
|
if (!this.canDrop) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
if (this.selectedCount === 0) return
|
if (this.selectedCount === 0) return
|
||||||
|
|
||||||
|
let el = event.target
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if (el !== null && !el.classList.contains('item')) {
|
||||||
|
el = el.parentElement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let items = []
|
let items = []
|
||||||
|
|
||||||
for (let i of this.selected) {
|
for (let i of this.selected) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[i].url,
|
from: this.req.items[i].url,
|
||||||
to: this.url + this.req.items[i].name
|
to: this.url + this.req.items[i].name,
|
||||||
|
name: this.req.items[i].name
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
api.move(items)
|
let base = el.querySelector('.name').innerHTML + '/'
|
||||||
.then(() => {
|
let path = this.$route.path + base
|
||||||
|
let baseItems = (await api.fetch(path)).items
|
||||||
|
|
||||||
|
let action = (overwrite, rename) => {
|
||||||
|
api.move(items, overwrite, rename).then(() => {
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
|
}).catch(this.$showError)
|
||||||
|
}
|
||||||
|
|
||||||
|
let conflict = upload.checkConflict(items, baseItems)
|
||||||
|
|
||||||
|
let overwrite = false
|
||||||
|
let rename = false
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit('showHover', {
|
||||||
|
prompt: 'replace-rename',
|
||||||
|
confirm: (event, option) => {
|
||||||
|
overwrite = option == 'overwrite'
|
||||||
|
rename = option == 'rename'
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
action(overwrite, rename)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(this.$showError)
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename)
|
||||||
},
|
},
|
||||||
click: function (event) {
|
click: function (event) {
|
||||||
if (this.selectedCount !== 0) event.preventDefault()
|
if (this.selectedCount !== 0) event.preventDefault()
|
||||||
|
|||||||
@@ -5,10 +5,29 @@
|
|||||||
<i class="material-icons">close</i>
|
<i class="material-icons">close</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<rename-button v-if="user.perm.rename"></rename-button>
|
<div class="title">
|
||||||
<delete-button v-if="user.perm.delete"></delete-button>
|
<span>{{ this.name }}</span>
|
||||||
<download-button v-if="user.perm.download"></download-button>
|
</div>
|
||||||
<info-button></info-button>
|
|
||||||
|
<preview-size-button v-if="isResizeEnabled && this.req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
|
||||||
|
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||||
|
<i class="material-icons">more_vert</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="dropdown" :class="{ active : showMore }">
|
||||||
|
<rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
|
||||||
|
<delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
|
||||||
|
<download-button :disabled="loading" v-if="user.perm.download"></download-button>
|
||||||
|
<info-button :disabled="loading"></info-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="loading" v-if="loading">
|
||||||
|
<div class="spinner">
|
||||||
|
<div class="bounce1"></div>
|
||||||
|
<div class="bounce2"></div>
|
||||||
|
<div class="bounce3"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
||||||
@@ -18,6 +37,7 @@
|
|||||||
<i class="material-icons">chevron_right</i>
|
<i class="material-icons">chevron_right</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<template v-if="!loading">
|
||||||
<div class="preview">
|
<div class="preview">
|
||||||
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
|
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
|
||||||
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
|
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
|
||||||
@@ -37,14 +57,18 @@
|
|||||||
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-show="showMore" @click="resetPrompts" class="overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import { baseURL } from '@/utils/constants'
|
import { baseURL, resizePreview } from '@/utils/constants'
|
||||||
import { files as api } from '@/api'
|
import { files as api } from '@/api'
|
||||||
|
import PreviewSizeButton from '@/components/buttons/PreviewSize'
|
||||||
import InfoButton from '@/components/buttons/Info'
|
import InfoButton from '@/components/buttons/Info'
|
||||||
import DeleteButton from '@/components/buttons/Delete'
|
import DeleteButton from '@/components/buttons/Delete'
|
||||||
import RenameButton from '@/components/buttons/Rename'
|
import RenameButton from '@/components/buttons/Rename'
|
||||||
@@ -61,6 +85,7 @@ const mediaTypes = [
|
|||||||
export default {
|
export default {
|
||||||
name: 'preview',
|
name: 'preview',
|
||||||
components: {
|
components: {
|
||||||
|
PreviewSizeButton,
|
||||||
InfoButton,
|
InfoButton,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
RenameButton,
|
RenameButton,
|
||||||
@@ -72,11 +97,13 @@ export default {
|
|||||||
previousLink: '',
|
previousLink: '',
|
||||||
nextLink: '',
|
nextLink: '',
|
||||||
listing: null,
|
listing: null,
|
||||||
subtitles: []
|
name: '',
|
||||||
|
subtitles: [],
|
||||||
|
fullSize: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['req', 'user', 'oldReq', 'jwt']),
|
...mapState(['req', 'user', 'oldReq', 'jwt', 'loading']),
|
||||||
hasPrevious () {
|
hasPrevious () {
|
||||||
return (this.previousLink !== '')
|
return (this.previousLink !== '')
|
||||||
},
|
},
|
||||||
@@ -84,36 +111,42 @@ export default {
|
|||||||
return (this.nextLink !== '')
|
return (this.nextLink !== '')
|
||||||
},
|
},
|
||||||
download () {
|
download () {
|
||||||
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
|
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||||
|
},
|
||||||
|
previewUrl () {
|
||||||
|
if (this.req.type === 'image' && !this.fullSize) {
|
||||||
|
return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||||
|
}
|
||||||
|
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||||
},
|
},
|
||||||
raw () {
|
raw () {
|
||||||
return `${this.download}&inline=true`
|
return `${this.previewUrl}&inline=true`
|
||||||
|
},
|
||||||
|
showMore () {
|
||||||
|
return this.$store.state.show === 'more'
|
||||||
|
},
|
||||||
|
isResizeEnabled () {
|
||||||
|
return resizePreview
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route: function () {
|
||||||
|
this.updatePreview()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
window.addEventListener('keyup', this.key)
|
window.addEventListener('keyup', this.key)
|
||||||
|
this.$store.commit('setPreviewMode', true)
|
||||||
if (this.req.subtitles) {
|
this.listing = this.oldReq.items
|
||||||
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
|
this.updatePreview()
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.oldReq.items) {
|
|
||||||
this.updateLinks(this.oldReq.items)
|
|
||||||
} else {
|
|
||||||
const path = url.removeLastDir(this.$route.path)
|
|
||||||
const res = await api.fetch(path)
|
|
||||||
this.updateLinks(res.items)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.$showError(e)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
window.removeEventListener('keyup', this.key)
|
window.removeEventListener('keyup', this.key)
|
||||||
|
this.$store.commit('setPreviewMode', false)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
back () {
|
back () {
|
||||||
|
this.$store.commit('setPreviewMode', false)
|
||||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||||
this.$router.push({ path: uri })
|
this.$router.push({ path: uri })
|
||||||
},
|
},
|
||||||
@@ -132,28 +165,57 @@ export default {
|
|||||||
if (this.hasPrevious) this.prev()
|
if (this.hasPrevious) this.prev()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateLinks (items) {
|
async updatePreview () {
|
||||||
for (let i = 0; i < items.length; i++) {
|
if (this.req.subtitles) {
|
||||||
if (items[i].name !== this.req.name) {
|
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let dirs = this.$route.fullPath.split("/")
|
||||||
|
this.name = decodeURIComponent(dirs[dirs.length - 1])
|
||||||
|
|
||||||
|
if (!this.listing) {
|
||||||
|
try {
|
||||||
|
const path = url.removeLastDir(this.$route.path)
|
||||||
|
const res = await api.fetch(path)
|
||||||
|
this.listing = res.items
|
||||||
|
} catch (e) {
|
||||||
|
this.$showError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previousLink = ''
|
||||||
|
this.nextLink = ''
|
||||||
|
|
||||||
|
for (let i = 0; i < this.listing.length; i++) {
|
||||||
|
if (this.listing[i].name !== this.name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let j = i - 1; j >= 0; j--) {
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
if (mediaTypes.includes(items[j].type)) {
|
if (mediaTypes.includes(this.listing[j].type)) {
|
||||||
this.previousLink = items[j].url
|
this.previousLink = this.listing[j].url
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let j = i + 1; j < items.length; j++) {
|
for (let j = i + 1; j < this.listing.length; j++) {
|
||||||
if (mediaTypes.includes(items[j].type)) {
|
if (mediaTypes.includes(this.listing[j].type)) {
|
||||||
this.nextLink = items[j].url
|
this.nextLink = this.listing[j].url
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
openMore () {
|
||||||
|
this.$store.commit('showHover', 'more')
|
||||||
|
},
|
||||||
|
resetPrompts () {
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
},
|
||||||
|
toggleSize () {
|
||||||
|
this.fullSize = !this.fullSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
<button class="button button--flat"
|
<button class="button button--flat"
|
||||||
@click="copy"
|
@click="copy"
|
||||||
:disabled="$route.path === dest"
|
|
||||||
:aria-label="$t('buttons.copy')"
|
:aria-label="$t('buttons.copy')"
|
||||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,6 +27,7 @@ import { mapState } from 'vuex'
|
|||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
import { files as api } from '@/api'
|
import { files as api } from '@/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
import * as upload from '@/utils/upload'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'copy',
|
name: 'copy',
|
||||||
@@ -42,25 +42,66 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
copy: async function (event) {
|
copy: async function (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
buttons.loading('copy')
|
|
||||||
let items = []
|
let items = []
|
||||||
|
|
||||||
// Create a new promise for each file.
|
// Create a new promise for each file.
|
||||||
for (let item of this.selected) {
|
for (let item of this.selected) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[item].url,
|
from: this.req.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name)
|
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||||
|
name: this.req.items[item].name
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
let action = async (overwrite, rename) => {
|
||||||
await api.copy(items)
|
buttons.loading('copy')
|
||||||
|
|
||||||
|
await api.copy(items, overwrite, rename).then(() => {
|
||||||
buttons.success('copy')
|
buttons.success('copy')
|
||||||
|
|
||||||
|
if (this.$route.path === this.dest) {
|
||||||
|
this.$store.commit('setReload', true)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.$router.push({ path: this.dest })
|
this.$router.push({ path: this.dest })
|
||||||
} catch (e) {
|
}).catch((e) => {
|
||||||
buttons.done('copy')
|
buttons.done('copy')
|
||||||
this.$showError(e)
|
this.$showError(e)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.$route.path === this.dest) {
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
action(false, true)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let dstItems = (await api.fetch(this.dest)).items
|
||||||
|
let conflict = upload.checkConflict(items, dstItems)
|
||||||
|
|
||||||
|
let overwrite = false
|
||||||
|
let rename = false
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit('showHover', {
|
||||||
|
prompt: 'replace-rename',
|
||||||
|
confirm: (event, option) => {
|
||||||
|
overwrite = option == 'overwrite'
|
||||||
|
rename = option == 'rename'
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
action(overwrite, rename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,19 +41,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
// If we're showing this on a listing,
|
|
||||||
// we can use the current request object
|
|
||||||
// to fill the move options.
|
|
||||||
if (this.req.kind === 'listing') {
|
|
||||||
this.fillOptions(this.req)
|
this.fillOptions(this.req)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, we must be on a preview or editor
|
|
||||||
// so we fetch the data from the previous directory.
|
|
||||||
files.fetch(url.removeLastDir(this.$route.path))
|
|
||||||
.then(this.fillOptions)
|
|
||||||
.catch(this.$showError)
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fillOptions (req) {
|
fillOptions (req) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { mapState } from 'vuex'
|
|||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
import { files as api } from '@/api'
|
import { files as api } from '@/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
import * as upload from '@/utils/upload'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'move',
|
name: 'move',
|
||||||
@@ -41,26 +42,51 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
move: async function (event) {
|
move: async function (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
buttons.loading('move')
|
|
||||||
let items = []
|
let items = []
|
||||||
|
|
||||||
for (let item of this.selected) {
|
for (let item of this.selected) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[item].url,
|
from: this.req.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name)
|
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||||
|
name: this.req.items[item].name
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
let action = async (overwrite, rename) => {
|
||||||
api.move(items)
|
buttons.loading('move')
|
||||||
|
|
||||||
|
await api.move(items, overwrite, rename).then(() => {
|
||||||
buttons.success('move')
|
buttons.success('move')
|
||||||
this.$router.push({ path: this.dest })
|
this.$router.push({ path: this.dest })
|
||||||
} catch (e) {
|
}).catch((e) => {
|
||||||
buttons.done('move')
|
buttons.done('move')
|
||||||
this.$showError(e)
|
this.$showError(e)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dstItems = (await api.fetch(this.dest)).items
|
||||||
|
let conflict = upload.checkConflict(items, dstItems)
|
||||||
|
|
||||||
|
let overwrite = false
|
||||||
|
let rename = false
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit('showHover', {
|
||||||
|
prompt: 'replace-rename',
|
||||||
|
confirm: (event, option) => {
|
||||||
|
overwrite = option == 'overwrite'
|
||||||
|
rename = option == 'rename'
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
action(overwrite, rename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import Copy from './Copy'
|
|||||||
import NewFile from './NewFile'
|
import NewFile from './NewFile'
|
||||||
import NewDir from './NewDir'
|
import NewDir from './NewDir'
|
||||||
import Replace from './Replace'
|
import Replace from './Replace'
|
||||||
|
import ReplaceRename from './ReplaceRename'
|
||||||
import Share from './Share'
|
import Share from './Share'
|
||||||
import Upload from './Upload'
|
import Upload from './Upload'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
@@ -35,6 +36,7 @@ export default {
|
|||||||
NewDir,
|
NewDir,
|
||||||
Help,
|
Help,
|
||||||
Replace,
|
Replace,
|
||||||
|
ReplaceRename,
|
||||||
Upload
|
Upload
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
@@ -87,6 +89,7 @@ export default {
|
|||||||
'newDir',
|
'newDir',
|
||||||
'download',
|
'download',
|
||||||
'replace',
|
'replace',
|
||||||
|
'replace-rename',
|
||||||
'share',
|
'share',
|
||||||
'upload'
|
'upload'
|
||||||
].indexOf(this.show) >= 0;
|
].indexOf(this.show) >= 0;
|
||||||
|
|||||||
35
frontend/src/components/prompts/ReplaceRename.vue
Normal file
35
frontend/src/components/prompts/ReplaceRename.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card floating">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('prompts.replace') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="button button--flat button--grey"
|
||||||
|
@click="$store.commit('closeHovers')"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
|
<button class="button button--flat button--blue"
|
||||||
|
@click="(event) => showConfirm(event, 'rename')"
|
||||||
|
:aria-label="$t('buttons.rename')"
|
||||||
|
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
||||||
|
<button class="button button--flat button--red"
|
||||||
|
@click="(event) => showConfirm(event, 'overwrite')"
|
||||||
|
:aria-label="$t('buttons.replace')"
|
||||||
|
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'replace-rename',
|
||||||
|
computed: mapState(['showConfirm'])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -27,9 +27,11 @@ export default {
|
|||||||
name: 'upload',
|
name: 'upload',
|
||||||
methods: {
|
methods: {
|
||||||
uploadFile: function () {
|
uploadFile: function () {
|
||||||
|
document.getElementById('upload-input').value = ''
|
||||||
document.getElementById('upload-input').click()
|
document.getElementById('upload-input').click()
|
||||||
},
|
},
|
||||||
uploadFolder: function () {
|
uploadFolder: function () {
|
||||||
|
document.getElementById('upload-folder-input').value = ''
|
||||||
document.getElementById('upload-folder-input').click()
|
document.getElementById('upload-folder-input').click()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
background: var(--red);
|
background: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--red:hover {
|
.button--blue {
|
||||||
background: var(--dark-red);
|
background: var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--flat {
|
.button--flat {
|
||||||
|
|||||||
@@ -52,6 +52,13 @@
|
|||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#listing .item img {
|
||||||
|
width: 4em;
|
||||||
|
height: 4em;
|
||||||
|
margin-right: 0.1em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
@@ -129,6 +136,11 @@
|
|||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#listing.list .item div:first-of-type img {
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
#listing.list .item div:last-of-type {
|
#listing.list .item div:last-of-type {
|
||||||
width: calc(100% - 3em);
|
width: calc(100% - 3em);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -125,8 +125,13 @@
|
|||||||
height: 3.7em;
|
height: 3.7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#previewer .action:first-of-type {
|
#previewer .bar .title {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
padding: 0 1em;
|
||||||
|
line-height: 2.7em;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#previewer .action i {
|
#previewer .action i {
|
||||||
@@ -184,6 +189,58 @@
|
|||||||
right: 0.5em;
|
right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* EDITOR */
|
||||||
|
|
||||||
|
#editor-container {
|
||||||
|
background-color: #fafafa;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 9999;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container .bar {
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
display: flex;
|
||||||
|
padding: 0.5em;
|
||||||
|
height: 3.7em;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container .title {
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 0 1em;
|
||||||
|
line-height: 2.7em;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewer .title span {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewer .loading {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container #editor {
|
||||||
|
height: calc(100vh - 8.2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container #breadcrumbs {
|
||||||
|
height: 2.3em;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container #breadcrumbs span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* * * * * * * * * * * * * * * *
|
/* * * * * * * * * * * * * * * *
|
||||||
* PROMPT *
|
* PROMPT *
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
"home": "Accueil",
|
"home": "Accueil",
|
||||||
"lastModified": "Dernière modification",
|
"lastModified": "Dernière modification",
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
"lonely": "Il semble qu'il n'y ai rien par ici...",
|
"lonely": "Il semble qu'il n'y ait rien par ici...",
|
||||||
"metadata": "Metadonnées",
|
"metadata": "Metadonnées",
|
||||||
"multipleSelectionEnabled": "Sélection multiple activée",
|
"multipleSelectionEnabled": "Sélection multiple activée",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
|
|||||||
@@ -3,7 +3,16 @@ const getters = {
|
|||||||
isFiles: state => !state.loading && state.route.name === 'Files',
|
isFiles: state => !state.loading && state.route.name === 'Files',
|
||||||
isListing: (state, getters) => getters.isFiles && state.req.isDir,
|
isListing: (state, getters) => getters.isFiles && state.req.isDir,
|
||||||
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
|
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
|
||||||
selectedCount: state => state.selected.length
|
isPreview: state => state.previewMode,
|
||||||
|
selectedCount: state => state.selected.length,
|
||||||
|
progress : state => {
|
||||||
|
if (state.upload.progress.length == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sum = state.upload.progress.reduce((acc, val) => acc + val)
|
||||||
|
return Math.ceil(sum / state.upload.size * 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getters
|
export default getters
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
|||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import mutations from './mutations'
|
import mutations from './mutations'
|
||||||
import getters from './getters'
|
import getters from './getters'
|
||||||
|
import upload from './modules/upload'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
@@ -22,12 +23,14 @@ const state = {
|
|||||||
show: null,
|
show: null,
|
||||||
showShell: false,
|
showShell: false,
|
||||||
showMessage: null,
|
showMessage: null,
|
||||||
showConfirm: null
|
showConfirm: null,
|
||||||
|
previewMode: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
strict: true,
|
strict: true,
|
||||||
state,
|
state,
|
||||||
getters,
|
getters,
|
||||||
mutations
|
mutations,
|
||||||
|
modules: { upload }
|
||||||
})
|
})
|
||||||
|
|||||||
102
frontend/src/store/modules/upload.js
Normal file
102
frontend/src/store/modules/upload.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { files as api } from '@/api'
|
||||||
|
import throttle from 'lodash.throttle'
|
||||||
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
|
const UPLOADS_LIMIT = 5;
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
id: 0,
|
||||||
|
size: 0,
|
||||||
|
progress: [],
|
||||||
|
queue: [],
|
||||||
|
uploads: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
setProgress(state, { id, loaded }) {
|
||||||
|
Vue.set(state.progress, id, loaded)
|
||||||
|
},
|
||||||
|
reset: (state) => {
|
||||||
|
state.id = 0
|
||||||
|
state.size = 0
|
||||||
|
state.progress = []
|
||||||
|
},
|
||||||
|
addJob: (state, item) => {
|
||||||
|
state.queue.push(item)
|
||||||
|
state.size += item.file.size
|
||||||
|
state.id++
|
||||||
|
},
|
||||||
|
moveJob(state) {
|
||||||
|
const item = state.queue[0]
|
||||||
|
state.queue.shift()
|
||||||
|
Vue.set(state.uploads, item.id, item)
|
||||||
|
},
|
||||||
|
removeJob(state, id) {
|
||||||
|
delete state.uploads[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeUnload = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.returnValue = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
upload: (context, item) => {
|
||||||
|
let uploadsCount = Object.keys(context.state.uploads).length;
|
||||||
|
|
||||||
|
let isQueueEmpty = context.state.queue.length == 0
|
||||||
|
let isUploadsEmpty = uploadsCount == 0
|
||||||
|
|
||||||
|
if (isQueueEmpty && isUploadsEmpty) {
|
||||||
|
window.addEventListener('beforeunload', beforeUnload)
|
||||||
|
buttons.loading('upload')
|
||||||
|
}
|
||||||
|
|
||||||
|
context.commit('addJob', item)
|
||||||
|
context.dispatch('processUploads')
|
||||||
|
},
|
||||||
|
finishUpload: (context, item) => {
|
||||||
|
context.commit('setProgress', { id: item.id, loaded: item.file.size })
|
||||||
|
context.commit('removeJob', item.id)
|
||||||
|
context.dispatch('processUploads')
|
||||||
|
},
|
||||||
|
processUploads: async (context) => {
|
||||||
|
let uploadsCount = Object.keys(context.state.uploads).length;
|
||||||
|
|
||||||
|
let isBellowLimit = uploadsCount < UPLOADS_LIMIT
|
||||||
|
let isQueueEmpty = context.state.queue.length == 0
|
||||||
|
let isUploadsEmpty = uploadsCount == 0
|
||||||
|
|
||||||
|
let isFinished = isQueueEmpty && isUploadsEmpty
|
||||||
|
let canProcess = isBellowLimit && !isQueueEmpty
|
||||||
|
|
||||||
|
if (isFinished) {
|
||||||
|
window.removeEventListener('beforeunload', beforeUnload)
|
||||||
|
buttons.success('upload')
|
||||||
|
context.commit('reset')
|
||||||
|
context.commit('setReload', true, { root: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canProcess) {
|
||||||
|
const item = context.state.queue[0];
|
||||||
|
context.commit('moveJob')
|
||||||
|
|
||||||
|
if (item.file.isDir) {
|
||||||
|
await api.post(item.path).catch(Vue.prototype.$showError)
|
||||||
|
} else {
|
||||||
|
let onUpload = throttle(
|
||||||
|
(event) => context.commit('setProgress', { id: item.id, loaded: event.loaded }),
|
||||||
|
100, { leading: true, trailing: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(item.path, item.file, item.overwrite, onUpload).catch(Vue.prototype.$showError)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.dispatch('finishUpload', item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { state, mutations, actions, namespaced: true }
|
||||||
@@ -78,13 +78,14 @@ const mutations = {
|
|||||||
updateClipboard: (state, value) => {
|
updateClipboard: (state, value) => {
|
||||||
state.clipboard.key = value.key
|
state.clipboard.key = value.key
|
||||||
state.clipboard.items = value.items
|
state.clipboard.items = value.items
|
||||||
|
state.clipboard.path = value.path
|
||||||
},
|
},
|
||||||
resetClipboard: (state) => {
|
resetClipboard: (state) => {
|
||||||
state.clipboard.key = ''
|
state.clipboard.key = ''
|
||||||
state.clipboard.items = []
|
state.clipboard.items = []
|
||||||
},
|
},
|
||||||
setProgress: (state, value) => {
|
setPreviewMode(state, value) {
|
||||||
state.progress = value
|
state.previewMode = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const noAuth = window.FileBrowser.NoAuth
|
|||||||
const authMethod = window.FileBrowser.AuthMethod
|
const authMethod = window.FileBrowser.AuthMethod
|
||||||
const loginPage = window.FileBrowser.LoginPage
|
const loginPage = window.FileBrowser.LoginPage
|
||||||
const theme = window.FileBrowser.Theme
|
const theme = window.FileBrowser.Theme
|
||||||
|
const enableThumbs = window.FileBrowser.EnableThumbs
|
||||||
|
const resizePreview = window.FileBrowser.ResizePreview
|
||||||
|
|
||||||
export {
|
export {
|
||||||
name,
|
name,
|
||||||
@@ -24,5 +26,7 @@ export {
|
|||||||
noAuth,
|
noAuth,
|
||||||
authMethod,
|
authMethod,
|
||||||
loginPage,
|
loginPage,
|
||||||
theme
|
theme,
|
||||||
|
enableThumbs,
|
||||||
|
resizePreview
|
||||||
}
|
}
|
||||||
|
|||||||
130
frontend/src/utils/upload.js
Normal file
130
frontend/src/utils/upload.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import store from '@/store'
|
||||||
|
import url from '@/utils/url'
|
||||||
|
|
||||||
|
export function checkConflict(files, items) {
|
||||||
|
if (typeof items === 'undefined' || items === null) {
|
||||||
|
items = []
|
||||||
|
}
|
||||||
|
|
||||||
|
let folder_upload = files[0].fullPath !== undefined
|
||||||
|
|
||||||
|
let conflict = false
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
let file = files[i]
|
||||||
|
let name = file.name
|
||||||
|
|
||||||
|
if (folder_upload) {
|
||||||
|
let dirs = file.fullPath.split("/")
|
||||||
|
if (dirs.length > 1) {
|
||||||
|
name = dirs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = items.findIndex(function hasConflict(element) {
|
||||||
|
return (element.name === this)
|
||||||
|
}, name)
|
||||||
|
|
||||||
|
if (res >= 0) {
|
||||||
|
conflict = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflict
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scanFiles(dt) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let reading = 0
|
||||||
|
const contents = []
|
||||||
|
|
||||||
|
if (dt.items !== undefined) {
|
||||||
|
for (let item of dt.items) {
|
||||||
|
if (item.kind === "file" && typeof item.webkitGetAsEntry === "function") {
|
||||||
|
const entry = item.webkitGetAsEntry()
|
||||||
|
readEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(dt.files)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEntry(entry, directory = "") {
|
||||||
|
if (entry.isFile) {
|
||||||
|
reading++
|
||||||
|
entry.file(file => {
|
||||||
|
reading--
|
||||||
|
|
||||||
|
file.fullPath = `${directory}${file.name}`
|
||||||
|
contents.push(file)
|
||||||
|
|
||||||
|
if (reading === 0) {
|
||||||
|
resolve(contents)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (entry.isDirectory) {
|
||||||
|
const dir = {
|
||||||
|
isDir: true,
|
||||||
|
size: 0,
|
||||||
|
fullPath: `${directory}${entry.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
contents.push(dir)
|
||||||
|
|
||||||
|
readReaderContent(entry.createReader(), `${directory}${entry.name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readReaderContent(reader, directory) {
|
||||||
|
reading++
|
||||||
|
|
||||||
|
reader.readEntries(function (entries) {
|
||||||
|
reading--
|
||||||
|
if (entries.length > 0) {
|
||||||
|
for (const entry of entries) {
|
||||||
|
readEntry(entry, `${directory}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
readReaderContent(reader, `${directory}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reading === 0) {
|
||||||
|
resolve(contents)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleFiles(files, path, overwrite = false) {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
let file = files[i]
|
||||||
|
|
||||||
|
let filename = (file.fullPath !== undefined) ? file.fullPath : file.name
|
||||||
|
let filenameEncoded = url.encodeRFC5987ValueChars(filename)
|
||||||
|
|
||||||
|
let id = store.state.upload.id
|
||||||
|
|
||||||
|
let itemPath = path + filenameEncoded
|
||||||
|
|
||||||
|
if (file.isDir) {
|
||||||
|
itemPath = path
|
||||||
|
let folders = file.fullPath.split("/")
|
||||||
|
|
||||||
|
for (let i = 0; i < folders.length; i++) {
|
||||||
|
let folder = folders[i]
|
||||||
|
let folderEncoded = encodeURIComponent(folder)
|
||||||
|
itemPath += folderEncoded + "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id,
|
||||||
|
path: itemPath,
|
||||||
|
file,
|
||||||
|
overwrite
|
||||||
|
}
|
||||||
|
|
||||||
|
store.dispatch('upload/upload', item);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,12 @@ function encodeRFC5987ValueChars(str) {
|
|||||||
replace(/%(?:7C|60|5E)/g, unescape);
|
replace(/%(?:7C|60|5E)/g, unescape);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encodePath(str) {
|
||||||
|
return str.split('/').map(v => encodeURIComponent(v)).join('/')
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
encodeRFC5987ValueChars: encodeRFC5987ValueChars,
|
encodeRFC5987ValueChars: encodeRFC5987ValueChars,
|
||||||
removeLastDir: removeLastDir
|
removeLastDir: removeLastDir,
|
||||||
|
encodePath: encodePath
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Noty from 'noty'
|
import Noty from 'noty'
|
||||||
|
import VueLazyload from 'vue-lazyload'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import { disableExternal } from '@/utils/constants'
|
import { disableExternal } from '@/utils/constants'
|
||||||
|
|
||||||
|
Vue.use(VueLazyload)
|
||||||
|
|
||||||
Vue.config.productionTip = true
|
Vue.config.productionTip = true
|
||||||
|
|
||||||
const notyDefault = {
|
const notyDefault = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div id="breadcrumbs">
|
<div id="breadcrumbs" v-if="isListing || error">
|
||||||
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||||
<i class="material-icons">home</i>
|
<i class="material-icons">home</i>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -10,14 +10,15 @@
|
|||||||
<router-link :to="link.url">{{ link.name }}</router-link>
|
<router-link :to="link.url">{{ link.name }}</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error">
|
<div v-if="error">
|
||||||
<not-found v-if="error.message === '404'"></not-found>
|
<not-found v-if="error.message === '404'"></not-found>
|
||||||
<forbidden v-else-if="error.message === '403'"></forbidden>
|
<forbidden v-else-if="error.message === '403'"></forbidden>
|
||||||
<internal-error v-else></internal-error>
|
<internal-error v-else></internal-error>
|
||||||
</div>
|
</div>
|
||||||
|
<preview v-else-if="isPreview"></preview>
|
||||||
<editor v-else-if="isEditor"></editor>
|
<editor v-else-if="isEditor"></editor>
|
||||||
<listing :class="{ multiple }" v-else-if="isListing"></listing>
|
<listing :class="{ multiple }" v-else-if="isListing"></listing>
|
||||||
<preview v-else-if="isPreview"></preview>
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h2 class="message">
|
<h2 class="message">
|
||||||
<span>{{ $t('files.loading') }}</span>
|
<span>{{ $t('files.loading') }}</span>
|
||||||
@@ -61,10 +62,11 @@ export default {
|
|||||||
'user',
|
'user',
|
||||||
'reload',
|
'reload',
|
||||||
'multiple',
|
'multiple',
|
||||||
'loading'
|
'loading',
|
||||||
|
'show'
|
||||||
]),
|
]),
|
||||||
isPreview () {
|
isPreview () {
|
||||||
return !this.loading && !this.isListing && !this.isEditor
|
return !this.loading && !this.isListing && !this.isEditor || this.loading && this.$store.state.previewMode
|
||||||
},
|
},
|
||||||
breadcrumbs () {
|
breadcrumbs () {
|
||||||
let parts = this.$route.path.split('/')
|
let parts = this.$route.path.split('/')
|
||||||
@@ -158,10 +160,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyEvent (event) {
|
keyEvent (event) {
|
||||||
|
if (this.show !== null) {
|
||||||
// Esc!
|
// Esc!
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
this.$store.commit('closeHovers')
|
this.$store.commit('closeHovers')
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esc!
|
||||||
|
if (event.keyCode === 27) {
|
||||||
// If we're on a listing, unselect all
|
// If we're on a listing, unselect all
|
||||||
// files and folders.
|
// files and folders.
|
||||||
if (this.isListing) {
|
if (this.isListing) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div id="progress">
|
<div id="progress">
|
||||||
<div v-bind:style="{ width: $store.state.progress + '%' }"></div>
|
<div v-bind:style="{ width: this.progress + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<site-header></site-header>
|
<site-header></site-header>
|
||||||
<sidebar></sidebar>
|
<sidebar></sidebar>
|
||||||
@@ -29,7 +29,7 @@ export default {
|
|||||||
Shell
|
Shell
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters([ 'isLogged' ]),
|
...mapGetters([ 'isLogged', 'progress' ]),
|
||||||
...mapState([ 'user' ])
|
...mapState([ 'user' ])
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
runtimeCompiler: true,
|
runtimeCompiler: true,
|
||||||
publicPath: '[{[ .StaticURL ]}]'
|
publicPath: '[{[ .StaticURL ]}]',
|
||||||
|
parallel: 2,
|
||||||
}
|
}
|
||||||
5
go.mod
5
go.mod
@@ -8,12 +8,13 @@ require (
|
|||||||
github.com/caddyserver/caddy v1.0.3
|
github.com/caddyserver/caddy v1.0.3
|
||||||
github.com/daaku/go.zipexe v1.0.1 // indirect
|
github.com/daaku/go.zipexe v1.0.1 // indirect
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
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/dsnet/compress v0.0.1 // indirect
|
||||||
github.com/golang/snappy v0.0.1 // indirect
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
github.com/gorilla/mux v1.7.3
|
github.com/gorilla/mux v1.7.3
|
||||||
github.com/gorilla/websocket v1.4.1
|
github.com/gorilla/websocket v1.4.1
|
||||||
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1
|
|
||||||
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1
|
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1
|
||||||
|
github.com/marusama/semaphore/v2 v2.4.1
|
||||||
github.com/mholt/archiver v3.1.1+incompatible
|
github.com/mholt/archiver v3.1.1+incompatible
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/nwaples/rardecode v1.0.0 // indirect
|
github.com/nwaples/rardecode v1.0.0 // indirect
|
||||||
@@ -24,11 +25,13 @@ require (
|
|||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.6.1
|
github.com/spf13/viper v1.6.1
|
||||||
|
github.com/stretchr/testify v1.6.1
|
||||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
||||||
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
go.etcd.io/bbolt v1.3.3
|
go.etcd.io/bbolt v1.3.3
|
||||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
|
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
|
||||||
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b // indirect
|
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b // indirect
|
||||||
golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect
|
golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect
|
||||||
golang.org/x/text v0.3.2 // indirect
|
golang.org/x/text v0.3.2 // indirect
|
||||||
|
|||||||
12
go.sum
12
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 h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
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/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 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
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=
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
@@ -85,8 +87,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
|||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1 h1:2MkEawJQTmAr6YI7T7j7SKxdTmYJOcaJZfzeVPr56PM=
|
|
||||||
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1/go.mod h1:lwnswzFVSy7B/k81M5rOLUU0fOBKHrDRIkPIBZd7PBo=
|
|
||||||
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
|
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
|
||||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||||
github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
@@ -127,6 +127,8 @@ github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNA
|
|||||||
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
|
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
|
||||||
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1 h1:PEhRT94KBTY4E0KdCYmhvDGWjSFBxc68j2M6PMRix8U=
|
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1 h1:PEhRT94KBTY4E0KdCYmhvDGWjSFBxc68j2M6PMRix8U=
|
||||||
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1/go.mod h1:wI697HNhDFM/vBruYM3ckbszQ2+DOIeH9qdBKMdf288=
|
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1/go.mod h1:wI697HNhDFM/vBruYM3ckbszQ2+DOIeH9qdBKMdf288=
|
||||||
|
github.com/marusama/semaphore/v2 v2.4.1 h1:Y29DhhFMvreVgoqF9EtaSJAF9t2E7Sk7i5VW81sqB8I=
|
||||||
|
github.com/marusama/semaphore/v2 v2.4.1/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
|
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
|
||||||
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
|
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
|
||||||
@@ -204,6 +206,8 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
|
|||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
@@ -239,6 +243,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-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 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
|
||||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
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-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/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
|
||||||
@@ -314,4 +320,6 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
|||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.Use
|
|||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "cty")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
if _, err := w.Write([]byte(signed)); err != nil {
|
if _, err := w.Write([]byte(signed)); err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type modifyRequest struct {
|
|||||||
Which []string `json:"which"` // Answer to: which fields?
|
Which []string `json:"which"` // Answer to: which fields?
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler, error) {
|
func NewHandler(imgSvc ImgService, fileCache FileCache, store *storage.Storage, server *settings.Server) (http.Handler, error) {
|
||||||
server.Clean()
|
server.Clean()
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
@@ -46,7 +46,7 @@ func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler,
|
|||||||
users.Handle("/{id:[0-9]+}", monkey(userDeleteHandler, "")).Methods("DELETE")
|
users.Handle("/{id:[0-9]+}", monkey(userDeleteHandler, "")).Methods("DELETE")
|
||||||
|
|
||||||
api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET")
|
api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET")
|
||||||
api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler, "/api/resources")).Methods("DELETE")
|
api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler(fileCache), "/api/resources")).Methods("DELETE")
|
||||||
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("POST")
|
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("POST")
|
||||||
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("PUT")
|
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("PUT")
|
||||||
api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler, "/api/resources")).Methods("PATCH")
|
api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler, "/api/resources")).Methods("PATCH")
|
||||||
@@ -59,6 +59,8 @@ func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler,
|
|||||||
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
|
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
|
||||||
|
|
||||||
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
|
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
|
||||||
|
api.PathPrefix("/preview/{size}/{path:.*}").
|
||||||
|
Handler(monkey(previewHandler(imgSvc, fileCache, server.EnableThumbnails, server.ResizePreview), "/api/preview")).Methods("GET")
|
||||||
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
|
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
|
||||||
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")
|
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")
|
||||||
|
|
||||||
|
|||||||
137
http/preview.go
Normal file
137
http/preview.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
//go:generate go-enum --sql --marshal --names --file $GOFILE
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/img"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
ENUM(
|
||||||
|
thumb
|
||||||
|
big
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
type PreviewSize int
|
||||||
|
|
||||||
|
type ImgService interface {
|
||||||
|
FormatFromExtension(ext string) (img.Format, error)
|
||||||
|
Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...img.Option) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileCache interface {
|
||||||
|
Store(ctx context.Context, key string, value []byte) error
|
||||||
|
Load(ctx context.Context, key string) ([]byte, bool, error)
|
||||||
|
Delete(ctx context.Context, key string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, resizePreview bool) handleFunc {
|
||||||
|
return 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)
|
||||||
|
|
||||||
|
previewSize, err := ParsePreviewSize(vars["size"])
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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, imgSvc, fileCache, file, previewSize, enableThumbnails, resizePreview)
|
||||||
|
default:
|
||||||
|
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", file.Type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleImagePreview(w http.ResponseWriter, r *http.Request, imgSvc ImgService, fileCache FileCache,
|
||||||
|
file *files.FileInfo, previewSize PreviewSize, enableThumbnails, resizePreview bool) (int, error) {
|
||||||
|
format, err := imgSvc.FormatFromExtension(file.Extension)
|
||||||
|
if err != nil {
|
||||||
|
// Unsupported extensions directly return the raw data
|
||||||
|
if err == img.ErrUnsupportedFormat {
|
||||||
|
return rawFileHandler(w, r, file)
|
||||||
|
}
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey := previewCacheKey(file.Path, previewSize)
|
||||||
|
cachedFile, ok, err := fileCache.Load(r.Context(), cacheKey)
|
||||||
|
if err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
_, _ = w.Write(cachedFile)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := file.Fs.Open(file.Path)
|
||||||
|
if err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
var (
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
options []img.Option
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case previewSize == PreviewSizeBig && resizePreview && format != img.FormatGif:
|
||||||
|
width = 1080
|
||||||
|
height = 1080
|
||||||
|
options = append(options, img.WithMode(img.ResizeModeFit), img.WithQuality(img.QualityMedium))
|
||||||
|
case previewSize == PreviewSizeThumb && enableThumbnails:
|
||||||
|
width = 128
|
||||||
|
height = 128
|
||||||
|
options = append(options, img.WithMode(img.ResizeModeFill), img.WithQuality(img.QualityLow), img.WithFormat(img.FormatJpeg))
|
||||||
|
default:
|
||||||
|
if _, err := rawFileHandler(w, r, file); err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
if err := imgSvc.Resize(context.Background(), fd, width, height, buf, options...); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := fileCache.Store(context.Background(), cacheKey, buf.Bytes()); err != nil {
|
||||||
|
fmt.Printf("failed to cache resized image: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, _ = w.Write(buf.Bytes())
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewCacheKey(fPath string, previewSize PreviewSize) string {
|
||||||
|
return fPath + previewSize.String()
|
||||||
|
}
|
||||||
100
http/preview_enum.go
Normal file
100
http/preview_enum.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Code generated by go-enum
|
||||||
|
// DO NOT EDIT!
|
||||||
|
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PreviewSizeThumb is a PreviewSize of type Thumb
|
||||||
|
PreviewSizeThumb PreviewSize = iota
|
||||||
|
// PreviewSizeBig is a PreviewSize of type Big
|
||||||
|
PreviewSizeBig
|
||||||
|
)
|
||||||
|
|
||||||
|
const _PreviewSizeName = "thumbbig"
|
||||||
|
|
||||||
|
var _PreviewSizeNames = []string{
|
||||||
|
_PreviewSizeName[0:5],
|
||||||
|
_PreviewSizeName[5:8],
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreviewSizeNames returns a list of possible string values of PreviewSize.
|
||||||
|
func PreviewSizeNames() []string {
|
||||||
|
tmp := make([]string, len(_PreviewSizeNames))
|
||||||
|
copy(tmp, _PreviewSizeNames)
|
||||||
|
return tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
var _PreviewSizeMap = map[PreviewSize]string{
|
||||||
|
0: _PreviewSizeName[0:5],
|
||||||
|
1: _PreviewSizeName[5:8],
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface.
|
||||||
|
func (x PreviewSize) String() string {
|
||||||
|
if str, ok := _PreviewSizeMap[x]; ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("PreviewSize(%d)", x)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _PreviewSizeValue = map[string]PreviewSize{
|
||||||
|
_PreviewSizeName[0:5]: 0,
|
||||||
|
_PreviewSizeName[5:8]: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePreviewSize attempts to convert a string to a PreviewSize
|
||||||
|
func ParsePreviewSize(name string) (PreviewSize, error) {
|
||||||
|
if x, ok := _PreviewSizeValue[name]; ok {
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
return PreviewSize(0), fmt.Errorf("%s is not a valid PreviewSize, try [%s]", name, strings.Join(_PreviewSizeNames, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the text marshaller method
|
||||||
|
func (x PreviewSize) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the text unmarshaller method
|
||||||
|
func (x *PreviewSize) UnmarshalText(text []byte) error {
|
||||||
|
name := string(text)
|
||||||
|
tmp, err := ParsePreviewSize(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (x *PreviewSize) Scan(value interface{}) error {
|
||||||
|
var name string
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
name = v
|
||||||
|
case []byte:
|
||||||
|
name = string(v)
|
||||||
|
case nil:
|
||||||
|
*x = PreviewSize(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := ParsePreviewSize(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (x PreviewSize) Value() (driver.Value, error) {
|
||||||
|
return x.String(), nil
|
||||||
|
}
|
||||||
40
http/raw.go
40
http/raw.go
@@ -4,16 +4,24 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
gopath "path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hacdias/fileutils"
|
|
||||||
"github.com/mholt/archiver"
|
"github.com/mholt/archiver"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/fileutils"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func slashClean(name string) string {
|
||||||
|
if name == "" || name[0] != '/' {
|
||||||
|
name = "/" + name
|
||||||
|
}
|
||||||
|
return gopath.Clean(name)
|
||||||
|
}
|
||||||
|
|
||||||
func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]string, error) {
|
func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]string, error) {
|
||||||
var fileSlice []string
|
var fileSlice []string
|
||||||
names := strings.Split(r.URL.Query().Get("files"), ",")
|
names := strings.Split(r.URL.Query().Get("files"), ",")
|
||||||
@@ -27,7 +35,7 @@ func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]strin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
name = fileutils.SlashClean(name)
|
name = slashClean(name)
|
||||||
fileSlice = append(fileSlice, filepath.Join(f.Path, name))
|
fileSlice = append(fileSlice, filepath.Join(f.Path, name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,6 +66,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) {
|
var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
if !d.user.Perm.Download {
|
if !d.user.Perm.Download {
|
||||||
return http.StatusAccepted, nil
|
return http.StatusAccepted, nil
|
||||||
@@ -81,7 +98,7 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
|
|||||||
return rawDirHandler(w, r, d, file)
|
return rawDirHandler(w, r, d, file)
|
||||||
})
|
})
|
||||||
|
|
||||||
func addFile(ar archiver.Writer, d *data, path string) error {
|
func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
|
||||||
// Checks are always done with paths with "/" as path separator.
|
// Checks are always done with paths with "/" as path separator.
|
||||||
path = strings.Replace(path, "\\", "/", -1)
|
path = strings.Replace(path, "\\", "/", -1)
|
||||||
if !d.Check(path) {
|
if !d.Check(path) {
|
||||||
@@ -99,10 +116,12 @@ func addFile(ar archiver.Writer, d *data, path string) error {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
|
filename := strings.TrimPrefix(path, commonPath)
|
||||||
|
filename = strings.TrimPrefix(filename, "/")
|
||||||
err = ar.Write(archiver.File{
|
err = ar.Write(archiver.File{
|
||||||
FileInfo: archiver.FileInfo{
|
FileInfo: archiver.FileInfo{
|
||||||
FileInfo: info,
|
FileInfo: info,
|
||||||
CustomName: strings.TrimPrefix(path, "/"),
|
CustomName: filename,
|
||||||
},
|
},
|
||||||
ReadCloser: file,
|
ReadCloser: file,
|
||||||
})
|
})
|
||||||
@@ -117,7 +136,7 @@ func addFile(ar archiver.Writer, d *data, path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
err = addFile(ar, d, filepath.Join(path, name))
|
err = addFile(ar, d, filepath.Join(path, name), commonPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -151,8 +170,10 @@ func rawDirHandler(w http.ResponseWriter, r *http.Request, d *data, file *files.
|
|||||||
}
|
}
|
||||||
defer ar.Close()
|
defer ar.Close()
|
||||||
|
|
||||||
|
commonDir := fileutils.CommonPrefix('/', filenames...)
|
||||||
|
|
||||||
for _, fname := range filenames {
|
for _, fname := range filenames {
|
||||||
err = addFile(ar, d, fname)
|
err = addFile(ar, d, fname, commonDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
@@ -168,12 +189,7 @@ func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo
|
|||||||
}
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
if r.URL.Query().Get("inline") == "true" {
|
setContentDisposition(w, r, file)
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
http.ServeContent(w, r, file.Name, file.ModTime, fd)
|
http.ServeContent(w, r, file.Name, file.ModTime, fd)
|
||||||
return 0, nil
|
return 0, nil
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
"github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
"github.com/filebrowser/filebrowser/v2/fileutils"
|
"github.com/filebrowser/filebrowser/v2/fileutils"
|
||||||
@@ -48,12 +50,32 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
|
|||||||
return renderJSON(w, r, file)
|
return renderJSON(w, r, file)
|
||||||
})
|
})
|
||||||
|
|
||||||
var resourceDeleteHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
func resourceDeleteHandler(fileCache FileCache) handleFunc {
|
||||||
|
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
if r.URL.Path == "/" || !d.user.Perm.Delete {
|
if r.URL.Path == "/" || !d.user.Perm.Delete {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := d.RunHook(func() error {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete thumbnails
|
||||||
|
for _, previewSizeName := range PreviewSizeNames() {
|
||||||
|
size, _ := ParsePreviewSize(previewSizeName)
|
||||||
|
if err := fileCache.Delete(r.Context(), previewCacheKey(file.Path, size)); err != nil { //nolint:govet
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.RunHook(func() error {
|
||||||
return d.user.Fs.RemoveAll(r.URL.Path)
|
return d.user.Fs.RemoveAll(r.URL.Path)
|
||||||
}, "delete", r.URL.Path, "", d.user)
|
}, "delete", r.URL.Path, "", d.user)
|
||||||
|
|
||||||
@@ -62,7 +84,8 @@ var resourceDeleteHandler = withUser(func(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
if !d.user.Perm.Create && r.Method == http.MethodPost {
|
if !d.user.Perm.Create && r.Method == http.MethodPost {
|
||||||
@@ -127,6 +150,10 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
|
|||||||
return nil
|
return nil
|
||||||
}, action, r.URL.Path, "", d.user)
|
}, action, r.URL.Path, "", d.user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
_ = d.user.Fs.RemoveAll(r.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -135,14 +162,26 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
|
|||||||
dst := r.URL.Query().Get("destination")
|
dst := r.URL.Query().Get("destination")
|
||||||
action := r.URL.Query().Get("action")
|
action := r.URL.Query().Get("action")
|
||||||
dst, err := url.QueryUnescape(dst)
|
dst, err := url.QueryUnescape(dst)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
|
|
||||||
if dst == "/" || src == "/" {
|
if dst == "/" || src == "/" {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
if err = checkParent(src, dst); err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
override := r.URL.Query().Get("override") == "true"
|
||||||
|
rename := r.URL.Query().Get("rename") == "true"
|
||||||
|
if !override && !rename {
|
||||||
|
if _, err = d.user.Fs.Stat(dst); err == nil {
|
||||||
|
return http.StatusConflict, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rename {
|
||||||
|
dst = addVersionSuffix(dst, d.user.Fs)
|
||||||
|
}
|
||||||
|
|
||||||
err = d.RunHook(func() error {
|
err = d.RunHook(func() error {
|
||||||
switch action {
|
switch action {
|
||||||
@@ -151,11 +190,14 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
|
|||||||
if !d.user.Perm.Create {
|
if !d.user.Perm.Create {
|
||||||
return errors.ErrPermissionDenied
|
return errors.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileutils.Copy(d.user.Fs, src, dst)
|
return fileutils.Copy(d.user.Fs, src, dst)
|
||||||
case "rename":
|
case "rename":
|
||||||
if !d.user.Perm.Rename {
|
if !d.user.Perm.Rename {
|
||||||
return errors.ErrPermissionDenied
|
return errors.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
dst = filepath.Clean("/" + dst)
|
||||||
|
|
||||||
return d.user.Fs.Rename(src, dst)
|
return d.user.Fs.Rename(src, dst)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
||||||
@@ -164,3 +206,35 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
|
|||||||
|
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
})
|
})
|
||||||
|
|
||||||
|
func checkParent(src, dst string) error {
|
||||||
|
rel, err := filepath.Rel(src, dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rel = filepath.ToSlash(rel)
|
||||||
|
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
|
||||||
|
return errors.ErrSourceIsParent
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addVersionSuffix(path string, fs afero.Fs) string {
|
||||||
|
counter := 1
|
||||||
|
dir, name := filepath.Split(path)
|
||||||
|
ext := filepath.Ext(name)
|
||||||
|
base := strings.TrimSuffix(name, ext)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if _, err := fs.Stat(path); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
|
||||||
|
path = filepath.ToSlash(dir) + renamed
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, box *
|
|||||||
"CSS": false,
|
"CSS": false,
|
||||||
"ReCaptcha": false,
|
"ReCaptcha": false,
|
||||||
"Theme": d.settings.Branding.Theme,
|
"Theme": d.settings.Branding.Theme,
|
||||||
|
"EnableThumbs": d.server.EnableThumbnails,
|
||||||
|
"ResizePreview": d.server.ResizePreview,
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.settings.Branding.Files != "" {
|
if d.settings.Branding.Files != "" {
|
||||||
|
|||||||
185
img/service.go
Normal file
185
img/service.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
//go:generate go-enum --sql --marshal --file $GOFILE
|
||||||
|
package img
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/marusama/semaphore/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrUnsupportedFormat means the given image format is not supported.
|
||||||
|
var ErrUnsupportedFormat = errors.New("unsupported image format")
|
||||||
|
|
||||||
|
// Service
|
||||||
|
type Service struct {
|
||||||
|
sem semaphore.Semaphore
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(workers int) *Service {
|
||||||
|
return &Service{
|
||||||
|
sem: semaphore.New(workers),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format is an image file format.
|
||||||
|
/*
|
||||||
|
ENUM(
|
||||||
|
jpeg
|
||||||
|
png
|
||||||
|
gif
|
||||||
|
tiff
|
||||||
|
bmp
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
type Format int
|
||||||
|
|
||||||
|
func (x Format) toImaging() imaging.Format {
|
||||||
|
switch x {
|
||||||
|
case FormatJpeg:
|
||||||
|
return imaging.JPEG
|
||||||
|
case FormatPng:
|
||||||
|
return imaging.PNG
|
||||||
|
case FormatGif:
|
||||||
|
return imaging.GIF
|
||||||
|
case FormatTiff:
|
||||||
|
return imaging.TIFF
|
||||||
|
case FormatBmp:
|
||||||
|
return imaging.BMP
|
||||||
|
default:
|
||||||
|
return imaging.JPEG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ENUM(
|
||||||
|
high
|
||||||
|
medium
|
||||||
|
low
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
type Quality int
|
||||||
|
|
||||||
|
func (x Quality) resampleFilter() imaging.ResampleFilter {
|
||||||
|
switch x {
|
||||||
|
case QualityHigh:
|
||||||
|
return imaging.Lanczos
|
||||||
|
case QualityMedium:
|
||||||
|
return imaging.Box
|
||||||
|
case QualityLow:
|
||||||
|
return imaging.NearestNeighbor
|
||||||
|
default:
|
||||||
|
return imaging.Box
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ENUM(
|
||||||
|
fit
|
||||||
|
fill
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
type ResizeMode int
|
||||||
|
|
||||||
|
func (s *Service) FormatFromExtension(ext string) (Format, error) {
|
||||||
|
format, err := imaging.FormatFromExtension(ext)
|
||||||
|
if err != nil {
|
||||||
|
return -1, ErrUnsupportedFormat
|
||||||
|
}
|
||||||
|
switch format {
|
||||||
|
case imaging.JPEG:
|
||||||
|
return FormatJpeg, nil
|
||||||
|
case imaging.PNG:
|
||||||
|
return FormatPng, nil
|
||||||
|
case imaging.GIF:
|
||||||
|
return FormatGif, nil
|
||||||
|
case imaging.TIFF:
|
||||||
|
return FormatTiff, nil
|
||||||
|
case imaging.BMP:
|
||||||
|
return FormatBmp, nil
|
||||||
|
}
|
||||||
|
return -1, ErrUnsupportedFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
type resizeConfig struct {
|
||||||
|
format Format
|
||||||
|
resizeMode ResizeMode
|
||||||
|
quality Quality
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*resizeConfig)
|
||||||
|
|
||||||
|
func WithFormat(format Format) Option {
|
||||||
|
return func(config *resizeConfig) {
|
||||||
|
config.format = format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMode(mode ResizeMode) Option {
|
||||||
|
return func(config *resizeConfig) {
|
||||||
|
config.resizeMode = mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithQuality(quality Quality) Option {
|
||||||
|
return func(config *resizeConfig) {
|
||||||
|
config.quality = quality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...Option) error {
|
||||||
|
if err := s.sem.Acquire(ctx, 1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.sem.Release(1)
|
||||||
|
|
||||||
|
format, wrappedReader, err := s.detectFormat(in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := resizeConfig{
|
||||||
|
format: format,
|
||||||
|
resizeMode: ResizeModeFit,
|
||||||
|
quality: QualityMedium,
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
option(&config)
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := imaging.Decode(wrappedReader, imaging.AutoOrientation(true))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch config.resizeMode {
|
||||||
|
case ResizeModeFill:
|
||||||
|
img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter())
|
||||||
|
default:
|
||||||
|
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
|
||||||
|
}
|
||||||
|
|
||||||
|
return imaging.Encode(out, img, config.format.toImaging())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) detectFormat(in io.Reader) (Format, io.Reader, error) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
r := io.TeeReader(in, buf)
|
||||||
|
|
||||||
|
_, imgFormat, err := image.DecodeConfig(r)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("%s: %w", err.Error(), ErrUnsupportedFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
format, err := ParseFormat(imgFormat)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, ErrUnsupportedFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
return format, io.MultiReader(buf, in), nil
|
||||||
|
}
|
||||||
259
img/service_enum.go
Normal file
259
img/service_enum.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
// Code generated by go-enum
|
||||||
|
// DO NOT EDIT!
|
||||||
|
|
||||||
|
package img
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FormatJpeg is a Format of type Jpeg
|
||||||
|
FormatJpeg Format = iota
|
||||||
|
// FormatPng is a Format of type Png
|
||||||
|
FormatPng
|
||||||
|
// FormatGif is a Format of type Gif
|
||||||
|
FormatGif
|
||||||
|
// FormatTiff is a Format of type Tiff
|
||||||
|
FormatTiff
|
||||||
|
// FormatBmp is a Format of type Bmp
|
||||||
|
FormatBmp
|
||||||
|
)
|
||||||
|
|
||||||
|
const _FormatName = "jpegpnggiftiffbmp"
|
||||||
|
|
||||||
|
var _FormatMap = map[Format]string{
|
||||||
|
0: _FormatName[0:4],
|
||||||
|
1: _FormatName[4:7],
|
||||||
|
2: _FormatName[7:10],
|
||||||
|
3: _FormatName[10:14],
|
||||||
|
4: _FormatName[14:17],
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface.
|
||||||
|
func (x Format) String() string {
|
||||||
|
if str, ok := _FormatMap[x]; ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Format(%d)", x)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _FormatValue = map[string]Format{
|
||||||
|
_FormatName[0:4]: 0,
|
||||||
|
_FormatName[4:7]: 1,
|
||||||
|
_FormatName[7:10]: 2,
|
||||||
|
_FormatName[10:14]: 3,
|
||||||
|
_FormatName[14:17]: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFormat attempts to convert a string to a Format
|
||||||
|
func ParseFormat(name string) (Format, error) {
|
||||||
|
if x, ok := _FormatValue[name]; ok {
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
return Format(0), fmt.Errorf("%s is not a valid Format", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the text marshaller method
|
||||||
|
func (x Format) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the text unmarshaller method
|
||||||
|
func (x *Format) UnmarshalText(text []byte) error {
|
||||||
|
name := string(text)
|
||||||
|
tmp, err := ParseFormat(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (x *Format) Scan(value interface{}) error {
|
||||||
|
var name string
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
name = v
|
||||||
|
case []byte:
|
||||||
|
name = string(v)
|
||||||
|
case nil:
|
||||||
|
*x = Format(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := ParseFormat(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (x Format) Value() (driver.Value, error) {
|
||||||
|
return x.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// QualityHigh is a Quality of type High
|
||||||
|
QualityHigh Quality = iota
|
||||||
|
// QualityMedium is a Quality of type Medium
|
||||||
|
QualityMedium
|
||||||
|
// QualityLow is a Quality of type Low
|
||||||
|
QualityLow
|
||||||
|
)
|
||||||
|
|
||||||
|
const _QualityName = "highmediumlow"
|
||||||
|
|
||||||
|
var _QualityMap = map[Quality]string{
|
||||||
|
0: _QualityName[0:4],
|
||||||
|
1: _QualityName[4:10],
|
||||||
|
2: _QualityName[10:13],
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface.
|
||||||
|
func (x Quality) String() string {
|
||||||
|
if str, ok := _QualityMap[x]; ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Quality(%d)", x)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _QualityValue = map[string]Quality{
|
||||||
|
_QualityName[0:4]: 0,
|
||||||
|
_QualityName[4:10]: 1,
|
||||||
|
_QualityName[10:13]: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseQuality attempts to convert a string to a Quality
|
||||||
|
func ParseQuality(name string) (Quality, error) {
|
||||||
|
if x, ok := _QualityValue[name]; ok {
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
return Quality(0), fmt.Errorf("%s is not a valid Quality", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the text marshaller method
|
||||||
|
func (x Quality) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the text unmarshaller method
|
||||||
|
func (x *Quality) UnmarshalText(text []byte) error {
|
||||||
|
name := string(text)
|
||||||
|
tmp, err := ParseQuality(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (x *Quality) Scan(value interface{}) error {
|
||||||
|
var name string
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
name = v
|
||||||
|
case []byte:
|
||||||
|
name = string(v)
|
||||||
|
case nil:
|
||||||
|
*x = Quality(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := ParseQuality(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (x Quality) Value() (driver.Value, error) {
|
||||||
|
return x.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ResizeModeFit is a ResizeMode of type Fit
|
||||||
|
ResizeModeFit ResizeMode = iota
|
||||||
|
// ResizeModeFill is a ResizeMode of type Fill
|
||||||
|
ResizeModeFill
|
||||||
|
)
|
||||||
|
|
||||||
|
const _ResizeModeName = "fitfill"
|
||||||
|
|
||||||
|
var _ResizeModeMap = map[ResizeMode]string{
|
||||||
|
0: _ResizeModeName[0:3],
|
||||||
|
1: _ResizeModeName[3:7],
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface.
|
||||||
|
func (x ResizeMode) String() string {
|
||||||
|
if str, ok := _ResizeModeMap[x]; ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("ResizeMode(%d)", x)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ResizeModeValue = map[string]ResizeMode{
|
||||||
|
_ResizeModeName[0:3]: 0,
|
||||||
|
_ResizeModeName[3:7]: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseResizeMode attempts to convert a string to a ResizeMode
|
||||||
|
func ParseResizeMode(name string) (ResizeMode, error) {
|
||||||
|
if x, ok := _ResizeModeValue[name]; ok {
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
return ResizeMode(0), fmt.Errorf("%s is not a valid ResizeMode", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the text marshaller method
|
||||||
|
func (x ResizeMode) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the text unmarshaller method
|
||||||
|
func (x *ResizeMode) UnmarshalText(text []byte) error {
|
||||||
|
name := string(text)
|
||||||
|
tmp, err := ParseResizeMode(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (x *ResizeMode) Scan(value interface{}) error {
|
||||||
|
var name string
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
name = v
|
||||||
|
case []byte:
|
||||||
|
name = string(v)
|
||||||
|
case nil:
|
||||||
|
*x = ResizeMode(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := ParseResizeMode(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (x ResizeMode) Value() (driver.Value, error) {
|
||||||
|
return x.String(), nil
|
||||||
|
}
|
||||||
398
img/service_test.go
Normal file
398
img/service_test.go
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
package img
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"image/gif"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/image/bmp"
|
||||||
|
"golang.org/x/image/tiff"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_Resize(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
options []Option
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
source func(t *testing.T) afero.File
|
||||||
|
matcher func(t *testing.T, reader io.Reader)
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"fill upscale": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 50, 20)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"fill downscale": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"fit upscale": {
|
||||||
|
options: []Option{WithMode(ResizeModeFit)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 50, 20)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(50, 20),
|
||||||
|
},
|
||||||
|
"fit downscale": {
|
||||||
|
options: []Option{WithMode(ResizeModeFit)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 75),
|
||||||
|
},
|
||||||
|
"keep original format": {
|
||||||
|
options: []Option{},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayPng(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatPng),
|
||||||
|
},
|
||||||
|
"convert to jpeg": {
|
||||||
|
options: []Option{WithFormat(FormatJpeg)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatJpeg),
|
||||||
|
},
|
||||||
|
"convert to png": {
|
||||||
|
options: []Option{WithFormat(FormatPng)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatPng),
|
||||||
|
},
|
||||||
|
"convert to gif": {
|
||||||
|
options: []Option{WithFormat(FormatGif)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatGif),
|
||||||
|
},
|
||||||
|
"convert to tiff": {
|
||||||
|
options: []Option{WithFormat(FormatTiff)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatTiff),
|
||||||
|
},
|
||||||
|
"convert to bmp": {
|
||||||
|
options: []Option{WithFormat(FormatBmp)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatBmp),
|
||||||
|
},
|
||||||
|
"convert to unknown": {
|
||||||
|
options: []Option{WithFormat(Format(-1))},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatJpeg),
|
||||||
|
},
|
||||||
|
"resize png": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayPng(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize gif": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayGif(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize tiff": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayTiff(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize bmp": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayBmp(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize with high quality": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityHigh)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize with medium quality": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityMedium)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize with low quality": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityLow)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize with unknown quality": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill), WithQuality(Quality(-1))},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"broken file": {
|
||||||
|
options: []Option{WithMode(ResizeModeFit)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.jpg")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.WriteString("this is not an image")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
svc := New(1)
|
||||||
|
source := test.source(t)
|
||||||
|
defer source.Close()
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
err := svc.Resize(context.Background(), source, test.width, test.height, buf, test.options...)
|
||||||
|
if (err != nil) != test.wantErr {
|
||||||
|
t.Fatalf("GetMarketSpecs() error = %v, wantErr %v", err, test.wantErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
test.matcher(t, buf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sizeMatcher(width, height int) func(t *testing.T, reader io.Reader) {
|
||||||
|
return func(t *testing.T, reader io.Reader) {
|
||||||
|
resizedImg, _, err := image.Decode(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, width, resizedImg.Bounds().Dx())
|
||||||
|
require.Equal(t, height, resizedImg.Bounds().Dy())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMatcher(format Format) func(t *testing.T, reader io.Reader) {
|
||||||
|
return func(t *testing.T, reader io.Reader) {
|
||||||
|
_, decodedFormat, err := image.DecodeConfig(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, format.String(), decodedFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrayJpeg(t *testing.T, width, height int) afero.File {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.jpg")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
err = jpeg.Encode(file, img, &jpeg.Options{Quality: 90})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrayPng(t *testing.T, width, height int) afero.File {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.png")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
err = png.Encode(file, img)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrayGif(t *testing.T, width, height int) afero.File {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.gif")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
err = gif.Encode(file, img, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrayTiff(t *testing.T, width, height int) afero.File {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.tiff")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
err = tiff.Encode(file, img, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrayBmp(t *testing.T, width, height int) afero.File {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.bmp")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
err = bmp.Encode(file, img)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_FormatFromExtension(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
ext string
|
||||||
|
want Format
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
"jpg": {
|
||||||
|
ext: ".jpg",
|
||||||
|
want: FormatJpeg,
|
||||||
|
},
|
||||||
|
"jpeg": {
|
||||||
|
ext: ".jpeg",
|
||||||
|
want: FormatJpeg,
|
||||||
|
},
|
||||||
|
"png": {
|
||||||
|
ext: ".png",
|
||||||
|
want: FormatPng,
|
||||||
|
},
|
||||||
|
"gif": {
|
||||||
|
ext: ".gif",
|
||||||
|
want: FormatGif,
|
||||||
|
},
|
||||||
|
"tiff": {
|
||||||
|
ext: ".tiff",
|
||||||
|
want: FormatTiff,
|
||||||
|
},
|
||||||
|
"bmp": {
|
||||||
|
ext: ".bmp",
|
||||||
|
want: FormatBmp,
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
ext: ".mov",
|
||||||
|
wantErr: ErrUnsupportedFormat,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
svc := New(1)
|
||||||
|
got, err := svc.FormatFromExtension(test.ext)
|
||||||
|
require.Truef(t, errors.Is(err, test.wantErr), "error = %v, wantErr %v", err, test.wantErr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.Equal(t, test.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ type Server struct {
|
|||||||
Port string `json:"port"`
|
Port string `json:"port"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Log string `json:"log"`
|
Log string `json:"log"`
|
||||||
|
EnableThumbnails bool `json:"enableThumbnails"`
|
||||||
|
ResizePreview bool `json:"resizePreview"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean cleans any variables that might need cleaning.
|
// Clean cleans any variables that might need cleaning.
|
||||||
|
|||||||
Reference in New Issue
Block a user