Major changes on API

pull/144/head
Henrique Dias 2017-07-02 17:40:52 +01:00
parent 01c78d2b36
commit 1f7974de38
No known key found for this signature in database
GPG Key ID: 936F5EB68D786730
18 changed files with 470 additions and 638 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
.DS_Store
node_modules/
_assets/dist_dev/*
_assets/dist/*
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -6,10 +6,10 @@ module.exports = {
env: {
NODE_ENV: '"production"'
},
index: path.resolve(__dirname, '../dist/templates/index.html'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: '_',
assetsPublicPath: '{{ .BaseURL }}',
assetsSubDirectory: 'static',
assetsPublicPath: '{{ .BaseURL }}/',
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
@ -27,10 +27,10 @@ module.exports = {
env: {
NODE_ENV: '"development"'
},
index: path.resolve(__dirname, '../dist_dev/templates/index.html'),
assetsRoot: path.resolve(__dirname, '../dist_dev/'),
assetsSubDirectory: '_',
assetsPublicPath: '{{ .BaseURL }}',
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist/'),
assetsSubDirectory: 'static',
assetsPublicPath: '{{ .BaseURL }}/',
produceSourceMap: true
}
}

View File

@ -16,7 +16,7 @@
window.addEventListener('load', function() {
if ('serviceWorker' in navigator &&
(window.location.protocol === 'https:' || isLocalhost)) {
navigator.serviceWorker.register('{{ .BaseURL }}/_/service-worker.js')
navigator.serviceWorker.register('{{ .BaseURL }}/sw.js')
.then(function(registration) {
// updatefound is fired if service-worker.js changes.
registration.onupdatefound = function() {

View File

@ -6,6 +6,7 @@ exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}

View File

@ -98,8 +98,9 @@ var webpackConfig = merge(baseWebpackConfig, {
]),
// service worker caching
new SWPrecacheWebpackPlugin({
cacheId: 'my-vue-app',
filename: 'service-worker.js',
cacheId: 'File Manager',
filename: 'sw.js',
replacePrefix: '{{ .BaseURL }}/',
staticFileGlobs: ['dist/**/*.{js,html,css}'],
minify: true,
stripPrefix: 'dist/'

View File

