rebuilding js

Former-commit-id: 027e2f6546614d28750e437b9a3545cb95235d9d [formerly 6dbfe621a5774304295c17f216b5c96beaaaa95a] [formerly d44822f30d9a3649b20daa7a3cdbf86c87e63c99 [formerly 3258552349]]
Former-commit-id: 7f34ddc1b32076c6ad2c2a4374b170b7f5d84000 [formerly aaafd299a933d25ebcb5fdebe1b00cb9e8309d7a]
Former-commit-id: 7bb183c165ba2c9711ba1c04e3af6e2048245ded
This commit is contained in:
Henrique Dias 2017-06-27 19:00:58 +01:00
parent 1e99d3d7c1
commit 826d491ff1
20 changed files with 11576 additions and 1816 deletions

File diff suppressed because it is too large Load Diff

122
_assets/js/app.js Normal file
View File

@ -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)

View File

@ -1,7 +1,10 @@
'use strict'
var data = (window.data || window.alert('Something is wrong, please refresh!'))
/*
var tempID = '_fm_internal_temporary_id'
var ssl = (window.location.protocol === 'https:')
var templates = {}
var selectedItems = []
var overlay
@ -34,14 +37,6 @@ Document.prototype.getCookie = function (name) {
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) {
for (let i = 0; i < rules.length; i++) {
@ -136,15 +131,15 @@ buttons.setDone = function (name, success = true) {
var webdav = {}
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) {
return new Promise((resolve, reject) => {
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.setRequestHeader('Destination', destination)
@ -276,7 +271,7 @@ var preventDefault = function (event) {
function logoutEvent (event) {
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.onreadystatechange = function () {
if (request.readyState === 4) {
@ -439,132 +434,6 @@ function deleteEvent (event) {
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) {
event.preventDefault()
@ -620,7 +489,7 @@ document.addEventListener('DOMContentLoaded', function (event) {
templates.message = document.querySelector('#message-template')
templates.move = document.querySelector('#move-template')
if (user.AllowEdit) {
if (data.user.AllowEdit) {
buttons.delete.addEventListener('click', deleteEvent)
}

9613
_assets/js/vue.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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" }}
{{- 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">
<i class="material-icons">{{- if eq .Display "mosaic" }}view_list{{ else }}view_module{{ end }}</i>
<span>Switch view</span>
@ -11,14 +29,14 @@
</button>
{{- end }}
{{- if and (.User.AllowNew) (.IsDir) }}
{{- if and (.User.AllowNew) (not .IsEditor) }}
<button aria-label="Upload" title="Upload" class="action" id="upload">
<i class="material-icons">file_upload</i>
<span>Upload</span>
</button>
{{- end }}
{{- if and .User.AllowEdit (not .IsDir) }}
{{- if and .User.AllowEdit (.IsEditor) }}
<button aria-label="Delete" title="Delete" class="action" id="delete">
<i class="material-icons">delete</i>
<span>Delete</span>
@ -26,11 +44,11 @@
{{- end }}
<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>
{{- if not .IsDir}}</a>{{ end }}
{{- if .IsEditor}}</a>{{ end }}
{{- if .IsDir }}
{{- if not .IsEditor }}
<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 Tar" data-format="tar" href="?download=tar"><li>tar</li></a>
@ -41,14 +59,16 @@
{{- end }}
</button>
<button title="Info" aria-label="Info" class="action" id="info">
<i class="material-icons">info</i>
<span>Info</span>
</button>
{{ end }}
{{ end }}
{{ define "left-side-actions" }}
{{- if and (not .IsDir) (.User.AllowEdit) }}
{{ end }}
{{ define "left" }}
{{- if and (not .IsDir) (.User.AllowEdit) }}
{{- if .Editor}}
{{- if eq .Data.Mode "markdown" }}
@ -64,12 +84,12 @@
{{- end }}
{{/* end if editor */}}
{{- end }}
<button aria-label="Save" class="action" id="save">
<i class="material-icons" title="Save">save</i>
</button>
{{- end }}
{{/* end if not dir and AllowEdit */}}
{{- end }}
@ -85,7 +105,7 @@
</button>
<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>
{{- end }}
{{ end }}
{{ end }}

View File

