Major changes on API
parent
01c78d2b36
commit
1f7974de38
|
@ -1,6 +1,6 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules/
|
node_modules/
|
||||||
_assets/dist_dev/*
|
_assets/dist/*
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
|
@ -6,10 +6,10 @@ module.exports = {
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: '"production"'
|
NODE_ENV: '"production"'
|
||||||
},
|
},
|
||||||
index: path.resolve(__dirname, '../dist/templates/index.html'),
|
index: path.resolve(__dirname, '../dist/index.html'),
|
||||||
assetsRoot: path.resolve(__dirname, '../dist'),
|
assetsRoot: path.resolve(__dirname, '../dist'),
|
||||||
assetsSubDirectory: '_',
|
assetsSubDirectory: 'static',
|
||||||
assetsPublicPath: '{{ .BaseURL }}',
|
assetsPublicPath: '{{ .BaseURL }}/',
|
||||||
productionSourceMap: true,
|
productionSourceMap: true,
|
||||||
// Gzip off by default as many popular static hosts such as
|
// Gzip off by default as many popular static hosts such as
|
||||||
// Surge or Netlify already gzip all static assets for you.
|
// Surge or Netlify already gzip all static assets for you.
|
||||||
|
@ -27,10 +27,10 @@ module.exports = {
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: '"development"'
|
NODE_ENV: '"development"'
|
||||||
},
|
},
|
||||||
index: path.resolve(__dirname, '../dist_dev/templates/index.html'),
|
index: path.resolve(__dirname, '../dist/index.html'),
|
||||||
assetsRoot: path.resolve(__dirname, '../dist_dev/'),
|
assetsRoot: path.resolve(__dirname, '../dist/'),
|
||||||
assetsSubDirectory: '_',
|
assetsSubDirectory: 'static',
|
||||||
assetsPublicPath: '{{ .BaseURL }}',
|
assetsPublicPath: '{{ .BaseURL }}/',
|
||||||
produceSourceMap: true
|
produceSourceMap: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
if ('serviceWorker' in navigator &&
|
if ('serviceWorker' in navigator &&
|
||||||
(window.location.protocol === 'https:' || isLocalhost)) {
|
(window.location.protocol === 'https:' || isLocalhost)) {
|
||||||
navigator.serviceWorker.register('{{ .BaseURL }}/_/service-worker.js')
|
navigator.serviceWorker.register('{{ .BaseURL }}/sw.js')
|
||||||
.then(function(registration) {
|
.then(function(registration) {
|
||||||
// updatefound is fired if service-worker.js changes.
|
// updatefound is fired if service-worker.js changes.
|
||||||
registration.onupdatefound = function() {
|
registration.onupdatefound = function() {
|
||||||
|
|
|
@ -6,6 +6,7 @@ exports.assetsPath = function (_path) {
|
||||||
var assetsSubDirectory = process.env.NODE_ENV === 'production'
|
var assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||||
? config.build.assetsSubDirectory
|
? config.build.assetsSubDirectory
|
||||||
: config.dev.assetsSubDirectory
|
: config.dev.assetsSubDirectory
|
||||||
|
|
||||||
return path.posix.join(assetsSubDirectory, _path)
|
return path.posix.join(assetsSubDirectory, _path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,8 +98,9 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||||
]),
|
]),
|
||||||
// service worker caching
|
// service worker caching
|
||||||
new SWPrecacheWebpackPlugin({
|
new SWPrecacheWebpackPlugin({
|
||||||
cacheId: 'my-vue-app',
|
cacheId: 'File Manager',
|
||||||
filename: 'service-worker.js',
|
filename: 'sw.js',
|
||||||
|
replacePrefix: '{{ .BaseURL }}/',
|
||||||
staticFileGlobs: ['dist/**/*.{js,html,css}'],
|
staticFileGlobs: ['dist/**/*.{js,html,css}'],
|
||||||
minify: true,
|
minify: true,
|
||||||
stripPrefix: 'dist/'
|
stripPrefix: 'dist/'
|
||||||
|
|
|
@ -4,27 +4,28 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
|
<meta name="base" content="{{ .BaseURL }}">
|
||||||
<title>File Manager</title>
|
<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="32x32" href="{{ .BaseURL }}/static/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="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
|
||||||
<!--[if IE]><link rel="shortcut icon" href="/static/img/icons/favicon.ico"><![endif]-->
|
<!--[if IE]><link rel="shortcut icon" href="/static/img/icons/favicon.ico"><![endif]-->
|
||||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
<link rel="manifest" href="{{ .BaseURL }}/_/manifest.json">
|
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
|
||||||
<meta name="theme-color" content="#4DBA87">
|
<meta name="theme-color" content="#2979ff">
|
||||||
|
|
||||||
<!-- Add to home screen for Safari on iOS -->
|
<!-- Add to home screen for Safari on iOS -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<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-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="assets">
|
<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 -->
|
<!-- Add to home screen for Windows -->
|
||||||
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/_/img/icons/msapplication-icon-144x144.png">
|
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
|
||||||
<meta name="msapplication-TileColor" content="#000000">
|
<meta name="msapplication-TileColor" content="#2979ff">
|
||||||
|
|
||||||
<% for (var chunk of webpack.chunks) {
|
<% for (var chunk of webpack.chunks) {
|
||||||
for (var file of chunk.files) {
|
for (var file of chunk.files) {
|
||||||
if (file.match(/\.(js|css)$/)) { %>
|
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>
|
<style>
|
||||||
#loading {
|
#loading {
|
||||||
|
@ -88,20 +89,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{{- if ne .User.StyleSheet "" -}}
|
|
||||||
<style>{{ CSS .User.StyleSheet }}</style>
|
|
||||||
{{- end -}}
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>
|
|
||||||
var info = {
|
|
||||||
user: JSON.parse('{{ Marshal .User }}'),
|
|
||||||
req: JSON.parse('{{ Marshal . }}'),
|
|
||||||
webdavURL: "{{ .WebDavURL }}",
|
|
||||||
baseURL: "{{.BaseURL}}"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
<div id="loading">
|
<div id="loading">
|
||||||
|
|
|
@ -3,18 +3,18 @@
|
||||||
"short_name": "File Manager",
|
"short_name": "File Manager",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/static/img/icons/android-chrome-192x192.png",
|
"src": "{{ .BaseURL }}/static/img/icons/android-chrome-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/static/img/icons/android-chrome-512x512.png",
|
"src": "{{ .BaseURL }}/static/img/icons/android-chrome-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": "/index.html",
|
"start_url": "{{ .BaseURL }}/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#000000",
|
"background_color": "#ffffff",
|
||||||
"theme_color": "#4DBA87"
|
"theme_color": "#2979ff"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -94,7 +94,6 @@ func parse(c *caddy.Controller) ([]*config, error) {
|
||||||
|
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
m.baseURL = args[0]
|
m.baseURL = args[0]
|
||||||
m.webDavURL = "/webdav"
|
|
||||||
m.SetBaseURL(args[0])
|
m.SetBaseURL(args[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,13 +107,6 @@ func parse(c *caddy.Controller) ([]*config, error) {
|
||||||
if m.AfterSave, err = makeCommand(c, m); err != nil {
|
if m.AfterSave, err = makeCommand(c, m); err != nil {
|
||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
case "webdav":
|
|
||||||
if !c.NextArg() {
|
|
||||||
return configs, c.ArgErr()
|
|
||||||
}
|
|
||||||
|
|
||||||
m.webDavURL = "c.Val()"
|
|
||||||
m.SetWebDavURL(c.Val())
|
|
||||||
case "show":
|
case "show":
|
||||||
if !c.NextArg() {
|
if !c.NextArg() {
|
||||||
return configs, c.ArgErr()
|
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 {
|
fn = func(r *http.Request, c *FileManager, u *User) error {
|
||||||
path := strings.Replace(r.URL.Path, m.baseURL+m.webDavURL, "", 1)
|
path := strings.Replace(r.URL.Path, m.baseURL+m.webDavURL, "", 1)
|
||||||
path = u.Scope() + "/" + path
|
path = u.Scope + "/" + path
|
||||||
path = filepath.Clean(path)
|
path = filepath.Clean(path)
|
||||||
|
|
||||||
for i := range args {
|
for i := range args {
|
||||||
|
|
|
@ -77,7 +77,7 @@ func command(c *requestContext, w http.ResponseWriter, r *http.Request) (int, er
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the path and initializes a buffer.
|
// 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)
|
path = filepath.Clean(path)
|
||||||
buff := new(bytes.Buffer)
|
buff := new(bytes.Buffer)
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,10 @@ import (
|
||||||
"github.com/mholt/archiver"
|
"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.
|
// tar.gz or tar.bz2) and sends it to be downloaded.
|
||||||
func serveDownload(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func downloadHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
query := r.URL.Query().Get("download")
|
query := r.URL.Query().Get("format")
|
||||||
|
|
||||||
if !c.fi.IsDir {
|
if !c.fi.IsDir {
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+c.fi.Name)
|
w.Header().Set("Content-Disposition", "attachment; filename="+c.fi.Name)
|
||||||
|
|
22
file.go
22
file.go
|
@ -29,6 +29,8 @@ var (
|
||||||
|
|
||||||
// file contains the information about a particular file or directory.
|
// file contains the information about a particular file or directory.
|
||||||
type file struct {
|
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.
|
// The name of the file.
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
// The Size of the file.
|
// The Size of the file.
|
||||||
|
@ -79,15 +81,13 @@ type listing struct {
|
||||||
func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
|
func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
i := &file{URL: c.RootURL() + url.Path}
|
i := &file{
|
||||||
i.VirtualPath = url.Path
|
URL: c.RootURL() + "/files" + url.Path,
|
||||||
i.VirtualPath = strings.TrimPrefix(i.VirtualPath, "/")
|
VirtualPath: url.Path,
|
||||||
i.VirtualPath = "/" + i.VirtualPath
|
Path: filepath.Join(u.Scope, url.Path),
|
||||||
|
}
|
||||||
|
|
||||||
i.Path = u.scope + i.VirtualPath
|
info, err := u.fileSystem.Stat(context.TODO(), i.Path)
|
||||||
i.Path = filepath.Clean(i.Path)
|
|
||||||
|
|
||||||
info, err := os.Stat(i.Path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return i, err
|
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.
|
// getListing gets the information about a specific directory and its files.
|
||||||
func (i *file) getListing(c *requestContext, r *http.Request) error {
|
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
|
// Gets the directory information using the Virtual File System of
|
||||||
// the user configuration.
|
// the user configuration.
|
||||||
f, err := c.us.fileSystem.OpenFile(context.TODO(), c.fi.VirtualPath, os.O_RDONLY, 0)
|
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
|
// Absolute URL
|
||||||
url := url.URL{Path: baseURL + name}
|
url := url.URL{Path: i.URL + name}
|
||||||
|
|
||||||
i := file{
|
i := file{
|
||||||
Name: f.Name(),
|
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
|
// 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)
|
i.Language = editorLanguage(i.Extension)
|
||||||
|
|
||||||
// If the editor will hold only content, leave now.
|
// If the editor will hold only content, leave now.
|
||||||
|
|
|
@ -29,11 +29,6 @@ type FileManager struct {
|
||||||
// a trailing slash and mustn't contain prefixURL, if set.
|
// a trailing slash and mustn't contain prefixURL, if set.
|
||||||
baseURL string
|
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 is a map with the different configurations for each user.
|
||||||
Users map[string]*User
|
Users map[string]*User
|
||||||
|
|
||||||
|
@ -43,8 +38,7 @@ type FileManager struct {
|
||||||
// AfterSave is a function that is called before saving a file.
|
// AfterSave is a function that is called before saving a file.
|
||||||
AfterSave Command
|
AfterSave Command
|
||||||
|
|
||||||
templates *rice.Box
|
assets *rice.Box
|
||||||
static http.Handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command is a command function.
|
// 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
|
// User contains the configuration for each user. It should be created
|
||||||
// using NewUser on a File Manager instance.
|
// using NewUser on a File Manager instance.
|
||||||
type User struct {
|
type User struct {
|
||||||
// scope is the physical path the user has access to.
|
// Scope is the physical path the user has access to.
|
||||||
scope string
|
Scope string
|
||||||
|
|
||||||
// fileSystem is the virtual file system the user has access.
|
// fileSystem is the virtual file system the user has access.
|
||||||
fileSystem webdav.FileSystem
|
fileSystem webdav.FileSystem
|
||||||
|
|
||||||
// handler handles incoming requests to the WebDAV backend.
|
|
||||||
handler *webdav.Handler
|
|
||||||
|
|
||||||
// Rules is an array of access and deny rules.
|
// Rules is an array of access and deny rules.
|
||||||
Rules []*Rule `json:"-"`
|
Rules []*Rule `json:"-"`
|
||||||
|
|
||||||
|
@ -106,13 +97,11 @@ func New(scope string) *FileManager {
|
||||||
Users: map[string]*User{},
|
Users: map[string]*User{},
|
||||||
BeforeSave: func(r *http.Request, m *FileManager, u *User) error { return nil },
|
BeforeSave: func(r *http.Request, m *FileManager, u *User) error { return nil },
|
||||||
AfterSave: 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()),
|
assets: rice.MustFindBox("./_assets/dist"),
|
||||||
templates: rice.MustFindBox("./_assets/dist_dev/templates"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetScope(scope, "")
|
m.SetScope(scope, "")
|
||||||
m.SetBaseURL("/")
|
m.SetBaseURL("/")
|
||||||
m.SetWebDavURL("/webdav")
|
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
@ -126,7 +115,7 @@ func (m FileManager) RootURL() string {
|
||||||
// WebDavURL returns the actual URL
|
// WebDavURL returns the actual URL
|
||||||
// where WebDAV can be accessed.
|
// where WebDAV can be accessed.
|
||||||
func (m FileManager) WebDavURL() string {
|
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
|
// SetPrefixURL updates the prefixURL of a File
|
||||||
|
@ -147,32 +136,6 @@ func (m *FileManager) SetBaseURL(url string) {
|
||||||
m.baseURL = strings.TrimSuffix(url, "/")
|
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.
|
// SetScope updates a user scope and its virtual file system.
|
||||||
// If the user string is blank, it will change the base scope.
|
// If the user string is blank, it will change the base scope.
|
||||||
func (m *FileManager) SetScope(scope string, username string) error {
|
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.Scope = strings.TrimSuffix(scope, "/")
|
||||||
u.fileSystem = webdav.Dir(u.scope)
|
u.fileSystem = webdav.Dir(u.Scope)
|
||||||
|
|
||||||
u.handler = &webdav.Handler{
|
|
||||||
Prefix: m.webDavURL,
|
|
||||||
FileSystem: u.fileSystem,
|
|
||||||
LockSystem: webdav.NewMemLS(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -208,9 +165,8 @@ func (m *FileManager) NewUser(username string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Users[username] = &User{
|
m.Users[username] = &User{
|
||||||
scope: m.User.scope,
|
|
||||||
fileSystem: m.User.fileSystem,
|
fileSystem: m.User.fileSystem,
|
||||||
handler: m.User.handler,
|
Scope: m.User.Scope,
|
||||||
Rules: m.User.Rules,
|
Rules: m.User.Rules,
|
||||||
AllowNew: m.User.AllowNew,
|
AllowNew: m.User.AllowNew,
|
||||||
AllowEdit: m.User.AllowEdit,
|
AllowEdit: m.User.AllowEdit,
|
||||||
|
@ -252,8 +208,3 @@ func (u User) Allowed(url string) bool {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scope returns the user scope.
|
|
||||||
func (u User) Scope() string {
|
|
||||||
return u.scope
|
|
||||||
}
|
|
||||||
|
|
250
http.go
250
http.go
|
@ -1,200 +1,98 @@
|
||||||
package filemanager
|
package filemanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"encoding/json"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// assetsURL is the url where static assets are served.
|
|
||||||
const assetsURL = "/_"
|
|
||||||
|
|
||||||
// requestContext contains the needed information to make handlers work.
|
// requestContext contains the needed information to make handlers work.
|
||||||
type requestContext struct {
|
type requestContext struct {
|
||||||
us *User
|
us *User
|
||||||
fm *FileManager
|
fm *FileManager
|
||||||
fi *file
|
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) {
|
func serveHTTP(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
var (
|
// Checks if the URL contains the baseURL and strips it. Otherwise, it just
|
||||||
code int
|
// returns a 404 error because we're not supposed to be here!
|
||||||
err error
|
p := strings.TrimPrefix(r.URL.Path, c.fm.baseURL)
|
||||||
)
|
|
||||||
|
|
||||||
// Checks if the URL contains the baseURL. If so, it strips it. Otherwise,
|
if len(p) >= len(r.URL.Path) && c.fm.baseURL != "" {
|
||||||
// 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 {
|
|
||||||
return http.StatusNotFound, nil
|
return http.StatusNotFound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the URL matches the Assets URL. Returns the asset if the
|
r.URL.Path = p
|
||||||
// method is GET and Status Forbidden otherwise.
|
|
||||||
if matchURL(r.URL.Path, assetsURL+"/") {
|
// Check if this request is made to the service worker. If so,
|
||||||
if r.Method == http.MethodGet {
|
// pass it through a template to add the needed variables.
|
||||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, assetsURL)
|
if r.URL.Path == "/sw.js" {
|
||||||
c.fm.static.ServeHTTP(w, r)
|
return renderFile(
|
||||||
return 0, nil
|
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()
|
return staticHandler(c, w, r)
|
||||||
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.
|
// Checks if this request is made to the API and directs to the
|
||||||
if matchURL(r.URL.Path, c.fm.webDavURL) {
|
// API handler if so.
|
||||||
return serveWebDAV(c, w, r)
|
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-frame-options", "SAMEORIGIN")
|
||||||
w.Header().Set("x-content-type", "nosniff")
|
w.Header().Set("x-content-type", "nosniff")
|
||||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||||
|
|
||||||
// Checks if the User is allowed to access this file
|
return renderFile(
|
||||||
if !c.us.Allowed(r.URL.Path) {
|
w,
|
||||||
if r.Method == http.MethodGet {
|
c.fm.assets.MustString("index.html"),
|
||||||
return htmlError(
|
"text/html",
|
||||||
w, http.StatusForbidden,
|
c.fm.RootURL(),
|
||||||
errors.New("You don't have permission to access this page"),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.StatusForbidden, nil
|
http.Redirect(w, r, c.fm.RootURL()+"/files"+r.URL.Path, http.StatusTemporaryRedirect)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
return 0, nil
|
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.
|
// staticHandler handles the static assets path.
|
||||||
func serveWebDAV(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func staticHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
var err error
|
if r.URL.Path != "/static/manifest.json" {
|
||||||
|
http.FileServer(c.fm.assets.HTTPBox()).ServeHTTP(w, r)
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, nil
|
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.
|
// 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) {
|
func checksumHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
query := r.URL.Query().Get("checksum")
|
query := r.URL.Query().Get("algo")
|
||||||
|
|
||||||
val, err := c.fi.Checksum(query)
|
val, err := c.fi.Checksum(query)
|
||||||
if err == errInvalidOption {
|
if err == errInvalidOption {
|
||||||
|
@ -207,30 +105,32 @@ func serveChecksum(c *requestContext, w http.ResponseWriter, r *http.Request) (i
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// responseWriterNoBody is a wrapper used to suprress the body of the response
|
// renderFile renders a file using a template with some needed variables.
|
||||||
// to a request. Mainly used for HEAD requests.
|
func renderFile(w http.ResponseWriter, file string, contentType string, baseURL string) (int, error) {
|
||||||
type responseWriterNoBody struct {
|
tpl := template.Must(template.New("file").Parse(file))
|
||||||
http.ResponseWriter
|
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
||||||
}
|
|
||||||
|
|
||||||
// newResponseWriterNoBody creates a new responseWriterNoBody.
|
err := tpl.Execute(w, map[string]string{"BaseURL": baseURL})
|
||||||
func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
|
if err != nil {
|
||||||
return &responseWriterNoBody{w}
|
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
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteHeader writes the header to the http.ResponseWriter.
|
// renderJSON prints the JSON version of data to the browser.
|
||||||
func (w responseWriterNoBody) WriteHeader(statusCode int) {
|
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
|
||||||
w.ResponseWriter.WriteHeader(statusCode)
|
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.
|
// 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.
|
// errorToHTTP converts errors to HTTP Status Code.
|
||||||
func errorToHTTP(err error, gone bool) int {
|
func errorToHTTP(err error, gone bool) int {
|
||||||
switch {
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return http.StatusOK
|
||||||
case os.IsPermission(err):
|
case os.IsPermission(err):
|
||||||
return http.StatusForbidden
|
return http.StatusForbidden
|
||||||
case os.IsNotExist(err):
|
case os.IsNotExist(err):
|
||||||
|
|
195
page.go
195
page.go
|
@ -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>`
|
|
|
@ -73,7 +73,7 @@ func search(c *requestContext, w http.ResponseWriter, r *http.Request) (int, err
|
||||||
search = parseSearch(value)
|
search = parseSearch(value)
|
||||||
scope := strings.TrimPrefix(r.URL.Path, "/")
|
scope := strings.TrimPrefix(r.URL.Path, "/")
|
||||||
scope = "/" + scope
|
scope = "/" + scope
|
||||||
scope = c.us.scope + scope
|
scope = c.us.Scope + scope
|
||||||
scope = strings.Replace(scope, "\\", "/", -1)
|
scope = strings.Replace(scope, "\\", "/", -1)
|
||||||
scope = filepath.Clean(scope)
|
scope = filepath.Clean(scope)
|
||||||
|
|
||||||
|
|
142
serve.go
142
serve.go
|
@ -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
|
|
||||||
}
|
|
Loading…
Reference in New Issue