Add shared code support to protect shared link

This commit is contained in:
Allan 2021-01-30 16:50:08 +08:00 committed by Allan
parent f3afd5cb79
commit 4a6fdb34ff
19 changed files with 506 additions and 286 deletions

View File

@ -1,15 +0,0 @@
FROM alpine:latest as alpine
RUN apk --update add ca-certificates
RUN apk --update add mailcap
FROM scratch
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=alpine /etc/mime.types /etc/mime.types
VOLUME /srv
EXPOSE 80
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]

1
Dockerfile Symbolic link
View File

@ -0,0 +1 @@
Dockerfile.debian

View File

@ -1,4 +1,5 @@
FROM alpine:latest as alpine FROM alpine:latest as alpine
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk --update add ca-certificates RUN apk --update add ca-certificates
RUN apk --update add mailcap RUN apk --update add mailcap

View File

@ -47,6 +47,7 @@ func init() {
flags.Bool("noauth", false, "use the noauth auther when using quick setup") flags.Bool("noauth", false, "use the noauth auther when using quick setup")
flags.String("username", "admin", "username for the first user when using quick config") flags.String("username", "admin", "username for the first user when using quick config")
flags.String("password", "", "hashed password for the first user when using quick config (default \"admin\")") flags.String("password", "", "hashed password for the first user when using quick config (default \"admin\")")
flags.String("salt", "", "The salt to use when for hashing share password. Can be any value. If changed, existing password-protected shares wil stop working.") //nolint:lll
addServerFlags(flags) addServerFlags(flags)
} }
@ -250,6 +251,10 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
_, disableExec := getParamB(flags, "disable-exec") _, disableExec := getParamB(flags, "disable-exec")
server.EnableExec = !disableExec server.EnableExec = !disableExec
if val, set := getParamB(flags, "salt"); set {
server.Salt = val
}
return server return server
} }

View File

@ -38,6 +38,7 @@ type FileInfo struct {
Subtitles []string `json:"subtitles,omitempty"` Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"` Checksums map[string]string `json:"checksums,omitempty"`
SharedCodeToken string `json:"sharedCodeToken,omitempty"`
} }
// FileOptions are the options when getting a file info. // FileOptions are the options when getting a file info.
@ -47,6 +48,7 @@ type FileOptions struct {
Modify bool Modify bool
Expand bool Expand bool
ReadHeader bool ReadHeader bool
SharedCodeToken string
Checker rules.Checker Checker rules.Checker
} }
@ -72,6 +74,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
IsDir: info.IsDir(), IsDir: info.IsDir(),
Size: info.Size(), Size: info.Size(),
Extension: filepath.Ext(info.Name()), Extension: filepath.Ext(info.Name()),
SharedCodeToken: opts.SharedCodeToken,
} }
if opts.Expand { if opts.Expand {

View File

@ -4,8 +4,10 @@ export async function list() {
return fetchJSON('/api/shares') return fetchJSON('/api/shares')
} }
export async function getHash(hash) { export async function getHash(hash, shared_code = "") {
return fetchJSON(`/api/public/share/${hash}`) return fetchJSON(`/api/public/share/${hash}`, {
headers: { 'X-SHARED-CODE': shared_code },
})
} }
export async function get(url) { export async function get(url) {
@ -23,14 +25,24 @@ export async function remove(hash) {
} }
} }
export async function create(url, expires = '', unit = 'hours') { export async function create(url, shared_code = '', expires = '', unit = 'hours') {
url = removePrefix(url) url = removePrefix(url)
url = `/api/share${url}` url = `/api/share${url}`
if (shared_code !== '' || expires !== '') {
url += '?'
var params = ''
if (expires !== '') { if (expires !== '') {
url += `?expires=${expires}&unit=${unit}` params += `expires=${expires}&unit=${unit}`
}
if (shared_code !== '') {
if (params != '') {
params += "&"
}
params += `shared_code=${shared_code}`
}
url += params
} }
return fetchJSON(url, { return fetchJSON(url, {
method: 'POST' method: 'POST',
}) })
} }

View File

