Add shared code support to protect shared link
This commit is contained in:
parent
f3afd5cb79
commit
4a6fdb34ff
15
Dockerfile
15
Dockerfile
@ -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
1
Dockerfile
Symbolic link
@ -0,0 +1 @@
|
||||
Dockerfile.debian
|
||||
@ -1,4 +1,5 @@
|
||||
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 mailcap
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@ func init() {
|
||||
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("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)
|
||||
}
|
||||
@ -250,6 +251,10 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
||||
_, disableExec := getParamB(flags, "disable-exec")
|
||||
server.EnableExec = !disableExec
|
||||
|
||||
if val, set := getParamB(flags, "salt"); set {
|
||||
server.Salt = val
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
|
||||
@ -26,28 +26,30 @@ import (
|
||||
// FileInfo describes a file.
|
||||
type FileInfo struct {
|
||||
*Listing
|
||||
Fs afero.Fs `json:"-"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Extension string `json:"extension"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
Mode os.FileMode `json:"mode"`
|
||||
IsDir bool `json:"isDir"`
|
||||
Type string `json:"type"`
|
||||
Subtitles []string `json:"subtitles,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Checksums map[string]string `json:"checksums,omitempty"`
|
||||
Fs afero.Fs `json:"-"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Extension string `json:"extension"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
Mode os.FileMode `json:"mode"`
|
||||
IsDir bool `json:"isDir"`
|
||||
Type string `json:"type"`
|
||||
Subtitles []string `json:"subtitles,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Checksums map[string]string `json:"checksums,omitempty"`
|
||||
SharedCodeToken string `json:"sharedCodeToken,omitempty"`
|
||||
}
|
||||
|
||||
// FileOptions are the options when getting a file info.
|
||||
type FileOptions struct {
|
||||
Fs afero.Fs
|
||||
Path string
|
||||
Modify bool
|
||||
Expand bool
|
||||
ReadHeader bool
|
||||
Checker rules.Checker
|
||||
Fs afero.Fs
|
||||
Path string
|
||||
Modify bool
|
||||
Expand bool
|
||||
ReadHeader bool
|
||||
SharedCodeToken string
|
||||
Checker rules.Checker
|
||||
}
|
||||
|
||||
// NewFileInfo creates a File object from a path and a given user. This File
|
||||
@ -64,14 +66,15 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||
}
|
||||
|
||||
file := &FileInfo{
|
||||
Fs: opts.Fs,
|
||||
Path: opts.Path,
|
||||
Name: info.Name(),
|
||||
ModTime: info.ModTime(),
|
||||
Mode: info.Mode(),
|
||||
IsDir: info.IsDir(),
|
||||
Size: info.Size(),
|
||||
Extension: filepath.Ext(info.Name()),
|
||||
Fs: opts.Fs,
|
||||
Path: opts.Path,
|
||||
Name: info.Name(),
|
||||
ModTime: info.ModTime(),
|
||||
Mode: info.Mode(),
|
||||
IsDir: info.IsDir(),
|
||||
Size: info.Size(),
|
||||
Extension: filepath.Ext(info.Name()),
|
||||
SharedCodeToken: opts.SharedCodeToken,
|
||||
}
|
||||
|
||||
if opts.Expand {
|
||||
|
||||
@ -4,8 +4,10 @@ export async function list() {
|
||||
return fetchJSON('/api/shares')
|
||||
}
|
||||
|
||||
export async function getHash(hash) {
|
||||
return fetchJSON(`/api/public/share/${hash}`)
|
||||
export async function getHash(hash, shared_code = "") {
|
||||
return fetchJSON(`/api/public/share/${hash}`, {
|
||||
headers: { 'X-SHARED-CODE': shared_code },
|
||||
})
|
||||
}
|
||||
|
||||
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 = `/api/share${url}`
|
||||
if (expires !== '') {
|
||||
url += `?expires=${expires}&unit=${unit}`
|
||||
if (shared_code !== '' || expires !== '') {
|
||||
url += '?'
|
||||
var params = ''
|
||||
if (expires !== '') {
|
||||
params += `expires=${expires}&unit=${unit}`
|
||||
}
|
||||
if (shared_code !== '') {
|
||||
if (params != '') {
|
||||
params += "&"
|
||||
}
|
||||
params += `shared_code=${shared_code}`
|
||||
}
|
||||
url += params
|
||||
}
|
||||
|
||||
return fetchJSON(url, {
|
||||
method: 'POST'
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,167 +1,220 @@
|
||||
<template>
|
||||
<div class="card floating" id="share">
|
||||
<div
|
||||
class="card floating"
|
||||
style="max-width: max-content; width: auto"
|
||||
id="share"
|
||||
>
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('buttons.share') }}</h2>
|
||||
<h2>{{ $t("buttons.share") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<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">
|
||||
<a :href="buildLink(link.hash)" target="_blank">
|
||||
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
|
||||
<template v-else>{{ $t('permanent') }}</template>
|
||||
<template v-if="link.expire !== 0">{{
|
||||
humanTime(link.expire)
|
||||
}}</template>
|
||||
<template v-else>{{ $t("permanent") }}</template>
|
||||
</a>
|
||||
|
||||
<button class="action"
|
||||
<button
|
||||
class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
: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"
|
||||
:data-clipboard-text="buildLink(link.hash)"
|
||||
<button
|
||||
class="action copy-clipboard"
|
||||
:data-clipboard-text="
|
||||
$t(downloadPromptsWithSharedCode(link.shared_code), [
|
||||
buildLink(link.hash),
|
||||
link.shared_code,
|
||||
])
|
||||
"
|
||||
: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>
|
||||
<input v-focus
|
||||
<input
|
||||
v-focus
|
||||
type="number"
|
||||
max="2147483647"
|
||||
min="0"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="time">
|
||||
v-model.trim="time"
|
||||
/>
|
||||
<select v-model="unit" :aria-label="$t('time.unit')">
|
||||
<option value="seconds">{{ $t('time.seconds') }}</option>
|
||||
<option value="minutes">{{ $t('time.minutes') }}</option>
|
||||
<option value="hours">{{ $t('time.hours') }}</option>
|
||||
<option value="days">{{ $t('time.days') }}</option>
|
||||
<option value="seconds">{{ $t("time.seconds") }}</option>
|
||||
<option value="minutes">{{ $t("time.minutes") }}</option>
|
||||
<option value="hours">{{ $t("time.hours") }}</option>
|
||||
<option value="days">{{ $t("time.days") }}</option>
|
||||
</select>
|
||||
<button class="action"
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="$t('buttons.optionalSharedCode')"
|
||||
v-model="shared_code"
|
||||
/>
|
||||
<button
|
||||
class="action"
|
||||
@click="submit"
|
||||
: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>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="button button--flat"
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||
:title="$t('buttons.close')"
|
||||
>
|
||||
{{ $t("buttons.close") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import { share as api } from '@/api'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import moment from 'moment'
|
||||
import Clipboard from 'clipboard'
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import { share as api } from "@/api";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import moment from "moment";
|
||||
import Clipboard from "clipboard";
|
||||
|
||||
export default {
|
||||
name: 'share',
|
||||
name: "share",
|
||||
data: function () {
|
||||
return {
|
||||
time: '',
|
||||
unit: 'hours',
|
||||
time: "",
|
||||
unit: "hours",
|
||||
hasPermanent: false,
|
||||
links: [],
|
||||
clip: null
|
||||
}
|
||||
clip: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'req', 'selected', 'selectedCount' ]),
|
||||
...mapGetters([ 'isListing' ]),
|
||||
url () {
|
||||
...mapState(["req", "selected", "selectedCount"]),
|
||||
...mapGetters(["isListing"]),
|
||||
url() {
|
||||
if (!this.isListing) {
|
||||
return this.$route.path
|
||||
return this.$route.path;
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||
// 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 {
|
||||
const links = await api.get(this.url)
|
||||
this.links = links
|
||||
this.sort()
|
||||
const links = await api.get(this.url);
|
||||
this.links = links;
|
||||
this.sort();
|
||||
|
||||
for (let link of this.links) {
|
||||
if (link.expire === 0) {
|
||||
this.hasPermanent = true
|
||||
break
|
||||
this.hasPermanent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.clip = new Clipboard('.copy-clipboard')
|
||||
this.clip.on('success', () => {
|
||||
this.$showSuccess(this.$t('success.linkCopied'))
|
||||
})
|
||||
mounted() {
|
||||
this.clip = new Clipboard(".copy-clipboard");
|
||||
this.clip.on("success", () => {
|
||||
this.$showSuccess(this.$t("success.linkCopied"));
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.clip.destroy()
|
||||
beforeDestroy() {
|
||||
this.clip.destroy();
|
||||
},
|
||||
methods: {
|
||||
submit: async function () {
|
||||
if (!this.time) return
|
||||
if (!this.time) return;
|
||||
|
||||
try {
|
||||
const res = await api.create(this.url, this.time, this.unit)
|
||||
this.links.push(res)
|
||||
this.sort()
|
||||
const res = await api.create(
|
||||
this.url,
|
||||
this.shared_code,
|
||||
this.time,
|
||||
this.unit
|
||||
);
|
||||
this.links.push(res);
|
||||
this.sort();
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
getPermalink: async function () {
|
||||
try {
|
||||
const res = await api.create(this.url)
|
||||
this.links.push(res)
|
||||
this.sort()
|
||||
this.hasPermanent = true
|
||||
const res = await api.create(this.url, this.shared_code_permalink);
|
||||
this.links.push(res);
|
||||
this.sort();
|
||||
this.hasPermanent = true;
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
deleteLink: async function (event, link) {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await api.remove(link.hash)
|
||||
if (link.expire === 0) this.hasPermanent = false
|
||||
this.links = this.links.filter(item => item.hash !== link.hash)
|
||||
event.preventDefault();
|
||||
try {
|
||||
await api.remove(link.hash);
|
||||
if (link.expire === 0) this.hasPermanent = false;
|
||||
this.links = this.links.filter((item) => item.hash !== link.hash);
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
humanTime (time) {
|
||||
return moment(time * 1000).fromNow()
|
||||
humanTime(time) {
|
||||
return moment(time * 1000).fromNow();
|
||||
},
|
||||
buildLink (hash) {
|
||||
return `${window.location.origin}${baseURL}/share/${hash}`
|
||||
buildLink(hash) {
|
||||
return `${window.location.origin}${baseURL}/share/${hash}`;
|
||||
},
|
||||
sort () {
|
||||
downloadPromptsWithSharedCode(shared_code) {
|
||||
return shared_code != undefined && shared_code != ""
|
||||
? "prompts.downloadPromptsWithSharedCode"
|
||||
: "prompts.downloadPrompts";
|
||||
},
|
||||
sort() {
|
||||
this.links = this.links.sort((a, b) => {
|
||||
if (a.expire === 0) return -1
|
||||
if (b.expire === 0) return 1
|
||||
return new Date(a.expire) - new Date(b.expire)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a.expire === 0) return -1;
|
||||
if (b.expire === 0) return 1;
|
||||
return new Date(a.expire) - new Date(b.expire);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@ -62,4 +62,12 @@
|
||||
|
||||
.share__box__items #listing.list .item .modified {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.share_wrong {
|
||||
background: var(--red);
|
||||
color: #fff;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
animation: .2s opac forwards;
|
||||
}
|
||||
@ -19,6 +19,7 @@
|
||||
"permalink": "Get Permanent Link",
|
||||
"previous": "Previous",
|
||||
"publish": "Publish",
|
||||
"password": "Password",
|
||||
"rename": "Rename",
|
||||
"replace": "Replace",
|
||||
"reportIssue": "Report Issue",
|
||||
@ -29,10 +30,13 @@
|
||||
"selectMultiple": "Select multiple",
|
||||
"share": "Share",
|
||||
"shell": "Toggle shell",
|
||||
"submit": "Submit",
|
||||
"switchView": "Switch view",
|
||||
"toggleSidebar": "Toggle sidebar",
|
||||
"update": "Update",
|
||||
"upload": "Upload"
|
||||
"upload": "Upload",
|
||||
"sharedCode": "Shared code",
|
||||
"optionalSharedCode": "Shared code(optional)"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@ -142,7 +146,10 @@
|
||||
"show": "Show",
|
||||
"size": "Size",
|
||||
"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": {
|
||||
"images": "Images",
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
"more": "更多",
|
||||
"move": "移动",
|
||||
"moveFile": "移动文件",
|
||||
"new": "新",
|
||||
"new": "新建",
|
||||
"next": "下一个",
|
||||
"ok": "确定",
|
||||
"permalink": "获取永久链接",
|
||||
@ -32,7 +32,9 @@
|
||||
"switchView": "切换显示方式",
|
||||
"toggleSidebar": "切换侧边栏",
|
||||
"update": "更新",
|
||||
"upload": "上传"
|
||||
"upload": "上传",
|
||||
"sharedCode": "提取码",
|
||||
"optionalSharedCode": "提取码(可选)"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "下载文件",
|
||||
@ -142,7 +144,10 @@
|
||||
"show": "点击以显示",
|
||||
"size": "大小",
|
||||
"upload": "上传",
|
||||
"uploadMessage": "选择上传项。"
|
||||
"uploadMessage": "选择上传项。",
|
||||
"downloadPrompts": "下载链接:{0}",
|
||||
"downloadPromptsWithSharedCode": "下载链接:{0} 提取码:{1}",
|
||||
"invalidSharedCode": "提取码无效!"
|
||||
},
|
||||
"search": {
|
||||
"images": "图像",
|
||||
|
||||
@ -2,7 +2,7 @@ import { sync } from 'vuex-router-sync'
|
||||
import store from '@/store'
|
||||
import router from '@/router'
|
||||
import i18n from '@/i18n'
|
||||
import Vue from '@/utils/vue'
|
||||
import Vue from 'vue'
|
||||
import { recaptcha, loginPage } from '@/utils/constants'
|
||||
import { login, validateLogin } from '@/utils/auth'
|
||||
import App from '@/App'
|
||||
|
||||
@ -1,45 +1,61 @@
|
||||
<template>
|
||||
<div v-if="!loading">
|
||||
<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>
|
||||
</router-link>
|
||||
|
||||
<span v-for="(link, index) in breadcrumbs" :key="index">
|
||||
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
|
||||
<router-link :to="link.url">{{ link.name }}</router-link>
|
||||
</span>
|
||||
<span class="chevron"
|
||||
><i class="material-icons">keyboard_arrow_right</i></span
|
||||
>
|
||||
<router-link :to="link.url">{{ link.name }}</router-link>
|
||||
</span>
|
||||
</div>
|
||||
<div class="share">
|
||||
<div class="share__box share__box__info">
|
||||
<div class="share__box__header">
|
||||
{{ req.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }}
|
||||
</div>
|
||||
<div class="share__box__element share__box__center share__box__icon">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t('prompts.displayName') }}</strong> {{ req.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 class="share__box__header">
|
||||
{{
|
||||
req.isDir
|
||||
? $t("download.downloadFolder")
|
||||
: $t("download.downloadFile")
|
||||
}}
|
||||
</div>
|
||||
<div class="share__box__element share__box__center share__box__icon">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t("prompts.displayName") }}</strong> {{ req.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="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">
|
||||
{{ $t('files.files') }}
|
||||
{{ $t("files.files") }}
|
||||
</div>
|
||||
<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)"
|
||||
v-bind:index="item.index"
|
||||
v-bind:name="item.name"
|
||||
@ -47,26 +63,40 @@
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size">
|
||||
v-bind:size="item.size"
|
||||
>
|
||||
</item>
|
||||
<div v-if="req.items.length > showLimit" class="item">
|
||||
<div>
|
||||
<p class="name"> + {{ req.items.length - showLimit }} </p>
|
||||
<p class="name">+ {{ req.items.length - showLimit }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="{ active: $store.state.multiple }" 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">
|
||||
<div
|
||||
:class="{ active: $store.state.multiple }"
|
||||
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>
|
||||
</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">
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t('files.lonely') }}</span>
|
||||
<span>{{ $t("files.lonely") }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
@ -74,151 +104,202 @@
|
||||
<div v-else-if="error">
|
||||
<not-found v-if="error.message === '404'"></not-found>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState, mapMutations, mapGetters} from 'vuex';
|
||||
import { share as api } from '@/api'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import Item from "@/components/files/ListingItem"
|
||||
import Forbidden from './errors/403'
|
||||
import NotFound from './errors/404'
|
||||
import InternalError from './errors/500'
|
||||
import { mapState, mapMutations, mapGetters } from "vuex";
|
||||
import { share as api } from "@/api";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import filesize from "filesize";
|
||||
import moment from "moment";
|
||||
import QrcodeVue from "qrcode.vue";
|
||||
import Item from "@/components/files/ListingItem";
|
||||
import Forbidden from "./errors/403";
|
||||
import NotFound from "./errors/404";
|
||||
import InternalError from "./errors/500";
|
||||
|
||||
export default {
|
||||
name: 'share',
|
||||
name: "share",
|
||||
components: {
|
||||
Item,
|
||||
Forbidden,
|
||||
NotFound,
|
||||
InternalError,
|
||||
QrcodeVue
|
||||
QrcodeVue,
|
||||
},
|
||||
data: () => ({
|
||||
error: null,
|
||||
path: '',
|
||||
showLimit: 500
|
||||
path: "",
|
||||
showLimit: 500,
|
||||
}),
|
||||
watch: {
|
||||
'$route': 'fetchData'
|
||||
$route: "fetchData",
|
||||
},
|
||||
created: async function () {
|
||||
const hash = this.$route.params.pathMatch.split('/')[0]
|
||||
this.setHash(hash)
|
||||
await this.fetchData()
|
||||
const hash = this.$route.params.pathMatch.split("/")[0];
|
||||
this.setHash(hash);
|
||||
await this.fetchData();
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
mounted() {
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
},
|
||||
computed: {
|
||||
...mapState(['hash', 'req', 'loading', 'multiple']),
|
||||
...mapGetters(['selectedCount']),
|
||||
...mapState(["hash", "req", "loading", "multiple"]),
|
||||
...mapGetters(["selectedCount"]),
|
||||
icon: function () {
|
||||
if (this.req.isDir) return 'folder'
|
||||
if (this.req.type === 'image') return 'insert_photo'
|
||||
if (this.req.type === 'audio') return 'volume_up'
|
||||
if (this.req.type === 'video') return 'movie'
|
||||
return 'insert_drive_file'
|
||||
if (this.req.isDir) return "folder";
|
||||
if (this.req.type === "image") return "insert_photo";
|
||||
if (this.req.type === "audio") return "volume_up";
|
||||
if (this.req.type === "video") return "movie";
|
||||
return "insert_drive_file";
|
||||
},
|
||||
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 () {
|
||||
return window.location.origin + this.link
|
||||
return window.location.origin + this.link;
|
||||
},
|
||||
humanSize: function () {
|
||||
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 () {
|
||||
return moment(this.req.modified).fromNow()
|
||||
return moment(this.req.modified).fromNow();
|
||||
},
|
||||
breadcrumbs () {
|
||||
let parts = this.path.split('/')
|
||||
breadcrumbs() {
|
||||
let parts = this.path.split("/");
|
||||
|
||||
if (parts[0] === '') {
|
||||
parts.shift()
|
||||
if (parts[0] === "") {
|
||||
parts.shift();
|
||||
}
|
||||
|
||||
if (parts[parts.length - 1] === '') {
|
||||
parts.pop()
|
||||
if (parts[parts.length - 1] === "") {
|
||||
parts.pop();
|
||||
}
|
||||
|
||||
let breadcrumbs = []
|
||||
let breadcrumbs = [];
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i === 0) {
|
||||
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: '/share/' + this.hash + '/' + parts[i] + '/' })
|
||||
} else {
|
||||
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' })
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: "/share/" + this.hash + "/" + parts[i] + "/",
|
||||
});
|
||||
} else {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: breadcrumbs[i - 1].url + parts[i] + "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift()
|
||||
breadcrumbs.shift();
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = '...'
|
||||
breadcrumbs[0].name = "...";
|
||||
}
|
||||
|
||||
return breadcrumbs
|
||||
}
|
||||
return breadcrumbs;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'setHash', 'resetSelected', 'updateRequest', 'setLoading' ]),
|
||||
...mapMutations([
|
||||
"setHash",
|
||||
"resetSelected",
|
||||
"updateRequest",
|
||||
"setLoading",
|
||||
]),
|
||||
base64: function (name) {
|
||||
return window.btoa(unescape(encodeURIComponent(name)))
|
||||
return window.btoa(unescape(encodeURIComponent(name)));
|
||||
},
|
||||
fetchData: async function () {
|
||||
// Reset view information.
|
||||
this.$store.commit('setReload', false)
|
||||
this.$store.commit('resetSelected')
|
||||
this.$store.commit('multiple', false)
|
||||
this.$store.commit('closeHovers')
|
||||
this.$store.commit("setReload", false);
|
||||
this.$store.commit("resetSelected");
|
||||
this.$store.commit("multiple", false);
|
||||
this.$store.commit("closeHovers");
|
||||
|
||||
// Set loading to true and reset the error.
|
||||
this.setLoading(true)
|
||||
this.error = null
|
||||
this.setLoading(true);
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
let file = await api.getHash(encodeURIComponent(this.$route.params.pathMatch))
|
||||
this.path = file.path
|
||||
if (file.isDir) 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)
|
||||
const shared_code = this.shared_code || "";
|
||||
let file = await api.getHash(
|
||||
encodeURIComponent(this.$route.params.pathMatch),
|
||||
shared_code
|
||||
);
|
||||
this.path = file.path;
|
||||
this.sharedCodeToken = file.sharedCodeToken || "";
|
||||
if (file.isDir)
|
||||
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) {
|
||||
this.error = e
|
||||
console.log(e);
|
||||
this.error = e;
|
||||
}
|
||||
},
|
||||
keyEvent (event) {
|
||||
keyEvent(event) {
|
||||
// Esc!
|
||||
if (event.keyCode === 27) {
|
||||
// If we're on a listing, unselect all
|
||||
// files and folders.
|
||||
if (this.selectedCount > 0) {
|
||||
this.resetSelected()
|
||||
this.resetSelected();
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleMultipleSelection () {
|
||||
this.$store.commit('multiple', !this.multiple)
|
||||
}
|
||||
}
|
||||
}
|
||||
toggleMultipleSelection() {
|
||||
this.$store.commit("multiple", !this.multiple);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
7
go.mod
7
go.mod
@ -2,7 +2,7 @@ module github.com/filebrowser/filebrowser/v2
|
||||
|
||||
require (
|
||||
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/asdine/storm v2.1.2+incompatible
|
||||
github.com/caddyserver/caddy v1.0.3
|
||||
@ -10,6 +10,7 @@ require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/golang/protobuf v1.3.3 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
@ -33,11 +34,11 @@ require (
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
|
||||
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b // indirect
|
||||
golang.org/x/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
|
||||
google.golang.org/appengine v1.5.0 // indirect
|
||||
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
|
||||
|
||||
22
go.sum
22
go.sum
@ -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/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.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
|
||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||
github.com/GeertJohan/go.rice v1.0.2 h1:PtRw+Tg3oa3HYwiDBZyvOJ8LdIyf6lAovJJtr7YOAYk=
|
||||
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/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=
|
||||
@ -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-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/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
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.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.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
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/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
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/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
|
||||
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/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=
|
||||
@ -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/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/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
|
||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
|
||||
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
|
||||
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/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
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/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 v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
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/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=
|
||||
@ -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-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-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
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/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@ -9,6 +10,7 @@ import (
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/files"
|
||||
"github.com/filebrowser/filebrowser/v2/share"
|
||||
)
|
||||
|
||||
var withHashFile = func(fn handleFunc) handleFunc {
|
||||
@ -19,6 +21,11 @@ var withHashFile = func(fn handleFunc) handleFunc {
|
||||
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)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
@ -27,12 +34,13 @@ var withHashFile = func(fn handleFunc) handleFunc {
|
||||
d.user = user
|
||||
|
||||
file, err := files.NewFileInfo(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: link.Path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
Fs: d.user.Fs,
|
||||
Path: link.Path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
SharedCodeToken: link.SharedCodeToken,
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
@ -43,11 +51,12 @@ var withHashFile = func(fn handleFunc) handleFunc {
|
||||
d.user.Fs = afero.NewBasePathFs(d.user.Fs, filepath.Dir(link.Path))
|
||||
|
||||
file, err = files.NewFileInfo(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
Checker: d,
|
||||
Fs: d.user.Fs,
|
||||
Path: path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
Checker: d,
|
||||
SharedCodeToken: link.SharedCodeToken,
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
@ -94,3 +103,20 @@ var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request,
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -82,6 +82,10 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
|
||||
rawExpire := r.URL.Query().Get("expires")
|
||||
unit := r.URL.Query().Get("unit")
|
||||
|
||||
if r.Body != nil {
|
||||
defer r.Body.Close()
|
||||
}
|
||||
|
||||
if rawExpire == "" {
|
||||
var err error
|
||||
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
|
||||
|
||||
if rawExpire != "" {
|
||||
//nolint:govet
|
||||
num, err := strconv.Atoi(rawExpire)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
@ -124,11 +129,30 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
|
||||
expire = time.Now().Add(add).Unix()
|
||||
}
|
||||
|
||||
s = &share.Link{
|
||||
Path: r.URL.Path,
|
||||
Hash: str,
|
||||
Expire: expire,
|
||||
UserID: d.user.ID,
|
||||
sharedCode := r.URL.Query().Get("shared_code")
|
||||
if sharedCode == "" {
|
||||
s = &share.Link{
|
||||
Path: r.URL.Path,
|
||||
Hash: str,
|
||||
Expire: expire,
|
||||
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 {
|
||||
|
||||
@ -38,6 +38,7 @@ type Server struct {
|
||||
Port string `json:"port"`
|
||||
Address string `json:"address"`
|
||||
Log string `json:"log"`
|
||||
Salt string `json:"salt"`
|
||||
EnableThumbnails bool `json:"enableThumbnails"`
|
||||
ResizePreview bool `json:"resizePreview"`
|
||||
EnableExec bool `json:"enableExec"`
|
||||
|
||||
@ -2,8 +2,10 @@ package share
|
||||
|
||||
// Link is the information needed to build a shareable link.
|
||||
type Link struct {
|
||||
Hash string `json:"hash" storm:"id,index"`
|
||||
Path string `json:"path" storm:"index"`
|
||||
UserID uint `json:"userID"`
|
||||
Expire int64 `json:"expire"`
|
||||
Hash string `json:"hash" storm:"id,index"`
|
||||
Path string `json:"path" storm:"index"`
|
||||
UserID uint `json:"userID"`
|
||||
Expire int64 `json:"expire"`
|
||||
SharedCode string `json:"shared_code,omitempty"`
|
||||
SharedCodeToken string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
@ -102,7 +102,9 @@ func (s *Storage) Gets(path string, id uint) ([]*Link, error) {
|
||||
if err := s.Delete(link.Hash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
links = append(links[:i], links[i+1:]...)
|
||||
if len(links) > i+1 {
|
||||
links = append(links[:i], links[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
wizard.sh
17
wizard.sh
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
@ -35,7 +35,8 @@ buildAssets () {
|
||||
|
||||
buildBinary () {
|
||||
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
|
||||
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "1"
|
||||
GREP="grep"
|
||||
if [ -x "$(command -v ggrep)" ]; then
|
||||
GREP="ggrep"
|
||||
fi
|
||||
|
||||
semver=$(echo "$1" | $GREP -P '^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)')
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
semver=$(echo "$1" | $GREP -P "^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)")
|
||||
err=$?
|
||||
if [ $err -ne 0 ]; then
|
||||
echo "❌ Not valid semver format. See semver.org"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🧼 Tidying up go modules"
|
||||
go mod tidy
|
||||
|
||||
echo "🐑 Creating a new commit for the new release"
|
||||
git commit --allow-empty -am "chore: version $semver"
|
||||
git tag "$1"
|
||||
git push
|
||||
git push --tags origin
|
||||
|
||||
echo "📦 Done! $semver released."
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user