@ -4,27 +4,28 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="base" content="{{ .BaseURL }}">
<title>File Manager</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/_/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/_/img/icons/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
<!--[if IE]><link rel="shortcut icon" href="/static/img/icons/favicon.ico"><![endif]-->
<!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" href="{{ .BaseURL }}/_/manifest.json">
<meta name="theme-color" content="#4DBA87">
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
<meta name="theme-color" content="#2979ff">
<!-- Add to home screen for Safari on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="{{ .BaseURL }}/_/img/icons/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png">
<!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/_/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff">
<% for (var chunk of webpack.chunks) {
for (var file of chunk.files) {
if (file.match(/\.(js|css)$/)) { %>
<link rel="<%= chunk.initial?'preload':'prefetch' %>" href="{{ .BaseURL }}/<%= file.replace('static', '') %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
<link rel="<%= chunk.initial?'preload':'prefetch' %>" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
<style>
#loading {
@ -88,20 +89,8 @@
}
}
</style>
{{- if ne .User.StyleSheet "" -}}
<style>{{ CSS .User.StyleSheet }}</style>
{{- end -}}
</head>
<body>
<script>
var info = {
user: JSON.parse('{{ Marshal .User }}'),
req: JSON.parse('{{ Marshal . }}'),
webdavURL: "{{ .WebDavURL }}",
baseURL: "{{.BaseURL}}"
}
</script>
<div id="app"></div>
<div id="loading">

View File

@ -3,18 +3,18 @@
"short_name": "File Manager",
"icons": [
{
"src": "/static/img/icons/android-chrome-192x192.png",
"src": "{{ .BaseURL }}/static/img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/img/icons/android-chrome-512x512.png",
"src": "{{ .BaseURL }}/static/img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/index.html",
"start_url": "{{ .BaseURL }}/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
"background_color": "#ffffff",
"theme_color": "#2979ff"
}

281
api.go Normal file
View File

@ -0,0 +1,281 @@
package filemanager
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strings"
)
func cleanURL(path string) (string, string) {
if path == "" {
return "", ""
}
path = strings.TrimPrefix(path, "/")
i := strings.Index(path, "/")
if i == -1 {
return "", path
}
return path[0:i], path[i:len(path)]
}
func serveAPI(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/auth" {
return getTokenHandler(c, w, r)
}
/* valid, user := validAuth(c, r)
if !valid {
return http.StatusForbidden, nil
}
fmt.Println(user)
c.us = user */
c.us = c.fm.User
var router string
router, r.URL.Path = cleanURL(r.URL.Path)
if !c.us.Allowed(r.URL.Path) {
return http.StatusForbidden, nil
}
if router == "checksum" || router == "download" {
var err error
c.fi, err = getInfo(r.URL, c.fm, c.us)
if err != nil {
return errorToHTTP(err, false), err
}
}
switch router {
case "download":
return downloadHandler(c, w, r)
case "checksum":
return checksumHandler(c, w, r)
case "command":
return command(c, w, r)
case "search":
return search(c, w, r)
case "resource":
return resourceHandler(c, w, r)
}
return http.StatusNotFound, nil
}
func resourceHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
switch r.Method {
case http.MethodGet:
return getHandler(c, w, r)
case http.MethodDelete:
return deleteHandler(c, w, r)
case http.MethodPut:
return putHandler(c, w, r)
case http.MethodPost:
// Handle renaming
}
/* // Execute beforeSave if it is a PUT request.
if r.Method == http.MethodPut {
if err := c.fm.BeforeSave(r, c.fm, c.us); err != nil {
return http.StatusInternalServerError, err
}
}
// Execute afterSave if it is a PUT request.
if r.Method == http.MethodPut {
if err := c.fm.AfterSave(r, c.fm, c.us); err != nil {
return http.StatusInternalServerError, err
}
} */
return http.StatusNotImplemented, nil
}
func getHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Obtains the information of the directory/file.
f, err := getInfo(r.URL, c.fm, c.us)
if err != nil {
return errorToHTTP(err, false), err
}
// If it's a dir and the path doesn't end with a trailing slash,
// redirect the user.
if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") {
r.URL.Path = r.URL.Path + "/"
}
// If it is a dir, go and serve the listing.
if f.IsDir {
c.fi = f
return listingHandler(c, w, r)
}
// Tries to get the file type.
if err = f.RetrieveFileType(); err != nil {
return errorToHTTP(err, true), err
}
// If it can't be edited or the user isn't allowed to,
// serve it as a listing, with a preview of the file.
if !f.CanBeEdited() || !c.us.AllowEdit {
f.Kind = "preview"
} else {
// Otherwise, we just bring the editor in!
f.Kind = "editor"
err = f.getEditor()
if err != nil {
return http.StatusInternalServerError, err
}
}
return renderJSON(w, f)
}
func listingHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
f := c.fi
f.Kind = "listing"
err := f.getListing(c, r)
if err != nil {
return errorToHTTP(err, true), err
}
listing := f.listing
cookieScope := c.fm.RootURL()
if cookieScope == "" {
cookieScope = "/"
}
// Copy the query values into the Listing struct
listing.Sort, listing.Order, err = handleSortOrder(w, r, cookieScope)
if err != nil {
return http.StatusBadRequest, err
}
listing.ApplySort()
listing.Display = displayMode(w, r, cookieScope)
return renderJSON(w, f)
}
func deleteHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Prevent the removal of the root directory.
if r.URL.Path == "/" {
return http.StatusForbidden, nil
}
// Remove the file or folder.
err := c.us.fileSystem.RemoveAll(context.TODO(), r.URL.Path)
if err != nil {
return errorToHTTP(err, true), err
}
return http.StatusOK, nil
}
func putHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if strings.HasSuffix(r.URL.Path, "/") {
err := c.us.fileSystem.Mkdir(context.TODO(), r.URL.Path, 0666)
return errorToHTTP(err, false), err
}
f, err := c.us.fileSystem.OpenFile(context.TODO(), r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
defer f.Close()
if err != nil {
return errorToHTTP(err, false), err
}
_, err = io.Copy(f, r.Body)
if err != nil {
return errorToHTTP(err, false), err
}
fi, err := f.Stat()
if err != nil {
return errorToHTTP(err, false), err
}
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
w.Header().Set("ETag", etag)
return http.StatusOK, nil
}
// displayMode obtaisn the display mode from URL, or from the
// cookie.
func displayMode(w http.ResponseWriter, r *http.Request, scope string) string {
displayMode := r.URL.Query().Get("display")
if displayMode == "" {
if displayCookie, err := r.Cookie("display"); err == nil {
displayMode = displayCookie.Value
}
}
if displayMode == "" || (displayMode != "mosaic" && displayMode != "list") {
displayMode = "mosaic"
}
http.SetCookie(w, &http.Cookie{
Name: "display",
Value: displayMode,
MaxAge: 31536000,
Path: scope,
Secure: r.TLS != nil,
})
return displayMode
}
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
// and reads 'limit' if given. The latter is 0 if not given. Sets cookies.
func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, err error) {
sort = r.URL.Query().Get("sort")
order = r.URL.Query().Get("order")
// 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,
MaxAge: 31536000,
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,
MaxAge: 31536000,
Path: scope,
Secure: r.TLS != nil,
})
}
return
}