@ -1,167 +1,220 @@
<template> <template>
<div class="card floating" id="share"> <div
class="card floating"
style="max-width: max-content; width: auto"
id="share"
>
<div class="card-title"> <div class="card-title">
<h2>{{ $t('buttons.share') }}</h2> <h2>{{ $t("buttons.share") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<ul> <ul>
<li v-if="!hasPermanent">
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
</li>
<li v-for="link in links" :key="link.hash"> <li v-for="link in links" :key="link.hash">
<a :href="buildLink(link.hash)" target="_blank"> <a :href="buildLink(link.hash)" target="_blank">
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template> <template v-if="link.expire !== 0">{{
<template v-else>{{ $t('permanent') }}</template> humanTime(link.expire)
}}</template>
<template v-else>{{ $t("permanent") }}</template>
</a> </a>
<button class="action" <button
class="action"
@click="deleteLink($event, link)" @click="deleteLink($event, link)"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button> :title="$t('buttons.delete')"
>
<i class="material-icons">delete</i>
</button>
<button class="action copy-clipboard" <button
:data-clipboard-text="buildLink(link.hash)" class="action copy-clipboard"
:data-clipboard-text="
$t(downloadPromptsWithSharedCode(link.shared_code), [
buildLink(link.hash),
link.shared_code,
])
"
:aria-label="$t('buttons.copyToClipboard')" :aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button> :title="$t('buttons.copyToClipboard')"
>
<i class="material-icons">content_paste</i>
</button>
</li>
<li v-if="!hasPermanent">
<div style="text-align: right">
<input
type="text"
:placeholder="$t('buttons.optionalSharedCode')"
v-model="shared_code_permalink"
/>
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{
$t("buttons.permalink")
}}</a>
</div>
</li> </li>
<li> <li>
<input v-focus <input
v-focus
type="number" type="number"
max="2147483647" max="2147483647"
min="0" min="0"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="time"> v-model.trim="time"
/>
<select v-model="unit" :aria-label="$t('time.unit')"> <select v-model="unit" :aria-label="$t('time.unit')">
<option value="seconds">{{ $t('time.seconds') }}</option> <option value="seconds">{{ $t("time.seconds") }}</option>
<option value="minutes">{{ $t('time.minutes') }}</option> <option value="minutes">{{ $t("time.minutes") }}</option>
<option value="hours">{{ $t('time.hours') }}</option> <option value="hours">{{ $t("time.hours") }}</option>
<option value="days">{{ $t('time.days') }}</option> <option value="days">{{ $t("time.days") }}</option>
</select> </select>
<button class="action" <input
type="text"
:placeholder="$t('buttons.optionalSharedCode')"
v-model="shared_code"
/>
<button
class="action"
@click="submit" @click="submit"
:aria-label="$t('buttons.create')" :aria-label="$t('buttons.create')"
:title="$t('buttons.create')"><i class="material-icons">add</i></button> :title="$t('buttons.create')"
>
<i class="material-icons">add</i>
</button>
</li> </li>
</ul> </ul>
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat" <button
class="button button--flat"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')" :aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button> :title="$t('buttons.close')"
>
{{ $t("buttons.close") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from "vuex";
import { share as api } from '@/api' import { share as api } from "@/api";
import { baseURL } from '@/utils/constants' import { baseURL } from "@/utils/constants";
import moment from 'moment' import moment from "moment";
import Clipboard from 'clipboard' import Clipboard from "clipboard";
export default { export default {
name: 'share', name: "share",
data: function () { data: function () {
return { return {
time: '', time: "",
unit: 'hours', unit: "hours",
hasPermanent: false, hasPermanent: false,
links: [], links: [],
clip: null clip: null,
} };
}, },
computed: { computed: {
...mapState([ 'req', 'selected', 'selectedCount' ]), ...mapState(["req", "selected", "selectedCount"]),
...mapGetters([ 'isListing' ]), ...mapGetters(["isListing"]),
url() { url() {
if (!this.isListing) { if (!this.isListing) {
return this.$route.path return this.$route.path;
} }
if (this.selectedCount === 0 || this.selectedCount > 1) { if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen. // This shouldn't happen.
return return;
} }
return this.req.items[this.selected[0]].url return this.req.items[this.selected[0]].url;
} },
}, },
async beforeMount() { async beforeMount() {
try { try {
const links = await api.get(this.url) const links = await api.get(this.url);
this.links = links this.links = links;
this.sort() this.sort();
for (let link of this.links) { for (let link of this.links) {
if (link.expire === 0) { if (link.expire === 0) {
this.hasPermanent = true this.hasPermanent = true;
break break;
} }
} }
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e);
} }
}, },
mounted() { mounted() {
this.clip = new Clipboard('.copy-clipboard') this.clip = new Clipboard(".copy-clipboard");
this.clip.on('success', () => { this.clip.on("success", () => {
this.$showSuccess(this.$t('success.linkCopied')) this.$showSuccess(this.$t("success.linkCopied"));
}) });
}, },
beforeDestroy() { beforeDestroy() {
this.clip.destroy() this.clip.destroy();
}, },
methods: { methods: {
submit: async function () { submit: async function () {
if (!this.time) return if (!this.time) return;
try { try {
const res = await api.create(this.url, this.time, this.unit) const res = await api.create(
this.links.push(res) this.url,
this.sort() this.shared_code,
this.time,
this.unit
);
this.links.push(res);
this.sort();
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e);
} }
}, },
getPermalink: async function () { getPermalink: async function () {
try { try {
const res = await api.create(this.url) const res = await api.create(this.url, this.shared_code_permalink);
this.links.push(res) this.links.push(res);
this.sort() this.sort();
this.hasPermanent = true this.hasPermanent = true;
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e);
} }
}, },
deleteLink: async function (event, link) { deleteLink: async function (event, link) {
event.preventDefault() event.preventDefault();
try { try {
await api.remove(link.hash) await api.remove(link.hash);
if (link.expire === 0) this.hasPermanent = false if (link.expire === 0) this.hasPermanent = false;
this.links = this.links.filter(item => item.hash !== link.hash) this.links = this.links.filter((item) => item.hash !== link.hash);
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e);
} }
}, },
humanTime(time) { humanTime(time) {
return moment(time * 1000).fromNow() return moment(time * 1000).fromNow();
}, },
buildLink(hash) { buildLink(hash) {
return `${window.location.origin}${baseURL}/share/${hash}` return `${window.location.origin}${baseURL}/share/${hash}`;
},
downloadPromptsWithSharedCode(shared_code) {
return shared_code != undefined && shared_code != ""
? "prompts.downloadPromptsWithSharedCode"
: "prompts.downloadPrompts";
}, },
sort() { sort() {
this.links = this.links.sort((a, b) => { this.links = this.links.sort((a, b) => {
if (a.expire === 0) return -1 if (a.expire === 0) return -1;
if (b.expire === 0) return 1 if (b.expire === 0) return 1;
return new Date(a.expire) - new Date(b.expire) return new Date(a.expire) - new Date(b.expire);
}) });
} },
} },
} };
</script> </script>