@ -16,29 +16,22 @@
var data = {
user: JSON.parse('{{ Marshal .User }}'),
webdavURL: "{{ .WebDavURL }}",
baseURL: "{{.BaseURL}}",
prefixURL:"{{ .PrefixURL }}"
baseURL: "{{.BaseURL}}"
}
</script>
<!-- TODO: remove this one after moving the JS to ^^ -->
<script>
var user = JSON.parse('{{ Marshal .User }}'),
webdavURL = "{{ .WebDavURL }}",
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/vue.js" defer></script>
<script src="{{ .BaseURL }}/_internal/js/app.js" defer></script>
<!--
{{- if .IsEditor }}
<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/editor.js" defer></script>
{{- end }}
{{- else }}
<script src="{{ .BaseURL }}/_internal/js/listing.js" defer></script>
{{- end }} -->
</head>
<body>
<body>
<header>
<div id="top-bar">
<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="24" fill="#fff"/>
</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>
<input type="text" aria-label="Write here to search" placeholder="Search or execute a command...">
<div>
<input type="text"
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>
<p><i class="material-icons spin">autorenew</i></p>
</div>
</div>
</div>
<div id="bottom-bar">
<div>
{{- if ne .Name "/"}}
<div data-dropdown tabindex="0" aria-label="Previous" role="button" class="action" id="previous">
<i class="material-icons" title="Previous">subdirectory_arrow_left</i>
<ul class="dropdown" id="breadcrumbs">
{{- range $item := .BreadcrumbMap }}
<a tabindex="0" href="{{ $absURL }}{{ $item.URL }}"><li>{{ $item.Name }}</li></a>
{{- end }}
</ul>
</div>
{{- end }}
<div>
{{- if ne .Name "/"}}
<div data-dropdown tabindex="0" aria-label="Previous" role="button" class="action" id="previous">
<i class="material-icons" title="Previous">subdirectory_arrow_left</i>
<ul class="dropdown" id="breadcrumbs">
{{- range $item := .BreadcrumbMap }}
<a tabindex="0" href="{{ $absURL }}{{ $item.URL }}"><li>{{ $item.Name }}</li></a>
{{- end }}
</ul>
</div>
{{- end }}
{{ if ne .Name "/"}}<p id="current-file">{{ .Name }}</p>{{ end }}
</div>
{{ if ne .Name "/"}}<p id="current-file">{{ .Name }}</p>{{ end }}
</div>
<div class="actions{{ if .IsDir }} disabled{{ end }}" id="file-only">
{{- template "left-side-actions" . -}}
</div>
<div class="actions{{ if not .IsEditor }} disabled{{ end }}" id="file-only">
{{- template "left-side-actions" . -}}
</div>
<button aria-label="More" class="action mobile-only" id="more">
<i class="material-icons">more_vert</i>
</button>
<button aria-label="More" class="action mobile-only" id="more">
<i class="material-icons">more_vert</i>
</button>
<div class="actions" id="main-actions">
{{- template "right-side-actions" . -}}
</div>
<div class="actions" id="main-actions">
{{- template "right-side-actions" . -}}
</div>
</div>
<div id="click-overlay"></div>
</header>
<div id="multiple-selection" class="mobile-only">
<p>Multiple selection enabled</p>
<div tabindex="0" role="button" class="action" id="multiple-selection-cancel">
<i class="material-icons" title="Clear">clear</i>
</div>
</div>
<div id="multiple-selection" class="mobile-only">
<p>Multiple selection enabled</p>
<div tabindex="0" role="button" class="action" id="multiple-selection-cancel">
<i class="material-icons" title="Clear">clear</i>
</div>
</div>
<nav id="sidebar">
<a class="action" href="{{ .BaseURL }}/">
<i class="material-icons">folder</i>
<span>My Files</span>
</a>
<div class="action" id="logout" tabindex="0" role="button" aria-label="Log out">
<i class="material-icons" title="Logout">exit_to_app</i>
<span>Logout</span>
</div>
</nav>
<nav id="sidebar">
<a class="action" href="{{ .BaseURL }}/">
<i class="material-icons">folder</i>
<span>My Files</span>
</a>
<div class="action" id="logout" tabindex="0" role="button" aria-label="Log out">
<i class="material-icons" title="Logout">exit_to_app</i>
<span>Logout</span>
</div>
</nav>
<main>
{{- template "content" . }}
</main>
<main>
{{- template "content" . }}
</main>
<div class="overlay"></div>
<div class="overlay"></div>
{{- if and (.User.AllowNew) (.IsDir) }}
<div class="floating">
<div tabindex="0" role="button" class="action" id="new">
<i class="material-icons" title="New file or directory">add</i>
</div>
</div>
{{- end }}
{{- if and (.User.AllowNew) (not .IsEditor) }}
<div class="floating">
<div tabindex="0" role="button" class="action" id="new">
<i class="material-icons" title="New file or directory">add</i>
</div>
</div>
{{- 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>
</html>

View File

@ -1,5 +1,39 @@
{{ 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 -}}
<div>
<div class="item header">

View File

@ -1,23 +1,35 @@
{{ define "content" }}
{{ with .Data}}
<main class="container">
{{ if eq .Type "image" }}
<center><img src="{{ .URL }}?raw=true"></center>
{{ else if eq .Type "audio" }}
<audio src="{{ .URL }}?raw=true" controls></audio>
{{ else if eq .Type "video" }}
<video src="{{ .URL }}?raw=true" controls>
<div id="previewer">
<div class="bar">
<button class="action" aria-label="Close Preview" id="close">
<i class="material-icons">close</i>
</button>
{{ 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,
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 }}
</main>
{{ end }}
</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 }}

43
assets.go Normal file
View File

@ -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
}

18
checksum.go Normal file
View File

@ -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
}

95
download.go Normal file
View File

@ -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
}

View File

@ -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
View File

@ -1,6 +1,7 @@
package filemanager
import (
"context"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
@ -14,7 +15,9 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
@ -29,25 +32,53 @@ var (
type fileInfo struct {
// Used to store the file's content temporarily.
content []byte
Name string
Size int64
URL string
// The name of the file.
Name string
// The Size of the file.
Size int64
// The absolute URL.
URL string
// The extension of the file.
Extension string
ModTime time.Time
Mode os.FileMode
IsDir bool
// The last modified time.
ModTime time.Time
// The File Mode.
Mode os.FileMode
// Indicates if this file is a directory.
IsDir bool
// Absolute path.
Path string
// Relative path to user's virtual File System.
VirtualPath string
// Indicates the file content type: video, text, image, music or blob.
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
// respective HTTP error code
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
}
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",
// 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
}
// 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 {
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
View File

@ -1,24 +1,23 @@
package filemanager
import (
"encoding/json"
"errors"
"io"
"io/ioutil"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/mholt/archiver"
)
// assetsURL is the url where static assets are served.
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) {
var (
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:
http.ServeFile(w, r, f.Path)
code, err = 0, nil
case f.IsDir:
code, err = serveListing(c, w, r)
default:
code, err = serveSingle(c, w, r)
code, err = serveDefault(c, w, r)
}
if err != nil {
@ -126,371 +123,54 @@ func serveHTTP(c *requestContext, w http.ResponseWriter, r *http.Request) (int,
return http.StatusNotImplemented, nil
}
// serveWebDAV handles the webDAV route of the File Manager.
func serveWebDAV(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var err error
// 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
}
// Checks for user permissions relatively to this path.
if !c.us.Allowed(strings.TrimPrefix(r.URL.Path, c.fm.webDavURL)) {
return http.StatusForbidden, nil
}
// newResponseWriterNoBody creates a new responseWriterNoBody.
func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
return &responseWriterNoBody{w}
}
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
}
// 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
}
// 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)
// WriteHeader writes the header to the http.ResponseWriter.
func (w responseWriterNoBody) WriteHeader(statusCode int) {
w.ResponseWriter.WriteHeader(statusCode)
}
var file []byte
var err error
// 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 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)
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:
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
}

View File

@ -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
}
}

View File

@ -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
View File

@ -7,6 +7,7 @@ import (
"html/template"
"log"
"net/http"
"strconv"
"strings"
rice "github.com/GeertJohan/go.rice"
@ -32,16 +33,13 @@ var functions = template.FuncMap{
// page contains the information needed to fill a page template.
type page struct {
minimal bool
User *User
Name string
Path string
IsDir bool
User *User
PrefixURL string
BaseURL string
WebDavURL string
IsEditor bool
Data interface{}
Editor bool
Display string
}
// 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
// of URLs and path segment names.
func (p page) BreadcrumbMap() []breadcrumbItem {
// TODO: when it is preview alongside with listing!!!!!!!!!!
result := []breadcrumbItem{}
if len(p.Path) == 0 {
@ -102,8 +101,8 @@ func (p page) PreviousLink() string {
return path
}
// PrintAsHTML formats the page in HTML and executes the template
func (p page) PrintAsHTML(w http.ResponseWriter, box *rice.Box, templates ...string) (int, error) {
// PrintHTML formats the page in HTML and executes the template
func (p page) PrintHTML(w http.ResponseWriter, box *rice.Box, templates ...string) (int, error) {
templates = append(templates, "actions")
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
func (p page) PrintAsJSON(w http.ResponseWriter) (int, error) {
func (p page) PrintJSON(w http.ResponseWriter) (int, error) {
marsh, err := json.MarshalIndent(p.Data, "", " ")
if err != nil {
return http.StatusInternalServerError, err
@ -166,3 +165,61 @@ func (p page) PrintAsJSON(w http.ResponseWriter) (int, error) {
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>`

View File

@ -82,6 +82,8 @@ func search(c *requestContext, w http.ResponseWriter, r *http.Request) (int, err
path = strings.ToLower(path)
}
path = strings.TrimPrefix(path, scope)
path = strings.TrimPrefix(path, "/")
path = strings.Replace(path, "\\", "/", -1)
is := false
@ -103,8 +105,6 @@ func search(c *requestContext, w http.ResponseWriter, r *http.Request) (int, err
return nil
}
path = strings.TrimPrefix(path, scope)
path = strings.TrimPrefix(path, "/")
return conn.WriteMessage(websocket.TextMessage, []byte(path))
})

192
serve.go Normal file
View File

@ -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
}

74
webdav.go Normal file
View File

@ -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
}