Merge pull request #1044 from filebrowser/fix_img_resize
commit
470f93cefc
|
@ -21,9 +21,17 @@ jobs:
|
||||||
root: .
|
root: .
|
||||||
paths:
|
paths:
|
||||||
- '*'
|
- '*'
|
||||||
|
test:
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.14.6
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
name: "Test"
|
||||||
|
command: go test ./...
|
||||||
build-go:
|
build-go:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.14.3
|
- image: circleci/golang:1.14.6
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: '~/project'
|
at: '~/project'
|
||||||
|
@ -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:
|
||||||
|
|
33
cmd/root.go
33
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"
|
||||||
|
@ -56,6 +59,10 @@ func addServerFlags(flags *pflag.FlagSet) {
|
||||||
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.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 +110,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)
|
||||||
|
|
||||||
|
@ -132,7 +157,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 +230,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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 && 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)
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,7 +13,7 @@
|
||||||
:aria-label="name"
|
:aria-label="name"
|
||||||
:aria-selected="isSelected">
|
:aria-selected="isSelected">
|
||||||
<div>
|
<div>
|
||||||
<img v-if="type==='image'" :src="thumbnailUrl">
|
<img v-if="type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl">
|
||||||
<i v-else class="material-icons">{{ icon }}</i>
|
<i v-else class="material-icons">{{ icon }}</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { baseURL } from '@/utils/constants'
|
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'
|
||||||
|
@ -76,6 +76,9 @@ export default {
|
||||||
thumbnailUrl () {
|
thumbnailUrl () {
|
||||||
const path = this.url.replace(/^\/files\//, '')
|
const path = this.url.replace(/^\/files\//, '')
|
||||||
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
|
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
|
||||||
|
},
|
||||||
|
isThumbsEnabled () {
|
||||||
|
return enableThumbs
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -213,4 +216,4 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -11,6 +11,7 @@ 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
|
||||||
|
|
||||||
export {
|
export {
|
||||||
name,
|
name,
|
||||||
|
@ -24,5 +25,6 @@ export {
|
||||||
noAuth,
|
noAuth,
|
||||||
authMethod,
|
authMethod,
|
||||||
loginPage,
|
loginPage,
|
||||||
theme
|
theme,
|
||||||
|
enableThumbs
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,4 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
runtimeCompiler: true,
|
runtimeCompiler: true,
|
||||||
publicPath: '[{[ .StaticURL ]}]'
|
publicPath: '[{[ .StaticURL ]}]',
|
||||||
|
parallel: 2,
|
||||||
}
|
}
|
3
go.mod
3
go.mod
|
@ -14,6 +14,7 @@ require (
|
||||||
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/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
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -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=
|
||||||
|
@ -316,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=
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -59,7 +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, "/api/preview")).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")
|
||||||
|
|
||||||
|
|
144
http/preview.go
144
http/preview.go
|
@ -1,14 +1,16 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/img"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -16,86 +18,110 @@ const (
|
||||||
sizeBig = "big"
|
sizeBig = "big"
|
||||||
)
|
)
|
||||||
|
|
||||||
type imageProcessor func(src image.Image) (image.Image, error)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
var previewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
type FileCache interface {
|
||||||
if !d.user.Perm.Download {
|
Store(ctx context.Context, key string, value []byte) error
|
||||||
return http.StatusAccepted, nil
|
Load(ctx context.Context, key string) ([]byte, bool, error)
|
||||||
}
|
}
|
||||||
vars := mux.Vars(r)
|
|
||||||
size := vars["size"]
|
|
||||||
if size != sizeBig && size != sizeThumb {
|
|
||||||
return http.StatusNotImplemented, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := files.NewFileInfo(files.FileOptions{
|
func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, resizePreview bool) handleFunc {
|
||||||
Fs: d.user.Fs,
|
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
Path: "/" + vars["path"],
|
if !d.user.Perm.Download {
|
||||||
Modify: d.user.Perm.Modify,
|
return http.StatusAccepted, nil
|
||||||
Expand: true,
|
}
|
||||||
Checker: d,
|
vars := mux.Vars(r)
|
||||||
|
size := vars["size"]
|
||||||
|
if size != sizeBig && size != sizeThumb {
|
||||||
|
return http.StatusNotImplemented, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := files.NewFileInfo(files.FileOptions{
|
||||||
|
Fs: d.user.Fs,
|
||||||
|
Path: "/" + vars["path"],
|
||||||
|
Modify: d.user.Perm.Modify,
|
||||||
|
Expand: true,
|
||||||
|
Checker: d,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentDisposition(w, r, file)
|
||||||
|
|
||||||
|
switch file.Type {
|
||||||
|
case "image":
|
||||||
|
return handleImagePreview(w, r, imgSvc, fileCache, file, size, enableThumbnails, resizePreview)
|
||||||
|
default:
|
||||||
|
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", file.Type)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
}
|
||||||
return errToStatus(err), err
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentDisposition(w, r, file)
|
func handleImagePreview(w http.ResponseWriter, r *http.Request, imgSvc ImgService, fileCache FileCache,
|
||||||
|
file *files.FileInfo, size string, enableThumbnails, resizePreview bool) (int, error) {
|
||||||
switch file.Type {
|
format, err := imgSvc.FormatFromExtension(file.Extension)
|
||||||
case "image":
|
|
||||||
return handleImagePreview(w, r, file, size)
|
|
||||||
default:
|
|
||||||
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", file.Type)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
func handleImagePreview(w http.ResponseWriter, r *http.Request, file *files.FileInfo, size string) (int, error) {
|
|
||||||
format, err := imaging.FormatFromExtension(file.Extension)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Unsupported extensions directly return the raw data
|
// Unsupported extensions directly return the raw data
|
||||||
if err == imaging.ErrUnsupportedFormat {
|
if err == img.ErrUnsupportedFormat {
|
||||||
return rawFileHandler(w, r, file)
|
return rawFileHandler(w, r, file)
|
||||||
}
|
}
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cacheKey := file.Path + size
|
||||||
|
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)
|
fd, err := file.Fs.Open(file.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
if format == imaging.GIF && size == sizeBig {
|
var (
|
||||||
if _, err := rawFileHandler(w, r, file); err != nil { //nolint: govet
|
width int
|
||||||
|
height int
|
||||||
|
options []img.Option
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case size == sizeBig && resizePreview && format != img.FormatGif:
|
||||||
|
width = 1080
|
||||||
|
height = 1080
|
||||||
|
options = append(options, img.WithMode(img.ResizeModeFit), img.WithQuality(img.QualityMedium))
|
||||||
|
case size == sizeThumb && 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 errToStatus(err), err
|
||||||
}
|
}
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var imgProcessor imageProcessor
|
buf := &bytes.Buffer{}
|
||||||
switch size {
|
if err := imgSvc.Resize(context.Background(), fd, width, height, buf, options...); err != nil {
|
||||||
case sizeBig:
|
return 0, err
|
||||||
imgProcessor = func(img image.Image) (image.Image, error) {
|
|
||||||
return imaging.Fit(img, 1080, 1080, imaging.Lanczos), nil
|
|
||||||
}
|
|
||||||
case sizeThumb:
|
|
||||||
imgProcessor = func(img image.Image) (image.Image, error) {
|
|
||||||
return imaging.Thumbnail(img, 128, 128, imaging.Box), nil
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return http.StatusBadRequest, fmt.Errorf("unsupported preview size %s", size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img, err := imaging.Decode(fd, imaging.AutoOrientation(true))
|
go func() {
|
||||||
if err != nil {
|
if err := fileCache.Store(context.Background(), cacheKey, buf.Bytes()); err != nil {
|
||||||
return errToStatus(err), err
|
fmt.Printf("failed to cache resized image: %v", err)
|
||||||
}
|
}
|
||||||
img, err = imgProcessor(img)
|
}()
|
||||||
if err != nil {
|
|
||||||
return errToStatus(err), err
|
_, _ = w.Write(buf.Bytes())
|
||||||
}
|
|
||||||
if imaging.Encode(w, img, format) != nil {
|
|
||||||
return errToStatus(err), err
|
|
||||||
}
|
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.settings.Branding.Files != "" {
|
if d.settings.Branding.Files != "" {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,14 +30,16 @@ func (s *Settings) GetRules() []rules.Rule {
|
||||||
|
|
||||||
// Server specific settings.
|
// Server specific settings.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Root string `json:"root"`
|
Root string `json:"root"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
Socket string `json:"socket"`
|
Socket string `json:"socket"`
|
||||||
TLSKey string `json:"tlsKey"`
|
TLSKey string `json:"tlsKey"`
|
||||||
TLSCert string `json:"tlsCert"`
|
TLSCert string `json:"tlsCert"`
|
||||||
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.
|
||||||
|
|
Loading…
Reference in New Issue