View File

@ -63,3 +63,11 @@
.share__box__items #listing.list .item .modified { .share__box__items #listing.list .item .modified {
width: 25%; width: 25%;
} }
.share_wrong {
background: var(--red);
color: #fff;
padding: .5em;
text-align: center;
animation: .2s opac forwards;
}

View File

@ -19,6 +19,7 @@
"permalink": "Get Permanent Link", "permalink": "Get Permanent Link",
"previous": "Previous", "previous": "Previous",
"publish": "Publish", "publish": "Publish",
"password": "Password",
"rename": "Rename", "rename": "Rename",
"replace": "Replace", "replace": "Replace",
"reportIssue": "Report Issue", "reportIssue": "Report Issue",
@ -29,10 +30,13 @@
"selectMultiple": "Select multiple", "selectMultiple": "Select multiple",
"share": "Share", "share": "Share",
"shell": "Toggle shell", "shell": "Toggle shell",
"submit": "Submit",
"switchView": "Switch view", "switchView": "Switch view",
"toggleSidebar": "Toggle sidebar", "toggleSidebar": "Toggle sidebar",
"update": "Update", "update": "Update",
"upload": "Upload" "upload": "Upload",
"sharedCode": "Shared code",
"optionalSharedCode": "Shared code(optional)"
}, },
"download": { "download": {
"downloadFile": "Download File", "downloadFile": "Download File",
@ -142,7 +146,10 @@
"show": "Show", "show": "Show",
"size": "Size", "size": "Size",
"upload": "Upload", "upload": "Upload",
"uploadMessage": "Select an option to upload." "uploadMessage": "Select an option to upload.",
"downloadPrompts": "Download url: {0}",
"downloadPromptsWithSharedCode": "Download url: {0} Shared code: {1}",
"invalidSharedCode": "Invalid shared code."
}, },
"search": { "search": {
"images": "Images", "images": "Images",

View File

@ -13,7 +13,7 @@
"more": "更多", "more": "更多",
"move": "移动", "move": "移动",
"moveFile": "移动文件", "moveFile": "移动文件",
"new": "新", "new": "新",
"next": "下一个", "next": "下一个",
"ok": "确定", "ok": "确定",
"permalink": "获取永久链接", "permalink": "获取永久链接",
@ -32,7 +32,9 @@
"switchView": "切换显示方式", "switchView": "切换显示方式",
"toggleSidebar": "切换侧边栏", "toggleSidebar": "切换侧边栏",
"update": "更新", "update": "更新",
"upload": "上传" "upload": "上传",
"sharedCode": "提取码",
"optionalSharedCode": "提取码(可选)"
}, },
"download": { "download": {
"downloadFile": "下载文件", "downloadFile": "下载文件",
@ -142,7 +144,10 @@
"show": "点击以显示", "show": "点击以显示",
"size": "大小", "size": "大小",
"upload": "上传", "upload": "上传",
"uploadMessage": "选择上传项。" "uploadMessage": "选择上传项。",
"downloadPrompts": "下载链接:{0}",
"downloadPromptsWithSharedCode": "下载链接:{0} 提取码:{1}",
"invalidSharedCode": "提取码无效!"
}, },
"search": { "search": {
"images": "图像", "images": "图像",

View File

@ -2,7 +2,7 @@ import { sync } from 'vuex-router-sync'
import store from '@/store' import store from '@/store'
import router from '@/router' import router from '@/router'
import i18n from '@/i18n' import i18n from '@/i18n'
import Vue from '@/utils/vue' import Vue from 'vue'
import { recaptcha, loginPage } from '@/utils/constants' import { recaptcha, loginPage } from '@/utils/constants'
import { login, validateLogin } from '@/utils/auth' import { login, validateLogin } from '@/utils/auth'
import App from '@/App' import App from '@/App'

View File

@ -1,45 +1,61 @@
<template> <template>
<div v-if="!loading"> <div v-if="!loading">
<div id="breadcrumbs"> <div id="breadcrumbs">
<router-link :to="'/share/' + hash" :aria-label="$t('files.home')" :title="$t('files.home')"> <router-link
:to="'/share/' + hash"
:aria-label="$t('files.home')"
:title="$t('files.home')"
>
<i class="material-icons">home</i> <i class="material-icons">home</i>
</router-link> </router-link>
<span v-for="(link, index) in breadcrumbs" :key="index"> <span v-for="(link, index) in breadcrumbs" :key="index">
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span> <span class="chevron"
><i class="material-icons">keyboard_arrow_right</i></span
>
<router-link :to="link.url">{{ link.name }}</router-link> <router-link :to="link.url">{{ link.name }}</router-link>
</span> </span>
</div> </div>
<div class="share"> <div class="share">
<div class="share__box share__box__info"> <div class="share__box share__box__info">
<div class="share__box__header"> <div class="share__box__header">
{{ req.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }} {{
req.isDir
? $t("download.downloadFolder")
: $t("download.downloadFile")
}}
</div> </div>
<div class="share__box__element share__box__center share__box__icon"> <div class="share__box__element share__box__center share__box__icon">
<i class="material-icons">{{ icon }}</i> <i class="material-icons">{{ icon }}</i>
</div> </div>
<div class="share__box__element"> <div class="share__box__element">
<strong>{{ $t('prompts.displayName') }}</strong> {{ req.name }} <strong>{{ $t("prompts.displayName") }}</strong> {{ req.name }}
</div> </div>
<div class="share__box__element"> <div class="share__box__element">
<strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }} <strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
</div> </div>
<div class="share__box__element"> <div class="share__box__element">
<strong>{{ $t('prompts.size') }}:</strong> {{ humanSize }} <strong>{{ $t("prompts.size") }}:</strong> {{ humanSize }}
</div> </div>
<div class="share__box__element share__box__center"> <div class="share__box__element share__box__center">
<a target="_blank" :href="link" class="button button--flat">{{ $t('buttons.download') }}</a> <a target="_blank" :href="link" class="button button--flat">{{
$t("buttons.download")
}}</a>
</div> </div>
<div class="share__box__element share__box__center"> <div class="share__box__element share__box__center">
<qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue> <qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
</div> </div>
</div> </div>
<div v-if="req.isDir && req.items.length > 0" class="share__box share__box__items"> <div
v-if="req.isDir && req.items.length > 0"
class="share__box share__box__items"
>
<div class="share__box__header" v-if="req.isDir"> <div class="share__box__header" v-if="req.isDir">
{{ $t('files.files') }} {{ $t("files.files") }}
</div> </div>
<div id="listing" class="list"> <div id="listing" class="list">
<item v-for="(item) in req.items.slice(0, this.showLimit)" <item
v-for="item in req.items.slice(0, this.showLimit)"
:key="base64(item.name)" :key="base64(item.name)"
v-bind:index="item.index" v-bind:index="item.index"
v-bind:name="item.name" v-bind:name="item.name"
@ -47,7 +63,8 @@
v-bind:url="item.url" v-bind:url="item.url"
v-bind:modified="item.modified" v-bind:modified="item.modified"
v-bind:type="item.type" v-bind:type="item.type"
v-bind:size="item.size"> v-bind:size="item.size"
>
</item> </item>
<div v-if="req.items.length > showLimit" class="item"> <div v-if="req.items.length > showLimit" class="item">
<div> <div>
@ -55,18 +72,31 @@
</div> </div>
</div> </div>
<div :class="{ active: $store.state.multiple }" id="multiple-selection"> <div
<p>{{ $t('files.multipleSelectionEnabled') }}</p> :class="{ active: $store.state.multiple }"
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action"> id="multiple-selection"
>
<p>{{ $t("files.multipleSelectionEnabled") }}</p>
<div
@click="$store.commit('multiple', false)"
tabindex="0"
role="button"
:title="$t('files.clear')"
:aria-label="$t('files.clear')"
class="action"
>
<i class="material-icons">clear</i> <i class="material-icons">clear</i>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div v-else-if="req.isDir && req.items.length === 0" class="share__box share__box__items"> <div
v-else-if="req.isDir && req.items.length === 0"
class="share__box share__box__items"
>
<h2 class="message"> <h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i> <i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t('files.lonely') }}</span> <span>{{ $t("files.lonely") }}</span>
</h2> </h2>
</div> </div>
</div> </div>
@ -74,136 +104,187 @@
<div v-else-if="error"> <div v-else-if="error">
<not-found v-if="error.message === '404'"></not-found> <not-found v-if="error.message === '404'"></not-found>
<forbidden v-else-if="error.message === '403'"></forbidden> <forbidden v-else-if="error.message === '403'"></forbidden>
<div v-else-if="error.message === '401'">
<div class="card floating" id="shared_code">
<div class="card-title">
<h2>{{ $t("buttons.sharedCode") }}</h2>
</div>
<div class="share_wrong" v-if="shared_code != undefined && shared_code != ''">
{{ $t("prompts.invalidSharedCode") }}
</div>
<div class="card-content">
<input
v-focus
type="text"
:placeholder="$t('buttons.sharedCode')"
v-model="shared_code"
@keyup.enter="fetchData"
/>
</div>
<div class="card-action">
<button
class="button button--flat"
@click="fetchData"
:aria-label="$t('buttons.submit')"
:title="$t('buttons.submit')"
>
{{ $t("buttons.submit") }}
</button>
</div>
</div>
</div>
<internal-error v-else></internal-error> <internal-error v-else></internal-error>
</div> </div>
</template> </template>
<script> <script>
import {mapState, mapMutations, mapGetters} from 'vuex'; import { mapState, mapMutations, mapGetters } from "vuex";
import { share as api } from '@/api' import { share as api } from "@/api";
import { baseURL } from '@/utils/constants' import { baseURL } from "@/utils/constants";
import filesize from 'filesize' import filesize from "filesize";
import moment from 'moment' import moment from "moment";
import QrcodeVue from 'qrcode.vue' import QrcodeVue from "qrcode.vue";
import Item from "@/components/files/ListingItem" import Item from "@/components/files/ListingItem";
import Forbidden from './errors/403' import Forbidden from "./errors/403";
import NotFound from './errors/404' import NotFound from "./errors/404";
import InternalError from './errors/500' import InternalError from "./errors/500";
export default { export default {
name: 'share', name: "share",
components: { components: {
Item, Item,
Forbidden, Forbidden,
NotFound, NotFound,
InternalError, InternalError,
QrcodeVue QrcodeVue,
}, },
data: () => ({ data: () => ({
error: null, error: null,
path: '', path: "",
showLimit: 500 showLimit: 500,
}), }),
watch: { watch: {
'$route': 'fetchData' $route: "fetchData",
}, },
created: async function () { created: async function () {
const hash = this.$route.params.pathMatch.split('/')[0] const hash = this.$route.params.pathMatch.split("/")[0];
this.setHash(hash) this.setHash(hash);
await this.fetchData() await this.fetchData();
}, },
mounted() { mounted() {
window.addEventListener('keydown', this.keyEvent) window.addEventListener("keydown", this.keyEvent);
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('keydown', this.keyEvent) window.removeEventListener("keydown", this.keyEvent);
}, },
computed: { computed: {
...mapState(['hash', 'req', 'loading', 'multiple']), ...mapState(["hash", "req", "loading", "multiple"]),
...mapGetters(['selectedCount']), ...mapGetters(["selectedCount"]),
icon: function () { icon: function () {
if (this.req.isDir) return 'folder' if (this.req.isDir) return "folder";
if (this.req.type === 'image') return 'insert_photo' if (this.req.type === "image") return "insert_photo";
if (this.req.type === 'audio') return 'volume_up' if (this.req.type === "audio") return "volume_up";
if (this.req.type === 'video') return 'movie' if (this.req.type === "video") return "movie";
return 'insert_drive_file' return "insert_drive_file";
}, },
link: function () { link: function () {
return `${baseURL}/api/public/dl/${this.hash}${this.path}` return `${baseURL}/api/public/dl/${this.hash}${this.path}?shared_code_token=${this.sharedCodeToken}`;
}, },
fullLink: function () { fullLink: function () {
return window.location.origin + this.link return window.location.origin + this.link;
}, },
humanSize: function () { humanSize: function () {
if (this.req.isDir) { if (this.req.isDir) {
return this.req.items.length return this.req.items.length;
} }
return filesize(this.req.size) return filesize(this.req.size);
}, },
humanTime: function () { humanTime: function () {
return moment(this.req.modified).fromNow() return moment(this.req.modified).fromNow();
}, },
breadcrumbs() { breadcrumbs() {
let parts = this.path.split('/') let parts = this.path.split("/");
if (parts[0] === '') { if (parts[0] === "") {
parts.shift() parts.shift();
} }
if (parts[parts.length - 1] === '') { if (parts[parts.length - 1] === "") {
parts.pop() parts.pop();
} }
let breadcrumbs = [] let breadcrumbs = [];
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
if (i === 0) { if (i === 0) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: '/share/' + this.hash + '/' + parts[i] + '/' }) breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: "/share/" + this.hash + "/" + parts[i] + "/",
});
} else { } else {
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' }) breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
} }
} }
if (breadcrumbs.length > 3) { if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) { while (breadcrumbs.length !== 4) {
breadcrumbs.shift() breadcrumbs.shift();
} }
breadcrumbs[0].name = '...' breadcrumbs[0].name = "...";
} }
return breadcrumbs return breadcrumbs;
} },
}, },
methods: { methods: {
...mapMutations([ 'setHash', 'resetSelected', 'updateRequest', 'setLoading' ]), ...mapMutations([
"setHash",
"resetSelected",
"updateRequest",
"setLoading",
]),
base64: function (name) { base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name))) return window.btoa(unescape(encodeURIComponent(name)));
}, },
fetchData: async function () { fetchData: async function () {
// Reset view information. // Reset view information.
this.$store.commit('setReload', false) this.$store.commit("setReload", false);
this.$store.commit('resetSelected') this.$store.commit("resetSelected");
this.$store.commit('multiple', false) this.$store.commit("multiple", false);
this.$store.commit('closeHovers') this.$store.commit("closeHovers");
// Set loading to true and reset the error. // Set loading to true and reset the error.
this.setLoading(true) this.setLoading(true);
this.error = null this.error = null;
try { try {
let file = await api.getHash(encodeURIComponent(this.$route.params.pathMatch)) const shared_code = this.shared_code || "";
this.path = file.path let file = await api.getHash(
if (file.isDir) file.items = file.items.map((item, index) => { encodeURIComponent(this.$route.params.pathMatch),
item.index = index shared_code
item.url = `/share/${this.hash}${this.path}/${encodeURIComponent(item.name)}` );
return item this.path = file.path;
}) this.sharedCodeToken = file.sharedCodeToken || "";
this.updateRequest(file) if (file.isDir)
this.setLoading(false) file.items = file.items.map((item, index) => {
item.index = index;
item.url = `/share/${this.hash}${this.path}/${encodeURIComponent(
item.name
)}`;
return item;
});
this.updateRequest(file);
this.setLoading(false);
} catch (e) { } catch (e) {
this.error = e console.log(e);
this.error = e;
} }
}, },
keyEvent(event) { keyEvent(event) {
@ -212,13 +293,13 @@ export default {
// If we're on a listing, unselect all // If we're on a listing, unselect all
// files and folders. // files and folders.
if (this.selectedCount > 0) { if (this.selectedCount > 0) {
this.resetSelected() this.resetSelected();
} }
} }
}, },
toggleMultipleSelection() { toggleMultipleSelection() {
this.$store.commit('multiple', !this.multiple) this.$store.commit("multiple", !this.multiple);
} },
} },
} };
</script> </script>

