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
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0
|
||||
@ -260,7 +264,7 @@ textarea {
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
padding-top: 5em;
|
||||
background-color: #fcfcfc;
|
||||
background-color: #ffffff;
|
||||
text-rendering: optimizespeed;
|
||||
}
|
||||
|
||||
@ -469,6 +473,8 @@ header {
|
||||
z-index: 999;
|
||||
padding: 1.7em 0;
|
||||
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 {
|
||||
@ -477,7 +483,9 @@ header h1 {
|
||||
}
|
||||
|
||||
header a,
|
||||
header a:hover {
|
||||
header a:hover,
|
||||
#toolbar a,
|
||||
#toolbar a:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@ -566,6 +574,7 @@ header p i {
|
||||
transition: .1s ease all;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#search.active div i,
|
||||
@ -675,8 +684,7 @@ header .only-side {
|
||||
display: none;
|
||||
}
|
||||
|
||||
header #prev:hover+.prev-links,
|
||||
header .prev-links:hover {
|
||||
.action:hover ul {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@ -684,41 +692,53 @@ header .prev-links:hover {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
header .prev-links {
|
||||
.action ul {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: 3.1em;
|
||||
left: 0;
|
||||
color: #7d7d7d;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
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;
|
||||
flex-direction: column-reverse;
|
||||
display: none;
|
||||
transition: .2s ease all;
|
||||
min-width: 12em;
|
||||
min-width: 3em;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
header .prev-links a {
|
||||
padding: .5em;
|
||||
.action ul:before {
|
||||
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;
|
||||
transition: .2s ease all;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
header .prev-links a:first-child {
|
||||
.action ul a:first-child {
|
||||
border: 0;
|
||||
border-bottom-right-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-left-radius: .2em;
|
||||
}
|
||||
|
||||
header .prev-links a:hover {
|
||||
.action ul a:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
@ -774,7 +794,7 @@ header .action span {
|
||||
border: 0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24);
|
||||
padding: .5em;
|
||||
width: 10em;
|
||||
width: 22em;
|
||||
border-radius: .2em;
|
||||
}
|
||||
|
||||
@ -1167,3 +1187,9 @@ i.spin {
|
||||
column-gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 450px) {
|
||||
#toolbar p {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
// TODO: way to get the webdav url
|
||||
|
||||
var tempID = "_fm_internal_temporary_id"
|
||||
var selectedItems = [];
|
||||
var token = "";
|
||||
@ -87,7 +89,7 @@ Element.prototype.changeToDone = function(error, html) {
|
||||
}
|
||||
|
||||
var toWebDavURL = function(url) {
|
||||
url = url.replace("/", "/webdav/")
|
||||
url = url.replace(baseURL + "/", webdavURL + "/");
|
||||
return window.location.origin + url
|
||||
}
|
||||
|
||||
@ -149,22 +151,6 @@ var preventDefault = function(event) {
|
||||
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
|
||||
var RemoveLastDirectoryPartOf = function(url) {
|
||||
var arr = url.split('/');
|
||||
@ -299,24 +285,20 @@ var renameEvent = function(event) {
|
||||
var handleFiles = function(files) {
|
||||
let button = document.getElementById("upload");
|
||||
let html = button.changeToLoading();
|
||||
let data = new FormData();
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
data.append(files[i].name, files[i]);
|
||||
}
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
request.open('POST', window.location.pathname);
|
||||
request.setRequestHeader("Upload", "true");
|
||||
request.open('PUT', toWebDavURL(window.location.pathname + files[i].name));
|
||||
request.setRequestHeader('Token', token);
|
||||
request.send(data);
|
||||
request.send(files[i]);
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState == 4) {
|
||||
if (request.status == 200) {
|
||||
if (request.status == 201) {
|
||||
reloadListing();
|
||||
}
|
||||
|
||||
button.changeToDone((request.status != 200), html);
|
||||
button.changeToDone((request.status != 201), html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -417,6 +399,7 @@ var addNewDirEvents = function() {
|
||||
|
||||
// Handles the new directory event
|
||||
var newDirEvent = function(event) {
|
||||
// TODO: create new dir button and new file button
|
||||
if (event.keyCode == 27) {
|
||||
document.getElementById('newdir').classList.toggle('enabled');
|
||||
setTimeout(() => {
|
||||
@ -430,13 +413,14 @@ var newDirEvent = function(event) {
|
||||
let button = document.getElementById('new');
|
||||
let html = button.changeToLoading();
|
||||
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('Filename', document.getElementById('newdir').value);
|
||||
request.send();
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState == 4) {
|
||||
button.changeToDone((request.status != 200), html);
|
||||
button.changeToDone((request.status != 201), html);
|
||||
reloadListing(() => {
|
||||
addNewDirEvents();
|
||||
});
|
||||
@ -466,6 +450,8 @@ document.addEventListener("changed-selected", function(event) {
|
||||
document.getElementById("rename").classList.remove("disabled");
|
||||
}
|
||||
|
||||
redefineDownloadURLs();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -473,6 +459,22 @@ document.addEventListener("changed-selected", function(event) {
|
||||
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) {
|
||||
let value = this.value;
|
||||
let box = document.querySelector('#search div');
|
||||
@ -832,13 +834,13 @@ document.addEventListener("editor", (event) => {
|
||||
let data = form2js(document.querySelector('form'));
|
||||
let html = button.changeToLoading();
|
||||
let request = new XMLHttpRequest();
|
||||
request.open("PUT", window.location);
|
||||
request.open("PUT", toWebDavURL(window.location.pathname));
|
||||
request.setRequestHeader('Kind', kind);
|
||||
request.setRequestHeader('Token', token);
|
||||
request.send(JSON.stringify(data));
|
||||
request.onreadystatechange = function() {
|
||||
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("download").addEventListener("click", downloadEvent);
|
||||
document.getElementById("open-nav").addEventListener("click", event => {
|
||||
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 }}
|
||||
|
||||
{{ 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="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>
|
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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -23,9 +22,7 @@ type Config struct {
|
||||
Token string // Anti CSRF token
|
||||
HugoEnabled bool // Enables the Hugo plugin for File Manager
|
||||
Users map[string]*User
|
||||
WebDav bool
|
||||
WebDavURL string
|
||||
WebDavHandler *webdav.Handler
|
||||
CurrentUser *User
|
||||
}
|
||||
|
||||
@ -48,8 +45,8 @@ func Parse(c *caddy.Controller) ([]Config, error) {
|
||||
|
||||
appendConfig := func(cfg Config) error {
|
||||
for _, c := range configs {
|
||||
if c.PathScope == cfg.PathScope {
|
||||
return fmt.Errorf("duplicate file managing config for %s", c.PathScope)
|
||||
if c.Scope == cfg.Scope {
|
||||
return fmt.Errorf("duplicate file managing config for %s", c.Scope)
|
||||
}
|
||||
}
|
||||
configs = append(configs, cfg)
|
||||
@ -59,8 +56,8 @@ func Parse(c *caddy.Controller) ([]Config, error) {
|
||||
for c.Next() {
|
||||
// Initialize the configuration with the default settings
|
||||
cfg := Config{User: &User{}}
|
||||
cfg.PathScope = "."
|
||||
cfg.Root = http.Dir(cfg.PathScope)
|
||||
cfg.Scope = "."
|
||||
cfg.FileSystem = webdav.Dir(cfg.Scope)
|
||||
cfg.BaseURL = ""
|
||||
cfg.FrontMatter = "yaml"
|
||||
cfg.HugoEnabled = false
|
||||
@ -85,6 +82,7 @@ func Parse(c *caddy.Controller) ([]Config, error) {
|
||||
cfg.BaseURL = strings.TrimPrefix(cfg.BaseURL, "/")
|
||||
cfg.BaseURL = strings.TrimSuffix(cfg.BaseURL, "/")
|
||||
cfg.BaseURL = "/" + cfg.BaseURL
|
||||
cfg.WebDavURL = cfg.BaseURL + "webdav"
|
||||
|
||||
if cfg.BaseURL == "/" {
|
||||
cfg.BaseURL = ""
|
||||
@ -105,31 +103,23 @@ func Parse(c *caddy.Controller) ([]Config, error) {
|
||||
return configs, c.Err("frontmatter type not supported")
|
||||
}
|
||||
case "webdav":
|
||||
cfg.WebDav = true
|
||||
|
||||
prefix := "webdav"
|
||||
if c.NextArg() {
|
||||
prefix = c.Val()
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
|
||||
prefix := c.Val()
|
||||
prefix = strings.TrimPrefix(prefix, "/")
|
||||
prefix = strings.TrimSuffix(prefix, "/")
|
||||
prefix = cfg.BaseURL + "/" + prefix
|
||||
|
||||
cfg.WebDavURL = prefix
|
||||
cfg.WebDavHandler = &webdav.Handler{
|
||||
Prefix: prefix,
|
||||
FileSystem: webdav.Dir(cfg.PathScope),
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
}
|
||||
case "show":
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
|
||||
user.PathScope = c.Val()
|
||||
user.PathScope = strings.TrimSuffix(user.PathScope, "/")
|
||||
user.Root = http.Dir(user.PathScope)
|
||||
user.Scope = c.Val()
|
||||
user.Scope = strings.TrimSuffix(user.Scope, "/")
|
||||
user.FileSystem = webdav.Dir(user.Scope)
|
||||
case "styles":
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
@ -233,13 +223,19 @@ func Parse(c *caddy.Controller) ([]Config, error) {
|
||||
user.AllowNew = cfg.AllowEdit
|
||||
user.Commands = cfg.Commands
|
||||
user.FrontMatter = cfg.FrontMatter
|
||||
user.PathScope = cfg.PathScope
|
||||
user.Root = cfg.Root
|
||||
user.Scope = cfg.Scope
|
||||
user.FileSystem = cfg.FileSystem
|
||||
user.Rules = cfg.Rules
|
||||
user.StyleSheet = cfg.StyleSheet
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Handler = &webdav.Handler{
|
||||
Prefix: cfg.WebDavURL,
|
||||
FileSystem: cfg.FileSystem,
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
}
|
||||
|
||||
caddyConf := httpserver.GetConfig(c)
|
||||
cfg.AbsoluteURL = strings.TrimSuffix(caddyConf.Addr.Path, "/") + "/" + cfg.BaseURL
|
||||
cfg.AbsoluteURL = strings.Replace(cfg.AbsoluteURL, "//", "/", -1)
|
||||
|
@ -1,14 +1,16 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
// User contains the configuration for each user
|
||||
type User struct {
|
||||
PathScope string `json:"-"` // Path the user have access
|
||||
Root http.FileSystem `json:"-"` // The virtual file system the user have access
|
||||
Scope string `json:"-"` // Path the user have access
|
||||
FileSystem webdav.FileSystem `json:"-"` // The virtual file system the user have access
|
||||
Handler *webdav.Handler `json:"-"` // The WebDav HTTP Handler
|
||||
StyleSheet string `json:"-"` // Costum stylesheet
|
||||
FrontMatter string `json:"-"` // Default frontmatter to save files in
|
||||
AllowNew bool // Can create files and folders
|
||||
|
@ -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())
|
||||
}
|
310
filemanager.go
310
filemanager.go
@ -1,6 +1,6 @@
|
||||
//go:generate go get github.com/jteeuwen/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
|
||||
// when directory path is requested instead of a specific file. Based on browse
|
||||
@ -9,20 +9,13 @@ package filemanager
|
||||
|
||||
import (
|
||||
e "errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hacdias/caddy-filemanager/assets"
|
||||
"github.com/hacdias/caddy-filemanager/config"
|
||||
"github.com/hacdias/caddy-filemanager/directory"
|
||||
"github.com/hacdias/caddy-filemanager/errors"
|
||||
"github.com/hacdias/caddy-filemanager/file"
|
||||
"github.com/hacdias/caddy-filemanager/handlers"
|
||||
"github.com/hacdias/caddy-filemanager/page"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
@ -38,30 +31,50 @@ type FileManager struct {
|
||||
func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var (
|
||||
c *config.Config
|
||||
fi *directory.Info
|
||||
fi *file.Info
|
||||
code int
|
||||
err error
|
||||
serveAssets bool
|
||||
user *config.User
|
||||
)
|
||||
|
||||
for i := range f.Configs {
|
||||
if httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
|
||||
c = &f.Configs[i]
|
||||
serveAssets = httpserver.Path(r.URL.Path).Matches(c.BaseURL + assets.BaseURL)
|
||||
username, _, _ := r.BasicAuth()
|
||||
// Checks if this Path should be handled by File Manager.
|
||||
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
w.Header().Set("x-frame-options", "SAMEORIGIN")
|
||||
w.Header().Set("x-content-type", "nosniff")
|
||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Obtains the user
|
||||
username, _, _ := r.BasicAuth()
|
||||
if _, ok := c.Users[username]; ok {
|
||||
user = c.Users[username]
|
||||
} else {
|
||||
user = c.User
|
||||
}
|
||||
|
||||
if c.WebDav && strings.HasPrefix(r.URL.Path, c.WebDavURL) {
|
||||
//url := strings.TrimPrefix(r.URL.Path, c.WebDavURL)
|
||||
// Checks if the request URL is for the WebDav server
|
||||
if strings.HasPrefix(r.URL.Path, c.WebDavURL) {
|
||||
// if !c.CheckToken(r) {
|
||||
// return http.StatusForbidden, nil
|
||||
// }
|
||||
|
||||
/*
|
||||
if !user.Allowed(url) {
|
||||
// Checks for user permissions relatively to this PATH
|
||||
if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.WebDavURL)) {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
@ -74,101 +87,103 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err
|
||||
if !user.AllowNew {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
c.WebDavHandler.ServeHTTP(w, r)
|
||||
// Preprocess the PUT request if it's the case
|
||||
if r.Method == http.MethodPut {
|
||||
if handlers.PreProccessPUT(w, r, c, user, fi) != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
c.Handler.ServeHTTP(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Checks if the user has permission to access the current directory.
|
||||
if !user.Allowed(r.URL.Path) {
|
||||
// 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 errors.PrintHTML(w, http.StatusForbidden, e.New("You don't have permission to access this page."))
|
||||
return page.PrintErrorHTML(
|
||||
w, http.StatusForbidden,
|
||||
e.New("You don't have permission to access this page."),
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 = directory.GetInfo(r.URL, c, user)
|
||||
fi, code, err = file.GetInfo(r.URL, c, user)
|
||||
if err != nil {
|
||||
if r.Method == http.MethodGet {
|
||||
return errors.PrintHTML(w, code, err)
|
||||
return page.PrintErrorHTML(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, "/") {
|
||||
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:
|
||||
code, err = handlers.ServeSingle(w, r, c, user, fi)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
case http.MethodGet:
|
||||
// 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)
|
||||
code, err = page.PrintErrorHTML(w, code, err)
|
||||
}
|
||||
|
||||
return code, err
|
||||
case http.MethodPut:
|
||||
if fi.IsDir {
|
||||
return http.StatusNotAcceptable, nil
|
||||
}
|
||||
|
||||
if !user.AllowEdit {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Update a file.
|
||||
return fi.Update(w, r, c, user)
|
||||
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.
|
||||
if r.Header.Get("Search") == "true" {
|
||||
// TODO: search commands.
|
||||
}
|
||||
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") != "" {
|
||||
@ -176,130 +191,13 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err
|
||||
return http.StatusUnauthorized, nil
|
||||
}
|
||||
|
||||
return command(w, r, c, user)
|
||||
return handlers.Command(w, r, c, user)
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a new folder.
|
||||
return newDirectory(w, r, c)
|
||||
default:
|
||||
return http.StatusNotImplemented, nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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/hacdias/caddy-filemanager/utils/variables"
|
||||
|
||||
"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 (
|
||||
"net/http"
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const template = `<!DOCTYPE html>
|
||||
const errTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TITLE</title>
|
||||
@ -32,6 +32,9 @@ const template = `<!DOCTYPE html>
|
||||
color: #eee;
|
||||
font-weight: bold;
|
||||
}
|
||||
p {
|
||||
line-height: 1.3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@ -45,9 +48,9 @@ const template = `<!DOCTYPE html>
|
||||
</div>
|
||||
</html>`
|
||||
|
||||
// PrintHTML prints the error page
|
||||
func PrintHTML(w http.ResponseWriter, code int, err error) (int, error) {
|
||||
tpl := template
|
||||
// PrintErrorHTML prints the error page
|
||||
func PrintErrorHTML(w http.ResponseWriter, code int, err error) (int, error) {
|
||||
tpl := errTemplate
|
||||
tpl = strings.Replace(tpl, "TITLE", strconv.Itoa(code)+" "+http.StatusText(code), -1)
|
||||
tpl = strings.Replace(tpl, "CODE", err.Error(), -1)
|
||||
|
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
|
||||
|
||||
import (
|
||||
@ -13,13 +14,13 @@ import (
|
||||
"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 {
|
||||
*Info
|
||||
Minimal bool
|
||||
}
|
||||
|
||||
// Info contains the information of a page
|
||||
// Info contains the information of a Page
|
||||
type Info struct {
|
||||
Name 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 i, t := range templates {
|
||||
// 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
|
||||
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
|
||||
// functions map
|
||||
if i == 0 {
|
||||
tpl, err = template.New(t).Funcs(functions).Parse(string(page))
|
||||
tpl, err = template.New(t).Funcs(functions).Parse(string(Page))
|
||||
} else {
|
||||
tpl, err = tpl.Parse(string(page))
|
||||
tpl, err = tpl.Parse(string(Page))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -135,7 +136,7 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro
|
||||
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) {
|
||||
marsh, err := json.Marshal(p.Info.Data)
|
||||
if err != nil {
|
||||
|
@ -5,13 +5,16 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// ToHTTPCode gets the respective HTTP code for an error
|
||||
func ToHTTPCode(err error) int {
|
||||
func ErrorToHTTPCode(err error, gone bool) int {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden
|
||||
case os.IsNotExist(err):
|
||||
if !gone {
|
||||
return http.StatusNotFound
|
||||
}
|
||||
|
||||
return http.StatusGone
|
||||
case os.IsExist(err):
|
||||
return http.StatusGone
|
||||
default:
|
Loading…
Reference in New Issue
Block a user