rebuilding js
Former-commit-id: 027e2f6546614d28750e437b9a3545cb95235d9d [formerly 6dbfe621a5774304295c17f216b5c96beaaaa95a] [formerly d44822f30d9a3649b20daa7a3cdbf86c87e63c99 [formerly 325855234967d92bf42b77b17fd8affdcc7f1392]] Former-commit-id: 7f34ddc1b32076c6ad2c2a4374b170b7f5d84000 [formerly aaafd299a933d25ebcb5fdebe1b00cb9e8309d7a] Former-commit-id: 7bb183c165ba2c9711ba1c04e3af6e2048245ded
This commit is contained in:
parent
1e99d3d7c1
commit
826d491ff1
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,122 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var data = (window.data || window.alert('Something is wrong, please refresh!'))
|
||||||
|
var ssl = (window.location.protocol === 'https:')
|
||||||
|
|
||||||
|
// Remove the last directory of an url
|
||||||
|
var removeLastDirectoryPartOf = function (url) {
|
||||||
|
var arr = url.split('/')
|
||||||
|
if (arr.pop() === '') {
|
||||||
|
arr.pop()
|
||||||
|
}
|
||||||
|
return (arr.join('/'))
|
||||||
|
}
|
||||||
|
|
||||||
|
var search = new window.Vue({
|
||||||
|
el: '#search',
|
||||||
|
data: {
|
||||||
|
hover: false,
|
||||||
|
focus: false,
|
||||||
|
scrollable: null,
|
||||||
|
box: null,
|
||||||
|
input: null
|
||||||
|
},
|
||||||
|
mounted: function () {
|
||||||
|
this.scrollable = document.querySelector('#search > div')
|
||||||
|
this.box = document.querySelector('#search > div div')
|
||||||
|
this.input = document.querySelector('#search input')
|
||||||
|
this.reset()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
reset: function () {
|
||||||
|
if (data.user.AllowCommands && data.user.Commands.length > 0) {
|
||||||
|
this.box.innerHTML = `Search or use one of your supported commands: ${data.user.Commands.join(", ")}.`
|
||||||
|
} else {
|
||||||
|
this.box.innerHTML = 'Type and press enter to search.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
supported: function () {
|
||||||
|
let value = this.input.value
|
||||||
|
let pieces = value.split(' ')
|
||||||
|
|
||||||
|
for (let i = 0; i < data.user.Commands.length; i++) {
|
||||||
|
if (pieces[0] === data.user.Commands[0]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
click: function (event) {
|
||||||
|
event.currentTarget.classList.add('active')
|
||||||
|
this.$el.querySelector('input').focus()
|
||||||
|
},
|
||||||
|
keyup: function (event) {
|
||||||
|
let el = event.currentTarget
|
||||||
|
|
||||||
|
if (el.value.length === 0) {
|
||||||
|
this.reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.supported() || !data.user.AllowCommands) {
|
||||||
|
this.box.innerHTML = 'Press enter to search.'
|
||||||
|
} else {
|
||||||
|
this.box.innerHTML = 'Press enter to execute.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submit: function (event) {
|
||||||
|
this.box.innerHTML = ''
|
||||||
|
this.$el.classList.add('ongoing')
|
||||||
|
|
||||||
|
let url = window.location.host + window.location.pathname
|
||||||
|
|
||||||
|
if (document.getElementById('editor')) {
|
||||||
|
url = removeLastDirectoryPartOf(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
let protocol = ssl ? 'wss:' : 'ws:'
|
||||||
|
|
||||||
|
if (this.supported() && data.user.AllowCommands) {
|
||||||
|
let conn = new window.WebSocket(`${protocol}//${url}?command=true`)
|
||||||
|
|
||||||
|
conn.onopen = () => {
|
||||||
|
conn.send(this.input.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.onmessage = (event) => {
|
||||||
|
this.box.innerHTML = event.data
|
||||||
|
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.onclose = (event) => {
|
||||||
|
this.$el.classList.remove('ongoing')
|
||||||
|
// TODO: if is listing!
|
||||||
|
// listing.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.box.innerHTML = '<ul></ul>'
|
||||||
|
|
||||||
|
let ul = this.box.querySelector('ul')
|
||||||
|
let conn = new window.WebSocket(`${protocol}//${url}?search=true`)
|
||||||
|
|
||||||
|
conn.onopen = () => {
|
||||||
|
conn.send(this.input.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.onmessage = (event) => {
|
||||||
|
ul.innerHTML += `<li><a href=".${event.data}">${event.data}</a></li>`
|
||||||
|
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.onclose = () => {
|
||||||
|
this.$el.classList.remove('ongoing')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(search)
|
|
@ -1,7 +1,10 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
var data = (window.data || window.alert('Something is wrong, please refresh!'))
|
||||||
|
|
||||||
|
/*
|
||||||
var tempID = '_fm_internal_temporary_id'
|
var tempID = '_fm_internal_temporary_id'
|
||||||
var ssl = (window.location.protocol === 'https:')
|
|
||||||
var templates = {}
|
var templates = {}
|
||||||
var selectedItems = []
|
var selectedItems = []
|
||||||
var overlay
|
var overlay
|
||||||
|
@ -34,14 +37,6 @@ Document.prototype.getCookie = function (name) {
|
||||||
return document.cookie.replace(re, '$1')
|
return document.cookie.replace(re, '$1')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the last directory of an url
|
|
||||||
var removeLastDirectoryPartOf = function (url) {
|
|
||||||
var arr = url.split('/')
|
|
||||||
if (arr.pop() === '') {
|
|
||||||
arr.pop()
|
|
||||||
}
|
|
||||||
return (arr.join('/'))
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCSSRule (rules) {
|
function getCSSRule (rules) {
|
||||||
for (let i = 0; i < rules.length; i++) {
|
for (let i = 0; i < rules.length; i++) {
|
||||||
|
@ -136,15 +131,15 @@ buttons.setDone = function (name, success = true) {
|
||||||
var webdav = {}
|
var webdav = {}
|
||||||
|
|
||||||
webdav.convertURL = function (url) {
|
webdav.convertURL = function (url) {
|
||||||
return window.location.origin + url.replace(baseURL + '/', webdavURL + '/')
|
return window.location.origin + url.replace(data.baseURL + '/', data.webdavURL + '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
webdav.move = function (oldLink, newLink) {
|
webdav.move = function (oldLink, newLink) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
let destination = newLink.replace(baseURL + '/', webdavURL + '/')
|
let destination = newLink.replace(data.baseURL + '/', data.webdavURL + '/')
|
||||||
|
|
||||||
destination = window.location.origin + destination.substring(prefixURL.length)
|
destination = window.location.origin + destination.substring(data.baseURL.length)
|
||||||
|
|
||||||
request.open('MOVE', webdav.convertURL(oldLink), true)
|
request.open('MOVE', webdav.convertURL(oldLink), true)
|
||||||
request.setRequestHeader('Destination', destination)
|
request.setRequestHeader('Destination', destination)
|
||||||
|
@ -276,7 +271,7 @@ var preventDefault = function (event) {
|
||||||
|
|
||||||
function logoutEvent (event) {
|
function logoutEvent (event) {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('GET', window.location.pathname, true, 'username', 'password')
|
request.open('GET', window.location.pathname, true, 'data.username', 'password')
|
||||||
request.send()
|
request.send()
|
||||||
request.onreadystatechange = function () {
|
request.onreadystatechange = function () {
|
||||||
if (request.readyState === 4) {
|
if (request.readyState === 4) {
|
||||||
|
@ -439,132 +434,6 @@ function deleteEvent (event) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSearchText () {
|
|
||||||
let box = document.querySelector('#search > div div')
|
|
||||||
|
|
||||||
if (user.AllowCommands) {
|
|
||||||
box.innerHTML = `Search or use one of your supported commands: ${user.Commands.join(", ")}.`
|
|
||||||
} else {
|
|
||||||
box.innerHTML = 'Type and press enter to search.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchEvent (event) {
|
|
||||||
if (this.value.length === 0) {
|
|
||||||
resetSearchText()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = this.value,
|
|
||||||
search = document.getElementById('search'),
|
|
||||||
scrollable = document.querySelector('#search > div'),
|
|
||||||
box = document.querySelector('#search > div div'),
|
|
||||||
pieces = value.split(' '),
|
|
||||||
supported = false
|
|
||||||
|
|
||||||
user.Commands.forEach(function (cmd) {
|
|
||||||
if (cmd == pieces[0]) {
|
|
||||||
supported = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!supported || !user.AllowCommands) {
|
|
||||||
box.innerHTML = 'Press enter to search.'
|
|
||||||
} else {
|
|
||||||
box.innerHTML = 'Press enter to execute.'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.keyCode === 13) {
|
|
||||||
box.innerHTML = ''
|
|
||||||
search.classList.add('ongoing')
|
|
||||||
|
|
||||||
let url = window.location.host + window.location.pathname
|
|
||||||
|
|
||||||
if (document.getElementById('editor')) {
|
|
||||||
url = removeLastDirectoryPartOf(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
let protocol = ssl ? 'wss:' : 'ws:'
|
|
||||||
|
|
||||||
if (supported && user.AllowCommands) {
|
|
||||||
let conn = new window.WebSocket(`${protocol}//${url}?command=true`)
|
|
||||||
|
|
||||||
conn.onopen = function () {
|
|
||||||
conn.send(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.onmessage = function (event) {
|
|
||||||
box.innerHTML = event.data
|
|
||||||
scrollable.scrollTop = scrollable.scrollHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.onclose = function (event) {
|
|
||||||
search.classList.remove('ongoing')
|
|
||||||
listing.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
box.innerHTML = '<ul></ul>'
|
|
||||||
|
|
||||||
let ul = box.querySelector('ul')
|
|
||||||
let conn = new window.WebSocket(`${protocol}//${url}?search=true`)
|
|
||||||
|
|
||||||
conn.onopen = function () {
|
|
||||||
conn.send(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.onmessage = function (event) {
|
|
||||||
ul.innerHTML += '<li><a href="' + event.data + '">' + event.data + '</a></li>'
|
|
||||||
scrollable.scrollTop = scrollable.scrollHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.onclose = function (event) {
|
|
||||||
search.classList.remove('ongoing')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupSearch () {
|
|
||||||
let search = document.getElementById('search')
|
|
||||||
let searchInput = search.querySelector('input')
|
|
||||||
let searchDiv = search.querySelector('div')
|
|
||||||
let hover = false
|
|
||||||
let focus = false
|
|
||||||
|
|
||||||
resetSearchText()
|
|
||||||
|
|
||||||
searchInput.addEventListener('focus', event => {
|
|
||||||
focus = true
|
|
||||||
search.classList.add('active')
|
|
||||||
})
|
|
||||||
|
|
||||||
searchDiv.addEventListener('mouseover', event => {
|
|
||||||
hover = true
|
|
||||||
search.classList.add('active')
|
|
||||||
})
|
|
||||||
|
|
||||||
searchInput.addEventListener('blur', event => {
|
|
||||||
focus = false
|
|
||||||
if (hover) return
|
|
||||||
search.classList.remove('active')
|
|
||||||
})
|
|
||||||
|
|
||||||
search.addEventListener('mouseleave', event => {
|
|
||||||
hover = false
|
|
||||||
if (focus) return
|
|
||||||
search.classList.remove('active')
|
|
||||||
})
|
|
||||||
|
|
||||||
search.addEventListener('click', event => {
|
|
||||||
search.classList.add('active')
|
|
||||||
search.querySelector('input').focus()
|
|
||||||
})
|
|
||||||
|
|
||||||
searchInput.addEventListener('keyup', searchEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeHelp (event) {
|
function closeHelp (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
@ -620,7 +489,7 @@ document.addEventListener('DOMContentLoaded', function (event) {
|
||||||
templates.message = document.querySelector('#message-template')
|
templates.message = document.querySelector('#message-template')
|
||||||
templates.move = document.querySelector('#move-template')
|
templates.move = document.querySelector('#move-template')
|
||||||
|
|
||||||
if (user.AllowEdit) {
|
if (data.user.AllowEdit) {
|
||||||
buttons.delete.addEventListener('click', deleteEvent)
|
buttons.delete.addEventListener('click', deleteEvent)
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,23 @@
|
||||||
|
{{ define "info-button" }}
|
||||||
|
<button title="Info" aria-label="Info" class="action" id="info">
|
||||||
|
<i class="material-icons">info</i>
|
||||||
|
<span>Info</span>
|
||||||
|
</button>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "delete-button" }}
|
||||||
|
<button aria-label="Delete" title="Delete" class="action" id="delete">
|
||||||
|
<i class="material-icons">delete</i>
|
||||||
|
<span>Delete</span>
|
||||||
|
</button>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
{{ define "right-side-actions" }}
|
{{ define "right-side-actions" }}
|
||||||
{{- if .IsDir }}
|
{{ template "info-button" }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "right" }}
|
||||||
|
{{- if not .IsEditor }}
|
||||||
<a aria-label="Switch View" href="?display={{- if eq .Display "mosaic" }}list{{ else }}mosaic{{ end }}" class="action">
|
<a aria-label="Switch View" href="?display={{- if eq .Display "mosaic" }}list{{ else }}mosaic{{ end }}" class="action">
|
||||||
<i class="material-icons">{{- if eq .Display "mosaic" }}view_list{{ else }}view_module{{ end }}</i>
|
<i class="material-icons">{{- if eq .Display "mosaic" }}view_list{{ else }}view_module{{ end }}</i>
|
||||||
<span>Switch view</span>
|
<span>Switch view</span>
|
||||||
|
@ -11,14 +29,14 @@
|
||||||
</button>
|
</button>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{- if and (.User.AllowNew) (.IsDir) }}
|
{{- if and (.User.AllowNew) (not .IsEditor) }}
|
||||||
<button aria-label="Upload" title="Upload" class="action" id="upload">
|
<button aria-label="Upload" title="Upload" class="action" id="upload">
|
||||||
<i class="material-icons">file_upload</i>
|
<i class="material-icons">file_upload</i>
|
||||||
<span>Upload</span>
|
<span>Upload</span>
|
||||||
</button>
|
</button>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{- if and .User.AllowEdit (not .IsDir) }}
|
{{- if and .User.AllowEdit (.IsEditor) }}
|
||||||
<button aria-label="Delete" title="Delete" class="action" id="delete">
|
<button aria-label="Delete" title="Delete" class="action" id="delete">
|
||||||
<i class="material-icons">delete</i>
|
<i class="material-icons">delete</i>
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
|
@ -26,11 +44,11 @@
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
<button {{ if .IsDir }}data-dropdown{{ end }} aria-label="Download" title="Download" class="action" id="download">
|
<button {{ if .IsDir }}data-dropdown{{ end }} aria-label="Download" title="Download" class="action" id="download">
|
||||||
{{- if not .IsDir}}<a href="?download=true">{{ end }}
|
{{- if .IsEditor}}<a href="?download=true">{{ end }}
|
||||||
<i class="material-icons">file_download</i><span>Download</span>
|
<i class="material-icons">file_download</i><span>Download</span>
|
||||||
{{- if not .IsDir}}</a>{{ end }}
|
{{- if .IsEditor}}</a>{{ end }}
|
||||||
|
|
||||||
{{- if .IsDir }}
|
{{- if not .IsEditor }}
|
||||||
<ul class="dropdown" id="download-drop">
|
<ul class="dropdown" id="download-drop">
|
||||||
<a tabindex="0" aria-label="Download as Zip" data-format="zip" href="?download=zip"><li>zip</li></a>
|
<a tabindex="0" aria-label="Download as Zip" data-format="zip" href="?download=zip"><li>zip</li></a>
|
||||||
<a tabindex="0" aria-label="Download as Tar" data-format="tar" href="?download=tar"><li>tar</li></a>
|
<a tabindex="0" aria-label="Download as Tar" data-format="tar" href="?download=tar"><li>tar</li></a>
|
||||||
|
@ -41,14 +59,16 @@
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button title="Info" aria-label="Info" class="action" id="info">
|
|
||||||
<i class="material-icons">info</i>
|
{{ end }}
|
||||||
<span>Info</span>
|
|
||||||
</button>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ define "left-side-actions" }}
|
{{ define "left-side-actions" }}
|
||||||
{{- if and (not .IsDir) (.User.AllowEdit) }}
|
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "left" }}
|
||||||
|
{{- if and (not .IsDir) (.User.AllowEdit) }}
|
||||||
{{- if .Editor}}
|
{{- if .Editor}}
|
||||||
|
|
||||||
{{- if eq .Data.Mode "markdown" }}
|
{{- if eq .Data.Mode "markdown" }}
|
||||||
|
@ -64,12 +84,12 @@
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/* end if editor */}}
|
{{/* end if editor */}}
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
<button aria-label="Save" class="action" id="save">
|
<button aria-label="Save" class="action" id="save">
|
||||||
<i class="material-icons" title="Save">save</i>
|
<i class="material-icons" title="Save">save</i>
|
||||||
</button>
|
</button>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
{{/* end if not dir and AllowEdit */}}
|
{{/* end if not dir and AllowEdit */}}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
@ -85,7 +105,7 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button aria-label="Delete" class="action" id="delete">
|
<button aria-label="Delete" class="action" id="delete">
|
||||||
<i class="material-icons" title="Delete">delete</i><span>Delete</span>
|
<i class="material-icons" title="Delete">delete</i><span>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{ end }}
|
{{ end }}
|
|
@ -16,29 +16,22 @@
|
||||||
var data = {
|
var data = {
|
||||||
user: JSON.parse('{{ Marshal .User }}'),
|
user: JSON.parse('{{ Marshal .User }}'),
|
||||||
webdavURL: "{{ .WebDavURL }}",
|
webdavURL: "{{ .WebDavURL }}",
|
||||||
baseURL: "{{.BaseURL}}",
|
baseURL: "{{.BaseURL}}"
|
||||||
prefixURL:"{{ .PrefixURL }}"
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- TODO: remove this one after moving the JS to ^^ -->
|
<script src="{{ .BaseURL }}/_internal/js/vue.js" defer></script>
|
||||||
<script>
|
<script src="{{ .BaseURL }}/_internal/js/app.js" defer></script>
|
||||||
var user = JSON.parse('{{ Marshal .User }}'),
|
<!--
|
||||||
webdavURL = "{{ .WebDavURL }}",
|
{{- if .IsEditor }}
|
||||||
baseURL = "{{.BaseURL}}",
|
|
||||||
prefixURL = "{{ .PrefixURL }}";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script src="{{ .BaseURL }}/_internal/js/common.js" defer></script>
|
|
||||||
{{- if .IsDir }}
|
|
||||||
<script src="{{ .BaseURL }}/_internal/js/listing.js" defer></script>
|
|
||||||
{{- else }}
|
|
||||||
<script src="{{ .BaseURL }}/_internal/js/vendor/ace/src-min/ace.js" defer></script>
|
<script src="{{ .BaseURL }}/_internal/js/vendor/ace/src-min/ace.js" defer></script>
|
||||||
<script src="{{ .BaseURL }}/_internal/js/vendor/form2js.js" defer></script>
|
<script src="{{ .BaseURL }}/_internal/js/vendor/form2js.js" defer></script>
|
||||||
<script src="{{ .BaseURL }}/_internal/js/editor.js" defer></script>
|
<script src="{{ .BaseURL }}/_internal/js/editor.js" defer></script>
|
||||||
{{- end }}
|
{{- else }}
|
||||||
|
<script src="{{ .BaseURL }}/_internal/js/listing.js" defer></script>
|
||||||
|
{{- end }} -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div id="top-bar">
|
<div id="top-bar">
|
||||||
<svg id="content" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144 144">
|
<svg id="content" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144 144">
|
||||||
|
@ -46,84 +39,88 @@
|
||||||
<circle cx="72" cy="72" r="48" fill="#40c4ff"/>
|
<circle cx="72" cy="72" r="48" fill="#40c4ff"/>
|
||||||
<circle cx="72" cy="72" r="24" fill="#fff"/>
|
<circle cx="72" cy="72" r="24" fill="#fff"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div id="search">
|
<div id="search" v-on:mouseleave="hover = false" v-on:click="click" v-bind:class="{ active: focus || hover }">
|
||||||
<i class="material-icons" title="Search">search</i>
|
<i class="material-icons" title="Search">search</i>
|
||||||
<input type="text" aria-label="Write here to search" placeholder="Search or execute a command...">
|
<input type="text"
|
||||||
<div>
|
v-on:focus="focus = true"
|
||||||
|
v-on:blur="focus = false"
|
||||||
|
v-on:keyup="keyup"
|
||||||
|
v-on:keyup.enter="submit"
|
||||||
|
aria-label="Write here to search"
|
||||||
|
placeholder="Search or execute a command...">
|
||||||
|
<div v-on:mouseover="hover = true">
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
<p><i class="material-icons spin">autorenew</i></p>
|
<p><i class="material-icons spin">autorenew</i></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="bottom-bar">
|
<div id="bottom-bar">
|
||||||
<div>
|
<div>
|
||||||
{{- if ne .Name "/"}}
|
{{- if ne .Name "/"}}
|
||||||
<div data-dropdown tabindex="0" aria-label="Previous" role="button" class="action" id="previous">
|
<div data-dropdown tabindex="0" aria-label="Previous" role="button" class="action" id="previous">
|
||||||
<i class="material-icons" title="Previous">subdirectory_arrow_left</i>
|
<i class="material-icons" title="Previous">subdirectory_arrow_left</i>
|
||||||
<ul class="dropdown" id="breadcrumbs">
|
<ul class="dropdown" id="breadcrumbs">
|
||||||
{{- range $item := .BreadcrumbMap }}
|
{{- range $item := .BreadcrumbMap }}
|
||||||
<a tabindex="0" href="{{ $absURL }}{{ $item.URL }}"><li>{{ $item.Name }}</li></a>
|
<a tabindex="0" href="{{ $absURL }}{{ $item.URL }}"><li>{{ $item.Name }}</li></a>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{ if ne .Name "/"}}<p id="current-file">{{ .Name }}</p>{{ end }}
|
{{ if ne .Name "/"}}<p id="current-file">{{ .Name }}</p>{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions{{ if .IsDir }} disabled{{ end }}" id="file-only">
|
<div class="actions{{ if not .IsEditor }} disabled{{ end }}" id="file-only">
|
||||||
{{- template "left-side-actions" . -}}
|
{{- template "left-side-actions" . -}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button aria-label="More" class="action mobile-only" id="more">
|
<button aria-label="More" class="action mobile-only" id="more">
|
||||||
<i class="material-icons">more_vert</i>
|
<i class="material-icons">more_vert</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="actions" id="main-actions">
|
<div class="actions" id="main-actions">
|
||||||
{{- template "right-side-actions" . -}}
|
{{- template "right-side-actions" . -}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="click-overlay"></div>
|
<div id="click-overlay"></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="multiple-selection" class="mobile-only">
|
<div id="multiple-selection" class="mobile-only">
|
||||||
<p>Multiple selection enabled</p>
|
<p>Multiple selection enabled</p>
|
||||||
<div tabindex="0" role="button" class="action" id="multiple-selection-cancel">
|
<div tabindex="0" role="button" class="action" id="multiple-selection-cancel">
|
||||||
<i class="material-icons" title="Clear">clear</i>
|
<i class="material-icons" title="Clear">clear</i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav id="sidebar">
|
<nav id="sidebar">
|
||||||
<a class="action" href="{{ .BaseURL }}/">
|
<a class="action" href="{{ .BaseURL }}/">
|
||||||
<i class="material-icons">folder</i>
|
<i class="material-icons">folder</i>
|
||||||
<span>My Files</span>
|
<span>My Files</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="action" id="logout" tabindex="0" role="button" aria-label="Log out">
|
<div class="action" id="logout" tabindex="0" role="button" aria-label="Log out">
|
||||||
<i class="material-icons" title="Logout">exit_to_app</i>
|
<i class="material-icons" title="Logout">exit_to_app</i>
|
||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
{{- template "content" . }}
|
{{- template "content" . }}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="overlay"></div>
|
<div class="overlay"></div>
|
||||||
|
|
||||||
{{- if and (.User.AllowNew) (.IsDir) }}
|
{{- if and (.User.AllowNew) (not .IsEditor) }}
|
||||||
<div class="floating">
|
<div class="floating">
|
||||||
<div tabindex="0" role="button" class="action" id="new">
|
<div tabindex="0" role="button" class="action" id="new">
|
||||||
<i class="material-icons" title="New file or directory">add</i>
|
<i class="material-icons" title="New file or directory">add</i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{ template "templates" . }}
|
{{ template "templates" . }}
|
||||||
|
|
||||||
<footer>Served with <a rel="noopener noreferrer" href="https://github.com/hacdias/caddy-filemanager">File Manager</a>.</footer>
|
<footer>Served with <a rel="noopener noreferrer" href="https://github.com/hacdias/caddy-filemanager">File Manager</a>.</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,5 +1,39 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="container {{ .Display }}" id="listing">
|
{{ if .Data.Preview }}
|
||||||
|
<div id="previewer">
|
||||||
|
<div class="bar">
|
||||||
|
<button class="action" aria-label="Close Preview" id="close">
|
||||||
|
<i class="material-icons">close</i>
|
||||||
|
</button>
|
||||||
|
{{ template "right-side-actions" . }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview">
|
||||||
|
{{ with .Data.PreviewItem}}
|
||||||
|
{{ if eq .Type "image" }}
|
||||||
|
<img src="{{ .URL }}?raw=true">
|
||||||
|
{{ else if eq .Type "audio" }}
|
||||||
|
<audio src="{{ .URL }}?raw=true" controls></audio>
|
||||||
|
{{ else if eq .Type "video" }}
|
||||||
|
<video src="{{ .URL }}?raw=true" controls>
|
||||||
|
Sorry, your browser doesn't support embedded videos,
|
||||||
|
but don't worry, you can <a href="?download=true">download it</a>
|
||||||
|
and watch it with your favorite video player!
|
||||||
|
</video>
|
||||||
|
{{ else if eq .Extension ".pdf" }}
|
||||||
|
<object class="pdf" data="{{ .URL }}?raw=true"></object>
|
||||||
|
{{ else if eq .Type "blob" }}
|
||||||
|
<a href="?download=true"><h2 class="message">Download <i class="material-icons">file_download</i></h2></a>
|
||||||
|
{{ else }}
|
||||||
|
<pre>{{ .StringifyContent }}</pre>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="container {{ .Data.Display }}" id="listing">
|
||||||
{{- with .Data -}}
|
{{- with .Data -}}
|
||||||
<div>
|
<div>
|
||||||
<div class="item header">
|
<div class="item header">
|
||||||
|
|
|
@ -1,23 +1,35 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
{{ with .Data}}
|
|
||||||
<main class="container">
|
<div id="previewer">
|
||||||
{{ if eq .Type "image" }}
|
<div class="bar">
|
||||||
<center><img src="{{ .URL }}?raw=true"></center>
|
<button class="action" aria-label="Close Preview" id="close">
|
||||||
{{ else if eq .Type "audio" }}
|
<i class="material-icons">close</i>
|
||||||
<audio src="{{ .URL }}?raw=true" controls></audio>
|
</button>
|
||||||
{{ else if eq .Type "video" }}
|
|
||||||
<video src="{{ .URL }}?raw=true" controls>
|
{{ template "info-button" }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview">
|
||||||
|
{{ with .Data}}
|
||||||
|
{{ if eq .Type "image" }}
|
||||||
|
<img src="{{ .URL }}?raw=true">
|
||||||
|
{{ else if eq .Type "audio" }}
|
||||||
|
<audio src="{{ .URL }}?raw=true" controls></audio>
|
||||||
|
{{ else if eq .Type "video" }}
|
||||||
|
<video src="{{ .URL }}?raw=true" controls>
|
||||||
Sorry, your browser doesn't support embedded videos,
|
Sorry, your browser doesn't support embedded videos,
|
||||||
but don't worry, you can <a href="?download=true">download it</a>
|
but don't worry, you can <a href="?download=true">download it</a>
|
||||||
and watch it with your favorite video player!
|
and watch it with your favorite video player!
|
||||||
</video>
|
</video>
|
||||||
{{ else if eq .Extension ".pdf" }}
|
{{ else if eq .Extension ".pdf" }}
|
||||||
<object class="pdf" data="{{ .URL }}?raw=true"></object>
|
<object class="pdf" data="{{ .URL }}?raw=true"></object>
|
||||||
{{ else if eq .Type "blob" }}
|
{{ else if eq .Type "blob" }}
|
||||||
<a href="?download=true"><h2 class="message">Download <i class="material-icons">file_download</i></h2></a>
|
<a href="?download=true"><h2 class="message">Download <i class="material-icons">file_download</i></h2></a>
|
||||||
{{ else}}
|
{{ else }}
|
||||||
<pre>{{ .StringifyContent }}</pre>
|
<pre>{{ .StringifyContent }}</pre>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</main>
|
{{ end }}
|
||||||
{{ end }}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
package filemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Serve provides the needed assets for the front-end
|
||||||
|
func serveAssets(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
// gets the filename to be used with Assets function
|
||||||
|
filename := strings.TrimPrefix(r.URL.Path, assetsURL)
|
||||||
|
|
||||||
|
var file []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(filename, "/css"):
|
||||||
|
filename = strings.Replace(filename, "/css/", "", 1)
|
||||||
|
file, err = c.fm.assets.css.Bytes(filename)
|
||||||
|
case strings.HasPrefix(filename, "/js"):
|
||||||
|
filename = strings.Replace(filename, "/js/", "", 1)
|
||||||
|
file, err = c.fm.assets.js.Bytes(filename)
|
||||||
|
default:
|
||||||
|
err = errors.New("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusNotFound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the file extension and its mimetype
|
||||||
|
extension := filepath.Ext(filename)
|
||||||
|
mediatype := mime.TypeByExtension(extension)
|
||||||
|
|
||||||
|
// Write the header with the Content-Type and write the file
|
||||||
|
// content to the buffer
|
||||||
|
w.Header().Set("Content-Type", mediatype)
|
||||||
|
w.Write(file)
|
||||||
|
return 200, nil
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package filemanager
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
|
||||||
|
func serveChecksum(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
query := r.URL.Query().Get("checksum")
|
||||||
|
|
||||||
|
val, err := c.fi.Checksum(query)
|
||||||
|
if err == errInvalidOption {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
} else if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(val))
|
||||||
|
return http.StatusOK, nil
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package filemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mholt/archiver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// serveDownload creates an archive in one of the supported formats (zip, tar,
|
||||||
|
// tar.gz or tar.bz2) and sends it to be downloaded.
|
||||||
|
func serveDownload(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
query := r.URL.Query().Get("download")
|
||||||
|
|
||||||
|
if !c.fi.IsDir {
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename="+c.fi.Name)
|
||||||
|
http.ServeFile(w, r, c.fi.Path)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []string{}
|
||||||
|
names := strings.Split(r.URL.Query().Get("files"), ",")
|
||||||
|
|
||||||
|
if len(names) != 0 {
|
||||||
|
for _, name := range names {
|
||||||
|
name, err := url.QueryUnescape(name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
files = append(files, filepath.Join(c.fi.Path, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
files = append(files, c.fi.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if query == "true" {
|
||||||
|
query = "zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
extension string
|
||||||
|
temp string
|
||||||
|
err error
|
||||||
|
tempfile string
|
||||||
|
)
|
||||||
|
|
||||||
|
temp, err = ioutil.TempDir("", "")
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.RemoveAll(temp)
|
||||||
|
tempfile = filepath.Join(temp, "temp")
|
||||||
|
|
||||||
|
switch query {
|
||||||
|
case "zip":
|
||||||
|
extension, err = ".zip", archiver.Zip.Make(tempfile, files)
|
||||||
|
case "tar":
|
||||||
|
extension, err = ".tar", archiver.Tar.Make(tempfile, files)
|
||||||
|
case "targz":
|
||||||
|
extension, err = ".tar.gz", archiver.TarGz.Make(tempfile, files)
|
||||||
|
case "tarbz2":
|
||||||
|
extension, err = ".tar.bz2", archiver.TarBz2.Make(tempfile, files)
|
||||||
|
case "tarxz":
|
||||||
|
extension, err = ".tar.xz", archiver.TarXZ.Make(tempfile, files)
|
||||||
|
default:
|
||||||
|
return http.StatusNotImplemented, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(temp + "/temp")
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := c.fi.Name
|
||||||
|
if name == "." || name == "" {
|
||||||
|
name = "download"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename="+name+extension)
|
||||||
|
io.Copy(w, file)
|
||||||
|
return http.StatusOK, nil
|
||||||
|
}
|
65
error.go
65
error.go
|
@ -1,65 +0,0 @@
|
||||||
package filemanager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const errTemplate = `<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>TITLE</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
background-color: #2196f3;
|
|
||||||
color: #fff;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
background-color: rgba(0,0,0,0.1);
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 1em;
|
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.center {
|
|
||||||
max-width: 40em;
|
|
||||||
margin: 2em auto 0;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #eee;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="center">
|
|
||||||
<h1>TITLE</h1>
|
|
||||||
|
|
||||||
<p>Try reloading the page or hitting the back button. If this error persists, it seems that you may have found a bug! Please create an issue at <a href="https://github.com/hacdias/caddy-filemanager/issues">hacdias/caddy-filemanager</a> repository on GitHub with the code below.</p>
|
|
||||||
|
|
||||||
<code>CODE</code>
|
|
||||||
</div>
|
|
||||||
</html>`
|
|
||||||
|
|
||||||
// htmlError prints the error page
|
|
||||||
func htmlError(w http.ResponseWriter, code int, err error) (int, error) {
|
|
||||||
tpl := errTemplate
|
|
||||||
tpl = strings.Replace(tpl, "TITLE", strconv.Itoa(code)+" "+http.StatusText(code), -1)
|
|
||||||
tpl = strings.Replace(tpl, "CODE", err.Error(), -1)
|
|
||||||
|
|
||||||
_, err = w.Write([]byte(tpl))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
return http.StatusOK, nil
|
|
||||||
}
|
|
231
file.go
231
file.go
|
@ -1,6 +1,7 @@
|
||||||
package filemanager
|
package filemanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
@ -14,7 +15,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -29,25 +32,53 @@ var (
|
||||||
type fileInfo struct {
|
type fileInfo struct {
|
||||||
// Used to store the file's content temporarily.
|
// Used to store the file's content temporarily.
|
||||||
content []byte
|
content []byte
|
||||||
|
// The name of the file.
|
||||||
Name string
|
Name string
|
||||||
Size int64
|
// The Size of the file.
|
||||||
URL string
|
Size int64
|
||||||
|
// The absolute URL.
|
||||||
|
URL string
|
||||||
|
// The extension of the file.
|
||||||
Extension string
|
Extension string
|
||||||
ModTime time.Time
|
// The last modified time.
|
||||||
Mode os.FileMode
|
ModTime time.Time
|
||||||
IsDir bool
|
// The File Mode.
|
||||||
|
Mode os.FileMode
|
||||||
|
// Indicates if this file is a directory.
|
||||||
|
IsDir bool
|
||||||
// Absolute path.
|
// Absolute path.
|
||||||
Path string
|
Path string
|
||||||
|
|
||||||
// Relative path to user's virtual File System.
|
// Relative path to user's virtual File System.
|
||||||
VirtualPath string
|
VirtualPath string
|
||||||
|
|
||||||
// Indicates the file content type: video, text, image, music or blob.
|
// Indicates the file content type: video, text, image, music or blob.
|
||||||
Type string
|
Type string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A listing is the context used to fill out a template.
|
||||||
|
type listing struct {
|
||||||
|
// The name of the directory (the last element of the path).
|
||||||
|
Name string
|
||||||
|
// The full path of the request relatively to a File System.
|
||||||
|
Path string
|
||||||
|
// The items (files and folders) in the path.
|
||||||
|
Items []fileInfo
|
||||||
|
// The number of directories in the listing.
|
||||||
|
NumDirs int
|
||||||
|
// The number of files (items that aren't directories) in the listing.
|
||||||
|
NumFiles int
|
||||||
|
// Which sorting order is used.
|
||||||
|
Sort string
|
||||||
|
// And which order.
|
||||||
|
Order string
|
||||||
|
// If ≠0 then Items have been limited to that many elements.
|
||||||
|
ItemsLimitedTo int
|
||||||
|
Display string
|
||||||
|
// Indicates if we're showing a preview in this Listing
|
||||||
|
Preview bool
|
||||||
|
// File to preview if Preview is true
|
||||||
|
PreviewItem *fileInfo
|
||||||
|
}
|
||||||
|
|
||||||
// getInfo gets the file information and, in case of error, returns the
|
// getInfo gets the file information and, in case of error, returns the
|
||||||
// respective HTTP error code
|
// respective HTTP error code
|
||||||
func getInfo(url *url.URL, c *FileManager, u *User) (*fileInfo, error) {
|
func getInfo(url *url.URL, c *FileManager, u *User) (*fileInfo, error) {
|
||||||
|
@ -75,21 +106,65 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*fileInfo, error) {
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var textExtensions = [...]string{
|
// getListing gets the information about a specific directory and its files.
|
||||||
".md", ".markdown", ".mdown", ".mmark",
|
func getListing(u *User, filePath string, baseURL string) (*listing, error) {
|
||||||
".asciidoc", ".adoc", ".ad",
|
// Gets the directory information using the Virtual File System of
|
||||||
".rst",
|
// the user configuration.
|
||||||
".json", ".toml", ".yaml", ".csv", ".xml", ".rss", ".conf", ".ini",
|
file, err := u.fileSystem.OpenFile(context.TODO(), filePath, os.O_RDONLY, 0)
|
||||||
".tex", ".sty",
|
if err != nil {
|
||||||
".css", ".sass", ".scss",
|
return nil, err
|
||||||
".js",
|
}
|
||||||
".html",
|
defer file.Close()
|
||||||
".txt", ".rtf",
|
|
||||||
".sh", ".bash", ".ps1", ".bat", ".cmd",
|
// Reads the directory and gets the information about the files.
|
||||||
".php", ".pl", ".py",
|
files, err := file.Readdir(-1)
|
||||||
"Caddyfile",
|
if err != nil {
|
||||||
".c", ".cc", ".h", ".hh", ".cpp", ".hpp", ".f90",
|
return nil, err
|
||||||
".f", ".bas", ".d", ".ada", ".nim", ".cr", ".java", ".cs", ".vala", ".vapi",
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
fileinfos []fileInfo
|
||||||
|
dirCount, fileCount int
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
name := f.Name()
|
||||||
|
allowed := u.Allowed("/" + name)
|
||||||
|
|
||||||
|
if !allowed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.IsDir() {
|
||||||
|
name += "/"
|
||||||
|
dirCount++
|
||||||
|
} else {
|
||||||
|
fileCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute URL
|
||||||
|
url := url.URL{Path: baseURL + name}
|
||||||
|
|
||||||
|
i := fileInfo{
|
||||||
|
Name: f.Name(),
|
||||||
|
Size: f.Size(),
|
||||||
|
ModTime: f.ModTime(),
|
||||||
|
Mode: f.Mode(),
|
||||||
|
IsDir: f.IsDir(),
|
||||||
|
URL: url.String(),
|
||||||
|
}
|
||||||
|
i.RetrieveFileType()
|
||||||
|
|
||||||
|
fileinfos = append(fileinfos, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &listing{
|
||||||
|
Name: path.Base(filePath),
|
||||||
|
Path: filePath,
|
||||||
|
Items: fileinfos,
|
||||||
|
NumDirs: dirCount,
|
||||||
|
NumFiles: fileCount,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveFileType obtains the mimetype and converts it to a simple
|
// RetrieveFileType obtains the mimetype and converts it to a simple
|
||||||
|
@ -212,3 +287,109 @@ func (i fileInfo) HumanModTime(format string) string {
|
||||||
func (i fileInfo) CanBeEdited() bool {
|
func (i fileInfo) CanBeEdited() bool {
|
||||||
return i.Type == "text"
|
return i.Type == "text"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplySort applies the sort order using .Order and .Sort
|
||||||
|
func (l listing) ApplySort() {
|
||||||
|
// Check '.Order' to know how to sort
|
||||||
|
if l.Order == "desc" {
|
||||||
|
switch l.Sort {
|
||||||
|
case "name":
|
||||||
|
sort.Sort(sort.Reverse(byName(l)))
|
||||||
|
case "size":
|
||||||
|
sort.Sort(sort.Reverse(bySize(l)))
|
||||||
|
case "time":
|
||||||
|
sort.Sort(sort.Reverse(byTime(l)))
|
||||||
|
default:
|
||||||
|
// If not one of the above, do nothing
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else { // If we had more Orderings we could add them here
|
||||||
|
switch l.Sort {
|
||||||
|
case "name":
|
||||||
|
sort.Sort(byName(l))
|
||||||
|
case "size":
|
||||||
|
sort.Sort(bySize(l))
|
||||||
|
case "time":
|
||||||
|
sort.Sort(byTime(l))
|
||||||
|
default:
|
||||||
|
sort.Sort(byName(l))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement sorting for listing
|
||||||
|
type byName listing
|
||||||
|
type bySize listing
|
||||||
|
type byTime listing
|
||||||
|
|
||||||
|
// By Name
|
||||||
|
func (l byName) Len() int {
|
||||||
|
return len(l.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l byName) Swap(i, j int) {
|
||||||
|
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat upper and lower case equally
|
||||||
|
func (l byName) Less(i, j int) bool {
|
||||||
|
if l.Items[i].IsDir && !l.Items[j].IsDir {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !l.Items[i].IsDir && l.Items[j].IsDir {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// By Size
|
||||||
|
func (l bySize) Len() int {
|
||||||
|
return len(l.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l bySize) Swap(i, j int) {
|
||||||
|
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const directoryOffset = -1 << 31 // = math.MinInt32
|
||||||
|
func (l bySize) Less(i, j int) bool {
|
||||||
|
iSize, jSize := l.Items[i].Size, l.Items[j].Size
|
||||||
|
if l.Items[i].IsDir {
|
||||||
|
iSize = directoryOffset + iSize
|
||||||
|
}
|
||||||
|
if l.Items[j].IsDir {
|
||||||
|
jSize = directoryOffset + jSize
|
||||||
|
}
|
||||||
|
return iSize < jSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// By Time
|
||||||
|
func (l byTime) Len() int {
|
||||||
|
return len(l.Items)
|
||||||
|
}
|
||||||
|
func (l byTime) Swap(i, j int) {
|
||||||
|
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
||||||
|
}
|
||||||
|
func (l byTime) Less(i, j int) bool {
|
||||||
|
return l.Items[i].ModTime.Before(l.Items[j].ModTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
var textExtensions = [...]string{
|
||||||
|
".md", ".markdown", ".mdown", ".mmark",
|
||||||
|
".asciidoc", ".adoc", ".ad",
|
||||||
|
".rst",
|
||||||
|
".json", ".toml", ".yaml", ".csv", ".xml", ".rss", ".conf", ".ini",
|
||||||
|
".tex", ".sty",
|
||||||
|
".css", ".sass", ".scss",
|
||||||
|
".js",
|
||||||
|
".html",
|
||||||
|
".txt", ".rtf",
|
||||||
|
".sh", ".bash", ".ps1", ".bat", ".cmd",
|
||||||
|
".php", ".pl", ".py",
|
||||||
|
"Caddyfile",
|
||||||
|
".c", ".cc", ".h", ".hh", ".cpp", ".hpp", ".f90",
|
||||||
|
".f", ".bas", ".d", ".ada", ".nim", ".cr", ".java", ".cs", ".vala", ".vapi",
|
||||||
|
}
|
||||||
|
|
416
http.go
416
http.go
|
@ -1,24 +1,23 @@
|
||||||
package filemanager
|
package filemanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mholt/archiver"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// assetsURL is the url where static assets are served.
|
// assetsURL is the url where static assets are served.
|
||||||
const assetsURL = "/_internal"
|
const assetsURL = "/_internal"
|
||||||
|
|
||||||
|
// requestContext contains the needed information to make handlers work.
|
||||||
|
type requestContext struct {
|
||||||
|
us *User
|
||||||
|
fm *FileManager
|
||||||
|
fi *fileInfo
|
||||||
|
pg *page
|
||||||
|
}
|
||||||
|
|
||||||
func serveHTTP(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func serveHTTP(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
var (
|
var (
|
||||||
code int
|
code int
|
||||||
|
@ -110,10 +109,8 @@ func serveHTTP(c *requestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||||
case r.URL.Query().Get("raw") == "true" && !f.IsDir:
|
case r.URL.Query().Get("raw") == "true" && !f.IsDir:
|
||||||
http.ServeFile(w, r, f.Path)
|
http.ServeFile(w, r, f.Path)
|
||||||
code, err = 0, nil
|
code, err = 0, nil
|
||||||
case f.IsDir:
|
|
||||||
code, err = serveListing(c, w, r)
|
|
||||||
default:
|
default:
|
||||||
code, err = serveSingle(c, w, r)
|
code, err = serveDefault(c, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -126,371 +123,54 @@ func serveHTTP(c *requestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||||
return http.StatusNotImplemented, nil
|
return http.StatusNotImplemented, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveWebDAV handles the webDAV route of the File Manager.
|
// responseWriterNoBody is a wrapper used to suprress the body of the response
|
||||||
func serveWebDAV(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
// to a request. Mainly used for HEAD requests.
|
||||||
var err error
|
type responseWriterNoBody struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
// Checks for user permissions relatively to this path.
|
// newResponseWriterNoBody creates a new responseWriterNoBody.
|
||||||
if !c.us.Allowed(strings.TrimPrefix(r.URL.Path, c.fm.webDavURL)) {
|
func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
|
||||||
return http.StatusForbidden, nil
|
return &responseWriterNoBody{w}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Method {
|
// Header executes the Header method from the http.ResponseWriter.
|
||||||
case "GET", "HEAD":
|
func (w responseWriterNoBody) Header() http.Header {
|
||||||
// Excerpt from RFC4918, section 9.4:
|
return w.ResponseWriter.Header()
|
||||||
//
|
}
|
||||||
// GET, when applied to a collection, may return the contents of an
|
|
||||||
// "index.html" resource, a human-readable view of the contents of
|
|
||||||
// the collection, or something else altogether.
|
|
||||||
//
|
|
||||||
// It was decided on https://github.com/hacdias/caddy-filemanager/issues/85
|
|
||||||
// that GET, for collections, will return the same as PROPFIND method.
|
|
||||||
path := strings.Replace(r.URL.Path, c.fm.webDavURL, "", 1)
|
|
||||||
path = c.us.scope + "/" + path
|
|
||||||
path = filepath.Clean(path)
|
|
||||||
|
|
||||||
var i os.FileInfo
|
|
||||||
i, err = os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
// Is there any error? WebDav will handle it... no worries.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if i.IsDir() {
|
|
||||||
r.Method = "PROPFIND"
|
|
||||||
|
|
||||||
if r.Method == "HEAD" {
|
|
||||||
w = newResponseWriterNoBody(w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
|
|
||||||
if !c.us.AllowEdit {
|
|
||||||
return http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
case "MKCOL", "COPY":
|
|
||||||
if !c.us.AllowNew {
|
|
||||||
return http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preprocess the PUT request if it's the case
|
|
||||||
if r.Method == http.MethodPut {
|
|
||||||
if err = c.fm.BeforeSave(r, c.fm, c.us); err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if put(c, w, r) != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.fm.handler.ServeHTTP(w, r)
|
|
||||||
if err = c.fm.AfterSave(r, c.fm, c.us); err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Write suprresses the body.
|
||||||
|
func (w responseWriterNoBody) Write(data []byte) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve provides the needed assets for the front-end
|
// WriteHeader writes the header to the http.ResponseWriter.
|
||||||
func serveAssets(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func (w responseWriterNoBody) WriteHeader(statusCode int) {
|
||||||
// gets the filename to be used with Assets function
|
w.ResponseWriter.WriteHeader(statusCode)
|
||||||
filename := strings.TrimPrefix(r.URL.Path, assetsURL)
|
}
|
||||||
|
|
||||||
var file []byte
|
// matchURL checks if the first URL matches the second.
|
||||||
var err error
|
func matchURL(first, second string) bool {
|
||||||
|
first = strings.ToLower(first)
|
||||||
|
second = strings.ToLower(second)
|
||||||
|
|
||||||
|
return strings.HasPrefix(first, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorToHTTP converts errors to HTTP Status Code.
|
||||||
|
func errorToHTTP(err error, gone bool) int {
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(filename, "/css"):
|
case os.IsPermission(err):
|
||||||
filename = strings.Replace(filename, "/css/", "", 1)
|
return http.StatusForbidden
|
||||||
file, err = c.fm.assets.css.Bytes(filename)
|
case os.IsNotExist(err):
|
||||||
case strings.HasPrefix(filename, "/js"):
|
if !gone {
|
||||||
filename = strings.Replace(filename, "/js/", "", 1)
|
return http.StatusNotFound
|
||||||
file, err = c.fm.assets.js.Bytes(filename)
|
}
|
||||||
|
|
||||||
|
return http.StatusGone
|
||||||
|
case os.IsExist(err):
|
||||||
|
return http.StatusGone
|
||||||
default:
|
default:
|
||||||
err = errors.New("not found")
|
return http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusNotFound, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the file extension and its mimetype
|
|
||||||
extension := filepath.Ext(filename)
|
|
||||||
mediatype := mime.TypeByExtension(extension)
|
|
||||||
|
|
||||||
// Write the header with the Content-Type and write the file
|
|
||||||
// content to the buffer
|
|
||||||
w.Header().Set("Content-Type", mediatype)
|
|
||||||
w.Write(file)
|
|
||||||
return 200, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
|
|
||||||
func serveChecksum(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
query := r.URL.Query().Get("checksum")
|
|
||||||
|
|
||||||
val, err := c.fi.Checksum(query)
|
|
||||||
if err == errInvalidOption {
|
|
||||||
return http.StatusBadRequest, err
|
|
||||||
} else if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte(val))
|
|
||||||
return http.StatusOK, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveSingle serves a single file in an editor (if it is editable), shows the
|
|
||||||
// plain file, or downloads it if it can't be shown.
|
|
||||||
func serveSingle(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if err = c.fi.RetrieveFileType(); err != nil {
|
|
||||||
return errorToHTTP(err, true), err
|
|
||||||
}
|
|
||||||
|
|
||||||
p := &page{
|
|
||||||
Name: c.fi.Name,
|
|
||||||
Path: c.fi.VirtualPath,
|
|
||||||
IsDir: false,
|
|
||||||
Data: c.fi,
|
|
||||||
User: c.us,
|
|
||||||
PrefixURL: c.fm.prefixURL,
|
|
||||||
BaseURL: c.fm.RootURL(),
|
|
||||||
WebDavURL: c.fm.WebDavURL(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the request accepts JSON, we send the file information.
|
|
||||||
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
|
||||||
return p.PrintAsJSON(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.fi.Type == "text" {
|
|
||||||
if err = c.fi.Read(); err != nil {
|
|
||||||
return errorToHTTP(err, true), err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.fi.CanBeEdited() && c.us.AllowEdit {
|
|
||||||
p.Data, err = getEditor(r, c.fi)
|
|
||||||
p.Editor = true
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.PrintAsHTML(w, c.fm.assets.templates, "frontmatter", "editor")
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.PrintAsHTML(w, c.fm.assets.templates, "single")
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveDownload creates an archive in one of the supported formats (zip, tar,
|
|
||||||
// tar.gz or tar.bz2) and sends it to be downloaded.
|
|
||||||
func serveDownload(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
query := r.URL.Query().Get("download")
|
|
||||||
|
|
||||||
if !c.fi.IsDir {
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+c.fi.Name)
|
|
||||||
http.ServeFile(w, r, c.fi.Path)
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
files := []string{}
|
|
||||||
names := strings.Split(r.URL.Query().Get("files"), ",")
|
|
||||||
|
|
||||||
if len(names) != 0 {
|
|
||||||
for _, name := range names {
|
|
||||||
name, err := url.QueryUnescape(name)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
files = append(files, filepath.Join(c.fi.Path, name))
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
files = append(files, c.fi.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
if query == "true" {
|
|
||||||
query = "zip"
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
extension string
|
|
||||||
temp string
|
|
||||||
err error
|
|
||||||
tempfile string
|
|
||||||
)
|
|
||||||
|
|
||||||
temp, err = ioutil.TempDir("", "")
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer os.RemoveAll(temp)
|
|
||||||
tempfile = filepath.Join(temp, "temp")
|
|
||||||
|
|
||||||
switch query {
|
|
||||||
case "zip":
|
|
||||||
extension, err = ".zip", archiver.Zip.Make(tempfile, files)
|
|
||||||
case "tar":
|
|
||||||
extension, err = ".tar", archiver.Tar.Make(tempfile, files)
|
|
||||||
case "targz":
|
|
||||||
extension, err = ".tar.gz", archiver.TarGz.Make(tempfile, files)
|
|
||||||
case "tarbz2":
|
|
||||||
extension, err = ".tar.bz2", archiver.TarBz2.Make(tempfile, files)
|
|
||||||
case "tarxz":
|
|
||||||
extension, err = ".tar.xz", archiver.TarXZ.Make(tempfile, files)
|
|
||||||
default:
|
|
||||||
return http.StatusNotImplemented, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Open(temp + "/temp")
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
name := c.fi.Name
|
|
||||||
if name == "." || name == "" {
|
|
||||||
name = "download"
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+name+extension)
|
|
||||||
io.Copy(w, file)
|
|
||||||
return http.StatusOK, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveListing presents the user with a listage of a directory folder.
|
|
||||||
func serveListing(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Loads the content of the directory.
|
|
||||||
listing, err := getListing(c.us, c.fi.VirtualPath, c.fm.RootURL()+r.URL.Path)
|
|
||||||
if err != nil {
|
|
||||||
return errorToHTTP(err, true), err
|
|
||||||
}
|
|
||||||
|
|
||||||
cookieScope := c.fm.RootURL()
|
|
||||||
if cookieScope == "" {
|
|
||||||
cookieScope = "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the query values into the Listing struct
|
|
||||||
var limit int
|
|
||||||
listing.Sort, listing.Order, limit, err = handleSortOrder(w, r, cookieScope)
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusBadRequest, err
|
|
||||||
}
|
|
||||||
|
|
||||||
listing.ApplySort()
|
|
||||||
|
|
||||||
if limit > 0 && limit <= len(listing.Items) {
|
|
||||||
listing.Items = listing.Items[:limit]
|
|
||||||
listing.ItemsLimitedTo = limit
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
|
||||||
marsh, err := json.Marshal(listing.Items)
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
if _, err := w.Write(marsh); err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.StatusOK, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
displayMode := r.URL.Query().Get("display")
|
|
||||||
|
|
||||||
if displayMode == "" {
|
|
||||||
if displayCookie, err := r.Cookie("display"); err == nil {
|
|
||||||
displayMode = displayCookie.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if displayMode == "" || (displayMode != "mosaic" && displayMode != "list") {
|
|
||||||
displayMode = "mosaic"
|
|
||||||
}
|
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "display",
|
|
||||||
Value: displayMode,
|
|
||||||
Path: cookieScope,
|
|
||||||
Secure: r.TLS != nil,
|
|
||||||
})
|
|
||||||
|
|
||||||
p := &page{
|
|
||||||
minimal: r.Header.Get("Minimal") == "true",
|
|
||||||
Name: listing.Name,
|
|
||||||
Path: c.fi.VirtualPath,
|
|
||||||
IsDir: true,
|
|
||||||
User: c.us,
|
|
||||||
PrefixURL: c.fm.prefixURL,
|
|
||||||
BaseURL: c.fm.RootURL(),
|
|
||||||
WebDavURL: c.fm.WebDavURL(),
|
|
||||||
Display: displayMode,
|
|
||||||
Data: listing,
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.PrintAsHTML(w, c.fm.assets.templates, "listing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
|
|
||||||
// and reads 'limit' if given. The latter is 0 if not given. Sets cookies.
|
|
||||||
func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
|
|
||||||
sort = r.URL.Query().Get("sort")
|
|
||||||
order = r.URL.Query().Get("order")
|
|
||||||
limitQuery := r.URL.Query().Get("limit")
|
|
||||||
|
|
||||||
// If the query 'sort' or 'order' is empty, use defaults or any values
|
|
||||||
// previously saved in Cookies.
|
|
||||||
switch sort {
|
|
||||||
case "":
|
|
||||||
sort = "name"
|
|
||||||
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
|
|
||||||
sort = sortCookie.Value
|
|
||||||
}
|
|
||||||
case "name", "size", "type":
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "sort",
|
|
||||||
Value: sort,
|
|
||||||
Path: scope,
|
|
||||||
Secure: r.TLS != nil,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
switch order {
|
|
||||||
case "":
|
|
||||||
order = "asc"
|
|
||||||
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
|
|
||||||
order = orderCookie.Value
|
|
||||||
}
|
|
||||||
case "asc", "desc":
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "order",
|
|
||||||
Value: order,
|
|
||||||
Path: scope,
|
|
||||||
Secure: r.TLS != nil,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if limitQuery != "" {
|
|
||||||
limit, err = strconv.Atoi(limitQuery)
|
|
||||||
// If the 'limit' query can't be interpreted as a number, return err.
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
package filemanager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// requestContext contains the needed information to make handlers work.
|
|
||||||
type requestContext struct {
|
|
||||||
us *User
|
|
||||||
fm *FileManager
|
|
||||||
fi *fileInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// responseWriterNoBody is a wrapper used to suprress the body of the response
|
|
||||||
// to a request. Mainly used for HEAD requests.
|
|
||||||
type responseWriterNoBody struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
// newResponseWriterNoBody creates a new responseWriterNoBody.
|
|
||||||
func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
|
|
||||||
return &responseWriterNoBody{w}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header executes the Header method from the http.ResponseWriter.
|
|
||||||
func (w responseWriterNoBody) Header() http.Header {
|
|
||||||
return w.ResponseWriter.Header()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write suprresses the body.
|
|
||||||
func (w responseWriterNoBody) Write(data []byte) (int, error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteHeader writes the header to the http.ResponseWriter.
|
|
||||||
func (w responseWriterNoBody) WriteHeader(statusCode int) {
|
|
||||||
w.ResponseWriter.WriteHeader(statusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchURL checks if the first URL matches the second.
|
|
||||||
func matchURL(first, second string) bool {
|
|
||||||
first = strings.ToLower(first)
|
|
||||||
second = strings.ToLower(second)
|
|
||||||
|
|
||||||
return strings.HasPrefix(first, second)
|
|
||||||
}
|
|
||||||
|
|
||||||
// errorToHTTP converts errors to HTTP Status Code.
|
|
||||||
func errorToHTTP(err error, gone bool) int {
|
|
||||||
switch {
|
|
||||||
case os.IsPermission(err):
|
|
||||||
return http.StatusForbidden
|
|
||||||
case os.IsNotExist(err):
|
|
||||||
if !gone {
|
|
||||||
return http.StatusNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.StatusGone
|
|
||||||
case os.IsExist(err):
|
|
||||||
return http.StatusGone
|
|
||||||
default:
|
|
||||||
return http.StatusInternalServerError
|
|
||||||
}
|
|
||||||
}
|
|
180
listing.go
180
listing.go
|
@ -1,180 +0,0 @@
|
||||||
package filemanager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A listing is the context used to fill out a template.
|
|
||||||
type listing struct {
|
|
||||||
// The name of the directory (the last element of the path).
|
|
||||||
Name string
|
|
||||||
// The full path of the request relatively to a File System.
|
|
||||||
Path string
|
|
||||||
// The items (files and folders) in the path.
|
|
||||||
Items []fileInfo
|
|
||||||
// The number of directories in the listing.
|
|
||||||
NumDirs int
|
|
||||||
// The number of files (items that aren't directories) in the listing.
|
|
||||||
NumFiles int
|
|
||||||
// Which sorting order is used.
|
|
||||||
Sort string
|
|
||||||
// And which order.
|
|
||||||
Order string
|
|
||||||
// If ≠0 then Items have been limited to that many elements.
|
|
||||||
ItemsLimitedTo int
|
|
||||||
}
|
|
||||||
|
|
||||||
// getListing gets the information about a specific directory and its files.
|
|
||||||
func getListing(u *User, filePath string, baseURL string) (*listing, error) {
|
|
||||||
// Gets the directory information using the Virtual File System of
|
|
||||||
// the user configuration.
|
|
||||||
file, err := u.fileSystem.OpenFile(context.TODO(), filePath, os.O_RDONLY, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
// Reads the directory and gets the information about the files.
|
|
||||||
files, err := file.Readdir(-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
fileinfos []fileInfo
|
|
||||||
dirCount, fileCount int
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, f := range files {
|
|
||||||
name := f.Name()
|
|
||||||
allowed := u.Allowed("/" + name)
|
|
||||||
|
|
||||||
if !allowed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.IsDir() {
|
|
||||||
name += "/"
|
|
||||||
dirCount++
|
|
||||||
} else {
|
|
||||||
fileCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Absolute URL
|
|
||||||
url := url.URL{Path: baseURL + name}
|
|
||||||
|
|
||||||
i := fileInfo{
|
|
||||||
Name: f.Name(),
|
|
||||||
Size: f.Size(),
|
|
||||||
ModTime: f.ModTime(),
|
|
||||||
Mode: f.Mode(),
|
|
||||||
IsDir: f.IsDir(),
|
|
||||||
URL: url.String(),
|
|
||||||
}
|
|
||||||
i.RetrieveFileType()
|
|
||||||
|
|
||||||
fileinfos = append(fileinfos, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &listing{
|
|
||||||
Name: path.Base(filePath),
|
|
||||||
Path: filePath,
|
|
||||||
Items: fileinfos,
|
|
||||||
NumDirs: dirCount,
|
|
||||||
NumFiles: fileCount,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApplySort applies the sort order using .Order and .Sort
|
|
||||||
func (l listing) ApplySort() {
|
|
||||||
// Check '.Order' to know how to sort
|
|
||||||
if l.Order == "desc" {
|
|
||||||
switch l.Sort {
|
|
||||||
case "name":
|
|
||||||
sort.Sort(sort.Reverse(byName(l)))
|
|
||||||
case "size":
|
|
||||||
sort.Sort(sort.Reverse(bySize(l)))
|
|
||||||
case "time":
|
|
||||||
sort.Sort(sort.Reverse(byTime(l)))
|
|
||||||
default:
|
|
||||||
// If not one of the above, do nothing
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else { // If we had more Orderings we could add them here
|
|
||||||
switch l.Sort {
|
|
||||||
case "name":
|
|
||||||
sort.Sort(byName(l))
|
|
||||||
case "size":
|
|
||||||
sort.Sort(bySize(l))
|
|
||||||
case "time":
|
|
||||||
sort.Sort(byTime(l))
|
|
||||||
default:
|
|
||||||
sort.Sort(byName(l))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement sorting for listing
|
|
||||||
type byName listing
|
|
||||||
type bySize listing
|
|
||||||
type byTime listing
|
|
||||||
|
|
||||||
// By Name
|
|
||||||
func (l byName) Len() int {
|
|
||||||
return len(l.Items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l byName) Swap(i, j int) {
|
|
||||||
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat upper and lower case equally
|
|
||||||
func (l byName) Less(i, j int) bool {
|
|
||||||
if l.Items[i].IsDir && !l.Items[j].IsDir {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !l.Items[i].IsDir && l.Items[j].IsDir {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// By Size
|
|
||||||
func (l bySize) Len() int {
|
|
||||||
return len(l.Items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l bySize) Swap(i, j int) {
|
|
||||||
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
const directoryOffset = -1 << 31 // = math.MinInt32
|
|
||||||
func (l bySize) Less(i, j int) bool {
|
|
||||||
iSize, jSize := l.Items[i].Size, l.Items[j].Size
|
|
||||||
if l.Items[i].IsDir {
|
|
||||||
iSize = directoryOffset + iSize
|
|
||||||
}
|
|
||||||
if l.Items[j].IsDir {
|
|
||||||
jSize = directoryOffset + jSize
|
|
||||||
}
|
|
||||||
return iSize < jSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// By Time
|
|
||||||
func (l byTime) Len() int {
|
|
||||||
return len(l.Items)
|
|
||||||
}
|
|
||||||
func (l byTime) Swap(i, j int) {
|
|
||||||
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
|
||||||
}
|
|
||||||
func (l byTime) Less(i, j int) bool {
|
|
||||||
return l.Items[i].ModTime.Before(l.Items[j].ModTime)
|
|
||||||
}
|
|
73
page.go
73
page.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
rice "github.com/GeertJohan/go.rice"
|
rice "github.com/GeertJohan/go.rice"
|
||||||
|
@ -32,16 +33,13 @@ var functions = template.FuncMap{
|
||||||
// page contains the information needed to fill a page template.
|
// page contains the information needed to fill a page template.
|
||||||
type page struct {
|
type page struct {
|
||||||
minimal bool
|
minimal bool
|
||||||
|
User *User
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
IsDir bool
|
|
||||||
User *User
|
|
||||||
PrefixURL string
|
|
||||||
BaseURL string
|
BaseURL string
|
||||||
WebDavURL string
|
WebDavURL string
|
||||||
|
IsEditor bool
|
||||||
Data interface{}
|
Data interface{}
|
||||||
Editor bool
|
|
||||||
Display string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// breadcrumbItem contains the Name and the URL of a breadcrumb piece.
|
// breadcrumbItem contains the Name and the URL of a breadcrumb piece.
|
||||||
|
@ -53,6 +51,7 @@ type breadcrumbItem struct {
|
||||||
// BreadcrumbMap returns p.Path where every element is a map
|
// BreadcrumbMap returns p.Path where every element is a map
|
||||||
// of URLs and path segment names.
|
// of URLs and path segment names.
|
||||||
func (p page) BreadcrumbMap() []breadcrumbItem {
|
func (p page) BreadcrumbMap() []breadcrumbItem {
|
||||||
|
// TODO: when it is preview alongside with listing!!!!!!!!!!
|
||||||
result := []breadcrumbItem{}
|
result := []breadcrumbItem{}
|
||||||
|
|
||||||
if len(p.Path) == 0 {
|
if len(p.Path) == 0 {
|
||||||
|
@ -102,8 +101,8 @@ func (p page) PreviousLink() string {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrintAsHTML formats the page in HTML and executes the template
|
// PrintHTML formats the page in HTML and executes the template
|
||||||
func (p page) PrintAsHTML(w http.ResponseWriter, box *rice.Box, templates ...string) (int, error) {
|
func (p page) PrintHTML(w http.ResponseWriter, box *rice.Box, templates ...string) (int, error) {
|
||||||
templates = append(templates, "actions")
|
templates = append(templates, "actions")
|
||||||
templates = append(templates, "templates")
|
templates = append(templates, "templates")
|
||||||
|
|
||||||
|
@ -153,7 +152,7 @@ func (p page) PrintAsHTML(w http.ResponseWriter, box *rice.Box, templates ...str
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrintAsJSON prints the current Page information in JSON
|
// PrintAsJSON prints the current Page information in JSON
|
||||||
func (p page) PrintAsJSON(w http.ResponseWriter) (int, error) {
|
func (p page) PrintJSON(w http.ResponseWriter) (int, error) {
|
||||||
marsh, err := json.MarshalIndent(p.Data, "", " ")
|
marsh, err := json.MarshalIndent(p.Data, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
|
@ -166,3 +165,61 @@ func (p page) PrintAsJSON(w http.ResponseWriter) (int, error) {
|
||||||
|
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// htmlError prints the error page
|
||||||
|
func htmlError(w http.ResponseWriter, code int, err error) (int, error) {
|
||||||
|
tpl := errTemplate
|
||||||
|
tpl = strings.Replace(tpl, "TITLE", strconv.Itoa(code)+" "+http.StatusText(code), -1)
|
||||||
|
tpl = strings.Replace(tpl, "CODE", err.Error(), -1)
|
||||||
|
|
||||||
|
_, err = w.Write([]byte(tpl))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
return http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const errTemplate = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>TITLE</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: #fff;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background-color: rgba(0,0,0,0.1);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 1em;
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
max-width: 40em;
|
||||||
|
margin: 2em auto 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #eee;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="center">
|
||||||
|
<h1>TITLE</h1>
|
||||||
|
|
||||||
|
<p>Try reloading the page or hitting the back button. If this error persists, it seems that you may have found a bug! Please create an issue at <a href="https://github.com/hacdias/caddy-filemanager/issues">hacdias/caddy-filemanager</a> repository on GitHub with the code below.</p>
|
||||||
|
|
||||||
|
<code>CODE</code>
|
||||||
|
</div>
|
||||||
|
</html>`
|
||||||
|
|
|
@ -82,6 +82,8 @@ func search(c *requestContext, w http.ResponseWriter, r *http.Request) (int, err
|
||||||
path = strings.ToLower(path)
|
path = strings.ToLower(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path = strings.TrimPrefix(path, scope)
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
path = strings.Replace(path, "\\", "/", -1)
|
path = strings.Replace(path, "\\", "/", -1)
|
||||||
is := false
|
is := false
|
||||||
|
|
||||||
|
@ -103,8 +105,6 @@ func search(c *requestContext, w http.ResponseWriter, r *http.Request) (int, err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
path = strings.TrimPrefix(path, scope)
|
|
||||||
path = strings.TrimPrefix(path, "/")
|
|
||||||
return conn.WriteMessage(websocket.TextMessage, []byte(path))
|
return conn.WriteMessage(websocket.TextMessage, []byte(path))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
package filemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func serveDefault(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
c.pg = &page{
|
||||||
|
Name: c.fi.Name,
|
||||||
|
Path: c.fi.VirtualPath,
|
||||||
|
User: c.us,
|
||||||
|
BaseURL: c.fm.RootURL(),
|
||||||
|
WebDavURL: c.fm.WebDavURL(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is a dir, go and serve the listing.
|
||||||
|
if c.fi.IsDir {
|
||||||
|
return serveListing(c, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to get the file type.
|
||||||
|
if err = c.fi.RetrieveFileType(); err != nil {
|
||||||
|
return errorToHTTP(err, true), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the request accepts JSON, we send the file information.
|
||||||
|
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||||
|
c.pg.Data = c.fi
|
||||||
|
return c.pg.PrintJSON(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is a text file, reads its content.
|
||||||
|
if c.fi.Type == "text" {
|
||||||
|
if err = c.fi.Read(); err != nil {
|
||||||
|
return errorToHTTP(err, true), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it can't be edited or the user isn't allowed to,
|
||||||
|
// serve it as a listing, with a preview of the file.
|
||||||
|
if !c.fi.CanBeEdited() || !c.us.AllowEdit {
|
||||||
|
return serveListing(c, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we just bring the editor in!
|
||||||
|
c.pg.IsEditor = true
|
||||||
|
c.pg.Data, err = getEditor(r, c.fi)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.pg.PrintHTML(w, c.fm.assets.templates, "frontmatter", "editor")
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveListing presents the user with a listage of a directory folder.
|
||||||
|
func serveListing(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
listing *listing
|
||||||
|
)
|
||||||
|
|
||||||
|
c.pg.minimal = (r.Header.Get("Minimal") == "true")
|
||||||
|
c.pg.IsEditor = false
|
||||||
|
|
||||||
|
if c.fi.IsDir {
|
||||||
|
// In this case, we are just showing a listing.
|
||||||
|
listing, err = getListing(c.us, c.fi.VirtualPath, c.fm.RootURL()+r.URL.Path)
|
||||||
|
} else {
|
||||||
|
// But in this case we are showing a listing alongside with a preview!
|
||||||
|
vpath := strings.TrimSuffix(c.fi.VirtualPath, c.fi.Name)
|
||||||
|
url := strings.TrimSuffix(r.URL.Path, c.fi.Name)
|
||||||
|
|
||||||
|
listing, err = getListing(c.us, vpath, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errorToHTTP(err, true), err
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieScope := c.fm.RootURL()
|
||||||
|
if cookieScope == "" {
|
||||||
|
cookieScope = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the query values into the Listing struct
|
||||||
|
var limit int
|
||||||
|
listing.Sort, listing.Order, limit, err = handleSortOrder(w, r, cookieScope)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
listing.ApplySort()
|
||||||
|
|
||||||
|
if limit > 0 && limit <= len(listing.Items) {
|
||||||
|
listing.Items = listing.Items[:limit]
|
||||||
|
listing.ItemsLimitedTo = limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a JSON request, print only the items... in JSON! (such a surprise -_-)
|
||||||
|
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||||
|
c.pg.Data = listing.Items
|
||||||
|
return c.pg.PrintJSON(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.fi.IsDir {
|
||||||
|
listing.Preview = true
|
||||||
|
listing.PreviewItem = c.fi
|
||||||
|
}
|
||||||
|
|
||||||
|
listing.Display = displayMode(w, r, cookieScope)
|
||||||
|
c.pg.Data = listing
|
||||||
|
return c.pg.PrintHTML(w, c.fm.assets.templates, "listing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// displayMode obtaisn the display mode from URL, or from the
|
||||||
|
// cookie.
|
||||||
|
func displayMode(w http.ResponseWriter, r *http.Request, scope string) string {
|
||||||
|
displayMode := r.URL.Query().Get("display")
|
||||||
|
|
||||||
|
if displayMode == "" {
|
||||||
|
if displayCookie, err := r.Cookie("display"); err == nil {
|
||||||
|
displayMode = displayCookie.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if displayMode == "" || (displayMode != "mosaic" && displayMode != "list") {
|
||||||
|
displayMode = "mosaic"
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "display",
|
||||||
|
Value: displayMode,
|
||||||
|
Path: scope,
|
||||||
|
Secure: r.TLS != nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
return displayMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
|
||||||
|
// and reads 'limit' if given. The latter is 0 if not given. Sets cookies.
|
||||||
|
func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
|
||||||
|
sort = r.URL.Query().Get("sort")
|
||||||
|
order = r.URL.Query().Get("order")
|
||||||
|
limitQuery := r.URL.Query().Get("limit")
|
||||||
|
|
||||||
|
// If the query 'sort' or 'order' is empty, use defaults or any values
|
||||||
|
// previously saved in Cookies.
|
||||||
|
switch sort {
|
||||||
|
case "":
|
||||||
|
sort = "name"
|
||||||
|
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
|
||||||
|
sort = sortCookie.Value
|
||||||
|
}
|
||||||
|
case "name", "size", "type":
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "sort",
|
||||||
|
Value: sort,
|
||||||
|
Path: scope,
|
||||||
|
Secure: r.TLS != nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
switch order {
|
||||||
|
case "":
|
||||||
|
order = "asc"
|
||||||
|
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
|
||||||
|
order = orderCookie.Value
|
||||||
|
}
|
||||||
|
case "asc", "desc":
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "order",
|
||||||
|
Value: order,
|
||||||
|
Path: scope,
|
||||||
|
Secure: r.TLS != nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if limitQuery != "" {
|
||||||
|
limit, err = strconv.Atoi(limitQuery)
|
||||||
|
// If the 'limit' query can't be interpreted as a number, return err.
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package filemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// serveWebDAV handles the webDAV route of the File Manager.
|
||||||
|
func serveWebDAV(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Checks for user permissions relatively to this path.
|
||||||
|
if !c.us.Allowed(strings.TrimPrefix(r.URL.Path, c.fm.webDavURL)) {
|
||||||
|
return http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case "GET", "HEAD":
|
||||||
|
// Excerpt from RFC4918, section 9.4:
|
||||||
|
//
|
||||||
|
// GET, when applied to a collection, may return the contents of an
|
||||||
|
// "index.html" resource, a human-readable view of the contents of
|
||||||
|
// the collection, or something else altogether.
|
||||||
|
//
|
||||||
|
// It was decided on https://github.com/hacdias/caddy-filemanager/issues/85
|
||||||
|
// that GET, for collections, will return the same as PROPFIND method.
|
||||||
|
path := strings.Replace(r.URL.Path, c.fm.webDavURL, "", 1)
|
||||||
|
path = c.us.scope + "/" + path
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
|
||||||
|
var i os.FileInfo
|
||||||
|
i, err = os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
// Is there any error? WebDav will handle it... no worries.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.IsDir() {
|
||||||
|
r.Method = "PROPFIND"
|
||||||
|
|
||||||
|
if r.Method == "HEAD" {
|
||||||
|
w = newResponseWriterNoBody(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
|
||||||
|
if !c.us.AllowEdit {
|
||||||
|
return http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
case "MKCOL", "COPY":
|
||||||
|
if !c.us.AllowNew {
|
||||||
|
return http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preprocess the PUT request if it's the case
|
||||||
|
if r.Method == http.MethodPut {
|
||||||
|
if err = c.fm.BeforeSave(r, c.fm, c.us); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if put(c, w, r) != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.fm.handler.ServeHTTP(w, r)
|
||||||
|
if err = c.fm.AfterSave(r, c.fm, c.us); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
Loading…
Reference in New Issue