7
go.mod
View File

@ -2,7 +2,7 @@ module github.com/filebrowser/filebrowser/v2
require ( require (
github.com/DataDog/zstd v1.4.0 // indirect github.com/DataDog/zstd v1.4.0 // indirect
github.com/GeertJohan/go.rice v1.0.0 github.com/GeertJohan/go.rice v1.0.2
github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56 // indirect github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56 // indirect
github.com/asdine/storm v2.1.2+incompatible github.com/asdine/storm v2.1.2+incompatible
github.com/caddyserver/caddy v1.0.3 github.com/caddyserver/caddy v1.0.3
@ -10,6 +10,7 @@ require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/dsnet/compress v0.0.1 // indirect github.com/dsnet/compress v0.0.1 // indirect
github.com/golang/protobuf v1.3.3 // indirect
github.com/golang/snappy v0.0.1 // indirect github.com/golang/snappy v0.0.1 // indirect
github.com/gorilla/mux v1.7.3 github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.1 github.com/gorilla/websocket v1.4.1
@ -33,11 +34,11 @@ require (
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/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-20210124154548-22da62e12c0c // indirect
golang.org/x/text v0.3.2 // indirect golang.org/x/text v0.3.2 // indirect
google.golang.org/appengine v1.5.0 // indirect google.golang.org/appengine v1.5.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.2.7 gopkg.in/yaml.v2 v2.2.8
) )
go 1.14 go 1.14

22
go.sum
View File

@ -5,8 +5,8 @@ github.com/DataDog/zstd v1.4.0 h1:vhoV+DUHnRZdKW1i5UMjAk2G4JY8wN4ayRfYDNdEhwo=
github.com/DataDog/zstd v1.4.0/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/DataDog/zstd v1.4.0/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg= github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ= github.com/GeertJohan/go.rice v1.0.2 h1:PtRw+Tg3oa3HYwiDBZyvOJ8LdIyf6lAovJJtr7YOAYk=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/GeertJohan/go.rice v1.0.2/go.mod h1:af5vUNlDNkCjOZeSGFgIJxDje9qdjsO6hshx0gTmZt4=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56 h1:3trCIB5GsAOIY8NxlfMztCYIhVsW9V5sZ+brsecjaiI= github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56 h1:3trCIB5GsAOIY8NxlfMztCYIhVsW9V5sZ+brsecjaiI=
github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
@ -33,6 +33,7 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/daaku/go.zipexe v1.0.1 h1:wV4zMsDOI2SZ2m7Tdz1Ps96Zrx+TzaK15VbUaGozw0M= github.com/daaku/go.zipexe v1.0.1 h1:wV4zMsDOI2SZ2m7Tdz1Ps96Zrx+TzaK15VbUaGozw0M=
@ -70,6 +71,8 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -107,6 +110,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE= github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -143,8 +147,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg= github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs= github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
@ -172,7 +176,9 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@ -280,8 +286,8 @@ golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2V
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
@ -318,8 +324,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 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.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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=

View File

@ -1,6 +1,7 @@
package http package http
import ( import (
"errors"
"net/http" "net/http"
"path" "path"
"path/filepath" "path/filepath"
@ -9,6 +10,7 @@ import (
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/files" "github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/share"
) )
var withHashFile = func(fn handleFunc) handleFunc { var withHashFile = func(fn handleFunc) handleFunc {
@ -19,6 +21,11 @@ var withHashFile = func(fn handleFunc) handleFunc {
return errToStatus(err), err return errToStatus(err), err
} }
status, err := authenticateShareRequest(r, link, d.server.Salt)
if err != nil {
return status, err
}
user, err := d.store.Users.Get(d.server.Root, link.UserID) user, err := d.store.Users.Get(d.server.Root, link.UserID)
if err != nil { if err != nil {
return errToStatus(err), err return errToStatus(err), err
@ -33,6 +40,7 @@ var withHashFile = func(fn handleFunc) handleFunc {
Expand: true, Expand: true,
ReadHeader: d.server.TypeDetectionByHeader, ReadHeader: d.server.TypeDetectionByHeader,
Checker: d, Checker: d,
SharedCodeToken: link.SharedCodeToken,
}) })
if err != nil { if err != nil {
return errToStatus(err), err return errToStatus(err), err
@ -48,6 +56,7 @@ var withHashFile = func(fn handleFunc) handleFunc {
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
Expand: true, Expand: true,
Checker: d, Checker: d,
SharedCodeToken: link.SharedCodeToken,
}) })
if err != nil { if err != nil {
return errToStatus(err), err return errToStatus(err), err
@ -94,3 +103,20 @@ var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request,
return rawDirHandler(w, r, d, file) return rawDirHandler(w, r, d, file)
}) })
func authenticateShareRequest(r *http.Request, l *share.Link, salt string) (int, error) {
if l.SharedCode == "" {
return 0, nil
}
sharedCodeToken := r.URL.Query().Get("shared_code_token")
if sharedCodeToken == l.SharedCodeToken {
return 0, nil
}
sharedCode := r.Header.Get("X-SHARED-CODE")
if l.SharedCode != sharedCode {
return http.StatusUnauthorized, errors.New("invalid shared code")
}
return 0, nil
}

