Compare commits

..

13 Commits

Author SHA1 Message Date
Oleg Lobanov
c746c1931d chore(release): 2.10.0 2020-11-24 12:00:10 +01:00
Oleg Lobanov
586d198d47 fix: fix hanging when reading a named pipe file (closes #1155) 2020-11-24 11:37:31 +01:00
Matt Doyle
9515ceeb42 feat: automatically jump to the next photo when deleting while previewing (#1143) 2020-11-23 19:08:14 +01:00
Julien Loir
e8b4e9af46 feat: add single click mode (#1139) 2020-11-23 19:06:37 +01:00
Tiger Nie
10e399b3c3 feat: add hide dotfiles param (#1148) 2020-11-20 11:51:28 +01:00
Oleg Lobanov
dcbc3286e2 Merge pull request #1133 from ramiresviana/fixes-5
Some fixes and shared view improvements
2020-11-04 17:58:18 +01:00
xufanglu
b185f9b56e chore: fix typo in config_init.go (#1131) 2020-11-04 17:47:36 +01:00
Ramires Viana
7096b3dab9 fix: empty folder in archive 2020-11-04 15:56:27 +00:00
Ramires Viana
36cacdf598 feat: shared item information 2020-11-04 15:56:27 +00:00
Ramires Viana
4e48ffc14d fix: previewer title overflow 2020-11-04 15:56:27 +00:00
Ramires Viana
e119bc55ea feat: shared folder file listing 2020-11-04 15:56:05 +00:00
Ramires Viana
1ce3068a99 fix: resource rename action invalid path 2020-11-03 12:30:56 +00:00
Liubomyr Piadyk
d562d1a60d chore: fix readme typo (#1128)
Commander runner -> Command runner
At other places it's referenced as Command runner.
2020-10-29 17:29:29 +01:00
28 changed files with 326 additions and 111 deletions

View File

@@ -2,6 +2,25 @@
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.10.0](https://github.com/filebrowser/filebrowser/compare/v2.9.0...v2.10.0) (2020-11-24)
### Features
* add hide dotfiles param ([#1148](https://github.com/filebrowser/filebrowser/issues/1148)) ([10e399b](https://github.com/filebrowser/filebrowser/commit/10e399b3c3dbdcfb4465a9d4138e1da6bae0873d))
* add single click mode ([#1139](https://github.com/filebrowser/filebrowser/issues/1139)) ([e8b4e9a](https://github.com/filebrowser/filebrowser/commit/e8b4e9af46d6e99dbeb965dd9727d9ed017d52a2))
* automatically jump to the next photo when deleting while previewing ([#1143](https://github.com/filebrowser/filebrowser/issues/1143)) ([9515cee](https://github.com/filebrowser/filebrowser/commit/9515ceeb42e5ef5267400220a2082dec775e843d))
* shared folder file listing ([e119bc5](https://github.com/filebrowser/filebrowser/commit/e119bc55ea82cefcbcc0571650107dfd5d73f570))
* shared item information ([36cacdf](https://github.com/filebrowser/filebrowser/commit/36cacdf598e4e09f064c8ace0ca7a6c24b23028e))
### Bug Fixes
* empty folder in archive ([7096b3d](https://github.com/filebrowser/filebrowser/commit/7096b3dab92441981c9964e4a6175af0a255d2be))
* fix hanging when reading a named pipe file (closes [#1155](https://github.com/filebrowser/filebrowser/issues/1155)) ([586d198](https://github.com/filebrowser/filebrowser/commit/586d198d47b525eeccc6fe587573a3ad83adb4f6))
* previewer title overflow ([4e48ffc](https://github.com/filebrowser/filebrowser/commit/4e48ffc14d09dabeea12dc495144277db62b5b7d))
* resource rename action invalid path ([1ce3068](https://github.com/filebrowser/filebrowser/commit/1ce3068a99c80c153fd41359255d173bce6e79e8))
## [2.9.0](https://github.com/filebrowser/filebrowser/compare/v2.8.0...v2.9.0) (2020-10-21)

View File

@@ -24,7 +24,7 @@ For installation instructions please refer to our docs at [https://filebrowser.o
[Authentication Method](https://filebrowser.org/configuration/authentication-method) - You can change the way the user authenticates with the filebrowser server
[Commander Runner](https://filebrowser.org/configuration/command-runner) - The command runner is a feature that enables you to execute any shell command you want before or after a certain event.
[Command Runner](https://filebrowser.org/configuration/command-runner) - The command runner is a feature that enables you to execute any shell command you want before or after a certain event.
[Custom Branding](https://filebrowser.org/configuration/custom-branding) - You can customize your File Browser installation by change its name to any other you want, by adding a global custom style sheet and by using your own logotype if you want.

View File

@@ -145,6 +145,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
fmt.Fprintf(w, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)

View File

@@ -61,7 +61,7 @@ override the options.`,
fmt.Printf(`
Congratulations! You've set up your database to use with File Browser.
Now add your first user via 'filebrowser users new' and then you just
Now add your first user via 'filebrowser users add' and then you just
need to call the main command to boot up the server.
`)
printSettings(ser, s, auther)

View File

@@ -302,8 +302,9 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
Signup: false,
CreateUserDir: false,
Defaults: settings.UserDefaults{
Scope: ".",
Locale: "en",
Scope: ".",
Locale: "en",
SingleClick: false,
Perm: users.Permissions{
Admin: false,
Execute: true,

View File

@@ -27,15 +27,16 @@ var usersCmd = &cobra.Command{
func printUsers(usrs []*users.User) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
for _, u := range usrs {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
u.ID,
u.Username,
u.Scope,
u.Locale,
u.ViewMode,
u.SingleClick,
u.Perm.Admin,
u.Perm.Execute,
u.Perm.Create,
@@ -75,6 +76,7 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.String("scope", ".", "scope for users")
flags.String("locale", "en", "locale for users")
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
flags.Bool("singleClick", false, "use single clicks only")
}
func getViewMode(flags *pflag.FlagSet) users.ViewMode {
@@ -95,6 +97,8 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
defaults.Locale = mustGetString(flags, flag.Name)
case "viewMode":
defaults.ViewMode = getViewMode(flags)
case "singleClick":
defaults.SingleClick = mustGetBool(flags, flag.Name)
case "perm.admin":
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
case "perm.execute":

View File

@@ -41,17 +41,19 @@ options you want to change.`,
checkErr(err)
defaults := settings.UserDefaults{
Scope: user.Scope,
Locale: user.Locale,
ViewMode: user.ViewMode,
Perm: user.Perm,
Sorting: user.Sorting,
Commands: user.Commands,
Scope: user.Scope,
Locale: user.Locale,
ViewMode: user.ViewMode,
SingleClick: user.SingleClick,
Perm: user.Perm,
Sorting: user.Sorting,
Commands: user.Commands,
}
getUserDefaults(flags, &defaults, false)
user.Scope = defaults.Scope
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
user.SingleClick = defaults.SingleClick
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting

View File

@@ -135,6 +135,10 @@ func (i *FileInfo) Checksum(algo string) error {
//nolint:goconst
//TODO: use constants
func (i *FileInfo) detectType(modify, saveContent bool) error {
if IsNamedPipe(i.Mode) {
i.Type = "blob"
return nil
}
// failing to detect the type should not return error.
// imagine the situation where a file in a dir with thousands
// of files couldn't be opened: we'd have immediately
@@ -232,9 +236,9 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
continue
}
if strings.HasPrefix(f.Mode().String(), "L") {
if IsSymlink(f.Mode()) {
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead if the target's.
// we stay with the link information instead of the target's.
info, err := i.Fs.Stat(fPath)
if err == nil {
f = info

View File

@@ -1,6 +1,7 @@
package files
import (
"os"
"unicode/utf8"
)
@@ -48,3 +49,11 @@ func isBinary(content []byte, _ int) bool {
}
return false
}
func IsNamedPipe(mode os.FileMode) bool {
return mode&os.ModeNamedPipe != 0
}
func IsSymlink(mode os.FileMode) bool {
return mode&os.ModeSymlink != 0
}

View File

@@ -191,10 +191,11 @@ table th {
}
}
.share__box, .share__box__download {
background: var(--surfaceSecondary) !important;
.share__box {
background: var(--surfacePrimary) !important;
color: var(--textPrimary);
}
.share__box__download {
border-bottom-color: var(--divider);
.share__box__element {
border-top-color: var(--divider);
}

View File

@@ -6,8 +6,8 @@
@dragstart="dragStart"
@dragover="dragOver"
@drop="drop"
@click="click"
@dblclick="open"
@click="itemClick"
@dblclick="dblclick"
@touchstart="touchstart"
:data-dir="isDir"
:aria-label="name"
@@ -47,7 +47,7 @@ export default {
},
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
computed: {
...mapState(['selected', 'req', 'user', 'jwt']),
...mapState(['user', 'selected', 'req', 'user', 'jwt']),
...mapGetters(['selectedCount']),
isSelected () {
return (this.selected.indexOf(this.index) !== -1)
@@ -170,8 +170,12 @@ export default {
action(overwrite, rename)
},
itemClick: function(event) {
if (this.user.singleClick && !this.$store.state.multiple) this.open()
else this.click(event)
},
click: function (event) {
if (this.selectedCount !== 0) event.preventDefault()
if (!this.user.singleClick && this.selectedCount !== 0) event.preventDefault()
if (this.$store.state.selected.indexOf(this.index) !== -1) {
this.removeSelected(this.index)
return
@@ -198,9 +202,12 @@ export default {
return
}
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
if (!this.user.singleClick && !event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
},
dblclick: function () {
if (!this.user.singleClick) this.open()
},
touchstart () {
setTimeout(() => {
this.touches = 0

View File

@@ -5,9 +5,7 @@
<i class="material-icons">close</i>
</button>
<div class="title">
<span>{{ this.name }}</span>
</div>
<div class="title">{{ this.name }}</div>
<preview-size-button v-if="isResizeEnabled && this.req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
@@ -135,16 +133,29 @@ export default {
}
},
async mounted () {
window.addEventListener('keyup', this.key)
window.addEventListener('keydown', this.key)
this.$store.commit('setPreviewMode', true)
this.listing = this.oldReq.items
this.$root.$on('preview-deleted', this.deleted)
this.updatePreview()
},
beforeDestroy () {
window.removeEventListener('keyup', this.key)
window.removeEventListener('keydown', this.key)
this.$store.commit('setPreviewMode', false)
this.$root.$off('preview-deleted', this.deleted)
},
methods: {
deleted () {
this.listing = this.listing.filter(item => item.name !== this.name)
if (this.hasNext) {
this.next()
} else if (!this.hasPrevious && !this.hasNext) {
this.back()
} else {
this.prev()
}
},
back () {
this.$store.commit('setPreviewMode', false)
let uri = url.removeLastDir(this.$route.path) + '/'
@@ -157,7 +168,6 @@ export default {
this.$router.push({ path: this.nextLink })
},
key (event) {
event.preventDefault()
if (this.show !== null) {
return

View File

@@ -20,7 +20,6 @@
<script>
import {mapGetters, mapMutations, mapState} from 'vuex'
import { files as api } from '@/api'
import url from '@/utils/url'
import buttons from '@/utils/buttons'
export default {
@@ -32,17 +31,20 @@ export default {
methods: {
...mapMutations(['closeHovers']),
submit: async function () {
this.closeHovers()
buttons.loading('delete')
try {
if (!this.isListing) {
await api.remove(this.$route.path)
buttons.success('delete')
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
this.$root.$emit('preview-deleted')
this.closeHovers()
return
}
this.closeHovers()
if (this.selectedCount === 0) {
return
}

View File

@@ -24,6 +24,10 @@
<input type="checkbox" :disabled="user.perm.admin" v-model="user.lockPassword"> {{ $t('settings.lockPassword') }}
</p>
<p>
<input type="checkbox" v-model="user.singleClick"> {{ $t('settings.singleClick') }}
</p>
<permissions :perm.sync="user.perm" />
<commands v-if="isExecEnabled" :commands.sync="user.commands" />

View File

@@ -1,29 +1,61 @@
.share__box {
text-align: center;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff;
display: block;
border-radius: 0.2em;
width: 90%;
max-width: 25em;
margin: 6em auto;
.share {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
}
.share__box__download {
width: 100%;
@media (max-width: 736px) {
.share {
display: block;
}
}
.share__box {
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff;
border-radius: 0.2em;
margin: 5px;
overflow: hidden;
}
.share__box__header {
padding: 1em;
cursor: pointer;
background: #ffffff;
color: rgba(0, 0, 0, 0.5);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
text-align: center;
}
.share__box__icon i {
font-size: 10em;
color: #40c4ff;
}
.share__box__center {
text-align: center;
}
.share__box__info {
padding: 2em 3em;
flex: 1 1 auto;
}
.share__box__title {
margin-top: .2em;
overflow: hidden;
text-overflow: ellipsis;
.share__box__element {
padding: 1em;
border-top: 1px solid rgba(0, 0, 0, 0.1);
word-break: break-all;
}
.share__box__items {
text-align: left;
flex: 10 0 25em;
}
.share__box__items #listing.list .item {
cursor: auto;
border-left: 0;
border-right: 0;
border-bottom: 0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.share__box__items #listing.list .item .name {
width: auto;
}

View File

@@ -119,18 +119,23 @@
#previewer .bar {
width: 100%;
text-align: right;
display: flex;
padding: 0.5em;
height: 3.7em;
}
#previewer .bar > * {
flex: 0 0 auto;
}
#previewer .bar .title {
margin-right: auto;
display: block;
flex: 1 1 auto;
padding: 0 1em;
line-height: 2.7em;
line-height: 2.3em;
overflow: hidden;
word-break: break-word;
text-overflow: ellipsis;
font-size: 1.2em;
color: #fff;
}
@@ -220,10 +225,6 @@
word-break: break-word;
}
#previewer .title span {
font-size: 1.2em;
}
#previewer .loading {
height: 100%;
width: 100%;

View File

@@ -32,7 +32,8 @@
"toggleSidebar": "Toggle sidebar",
"update": "Update",
"upload": "Upload",
"permalink": "Get Permanent Link"
"permalink": "Get Permanent Link",
"hideDotfiles": "Hide dotfiles"
},
"success": {
"linkCopied": "Link copied!"
@@ -173,6 +174,7 @@
"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.",
"allowSignup": "Allow users to signup",
"createUserDir": "Auto create user home dir while adding new user",
"singleClick": "Use single clicks to open files and directories",
"insertRegex": "Insert regex expression",
"insertPath": "Insert the path",
"userUpdated": "User updated!",
@@ -188,7 +190,8 @@
"execute": "Execute commands",
"rename": "Rename or move files and directories",
"share": "Share files"
}
},
"hideDotfiles": "Hide dotfiles"
},
"sidebar": {
"help": "Help",
@@ -244,4 +247,4 @@
"downloadFile": "Download File",
"downloadFolder": "Download Folder"
}
}
}

View File

@@ -1,29 +1,56 @@
<template>
<div class="share" v-if="loaded">
<a target="_blank" :href="link">
<div class="share__box">
<div class="share__box__download" v-if="file.isDir">{{ $t('download.downloadFolder') }}</div>
<div class="share__box__download" v-else>{{ $t('download.downloadFile') }}</div>
<div class="share__box__info">
<svg v-if="file.isDir" fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
<svg v-else fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
<h1 class="share__box__title">{{ file.name }}</h1>
<div class="share__box share__box__info">
<div class="share__box__header">
{{ file.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }}
</div>
<div class="share__box__element share__box__center share__box__icon">
<i class="material-icons">{{ file.isDir ? 'folder' : 'insert_drive_file'}}</i>
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.displayName') }}</strong> {{ file.name }}
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.size') }}:</strong> {{ humanSize }}
</div>
<div class="share__box__element share__box__center">
<a target="_blank" :href="link" class="button button--flat">{{ $t('buttons.download') }}</a>
</div>
<div class="share__box__element share__box__center">
<qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
</div>
</div>
<div v-if="file.isDir" class="share__box share__box__items">
<div class="share__box__header" v-if="file.isDir">
{{ $t('files.files') }}
</div>
</a>
<div id="listing" class="list">
<div class="item" v-for="(item) in file.items.slice(0, this.showLimit)" :key="base64(item.name)">
<div>
<i class="material-icons">{{ item.isDir ? 'folder' : (item.type==='image') ? 'insert_photo' : 'insert_drive_file' }}</i>
</div>
<div>
<p class="name">{{ item.name }}</p>
</div>
</div>
<div v-if="file.items.length > showLimit" class="item">
<div>
<p class="name"> + {{ file.items.length - showLimit }} </p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { share as api } from '@/api'
import { baseURL } from '@/utils/constants'
import filesize from 'filesize'
import moment from 'moment'
import QrcodeVue from 'qrcode.vue'
export default {
@@ -34,7 +61,8 @@ export default {
data: () => ({
loaded: false,
notFound: false,
file: null
file: null,
showLimit: 500
}),
watch: {
'$route': 'fetchData'
@@ -52,8 +80,21 @@ export default {
fullLink: function () {
return window.location.origin + this.link
},
humanSize: function () {
if (this.file.isDir) {
return this.file.items.length
}
return filesize(this.file.size)
},
humanTime: function () {
return moment(this.file.modified).fromNow()
}
},
methods: {
base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name)))
},
fetchData: async function () {
try {
this.file = await api.getHash(this.hash)

View File

@@ -6,6 +6,7 @@
</div>
<div class="card-content">
<p><input type="checkbox" v-model="hideDotfiles"> {{ $t('settings.hideDotfiles') }}</p>
<h3>{{ $t('settings.language') }}</h3>
<languages class="input input--block" :locale.sync="locale"></languages>
</div>
@@ -67,6 +68,7 @@ export default {
},
created () {
this.locale = this.user.locale
this.hideDotfiles = this.user.hideDotfiles
},
methods: {
...mapMutations([ 'updateUser' ]),
@@ -90,8 +92,8 @@ export default {
event.preventDefault()
try {
const data = { id: this.user.id, locale: this.locale }
await api.update(data, ['locale'])
const data = { id: this.user.id, locale: this.locale, hideDotfiles: this.hideDotfiles }
await api.update(data, ['locale', 'hideDotfiles'])
this.updateUser(data)
this.$showSuccess(this.$t('settings.settingsUpdated'))
} catch (e) {

View File

@@ -23,9 +23,11 @@ type userInfo struct {
ID uint `json:"id"`
Locale string `json:"locale"`
ViewMode users.ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Perm users.Permissions `json:"perm"`
Commands []string `json:"commands"`
LockPassword bool `json:"lockPassword"`
HideDotfiles bool `json:"hideDotfiles"`
}
type authToken struct {
@@ -172,9 +174,11 @@ func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.Use
ID: user.ID,
Locale: user.Locale,
ViewMode: user.ViewMode,
SingleClick: user.SingleClick,
Perm: user.Perm,
LockPassword: user.LockPassword,
Commands: user.Commands,
HideDotfiles: user.HideDotfiles,
},
StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().Unix(),

View File

@@ -7,6 +7,7 @@ import (
"github.com/tomasen/realip"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/filebrowser/filebrowser/v2/runner"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
@@ -26,6 +27,10 @@ type data struct {
// Check implements rules.Checker.
func (d *data) Check(path string) bool {
if d.user.HideDotfiles && rules.MatchHidden(path) {
return false
}
allow := true
for _, rule := range d.settings.Rules {
if rule.Matches(path) {

View File

@@ -28,7 +28,7 @@ var withHashFile = func(fn handleFunc) handleFunc {
Fs: d.user.Fs,
Path: link.Path,
Modify: d.user.Perm.Modify,
Expand: false,
Expand: true,
Checker: d,
})
if err != nil {
@@ -54,7 +54,15 @@ func ifPathWithName(r *http.Request) string {
}
var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
return renderJSON(w, r, d.raw)
file := d.raw.(*files.FileInfo)
if file.IsDir {
file.Listing.Sorting = files.Sorting{By: "name", Asc: false}
file.Listing.ApplySort()
return renderJSON(w, r, file)
}
return renderJSON(w, r, file)
})
var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {

View File

@@ -1,7 +1,9 @@
package http
import (
"bytes"
"errors"
"io/ioutil"
"net/http"
"net/url"
gopath "path"
@@ -9,6 +11,7 @@ import (
"strings"
"github.com/mholt/archiver"
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/fileutils"
@@ -91,6 +94,11 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
return errToStatus(err), err
}
if files.IsNamedPipe(file.Mode) {
setContentDisposition(w, r, file)
return 0, nil
}
if !file.IsDir {
return rawFileHandler(w, r, file)
}
@@ -110,23 +118,32 @@ func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
return err
}
file, err := d.user.Fs.Open(path)
if err != nil {
return err
var (
file afero.File
arcReadCloser = ioutil.NopCloser(&bytes.Buffer{})
)
if !files.IsNamedPipe(info.Mode()) {
file, err = d.user.Fs.Open(path)
if err != nil {
return err
}
defer file.Close()
arcReadCloser = file
}
defer file.Close()
filename := strings.TrimPrefix(path, commonPath)
filename = strings.TrimPrefix(filename, "/")
err = ar.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: info,
CustomName: filename,
},
ReadCloser: file,
})
if err != nil {
return err
if path != commonPath {
filename := strings.TrimPrefix(path, commonPath)
filename = strings.TrimPrefix(filename, "/")
err = ar.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: info,
CustomName: filename,
},
ReadCloser: arcReadCloser,
})
if err != nil {
return err
}
}
if info.IsDir() {

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
@@ -122,7 +123,7 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
}
err := d.RunHook(func() error {
dir, _ := filepath.Split(r.URL.Path)
dir, _ := path.Split(r.URL.Path)
err := d.user.Fs.MkdirAll(dir, 0775)
if err != nil {
return err
@@ -196,7 +197,8 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
if !d.user.Perm.Rename {
return errors.ErrPermissionDenied
}
dst = filepath.Clean("/" + dst)
src = path.Clean("/" + src)
dst = path.Clean("/" + dst)
return d.user.Fs.Rename(src, dst)
default:
@@ -221,20 +223,20 @@ func checkParent(src, dst string) error {
return nil
}
func addVersionSuffix(path string, fs afero.Fs) string {
func addVersionSuffix(source string, fs afero.Fs) string {
counter := 1
dir, name := filepath.Split(path)
dir, name := path.Split(source)
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
for {
if _, err := fs.Stat(path); err != nil {
if _, err := fs.Stat(source); err != nil {
break
}
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
path = filepath.ToSlash(dir) + renamed
source = path.Join(dir, renamed)
counter++
}
return path
return source
}

View File

@@ -1,6 +1,7 @@
package rules
import (
"path/filepath"
"regexp"
"strings"
)
@@ -18,6 +19,12 @@ type Rule struct {
Regexp *Regexp `json:"regexp"`
}
// MatchHidden matches paths with a basename
// that begins with a dot.
func MatchHidden(path string) bool {
return strings.HasPrefix(filepath.Base(path), ".")
}
// Matches matches a path against a rule.
func (r *Rule) Matches(path string) bool {
if r.Regex {

23
rules/rules_test.go Normal file
View File

@@ -0,0 +1,23 @@
package rules
import "testing"
func TestMatchHidden(t *testing.T) {
cases := map[string]bool{
"/": false,
"/src": false,
"/src/": false,
"/.circleci": true,
"/a/b/c/.docker.json": true,
".docker.json": true,
"Dockerfile": false,
"/Dockerfile": false,
}
for path, want := range cases {
got := MatchHidden(path)
if got != want {
t.Errorf("MatchHidden(%s)=%v; want %v", path, got, want)
}
}
}

View File

@@ -8,12 +8,14 @@ import (
// UserDefaults is a type that holds the default values
// for some fields on User.
type UserDefaults struct {
Scope string `json:"scope"`
Locale string `json:"locale"`
ViewMode users.ViewMode `json:"viewMode"`
Sorting files.Sorting `json:"sorting"`
Perm users.Permissions `json:"perm"`
Commands []string `json:"commands"`
Scope string `json:"scope"`
Locale string `json:"locale"`
ViewMode users.ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Sorting files.Sorting `json:"sorting"`
Perm users.Permissions `json:"perm"`
Commands []string `json:"commands"`
HideDotfiles bool `json:"hideDotfiles"`
}
// Apply applies the default options to a user.
@@ -21,7 +23,9 @@ func (d *UserDefaults) Apply(u *users.User) {
u.Scope = d.Scope
u.Locale = d.Locale
u.ViewMode = d.ViewMode
u.SingleClick = d.SingleClick
u.Perm = d.Perm
u.Sorting = d.Sorting
u.Commands = d.Commands
u.HideDotfiles = d.HideDotfiles
}

View File

@@ -28,11 +28,13 @@ type User struct {
Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"`
ViewMode ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Perm Permissions `json:"perm"`
Commands []string `json:"commands"`
Sorting files.Sorting `json:"sorting"`
Fs afero.Fs `json:"-" yaml:"-"`
Rules []rules.Rule `json:"rules"`
HideDotfiles bool `json:"hideDotfiles"`
}
// GetRules implements rules.Provider.