diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
new file mode 100644
index 00000000..64de64ca
--- /dev/null
+++ b/frontend/.eslintrc.js
@@ -0,0 +1,198 @@
+module.exports = {
+ root: true,
+ parserOptions: {
+ parser: 'babel-eslint',
+ sourceType: 'module'
+ },
+ env: {
+ browser: true,
+ node: true,
+ es6: true
+ },
+ extends: ['plugin:vue/recommended', 'eslint:recommended'],
+
+ // add your custom rules here
+ // it is base on https://github.com/vuejs/eslint-config-vue
+ rules: {
+ 'vue/max-attributes-per-line': [2, {
+ 'singleline': 10,
+ 'multiline': {
+ 'max': 1,
+ 'allowFirstLine': false
+ }
+ }],
+ 'vue/singleline-html-element-content-newline': 'off',
+ 'vue/multiline-html-element-content-newline': 'off',
+ 'vue/name-property-casing': ['error', 'PascalCase'],
+ 'vue/no-v-html': 'off',
+ 'accessor-pairs': 2,
+ 'arrow-spacing': [2, {
+ 'before': true,
+ 'after': true
+ }],
+ 'block-spacing': [2, 'always'],
+ 'brace-style': [2, '1tbs', {
+ 'allowSingleLine': true
+ }],
+ 'camelcase': [0, {
+ 'properties': 'always'
+ }],
+ 'comma-dangle': [2, 'never'],
+ 'comma-spacing': [2, {
+ 'before': false,
+ 'after': true
+ }],
+ 'comma-style': [2, 'last'],
+ 'constructor-super': 2,
+ 'curly': [2, 'multi-line'],
+ 'dot-location': [2, 'property'],
+ 'eol-last': 2,
+ 'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
+ 'generator-star-spacing': [2, {
+ 'before': true,
+ 'after': true
+ }],
+ 'handle-callback-err': [2, '^(err|error)$'],
+ 'indent': [2, 2, {
+ 'SwitchCase': 1
+ }],
+ 'jsx-quotes': [2, 'prefer-single'],
+ 'key-spacing': [2, {
+ 'beforeColon': false,
+ 'afterColon': true
+ }],
+ 'keyword-spacing': [2, {
+ 'before': true,
+ 'after': true
+ }],
+ 'new-cap': [2, {
+ 'newIsCap': true,
+ 'capIsNew': false
+ }],
+ 'new-parens': 2,
+ 'no-array-constructor': 2,
+ 'no-caller': 2,
+ 'no-console': 'off',
+ 'no-class-assign': 2,
+ 'no-cond-assign': 2,
+ 'no-const-assign': 2,
+ 'no-control-regex': 0,
+ 'no-delete-var': 2,
+ 'no-dupe-args': 2,
+ 'no-dupe-class-members': 2,
+ 'no-dupe-keys': 2,
+ 'no-duplicate-case': 2,
+ 'no-empty-character-class': 2,
+ 'no-empty-pattern': 2,
+ 'no-eval': 2,
+ 'no-ex-assign': 2,
+ 'no-extend-native': 2,
+ 'no-extra-bind': 2,
+ 'no-extra-boolean-cast': 2,
+ 'no-extra-parens': [2, 'functions'],
+ 'no-fallthrough': 2,
+ 'no-floating-decimal': 2,
+ 'no-func-assign': 2,
+ 'no-implied-eval': 2,
+ 'no-inner-declarations': [2, 'functions'],
+ 'no-invalid-regexp': 2,
+ 'no-irregular-whitespace': 2,
+ 'no-iterator': 2,
+ 'no-label-var': 2,
+ 'no-labels': [2, {
+ 'allowLoop': false,
+ 'allowSwitch': false
+ }],
+ 'no-lone-blocks': 2,
+ 'no-mixed-spaces-and-tabs': 2,
+ 'no-multi-spaces': 2,
+ 'no-multi-str': 2,
+ 'no-multiple-empty-lines': [2, {
+ 'max': 1
+ }],
+ 'no-native-reassign': 2,
+ 'no-negated-in-lhs': 2,
+ 'no-new-object': 2,
+ 'no-new-require': 2,
+ 'no-new-symbol': 2,
+ 'no-new-wrappers': 2,
+ 'no-obj-calls': 2,
+ 'no-octal': 2,
+ 'no-octal-escape': 2,
+ 'no-path-concat': 2,
+ 'no-proto': 2,
+ 'no-redeclare': 2,
+ 'no-regex-spaces': 2,
+ 'no-return-assign': [2, 'except-parens'],
+ 'no-self-assign': 2,
+ 'no-self-compare': 2,
+ 'no-sequences': 2,
+ 'no-shadow-restricted-names': 2,
+ 'no-spaced-func': 2,
+ 'no-sparse-arrays': 2,
+ 'no-this-before-super': 2,
+ 'no-throw-literal': 2,
+ 'no-trailing-spaces': 2,
+ 'no-undef': 2,
+ 'no-undef-init': 2,
+ 'no-unexpected-multiline': 2,
+ 'no-unmodified-loop-condition': 2,
+ 'no-unneeded-ternary': [2, {
+ 'defaultAssignment': false
+ }],
+ 'no-unreachable': 2,
+ 'no-unsafe-finally': 2,
+ 'no-unused-vars': [2, {
+ 'vars': 'all',
+ 'args': 'none'
+ }],
+ 'no-useless-call': 2,
+ 'no-useless-computed-key': 2,
+ 'no-useless-constructor': 2,
+ 'no-useless-escape': 0,
+ 'no-whitespace-before-property': 2,
+ 'no-with': 2,
+ 'one-var': [2, {
+ 'initialized': 'never'
+ }],
+ 'operator-linebreak': [2, 'after', {
+ 'overrides': {
+ '?': 'before',
+ ':': 'before'
+ }
+ }],
+ 'padded-blocks': [2, 'never'],
+ 'quotes': [2, 'single', {
+ 'avoidEscape': true,
+ 'allowTemplateLiterals': true
+ }],
+ 'semi': [2, 'never'],
+ 'semi-spacing': [2, {
+ 'before': false,
+ 'after': true
+ }],
+ 'space-before-blocks': [2, 'always'],
+ 'space-before-function-paren': [2, 'never'],
+ 'space-in-parens': [2, 'never'],
+ 'space-infix-ops': 2,
+ 'space-unary-ops': [2, {
+ 'words': true,
+ 'nonwords': false
+ }],
+ 'spaced-comment': [2, 'always', {
+ 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
+ }],
+ 'template-curly-spacing': [2, 'never'],
+ 'use-isnan': 2,
+ 'valid-typeof': 2,
+ 'wrap-iife': [2, 'any'],
+ 'yield-star-spacing': [2, 'both'],
+ 'yoda': [2, 'never'],
+ 'prefer-const': 2,
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+ 'object-curly-spacing': [2, 'always', {
+ objectsInObjects: false
+ }],
+ 'array-bracket-spacing': [2, 'never']
+ }
+}
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 1c3d3dff..fb6b5f83 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,17 +1,17 @@
-
+
diff --git a/frontend/src/components/prompts/Info.vue b/frontend/src/components/prompts/Info.vue
index 2265b85c..fb91b6db 100644
--- a/frontend/src/components/prompts/Info.vue
+++ b/frontend/src/components/prompts/Info.vue
@@ -7,8 +7,8 @@
{{ $t('prompts.filesSelected', { count: selected.length }) }}
-
{{ $t('prompts.displayName') }} {{ name }}
-
{{ $t('prompts.size') }}: {{ humanSize }}
+
{{ $t('prompts.displayName') }} {{ name }}
+
{{ $t('prompts.size') }}: {{ humanSize }}
{{ $t('prompts.lastModified') }}: {{ humanTime }}
@@ -25,57 +25,59 @@
- {{ $t('buttons.ok') }}
+ :title="$t('buttons.ok')"
+ @click="$store.commit('closeHovers')"
+ >{{ $t('buttons.ok') }}
diff --git a/frontend/src/components/prompts/NewFile.vue b/frontend/src/components/prompts/NewFile.vue
index 5a6d5cd6..26d89698 100644
--- a/frontend/src/components/prompts/NewFile.vue
+++ b/frontend/src/components/prompts/NewFile.vue
@@ -6,21 +6,21 @@
{{ $t('buttons.cancel') }}
{{ $t('buttons.create') }}
@@ -32,14 +32,14 @@ import { files as api } from '@/api'
import url from '@/utils/url'
export default {
- name: 'new-file',
+ name: 'NewFile',
data: function() {
return {
name: ''
- };
+ }
},
computed: {
- ...mapGetters([ 'isFiles', 'isListing' ])
+ ...mapGetters(['isFiles', 'isListing'])
},
methods: {
submit: async function(event) {
@@ -66,6 +66,6 @@ export default {
this.$store.commit('closeHovers')
}
}
-};
+}
diff --git a/frontend/src/components/prompts/Prompts.vue b/frontend/src/components/prompts/Prompts.vue
index 8e530a27..f799908e 100644
--- a/frontend/src/components/prompts/Prompts.vue
+++ b/frontend/src/components/prompts/Prompts.vue
@@ -1,7 +1,7 @@
@@ -21,7 +21,7 @@ import { mapState } from 'vuex'
import buttons from '@/utils/buttons'
export default {
- name: 'prompts',
+ name: 'Prompts',
components: {
Info,
Delete,
@@ -35,7 +35,7 @@ export default {
Help,
Replace
},
- data: function () {
+ data: function() {
return {
pluginData: {
buttons,
@@ -46,7 +46,7 @@ export default {
},
computed: {
...mapState(['show', 'plugins']),
- currentComponent: function () {
+ currentComponent: function() {
const matched = [
'info',
'help',
@@ -59,16 +59,16 @@ export default {
'download',
'replace',
'share'
- ].indexOf(this.show) >= 0;
+ ].indexOf(this.show) >= 0
- return matched && this.show || null;
+ return matched && this.show || null
},
- showOverlay: function () {
+ showOverlay: function() {
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
}
},
methods: {
- resetPrompts () {
+ resetPrompts() {
this.$store.commit('closeHovers')
}
}
diff --git a/frontend/src/components/prompts/Rename.vue b/frontend/src/components/prompts/Rename.vue
index 1c130bc5..27081c0d 100644
--- a/frontend/src/components/prompts/Rename.vue
+++ b/frontend/src/components/prompts/Rename.vue
@@ -6,19 +6,23 @@
- {{ $t('buttons.cancel') }}
- {{ $t('buttons.cancel') }}
+ {{ $t('buttons.rename') }}
+ :title="$t('buttons.rename')"
+ @click="submit"
+ >{{ $t('buttons.rename') }}
@@ -29,13 +33,13 @@ import url from '@/utils/url'
import { files as api } from '@/api'
export default {
- name: 'rename',
- data: function () {
+ name: 'Rename',
+ data: function() {
return {
name: ''
}
},
- created () {
+ created() {
this.name = this.oldName()
},
computed: {
@@ -43,10 +47,10 @@ export default {
...mapGetters(['isListing'])
},
methods: {
- cancel: function () {
+ cancel: function() {
this.$store.commit('closeHovers')
},
- oldName: function () {
+ oldName: function() {
if (!this.isListing) {
return this.req.name
}
@@ -58,7 +62,7 @@ export default {
return this.req.items[this.selected[0]].name
},
- submit: async function () {
+ submit: async function() {
let oldLink = ''
let newLink = ''
diff --git a/frontend/src/components/prompts/Replace.vue b/frontend/src/components/prompts/Replace.vue
index cf709249..0bdc7f95 100644
--- a/frontend/src/components/prompts/Replace.vue
+++ b/frontend/src/components/prompts/Replace.vue
@@ -9,14 +9,18 @@
- {{ $t('buttons.cancel') }}
- {{ $t('buttons.cancel') }}
+ {{ $t('buttons.replace') }}
+ :title="$t('buttons.replace')"
+ @click="showConfirm"
+ >{{ $t('buttons.replace') }}
@@ -25,7 +29,7 @@
import { mapState } from 'vuex'
export default {
- name: 'replace',
+ name: 'Replace',
computed: mapState(['showConfirm'])
}
diff --git a/frontend/src/components/prompts/Share.vue b/frontend/src/components/prompts/Share.vue
index 456b51e3..5f1f3016 100644
--- a/frontend/src/components/prompts/Share.vue
+++ b/frontend/src/components/prompts/Share.vue
@@ -1,5 +1,5 @@
-
+
{{ $t('buttons.share') }}
@@ -7,7 +7,7 @@
- {{ $t('buttons.close') }}
+ :title="$t('buttons.close')"
+ @click="$store.commit('closeHovers')"
+ >{{ $t('buttons.close') }}
@@ -65,8 +75,8 @@ import moment from 'moment'
import Clipboard from 'clipboard'
export default {
- name: 'share',
- data: function () {
+ name: 'Share',
+ data: function() {
return {
time: '',
unit: 'hours',
@@ -76,9 +86,9 @@ export default {
}
},
computed: {
- ...mapState([ 'req', 'selected', 'selectedCount' ]),
- ...mapGetters([ 'isListing' ]),
- url () {
+ ...mapState(['req', 'selected', 'selectedCount']),
+ ...mapGetters(['isListing']),
+ url() {
if (!this.isListing) {
return this.$route.path
}
@@ -91,13 +101,13 @@ export default {
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()
- for (let link of this.links) {
+ for (const link of this.links) {
if (link.expire === 0) {
this.hasPermanent = true
break
@@ -107,17 +117,17 @@ export default {
this.$showError(e)
}
},
- mounted () {
+ mounted() {
this.clip = new Clipboard('.copy-clipboard')
this.clip.on('success', () => {
this.$showSuccess(this.$t('success.linkCopied'))
})
},
- beforeDestroy () {
+ beforeDestroy() {
this.clip.destroy()
},
methods: {
- submit: async function () {
+ submit: async function() {
if (!this.time) return
try {
@@ -128,7 +138,7 @@ export default {
this.$showError(e)
}
},
- getPermalink: async function () {
+ getPermalink: async function() {
try {
const res = await api.create(this.url)
this.links.push(res)
@@ -138,9 +148,9 @@ export default {
this.$showError(e)
}
},
- deleteLink: async function (event, link) {
+ deleteLink: async function(event, link) {
event.preventDefault()
- try {
+ try {
await api.remove(link.hash)
if (link.expire === 0) this.hasPermanent = false
this.links = this.links.filter(item => item.hash !== link.hash)
@@ -148,13 +158,13 @@ export default {
this.$showError(e)
}
},
- humanTime (time) {
+ humanTime(time) {
return moment(time * 1000).fromNow()
},
- buildLink (hash) {
+ buildLink(hash) {
return `${window.location.origin}${baseURL}/share/${hash}`
},
- sort () {
+ sort() {
this.links = this.links.sort((a, b) => {
if (a.expire === 0) return -1
if (b.expire === 0) return 1
diff --git a/frontend/src/components/settings/Commands.vue b/frontend/src/components/settings/Commands.vue
index f09fe53a..5f1ccf1a 100644
--- a/frontend/src/components/settings/Commands.vue
+++ b/frontend/src/components/settings/Commands.vue
@@ -2,20 +2,20 @@
\ No newline at end of file
+
diff --git a/frontend/src/components/settings/UserForm.vue b/frontend/src/components/settings/UserForm.vue
index 35a872f8..bd990ed7 100644
--- a/frontend/src/components/settings/UserForm.vue
+++ b/frontend/src/components/settings/UserForm.vue
@@ -2,26 +2,26 @@
{{ $t('settings.username') }}
-
+
{{ $t('settings.password') }}
-
+
{{ $t('settings.scope') }}
-
+
{{ $t('settings.language') }}
-
+
- {{ $t('settings.lockPassword') }}
+ {{ $t('settings.lockPassword') }}
@@ -42,21 +42,21 @@ import Permissions from './Permissions'
import Commands from './Commands'
export default {
- name: 'user',
+ name: 'User',
components: {
Permissions,
Languages,
Rules,
Commands
},
- props: [ 'user', 'isNew', 'isDefault' ],
+ props: ['user', 'isNew', 'isDefault'],
computed: {
- passwordPlaceholder () {
+ passwordPlaceholder() {
return this.isNew ? '' : this.$t('settings.avoidChanges')
}
},
watch: {
- 'user.perm.admin': function () {
+ 'user.perm.admin': function() {
if (!this.user.perm.admin) return
this.user.lockPassword = false
}
diff --git a/frontend/src/css/listing.css b/frontend/src/css/listing.css
index 48c78f10..a97fd6b8 100644
--- a/frontend/src/css/listing.css
+++ b/frontend/src/css/listing.css
@@ -1,6 +1,6 @@
#listing h2 {
margin: 0 0 0 0.5em;
- font-size: .9em;
+ font-size: 0.9em;
color: rgba(0, 0, 0, 0.38);
font-weight: 500;
}
@@ -10,7 +10,7 @@
overflow: hidden;
}
-#listing>div {
+#listing > div {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
@@ -22,7 +22,7 @@
display: flex;
flex-wrap: nowrap;
color: #6f6f6f;
- transition: .1s ease background, .1s ease opacity;
+ transition: 0.1s ease background, 0.1s ease opacity;
align-items: center;
cursor: pointer;
}
@@ -52,6 +52,13 @@
vertical-align: bottom;
}
+#listing .item img {
+ width: 4em;
+ height: 4em;
+ margin-right: 0.1em;
+ vertical-align: bottom;
+}
+
.message {
text-align: center;
font-size: 2em;
@@ -64,7 +71,7 @@
.message i {
font-size: 2.5em;
- margin-bottom: .2em;
+ margin-bottom: 0.2em;
display: block;
}
@@ -75,14 +82,14 @@
#listing.mosaic .item {
width: calc(33% - 1em);
- margin: .5em;
+ margin: 0.5em;
padding: 0.5em;
border-radius: 0.2em;
- box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.12);
}
#listing.mosaic .item:hover {
- box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24) !important;
}
#listing.mosaic .header {
@@ -116,7 +123,7 @@
display: none;
}
-#listing .item[aria-selected=true] {
+#listing .item[aria-selected="true"] {
background: var(--blue) !important;
color: #fff !important;
}
@@ -129,6 +136,11 @@
font-size: 2em;
}
+#listing.list .item div:first-of-type img {
+ width: 2em;
+ height: 2em;
+}
+
#listing.list .item div:last-of-type {
width: calc(100% - 3em);
display: flex;
@@ -151,19 +163,19 @@
#listing.list .header i {
font-size: 1.5em;
vertical-align: middle;
- margin-left: .2em;
+ margin-left: 0.2em;
}
#listing.list .item.header {
display: flex !important;
background: #fafafa;
z-index: 999;
- padding: .85em;
+ padding: 0.85em;
border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
-#listing.list .item.header>div:first-child {
+#listing.list .item.header > div:first-child {
width: 0;
}
@@ -175,7 +187,7 @@
color: inherit;
}
-#listing.list .item.header>div:first-child {
+#listing.list .item.header > div:first-child {
width: 0;
}
@@ -193,7 +205,7 @@
#listing.list .header i {
opacity: 0;
- transition: .1s ease all;
+ transition: 0.1s ease all;
}
#listing.list .header p:hover i,
@@ -215,7 +227,7 @@
height: 4em;
padding: 0.5em 0.5em 0.5em 1em;
justify-content: space-between;
- transition: .2s ease bottom;
+ transition: 0.2s ease bottom;
}
#listing #multiple-selection.active {
diff --git a/frontend/src/i18n/index.js b/frontend/src/i18n/index.js
index bdc37b03..44b2c923 100644
--- a/frontend/src/i18n/index.js
+++ b/frontend/src/i18n/index.js
@@ -22,7 +22,7 @@ import zhTW from './zh-tw.json'
Vue.use(VueI18n)
-export function detectLocale () {
+export function detectLocale() {
let locale = (navigator.language || navigator.browserLangugae).toLowerCase()
switch (true) {
case /^ar.*/i.test(locale):
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 520bb8d7..fa612f18 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -9,7 +9,7 @@ import App from '@/App'
sync(store, router)
-async function start () {
+async function start() {
if (loginPage) {
await validateLogin()
} else {
@@ -17,7 +17,7 @@ async function start () {
}
if (recaptcha) {
- await new Promise (resolve => {
+ await new Promise(resolve => {
const check = () => {
if (typeof window.grecaptcha === 'undefined') {
setTimeout(check, 100)
@@ -35,8 +35,8 @@ async function start () {
store,
router,
i18n,
- template: '
',
- components: { App }
+ components: { App },
+ template: '
'
})
}
diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.js
index 747c91be..495d7200 100644
--- a/frontend/src/store/mutations.js
+++ b/frontend/src/store/mutations.js
@@ -52,7 +52,7 @@ const mutations = {
state.plugins.push(value)
},
removeSelected: (state, value) => {
- let i = state.selected.indexOf(value)
+ const i = state.selected.indexOf(value)
if (i === -1) return
state.selected.splice(i, 1)
},
@@ -62,7 +62,7 @@ const mutations = {
updateUser: (state, value) => {
if (typeof value !== 'object') return
- for (let field in value) {
+ for (const field in value) {
if (field === 'locale') {
moment.locale(value[field])
i18n.default.locale = value[field]
diff --git a/frontend/src/utils/auth.js b/frontend/src/utils/auth.js
index ae050ba3..9ee7efbc 100644
--- a/frontend/src/utils/auth.js
+++ b/frontend/src/utils/auth.js
@@ -3,7 +3,7 @@ import router from '@/router'
import { Base64 } from 'js-base64'
import { baseURL } from '@/utils/constants'
-export function parseToken (token) {
+export function parseToken(token) {
const parts = token.split('.')
if (parts.length !== 3) {
@@ -21,7 +21,7 @@ export function parseToken (token) {
store.commit('setUser', data.user)
}
-export async function validateLogin () {
+export async function validateLogin() {
try {
if (localStorage.getItem('jwt')) {
await renew(localStorage.getItem('jwt'))
@@ -31,7 +31,7 @@ export async function validateLogin () {
}
}
-export async function login (username, password, recaptcha) {
+export async function login(username, password, recaptcha) {
const data = { username, password, recaptcha }
const res = await fetch(`${baseURL}/api/login`, {
@@ -51,11 +51,11 @@ export async function login (username, password, recaptcha) {
}
}
-export async function renew (jwt) {
+export async function renew(jwt) {
const res = await fetch(`${baseURL}/api/renew`, {
method: 'POST',
headers: {
- 'X-Auth': jwt,
+ 'X-Auth': jwt
}
})
@@ -68,7 +68,7 @@ export async function renew (jwt) {
}
}
-export async function signup (username, password) {
+export async function signup(username, password) {
const data = { username, password }
const res = await fetch(`${baseURL}/api/signup`, {
@@ -84,9 +84,9 @@ export async function signup (username, password) {
}
}
-export function logout () {
+export function logout() {
store.commit('setJWT', '')
store.commit('setUser', null)
localStorage.setItem('jwt', null)
- router.push({path: '/login'})
+ router.push({ path: '/login' })
}
diff --git a/frontend/src/utils/buttons.js b/frontend/src/utils/buttons.js
index 8536b813..33afa435 100644
--- a/frontend/src/utils/buttons.js
+++ b/frontend/src/utils/buttons.js
@@ -1,5 +1,5 @@
-function loading (button) {
- let el = document.querySelector(`#${button}-button > i`)
+function loading(button) {
+ const el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line
@@ -16,8 +16,8 @@ function loading (button) {
}, 100)
}
-function done (button) {
- let el = document.querySelector(`#${button}-button > i`)
+function done(button) {
+ const el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line
@@ -33,8 +33,8 @@ function done (button) {
}, 100)
}
-function success (button) {
- let el = document.querySelector(`#${button}-button > i`)
+function success(button) {
+ const el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line
diff --git a/frontend/src/utils/cookie.js b/frontend/src/utils/cookie.js
index 5004b602..68e5f5b2 100644
--- a/frontend/src/utils/cookie.js
+++ b/frontend/src/utils/cookie.js
@@ -1,4 +1,4 @@
-export default function (name) {
- let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$')
+export default function(name) {
+ const re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$')
return document.cookie.replace(re, '$1')
}
diff --git a/frontend/src/utils/css.js b/frontend/src/utils/css.js
index 15ab99fe..7f47aa88 100644
--- a/frontend/src/utils/css.js
+++ b/frontend/src/utils/css.js
@@ -1,10 +1,10 @@
-export default function getRule (rules) {
+export default function getRule(rules) {
for (let i = 0; i < rules.length; i++) {
rules[i] = rules[i].toLowerCase()
}
let result = null
- let find = Array.prototype.find
+ const find = Array.prototype.find
find.call(document.styleSheets, styleSheet => {
result = find.call(styleSheet.cssRules, cssRule => {
diff --git a/frontend/src/utils/url.js b/frontend/src/utils/url.js
index 44779d3a..cf723054 100644
--- a/frontend/src/utils/url.js
+++ b/frontend/src/utils/url.js
@@ -1,4 +1,4 @@
-function removeLastDir (url) {
+function removeLastDir(url) {
var arr = url.split('/')
if (arr.pop() === '') {
arr.pop()
@@ -10,14 +10,14 @@ function removeLastDir (url) {
// this code borrow from mozilla
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Examples
function encodeRFC5987ValueChars(str) {
- return encodeURIComponent(str).
- // Note that although RFC3986 reserves "!", RFC5987 does not,
- // so we do not need to escape it
- replace(/['()]/g, escape). // i.e., %27 %28 %29
- replace(/\*/g, '%2A').
- // The following are not required for percent-encoding per RFC5987,
- // so we can allow for a little better readability over the wire: |`^
- replace(/%(?:7C|60|5E)/g, unescape);
+ return encodeURIComponent(str)
+ // Note that although RFC3986 reserves "!", RFC5987 does not,
+ // so we do not need to escape it
+ .replace(/['()]/g, escape) // i.e., %27 %28 %29
+ .replace(/\*/g, '%2A')
+ // The following are not required for percent-encoding per RFC5987,
+ // so we can allow for a little better readability over the wire: |`^
+ .replace(/%(?:7C|60|5E)/g, unescape)
}
export default {
diff --git a/frontend/src/utils/vue.js b/frontend/src/utils/vue.js
index b96d5816..6749305e 100644
--- a/frontend/src/utils/vue.js
+++ b/frontend/src/utils/vue.js
@@ -24,19 +24,19 @@ Vue.prototype.$showSuccess = (message) => {
}
Vue.prototype.$showError = (error) => {
- let btns = [
- Noty.button(i18n.t('buttons.close'), '', function () {
+ const btns = [
+ Noty.button(i18n.t('buttons.close'), '', function() {
n.close()
})
]
if (!disableExternal) {
- btns.unshift(Noty.button(i18n.t('buttons.reportIssue'), '', function () {
+ btns.unshift(Noty.button(i18n.t('buttons.reportIssue'), '', function() {
window.open('https://github.com/filebrowser/filebrowser/issues/new/choose')
}))
}
- let n = new Noty(Object.assign({}, notyDefault, {
+ const n = new Noty(Object.assign({}, notyDefault, {
text: error.message || error,
type: 'error',
timeout: null,
@@ -47,7 +47,7 @@ Vue.prototype.$showError = (error) => {
}
Vue.directive('focus', {
- inserted: function (el) {
+ inserted: function(el) {
el.focus()
}
})
diff --git a/frontend/src/views/Files.vue b/frontend/src/views/Files.vue
index b9d4ddeb..3684677f 100644
--- a/frontend/src/views/Files.vue
+++ b/frontend/src/views/Files.vue
@@ -11,13 +11,13 @@
-
-
-
+
+
+
-
-
-
+
+
+
{{ $t('files.loading') }}
@@ -35,12 +35,12 @@ import Listing from '@/components/files/Listing'
import { files as api } from '@/api'
import { mapGetters, mapState, mapMutations } from 'vuex'
-function clean (path) {
+function clean(path) {
return path.endsWith('/') ? path.slice(0, -1) : path
}
export default {
- name: 'files',
+ name: 'Files',
components: {
Forbidden,
NotFound,
@@ -63,11 +63,11 @@ export default {
'multiple',
'loading'
]),
- isPreview () {
+ isPreview() {
return !this.loading && !this.isListing && !this.isEditor
},
- breadcrumbs () {
- let parts = this.$route.path.split('/')
+ breadcrumbs() {
+ const parts = this.$route.path.split('/')
if (parts[0] === '') {
parts.shift()
@@ -77,7 +77,7 @@ export default {
parts.pop()
}
- let breadcrumbs = []
+ const breadcrumbs = []
for (let i = 0; i < parts.length; i++) {
if (i === 0) {
@@ -100,34 +100,34 @@ export default {
return breadcrumbs
}
},
- data: function () {
+ data: function() {
return {
error: null
}
},
- created () {
- this.fetchData()
- },
watch: {
'$route': 'fetchData',
- 'reload': function () {
+ 'reload': function() {
this.fetchData()
}
},
- mounted () {
+ created() {
+ this.fetchData()
+ },
+ mounted() {
window.addEventListener('keydown', this.keyEvent)
window.addEventListener('scroll', this.scroll)
},
- beforeDestroy () {
+ beforeDestroy() {
window.removeEventListener('keydown', this.keyEvent)
window.removeEventListener('scroll', this.scroll)
},
- destroyed () {
+ destroyed() {
this.$store.commit('updateRequest', {})
},
methods: {
- ...mapMutations([ 'setLoading' ]),
- async fetchData () {
+ ...mapMutations(['setLoading']),
+ async fetchData() {
// Reset view information.
this.$store.commit('setReload', false)
this.$store.commit('resetSelected')
@@ -157,7 +157,7 @@ export default {
this.setLoading(false)
}
},
- keyEvent (event) {
+ keyEvent(event) {
// Esc!
if (event.keyCode === 27) {
this.$store.commit('closeHovers')
@@ -212,7 +212,7 @@ export default {
}
}
},
- scroll () {
+ scroll() {
if (this.req.kind !== 'listing' || this.$store.state.user.viewMode === 'mosaic') return
let top = 112 - window.scrollY
@@ -223,10 +223,10 @@ export default {
document.querySelector('#listing.list .item.header').style.top = top + 'px'
},
- openSidebar () {
+ openSidebar() {
this.$store.commit('showHover', 'sidebar')
},
- openSearch () {
+ openSearch() {
this.$store.commit('showHover', 'search')
}
}
diff --git a/frontend/src/views/Layout.vue b/frontend/src/views/Layout.vue
index 8db58b32..07704780 100644
--- a/frontend/src/views/Layout.vue
+++ b/frontend/src/views/Layout.vue
@@ -1,15 +1,15 @@
@@ -21,7 +21,7 @@ import SiteHeader from '@/components/Header'
import Shell from '@/components/Shell'
export default {
- name: 'layout',
+ name: 'Layout',
components: {
Sidebar,
SiteHeader,
@@ -29,11 +29,11 @@ export default {
Shell
},
computed: {
- ...mapGetters([ 'isLogged' ]),
- ...mapState([ 'user' ])
+ ...mapGetters(['isLogged']),
+ ...mapState(['user'])
},
watch: {
- '$route': function () {
+ '$route': function() {
this.$store.commit('resetSelected')
this.$store.commit('multiple', false)
if (this.$store.state.show !== 'success') this.$store.commit('closeHovers')
diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue
index 5464a053..f89b9f98 100644
--- a/frontend/src/views/Login.vue
+++ b/frontend/src/views/Login.vue
@@ -5,14 +5,14 @@
{{ name }}
{{ error }}
-
-
-
+
+
+
-
+
- {{ createMode ? $t('login.loginInstead') : $t('login.createAnAccount') }}
+
{{ createMode ? $t('login.loginInstead') : $t('login.createAnAccount') }}
@@ -22,13 +22,8 @@ import * as auth from '@/utils/auth'
import { name, logoURL, recaptcha, recaptchaKey, signup } from '@/utils/constants'
export default {
- name: 'login',
- computed: {
- signup: () => signup,
- name: () => name,
- logoURL: () => logoURL
- },
- data: function () {
+ name: 'Login',
+ data: function() {
return {
createMode: false,
error: '',
@@ -38,7 +33,12 @@ export default {
passwordConfirm: ''
}
},
- mounted () {
+ computed: {
+ signup: () => signup,
+ name: () => name,
+ logoURL: () => logoURL
+ },
+ mounted() {
if (!recaptcha) return
window.grecaptcha.render('recaptcha', {
@@ -46,10 +46,10 @@ export default {
})
},
methods: {
- toggleMode () {
+ toggleMode() {
this.createMode = !this.createMode
},
- async submit (event) {
+ async submit(event) {
event.preventDefault()
event.stopPropagation()
@@ -83,7 +83,7 @@ export default {
await auth.login(this.username, this.password, captcha)
this.$router.push({ path: redirect })
} catch (e) {
- if (e.message == 409) {
+ if (e.message === 409) {
this.error = this.$t('login.usernameTaken')
} else {
this.error = this.$t('login.wrongCredentials')
diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue
index 5b010243..3919c671 100644
--- a/frontend/src/views/Settings.vue
+++ b/frontend/src/views/Settings.vue
@@ -1,12 +1,12 @@
-
+
{{ $t('settings.profileSettings') }}
{{ $t('settings.globalSettings') }}
{{ $t('settings.userManagement') }}
-
+
@@ -14,7 +14,7 @@
import { mapState } from 'vuex'
export default {
- name: 'settings',
- computed: mapState([ 'user' ])
+ name: 'Settings',
+ computed: mapState(['user'])
}
diff --git a/frontend/src/views/Share.vue b/frontend/src/views/Share.vue
index bbc15d75..ee8bc9e6 100644
--- a/frontend/src/views/Share.vue
+++ b/frontend/src/views/Share.vue
@@ -1,20 +1,20 @@
-
+
-
{{ $t('download.downloadFolder') }}
-
{{ $t('download.downloadFile') }}
+
{{ $t('download.downloadFolder') }}
+
{{ $t('download.downloadFile') }}
-
-
+
+
-
-
+
+
{{ file.name }}
-
+
@@ -27,7 +27,7 @@ import { baseURL } from '@/utils/constants'
import QrcodeVue from 'qrcode.vue'
export default {
- name: 'share',
+ name: 'Share',
components: {
QrcodeVue
},
@@ -36,25 +36,25 @@ export default {
notFound: false,
file: null
}),
+ computed: {
+ hash: function() {
+ return this.$route.params.pathMatch
+ },
+ link: function() {
+ return `${baseURL}/api/public/dl/${this.hash}/${encodeURI(this.file.name)}`
+ },
+ fullLink: function() {
+ return window.location.origin + this.link
+ }
+ },
watch: {
'$route': 'fetchData'
},
- created: function () {
+ created: function() {
this.fetchData()
},
- computed: {
- hash: function () {
- return this.$route.params.pathMatch
- },
- link: function () {
- return `${baseURL}/api/public/dl/${this.hash}/${encodeURI(this.file.name)}`
- },
- fullLink: function () {
- return window.location.origin + this.link
- },
- },
methods: {
- fetchData: async function () {
+ fetchData: async function() {
try {
this.file = await api.getHash(this.hash)
this.loaded = true
diff --git a/frontend/src/views/errors/403.vue b/frontend/src/views/errors/403.vue
index 47c6c897..7d7267d4 100644
--- a/frontend/src/views/errors/403.vue
+++ b/frontend/src/views/errors/403.vue
@@ -8,6 +8,6 @@
diff --git a/frontend/src/views/errors/404.vue b/frontend/src/views/errors/404.vue
index 61dbe144..856ec5a7 100644
--- a/frontend/src/views/errors/404.vue
+++ b/frontend/src/views/errors/404.vue
@@ -8,6 +8,6 @@
diff --git a/frontend/src/views/errors/500.vue b/frontend/src/views/errors/500.vue
index 0bd86786..f68febdb 100644
--- a/frontend/src/views/errors/500.vue
+++ b/frontend/src/views/errors/500.vue
@@ -8,6 +8,6 @@
diff --git a/frontend/src/views/settings/Global.vue b/frontend/src/views/settings/Global.vue
index a0fbfa8a..af042b58 100644
--- a/frontend/src/views/settings/Global.vue
+++ b/frontend/src/views/settings/Global.vue
@@ -1,14 +1,14 @@
-
+
@@ -106,25 +106,25 @@ import Rules from '@/components/settings/Rules'
import Themes from '@/components/settings/Themes'
export default {
- name: 'settings',
+ name: 'Settings',
components: {
Themes,
UserForm,
Rules
},
- data: function () {
+ data: function() {
return {
originalSettings: null,
settings: null
}
},
computed: {
- ...mapState([ 'user' ])
+ ...mapState(['user'])
},
- async created () {
+ async created() {
try {
const original = await api.get()
- let settings = { ...original, commands: [] }
+ const settings = { ...original, commands: [] }
for (const key in original.commands) {
settings.commands.push({
@@ -142,9 +142,9 @@ export default {
}
},
methods: {
- capitalize (name, where = '_') {
+ capitalize(name, where = '_') {
if (where === 'caps') where = /(?=[A-Z])/
- let splitted = name.split(where)
+ const splitted = name.split(where)
name = ''
for (let i = 0; i < splitted.length; i++) {
@@ -153,8 +153,8 @@ export default {
return name.slice(0, -1)
},
- async save () {
- let settings = {
+ async save() {
+ const settings = {
...this.settings,
shell: this.settings.shell.trim().split(' ').filter(s => s !== ''),
commands: {}
diff --git a/frontend/src/views/settings/Profile.vue b/frontend/src/views/settings/Profile.vue
index 32d80404..57e5d981 100644
--- a/frontend/src/views/settings/Profile.vue
+++ b/frontend/src/views/settings/Profile.vue
@@ -1,13 +1,13 @@
-
+
{{ $t('settings.profileSettings') }}
{{ $t('settings.language') }}
-
+
@@ -15,14 +15,14 @@
-
+
{{ $t('settings.changePassword') }}
-
-
+
+
@@ -38,11 +38,11 @@ import { users as api } from '@/api'
import Languages from '@/components/settings/Languages'
export default {
- name: 'settings',
+ name: 'Settings',
components: {
Languages
},
- data: function () {
+ data: function() {
return {
password: '',
passwordConf: '',
@@ -50,8 +50,8 @@ export default {
}
},
computed: {
- ...mapState([ 'user' ]),
- passwordClass () {
+ ...mapState(['user']),
+ passwordClass() {
const baseClass = 'input input--block'
if (this.password === '' && this.passwordConf === '') {
@@ -65,12 +65,12 @@ export default {
return `${baseClass} input--red`
}
},
- created () {
+ created() {
this.locale = this.user.locale
},
methods: {
- ...mapMutations([ 'updateUser' ]),
- async updatePassword (event) {
+ ...mapMutations(['updateUser']),
+ async updatePassword(event) {
event.preventDefault()
if (this.password !== this.passwordConf || this.password === '') {
@@ -86,7 +86,7 @@ export default {
this.$showError(e)
}
},
- async updateSettings (event) {
+ async updateSettings(event) {
event.preventDefault()
try {
diff --git a/frontend/src/views/settings/User.vue b/frontend/src/views/settings/User.vue
index 01581d21..77de6ac9 100644
--- a/frontend/src/views/settings/User.vue
+++ b/frontend/src/views/settings/User.vue
@@ -1,27 +1,29 @@
-
+
{{ $t('settings.newUser') }}
{{ $t('settings.user') }} {{ user.username }}
-
+
{{ $t('buttons.delete') }}
+ :title="$t('buttons.delete')"
+ @click.prevent="deletePrompt"
+ >{{ $t('buttons.delete') }}
+ :value="$t('buttons.save')"
+ >
@@ -31,15 +33,19 @@
-
+ :title="$t('buttons.cancel')"
+ @click="closeHovers"
+ >
{{ $t('buttons.cancel') }}
-
+
{{ $t('buttons.delete') }}
@@ -54,7 +60,7 @@ import UserForm from '@/components/settings/UserForm'
import deepClone from 'lodash.clonedeep'
export default {
- name: 'user',
+ name: 'User',
components: {
UserForm
},
@@ -65,27 +71,27 @@ export default {
loaded: false
}
},
- created () {
- this.fetchData()
- },
computed: {
- isNew () {
+ isNew() {
return this.$route.path === '/settings/users/new'
}
},
watch: {
'$route': 'fetchData',
- 'user.perm.admin': function () {
+ 'user.perm.admin': function() {
if (!this.user.perm.admin) return
this.user.lockPassword = false
}
},
+ created() {
+ this.fetchData()
+ },
methods: {
- ...mapMutations([ 'closeHovers', 'showHover', 'setUser' ]),
- async fetchData () {
+ ...mapMutations(['closeHovers', 'showHover', 'setUser']),
+ async fetchData() {
try {
if (this.isNew) {
- let { defaults } = await settings.get()
+ const { defaults } = await settings.get()
this.user = {
...defaults,
username: '',
@@ -104,10 +110,10 @@ export default {
this.$router.push({ path: '/settings/users/new' })
}
},
- deletePrompt () {
+ deletePrompt() {
this.showHover('deleteUser')
},
- async deleteUser (event) {
+ async deleteUser(event) {
event.preventDefault()
try {
@@ -118,9 +124,9 @@ export default {
this.$showError(e)
}
},
- async save (event) {
+ async save(event) {
event.preventDefault()
- let user = {
+ const user = {
...this.originalUser,
...this.user
}
diff --git a/frontend/src/views/settings/Users.vue b/frontend/src/views/settings/Users.vue
index fcbe100b..10389e2c 100644
--- a/frontend/src/views/settings/Users.vue
+++ b/frontend/src/views/settings/Users.vue
@@ -11,7 +11,7 @@
{{ $t('settings.username') }}
{{ $t('settings.admin') }}
{{ $t('settings.scope') }}
-
+
@@ -31,13 +31,13 @@
import { users as api } from '@/api'
export default {
- name: 'users',
- data: function () {
+ name: 'Users',
+ data: function() {
return {
users: []
}
},
- async created () {
+ async created() {
try {
this.users = await api.getAll()
} catch (e) {
diff --git a/frontend/vue.config.js b/frontend/vue.config.js
index c966ee81..2337421e 100644
--- a/frontend/vue.config.js
+++ b/frontend/vue.config.js
@@ -1,4 +1,4 @@
module.exports = {
runtimeCompiler: true,
publicPath: '[{[ .StaticURL ]}]'
-}
\ No newline at end of file
+}
diff --git a/go.mod b/go.mod
index 043493cc..05df28b2 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
github.com/caddyserver/caddy v1.0.3
github.com/daaku/go.zipexe v1.0.1 // indirect
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/snappy v0.0.1 // indirect
github.com/gorilla/mux v1.7.3
@@ -29,6 +30,7 @@ require (
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
go.etcd.io/bbolt v1.3.3
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
+ golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b // indirect
golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect
golang.org/x/text v0.3.2 // indirect
diff --git a/go.sum b/go.sum
index 271709a8..6ce903fc 100644
--- a/go.sum
+++ b/go.sum
@@ -43,6 +43,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
+github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@@ -239,6 +241,9 @@ golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
diff --git a/http/http.go b/http/http.go
index 20828f44..5c1441e7 100644
--- a/http/http.go
+++ b/http/http.go
@@ -59,6 +59,7 @@ func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler,
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
+ api.PathPrefix("/thumbnail").Handler(monkey(thumbnailHandler, "/api/thumbnail")).Methods("GET")
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")
diff --git a/http/thumbnail.go b/http/thumbnail.go
new file mode 100644
index 00000000..acffa078
--- /dev/null
+++ b/http/thumbnail.go
@@ -0,0 +1,92 @@
+package http
+
+import (
+ "github.com/disintegration/imaging"
+ "github.com/filebrowser/filebrowser/v2/errors"
+ "github.com/filebrowser/filebrowser/v2/files"
+ "image"
+ "image/gif"
+ "image/jpeg"
+ "image/png"
+ "mime"
+ "net/http"
+ "net/url"
+)
+
+var thumbnailHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
+ if !d.user.Perm.Download {
+ return http.StatusAccepted, nil
+ }
+
+ file, err := files.NewFileInfo(files.FileOptions{
+ Fs: d.user.Fs,
+ Path: r.URL.Path,
+ Modify: d.user.Perm.Modify,
+ Expand: true,
+ Checker: d,
+ })
+ if err != nil {
+ return errToStatus(err), err
+ }
+
+ if file.IsDir || file.Type != "image" {
+ return http.StatusNotFound, nil
+ }
+
+ return thumbnailFileHandler(w, r, file)
+})
+
+func thumbnailFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
+ fd, err := file.Fs.Open(file.Path)
+ if err != nil {
+ return errToStatus(err), err
+ }
+ defer fd.Close()
+
+ if r.URL.Query().Get("inline") == "true" {
+ w.Header().Set("Content-Disposition", "inline")
+ } else {
+ // As per RFC6266 section 4.3
+ w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
+ }
+
+ srcImg, err := imaging.Decode(fd, imaging.AutoOrientation(true))
+ if err != nil {
+ return errToStatus(err), err
+ }
+ dstImg := fitResizeImage(srcImg)
+ w.Header().Add("Content-Type", mime.TypeByExtension(file.Extension))
+ err = func() error {
+ switch file.Extension {
+ case ".jpg", ".jpeg":
+ return jpeg.Encode(w, dstImg, nil)
+ case ".png":
+ return png.Encode(w, dstImg)
+ case ".gif":
+ return gif.Encode(w, dstImg, nil)
+ default:
+ return errors.ErrNotExist
+ }
+ }()
+ if err != nil {
+ return errToStatus(err), err
+ }
+ return 0, nil
+}
+
+const maxSize = 1080
+
+func fitResizeImage(srcImage image.Image) image.Image {
+ width := srcImage.Bounds().Dx()
+ height := srcImage.Bounds().Dy()
+ if width > maxSize && width > height {
+ width = maxSize
+ height = 0
+ } else if height > maxSize && height > width {
+ width = 0
+ height = maxSize
+ } else {
+ return srcImage
+ }
+ return imaging.Resize(srcImage, width, height, imaging.NearestNeighbor)
+}