View File

@ -82,6 +82,10 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
rawExpire := r.URL.Query().Get("expires") rawExpire := r.URL.Query().Get("expires")
unit := r.URL.Query().Get("unit") unit := r.URL.Query().Get("unit")
if r.Body != nil {
defer r.Body.Close()
}
if rawExpire == "" { if rawExpire == "" {
var err error var err error
s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID) s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID)
@ -104,6 +108,7 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
var expire int64 = 0 var expire int64 = 0
if rawExpire != "" { if rawExpire != "" {
//nolint:govet
num, err := strconv.Atoi(rawExpire) num, err := strconv.Atoi(rawExpire)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
@ -124,11 +129,30 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
expire = time.Now().Add(add).Unix() expire = time.Now().Add(add).Unix()
} }
sharedCode := r.URL.Query().Get("shared_code")
if sharedCode == "" {
s = &share.Link{ s = &share.Link{
Path: r.URL.Path, Path: r.URL.Path,
Hash: str, Hash: str,
Expire: expire, Expire: expire,
UserID: d.user.ID, UserID: d.user.ID,
SharedCodeToken: "",
SharedCode: "",
}
} else {
tokenBuffer := make([]byte, 96)
if _, err := rand.Read(tokenBuffer); err != nil {
return http.StatusInternalServerError, err
}
token := base64.URLEncoding.EncodeToString(tokenBuffer)
s = &share.Link{
Path: r.URL.Path,
Hash: str,
Expire: expire,
UserID: d.user.ID,
SharedCodeToken: token,
SharedCode: sharedCode,
}
} }
if err := d.store.Share.Save(s); err != nil { if err := d.store.Share.Save(s); err != nil {

View File

@ -38,6 +38,7 @@ type Server struct {
Port string `json:"port"` Port string `json:"port"`
Address string `json:"address"` Address string `json:"address"`
Log string `json:"log"` Log string `json:"log"`
Salt string `json:"salt"`
EnableThumbnails bool `json:"enableThumbnails"` EnableThumbnails bool `json:"enableThumbnails"`
ResizePreview bool `json:"resizePreview"` ResizePreview bool `json:"resizePreview"`
EnableExec bool `json:"enableExec"` EnableExec bool `json:"enableExec"`

View File

@ -6,4 +6,6 @@ type Link struct {
Path string `json:"path" storm:"index"` Path string `json:"path" storm:"index"`
UserID uint `json:"userID"` UserID uint `json:"userID"`
Expire int64 `json:"expire"` Expire int64 `json:"expire"`
SharedCode string `json:"shared_code,omitempty"`
SharedCodeToken string `json:"token,omitempty"`
} }

View File

@ -102,9 +102,11 @@ func (s *Storage) Gets(path string, id uint) ([]*Link, error) {
if err := s.Delete(link.Hash); err != nil { if err := s.Delete(link.Hash); err != nil {
return nil, err return nil, err
} }
if len(links) > i+1 {
links = append(links[:i], links[i+1:]...) links = append(links[:i], links[i+1:]...)
} }
} }
}
return links, nil return links, nil
} }

