Compare commits

...

28 Commits

Author SHA1 Message Date
Oleg Lobanov
9f858398ab chore(release): 2.9.0 2020-10-21 16:52:29 +02:00
Aiden McClelland
0ac80e8387 feat: support WKWebview custom protocol (#1113) 2020-10-21 16:41:09 +02:00
Hissy
0dca0b92d1 chore: update zh-cn and zh-tw (#1121) 2020-10-21 16:39:41 +02:00
Oleg Lobanov
c9b36ba32e Merge pull request #1124 from ramiresviana/fixes-4 2020-10-21 16:37:39 +02:00
Ramires Viana
f2c4e78381 fix: allow start from Windows explorer 2020-10-19 13:41:40 +00:00
Ramires Viana
05bff54b71 fix: preview case sensitive file extension 2020-10-19 13:25:09 +00:00
Ramires Viana
2bd163d92a fix: search missing path slash 2020-10-19 13:14:36 +00:00
Ramires Viana
5e27ba5c8c fix: file upload missing path slash 2020-10-19 13:11:26 +00:00
Oleg Lobanov
5aaeb3b76d chore(release): 2.8.0 2020-10-05 09:53:09 +02:00
Daniel Pham
36fb9f562a fix: fix empty command name (#1106) 2020-10-05 09:52:27 +02:00
Xabi
ad99bf1801 fix: fix panic when accessing nonexistent .js file in static path (#1105) 2020-10-02 15:09:03 +02:00
Oleg Lobanov
4c2a094255 Merge pull request #1100 from ramiresviana/fixes-3 2020-10-01 16:53:35 +02:00
Keagan McClelland
97693cc611 feat: add disable exec flag (#1090) 2020-10-01 16:45:24 +02:00
Ramires Viana
c6d4fcd08f fix: empty commands setting 2020-09-29 14:05:03 +00:00
Ramires Viana
dd7b9ddd85 fix: preview key shortcut conflict 2020-09-29 14:04:55 +00:00
Ramires Viana
26d62e4117 fix: search results absolute url 2020-09-29 14:04:43 +00:00
Ramires Viana
babd7783af fix: file upload path encoding 2020-09-29 14:04:03 +00:00
Oleg Lobanov
1529e796df chore(release): 2.7.0 2020-09-11 19:21:08 +02:00
Oleg Lobanov
d4b904b92b chore: pass docker password via stdin 2020-09-11 18:57:14 +02:00
Oleg Lobanov
12d4177823 build: bump go version to 1.15.2 (#1081) 2020-09-11 18:07:01 +02:00
Oleg Lobanov
8142b32f38 feat: put selected files in the root of the archive (closes #1065) 2020-09-11 16:54:22 +02:00
Oleg Lobanov
c5abbb4e1c chore: fix lint errors 2020-09-11 16:02:16 +02:00
Oleg Lobanov
65ac73414f feat: add --socket-perm flag to control unix socket file permissions (closes #1060) 2020-09-11 15:59:06 +02:00
Oleg Lobanov
ede4213c8e chore: fix french translation (closes #1071) 2020-09-11 15:16:58 +02:00
Agneev Mukherjee
b60d291490 chore: fix URLs for assets (#1074) 2020-09-05 15:23:42 +02:00
Oleg Lobanov
b9ede79888 Merge pull request #1066 from ramiresviana/preview-mobile-dropdown 2020-08-25 16:33:57 +02:00
Ramires Viana
3d2cb838d1 feat: preview size button 2020-08-25 14:14:15 +00:00
Ramires Viana
778734419d feat: preview mobile dropdown 2020-08-18 12:47:23 +00:00
37 changed files with 444 additions and 126 deletions

View File

@@ -23,7 +23,7 @@ jobs:
- '*' - '*'
test: test:
docker: docker:
- image: circleci/golang:1.14.6 - image: circleci/golang:1.15.2
steps: steps:
- checkout - checkout
- run: - run:
@@ -31,7 +31,7 @@ jobs:
command: go test ./... command: go test ./...
build-go: build-go:
docker: docker:
- image: circleci/golang:1.14.6 - image: circleci/golang:1.15.2
steps: steps:
- attach_workspace: - attach_workspace:
at: '~/project' at: '~/project'
@@ -49,12 +49,12 @@ jobs:
- '*' - '*'
release: release:
docker: docker:
- image: circleci/golang:1.14.6 - 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:

View File

@@ -2,6 +2,48 @@
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.9.0](https://github.com/filebrowser/filebrowser/compare/v2.8.0...v2.9.0) (2020-10-21)
### Features
* support WKWebview custom protocol ([#1113](https://github.com/filebrowser/filebrowser/issues/1113)) ([0ac80e8](https://github.com/filebrowser/filebrowser/commit/0ac80e8387a69924284259bde448af2813d84ed1))
### Bug Fixes
* allow start from Windows explorer ([f2c4e78](https://github.com/filebrowser/filebrowser/commit/f2c4e78381610879eda5316d38a999c89df6c14a))
* file upload missing path slash ([5e27ba5](https://github.com/filebrowser/filebrowser/commit/5e27ba5c8c1be603c6ae7fec8de48e3532dea1f7))
* preview case sensitive file extension ([05bff54](https://github.com/filebrowser/filebrowser/commit/05bff54b71543fd232f1089c40504d0cbfd106be))
* search missing path slash ([2bd163d](https://github.com/filebrowser/filebrowser/commit/2bd163d92a856d65c8d4615e37898470c1edf2f4))
## [2.8.0](https://github.com/filebrowser/filebrowser/compare/v2.7.0...v2.8.0) (2020-10-05)
### Features
* add disable exec flag ([#1090](https://github.com/filebrowser/filebrowser/issues/1090)) ([97693cc](https://github.com/filebrowser/filebrowser/commit/97693cc6117ce1c956baede91de5dd48b904e175))
### Bug Fixes
* empty commands setting ([c6d4fcd](https://github.com/filebrowser/filebrowser/commit/c6d4fcd08f5f1531c2cef514dc86019e23e7289f))
* file upload path encoding ([babd778](https://github.com/filebrowser/filebrowser/commit/babd7783afe85b790e1c558375d7b5013b2d366f))
* fix empty command name ([#1106](https://github.com/filebrowser/filebrowser/issues/1106)) ([36fb9f5](https://github.com/filebrowser/filebrowser/commit/36fb9f562a2c005ca4390fdebde0b4690201dff9))
* fix panic when accessing nonexistent .js file in static path ([#1105](https://github.com/filebrowser/filebrowser/issues/1105)) ([ad99bf1](https://github.com/filebrowser/filebrowser/commit/ad99bf180197e0e6d82231a86457585de16366a8))
* preview key shortcut conflict ([dd7b9dd](https://github.com/filebrowser/filebrowser/commit/dd7b9ddd8546361060ef99e838a691b2fc6c495a))
* search results absolute url ([26d62e4](https://github.com/filebrowser/filebrowser/commit/26d62e411716a5eb9a5a703e47484cfb3fbf3bd0))
## [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.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) ### [2.6.1](https://github.com/filebrowser/filebrowser/compare/v2.6.0...v2.6.1) (2020-07-28)

View File

@@ -140,6 +140,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address) fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert) fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey) fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
fmt.Fprintln(w, "\nDefaults:") fmt.Fprintln(w, "\nDefaults:")
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope) fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale) fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)

View File

@@ -2,7 +2,6 @@ package cmd
import ( import (
"fmt" "fmt"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -32,7 +31,7 @@ override the options.`,
s := &settings.Settings{ s := &settings.Settings{
Key: generateKey(), Key: generateKey(),
Signup: mustGetBool(flags, "signup"), Signup: mustGetBool(flags, "signup"),
Shell: strings.Split(strings.TrimSpace(mustGetString(flags, "shell")), " "), Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
AuthMethod: authMethod, AuthMethod: authMethod,
Defaults: defaults, Defaults: defaults,
Branding: settings.Branding{ Branding: settings.Branding{

View File

@@ -1,8 +1,6 @@
package cmd package cmd
import ( import (
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
@@ -50,7 +48,7 @@ you want to change. Other options will remain unchanged.`,
case "auth.method": case "auth.method":
hasAuth = true hasAuth = true
case "shell": case "shell":
set.Shell = strings.Split(strings.TrimSpace(mustGetString(flags, flag.Name)), " ") set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
case "branding.name": case "branding.name":
set.Branding.Name = mustGetString(flags, flag.Name) set.Branding.Name = mustGetString(flags, flag.Name)
case "branding.disableExternal": case "branding.disableExternal":

View File

@@ -35,6 +35,7 @@ var (
func init() { func init() {
cobra.OnInitialize(initConfig) cobra.OnInitialize(initConfig)
cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n") rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
@@ -58,11 +59,13 @@ 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.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.Int("img-processors", 4, "image processors count") flags.Int("img-processors", 4, "image processors count")
flags.Bool("disable-thumbnails", false, "disable image thumbnails") flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews") flags.Bool("disable-preview-resize", false, "disable resize of image previews")
flags.Bool("disable-exec", false, "disables Command Runner feature")
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -143,6 +146,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)
@@ -236,6 +243,9 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
_, disablePreviewResize := getParamB(flags, "disable-preview-resize") _, disablePreviewResize := getParamB(flags, "disable-preview-resize")
server.ResizePreview = !disablePreviewResize server.ResizePreview = !disablePreviewResize
_, disableExec := getParamB(flags, "disable-exec")
server.EnableExec = !disableExec
return server return server
} }

View File

@@ -7,6 +7,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/asdine/storm" "github.com/asdine/storm"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -178,3 +179,15 @@ func cleanUpMapValue(v interface{}) interface{} {
return v return v
} }
} }
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
// then returns empty string array, else returns the splitted word array of cmd.
// This is to ensure the result will never be []string{""}
func convertCmdStrToCmdArray(cmd string) []string {
var cmdArray []string
trimmedCmdStr := strings.TrimSpace(cmd)
if trimmedCmdStr != "" {
cmdArray = strings.Split(trimmedCmdStr, " ")
}
return cmdArray
}

View File

@@ -3,6 +3,7 @@ package fileutils
import ( import (
"io" "io"
"os" "os"
"path"
"path/filepath" "path/filepath"
"github.com/spf13/afero" "github.com/spf13/afero"
@@ -50,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
View 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)
}
})
}
}

View File

@@ -13,24 +13,25 @@
<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 -->
<script> <script>
window.FileBrowser = JSON.parse(`[{[ .Json ]}]`); window.FileBrowser = JSON.parse(`[{[ .Json ]}]`);
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL; var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
var dynamicManifest = { var dynamicManifest = {
"name": window.FileBrowser.Name || 'File Browser', "name": window.FileBrowser.Name || 'File Browser',

View File

@@ -85,6 +85,11 @@ export function download (format, ...files) {
export async function post (url, content = '', overwrite = false, onupload) { export async function post (url, content = '', overwrite = false, onupload) {
url = removePrefix(url) url = removePrefix(url)
let bufferContent
if (content instanceof Blob && !['http:', 'https:'].includes(window.location.protocol)) {
bufferContent = await new Response(content).arrayBuffer()
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new XMLHttpRequest() let request = new XMLHttpRequest()
request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true) request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true)
@@ -108,7 +113,7 @@ export async function post (url, content = '', overwrite = false, onupload) {
reject(error) reject(error)
} }
request.send(content) request.send(bufferContent || content)
}) })
} }

View File

@@ -1,8 +1,31 @@
import { fetchJSON, removePrefix } from './utils' import { fetchURL, removePrefix } from './utils'
import url from '../utils/url'
export default async function search (url, query) { export default async function search (base, query) {
url = removePrefix(url) base = removePrefix(base)
query = encodeURIComponent(query) query = encodeURIComponent(query)
return fetchJSON(`/api/search${url}?query=${query}`, {}) if (!base.endsWith('/')) {
} base += '/'
}
let res = await fetchURL(`/api/search${base}?query=${query}`, {})
if (res.status === 200) {
let data = await res.json()
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path)
if (item.dir) {
item.url += '/'
}
return item
})
return data
} else {
throw Error(res.status)
}
}

View File

@@ -1,5 +1,5 @@
<template> <template>
<header v-if="!isEditor"> <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>
@@ -37,7 +37,7 @@
<delete-button v-show="showDeleteButton"></delete-button> <delete-button v-show="showDeleteButton"></delete-button>
</div> </div>
<shell-button v-show="user.perm.execute" /> <shell-button v-if="isExecEnabled && user.perm.execute" />
<switch-button v-show="isListing"></switch-button> <switch-button v-show="isListing"></switch-button>
<download-button v-show="showDownloadButton"></download-button> <download-button v-show="showDownloadButton"></download-button>
<upload-button v-show="showUpload"></upload-button> <upload-button v-show="showUpload"></upload-button>
@@ -68,7 +68,7 @@ import CopyButton from './buttons/Copy'
import ShareButton from './buttons/Share' import ShareButton from './buttons/Share'
import ShellButton from './buttons/Shell' import ShellButton from './buttons/Shell'
import {mapGetters, mapState} from 'vuex' import {mapGetters, mapState} from 'vuex'
import { logoURL } from '@/utils/constants' import { logoURL, enableExec } from '@/utils/constants'
import * as api from '@/api' import * as api from '@/api'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
@@ -108,6 +108,7 @@ export default {
'selectedCount', 'selectedCount',
'isFiles', 'isFiles',
'isEditor', 'isEditor',
'isPreview',
'isListing', 'isListing',
'isLogged' 'isLogged'
]), ]),
@@ -119,6 +120,7 @@ export default {
'multiple' 'multiple'
]), ]),
logoURL: () => logoURL, logoURL: () => logoURL,
isExecEnabled: () => enableExec,
isMobile () { isMobile () {
return this.width <= 736 return this.width <= 736
}, },

View File

@@ -49,7 +49,7 @@
</template> </template>
<ul v-show="results.length > 0"> <ul v-show="results.length > 0">
<li v-for="(s,k) in filteredResults" :key="k"> <li v-for="(s,k) in filteredResults" :key="k">
<router-link @click.native="close" :to="'./' + s.path"> <router-link @click.native="close" :to="s.url">
<i v-if="s.dir" class="material-icons">folder</i> <i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i> <i v-else class="material-icons">insert_drive_file</i>
<span>./{{ s.path }}</span> <span>./{{ s.path }}</span>
@@ -183,8 +183,12 @@ export default {
this.ongoing = true this.ongoing = true
try {
this.results = await search(path, this.value)
} catch (error) {
this.$showError(error)
}
this.results = await search(path, this.value)
this.ongoing = false this.ongoing = false
} }
} }

View 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>

View File

@@ -77,6 +77,13 @@ export default {
window.removeEventListener('resize', this.onResize) window.removeEventListener('resize', this.onResize)
document.removeEventListener('mouseup', this.onMouseUp) document.removeEventListener('mouseup', this.onMouseUp)
}, },
watch: {
src: function () {
this.scale = 1
this.setZoom()
this.setCenter()
}
},
methods: { methods: {
onLoad() { onLoad() {
let img = this.$refs.imgex let img = this.$refs.imgex

View File

@@ -368,7 +368,7 @@ export default {
} }
let files = await upload.scanFiles(dt) let files = await upload.scanFiles(dt)
let path = this.$route.path + base let path = this.$route.path.endsWith('/') ? this.$route.path + base : this.$route.path + '/' + base
let items = this.req.items let items = this.req.items
if (base !== '') { if (base !== '') {
@@ -409,7 +409,7 @@ export default {
} }
} }
let path = this.$route.path let path = this.$route.path.endsWith('/') ? this.$route.path : this.$route.path + '/'
let conflict = upload.checkConflict(files, this.req.items) let conflict = upload.checkConflict(files, this.req.items)
if (conflict) { if (conflict) {

View File

@@ -9,10 +9,17 @@
<span>{{ this.name }}</span> <span>{{ this.name }}</span>
</div> </div>
<rename-button :disabled="loading" v-if="user.perm.rename"></rename-button> <preview-size-button v-if="isResizeEnabled && this.req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
<delete-button :disabled="loading" v-if="user.perm.delete"></delete-button> <button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<download-button :disabled="loading" v-if="user.perm.download"></download-button> <i class="material-icons">more_vert</i>
<info-button :disabled="loading"></info-button> </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>
<div class="loading" v-if="loading"> <div class="loading" v-if="loading">
@@ -45,20 +52,23 @@
but don't worry, you can <a :href="download">download it</a> but don't worry, you can <a :href="download">download it</a>
and watch it with your favorite video player! and watch it with your favorite video player!
</video> </video>
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object> <object v-else-if="req.extension.toLowerCase() == '.pdf'" class="pdf" :data="raw"></object>
<a v-else-if="req.type == 'blob'" :href="download"> <a v-else-if="req.type == 'blob'" :href="download">
<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> </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'
@@ -75,6 +85,7 @@ const mediaTypes = [
export default { export default {
name: 'preview', name: 'preview',
components: { components: {
PreviewSizeButton,
InfoButton, InfoButton,
DeleteButton, DeleteButton,
RenameButton, RenameButton,
@@ -87,11 +98,12 @@ export default {
nextLink: '', nextLink: '',
listing: null, listing: null,
name: '', name: '',
subtitles: [] subtitles: [],
fullSize: false
} }
}, },
computed: { computed: {
...mapState(['req', 'user', 'oldReq', 'jwt', 'loading']), ...mapState(['req', 'user', 'oldReq', 'jwt', 'loading', 'show']),
hasPrevious () { hasPrevious () {
return (this.previousLink !== '') return (this.previousLink !== '')
}, },
@@ -102,13 +114,19 @@ export default {
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}` return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
}, },
previewUrl () { previewUrl () {
if (this.req.type === 'image') { if (this.req.type === 'image' && !this.fullSize) {
return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}` 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}` return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
}, },
raw () { raw () {
return `${this.previewUrl}&inline=true` return `${this.previewUrl}&inline=true`
},
showMore () {
return this.$store.state.show === 'more'
},
isResizeEnabled () {
return resizePreview
} }
}, },
watch: { watch: {
@@ -141,6 +159,10 @@ export default {
key (event) { key (event) {
event.preventDefault() event.preventDefault()
if (this.show !== null) {
return
}
if (event.which === 13 || event.which === 39) { // right arrow if (event.which === 13 || event.which === 39) { // right arrow
if (this.hasNext) this.next() if (this.hasNext) this.next()
} else if (event.which === 37) { // left arrow } else if (event.which === 37) { // left arrow
@@ -189,6 +211,15 @@ export default {
return return
} }
},
openMore () {
this.$store.commit('showHover', 'more')
},
resetPrompts () {
this.$store.commit('closeHovers')
},
toggleSize () {
this.fullSize = !this.fullSize
} }
} }
} }

View File

@@ -16,7 +16,11 @@ export default {
return this.commands.join(' ') return this.commands.join(' ')
}, },
set (value) { set (value) {
this.$emit('update:commands', value.split(' ')) if (value !== '') {
this.$emit('update:commands', value.split(' '))
} else {
this.$emit('update:commands', [])
}
} }
} }
} }

View File

@@ -9,13 +9,14 @@
<p><input type="checkbox" :disabled="admin" v-model="perm.delete"> {{ $t('settings.perm.delete') }}</p> <p><input type="checkbox" :disabled="admin" v-model="perm.delete"> {{ $t('settings.perm.delete') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.download"> {{ $t('settings.perm.download') }}</p> <p><input type="checkbox" :disabled="admin" v-model="perm.download"> {{ $t('settings.perm.download') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.modify"> {{ $t('settings.perm.modify') }}</p> <p><input type="checkbox" :disabled="admin" v-model="perm.modify"> {{ $t('settings.perm.modify') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.execute"> {{ $t('settings.perm.execute') }}</p> <p v-if="isExecEnabled"><input type="checkbox" :disabled="admin" v-model="perm.execute"> {{ $t('settings.perm.execute') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.rename"> {{ $t('settings.perm.rename') }}</p> <p><input type="checkbox" :disabled="admin" v-model="perm.rename"> {{ $t('settings.perm.rename') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.share"> {{ $t('settings.perm.share') }}</p> <p><input type="checkbox" :disabled="admin" v-model="perm.share"> {{ $t('settings.perm.share') }}</p>
</div> </div>
</template> </template>
<script> <script>
import { enableExec } from '@/utils/constants'
export default { export default {
name: 'permissions', name: 'permissions',
props: ['perm'], props: ['perm'],
@@ -33,7 +34,8 @@ export default {
this.perm.admin = value this.perm.admin = value
} }
} },
isExecEnabled: () => enableExec
} }
} }
</script> </script>

View File

@@ -25,7 +25,7 @@
</p> </p>
<permissions :perm.sync="user.perm" /> <permissions :perm.sync="user.perm" />
<commands :commands.sync="user.commands" /> <commands v-if="isExecEnabled" :commands.sync="user.commands" />
<div v-if="!isDefault"> <div v-if="!isDefault">
<h3>{{ $t('settings.rules') }}</h3> <h3>{{ $t('settings.rules') }}</h3>
@@ -40,6 +40,7 @@ import Languages from './Languages'
import Rules from './Rules' import Rules from './Rules'
import Permissions from './Permissions' import Permissions from './Permissions'
import Commands from './Commands' import Commands from './Commands'
import { enableExec } from '@/utils/constants'
export default { export default {
name: 'user', name: 'user',
@@ -53,7 +54,8 @@ export default {
computed: { computed: {
passwordPlaceholder () { passwordPlaceholder () {
return this.isNew ? '' : this.$t('settings.avoidChanges') return this.isNew ? '' : this.$t('settings.avoidChanges')
} },
isExecEnabled: () => enableExec
}, },
watch: { watch: {
'user.perm.admin': function () { 'user.perm.admin': function () {

View File

@@ -179,7 +179,7 @@
"userDefaults": "User default settings", "userDefaults": "User default settings",
"defaultUserDescription": "This are the default settings for new users.", "defaultUserDescription": "This are the default settings for new users.",
"executeOnShell": "Execute on shell", "executeOnShell": "Execute on shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.", "executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"perm": { "perm": {
"create": "Create files and directories", "create": "Create files and directories",
"delete": "Delete files and directories", "delete": "Delete files and directories",

View File

@@ -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",

View File

@@ -90,8 +90,8 @@
"copy": "复制", "copy": "复制",
"copyMessage": "请选择欲复制至的目录:", "copyMessage": "请选择欲复制至的目录:",
"currentlyNavigating": "当前目录:", "currentlyNavigating": "当前目录:",
"deleteMessageMultiple": "你确定要删除这 {count} 个文件吗?", "deleteMessageMultiple": "你确定要删除这 {count} 个文件吗",
"deleteMessageSingle": "你确定要删除这个文件/文件夹吗?", "deleteMessageSingle": "你确定要删除这个文件/文件夹吗",
"deleteTitle": "删除文件", "deleteTitle": "删除文件",
"displayName": "名称:", "displayName": "名称:",
"download": "下载文件", "download": "下载文件",
@@ -112,31 +112,38 @@
"replaceMessage": "您尝试上传的文件中有一个与现有文件的名称存在冲突。是否替换现有的同名文件?", "replaceMessage": "您尝试上传的文件中有一个与现有文件的名称存在冲突。是否替换现有的同名文件?",
"rename": "重命名", "rename": "重命名",
"renameMessage": "请输入新名称,旧名称为:", "renameMessage": "请输入新名称,旧名称为:",
"show": "示", "show": "点击以显示",
"size": "大小", "size": "大小",
"schedule": "计划", "schedule": "计划",
"scheduleMessage": "请选择发布这篇帖子的日期。", "scheduleMessage": "请选择发布这篇帖子的日期。",
"newArchetype": "创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。" "newArchetype": "创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。",
"upload": "上传",
"uploadMessage": "选择上传项。"
}, },
"settings": { "settings": {
"instanceName": "Instance name", "themes": {
"title": "主题",
"light": "浅色",
"dark": "深色"
},
"instanceName": "实例名称",
"brandingDirectoryPath": "品牌信息文件夹路径", "brandingDirectoryPath": "品牌信息文件夹路径",
"documentation": "帮助文档", "documentation": "帮助文档",
"branding": "品牌", "branding": "品牌",
"disableExternalLinks": "禁止外部链接(帮助文档除外)", "disableExternalLinks": "禁止外部链接(帮助文档除外)",
"brandingHelp": "您可以通过改变名称,更换商标加入自定义样式甚至禁用外部链接来自定义File Browser的外观和给人的感觉。\n想获得更多信息请查看 {0} 。", "brandingHelp": "您可以通过改变实例名称,更换Logo,加入自定义样式,甚至禁用到Github的外部链接来自定义File Browser的外观和给人的感觉。\n想获得更多信息请查看 {0} 。",
"admin": "管理员", "admin": "管理员",
"administrator": "管理员", "administrator": "管理员",
"allowCommands": "执行命令(Linux 代码)", "allowCommands": "执行命令shell 命令)",
"allowEdit": "编辑、重命名或删除文件/目录", "allowEdit": "编辑、重命名或删除文件/目录",
"allowNew": "创建新文件和目录", "allowNew": "创建新文件和目录",
"allowPublish": "发布新的帖子与页面", "allowPublish": "发布新的帖子与页面",
"avoidChanges": "(留空以避免更改)", "avoidChanges": "留空以避免更改",
"changePassword": "更改密码", "changePassword": "更改密码",
"commandRunner": "命令执行器", "commandRunner": "命令执行器",
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.", "commandRunnerHelp": "在这里你可以设置在下面的事件中执行的命令。每行必须写一条命令。可以在命令中使用环境变量 {0} 和 {1}。关于此功能和可用环境变量的更多信息,请阅读{2}.",
"commandsUpdated": "命令已更新!", "commandsUpdated": "命令已更新!",
"customStylesheet": "自定义样式表", "customStylesheet": "自定义样式表CSS",
"examples": "例子", "examples": "例子",
"globalSettings": "全局设置", "globalSettings": "全局设置",
"language": "语言", "language": "语言",
@@ -149,15 +156,15 @@
"permissions": "权限", "permissions": "权限",
"permissionsHelp": "您可以将该用户设置为管理员,也可以单独选择各项权限。如果选择了“管理员”,则其他的选项会被自动勾上,同时该用户可以管理其他用户。", "permissionsHelp": "您可以将该用户设置为管理员,也可以单独选择各项权限。如果选择了“管理员”,则其他的选项会被自动勾上,同时该用户可以管理其他用户。",
"profileSettings": "个人设置", "profileSettings": "个人设置",
"ruleExample1": "阻止用户访问所有文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore)。", "ruleExample1": "阻止用户访问所有文件夹下任何以 . 开头的文件隐藏文件, 例如: .git, .gitignore。",
"ruleExample2": "阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。", "ruleExample2": "阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。",
"rules": "规则", "rules": "规则",
"rulesHelp": "您可以为该用户制定一组黑名单或白名单式的规则,被屏蔽的文件将不会显示在列表中,用户也无权限访问,支持相对于目录范围的路径。", "rulesHelp": "您可以为该用户制定一组黑名单或白名单式的规则,被屏蔽的文件将不会显示在列表中,用户也无权限访问,支持相对于目录范围的路径。",
"scope": "目录范围", "scope": "目录范围",
"settingsUpdated": "设置已更新!", "settingsUpdated": "设置已更新!",
"user": "用户", "user": "用户",
"userCommands": "用户命令(Linux 代码)", "userCommands": "用户命令shell 命令)",
"userCommandsHelp": "指定该用户可以执行的命令(Linux 代码),用空格分隔。例如:", "userCommandsHelp": "指定该用户可以执行的命令shell 代码,用空格分隔。例如:",
"userCreated": "用户已创建!", "userCreated": "用户已创建!",
"userDeleted": "用户已删除!", "userDeleted": "用户已删除!",
"userManagement": "用户管理", "userManagement": "用户管理",
@@ -167,12 +174,12 @@
"allowSignup": "允许用户注册", "allowSignup": "允许用户注册",
"createUserDir": "在添加新用户的同时自动创建用户的个人目录", "createUserDir": "在添加新用户的同时自动创建用户的个人目录",
"insertRegex": "插入正则表达式", "insertRegex": "插入正则表达式",
"insertPath": "Insert the path", "insertPath": "插入路径",
"userUpdated": "用户已更新!", "userUpdated": "用户已更新!",
"userDefaults": "用户默认设置", "userDefaults": "用户默认设置",
"defaultUserDescription": "这些是新用户的默认设置", "defaultUserDescription": "这些是新用户的默认设置",
"executeOnShell": "在Shell中执行", "executeOnShell": "在Shell中执行",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.", "executeOnShellDescription": "默认情况下File Browser通过直接调用命令的二进制包来执行命令如果想在shell中执行如Bash、PowerShell你可以在这里定义所使用的shell和参数。如果设置了这个选项所执行的命令会作为参数追加在后面。本选项对用户命令和事件钩子都生效。",
"perm": { "perm": {
"create": "创建文件和文件夹", "create": "创建文件和文件夹",
"delete": "删除文件和文件夹", "delete": "删除文件和文件夹",
@@ -209,18 +216,22 @@
"languages": { "languages": {
"ar": "العربية", "ar": "العربية",
"en": "English", "en": "English",
"is": "Icelandic",
"it": "Italiano", "it": "Italiano",
"fr": "Français", "fr": "Français",
"pt": "Português", "pt": "Português",
"ptBR": "Português (Brasil)", "ptBR": "PortuguêsBrasil",
"ja": "日本語", "ja": "日本語",
"zhCN": "中文 (简体)", "zhCN": "中文简体",
"zhTW": "中文 (繁體)", "zhTW": "中文繁體",
"es": "Español", "es": "Español",
"de": "Deutsch", "de": "Deutsch",
"ru": "Русский", "ru": "Русский",
"pl": "Polski", "pl": "Polski",
"ko": "한국어" "ko": "한국어",
"nlBE": "DutchBelgium",
"ro": "Romanian",
"svSE": "SwedishSweden"
}, },
"time": { "time": {
"unit": "时间单位", "unit": "时间单位",

View File

@@ -116,15 +116,22 @@
"size": "大小", "size": "大小",
"schedule": "計畫", "schedule": "計畫",
"scheduleMessage": "請選擇發佈這篇貼文的日期。", "scheduleMessage": "請選擇發佈這篇貼文的日期。",
"newArchetype": "建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。" "newArchetype": "建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。",
"upload": "上傳",
"uploadMessage": "選擇上傳項。"
}, },
"settings": { "settings": {
"instanceName": "Instance name", "themes": {
"brandingDirectoryPath": "Branding directory path", "title": "主題",
"documentation": "documentation", "light": "淺色",
"branding": "Branding", "dark": "深色"
"disableExternalLinks": "Disable external links (except documentation)", },
"brandingHelp": "You can costumize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.", "instanceName": "例項名稱",
"brandingDirectoryPath": "品牌資訊資料夾路徑",
"documentation": "幫助文件",
"branding": "品牌",
"disableExternalLinks": "禁止外部連結(幫助文件除外)",
"brandingHelp": "您可以通過改變例項名稱更換Logo加入自定義樣式甚至禁用到Github的外部連結來自定義File Browser的外觀和給人的感覺。\n想獲得更多資訊請檢視 {0} 。",
"admin": "管理員", "admin": "管理員",
"administrator": "管理員", "administrator": "管理員",
"allowCommands": "執行命令", "allowCommands": "執行命令",
@@ -133,8 +140,8 @@
"allowPublish": "發佈新的貼文與頁面", "allowPublish": "發佈新的貼文與頁面",
"avoidChanges": "(留空以避免更改)", "avoidChanges": "(留空以避免更改)",
"changePassword": "更改密碼", "changePassword": "更改密碼",
"commandRunner": "Command runner", "commandRunner": "命令執行器",
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.", "commandRunnerHelp": "在這裡你可以設定在下面的事件中執行的命令。每行必須寫一條命令。可以在命令中使用環境變數 {0} 和 {1}。關於此功能和可用環境變數的更多資訊,請閱讀{2}.",
"commandsUpdated": "命令已更新!", "commandsUpdated": "命令已更新!",
"customStylesheet": "自定義樣式表", "customStylesheet": "自定義樣式表",
"examples": "範例", "examples": "範例",
@@ -163,22 +170,22 @@
"userManagement": "使用者管理", "userManagement": "使用者管理",
"username": "使用者名稱", "username": "使用者名稱",
"users": "使用者", "users": "使用者",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.", "globalRules": "這是全局允許與禁止規則。它們作用於所有使用者。您可以給每個使用者定義單獨的特殊規則來覆蓋全局規則。",
"allowSignup": "Allow users to signup", "allowSignup": "允許使用者註冊",
"createUserDir": "Auto create user home dir while adding new user", "createUserDir": "在新增新使用者的同時自動建立使用者的個人目錄",
"insertRegex": "Insert regex expression", "insertRegex": "插入正規表示式",
"insertPath": "Insert the path", "insertPath": "插入路徑",
"userUpdated": "使用者已更新!", "userUpdated": "使用者已更新!",
"userDefaults": "User default settings", "userDefaults": "使用者預設選項",
"defaultUserDescription": "This are the default settings for new users.", "defaultUserDescription": "這些是新使用者的預設設定。",
"executeOnShell": "Execute on shell", "executeOnShell": "在Shell中執行",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.", "executeOnShellDescription": "預設情況下File Browser通過直接呼叫命令的二進位制包來執行命令如果想在shell中執行如Bash、PowerShell你可以在這裡定義所使用的shell和參數。如果設定了這個選項所執行的命令會作為參數追加在後面。本選項對使用者命令和事件鉤子都生效。",
"perm": { "perm": {
"create": "建立檔案和資料夾", "create": "建立檔案和資料夾",
"delete": "刪除檔案和資料夾", "delete": "刪除檔案和資料夾",
"download": "下載", "download": "下載",
"modify": "編輯檔案", "modify": "編輯檔案",
"execute": "Execute commands", "execute": "執行命令",
"rename": "重命名或移動檔案/資料夾", "rename": "重命名或移動檔案/資料夾",
"share": "分享檔案" "share": "分享檔案"
} }
@@ -203,12 +210,13 @@
"types": "類型", "types": "類型",
"video": "影片", "video": "影片",
"search": "搜尋...", "search": "搜尋...",
"typeToSearch": "Type to search...", "typeToSearch": "輸入以搜尋...",
"pressToSearch": "Press enter to search..." "pressToSearch": "按確認鍵搜尋..."
}, },
"languages": { "languages": {
"ar": "العربية", "ar": "العربية",
"en": "English", "en": "English",
"is": "Icelandic",
"it": "Italiano", "it": "Italiano",
"fr": "Français", "fr": "Français",
"pt": "Português", "pt": "Português",
@@ -220,7 +228,10 @@
"de": "Deutsch", "de": "Deutsch",
"ru": "Русский", "ru": "Русский",
"pl": "Polski", "pl": "Polski",
"ko": "한국어" "ko": "한국어",
"nlBE": "DutchBelgium",
"ro": "Romanian",
"svSE": "SwedishSweden"
}, },
"time": { "time": {
"unit": "時間單位", "unit": "時間單位",

View File

@@ -3,6 +3,7 @@ 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'),
isPreview: state => state.previewMode,
selectedCount: state => state.selected.length, selectedCount: state => state.selected.length,
progress : state => { progress : state => {
if (state.upload.progress.length == 0) { if (state.upload.progress.length == 0) {

View File

@@ -12,6 +12,8 @@ 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 enableThumbs = window.FileBrowser.EnableThumbs
const resizePreview = window.FileBrowser.ResizePreview
const enableExec = window.FileBrowser.EnableExec
export { export {
name, name,
@@ -26,5 +28,7 @@ export {
authMethod, authMethod,
loginPage, loginPage,
theme, theme,
enableThumbs enableThumbs,
resizePreview,
enableExec
} }

View File

@@ -96,31 +96,25 @@ export function scanFiles(dt) {
}) })
} }
export function handleFiles(files, path, overwrite = false) { export function handleFiles(files, base, overwrite = false) {
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
let id = store.state.upload.id
let path = base
let file = files[i] let file = files[i]
let filename = (file.fullPath !== undefined) ? file.fullPath : file.name if (file.fullPath !== undefined) {
let filenameEncoded = url.encodeRFC5987ValueChars(filename) path += url.encodePath(file.fullPath)
} else {
let id = store.state.upload.id path += url.encodeRFC5987ValueChars(file.name)
}
let itemPath = path + filenameEncoded
if (file.isDir) { if (file.isDir) {
itemPath = path 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 = { const item = {
id, id,
path: itemPath, path,
file, file,
overwrite overwrite
} }

View File

@@ -7,7 +7,7 @@
<sidebar></sidebar> <sidebar></sidebar>
<main> <main>
<router-view></router-view> <router-view></router-view>
<shell v-if="isLogged && user.perm.execute" /> <shell v-if="isExecEnabled && isLogged && user.perm.execute" />
</main> </main>
<prompts></prompts> <prompts></prompts>
</div> </div>
@@ -19,6 +19,7 @@ import Sidebar from '@/components/Sidebar'
import Prompts from '@/components/prompts/Prompts' import Prompts from '@/components/prompts/Prompts'
import SiteHeader from '@/components/Header' import SiteHeader from '@/components/Header'
import Shell from '@/components/Shell' import Shell from '@/components/Shell'
import { enableExec } from '@/utils/constants'
export default { export default {
name: 'layout', name: 'layout',
@@ -30,7 +31,8 @@ export default {
}, },
computed: { computed: {
...mapGetters([ 'isLogged', 'progress' ]), ...mapGetters([ 'isLogged', 'progress' ]),
...mapState([ 'user' ]) ...mapState([ 'user' ]),
isExecEnabled: () => enableExec
}, },
watch: { watch: {
'$route': function () { '$route': function () {

View File

@@ -14,9 +14,11 @@
<p class="small">{{ $t('settings.globalRules') }}</p> <p class="small">{{ $t('settings.globalRules') }}</p>
<rules :rules.sync="settings.rules" /> <rules :rules.sync="settings.rules" />
<h3>{{ $t('settings.executeOnShell') }}</h3> <div v-if="isExecEnabled">
<p class="small">{{ $t('settings.executeOnShellDescription') }}</p> <h3>{{ $t('settings.executeOnShell') }}</h3>
<input class="input input--block" type="text" placeholder="bash -c, cmd /c, ..." v-model="settings.shell" /> <p class="small">{{ $t('settings.executeOnShellDescription') }}</p>
<input class="input input--block" type="text" placeholder="bash -c, cmd /c, ..." v-model="settings.shell" />
</div>
<h3>{{ $t('settings.branding') }}</h3> <h3>{{ $t('settings.branding') }}</h3>
@@ -67,7 +69,7 @@
</div> </div>
</form> </form>
<form class="card" @submit.prevent="save"> <form v-if="isExecEnabled" class="card" @submit.prevent="save">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('settings.commandRunner') }}</h2> <h2>{{ $t('settings.commandRunner') }}</h2>
</div> </div>
@@ -104,6 +106,7 @@ import { settings as api } from '@/api'
import UserForm from '@/components/settings/UserForm' import UserForm from '@/components/settings/UserForm'
import Rules from '@/components/settings/Rules' import Rules from '@/components/settings/Rules'
import Themes from '@/components/settings/Themes' import Themes from '@/components/settings/Themes'
import { enableExec } from '@/utils/constants'
export default { export default {
name: 'settings', name: 'settings',
@@ -119,7 +122,8 @@ export default {
} }
}, },
computed: { computed: {
...mapState([ 'user' ]) ...mapState([ 'user' ]),
isExecEnabled: () => enableExec
}, },
async created () { async created () {
try { try {

View File

@@ -59,7 +59,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
} }
} }
if !d.user.CanExecute(strings.Split(raw, " ")[0]) { if !d.server.EnableExec || !d.user.CanExecute(strings.Split(raw, " ")[0]) {
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:shadow if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:shadow
wsErr(conn, r, http.StatusInternalServerError, err) wsErr(conn, r, http.StatusInternalServerError, err)
} }

View File

@@ -51,7 +51,7 @@ func handle(fn handleFunc, prefix string, store *storage.Storage, server *settin
} }
status, err := fn(w, r, &data{ status, err := fn(w, r, &data{
Runner: &runner.Runner{Settings: settings}, Runner: &runner.Runner{Enabled: server.EnableExec, Settings: settings},
store: store, store: store,
settings: settings, settings: settings,
server: server, server: server,

View File

@@ -11,6 +11,7 @@ import (
"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"
) )
@@ -97,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) {
@@ -115,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,
}) })
@@ -133,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
} }
@@ -167,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
} }

View File

@@ -40,6 +40,8 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, box *
"ReCaptcha": false, "ReCaptcha": false,
"Theme": d.settings.Branding.Theme, "Theme": d.settings.Branding.Theme,
"EnableThumbs": d.server.EnableThumbnails, "EnableThumbs": d.server.EnableThumbnails,
"ResizePreview": d.server.ResizePreview,
"EnableExec": d.server.EnableExec,
} }
if d.settings.Branding.Files != "" { if d.settings.Branding.Files != "" {
@@ -77,7 +79,14 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, box *
data["Json"] = string(b) data["Json"] = string(b)
index := template.Must(template.New("index").Delims("[{[", "]}]").Parse(box.MustString(file))) fileContents, err := box.String(file)
if err != nil {
if err == os.ErrNotExist {
return http.StatusNotFound, err
}
return http.StatusInternalServerError, err
}
index := template.Must(template.New("index").Delims("[{[", "]}]").Parse(fileContents))
err = index.Execute(w, data) err = index.Execute(w, data)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err

View File

@@ -13,6 +13,7 @@ import (
// Runner is a commands runner. // Runner is a commands runner.
type Runner struct { type Runner struct {
Enabled bool
*settings.Settings *settings.Settings
} }
@@ -21,11 +22,13 @@ func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.Use
path = user.FullPath(path) path = user.FullPath(path)
dst = user.FullPath(dst) dst = user.FullPath(dst)
if val, ok := r.Commands["before_"+evt]; ok { if r.Enabled {
for _, command := range val { if val, ok := r.Commands["before_"+evt]; ok {
err := r.exec(command, "before_"+evt, path, dst, user) for _, command := range val {
if err != nil { err := r.exec(command, "before_"+evt, path, dst, user)
return err if err != nil {
return err
}
} }
} }
} }
@@ -35,11 +38,13 @@ func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.Use
return err return err
} }
if val, ok := r.Commands["after_"+evt]; ok { if r.Enabled {
for _, command := range val { if val, ok := r.Commands["after_"+evt]; ok {
err := r.exec(command, "after_"+evt, path, dst, user) for _, command := range val {
if err != nil { err := r.exec(command, "after_"+evt, path, dst, user)
return err if err != nil {
return err
}
} }
} }
} }

View File

@@ -60,7 +60,9 @@ func Search(fs afero.Fs, scope, query string, checker rules.Checker, found func(
if len(search.Terms) > 0 { if len(search.Terms) > 0 {
for _, term := range search.Terms { for _, term := range search.Terms {
if strings.Contains(path, term) { if strings.Contains(path, term) {
return found(strings.TrimPrefix(originalPath, scope), f) originalPath = strings.TrimPrefix(originalPath, scope)
originalPath = strings.TrimPrefix(originalPath, "/")
return found(originalPath, f)
} }
} }
} }

View File

@@ -40,6 +40,7 @@ type Server struct {
Log string `json:"log"` Log string `json:"log"`
EnableThumbnails bool `json:"enableThumbnails"` EnableThumbnails bool `json:"enableThumbnails"`
ResizePreview bool `json:"resizePreview"` ResizePreview bool `json:"resizePreview"`
EnableExec bool `json:"enableExec"`
} }
// Clean cleans any variables that might need cleaning. // Clean cleans any variables that might need cleaning.