54
auth.go Normal file
View File

@ -0,0 +1,54 @@
package filemanager
import (
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
)
/* Set up a global string for our secret */
var key = []byte("secret")
type claims struct {
*User
jwt.StandardClaims
}
func getTokenHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// TODO: get user and password info from the request
// check if the password is correct for that user using a DB or JSOn
// or something.
claims := claims{
c.fm.User,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Minute * 5).Unix(),
Issuer: "test",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
string, err := token.SignedString(key)
if err != nil {
return http.StatusInternalServerError, err
}
w.Write([]byte(string))
return 0, nil
}
func validAuth(c *requestContext, r *http.Request) (bool, *User) {
token, err := request.ParseFromRequestWithClaims(r, request.AuthorizationHeaderExtractor, &claims{},
func(token *jwt.Token) (interface{}, error) {
return key, nil
})
if err == nil && token.Valid {
return true, c.fm.User
}
return false, nil
}

View File

@ -94,7 +94,6 @@ func parse(c *caddy.Controller) ([]*config, error) {
if len(args) > 0 {
m.baseURL = args[0]
m.webDavURL = "/webdav"
m.SetBaseURL(args[0])
}
@ -108,13 +107,6 @@ func parse(c *caddy.Controller) ([]*config, error) {
if m.AfterSave, err = makeCommand(c, m); err != nil {
return configs, err
}
case "webdav":
if !c.NextArg() {
return configs, c.ArgErr()
}
m.webDavURL = "c.Val()"
m.SetWebDavURL(c.Val())
case "show":
if !c.NextArg() {
return configs, c.ArgErr()
@ -251,7 +243,7 @@ func makeCommand(c *caddy.Controller, m *config) (Command, error) {
fn = func(r *http.Request, c *FileManager, u *User) error {
path := strings.Replace(r.URL.Path, m.baseURL+m.webDavURL, "", 1)
path = u.Scope() + "/" + path
path = u.Scope + "/" + path
path = filepath.Clean(path)
for i := range args {

View File

@ -77,7 +77,7 @@ func command(c *requestContext, w http.ResponseWriter, r *http.Request) (int, er
}
// Gets the path and initializes a buffer.
path := c.us.scope + "/" + r.URL.Path
path := c.us.Scope + "/" + r.URL.Path
path = filepath.Clean(path)
buff := new(bytes.Buffer)

View File

@ -12,10 +12,10 @@ import (
"github.com/mholt/archiver"
)
// serveDownload creates an archive in one of the supported formats (zip, tar,
// downloadHandler creates an archive in one of the supported formats (zip, tar,
// tar.gz or tar.bz2) and sends it to be downloaded.
func serveDownload(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("download")
func downloadHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("format")
if !c.fi.IsDir {
w.Header().Set("Content-Disposition", "attachment; filename="+c.fi.Name)

22
file.go
View File

@ -29,6 +29,8 @@ var (
// file contains the information about a particular file or directory.
type file struct {
// Indicates the Kind of view on the front-end (listing, editor or preview).
Kind string `json:"kind"`
// The name of the file.
Name string `json:"name"`
// The Size of the file.
@ -79,15 +81,13 @@ type listing struct {
func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
var err error
i := &file{URL: c.RootURL() + url.Path}
i.VirtualPath = url.Path
i.VirtualPath = strings.TrimPrefix(i.VirtualPath, "/")
i.VirtualPath = "/" + i.VirtualPath
i := &file{
URL: c.RootURL() + "/files" + url.Path,
VirtualPath: url.Path,
Path: filepath.Join(u.Scope, url.Path),
}
i.Path = u.scope + i.VirtualPath
i.Path = filepath.Clean(i.Path)
info, err := os.Stat(i.Path)
info, err := u.fileSystem.Stat(context.TODO(), i.Path)
if err != nil {
return i, err
}
@ -103,8 +103,6 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
// getListing gets the information about a specific directory and its files.
func (i *file) getListing(c *requestContext, r *http.Request) error {
baseURL := c.fm.RootURL() + r.URL.Path
// Gets the directory information using the Virtual File System of
// the user configuration.
f, err := c.us.fileSystem.OpenFile(context.TODO(), c.fi.VirtualPath, os.O_RDONLY, 0)
@ -140,7 +138,7 @@ func (i *file) getListing(c *requestContext, r *http.Request) error {
}
// Absolute URL
url := url.URL{Path: baseURL + name}
url := url.URL{Path: i.URL + name}
i := file{
Name: f.Name(),
@ -165,7 +163,7 @@ func (i *file) getListing(c *requestContext, r *http.Request) error {
}
// getEditor gets the editor based on a Info struct
func (i *file) getEditor(r *http.Request) error {
func (i *file) getEditor() error {
i.Language = editorLanguage(i.Extension)
// If the editor will hold only content, leave now.

View File

@ -29,11 +29,6 @@ type FileManager struct {
// a trailing slash and mustn't contain prefixURL, if set.
baseURL string
// webDavURL is the path where the WebDAV will be accessible. It can be set to ""
// in order to override the GUI and only use the WebDAV. It musn't end with
// a trailing slash.
webDavURL string
// Users is a map with the different configurations for each user.
Users map[string]*User
@ -43,8 +38,7 @@ type FileManager struct {
// AfterSave is a function that is called before saving a file.
AfterSave Command
templates *rice.Box
static http.Handler
assets *rice.Box
}
// Command is a command function.
@ -53,15 +47,12 @@ type Command func(r *http.Request, m *FileManager, u *User) error
// User contains the configuration for each user. It should be created
// using NewUser on a File Manager instance.
type User struct {
// scope is the physical path the user has access to.
scope string
// Scope is the physical path the user has access to.
Scope string
// fileSystem is the virtual file system the user has access.
fileSystem webdav.FileSystem
// handler handles incoming requests to the WebDAV backend.
handler *webdav.Handler
// Rules is an array of access and deny rules.
Rules []*Rule `json:"-"`
@ -106,13 +97,11 @@ func New(scope string) *FileManager {
Users: map[string]*User{},
BeforeSave: func(r *http.Request, m *FileManager, u *User) error { return nil },
AfterSave: func(r *http.Request, m *FileManager, u *User) error { return nil },
static: http.FileServer(rice.MustFindBox("./_assets/dist_dev/_").HTTPBox()),
templates: rice.MustFindBox("./_assets/dist_dev/templates"),
assets: rice.MustFindBox("./_assets/dist"),
}
m.SetScope(scope, "")
m.SetBaseURL("/")
m.SetWebDavURL("/webdav")
return m
}
@ -126,7 +115,7 @@ func (m FileManager) RootURL() string {
// WebDavURL returns the actual URL
// where WebDAV can be accessed.
func (m FileManager) WebDavURL() string {
return m.prefixURL + m.baseURL + m.webDavURL
return m.prefixURL + m.baseURL + "/api/webdav"
}
// SetPrefixURL updates the prefixURL of a File
@ -147,32 +136,6 @@ func (m *FileManager) SetBaseURL(url string) {
m.baseURL = strings.TrimSuffix(url, "/")
}
// SetWebDavURL updates the webDavURL of a File Manager
// object and updates it's main handler.
func (m *FileManager) SetWebDavURL(url string) {
url = strings.TrimPrefix(url, "/")
url = strings.TrimSuffix(url, "/")
m.webDavURL = "/" + url
// update base user webdav handler
m.handler = &webdav.Handler{
Prefix: m.webDavURL,
FileSystem: m.fileSystem,
LockSystem: webdav.NewMemLS(),
}
// update other users' handlers to match
// the new URL
for _, u := range m.Users {
u.handler = &webdav.Handler{
Prefix: m.webDavURL,
FileSystem: u.fileSystem,
LockSystem: webdav.NewMemLS(),
}
}
}
// SetScope updates a user scope and its virtual file system.
// If the user string is blank, it will change the base scope.
func (m *FileManager) SetScope(scope string, username string) error {
@ -188,14 +151,8 @@ func (m *FileManager) SetScope(scope string, username string) error {
}
}
u.scope = strings.TrimSuffix(scope, "/")
u.fileSystem = webdav.Dir(u.scope)
u.handler = &webdav.Handler{
Prefix: m.webDavURL,
FileSystem: u.fileSystem,
LockSystem: webdav.NewMemLS(),
}
u.Scope = strings.TrimSuffix(scope, "/")
u.fileSystem = webdav.Dir(u.Scope)
return nil
}
@ -208,9 +165,8 @@ func (m *FileManager) NewUser(username string) error {
}
m.Users[username] = &User{
scope: m.User.scope,
fileSystem: m.User.fileSystem,
handler: m.User.handler,
Scope: m.User.Scope,
Rules: m.User.Rules,
AllowNew: m.User.AllowNew,
AllowEdit: m.User.AllowEdit,
@ -252,8 +208,3 @@ func (u User) Allowed(url string) bool {
return true
}
// Scope returns the user scope.
func (u User) Scope() string {
return u.scope
}

250
http.go
View File

@ -1,200 +1,98 @@
package filemanager
import (
"errors"
"encoding/json"
"html/template"
"net/http"
"os"
"path/filepath"
"strings"
)
// assetsURL is the url where static assets are served.
const assetsURL = "/_"
// requestContext contains the needed information to make handlers work.
type requestContext struct {
us *User
fm *FileManager
fi *file
pg *page
}
// serveHTTP is the main entry point of this HTML application.
func serveHTTP(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var (
code int
err error
)
// Checks if the URL contains the baseURL and strips it. Otherwise, it just
// returns a 404 error because we're not supposed to be here!
p := strings.TrimPrefix(r.URL.Path, c.fm.baseURL)
// Checks if the URL contains the baseURL. If so, it strips it. Otherwise,
// it throws an error.
if p := strings.TrimPrefix(r.URL.Path, c.fm.baseURL); len(p) < len(r.URL.Path) || len(c.fm.baseURL) == 0 {
r.URL.Path = p
} else {
if len(p) >= len(r.URL.Path) && c.fm.baseURL != "" {
return http.StatusNotFound, nil
}
// Checks if the URL matches the Assets URL. Returns the asset if the
// method is GET and Status Forbidden otherwise.
if matchURL(r.URL.Path, assetsURL+"/") {
if r.Method == http.MethodGet {
r.URL.Path = strings.TrimPrefix(r.URL.Path, assetsURL)
c.fm.static.ServeHTTP(w, r)
return 0, nil
r.URL.Path = p
// Check if this request is made to the service worker. If so,
// pass it through a template to add the needed variables.
if r.URL.Path == "/sw.js" {
return renderFile(
w,
c.fm.assets.MustString(r.URL.Path),
"application/javascript",
c.fm.RootURL(),
)
}
return http.StatusForbidden, nil
// Checks if this request is made to the static assets folder. If so, and
// if it is a GET request, returns with the asset. Otherwise, returns
// a status not implemented.
if matchURL(r.URL.Path, "/static") {
if r.Method != http.MethodGet {
return http.StatusNotImplemented, nil
}
username, _, _ := r.BasicAuth()
if _, ok := c.fm.Users[username]; ok {
c.us = c.fm.Users[username]
} else {
c.us = c.fm.User
return staticHandler(c, w, r)
}
// Checks if the request URL is for the WebDav server.
if matchURL(r.URL.Path, c.fm.webDavURL) {
return serveWebDAV(c, w, r)
// Checks if this request is made to the API and directs to the
// API handler if so.
if matchURL(r.URL.Path, "/api") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
return serveAPI(c, w, r)
}
// Checks if this request is made to the base path /files. If so,
// shows the index.html page.
if matchURL(r.URL.Path, "/files") {
w.Header().Set("x-frame-options", "SAMEORIGIN")
w.Header().Set("x-content-type", "nosniff")
w.Header().Set("x-xss-protection", "1; mode=block")
// Checks if the User is allowed to access this file
if !c.us.Allowed(r.URL.Path) {
if r.Method == http.MethodGet {
return htmlError(
w, http.StatusForbidden,
errors.New("You don't have permission to access this page"),
return renderFile(
w,
c.fm.assets.MustString("index.html"),
"text/html",
c.fm.RootURL(),
)
}
return http.StatusForbidden, nil
}
if r.URL.Query().Get("search") != "" {
return search(c, w, r)
}
if r.URL.Query().Get("command") != "" {
return command(c, w, r)
}
if r.Method == http.MethodGet {
var f *file
// Obtains the information of the directory/file.
f, err = getInfo(r.URL, c.fm, c.us)
if err != nil {
if r.Method == http.MethodGet {
return htmlError(w, code, err)
}
code = errorToHTTP(err, false)
return code, err
}
c.fi = f
// If it's a dir and the path doesn't end with a trailing slash,
// redirect the user.
if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, c.fm.RootURL()+r.URL.Path+"/", http.StatusTemporaryRedirect)
http.Redirect(w, r, c.fm.RootURL()+"/files"+r.URL.Path, http.StatusTemporaryRedirect)
return 0, nil
}
switch {
case r.URL.Query().Get("download") != "":
code, err = serveDownload(c, w, r)
case !f.IsDir && r.URL.Query().Get("checksum") != "":
code, err = serveChecksum(c, w, r)
case r.URL.Query().Get("raw") == "true" && !f.IsDir:
http.ServeFile(w, r, f.Path)
code, err = 0, nil
default:
code, err = serveDefault(c, w, r)
}
if err != nil {
code, err = htmlError(w, code, err)
}
return code, err
}
return http.StatusNotImplemented, nil
}
// serveWebDAV handles the webDAV route of the File Manager.
func serveWebDAV(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var err error
// Checks for user permissions relatively to this path.
if !c.us.Allowed(strings.TrimPrefix(r.URL.Path, c.fm.webDavURL)) {
return http.StatusForbidden, nil
}
switch r.Method {
case "GET", "HEAD":
// Excerpt from RFC4918, section 9.4:
//
// GET, when applied to a collection, may return the contents of an
// "index.html" resource, a human-readable view of the contents of
// the collection, or something else altogether.
//
// It was decided on https://github.com/hacdias/caddy-filemanager/issues/85
// that GET, for collections, will return the same as PROPFIND method.
path := strings.Replace(r.URL.Path, c.fm.webDavURL, "", 1)
path = c.us.scope + "/" + path
path = filepath.Clean(path)
var i os.FileInfo
i, err = os.Stat(path)
if err != nil {
// Is there any error? WebDav will handle it... no worries.
break
}
if i.IsDir() {
r.Method = "PROPFIND"
if r.Method == "HEAD" {
w = newResponseWriterNoBody(w)
}
}
case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
if !c.us.AllowEdit {
return http.StatusForbidden, nil
}
case "MKCOL", "COPY":
if !c.us.AllowNew {
return http.StatusForbidden, nil
}
}
// Execute beforeSave if it is a PUT request.
if r.Method == http.MethodPut {
if err = c.fm.BeforeSave(r, c.fm, c.us); err != nil {
return http.StatusInternalServerError, err
}
}
c.fm.handler.ServeHTTP(w, r)
// Execute afterSave if it is a PUT request.
if r.Method == http.MethodPut {
if err = c.fm.AfterSave(r, c.fm, c.us); err != nil {
return http.StatusInternalServerError, err
}
}
// staticHandler handles the static assets path.
func staticHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/static/manifest.json" {
http.FileServer(c.fm.assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil
}
return renderFile(
w,
c.fm.assets.MustString(r.URL.Path),
"application/json",
c.fm.RootURL(),
)
}
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
func serveChecksum(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("checksum")
func checksumHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo")
val, err := c.fi.Checksum(query)
if err == errInvalidOption {
@ -207,30 +105,32 @@ func serveChecksum(c *requestContext, w http.ResponseWriter, r *http.Request) (i
return 0, nil
}
// responseWriterNoBody is a wrapper used to suprress the body of the response
// to a request. Mainly used for HEAD requests.
type responseWriterNoBody struct {
http.ResponseWriter
}
// renderFile renders a file using a template with some needed variables.
func renderFile(w http.ResponseWriter, file string, contentType string, baseURL string) (int, error) {
tpl := template.Must(template.New("file").Parse(file))
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
// newResponseWriterNoBody creates a new responseWriterNoBody.
func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
return &responseWriterNoBody{w}
}
err := tpl.Execute(w, map[string]string{"BaseURL": baseURL})
if err != nil {
return http.StatusInternalServerError, err
}
// Header executes the Header method from the http.ResponseWriter.
func (w responseWriterNoBody) Header() http.Header {
return w.ResponseWriter.Header()
}
// Write suprresses the body.
func (w responseWriterNoBody) Write(data []byte) (int, error) {
return 0, nil
}
// WriteHeader writes the header to the http.ResponseWriter.
func (w responseWriterNoBody) WriteHeader(statusCode int) {
w.ResponseWriter.WriteHeader(statusCode)
// renderJSON prints the JSON version of data to the browser.
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
marsh, err := json.Marshal(data)
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 0, nil
}
// matchURL checks if the first URL matches the second.
@ -244,6 +144,8 @@ func matchURL(first, second string) bool {
// errorToHTTP converts errors to HTTP Status Code.
func errorToHTTP(err error, gone bool) int {
switch {
case err == nil:
return http.StatusOK
case os.IsPermission(err):
return http.StatusForbidden
case os.IsNotExist(err):

195
page.go
View File

@ -1,195 +0,0 @@
package filemanager
import (
"bytes"
"encoding/json"
"html/template"
"net/http"
"strconv"
"strings"
)
// functions contains the non-standard functions that are available
// to use on the HTML templates.
var functions = template.FuncMap{
"CSS": func(s string) template.CSS {
return template.CSS(s)
},
"Marshal": func(v interface{}) template.JS {
a, _ := json.Marshal(v)
return template.JS(a)
},
}
// page contains the information needed to fill a page template.
type page struct {
User *User `json:"-"`
BaseURL string `json:"-"`
WebDavURL string `json:"-"`
Kind string `json:"kind"`
Data *file `json:"data"`
}
/*
// breadcrumbItem contains the Name and the URL of a breadcrumb piece.
type breadcrumbItem struct {
Name string
URL string
}
// BreadcrumbMap returns p.Path where every element is a map
// of URLs and path segment names.
func (p page) BreadcrumbMap() []breadcrumbItem {
// TODO: when it is preview alongside with listing!!!!!!!!!!
result := []breadcrumbItem{}
if len(p.Path) == 0 {
return result
}
// skip trailing slash
lpath := p.Path
if lpath[len(lpath)-1] == '/' {
lpath = lpath[:len(lpath)-1]
}
parts := strings.Split(lpath, "/")
for i, part := range parts {
if i == len(parts)-1 {
continue
}
if i == 0 && part == "" {
result = append([]breadcrumbItem{{
Name: "/",
URL: "/",
}}, result...)
continue
}
result = append([]breadcrumbItem{{
Name: part,
URL: strings.Join(parts[:i+1], "/") + "/",
}}, result...)
}
return result
}
// PreviousLink returns the URL of the previous folder.
func (p page) PreviousLink() string {
path := strings.TrimSuffix(p.Path, "/")
path = strings.TrimPrefix(path, "/")
path = p.BaseURL + "/" + path
path = path[0 : len(path)-len(p.Name)]
if len(path) < len(p.BaseURL+"/") {
return ""
}
return path
} */
func (p page) Render(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if strings.Contains(r.Header.Get("Accept"), "application/json") {
marsh, err := json.MarshalIndent(p, "", " ")
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 0, nil
}
var tpl *template.Template
// Get the template from the assets
file, err := c.fm.templates.String("index.html")
// Check if there is some error. If so, the template doesn't exist
if err != nil {
return http.StatusInternalServerError, err
}
tpl, err = template.New("index").Funcs(functions).Parse(file)
if err != nil {
return http.StatusInternalServerError, err
}
buf := &bytes.Buffer{}
err = tpl.Execute(buf, p)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, err = buf.WriteTo(w)
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
// htmlError prints the error page
func htmlError(w http.ResponseWriter, code int, err error) (int, error) {
tpl := errTemplate
tpl = strings.Replace(tpl, "TITLE", strconv.Itoa(code)+" "+http.StatusText(code), -1)
tpl = strings.Replace(tpl, "CODE", err.Error(), -1)
_, err = w.Write([]byte(tpl))
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
const errTemplate = `<!DOCTYPE html>
<html>
<head>
<title>TITLE</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<style>
html {
background-color: #2196f3;
color: #fff;
font-family: sans-serif;
}
code {
background-color: rgba(0,0,0,0.1);
border-radius: 5px;
padding: 1em;
display: block;
box-sizing: border-box;
}
.center {
max-width: 40em;
margin: 2em auto 0;
}
a {
text-decoration: none;
color: #eee;
font-weight: bold;
}
p {
line-height: 1.3;
}
</style>
</head>
<body>
<div class="center">
<h1>TITLE</h1>
<p>Try reloading the page or hitting the back button. If this error persists, it seems that you may have found a bug! Please create an issue at <a href="https://github.com/hacdias/caddy-filemanager/issues">hacdias/caddy-filemanager</a> repository on GitHub with the code below.</p>
<code>CODE</code>
</div>
</html>`

View File

@ -73,7 +73,7 @@ func search(c *requestContext, w http.ResponseWriter, r *http.Request) (int, err
search = parseSearch(value)
scope := strings.TrimPrefix(r.URL.Path, "/")
scope = "/" + scope
scope = c.us.scope + scope
scope = c.us.Scope + scope
scope = strings.Replace(scope, "\\", "/", -1)
scope = filepath.Clean(scope)

142
serve.go
View File

@ -1,142 +0,0 @@
package filemanager
import (
"net/http"
)
func serveDefault(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var err error
// Starts building the page.
c.pg = &page{
User: c.us,
BaseURL: c.fm.RootURL(),
WebDavURL: c.fm.WebDavURL(),
Data: c.fi,
}
// If it is a dir, go and serve the listing.
if c.fi.IsDir {
return serveListing(c, w, r)
}
// Tries to get the file type.
if err = c.fi.RetrieveFileType(); err != nil {
return errorToHTTP(err, true), err
}
// If it can't be edited or the user isn't allowed to,
// serve it as a listing, with a preview of the file.
if !c.fi.CanBeEdited() || !c.us.AllowEdit {
c.pg.Kind = "preview"
} else {
// Otherwise, we just bring the editor in!
c.pg.Kind = "editor"
err = c.fi.getEditor(r)
if err != nil {
return http.StatusInternalServerError, err
}
}
return c.pg.Render(c, w, r)
}
// serveListing presents the user with a listage of a directory folder.
func serveListing(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var err error
c.pg.Kind = "listing"
err = c.fi.getListing(c, r)
if err != nil {
return errorToHTTP(err, true), err
}
listing := c.fi.listing
cookieScope := c.fm.RootURL()
if cookieScope == "" {
cookieScope = "/"
}
// Copy the query values into the Listing struct
listing.Sort, listing.Order, err = handleSortOrder(w, r, cookieScope)
if err != nil {
return http.StatusBadRequest, err
}
listing.ApplySort()
listing.Display = displayMode(w, r, cookieScope)
return c.pg.Render(c, w, r)
}
// displayMode obtaisn the display mode from URL, or from the
// cookie.
func displayMode(w http.ResponseWriter, r *http.Request, scope string) string {
displayMode := r.URL.Query().Get("display")
if displayMode == "" {
if displayCookie, err := r.Cookie("display"); err == nil {
displayMode = displayCookie.Value
}
}
if displayMode == "" || (displayMode != "mosaic" && displayMode != "list") {
displayMode = "mosaic"
}
http.SetCookie(w, &http.Cookie{
Name: "display",
Value: displayMode,
MaxAge: 31536000,
Path: scope,
Secure: r.TLS != nil,
})
return displayMode
}
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
// and reads 'limit' if given. The latter is 0 if not given. Sets cookies.
func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, err error) {
sort = r.URL.Query().Get("sort")
order = r.URL.Query().Get("order")
// 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,
MaxAge: 31536000,
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,
MaxAge: 31536000,
Path: scope,
Secure: r.TLS != nil,
})
}
return
}