mirror of
https://github.com/filebrowser/filebrowser.git
synced 2024-06-07 23:00:43 +00:00
Merge branch 'dev'
This commit is contained in:
commit
46e19a7094
@ -33,6 +33,10 @@ video {
|
|||||||
display: inline-block
|
display: inline-block
|
||||||
}
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
audio:not([controls]) {
|
audio:not([controls]) {
|
||||||
display: none;
|
display: none;
|
||||||
height: 0
|
height: 0
|
||||||
@ -260,7 +264,7 @@ textarea {
|
|||||||
body {
|
body {
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: 'Roboto', sans-serif;
|
||||||
padding-top: 5em;
|
padding-top: 5em;
|
||||||
background-color: #fcfcfc;
|
background-color: #ffffff;
|
||||||
text-rendering: optimizespeed;
|
text-rendering: optimizespeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,6 +473,8 @@ header {
|
|||||||
z-index: 999;
|
z-index: 999;
|
||||||
padding: 1.7em 0;
|
padding: 1.7em 0;
|
||||||
background-color: #2196f3;
|
background-color: #2196f3;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.075);
|
||||||
|
box-shadow: 0 0 5px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
header h1 {
|
header h1 {
|
||||||
@ -477,7 +483,9 @@ header h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header a,
|
header a,
|
||||||
header a:hover {
|
header a:hover,
|
||||||
|
#toolbar a,
|
||||||
|
#toolbar a:hover {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,6 +574,7 @@ header p i {
|
|||||||
transition: .1s ease all;
|
transition: .1s ease all;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search.active div i,
|
#search.active div i,
|
||||||
@ -675,8 +684,7 @@ header .only-side {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
header #prev:hover+.prev-links,
|
.action:hover ul {
|
||||||
header .prev-links:hover {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -684,41 +692,53 @@ header .prev-links:hover {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
header .prev-links {
|
.action ul {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 3.1em;
|
||||||
left: 0;
|
left: 0;
|
||||||
color: #7d7d7d;
|
color: #7d7d7d;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
|
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
|
||||||
border-radius: .2em;
|
border-radius: .2em;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
display: none;
|
display: none;
|
||||||
transition: .2s ease all;
|
transition: .2s ease all;
|
||||||
min-width: 12em;
|
min-width: 3em;
|
||||||
|
z-index: 99999;
|
||||||
}
|
}
|
||||||
|
|
||||||
header .prev-links a {
|
.action ul:before {
|
||||||
padding: .5em;
|
top: -16px;
|
||||||
|
left: 1em;
|
||||||
|
right: auto;
|
||||||
|
border: 8px solid transparent;
|
||||||
|
border-bottom-color: #ffffff;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action ul a {
|
||||||
|
padding: .3em .5em;
|
||||||
border-bottom: 1px solid #f5f5f5;
|
border-bottom: 1px solid #f5f5f5;
|
||||||
transition: .2s ease all;
|
transition: .2s ease all;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
header .prev-links a:first-child {
|
.action ul a:first-child {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom-right-radius: .2em;
|
border-bottom-right-radius: .2em;
|
||||||
border-bottom-left-radius: .2em;
|
border-bottom-left-radius: .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
header .prev-links a:last-child {
|
.action ul a:last-child {
|
||||||
border-top-right-radius: .2em;
|
border-top-right-radius: .2em;
|
||||||
border-top-left-radius: .2em;
|
border-top-left-radius: .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
header .prev-links a:hover {
|
.action ul a:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -774,7 +794,7 @@ header .action span {
|
|||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24);
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
width: 10em;
|
width: 22em;
|
||||||
border-radius: .2em;
|
border-radius: .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1167,3 +1187,9 @@ i.spin {
|
|||||||
column-gap: 0;
|
column-gap: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 450px) {
|
||||||
|
#toolbar p {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// TODO: way to get the webdav url
|
||||||
|
|
||||||
var tempID = "_fm_internal_temporary_id"
|
var tempID = "_fm_internal_temporary_id"
|
||||||
var selectedItems = [];
|
var selectedItems = [];
|
||||||
var token = "";
|
var token = "";
|
||||||
@ -87,7 +89,7 @@ Element.prototype.changeToDone = function(error, html) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var toWebDavURL = function(url) {
|
var toWebDavURL = function(url) {
|
||||||
url = url.replace("/", "/webdav/")
|
url = url.replace(baseURL + "/", webdavURL + "/");
|
||||||
return window.location.origin + url
|
return window.location.origin + url
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,22 +151,6 @@ var preventDefault = function(event) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download file event
|
|
||||||
var downloadEvent = function(event) {
|
|
||||||
if (this.classList.contains('disabled')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (selectedItems.length) {
|
|
||||||
Array.from(selectedItems).forEach(item => {
|
|
||||||
window.open(item + "?download=true");
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.open(window.location + "?download=true");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the last directory of an url
|
// Remove the last directory of an url
|
||||||
var RemoveLastDirectoryPartOf = function(url) {
|
var RemoveLastDirectoryPartOf = function(url) {
|
||||||
var arr = url.split('/');
|
var arr = url.split('/');
|
||||||
@ -299,24 +285,20 @@ var renameEvent = function(event) {
|
|||||||
var handleFiles = function(files) {
|
var handleFiles = function(files) {
|
||||||
let button = document.getElementById("upload");
|
let button = document.getElementById("upload");
|
||||||
let html = button.changeToLoading();
|
let html = button.changeToLoading();
|
||||||
let data = new FormData();
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
data.append(files[i].name, files[i]);
|
let request = new XMLHttpRequest();
|
||||||
}
|
request.open('PUT', toWebDavURL(window.location.pathname + files[i].name));
|
||||||
|
request.setRequestHeader('Token', token);
|
||||||
|
request.send(files[i]);
|
||||||
|
request.onreadystatechange = function() {
|
||||||
|
if (request.readyState == 4) {
|
||||||
|
if (request.status == 201) {
|
||||||
|
reloadListing();
|
||||||
|
}
|
||||||
|
|
||||||
let request = new XMLHttpRequest();
|
button.changeToDone((request.status != 201), html);
|
||||||
request.open('POST', window.location.pathname);
|
|
||||||
request.setRequestHeader("Upload", "true");
|
|
||||||
request.setRequestHeader('Token', token);
|
|
||||||
request.send(data);
|
|
||||||
request.onreadystatechange = function() {
|
|
||||||
if (request.readyState == 4) {
|
|
||||||
if (request.status == 200) {
|
|
||||||
reloadListing();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.changeToDone((request.status != 200), html);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,6 +399,7 @@ var addNewDirEvents = function() {
|
|||||||
|
|
||||||
// Handles the new directory event
|
// Handles the new directory event
|
||||||
var newDirEvent = function(event) {
|
var newDirEvent = function(event) {
|
||||||
|
// TODO: create new dir button and new file button
|
||||||
if (event.keyCode == 27) {
|
if (event.keyCode == 27) {
|
||||||
document.getElementById('newdir').classList.toggle('enabled');
|
document.getElementById('newdir').classList.toggle('enabled');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -430,13 +413,14 @@ var newDirEvent = function(event) {
|
|||||||
let button = document.getElementById('new');
|
let button = document.getElementById('new');
|
||||||
let html = button.changeToLoading();
|
let html = button.changeToLoading();
|
||||||
let request = new XMLHttpRequest();
|
let request = new XMLHttpRequest();
|
||||||
request.open("POST", window.location);
|
let name = document.getElementById('newdir').value;
|
||||||
|
|
||||||
|
request.open((name.endsWith("/") ? "MKCOL" : "PUT"), toWebDavURL(window.location.pathname + name));
|
||||||
request.setRequestHeader('Token', token);
|
request.setRequestHeader('Token', token);
|
||||||
request.setRequestHeader('Filename', document.getElementById('newdir').value);
|
|
||||||
request.send();
|
request.send();
|
||||||
request.onreadystatechange = function() {
|
request.onreadystatechange = function() {
|
||||||
if (request.readyState == 4) {
|
if (request.readyState == 4) {
|
||||||
button.changeToDone((request.status != 200), html);
|
button.changeToDone((request.status != 201), html);
|
||||||
reloadListing(() => {
|
reloadListing(() => {
|
||||||
addNewDirEvents();
|
addNewDirEvents();
|
||||||
});
|
});
|
||||||
@ -466,6 +450,8 @@ document.addEventListener("changed-selected", function(event) {
|
|||||||
document.getElementById("rename").classList.remove("disabled");
|
document.getElementById("rename").classList.remove("disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
redefineDownloadURLs();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,6 +459,22 @@ document.addEventListener("changed-selected", function(event) {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var redefineDownloadURLs = function() {
|
||||||
|
let files = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < selectedItems.length; i++) {
|
||||||
|
files += selectedItems[i].replace(window.location.pathname, "") + ",";
|
||||||
|
}
|
||||||
|
|
||||||
|
files = files.substring(0, files.length - 1);
|
||||||
|
files = encodeURIComponent(files);
|
||||||
|
|
||||||
|
let links = document.querySelectorAll("#download ul a");
|
||||||
|
Array.from(links).forEach(link => {
|
||||||
|
link.href = "?download=" + link.dataset.format + "&files=" + files;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var searchEvent = function(event) {
|
var searchEvent = function(event) {
|
||||||
let value = this.value;
|
let value = this.value;
|
||||||
let box = document.querySelector('#search div');
|
let box = document.querySelector('#search div');
|
||||||
@ -832,13 +834,13 @@ document.addEventListener("editor", (event) => {
|
|||||||
let data = form2js(document.querySelector('form'));
|
let data = form2js(document.querySelector('form'));
|
||||||
let html = button.changeToLoading();
|
let html = button.changeToLoading();
|
||||||
let request = new XMLHttpRequest();
|
let request = new XMLHttpRequest();
|
||||||
request.open("PUT", window.location);
|
request.open("PUT", toWebDavURL(window.location.pathname));
|
||||||
request.setRequestHeader('Kind', kind);
|
request.setRequestHeader('Kind', kind);
|
||||||
request.setRequestHeader('Token', token);
|
request.setRequestHeader('Token', token);
|
||||||
request.send(JSON.stringify(data));
|
request.send(JSON.stringify(data));
|
||||||
request.onreadystatechange = function() {
|
request.onreadystatechange = function() {
|
||||||
if (request.readyState == 4) {
|
if (request.readyState == 4) {
|
||||||
button.changeToDone((request.status != 200), html);
|
button.changeToDone((request.status != 201), html);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -891,7 +893,6 @@ document.addEventListener("DOMContentLoaded", function(event) {
|
|||||||
document.getElementById("delete").addEventListener("click", deleteEvent);
|
document.getElementById("delete").addEventListener("click", deleteEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("download").addEventListener("click", downloadEvent);
|
|
||||||
document.getElementById("open-nav").addEventListener("click", event => {
|
document.getElementById("open-nav").addEventListener("click", event => {
|
||||||
document.querySelector("header > div:nth-child(2)").classList.toggle("active");
|
document.querySelector("header > div:nth-child(2)").classList.toggle("active");
|
||||||
});
|
});
|
28
_embed/templates/actions.tmpl
Normal file
28
_embed/templates/actions.tmpl
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{{ define "actions" }}
|
||||||
|
<div class="action" id="open">
|
||||||
|
<i class="material-icons" title="See raw">open_in_new</i> <span>See raw</span>
|
||||||
|
</div>
|
||||||
|
{{ if and .IsDir .User.AllowEdit }}
|
||||||
|
<div class="action" id="rename">
|
||||||
|
<i class="material-icons" title="Edit">mode_edit</i>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
<div class="action" id="download">
|
||||||
|
<a href="?download=true">
|
||||||
|
<i class="material-icons" title="Download">file_download</i> <span>Download</span>
|
||||||
|
</a>
|
||||||
|
{{ if .IsDir }}
|
||||||
|
<ul class="prev-links">
|
||||||
|
<a data-format="tarbz2" href="?download=tarbz2"><li>tar.bz2</li></a>
|
||||||
|
<a data-format="targz" href="?download=targz"><li>tar.gz</li></a>
|
||||||
|
<a data-format="tar" href="?download=tar"><li>tar</li></a>
|
||||||
|
<a data-format="zip" href="?download=zip"><li>zip</li></a>
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ if .User.AllowEdit }}
|
||||||
|
<div class="action" id="delete">
|
||||||
|
<i class="material-icons" title="Delete">delete</i> <span>Delete</span>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
126
_embed/templates/base.tmpl
Normal file
126
_embed/templates/base.tmpl
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
{{ $absURL := .Config.AbsoluteURL }}
|
||||||
|
<head>
|
||||||
|
<title>{{.Name}}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
<link href='https://fonts.googleapis.com/css?family=Roboto:400,500' rel='stylesheet' type='text/css'>
|
||||||
|
<link rel="stylesheet" href="{{ .Config.AbsoluteURL }}/_filemanagerinternal/css/styles.css">
|
||||||
|
{{ if ne .User.StyleSheet "" }}
|
||||||
|
<style>{{ CSS .User.StyleSheet }}</style>
|
||||||
|
{{ end }}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
{{ $lnk := .PreviousLink }}
|
||||||
|
<div class="action{{ if eq $lnk ""}} disabled{{ end }}" id="prev">
|
||||||
|
{{ if ne $lnk ""}}<a href="{{ $lnk }}">{{ end }}
|
||||||
|
<i class="material-icons" title="Previous">subdirectory_arrow_left</i>
|
||||||
|
{{ if ne $lnk ""}}</a>{{ end }}
|
||||||
|
|
||||||
|
{{ if ne $lnk ""}}
|
||||||
|
<ul class="prev-links">
|
||||||
|
{{ range $link, $name := .BreadcrumbMap }}<a href="{{ $absURL }}{{ $link }}"><li>{{ $name }}</li></a>{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action" id="open-nav">
|
||||||
|
<i class="material-icons" title="Menu">menu</i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if ne .Name "/"}}<p>{{ .Name }}</p>{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="only-side">
|
||||||
|
{{ $lnk := .PreviousLink }}
|
||||||
|
{{ if ne $lnk ""}}<a href="{{ $lnk }}">{{ end }}
|
||||||
|
<div class="action{{ if eq $lnk ""}} disabled{{ end }}" id="prev">
|
||||||
|
<i class="material-icons" title="Previous">subdirectory_arrow_left</i>
|
||||||
|
</div>
|
||||||
|
{{ if ne $lnk ""}}</a>{{ end }}
|
||||||
|
|
||||||
|
<p><a href="{{ if eq .Config.AbsoluteURL "" }}/{{ else }}{{ .Config.AbsoluteURL }}{{ end }}">File Manager</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if .IsDir}}
|
||||||
|
{{ if .User.AllowCommands }}
|
||||||
|
<div id="search">
|
||||||
|
<i class="material-icons" title="Storage">storage</i>
|
||||||
|
<input type="text" placeholder="Execute a command...">
|
||||||
|
<div>Write your git, mercurial or svn command and press enter.</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="action" id="view">
|
||||||
|
<i class="material-icons" title="Switch view">view_headline</i> <span>Switch view</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if .User.AllowNew }}
|
||||||
|
<div class="action" id="upload">
|
||||||
|
<i class="material-icons" title="Upload">file_upload</i> <span>Upload</span>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="action">
|
||||||
|
<a href="?download=true">
|
||||||
|
<i class="material-icons" title="Download">file_download</i> <span>Download</span>
|
||||||
|
</a>
|
||||||
|
<ul class="prev-links">
|
||||||
|
<a href="?download=tarbz2"><li>tar.bz2</li></a>
|
||||||
|
<a href="?download=targz"><li>tar.gz</li></a>
|
||||||
|
<a href="?download=tar"><li>tar</li></a>
|
||||||
|
<a href="?download=zip"><li>zip</li></a>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{ else }}
|
||||||
|
{{ template "actions" . }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="action" id="logout">
|
||||||
|
<i class="material-icons" title="Logout">exit_to_app</i> <span>Logout</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="overlay"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{ if .IsDir }}
|
||||||
|
<div id="toolbar">
|
||||||
|
<div>
|
||||||
|
<div class="action" id="back">
|
||||||
|
<i class="material-icons" title="Back">arrow_back</i>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<span id="selected-number">0</span>
|
||||||
|
selected.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ template "actions" . }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{{ template "content" . }}
|
||||||
|
<span id="token">{{ .Config.Token }}</span>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Served with
|
||||||
|
<a rel="noopener noreferrer" href="https://caddyserver.com">Caddy</a>
|
||||||
|
and
|
||||||
|
<a rel="noopener noreferrer" href="https://github.com/hacdias/caddy-filemanager">File Manager</a>.
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- SCRIPTS -->
|
||||||
|
<!-- User Data and Permissions; WebDavURL -->
|
||||||
|
<script>var user = JSON.parse('{{ Marshal .User }}'), webdavURL = "{{.Config.WebDavURL}}", baseURL = "{{.Config.BaseURL}}";</script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/ace.js"></script>
|
||||||
|
<script src="{{ .Config.AbsoluteURL }}/_filemanagerinternal/js/form2js.js"></script>
|
||||||
|
<script src="{{ .Config.AbsoluteURL }}/_filemanagerinternal/js/application.js"></script>
|
||||||
|
{{ if .Config.HugoEnabled }}<script src="{{ .Config.AbsoluteURL }}/_hugointernal/js/application.js"></script>{{ end }}
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -38,7 +38,7 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if .User.AllowNew }}
|
{{ if .User.AllowNew }}
|
||||||
<input id="newdir" type="text" placeholder="Name...">
|
<input id="newdir" type="text" placeholder="Name. End with a trailing slash to create a dir.">
|
||||||
<div class="floating">
|
<div class="floating">
|
||||||
<div class="action" id="new">
|
<div class="action" id="new">
|
||||||
<i class="material-icons" title="New file or directory. If you don't write an extension, a directory will be created.">add</i>
|
<i class="material-icons" title="New file or directory. If you don't write an extension, a directory will be created.">add</i>
|
21
_embed/templates/single.tmpl
Normal file
21
_embed/templates/single.tmpl
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{{ define "content" }}
|
||||||
|
{{ with .Data}}
|
||||||
|
<main class="container">
|
||||||
|
{{ if eq .Type "image" }}
|
||||||
|
<img src="{{ .URL }}?raw=true">
|
||||||
|
{{ else if eq .Type "audio" }}
|
||||||
|
<audio src="{{ .URL }}?raw=true"></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 .Type "blob" }}
|
||||||
|
<a href="?download=true">Download</a>
|
||||||
|
{{ else}}
|
||||||
|
<pre>{{ .StringifyContent }}</pre>
|
||||||
|
{{ end }}
|
||||||
|
</main>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
@ -1,18 +0,0 @@
|
|||||||
{{ define "actions" }}
|
|
||||||
<div class="action" id="open">
|
|
||||||
<i class="material-icons" title="See raw">open_in_new</i> <span>See raw</span>
|
|
||||||
</div>
|
|
||||||
{{ if and .IsDir .User.AllowEdit }}
|
|
||||||
<div class="action" id="rename">
|
|
||||||
<i class="material-icons" title="Edit">mode_edit</i>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
<div class="action" id="download">
|
|
||||||
<i class="material-icons" title="Download">file_download</i> <span>Download</span>
|
|
||||||
</div>
|
|
||||||
{{ if .User.AllowEdit }}
|
|
||||||
<div class="action" id="delete">
|
|
||||||
<i class="material-icons" title="Delete">delete</i> <span>Delete</span>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
@ -1,139 +0,0 @@
|
|||||||
{{ $absURL := .Config.AbsoluteURL }}
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>{{.Name}}</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
||||||
<link href='https://fonts.googleapis.com/css?family=Roboto:400,500' rel='stylesheet' type='text/css'>
|
|
||||||
<link rel="stylesheet" href="{{ .Config.AbsoluteURL }}/_filemanagerinternal/css/styles.css">
|
|
||||||
|
|
||||||
{{ if ne .User.StyleSheet "" }}
|
|
||||||
<style>
|
|
||||||
{{ CSS .User.StyleSheet }}
|
|
||||||
</style>
|
|
||||||
{{ end }}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<div>
|
|
||||||
{{ $lnk := .PreviousLink }}
|
|
||||||
|
|
||||||
<div class="action{{ if eq $lnk ""}} disabled{{ end }}" id="prev">
|
|
||||||
{{ if ne $lnk ""}}
|
|
||||||
<a href="{{ $lnk }}">
|
|
||||||
{{ end }}
|
|
||||||
<i class="material-icons" title="Previous">subdirectory_arrow_left</i>
|
|
||||||
{{ if ne $lnk ""}}
|
|
||||||
</a>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ if ne $lnk ""}}
|
|
||||||
<ul class="prev-links">
|
|
||||||
{{ range $link, $name := .BreadcrumbMap }}
|
|
||||||
<a href="{{ $absURL }}{{ $link }}"><li>{{ $name }}</li></a>
|
|
||||||
{{ end }}
|
|
||||||
</ul>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<div class="action" id="open-nav">
|
|
||||||
<i class="material-icons" title="Menu">menu</i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{ if ne .Name "/"}}
|
|
||||||
{{ .Name }}
|
|
||||||
</p>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="only-side">
|
|
||||||
{{ $lnk := .PreviousLink }}
|
|
||||||
{{ if ne $lnk ""}}
|
|
||||||
<a href="{{ $lnk }}">
|
|
||||||
{{ end }}
|
|
||||||
<div class="action{{ if eq $lnk ""}} disabled{{ end }}" id="prev">
|
|
||||||
<i class="material-icons" title="Previous">subdirectory_arrow_left</i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ if ne $lnk ""}}
|
|
||||||
</a>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<a href="{{ if eq .Config.AbsoluteURL "" }}/{{ else }}{{ .Config.AbsoluteURL }}{{ end }}">
|
|
||||||
File Manager
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ if .IsDir}}
|
|
||||||
|
|
||||||
{{ if .User.AllowCommands }}
|
|
||||||
<div id="search">
|
|
||||||
<i class="material-icons" title="Storage">storage</i>
|
|
||||||
<input type="text" placeholder="Execute a command...">
|
|
||||||
<div>Write your git, mercurial or svn command and press enter.</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<div class="action" id="view">
|
|
||||||
<i class="material-icons" title="Switch view">view_headline</i> <span>Switch view</span>
|
|
||||||
</div>
|
|
||||||
{{ if .User.AllowNew }}
|
|
||||||
<div class="action" id="upload">
|
|
||||||
<i class="material-icons" title="Upload">file_upload</i> <span>Upload</span>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ else }}
|
|
||||||
{{ template "actions" . }}
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<div class="action" id="logout">
|
|
||||||
<i class="material-icons" title="Logout">exit_to_app</i> <span>Logout</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="overlay"></div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{{ if .IsDir }}
|
|
||||||
<div id="toolbar">
|
|
||||||
<div>
|
|
||||||
<div class="action" id="back">
|
|
||||||
<i class="material-icons" title="Back">arrow_back</i>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<span id="selected-number">0</span>
|
|
||||||
selected.</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ template "actions" . }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<main>
|
|
||||||
{{ template "content" . }}
|
|
||||||
<span id="token">{{ .Config.Token }}</span>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
Served with
|
|
||||||
<a rel="noopener noreferrer" href="https://caddyserver.com">Caddy</a>
|
|
||||||
and
|
|
||||||
<a rel="noopener noreferrer" href="https://github.com/hacdias/caddy-filemanager">File Manager</a>.
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<!-- SCRIPTS -->
|
|
||||||
<!-- User Data and Permissions -->
|
|
||||||
<script>var user = JSON.parse('{{ Marshal .User }}');</script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/ace.js"></script>
|
|
||||||
<script src="{{ .Config.AbsoluteURL }}/_filemanagerinternal/js/form2js.js"></script>
|
|
||||||
<script src="{{ .Config.AbsoluteURL }}/_filemanagerinternal/js/application.js"></script>
|
|
||||||
{{ if .Config.HugoEnabled }}<script src="{{ .Config.AbsoluteURL }}/_hugointernal/js/application.js"></script>{{ end }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,15 +0,0 @@
|
|||||||
{{ define "content" }}
|
|
||||||
{{ with .Data}}
|
|
||||||
<main class="container">
|
|
||||||
{{ if eq .Type "image" }}
|
|
||||||
<img src="{{ .URL }}?raw=true">
|
|
||||||
{{ else if eq .Type "audio" }}
|
|
||||||
<audio src="{{ .URL }}?raw=true"></audio>
|
|
||||||
{{ else if eq .Type "video" }}
|
|
||||||
|
|
||||||
{{ else}}
|
|
||||||
<pre>{{ .Content }}</pre>
|
|
||||||
{{ end }}
|
|
||||||
</main>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
@ -3,7 +3,6 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -17,16 +16,14 @@ import (
|
|||||||
// Config is a configuration for browsing in a particualr path.
|
// Config is a configuration for browsing in a particualr path.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
*User
|
*User
|
||||||
BaseURL string
|
BaseURL string
|
||||||
AbsoluteURL string
|
AbsoluteURL string
|
||||||
AddrPath string
|
AddrPath string
|
||||||
Token string // Anti CSRF token
|
Token string // Anti CSRF token
|
||||||
HugoEnabled bool // Enables the Hugo plugin for File Manager
|
HugoEnabled bool // Enables the Hugo plugin for File Manager
|
||||||
Users map[string]*User
|
Users map[string]*User
|
||||||
WebDav bool
|
WebDavURL string
|
||||||
WebDavURL string
|
CurrentUser *User
|
||||||
WebDavHandler *webdav.Handler
|
|
||||||
CurrentUser *User
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rule is a dissalow/allow rule
|
// Rule is a dissalow/allow rule
|
||||||
@ -48,8 +45,8 @@ func Parse(c *caddy.Controller) ([]Config, error) {
|
|||||||
|
|
||||||
appendConfig := func(cfg Config) error {
|
appendConfig := func(cfg Config) error {
|
||||||
for _, c := range configs {
|
for _, c := range configs {
|
||||||
if c.PathScope == cfg.PathScope {
|
if c.Scope == cfg.Scope {
|
||||||
return fmt.Errorf("duplicate file managing config for %s", c.PathScope)
|
return fmt.Errorf("duplicate file managing config for %s", c.Scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
configs = append(configs, cfg)
|
configs = append(configs, cfg)
|
||||||
@ -59,8 +56,8 @@ func Parse(c *caddy.Controller) ([]Config, error) {
|
|||||||
for c.Next() {
|
for c.Next() {
|
||||||
// Initialize the configuration with the default settings
|
// Initialize the configuration with the default settings
|
||||||
cfg := Config{User: &User{}}
|
cfg := Config{User: &User{}}
|
||||||
cfg.PathScope = "."
|
cfg.Scope = "."
|
||||||
cfg.Root = http.Dir(cfg.PathScope)
|
cfg.FileSystem = webdav.Dir(cfg.Scope)
|
||||||
cfg.BaseURL = ""
|
cfg.BaseURL = ""
|
||||||
cfg.FrontMatter = "yaml"
|
cfg.FrontMatter = "yaml"
|
||||||
cfg.HugoEnabled = false
|
cfg.HugoEnabled = false
|
||||||
@ -85,6 +82,7 @@ func Parse(c *caddy.Controller) ([]Config, error) {
|
|||||||
cfg.BaseURL = strings.TrimPrefix(cfg.BaseURL, "/")
|
cfg.BaseURL = strings.TrimPrefix(cfg.BaseURL, "/")
|
||||||
cfg.BaseURL = strings.TrimSuffix(cfg.BaseURL, "/")
|
cfg.BaseURL = strings.TrimSuffix(cfg.BaseURL, "/")
|
||||||
cfg.BaseURL = "/" + cfg.BaseURL
|
cfg.BaseURL = "/" + cfg.BaseURL
|
||||||
|
cfg.WebDavURL = cfg.BaseURL + "webdav"
|
||||||
|
|
||||||
if cfg.BaseURL == "/" {
|
if cfg.BaseURL == "/" {
|
||||||
cfg.BaseURL = ""
|
cfg.BaseURL = ""
|
||||||
@ -105,31 +103,23 @@ func Parse(c *caddy.Controller) ([]Config, error) {
|
|||||||
return configs, c.Err("frontmatter type not supported")
|
return configs, c.Err("frontmatter type not supported")
|
||||||
}
|
}
|
||||||
case "webdav":
|
case "webdav":
|
||||||
cfg.WebDav = true
|
if !c.NextArg() {
|
||||||
|
return configs, c.ArgErr()
|
||||||
prefix := "webdav"
|
|
||||||
if c.NextArg() {
|
|
||||||
prefix = c.Val()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefix := c.Val()
|
||||||
prefix = strings.TrimPrefix(prefix, "/")
|
prefix = strings.TrimPrefix(prefix, "/")
|
||||||
prefix = strings.TrimSuffix(prefix, "/")
|
prefix = strings.TrimSuffix(prefix, "/")
|
||||||
prefix = cfg.BaseURL + "/" + prefix
|
prefix = cfg.BaseURL + "/" + prefix
|
||||||
|
|
||||||
cfg.WebDavURL = prefix
|
cfg.WebDavURL = prefix
|
||||||
cfg.WebDavHandler = &webdav.Handler{
|
|
||||||
Prefix: prefix,
|
|
||||||
FileSystem: webdav.Dir(cfg.PathScope),
|
|
||||||
LockSystem: webdav.NewMemLS(),
|
|
||||||
}
|
|
||||||
case "show":
|
case "show":
|
||||||
if !c.NextArg() {
|
if !c.NextArg() {
|
||||||
return configs, c.ArgErr()
|
return configs, c.ArgErr()
|
||||||
}
|
}
|
||||||
|
|
||||||
user.PathScope = c.Val()
|
user.Scope = c.Val()
|
||||||
user.PathScope = strings.TrimSuffix(user.PathScope, "/")
|
user.Scope = strings.TrimSuffix(user.Scope, "/")
|
||||||
user.Root = http.Dir(user.PathScope)
|
user.FileSystem = webdav.Dir(user.Scope)
|
||||||
case "styles":
|
case "styles":
|
||||||
if !c.NextArg() {
|
if !c.NextArg() {
|
||||||
return configs, c.ArgErr()
|
return configs, c.ArgErr()
|
||||||
@ -233,13 +223,19 @@ func Parse(c *caddy.Controller) ([]Config, error) {
|
|||||||
user.AllowNew = cfg.AllowEdit
|
user.AllowNew = cfg.AllowEdit
|
||||||
user.Commands = cfg.Commands
|
user.Commands = cfg.Commands
|
||||||
user.FrontMatter = cfg.FrontMatter
|
user.FrontMatter = cfg.FrontMatter
|
||||||
user.PathScope = cfg.PathScope
|
user.Scope = cfg.Scope
|
||||||
user.Root = cfg.Root
|
user.FileSystem = cfg.FileSystem
|
||||||
user.Rules = cfg.Rules
|
user.Rules = cfg.Rules
|
||||||
user.StyleSheet = cfg.StyleSheet
|
user.StyleSheet = cfg.StyleSheet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.Handler = &webdav.Handler{
|
||||||
|
Prefix: cfg.WebDavURL,
|
||||||
|
FileSystem: cfg.FileSystem,
|
||||||
|
LockSystem: webdav.NewMemLS(),
|
||||||
|
}
|
||||||
|
|
||||||
caddyConf := httpserver.GetConfig(c)
|
caddyConf := httpserver.GetConfig(c)
|
||||||
cfg.AbsoluteURL = strings.TrimSuffix(caddyConf.Addr.Path, "/") + "/" + cfg.BaseURL
|
cfg.AbsoluteURL = strings.TrimSuffix(caddyConf.Addr.Path, "/") + "/" + cfg.BaseURL
|
||||||
cfg.AbsoluteURL = strings.Replace(cfg.AbsoluteURL, "//", "/", -1)
|
cfg.AbsoluteURL = strings.Replace(cfg.AbsoluteURL, "//", "/", -1)
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/webdav"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User contains the configuration for each user
|
// User contains the configuration for each user
|
||||||
type User struct {
|
type User struct {
|
||||||
PathScope string `json:"-"` // Path the user have access
|
Scope string `json:"-"` // Path the user have access
|
||||||
Root http.FileSystem `json:"-"` // The virtual file system the user have access
|
FileSystem webdav.FileSystem `json:"-"` // The virtual file system the user have access
|
||||||
StyleSheet string `json:"-"` // Costum stylesheet
|
Handler *webdav.Handler `json:"-"` // The WebDav HTTP Handler
|
||||||
FrontMatter string `json:"-"` // Default frontmatter to save files in
|
StyleSheet string `json:"-"` // Costum stylesheet
|
||||||
AllowNew bool // Can create files and folders
|
FrontMatter string `json:"-"` // Default frontmatter to save files in
|
||||||
AllowEdit bool // Can edit/rename files
|
AllowNew bool // Can create files and folders
|
||||||
AllowCommands bool // Can execute commands
|
AllowEdit bool // Can edit/rename files
|
||||||
Commands []string // Available Commands
|
AllowCommands bool // Can execute commands
|
||||||
Rules []*Rule `json:"-"` // Access rules
|
Commands []string // Available Commands
|
||||||
|
Rules []*Rule `json:"-"` // Access rules
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allowed checks if the user has permission to access a directory/file
|
// Allowed checks if the user has permission to access a directory/file
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
package directory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hacdias/caddy-filemanager/frontmatter"
|
|
||||||
"github.com/spf13/hugo/parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Editor contains the information for the editor page
|
|
||||||
type Editor struct {
|
|
||||||
Class string
|
|
||||||
Mode string
|
|
||||||
Content string
|
|
||||||
FrontMatter *frontmatter.Content
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEditor gets the editor based on a FileInfo struct
|
|
||||||
func (i *Info) GetEditor() (*Editor, error) {
|
|
||||||
// Create a new editor variable and set the mode
|
|
||||||
editor := new(Editor)
|
|
||||||
editor.Mode = strings.TrimPrefix(filepath.Ext(i.Name), ".")
|
|
||||||
|
|
||||||
switch editor.Mode {
|
|
||||||
case "md", "markdown", "mdown", "mmark":
|
|
||||||
editor.Mode = "markdown"
|
|
||||||
case "asciidoc", "adoc", "ad":
|
|
||||||
editor.Mode = "asciidoc"
|
|
||||||
case "rst":
|
|
||||||
editor.Mode = "rst"
|
|
||||||
case "html", "htm":
|
|
||||||
editor.Mode = "html"
|
|
||||||
case "js":
|
|
||||||
editor.Mode = "javascript"
|
|
||||||
}
|
|
||||||
|
|
||||||
var page parser.Page
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Handle the content depending on the file extension
|
|
||||||
switch editor.Mode {
|
|
||||||
case "markdown", "asciidoc", "rst":
|
|
||||||
if HasFrontMatterRune(i.Raw) {
|
|
||||||
// Starts a new buffer and parses the file using Hugo's functions
|
|
||||||
buffer := bytes.NewBuffer(i.Raw)
|
|
||||||
page, err = parser.ReadFrom(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return editor, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parses the page content and the frontmatter
|
|
||||||
editor.Content = strings.TrimSpace(string(page.Content()))
|
|
||||||
editor.FrontMatter, _, err = frontmatter.Pretty(page.FrontMatter())
|
|
||||||
editor.Class = "complete"
|
|
||||||
} else {
|
|
||||||
// The editor will handle only content
|
|
||||||
editor.Class = "content-only"
|
|
||||||
editor.Content = i.Content
|
|
||||||
}
|
|
||||||
case "json", "toml", "yaml":
|
|
||||||
// Defines the class and declares an error
|
|
||||||
editor.Class = "frontmatter-only"
|
|
||||||
|
|
||||||
// Checks if the file already has the frontmatter rune and parses it
|
|
||||||
if HasFrontMatterRune(i.Raw) {
|
|
||||||
editor.FrontMatter, _, err = frontmatter.Pretty(i.Raw)
|
|
||||||
} else {
|
|
||||||
editor.FrontMatter, _, err = frontmatter.Pretty(AppendFrontMatterRune(i.Raw, editor.Mode))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there were any errors
|
|
||||||
if err != nil {
|
|
||||||
return editor, err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// The editor will handle only content
|
|
||||||
editor.Class = "content-only"
|
|
||||||
editor.Content = i.Content
|
|
||||||
}
|
|
||||||
return editor, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasFrontMatterRune checks if the file has the frontmatter rune
|
|
||||||
func HasFrontMatterRune(file []byte) bool {
|
|
||||||
return strings.HasPrefix(string(file), "---") ||
|
|
||||||
strings.HasPrefix(string(file), "+++") ||
|
|
||||||
strings.HasPrefix(string(file), "{")
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendFrontMatterRune appends the frontmatter rune to a file
|
|
||||||
func AppendFrontMatterRune(frontmatter []byte, language string) []byte {
|
|
||||||
switch language {
|
|
||||||
case "yaml":
|
|
||||||
return []byte("---\n" + string(frontmatter) + "\n---")
|
|
||||||
case "toml":
|
|
||||||
return []byte("+++\n" + string(frontmatter) + "\n+++")
|
|
||||||
case "json":
|
|
||||||
return frontmatter
|
|
||||||
}
|
|
||||||
|
|
||||||
return frontmatter
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanBeEdited checks if the extension of a file is supported by the editor
|
|
||||||
func CanBeEdited(filename string) bool {
|
|
||||||
extensions := [...]string{
|
|
||||||
"md", "markdown", "mdown", "mmark",
|
|
||||||
"asciidoc", "adoc", "ad",
|
|
||||||
"rst",
|
|
||||||
".json", ".toml", ".yaml",
|
|
||||||
".css", ".sass", ".scss",
|
|
||||||
".js",
|
|
||||||
".html",
|
|
||||||
".txt",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, extension := range extensions {
|
|
||||||
if strings.HasSuffix(filename, extension) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
@ -1,314 +0,0 @@
|
|||||||
package directory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/hacdias/caddy-filemanager/config"
|
|
||||||
p "github.com/hacdias/caddy-filemanager/page"
|
|
||||||
"github.com/hacdias/caddy-filemanager/utils/errors"
|
|
||||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Info is the information about a particular file or directory
|
|
||||||
type Info struct {
|
|
||||||
IsDir bool
|
|
||||||
Name string
|
|
||||||
Size int64
|
|
||||||
URL string
|
|
||||||
Path string // The relative Path of the file/directory relative to Caddyfile.
|
|
||||||
RootPath string // The Path of the file/directory on http.FileSystem.
|
|
||||||
ModTime time.Time
|
|
||||||
Mode os.FileMode
|
|
||||||
Mimetype string
|
|
||||||
Content string
|
|
||||||
Raw []byte
|
|
||||||
Type string
|
|
||||||
UserAllowed bool // Indicates if the user has permissions to open this directory
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInfo gets the file information and, in case of error, returns the
|
|
||||||
// respective HTTP error code
|
|
||||||
func GetInfo(url *url.URL, c *config.Config, u *config.User) (*Info, int, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
rootPath := strings.Replace(url.Path, c.BaseURL, "", 1)
|
|
||||||
rootPath = strings.TrimPrefix(rootPath, "/")
|
|
||||||
rootPath = "/" + rootPath
|
|
||||||
|
|
||||||
relpath := u.PathScope + rootPath
|
|
||||||
relpath = strings.Replace(relpath, "\\", "/", -1)
|
|
||||||
relpath = filepath.Clean(relpath)
|
|
||||||
|
|
||||||
file := &Info{
|
|
||||||
URL: url.Path,
|
|
||||||
RootPath: rootPath,
|
|
||||||
Path: relpath,
|
|
||||||
}
|
|
||||||
f, err := u.Root.Open(rootPath)
|
|
||||||
if err != nil {
|
|
||||||
return file, errors.ToHTTPCode(err), err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
info, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return file, errors.ToHTTPCode(err), err
|
|
||||||
}
|
|
||||||
|
|
||||||
file.IsDir = info.IsDir()
|
|
||||||
file.ModTime = info.ModTime()
|
|
||||||
file.Name = info.Name()
|
|
||||||
file.Size = info.Size()
|
|
||||||
return file, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExtendedInfo is used to get extra parameters for FileInfo struct
|
|
||||||
func (i *Info) GetExtendedInfo() error {
|
|
||||||
err := i.Read()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
i.Type = SimplifyMimeType(i.Mimetype)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read is used to read a file and store its content
|
|
||||||
func (i *Info) Read() error {
|
|
||||||
raw, err := ioutil.ReadFile(i.Path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
i.Mimetype = http.DetectContentType(raw)
|
|
||||||
i.Content = string(raw)
|
|
||||||
i.Raw = raw
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HumanSize returns the size of the file as a human-readable string
|
|
||||||
// in IEC format (i.e. power of 2 or base 1024).
|
|
||||||
func (i Info) HumanSize() string {
|
|
||||||
return humanize.IBytes(uint64(i.Size))
|
|
||||||
}
|
|
||||||
|
|
||||||
// HumanModTime returns the modified time of the file as a human-readable string.
|
|
||||||
func (i Info) HumanModTime(format string) string {
|
|
||||||
return i.ModTime.Format(format)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeAsHTML is used to serve single file pages
|
|
||||||
func (i *Info) ServeAsHTML(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
|
|
||||||
if i.IsDir {
|
|
||||||
return i.serveListing(w, r, c, u)
|
|
||||||
}
|
|
||||||
|
|
||||||
return i.serveSingleFile(w, r, c, u)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Info) serveSingleFile(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
|
|
||||||
err := i.GetExtendedInfo()
|
|
||||||
if err != nil {
|
|
||||||
return errors.ToHTTPCode(err), err
|
|
||||||
}
|
|
||||||
|
|
||||||
if i.Type == "blob" {
|
|
||||||
return i.ServeRawFile(w, r, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
page := &p.Page{
|
|
||||||
Info: &p.Info{
|
|
||||||
Name: i.Name,
|
|
||||||
Path: i.RootPath,
|
|
||||||
IsDir: false,
|
|
||||||
Data: i,
|
|
||||||
User: u,
|
|
||||||
Config: c,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if CanBeEdited(i.Name) && u.AllowEdit {
|
|
||||||
editor, err := i.GetEditor()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
page.Info.Data = editor
|
|
||||||
return page.PrintAsHTML(w, "frontmatter", "editor")
|
|
||||||
}
|
|
||||||
|
|
||||||
return page.PrintAsHTML(w, "single")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Info) serveListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
file, err := u.Root.Open(i.RootPath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.ToHTTPCode(err), err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
listing, err := i.loadDirectoryContents(file, r.URL.Path, u)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
switch {
|
|
||||||
case os.IsPermission(err):
|
|
||||||
return http.StatusForbidden, err
|
|
||||||
case os.IsExist(err):
|
|
||||||
return http.StatusGone, err
|
|
||||||
default:
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listing.Context = httpserver.Context{
|
|
||||||
Root: c.Root,
|
|
||||||
Req: r,
|
|
||||||
URL: r.URL,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the query values into the Listing struct
|
|
||||||
var limit int
|
|
||||||
listing.Sort, listing.Order, limit, err = handleSortOrder(w, r, c.PathScope)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
page := &p.Page{
|
|
||||||
Info: &p.Info{
|
|
||||||
Name: listing.Name,
|
|
||||||
Path: i.RootPath,
|
|
||||||
IsDir: true,
|
|
||||||
User: u,
|
|
||||||
Config: c,
|
|
||||||
Data: listing,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Header.Get("Minimal") == "true" {
|
|
||||||
page.Minimal = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return page.PrintAsHTML(w, "listing")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i Info) loadDirectoryContents(file http.File, path string, u *config.User) (*Listing, error) {
|
|
||||||
files, err := file.Readdir(-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
listing := directoryListing(files, i.RootPath, path, u)
|
|
||||||
return &listing, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func directoryListing(files []os.FileInfo, urlPath string, basePath string, u *config.User) Listing {
|
|
||||||
var (
|
|
||||||
fileinfos []Info
|
|
||||||
dirCount, fileCount int
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, f := range files {
|
|
||||||
name := f.Name()
|
|
||||||
|
|
||||||
if f.IsDir() {
|
|
||||||
name += "/"
|
|
||||||
dirCount++
|
|
||||||
} else {
|
|
||||||
fileCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Absolute URL
|
|
||||||
url := url.URL{Path: basePath + name}
|
|
||||||
fileinfos = append(fileinfos, Info{
|
|
||||||
IsDir: f.IsDir(),
|
|
||||||
Name: f.Name(),
|
|
||||||
Size: f.Size(),
|
|
||||||
URL: url.String(),
|
|
||||||
ModTime: f.ModTime().UTC(),
|
|
||||||
Mode: f.Mode(),
|
|
||||||
UserAllowed: u.Allowed(url.String()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Listing{
|
|
||||||
Name: path.Base(urlPath),
|
|
||||||
Path: urlPath,
|
|
||||||
Items: fileinfos,
|
|
||||||
NumDirs: dirCount,
|
|
||||||
NumFiles: fileCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeRawFile serves raw files
|
|
||||||
func (i *Info) ServeRawFile(w http.ResponseWriter, r *http.Request, c *config.Config) (int, error) {
|
|
||||||
err := i.GetExtendedInfo()
|
|
||||||
if err != nil {
|
|
||||||
return errors.ToHTTPCode(err), err
|
|
||||||
}
|
|
||||||
|
|
||||||
if i.Type != "text" {
|
|
||||||
i.Read()
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", i.Mimetype)
|
|
||||||
w.Write([]byte(i.Content))
|
|
||||||
return 200, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimplifyMimeType returns the base type of a file
|
|
||||||
func SimplifyMimeType(name string) string {
|
|
||||||
if strings.HasPrefix(name, "video") {
|
|
||||||
return "video"
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(name, "audio") {
|
|
||||||
return "audio"
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(name, "image") {
|
|
||||||
return "image"
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(name, "text") {
|
|
||||||
return "text"
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(name, "application/javascript") {
|
|
||||||
return "text"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "blob"
|
|
||||||
}
|
|
@ -1,141 +0,0 @@
|
|||||||
package directory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
Path string
|
|
||||||
// The items (files and folders) in the path
|
|
||||||
Items []Info
|
|
||||||
// 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
|
|
||||||
httpserver.Context `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), 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 err != nil { // if the 'limit' query can't be interpreted as a number, return err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) }
|
|
||||||
|
|
||||||
// Add sorting method to "Listing"
|
|
||||||
// it will apply what's in ".Sort" and ".Order"
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,139 +0,0 @@
|
|||||||
package directory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hacdias/caddy-filemanager/config"
|
|
||||||
"github.com/spf13/hugo/parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update is used to update a file that was edited
|
|
||||||
func (i *Info) Update(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
|
|
||||||
var data map[string]interface{}
|
|
||||||
kind := r.Header.Get("kind")
|
|
||||||
|
|
||||||
if kind == "" {
|
|
||||||
return http.StatusBadRequest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the JSON information
|
|
||||||
rawBuffer := new(bytes.Buffer)
|
|
||||||
rawBuffer.ReadFrom(r.Body)
|
|
||||||
err := json.Unmarshal(rawBuffer.Bytes(), &data)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var file []byte
|
|
||||||
var code int
|
|
||||||
|
|
||||||
switch kind {
|
|
||||||
case "frontmatter-only":
|
|
||||||
if file, code, err = ParseFrontMatterOnlyFile(data, i.Name); err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
case "content-only":
|
|
||||||
mainContent := data["content"].(string)
|
|
||||||
mainContent = strings.TrimSpace(mainContent)
|
|
||||||
file = []byte(mainContent)
|
|
||||||
case "complete":
|
|
||||||
if file, code, err = ParseCompleteFile(data, i.Name, u.FrontMatter); err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return http.StatusBadRequest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the file
|
|
||||||
err = ioutil.WriteFile(i.Path, file, 0666)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return code, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseFrontMatterOnlyFile parses a frontmatter only file
|
|
||||||
func ParseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, int, error) {
|
|
||||||
frontmatter := strings.TrimPrefix(filepath.Ext(filename), ".")
|
|
||||||
f, code, err := ParseFrontMatter(data, frontmatter)
|
|
||||||
fString := string(f)
|
|
||||||
|
|
||||||
// If it's toml or yaml, strip frontmatter identifier
|
|
||||||
if frontmatter == "toml" {
|
|
||||||
fString = strings.TrimSuffix(fString, "+++\n")
|
|
||||||
fString = strings.TrimPrefix(fString, "+++\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if frontmatter == "yaml" {
|
|
||||||
fString = strings.TrimSuffix(fString, "---\n")
|
|
||||||
fString = strings.TrimPrefix(fString, "---\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
f = []byte(fString)
|
|
||||||
return f, code, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseFrontMatter is the frontmatter parser
|
|
||||||
func ParseFrontMatter(data interface{}, frontmatter string) ([]byte, int, error) {
|
|
||||||
var mark rune
|
|
||||||
|
|
||||||
switch frontmatter {
|
|
||||||
case "toml":
|
|
||||||
mark = rune('+')
|
|
||||||
case "json":
|
|
||||||
mark = rune('{')
|
|
||||||
case "yaml":
|
|
||||||
mark = rune('-')
|
|
||||||
default:
|
|
||||||
return []byte{}, http.StatusBadRequest, errors.New("Can't define the frontmatter.")
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := parser.InterfaceToFrontMatter(data, mark)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return f, http.StatusOK, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseCompleteFile parses a complete file
|
|
||||||
func ParseCompleteFile(data map[string]interface{}, filename string, frontmatter string) ([]byte, int, error) {
|
|
||||||
mainContent := ""
|
|
||||||
|
|
||||||
if _, ok := data["content"]; ok {
|
|
||||||
// The main content of the file
|
|
||||||
mainContent = data["content"].(string)
|
|
||||||
mainContent = "\n\n" + strings.TrimSpace(mainContent) + "\n"
|
|
||||||
|
|
||||||
// Removes the main content from the rest of the frontmatter
|
|
||||||
delete(data, "content")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := data["date"]; ok {
|
|
||||||
data["date"] = data["date"].(string) + ":00"
|
|
||||||
}
|
|
||||||
|
|
||||||
front, code, err := ParseFrontMatter(data, frontmatter)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(frontmatter)
|
|
||||||
return []byte{}, code, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generates the final file
|
|
||||||
f := new(bytes.Buffer)
|
|
||||||
f.Write(front)
|
|
||||||
f.Write([]byte(mainContent))
|
|
||||||
return f.Bytes(), http.StatusOK, nil
|
|
||||||
}
|
|
147
file/info.go
Normal file
147
file/info.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
humanize "github.com/dustin/go-humanize"
|
||||||
|
"github.com/hacdias/caddy-filemanager/config"
|
||||||
|
"github.com/hacdias/caddy-filemanager/utils/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info contains the information about a particular file or directory
|
||||||
|
type Info struct {
|
||||||
|
os.FileInfo
|
||||||
|
URL string
|
||||||
|
Path string // Relative path to Caddyfile
|
||||||
|
VirtualPath string // Relative path to u.FileSystem
|
||||||
|
Mimetype string
|
||||||
|
Content []byte
|
||||||
|
Type string
|
||||||
|
UserAllowed bool // Indicates if the user has enough permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInfo gets the file information and, in case of error, returns the
|
||||||
|
// respective HTTP error code
|
||||||
|
func GetInfo(url *url.URL, c *config.Config, u *config.User) (*Info, int, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
i := &Info{URL: url.Path}
|
||||||
|
i.VirtualPath = strings.Replace(url.Path, c.BaseURL, "", 1)
|
||||||
|
i.VirtualPath = strings.TrimPrefix(i.VirtualPath, "/")
|
||||||
|
i.VirtualPath = "/" + i.VirtualPath
|
||||||
|
|
||||||
|
i.Path = u.Scope + i.VirtualPath
|
||||||
|
i.Path = strings.Replace(i.Path, "\\", "/", -1)
|
||||||
|
i.Path = filepath.Clean(i.Path)
|
||||||
|
|
||||||
|
i.FileInfo, err = os.Stat(i.Path)
|
||||||
|
if err != nil {
|
||||||
|
return i, errors.ErrorToHTTPCode(err, true), err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetrieveFileType obtains the mimetype and a simplified internal Type
|
||||||
|
// using the first 512 bytes from the file.
|
||||||
|
func (i *Info) RetrieveFileType() error {
|
||||||
|
i.Mimetype = mime.TypeByExtension(filepath.Ext(i.Name()))
|
||||||
|
|
||||||
|
if i.Mimetype == "" {
|
||||||
|
err := i.Read()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.Mimetype = http.DetectContentType(i.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.Type = simplifyMediaType(i.Mimetype)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads the file.
|
||||||
|
func (i *Info) Read() error {
|
||||||
|
if len(i.Content) != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
i.Content, err = ioutil.ReadFile(i.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringifyContent returns the string version of Raw
|
||||||
|
func (i Info) StringifyContent() string {
|
||||||
|
return string(i.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HumanSize returns the size of the file as a human-readable string
|
||||||
|
// in IEC format (i.e. power of 2 or base 1024).
|
||||||
|
func (i Info) HumanSize() string {
|
||||||
|
return humanize.IBytes(uint64(i.Size()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HumanModTime returns the modified time of the file as a human-readable string.
|
||||||
|
func (i Info) HumanModTime(format string) string {
|
||||||
|
return i.ModTime().Format(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanBeEdited checks if the extension of a file is supported by the editor
|
||||||
|
func (i Info) CanBeEdited() bool {
|
||||||
|
if i.Type == "text" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions := [...]string{
|
||||||
|
"md", "markdown", "mdown", "mmark",
|
||||||
|
"asciidoc", "adoc", "ad",
|
||||||
|
"rst",
|
||||||
|
".json", ".toml", ".yaml",
|
||||||
|
".css", ".sass", ".scss",
|
||||||
|
".js",
|
||||||
|
".html",
|
||||||
|
".txt",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, extension := range extensions {
|
||||||
|
if strings.HasSuffix(i.Name(), extension) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func simplifyMediaType(name string) string {
|
||||||
|
if strings.HasPrefix(name, "video") {
|
||||||
|
return "video"
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(name, "audio") {
|
||||||
|
return "audio"
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(name, "image") {
|
||||||
|
return "image"
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(name, "text") {
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(name, "application/javascript") {
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "blob"
|
||||||
|
}
|
172
file/listing.go
Normal file
172
file/listing.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hacdias/caddy-filemanager/config"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 []Info
|
||||||
|
// 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
|
||||||
|
httpserver.Context `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetListing gets the information about a specific directory and its files.
|
||||||
|
func GetListing(u *config.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(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 []Info
|
||||||
|
dirCount, fileCount int
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
name := f.Name()
|
||||||
|
|
||||||
|
if f.IsDir() {
|
||||||
|
name += "/"
|
||||||
|
dirCount++
|
||||||
|
} else {
|
||||||
|
fileCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute URL
|
||||||
|
url := url.URL{Path: baseURL + name}
|
||||||
|
fileinfos = append(fileinfos, Info{
|
||||||
|
FileInfo: f,
|
||||||
|
URL: url.String(),
|
||||||
|
UserAllowed: u.Allowed(filePath),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
390
filemanager.go
390
filemanager.go
@ -1,6 +1,6 @@
|
|||||||
//go:generate go get github.com/jteeuwen/go-bindata
|
//go:generate go get github.com/jteeuwen/go-bindata
|
||||||
//go:generate go install github.com/jteeuwen/go-bindata/go-bindata
|
//go:generate go install github.com/jteeuwen/go-bindata/go-bindata
|
||||||
//go:generate go-bindata -pkg assets -ignore .jsbeautifyrc -prefix "assets/embed" -o assets/binary.go assets/embed/...
|
//go:generate go-bindata -pkg assets -ignore .jsbeautifyrc -prefix "_embed" -o assets/binary.go _embed/...
|
||||||
|
|
||||||
// Package filemanager provides middleware for managing files in a directory
|
// Package filemanager provides middleware for managing files in a directory
|
||||||
// when directory path is requested instead of a specific file. Based on browse
|
// when directory path is requested instead of a specific file. Based on browse
|
||||||
@ -9,20 +9,13 @@ package filemanager
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
e "errors"
|
e "errors"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hacdias/caddy-filemanager/assets"
|
"github.com/hacdias/caddy-filemanager/assets"
|
||||||
"github.com/hacdias/caddy-filemanager/config"
|
"github.com/hacdias/caddy-filemanager/config"
|
||||||
"github.com/hacdias/caddy-filemanager/directory"
|
"github.com/hacdias/caddy-filemanager/file"
|
||||||
"github.com/hacdias/caddy-filemanager/errors"
|
"github.com/hacdias/caddy-filemanager/handlers"
|
||||||
"github.com/hacdias/caddy-filemanager/page"
|
"github.com/hacdias/caddy-filemanager/page"
|
||||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
)
|
)
|
||||||
@ -37,269 +30,174 @@ type FileManager struct {
|
|||||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||||
func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
var (
|
var (
|
||||||
c *config.Config
|
c *config.Config
|
||||||
fi *directory.Info
|
fi *file.Info
|
||||||
code int
|
code int
|
||||||
err error
|
err error
|
||||||
serveAssets bool
|
user *config.User
|
||||||
user *config.User
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for i := range f.Configs {
|
for i := range f.Configs {
|
||||||
if httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
|
// Checks if this Path should be handled by File Manager.
|
||||||
c = &f.Configs[i]
|
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
|
||||||
serveAssets = httpserver.Path(r.URL.Path).Matches(c.BaseURL + assets.BaseURL)
|
return f.Next.ServeHTTP(w, r)
|
||||||
username, _, _ := r.BasicAuth()
|
}
|
||||||
|
|
||||||
if _, ok := c.Users[username]; ok {
|
w.Header().Set("x-frame-options", "SAMEORIGIN")
|
||||||
user = c.Users[username]
|
w.Header().Set("x-content-type", "nosniff")
|
||||||
} else {
|
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||||
user = c.User
|
|
||||||
|
c = &f.Configs[i]
|
||||||
|
|
||||||
|
// Checks if the URL matches the Assets URL. Returns the asset if the
|
||||||
|
// method is GET and Status Forbidden otherwise.
|
||||||
|
if httpserver.Path(r.URL.Path).Matches(c.BaseURL + assets.BaseURL) {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
return assets.Serve(w, r, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.WebDav && strings.HasPrefix(r.URL.Path, c.WebDavURL) {
|
return http.StatusForbidden, nil
|
||||||
//url := strings.TrimPrefix(r.URL.Path, c.WebDavURL)
|
}
|
||||||
|
|
||||||
/*
|
// Obtains the user
|
||||||
if !user.Allowed(url) {
|
username, _, _ := r.BasicAuth()
|
||||||
return http.StatusForbidden, nil
|
if _, ok := c.Users[username]; ok {
|
||||||
}
|
user = c.Users[username]
|
||||||
|
} else {
|
||||||
|
user = c.User
|
||||||
|
}
|
||||||
|
|
||||||
switch r.Method {
|
// Checks if the request URL is for the WebDav server
|
||||||
case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
|
if strings.HasPrefix(r.URL.Path, c.WebDavURL) {
|
||||||
if !user.AllowEdit {
|
// if !c.CheckToken(r) {
|
||||||
return http.StatusForbidden, nil
|
// return http.StatusForbidden, nil
|
||||||
}
|
// }
|
||||||
case "MKCOL", "COPY":
|
|
||||||
if !user.AllowNew {
|
|
||||||
return http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
|
|
||||||
c.WebDavHandler.ServeHTTP(w, r)
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the user has permission to access the current directory.
|
|
||||||
if !user.Allowed(r.URL.Path) {
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
return errors.PrintHTML(w, http.StatusForbidden, e.New("You don't have permission to access this page."))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Checks for user permissions relatively to this PATH
|
||||||
|
if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.WebDavURL)) {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this request is neither to server assets, nor to upload/create
|
|
||||||
// a new file or directory.
|
|
||||||
if r.Method != http.MethodPost && !serveAssets {
|
|
||||||
// Gets the information of the directory/file
|
|
||||||
fi, code, err = directory.GetInfo(r.URL, c, user)
|
|
||||||
if err != nil {
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
return errors.PrintHTML(w, code, err)
|
|
||||||
}
|
|
||||||
return code, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a dir and the path doesn't end with a trailing slash,
|
|
||||||
// redirect the user.
|
|
||||||
if fi.IsDir && !strings.HasSuffix(r.URL.Path, "/") {
|
|
||||||
http.Redirect(w, r, c.AddrPath+r.URL.Path+"/", http.StatusTemporaryRedirect)
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Security measures against CSRF attacks.
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
if !c.CheckToken(r) {
|
|
||||||
return http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route the request depending on the HTTP Method.
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
|
||||||
// Read and show directory or file.
|
|
||||||
if serveAssets {
|
|
||||||
return assets.Serve(w, r, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate anti security token.
|
|
||||||
c.GenerateToken()
|
|
||||||
|
|
||||||
if !fi.IsDir {
|
|
||||||
query := r.URL.Query()
|
|
||||||
if val, ok := query["raw"]; ok && val[0] == "true" {
|
|
||||||
return fi.ServeRawFile(w, r, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if val, ok := query["download"]; ok && val[0] == "true" {
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name)
|
|
||||||
return fi.ServeRawFile(w, r, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
code, err := fi.ServeAsHTML(w, r, c, user)
|
|
||||||
if err != nil {
|
|
||||||
return errors.PrintHTML(w, code, err)
|
|
||||||
}
|
|
||||||
return code, err
|
|
||||||
case http.MethodPut:
|
|
||||||
if fi.IsDir {
|
|
||||||
return http.StatusNotAcceptable, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !user.AllowEdit {
|
if !user.AllowEdit {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
case "MKCOL", "COPY":
|
||||||
// Update a file.
|
if !user.AllowNew {
|
||||||
return fi.Update(w, r, c, user)
|
return http.StatusForbidden, nil
|
||||||
case http.MethodPost:
|
|
||||||
// Upload a new file.
|
|
||||||
if r.Header.Get("Upload") == "true" {
|
|
||||||
if !user.AllowNew {
|
|
||||||
return http.StatusUnauthorized, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return upload(w, r, c)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Search and git commands.
|
// Preprocess the PUT request if it's the case
|
||||||
if r.Header.Get("Search") == "true" {
|
if r.Method == http.MethodPut {
|
||||||
// TODO: search commands.
|
if handlers.PreProccessPUT(w, r, c, user, fi) != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// VCS commands.
|
c.Handler.ServeHTTP(w, r)
|
||||||
if r.Header.Get("Command") != "" {
|
return 0, nil
|
||||||
if !user.AllowCommands {
|
}
|
||||||
return http.StatusUnauthorized, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return command(w, r, c, user)
|
// Checks if the User is allowed to access this file
|
||||||
|
if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.BaseURL)) {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
return page.PrintErrorHTML(
|
||||||
|
w, http.StatusForbidden,
|
||||||
|
e.New("You don't have permission to access this page."),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
// Generate anti security token.
|
||||||
|
/* c.GenerateToken()
|
||||||
|
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "token",
|
||||||
|
Value: c.Token,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
co, err := r.Cookie("token")
|
||||||
|
fmt.Println(co.Value) */
|
||||||
|
|
||||||
|
/* Name string
|
||||||
|
Value string
|
||||||
|
|
||||||
|
Path string // optional
|
||||||
|
Domain string // optional
|
||||||
|
Expires time.Time // optional
|
||||||
|
RawExpires string // for reading cookies only
|
||||||
|
|
||||||
|
// MaxAge=0 means no 'Max-Age' attribute specified.
|
||||||
|
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
|
||||||
|
// MaxAge>0 means Max-Age attribute present and given in seconds
|
||||||
|
MaxAge int
|
||||||
|
Secure bool
|
||||||
|
HttpOnly bool
|
||||||
|
Raw string
|
||||||
|
Unparsed []string // Raw text of unparsed attribute-value pairs*/
|
||||||
|
|
||||||
|
// Gets the information of the directory/file
|
||||||
|
fi, code, err = file.GetInfo(r.URL, c, user)
|
||||||
|
if err != nil {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
return page.PrintErrorHTML(w, code, err)
|
||||||
}
|
}
|
||||||
|
return code, err
|
||||||
|
}
|
||||||
|
|
||||||
// Creates a new folder.
|
// If it's a dir and the path doesn't end with a trailing slash,
|
||||||
return newDirectory(w, r, c)
|
// redirect the user.
|
||||||
|
if fi.IsDir() && !strings.HasSuffix(r.URL.Path, "/") {
|
||||||
|
http.Redirect(w, r, c.AddrPath+r.URL.Path+"/", http.StatusTemporaryRedirect)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case r.URL.Query().Get("download") != "":
|
||||||
|
code, err = handlers.Download(w, r, c, fi)
|
||||||
|
case r.URL.Query().Get("raw") == "true" && !fi.IsDir():
|
||||||
|
http.ServeFile(w, r, fi.Path)
|
||||||
|
code, err = 0, nil
|
||||||
|
case fi.IsDir():
|
||||||
|
code, err = handlers.ServeListing(w, r, c, user, fi)
|
||||||
default:
|
default:
|
||||||
return http.StatusNotImplemented, nil
|
code, err = handlers.ServeSingle(w, r, c, user, fi)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
code, err = page.PrintErrorHTML(w, code, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
// TODO: This anti CSCF measure is not being applied to requests
|
||||||
|
// to the WebDav URL namespace. Anyone has ideas?
|
||||||
|
// if !c.CheckToken(r) {
|
||||||
|
// return http.StatusForbidden, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// VCS commands.
|
||||||
|
if r.Header.Get("Command") != "" {
|
||||||
|
if !user.AllowCommands {
|
||||||
|
return http.StatusUnauthorized, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlers.Command(w, r, c, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return http.StatusNotImplemented, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.Next.ServeHTTP(w, r)
|
return f.Next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload is used to handle the upload requests to the server
|
|
||||||
func upload(w http.ResponseWriter, r *http.Request, c *config.Config) (int, error) {
|
|
||||||
// Parse the multipart form in the request
|
|
||||||
err := r.ParseMultipartForm(100000)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// For each file header in the multipart form
|
|
||||||
for _, headers := range r.MultipartForm.File {
|
|
||||||
// Handle each file
|
|
||||||
for _, header := range headers {
|
|
||||||
// Open the first file
|
|
||||||
var src multipart.File
|
|
||||||
if src, err = header.Open(); nil != err {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
filename := strings.Replace(r.URL.Path, c.BaseURL, c.PathScope, 1)
|
|
||||||
filename = filename + header.Filename
|
|
||||||
filename = filepath.Clean(filename)
|
|
||||||
|
|
||||||
// Create the file
|
|
||||||
var dst *os.File
|
|
||||||
if dst, err = os.Create(filename); nil != err {
|
|
||||||
if os.IsExist(err) {
|
|
||||||
return http.StatusConflict, err
|
|
||||||
}
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the file content
|
|
||||||
if _, err = io.Copy(dst, src); nil != err {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer dst.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.StatusOK, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newDirectory makes a new directory
|
|
||||||
func newDirectory(w http.ResponseWriter, r *http.Request, c *config.Config) (int, error) {
|
|
||||||
filename := r.Header.Get("Filename")
|
|
||||||
|
|
||||||
if filename == "" {
|
|
||||||
return http.StatusBadRequest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
path := strings.Replace(r.URL.Path, c.BaseURL, c.PathScope, 1) + filename
|
|
||||||
path = filepath.Clean(path)
|
|
||||||
extension := filepath.Ext(path)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if extension == "" {
|
|
||||||
err = os.MkdirAll(path, 0775)
|
|
||||||
} else {
|
|
||||||
err = ioutil.WriteFile(path, []byte(""), 0775)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
switch {
|
|
||||||
case os.IsPermission(err):
|
|
||||||
return http.StatusForbidden, err
|
|
||||||
case os.IsExist(err):
|
|
||||||
return http.StatusConflict, err
|
|
||||||
default:
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return http.StatusCreated, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// command handles the requests for VCS related commands: git, svn and mercurial
|
|
||||||
func command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
|
|
||||||
command := strings.Split(r.Header.Get("command"), " ")
|
|
||||||
|
|
||||||
// Check if the command is allowed
|
|
||||||
mayContinue := false
|
|
||||||
|
|
||||||
for _, cmd := range u.Commands {
|
|
||||||
if cmd == command[0] {
|
|
||||||
mayContinue = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !mayContinue {
|
|
||||||
return http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the program is talled is installed on the computer
|
|
||||||
if _, err := exec.LookPath(command[0]); err != nil {
|
|
||||||
return http.StatusNotImplemented, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
path := strings.Replace(r.URL.Path, c.BaseURL, c.PathScope, 1)
|
|
||||||
path = filepath.Clean(path)
|
|
||||||
|
|
||||||
cmd := exec.Command(command[0], command[1:len(command)]...)
|
|
||||||
cmd.Dir = path
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
page := &page.Page{Info: &page.Info{Data: string(output)}}
|
|
||||||
return page.PrintAsJSON(w)
|
|
||||||
}
|
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
"github.com/hacdias/caddy-filemanager/utils/variables"
|
"github.com/hacdias/caddy-filemanager/utils/variables"
|
||||||
|
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
24
frontmatter/runes.go
Normal file
24
frontmatter/runes.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package frontmatter
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// HasRune checks if the file has the frontmatter rune
|
||||||
|
func HasRune(file []byte) bool {
|
||||||
|
return strings.HasPrefix(string(file), "---") ||
|
||||||
|
strings.HasPrefix(string(file), "+++") ||
|
||||||
|
strings.HasPrefix(string(file), "{")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendRune appends the frontmatter rune to a file
|
||||||
|
func AppendRune(frontmatter []byte, language string) []byte {
|
||||||
|
switch language {
|
||||||
|
case "yaml":
|
||||||
|
return []byte("---\n" + string(frontmatter) + "\n---")
|
||||||
|
case "toml":
|
||||||
|
return []byte("+++\n" + string(frontmatter) + "\n+++")
|
||||||
|
case "json":
|
||||||
|
return frontmatter
|
||||||
|
}
|
||||||
|
|
||||||
|
return frontmatter
|
||||||
|
}
|
48
handlers/command.go
Normal file
48
handlers/command.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hacdias/caddy-filemanager/config"
|
||||||
|
"github.com/hacdias/caddy-filemanager/page"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command handles the requests for VCS related commands: git, svn and mercurial
|
||||||
|
func Command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
|
||||||
|
command := strings.Split(r.Header.Get("command"), " ")
|
||||||
|
|
||||||
|
// Check if the command is allowed
|
||||||
|
mayContinue := false
|
||||||
|
|
||||||
|
for _, cmd := range u.Commands {
|
||||||
|
if cmd == command[0] {
|
||||||
|
mayContinue = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mayContinue {
|
||||||
|
return http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the program is talled is installed on the computer
|
||||||
|
if _, err := exec.LookPath(command[0]); err != nil {
|
||||||
|
return http.StatusNotImplemented, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
path := strings.Replace(r.URL.Path, c.BaseURL, c.Scope, 1)
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
|
||||||
|
cmd := exec.Command(command[0], command[1:len(command)]...)
|
||||||
|
cmd.Dir = path
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &page.Page{Info: &page.Info{Data: string(output)}}
|
||||||
|
return p.PrintAsJSON(w)
|
||||||
|
}
|
83
handlers/download.go
Normal file
83
handlers/download.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hacdias/caddy-filemanager/config"
|
||||||
|
"github.com/hacdias/caddy-filemanager/file"
|
||||||
|
"github.com/mholt/archiver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Download creates an archieve in one of the supported formats (zip, tar,
|
||||||
|
// tar.gz or tar.bz2) and sends it to be downloaded.
|
||||||
|
func Download(w http.ResponseWriter, r *http.Request, c *config.Config, i *file.Info) (int, error) {
|
||||||
|
query := r.URL.Query().Get("download")
|
||||||
|
|
||||||
|
if !i.IsDir() {
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename="+i.Name())
|
||||||
|
http.ServeFile(w, r, i.Path)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []string{}
|
||||||
|
names := strings.Split(r.URL.Query().Get("files"), ",")
|
||||||
|
|
||||||
|
if len(names) != 0 {
|
||||||
|
for _, name := range names {
|
||||||
|
files = append(files, filepath.Join(i.Path, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
files = append(files, i.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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename="+i.Name()+extension)
|
||||||
|
io.Copy(w, file)
|
||||||
|
return http.StatusOK, nil
|
||||||
|
}
|
84
handlers/editor.go
Normal file
84
handlers/editor.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hacdias/caddy-filemanager/file"
|
||||||
|
"github.com/hacdias/caddy-filemanager/frontmatter"
|
||||||
|
"github.com/spf13/hugo/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Editor contains the information for the editor page
|
||||||
|
type Editor struct {
|
||||||
|
Class string
|
||||||
|
Mode string
|
||||||
|
Content string
|
||||||
|
FrontMatter *frontmatter.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEditor gets the editor based on a FileInfo struct
|
||||||
|
func GetEditor(i *file.Info) (*Editor, error) {
|
||||||
|
// Create a new editor variable and set the mode
|
||||||
|
editor := new(Editor)
|
||||||
|
editor.Mode = strings.TrimPrefix(filepath.Ext(i.Name()), ".")
|
||||||
|
|
||||||
|
switch editor.Mode {
|
||||||
|
case "md", "markdown", "mdown", "mmark":
|
||||||
|
editor.Mode = "markdown"
|
||||||
|
case "asciidoc", "adoc", "ad":
|
||||||
|
editor.Mode = "asciidoc"
|
||||||
|
case "rst":
|
||||||
|
editor.Mode = "rst"
|
||||||
|
case "html", "htm":
|
||||||
|
editor.Mode = "html"
|
||||||
|
case "js":
|
||||||
|
editor.Mode = "javascript"
|
||||||
|
}
|
||||||
|
|
||||||
|
var page parser.Page
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Handle the content depending on the file extension
|
||||||
|
switch editor.Mode {
|
||||||
|
case "json", "toml", "yaml":
|
||||||
|
// Defines the class and declares an error
|
||||||
|
editor.Class = "frontmatter-only"
|
||||||
|
|
||||||
|
// Checks if the file already has the frontmatter rune and parses it
|
||||||
|
if frontmatter.HasRune(i.Content) {
|
||||||
|
editor.FrontMatter, _, err = frontmatter.Pretty(i.Content)
|
||||||
|
} else {
|
||||||
|
editor.FrontMatter, _, err = frontmatter.Pretty(frontmatter.AppendRune(i.Content, editor.Mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there were any errors
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
fallthrough
|
||||||
|
case "markdown", "asciidoc", "rst":
|
||||||
|
if frontmatter.HasRune(i.Content) {
|
||||||
|
// Starts a new buffer and parses the file using Hugo's functions
|
||||||
|
buffer := bytes.NewBuffer(i.Content)
|
||||||
|
page, err = parser.ReadFrom(buffer)
|
||||||
|
editor.Class = "complete"
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
// Parses the page content and the frontmatter
|
||||||
|
editor.Content = strings.TrimSpace(string(page.Content()))
|
||||||
|
editor.FrontMatter, _, err = frontmatter.Pretty(page.FrontMatter())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
editor.Class = "content-only"
|
||||||
|
editor.Content = i.StringifyContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor, nil
|
||||||
|
}
|
123
handlers/listing.go
Normal file
123
handlers/listing.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hacdias/caddy-filemanager/config"
|
||||||
|
"github.com/hacdias/caddy-filemanager/file"
|
||||||
|
"github.com/hacdias/caddy-filemanager/page"
|
||||||
|
"github.com/hacdias/caddy-filemanager/utils/errors"
|
||||||
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServeListing presents the user with a listage of a directory folder.
|
||||||
|
func ServeListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User, i *file.Info) (int, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Loads the content of the directory
|
||||||
|
listing, err := file.GetListing(u, i.VirtualPath, r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ErrorToHTTPCode(err, true), err
|
||||||
|
}
|
||||||
|
|
||||||
|
listing.Context = httpserver.Context{
|
||||||
|
Root: http.Dir(u.Scope),
|
||||||
|
Req: r,
|
||||||
|
URL: r.URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the query values into the Listing struct
|
||||||
|
var limit int
|
||||||
|
listing.Sort, listing.Order, limit, err = handleSortOrder(w, r, c.Scope)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
page := &page.Page{
|
||||||
|
Minimal: r.Header.Get("Minimal") == "true",
|
||||||
|
Info: &page.Info{
|
||||||
|
Name: listing.Name,
|
||||||
|
Path: i.VirtualPath,
|
||||||
|
IsDir: true,
|
||||||
|
User: u,
|
||||||
|
Config: c,
|
||||||
|
Data: listing,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.PrintAsHTML(w, "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
|
||||||
|
}
|
140
handlers/put.go
Normal file
140
handlers/put.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hacdias/caddy-filemanager/config"
|
||||||
|
"github.com/hacdias/caddy-filemanager/file"
|
||||||
|
"github.com/spf13/hugo/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PreProccessPUT is used to update a file that was edited
|
||||||
|
func PreProccessPUT(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
c *config.Config,
|
||||||
|
u *config.User,
|
||||||
|
i *file.Info,
|
||||||
|
) (err error) {
|
||||||
|
var (
|
||||||
|
data map[string]interface{}
|
||||||
|
file []byte
|
||||||
|
kind string
|
||||||
|
rawBuffer = new(bytes.Buffer)
|
||||||
|
)
|
||||||
|
|
||||||
|
kind = r.Header.Get("kind")
|
||||||
|
rawBuffer.ReadFrom(r.Body)
|
||||||
|
|
||||||
|
if kind != "" {
|
||||||
|
err = json.Unmarshal(rawBuffer.Bytes(), &data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case "frontmatter-only":
|
||||||
|
if file, err = parseFrontMatterOnlyFile(data, i.Name()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "content-only":
|
||||||
|
mainContent := data["content"].(string)
|
||||||
|
mainContent = strings.TrimSpace(mainContent)
|
||||||
|
file = []byte(mainContent)
|
||||||
|
case "complete":
|
||||||
|
if file, err = parseCompleteFile(data, i.Name(), u.FrontMatter); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
file = rawBuffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite the request Body
|
||||||
|
r.Body = ioutil.NopCloser(bytes.NewReader(file))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFrontMatterOnlyFile parses a frontmatter only file
|
||||||
|
func parseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, error) {
|
||||||
|
frontmatter := strings.TrimPrefix(filepath.Ext(filename), ".")
|
||||||
|
f, err := parseFrontMatter(data, frontmatter)
|
||||||
|
fString := string(f)
|
||||||
|
|
||||||
|
// If it's toml or yaml, strip frontmatter identifier
|
||||||
|
if frontmatter == "toml" {
|
||||||
|
fString = strings.TrimSuffix(fString, "+++\n")
|
||||||
|
fString = strings.TrimPrefix(fString, "+++\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if frontmatter == "yaml" {
|
||||||
|
fString = strings.TrimSuffix(fString, "---\n")
|
||||||
|
fString = strings.TrimPrefix(fString, "---\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
f = []byte(fString)
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFrontMatter is the frontmatter parser
|
||||||
|
func parseFrontMatter(data interface{}, frontmatter string) ([]byte, error) {
|
||||||
|
var mark rune
|
||||||
|
|
||||||
|
switch frontmatter {
|
||||||
|
case "toml":
|
||||||
|
mark = rune('+')
|
||||||
|
case "json":
|
||||||
|
mark = rune('{')
|
||||||
|
case "yaml":
|
||||||
|
mark = rune('-')
|
||||||
|
default:
|
||||||
|
return []byte{}, errors.New("Can't define the frontmatter.")
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := parser.InterfaceToFrontMatter(data, mark)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCompleteFile parses a complete file
|
||||||
|
func parseCompleteFile(data map[string]interface{}, filename string, frontmatter string) ([]byte, error) {
|
||||||
|
mainContent := ""
|
||||||
|
|
||||||
|
if _, ok := data["content"]; ok {
|
||||||
|
// The main content of the file
|
||||||
|
mainContent = data["content"].(string)
|
||||||
|
mainContent = "\n\n" + strings.TrimSpace(mainContent) + "\n"
|
||||||
|
|
||||||
|
// Removes the main content from the rest of the frontmatter
|
||||||
|
delete(data, "content")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := data["date"]; ok {
|
||||||
|
data["date"] = data["date"].(string) + ":00"
|
||||||
|
}
|
||||||
|
|
||||||
|
front, err := parseFrontMatter(data, frontmatter)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(frontmatter)
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates the final file
|
||||||
|
f := new(bytes.Buffer)
|
||||||
|
f.Write(front)
|
||||||
|
f.Write([]byte(mainContent))
|
||||||
|
return f.Bytes(), nil
|
||||||
|
}
|
48
handlers/single.go
Normal file
48
handlers/single.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/hacdias/caddy-filemanager/config"
|
||||||
|
"github.com/hacdias/caddy-filemanager/file"
|
||||||
|
"github.com/hacdias/caddy-filemanager/page"
|
||||||
|
"github.com/hacdias/caddy-filemanager/utils/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User, i *file.Info) (int, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if err = i.RetrieveFileType(); err != nil {
|
||||||
|
return errors.ErrorToHTTPCode(err, true), err
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.Type == "text" {
|
||||||
|
if err = i.Read(); err != nil {
|
||||||
|
return errors.ErrorToHTTPCode(err, true), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &page.Page{
|
||||||
|
Info: &page.Info{
|
||||||
|
Name: i.Name(),
|
||||||
|
Path: i.VirtualPath,
|
||||||
|
IsDir: false,
|
||||||
|
Data: i,
|
||||||
|
User: u,
|
||||||
|
Config: c,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.CanBeEdited() && u.AllowEdit {
|
||||||
|
p.Data, err = GetEditor(i)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.PrintAsHTML(w, "frontmatter", "editor")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.PrintAsHTML(w, "single")
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package errors
|
package page
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const template = `<!DOCTYPE html>
|
const errTemplate = `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>TITLE</title>
|
<title>TITLE</title>
|
||||||
@ -32,6 +32,9 @@ const template = `<!DOCTYPE html>
|
|||||||
color: #eee;
|
color: #eee;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
p {
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -45,9 +48,9 @@ const template = `<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
// PrintHTML prints the error page
|
// PrintErrorHTML prints the error page
|
||||||
func PrintHTML(w http.ResponseWriter, code int, err error) (int, error) {
|
func PrintErrorHTML(w http.ResponseWriter, code int, err error) (int, error) {
|
||||||
tpl := template
|
tpl := errTemplate
|
||||||
tpl = strings.Replace(tpl, "TITLE", strconv.Itoa(code)+" "+http.StatusText(code), -1)
|
tpl = strings.Replace(tpl, "TITLE", strconv.Itoa(code)+" "+http.StatusText(code), -1)
|
||||||
tpl = strings.Replace(tpl, "CODE", err.Error(), -1)
|
tpl = strings.Replace(tpl, "CODE", err.Error(), -1)
|
||||||
|
|
13
page/page.go
13
page/page.go
@ -1,3 +1,4 @@
|
|||||||
|
// Package page is used to render the HTML to the end user
|
||||||
package page
|
package page
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -13,13 +14,13 @@ import (
|
|||||||
"github.com/hacdias/caddy-filemanager/utils/variables"
|
"github.com/hacdias/caddy-filemanager/utils/variables"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Page contains the informations and functions needed to show the page
|
// Page contains the informations and functions needed to show the Page
|
||||||
type Page struct {
|
type Page struct {
|
||||||
*Info
|
*Info
|
||||||
Minimal bool
|
Minimal bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info contains the information of a page
|
// Info contains the information of a Page
|
||||||
type Info struct {
|
type Info struct {
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
@ -101,7 +102,7 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro
|
|||||||
// For each template, add it to the the tpl variable
|
// For each template, add it to the the tpl variable
|
||||||
for i, t := range templates {
|
for i, t := range templates {
|
||||||
// Get the template from the assets
|
// Get the template from the assets
|
||||||
page, err := assets.Asset("templates/" + t + ".tmpl")
|
Page, err := assets.Asset("templates/" + t + ".tmpl")
|
||||||
|
|
||||||
// Check if there is some error. If so, the template doesn't exist
|
// Check if there is some error. If so, the template doesn't exist
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -112,9 +113,9 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro
|
|||||||
// If it's the first iteration, creates a new template and add the
|
// If it's the first iteration, creates a new template and add the
|
||||||
// functions map
|
// functions map
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
tpl, err = template.New(t).Funcs(functions).Parse(string(page))
|
tpl, err = template.New(t).Funcs(functions).Parse(string(Page))
|
||||||
} else {
|
} else {
|
||||||
tpl, err = tpl.Parse(string(page))
|
tpl, err = tpl.Parse(string(Page))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -135,7 +136,7 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro
|
|||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrintAsJSON prints the current page infromation in JSON
|
// PrintAsJSON prints the current Page infromation in JSON
|
||||||
func (p Page) PrintAsJSON(w http.ResponseWriter) (int, error) {
|
func (p Page) PrintAsJSON(w http.ResponseWriter) (int, error) {
|
||||||
marsh, err := json.Marshal(p.Info.Data)
|
marsh, err := json.Marshal(p.Info.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -5,13 +5,16 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToHTTPCode gets the respective HTTP code for an error
|
func ErrorToHTTPCode(err error, gone bool) int {
|
||||||
func ToHTTPCode(err error) int {
|
|
||||||
switch {
|
switch {
|
||||||
case os.IsPermission(err):
|
case os.IsPermission(err):
|
||||||
return http.StatusForbidden
|
return http.StatusForbidden
|
||||||
case os.IsNotExist(err):
|
case os.IsNotExist(err):
|
||||||
return http.StatusNotFound
|
if !gone {
|
||||||
|
return http.StatusNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusGone
|
||||||
case os.IsExist(err):
|
case os.IsExist(err):
|
||||||
return http.StatusGone
|
return http.StatusGone
|
||||||
default:
|
default:
|
Loading…
Reference in New Issue
Block a user