Merge branch 'dev'

This commit is contained in:
Henrique Dias 2016-10-27 20:31:00 +01:00
commit 46e19a7094
36 changed files with 1329 additions and 1246 deletions

View File

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

View File

@ -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");
}); });

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -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
View 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
View 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())
}

View File

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

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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")
}

View File

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

View File

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

View File

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