Updates
Former-commit-id: 8220f5660a75af69ee6358af3782416dae9aa185 [formerly 098749492c954879b5a01324076804e74947c780] [formerly 34f6dda3fafc72730b4e44d0c23ade6940bb90d4 [formerly 4b3ebca48c
]]
Former-commit-id: 722e71cd19b9ecceb627cc1e65572839bd2a55c8 [formerly 23d5ac81b9ff1f39b51cc042eb2ac12d2e5da6fc]
Former-commit-id: c4e5ea0d6db7edcfdc3b551e0c43d1c77ef587c3
pull/726/head
parent
69a96e56b7
commit
1e99d3d7c1
46
assets.go
46
assets.go
|
@ -1,46 +0,0 @@
|
|||
package filemanager
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// assetsURL is the url where static assets are served.
|
||||
const assetsURL = "/_internal"
|
||||
|
||||
// Serve provides the needed assets for the front-end
|
||||
func serveAssets(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// gets the filename to be used with Assets function
|
||||
filename := strings.TrimPrefix(r.URL.Path, assetsURL)
|
||||
|
||||
var file []byte
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(filename, "/css"):
|
||||
filename = strings.Replace(filename, "/css/", "", 1)
|
||||
file, err = c.fm.assets.css.Bytes(filename)
|
||||
case strings.HasPrefix(filename, "/js"):
|
||||
filename = strings.Replace(filename, "/js/", "", 1)
|
||||
file, err = c.fm.assets.js.Bytes(filename)
|
||||
default:
|
||||
err = errors.New("not found")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
// Get the file extension and its mimetype
|
||||
extension := filepath.Ext(filename)
|
||||
mediatype := mime.TypeByExtension(extension)
|
||||
|
||||
// Write the header with the Content-Type and write the file
|
||||
// content to the buffer
|
||||
w.Header().Set("Content-Type", mediatype)
|
||||
w.Write(file)
|
||||
return 200, nil
|
||||
}
|
50
checksum.go
50
checksum.go
|
@ -1,50 +0,0 @@
|
|||
package filemanager
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
e "errors"
|
||||
"hash"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 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")
|
||||
|
||||
file, err := os.Open(c.fi.Path)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
var h hash.Hash
|
||||
|
||||
switch query {
|
||||
case "md5":
|
||||
h = md5.New()
|
||||
case "sha1":
|
||||
h = sha1.New()
|
||||
case "sha256":
|
||||
h = sha256.New()
|
||||
case "sha512":
|
||||
h = sha512.New()
|
||||
default:
|
||||
return http.StatusBadRequest, e.New("Unknown HASH type")
|
||||
}
|
||||
|
||||
_, err = io.Copy(h, file)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
val := hex.EncodeToString(h.Sum(nil))
|
||||
w.Write([]byte(val))
|
||||
return http.StatusOK, nil
|
||||
}
|
95
download.go
95
download.go
|
@ -1,95 +0,0 @@
|
|||
package filemanager
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/archiver"
|
||||
)
|
||||
|
||||
// download creates an archive in one of the supported formats (zip, tar,
|
||||
// tar.gz or tar.bz2) and sends it to be downloaded.
|
||||
func download(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
query := r.URL.Query().Get("download")
|
||||
|
||||
if !c.fi.IsDir {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+c.fi.Name)
|
||||
http.ServeFile(w, r, c.fi.Path)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
files := []string{}
|
||||
names := strings.Split(r.URL.Query().Get("files"), ",")
|
||||
|
||||
if len(names) != 0 {
|
||||
for _, name := range names {
|
||||
name, err := url.QueryUnescape(name)
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
files = append(files, filepath.Join(c.fi.Path, name))
|
||||
}
|
||||
|
||||
} else {
|
||||
files = append(files, c.fi.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)
|
||||
case "tarxz":
|
||||
extension, err = ".tar.xz", archiver.TarXZ.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
|
||||
}
|
||||
|
||||
name := c.fi.Name
|
||||
if name == "." || name == "" {
|
||||
name = "download"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+name+extension)
|
||||
io.Copy(w, file)
|
||||
return http.StatusOK, nil
|
||||
}
|
44
editor.go
44
editor.go
|
@ -87,50 +87,6 @@ Error:
|
|||
return e, nil
|
||||
}
|
||||
|
||||
// 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(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var err error
|
||||
|
||||
if err = c.fi.RetrieveFileType(); err != nil {
|
||||
return errorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
p := &page{
|
||||
Name: c.fi.Name,
|
||||
Path: c.fi.VirtualPath,
|
||||
IsDir: false,
|
||||
Data: c.fi,
|
||||
User: c.us,
|
||||
PrefixURL: c.fm.prefixURL,
|
||||
BaseURL: c.fm.RootURL(),
|
||||
WebDavURL: c.fm.WebDavURL(),
|
||||
}
|
||||
|
||||
// If the request accepts JSON, we send the file information.
|
||||
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||
return p.PrintAsJSON(w)
|
||||
}
|
||||
|
||||
if c.fi.Type == "text" {
|
||||
if err = c.fi.Read(); err != nil {
|
||||
return errorToHTTP(err, true), err
|
||||
}
|
||||
}
|
||||
|
||||
if c.fi.CanBeEdited() && c.us.AllowEdit {
|
||||
p.Data, err = getEditor(r, c.fi)
|
||||
p.Editor = true
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return p.PrintAsHTML(w, c.fm.assets.templates, "frontmatter", "editor")
|
||||
}
|
||||
|
||||
return p.PrintAsHTML(w, c.fm.assets.templates, "single")
|
||||
}
|
||||
|
||||
func editorClass(mode string) string {
|
||||
switch mode {
|
||||
case "json", "toml", "yaml":
|
||||
|
|
43
file.go
43
file.go
|
@ -1,6 +1,14 @@
|
|||
package filemanager
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
|
@ -13,6 +21,10 @@ import (
|
|||
humanize "github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidOption = errors.New("Invalid option")
|
||||
)
|
||||
|
||||
// fileInfo contains the information about a particular file or directory.
|
||||
type fileInfo struct {
|
||||
// Used to store the file's content temporarily.
|
||||
|
@ -149,6 +161,37 @@ func (i *fileInfo) Read() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (i fileInfo) Checksum(kind string) (string, error) {
|
||||
file, err := os.Open(i.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
var h hash.Hash
|
||||
|
||||
switch kind {
|
||||
case "md5":
|
||||
h = md5.New()
|
||||
case "sha1":
|
||||
h = sha1.New()
|
||||
case "sha256":
|
||||
h = sha256.New()
|
||||
case "sha512":
|
||||
h = sha512.New()
|
||||
default:
|
||||
return "", errInvalidOption
|
||||
}
|
||||
|
||||
_, err = io.Copy(h, file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// StringifyContent returns a string with the file content.
|
||||
func (i fileInfo) StringifyContent() string {
|
||||
return string(i.content)
|
||||
|
|
516
http.go
516
http.go
|
@ -1,66 +1,496 @@
|
|||
package filemanager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/archiver"
|
||||
)
|
||||
|
||||
// requestContext contains the needed information to make handlers work.
|
||||
type requestContext struct {
|
||||
us *User
|
||||
fm *FileManager
|
||||
fi *fileInfo
|
||||
// assetsURL is the url where static assets are served.
|
||||
const assetsURL = "/_internal"
|
||||
|
||||
func serveHTTP(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var (
|
||||
code int
|
||||
err error
|
||||
)
|
||||
|
||||
// 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) {
|
||||
r.URL.Path = p
|
||||
} else {
|
||||
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 {
|
||||
return serveAssets(c, w, r)
|
||||
}
|
||||
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
username, _, _ := r.BasicAuth()
|
||||
if _, ok := c.fm.Users[username]; ok {
|
||||
c.us = c.fm.Users[username]
|
||||
} else {
|
||||
c.us = c.fm.User
|
||||
}
|
||||
|
||||
// Checks if the request URL is for the WebDav server.
|
||||
if matchURL(r.URL.Path, c.fm.webDavURL) {
|
||||
return serveWebDAV(c, w, r)
|
||||
}
|
||||
|
||||
w.Header().Set("x-frame-options", "SAMEORIGIN")
|
||||
w.Header().Set("x-content-type", "nosniff")
|
||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||
|
||||
// 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 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 *fileInfo
|
||||
|
||||
// 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)
|
||||
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
|
||||
case f.IsDir:
|
||||
code, err = serveListing(c, w, r)
|
||||
default:
|
||||
code, err = serveSingle(c, w, r)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
code, err = htmlError(w, code, err)
|
||||
}
|
||||
|
||||
return code, err
|
||||
}
|
||||
|
||||
return http.StatusNotImplemented, 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
|
||||
}
|
||||
// serveWebDAV handles the webDAV route of the File Manager.
|
||||
func serveWebDAV(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var err error
|
||||
|
||||
// newResponseWriterNoBody creates a new responseWriterNoBody.
|
||||
func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
|
||||
return &responseWriterNoBody{w}
|
||||
}
|
||||
// Checks for user permissions relatively to this path.
|
||||
if !c.us.Allowed(strings.TrimPrefix(r.URL.Path, c.fm.webDavURL)) {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Header executes the Header method from the http.ResponseWriter.
|
||||
func (w responseWriterNoBody) Header() http.Header {
|
||||
return w.ResponseWriter.Header()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Preprocess the PUT request if it's the case
|
||||
if r.Method == http.MethodPut {
|
||||
if err = c.fm.BeforeSave(r, c.fm, c.us); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
if put(c, w, r) != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
c.fm.handler.ServeHTTP(w, r)
|
||||
if err = c.fm.AfterSave(r, c.fm, c.us); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
// Serve provides the needed assets for the front-end
|
||||
func serveAssets(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// gets the filename to be used with Assets function
|
||||
filename := strings.TrimPrefix(r.URL.Path, assetsURL)
|
||||
|
||||
// matchURL checks if the first URL matches the second.
|
||||
func matchURL(first, second string) bool {
|
||||
first = strings.ToLower(first)
|
||||
second = strings.ToLower(second)
|
||||
var file []byte
|
||||
var err error
|
||||
|
||||
return strings.HasPrefix(first, second)
|
||||
}
|
||||
|
||||
// errorToHTTP converts errors to HTTP Status Code.
|
||||
func errorToHTTP(err error, gone bool) int {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden
|
||||
case os.IsNotExist(err):
|
||||
if !gone {
|
||||
return http.StatusNotFound
|
||||
case strings.HasPrefix(filename, "/css"):
|
||||
filename = strings.Replace(filename, "/css/", "", 1)
|
||||
file, err = c.fm.assets.css.Bytes(filename)
|
||||
case strings.HasPrefix(filename, "/js"):
|
||||
filename = strings.Replace(filename, "/js/", "", 1)
|
||||
file, err = c.fm.assets.js.Bytes(filename)
|
||||
default:
|
||||
err = errors.New("not found")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
// Get the file extension and its mimetype
|
||||
extension := filepath.Ext(filename)
|
||||
mediatype := mime.TypeByExtension(extension)
|
||||
|
||||
// Write the header with the Content-Type and write the file
|
||||
// content to the buffer
|
||||
w.Header().Set("Content-Type", mediatype)
|
||||
w.Write(file)
|
||||
return 200, nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
val, err := c.fi.Checksum(query)
|
||||
if err == errInvalidOption {
|
||||
return http.StatusBadRequest, err
|
||||
} else if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
w.Write([]byte(val))
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// 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(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var err error
|
||||
|
||||
if err = c.fi.RetrieveFileType(); err != nil {
|
||||
return errorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
p := &page{
|
||||
Name: c.fi.Name,
|
||||
Path: c.fi.VirtualPath,
|
||||
IsDir: false,
|
||||
Data: c.fi,
|
||||
User: c.us,
|
||||
PrefixURL: c.fm.prefixURL,
|
||||
BaseURL: c.fm.RootURL(),
|
||||
WebDavURL: c.fm.WebDavURL(),
|
||||
}
|
||||
|
||||
// If the request accepts JSON, we send the file information.
|
||||
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||
return p.PrintAsJSON(w)
|
||||
}
|
||||
|
||||
if c.fi.Type == "text" {
|
||||
if err = c.fi.Read(); err != nil {
|
||||
return errorToHTTP(err, true), err
|
||||
}
|
||||
}
|
||||
|
||||
if c.fi.CanBeEdited() && c.us.AllowEdit {
|
||||
p.Data, err = getEditor(r, c.fi)
|
||||
p.Editor = true
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusGone
|
||||
case os.IsExist(err):
|
||||
return http.StatusGone
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
return p.PrintAsHTML(w, c.fm.assets.templates, "frontmatter", "editor")
|
||||
}
|
||||
|
||||
return p.PrintAsHTML(w, c.fm.assets.templates, "single")
|
||||
}
|
||||
|
||||
// serveDownload 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")
|
||||
|
||||
if !c.fi.IsDir {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+c.fi.Name)
|
||||
http.ServeFile(w, r, c.fi.Path)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
files := []string{}
|
||||
names := strings.Split(r.URL.Query().Get("files"), ",")
|
||||
|
||||
if len(names) != 0 {
|
||||
for _, name := range names {
|
||||
name, err := url.QueryUnescape(name)
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
files = append(files, filepath.Join(c.fi.Path, name))
|
||||
}
|
||||
|
||||
} else {
|
||||
files = append(files, c.fi.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)
|
||||
case "tarxz":
|
||||
extension, err = ".tar.xz", archiver.TarXZ.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
|
||||
}
|
||||
|
||||
name := c.fi.Name
|
||||
if name == "." || name == "" {
|
||||
name = "download"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+name+extension)
|
||||
io.Copy(w, file)
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Loads the content of the directory.
|
||||
listing, err := getListing(c.us, c.fi.VirtualPath, c.fm.RootURL()+r.URL.Path)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
cookieScope := c.fm.RootURL()
|
||||
if cookieScope == "" {
|
||||
cookieScope = "/"
|
||||
}
|
||||
|
||||
// Copy the query values into the Listing struct
|
||||
var limit int
|
||||
listing.Sort, listing.Order, limit, err = handleSortOrder(w, r, cookieScope)
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
Path: cookieScope,
|
||||
Secure: r.TLS != nil,
|
||||
})
|
||||
|
||||
p := &page{
|
||||
minimal: r.Header.Get("Minimal") == "true",
|
||||
Name: listing.Name,
|
||||
Path: c.fi.VirtualPath,
|
||||
IsDir: true,
|
||||
User: c.us,
|
||||
PrefixURL: c.fm.prefixURL,
|
||||
BaseURL: c.fm.RootURL(),
|
||||
WebDavURL: c.fm.WebDavURL(),
|
||||
Display: displayMode,
|
||||
Data: listing,
|
||||
}
|
||||
|
||||
return p.PrintAsHTML(w, c.fm.assets.templates, "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
|
||||
}
|
||||
|
|
136
http_listing.go
136
http_listing.go
|
@ -1,136 +0,0 @@
|
|||
package filemanager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// Loads the content of the directory.
|
||||
listing, err := getListing(c.us, c.fi.VirtualPath, c.fm.RootURL()+r.URL.Path)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
cookieScope := c.fm.RootURL()
|
||||
if cookieScope == "" {
|
||||
cookieScope = "/"
|
||||
}
|
||||
|
||||
// Copy the query values into the Listing struct
|
||||
var limit int
|
||||
listing.Sort, listing.Order, limit, err = handleSortOrder(w, r, cookieScope)
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
Path: cookieScope,
|
||||
Secure: r.TLS != nil,
|
||||
})
|
||||
|
||||
p := &page{
|
||||
minimal: r.Header.Get("Minimal") == "true",
|
||||
Name: listing.Name,
|
||||
Path: c.fi.VirtualPath,
|
||||
IsDir: true,
|
||||
User: c.us,
|
||||
PrefixURL: c.fm.prefixURL,
|
||||
BaseURL: c.fm.RootURL(),
|
||||
WebDavURL: c.fm.WebDavURL(),
|
||||
Display: displayMode,
|
||||
Data: listing,
|
||||
}
|
||||
|
||||
return p.PrintAsHTML(w, c.fm.assets.templates, "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
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package filemanager
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// requestContext contains the needed information to make handlers work.
|
||||
type requestContext struct {
|
||||
us *User
|
||||
fm *FileManager
|
||||
fi *fileInfo
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// newResponseWriterNoBody creates a new responseWriterNoBody.
|
||||
func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
|
||||
return &responseWriterNoBody{w}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// matchURL checks if the first URL matches the second.
|
||||
func matchURL(first, second string) bool {
|
||||
first = strings.ToLower(first)
|
||||
second = strings.ToLower(second)
|
||||
|
||||
return strings.HasPrefix(first, second)
|
||||
}
|
||||
|
||||
// errorToHTTP converts errors to HTTP Status Code.
|
||||
func errorToHTTP(err error, gone bool) int {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden
|
||||
case os.IsNotExist(err):
|
||||
if !gone {
|
||||
return http.StatusNotFound
|
||||
}
|
||||
|
||||
return http.StatusGone
|
||||
case os.IsExist(err):
|
||||
return http.StatusGone
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
182
webdav.go
182
webdav.go
|
@ -1,182 +0,0 @@
|
|||
package filemanager
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func serveHTTP(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var (
|
||||
code int
|
||||
err error
|
||||
)
|
||||
|
||||
// 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) {
|
||||
r.URL.Path = p
|
||||
} else {
|
||||
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 {
|
||||
return serveAssets(c, w, r)
|
||||
}
|
||||
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
username, _, _ := r.BasicAuth()
|
||||
if _, ok := c.fm.Users[username]; ok {
|
||||
c.us = c.fm.Users[username]
|
||||
} else {
|
||||
c.us = c.fm.User
|
||||
}
|
||||
|
||||
// Checks if the request URL is for the WebDav server.
|
||||
if matchURL(r.URL.Path, c.fm.webDavURL) {
|
||||
return serveWebDAV(c, w, r)
|
||||
}
|
||||
|
||||
w.Header().Set("x-frame-options", "SAMEORIGIN")
|
||||
w.Header().Set("x-content-type", "nosniff")
|
||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||
|
||||
// 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 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 *fileInfo
|
||||
|
||||
// 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)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case r.URL.Query().Get("download") != "":
|
||||
code, err = download(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
|
||||
case f.IsDir:
|
||||
code, err = serveListing(c, w, r)
|
||||
default:
|
||||
code, err = serveSingle(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
|
||||
}
|
||||
}
|
||||
|
||||
// Preprocess the PUT request if it's the case
|
||||
if r.Method == http.MethodPut {
|
||||
if err = c.fm.BeforeSave(r, c.fm, c.us); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
if put(c, w, r) != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
c.fm.handler.ServeHTTP(w, r)
|
||||
if err = c.fm.AfterSave(r, c.fm, c.us); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
Loading…
Reference in New Issue