View File

@ -1,4 +1,4 @@
#!/usr/bin/env sh #!/bin/bash
set -e set -e
@ -35,7 +35,8 @@ buildAssets () {
buildBinary () { buildBinary () {
if ! [ -x "$(command -v rice)" ]; then if ! [ -x "$(command -v rice)" ]; then
go install github.com/GeertJohan/go.rice/rice go get github.com/GeertJohan/go.rice
go get github.com/GeertJohan/go.rice/rice
fi fi
cd $REPO/http cd $REPO/http
@ -55,28 +56,24 @@ release () {
echo "❌ This release script requires a single argument corresponding to the semver to be released. See semver.org" echo "❌ This release script requires a single argument corresponding to the semver to be released. See semver.org"
exit 1 exit 1
fi fi
echo "1"
GREP="grep" GREP="grep"
if [ -x "$(command -v ggrep)" ]; then if [ -x "$(command -v ggrep)" ]; then
GREP="ggrep" GREP="ggrep"
fi fi
semver=$(echo "$1" | $GREP -P "^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)")
semver=$(echo "$1" | $GREP -P '^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)') err=$?
if [ $err -ne 0 ]; then
if [ $? -ne 0 ]; then
echo "❌ Not valid semver format. See semver.org" echo "❌ Not valid semver format. See semver.org"
exit 1 exit 1
fi fi
echo "🧼 Tidying up go modules" echo "🧼 Tidying up go modules"
go mod tidy go mod tidy
echo "🐑 Creating a new commit for the new release" echo "🐑 Creating a new commit for the new release"
git commit --allow-empty -am "chore: version $semver" git commit --allow-empty -am "chore: version $semver"
git tag "$1" git tag "$1"
git push git push
git push --tags origin git push --tags origin
echo "📦 Done! $semver released." echo "📦 Done! $semver released."
} }