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
}
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;
}
@ -1166,4 +1186,10 @@ i.spin {
column-count: 1;
column-gap: 0;
}
}
}
@media screen and (max-width: 450px) {
#toolbar p {
display: none;
}
}

View File

@ -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('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();
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 != 201), html);
}
button.changeToDone((request.status != 200), 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");
});

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

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 (
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"
@ -17,16 +16,14 @@ import (
// Config is a configuration for browsing in a particualr path.
type Config struct {
*User
BaseURL string
AbsoluteURL string
AddrPath string
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
BaseURL string
AbsoluteURL string
AddrPath string
Token string // Anti CSRF token
HugoEnabled bool // Enables the Hugo plugin for File Manager
Users map[string]*User
WebDavURL string
CurrentUser *User
}
// Rule is a dissalow/allow rule
@ -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)

View File

@ -1,21 +1,23 @@
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
StyleSheet string `json:"-"` // Costum stylesheet
FrontMatter string `json:"-"` // Default frontmatter to save files in
AllowNew bool // Can create files and folders
AllowEdit bool // Can edit/rename files
AllowCommands bool // Can execute commands
Commands []string // Available Commands
Rules []*Rule `json:"-"` // Access rules
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
AllowEdit bool // Can edit/rename files
AllowCommands bool // Can execute commands
Commands []string // Available Commands
Rules []*Rule `json:"-"` // Access rules
}
// 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 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"
)
@ -37,269 +30,174 @@ type FileManager struct {
// 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) {
var (
c *config.Config
fi *directory.Info
code int
err error
serveAssets bool
user *config.User
c *config.Config
fi *file.Info
code int
err error
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)
}
if _, ok := c.Users[username]; ok {
user = c.Users[username]
} else {
user = c.User
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)
}
if c.WebDav && strings.HasPrefix(r.URL.Path, c.WebDavURL) {
//url := strings.TrimPrefix(r.URL.Path, c.WebDavURL)
return http.StatusForbidden, nil
}
/*
if !user.Allowed(url) {
return http.StatusForbidden, nil
}
// Obtains the user
username, _, _ := r.BasicAuth()
if _, ok := c.Users[username]; ok {
user = c.Users[username]
} else {
user = c.User
}
switch r.Method {
case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
if !user.AllowEdit {
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 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
// }
// Checks for user permissions relatively to this PATH
if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.WebDavURL)) {
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 {
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)
}
return code, err
case http.MethodPut:
if fi.IsDir {
return http.StatusNotAcceptable, nil
}
case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
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)
case "MKCOL", "COPY":
if !user.AllowNew {
return http.StatusForbidden, nil
}
}
// Search and git commands.
if r.Header.Get("Search") == "true" {
// TODO: search commands.
// 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
}
}
// VCS commands.
if r.Header.Get("Command") != "" {
if !user.AllowCommands {
return http.StatusUnauthorized, nil
}
c.Handler.ServeHTTP(w, r)
return 0, 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.
return newDirectory(w, r, c)
// 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
}
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:
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)
}
// 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/hacdias/caddy-filemanager/utils/variables"
"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 (
"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)

View File

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

View File

@ -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):
return http.StatusNotFound
if !gone {
return http.StatusNotFound
}
return http.StatusGone
case os.IsExist(err):
return http.StatusGone
default: