diff --git a/.gitignore b/.gitignore
index ae35a22a..5279bfe4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
.DS_Store
node_modules/
-_assets/dist_dev/*
+_assets/dist/*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
diff --git a/_assets/build/config.js b/_assets/build/config.js
index 7525a0df..8f6190f2 100644
--- a/_assets/build/config.js
+++ b/_assets/build/config.js
@@ -6,10 +6,10 @@ module.exports = {
env: {
NODE_ENV: '"production"'
},
- index: path.resolve(__dirname, '../dist/templates/index.html'),
+ index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
- assetsSubDirectory: '_',
- assetsPublicPath: '{{ .BaseURL }}',
+ assetsSubDirectory: 'static',
+ assetsPublicPath: '{{ .BaseURL }}/',
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
@@ -27,10 +27,10 @@ module.exports = {
env: {
NODE_ENV: '"development"'
},
- index: path.resolve(__dirname, '../dist_dev/templates/index.html'),
- assetsRoot: path.resolve(__dirname, '../dist_dev/'),
- assetsSubDirectory: '_',
- assetsPublicPath: '{{ .BaseURL }}',
+ index: path.resolve(__dirname, '../dist/index.html'),
+ assetsRoot: path.resolve(__dirname, '../dist/'),
+ assetsSubDirectory: 'static',
+ assetsPublicPath: '{{ .BaseURL }}/',
produceSourceMap: true
}
}
diff --git a/_assets/build/service-worker-prod.js b/_assets/build/service-worker-prod.js
index cc9d4936..1179ec20 100644
--- a/_assets/build/service-worker-prod.js
+++ b/_assets/build/service-worker-prod.js
@@ -16,7 +16,7 @@
window.addEventListener('load', function() {
if ('serviceWorker' in navigator &&
(window.location.protocol === 'https:' || isLocalhost)) {
- navigator.serviceWorker.register('{{ .BaseURL }}/_/service-worker.js')
+ navigator.serviceWorker.register('{{ .BaseURL }}/sw.js')
.then(function(registration) {
// updatefound is fired if service-worker.js changes.
registration.onupdatefound = function() {
diff --git a/_assets/build/utils.js b/_assets/build/utils.js
index d078ded4..9062bbfa 100644
--- a/_assets/build/utils.js
+++ b/_assets/build/utils.js
@@ -6,6 +6,7 @@ exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
+
return path.posix.join(assetsSubDirectory, _path)
}
diff --git a/_assets/build/webpack.prod.conf.js b/_assets/build/webpack.prod.conf.js
index 2f1e461c..f057c334 100644
--- a/_assets/build/webpack.prod.conf.js
+++ b/_assets/build/webpack.prod.conf.js
@@ -98,8 +98,9 @@ var webpackConfig = merge(baseWebpackConfig, {
]),
// service worker caching
new SWPrecacheWebpackPlugin({
- cacheId: 'my-vue-app',
- filename: 'service-worker.js',
+ cacheId: 'File Manager',
+ filename: 'sw.js',
+ replacePrefix: '{{ .BaseURL }}/',
staticFileGlobs: ['dist/**/*.{js,html,css}'],
minify: true,
stripPrefix: 'dist/'
diff --git a/_assets/index.html b/_assets/index.html
index 42e3d235..880bbc3c 100644
--- a/_assets/index.html
+++ b/_assets/index.html
@@ -4,27 +4,28 @@
+
diff --git a/_assets/static/manifest.json b/_assets/static/manifest.json
index 7e111fc1..25bd2d98 100644
--- a/_assets/static/manifest.json
+++ b/_assets/static/manifest.json
@@ -3,18 +3,18 @@
"short_name": "File Manager",
"icons": [
{
- "src": "/static/img/icons/android-chrome-192x192.png",
+ "src": "{{ .BaseURL }}/static/img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
- "src": "/static/img/icons/android-chrome-512x512.png",
+ "src": "{{ .BaseURL }}/static/img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
- "start_url": "/index.html",
+ "start_url": "{{ .BaseURL }}/",
"display": "standalone",
- "background_color": "#000000",
- "theme_color": "#4DBA87"
+ "background_color": "#ffffff",
+ "theme_color": "#2979ff"
}
diff --git a/api.go b/api.go
new file mode 100644
index 00000000..8627d22c
--- /dev/null
+++ b/api.go
@@ -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
+}
diff --git a/auth.go b/auth.go
new file mode 100644
index 00000000..85a2c703
--- /dev/null
+++ b/auth.go
@@ -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
+}
diff --git a/caddy/filemanager/filemanager.go b/caddy/filemanager/filemanager.go
index baa0b613..68af4fc0 100644
--- a/caddy/filemanager/filemanager.go
+++ b/caddy/filemanager/filemanager.go
@@ -94,7 +94,6 @@ func parse(c *caddy.Controller) ([]*config, error) {
if len(args) > 0 {
m.baseURL = args[0]
- m.webDavURL = "/webdav"
m.SetBaseURL(args[0])
}
@@ -108,13 +107,6 @@ func parse(c *caddy.Controller) ([]*config, error) {
if m.AfterSave, err = makeCommand(c, m); err != nil {
return configs, err
}
- case "webdav":
- if !c.NextArg() {
- return configs, c.ArgErr()
- }
-
- m.webDavURL = "c.Val()"
- m.SetWebDavURL(c.Val())
case "show":
if !c.NextArg() {
return configs, c.ArgErr()
@@ -251,7 +243,7 @@ func makeCommand(c *caddy.Controller, m *config) (Command, error) {
fn = func(r *http.Request, c *FileManager, u *User) error {
path := strings.Replace(r.URL.Path, m.baseURL+m.webDavURL, "", 1)
- path = u.Scope() + "/" + path
+ path = u.Scope + "/" + path
path = filepath.Clean(path)
for i := range args {
diff --git a/command.go b/command.go
index cf35bdb4..ae24bfe0 100644
--- a/command.go
+++ b/command.go
@@ -77,7 +77,7 @@ func command(c *requestContext, w http.ResponseWriter, r *http.Request) (int, er
}
// Gets the path and initializes a buffer.
- path := c.us.scope + "/" + r.URL.Path
+ path := c.us.Scope + "/" + r.URL.Path
path = filepath.Clean(path)
buff := new(bytes.Buffer)
diff --git a/download.go b/download.go
index 4c7618b3..338eab9e 100644
--- a/download.go
+++ b/download.go
@@ -12,10 +12,10 @@ import (
"github.com/mholt/archiver"
)
-// serveDownload creates an archive in one of the supported formats (zip, tar,
+// downloadHandler creates an archive in one of the supported formats (zip, tar,
// tar.gz or tar.bz2) and sends it to be downloaded.
-func serveDownload(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
- query := r.URL.Query().Get("download")
+func downloadHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
+ query := r.URL.Query().Get("format")
if !c.fi.IsDir {
w.Header().Set("Content-Disposition", "attachment; filename="+c.fi.Name)
diff --git a/file.go b/file.go
index f3ecf05c..eb4f442b 100644
--- a/file.go
+++ b/file.go
@@ -29,6 +29,8 @@ var (
// file contains the information about a particular file or directory.
type file struct {
+ // Indicates the Kind of view on the front-end (listing, editor or preview).
+ Kind string `json:"kind"`
// The name of the file.
Name string `json:"name"`
// The Size of the file.
@@ -79,15 +81,13 @@ type listing struct {
func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
var err error
- i := &file{URL: c.RootURL() + url.Path}
- i.VirtualPath = url.Path
- i.VirtualPath = strings.TrimPrefix(i.VirtualPath, "/")
- i.VirtualPath = "/" + i.VirtualPath
+ i := &file{
+ URL: c.RootURL() + "/files" + url.Path,
+ VirtualPath: url.Path,
+ Path: filepath.Join(u.Scope, url.Path),
+ }
- i.Path = u.scope + i.VirtualPath
- i.Path = filepath.Clean(i.Path)
-
- info, err := os.Stat(i.Path)
+ info, err := u.fileSystem.Stat(context.TODO(), i.Path)
if err != nil {
return i, err
}
@@ -103,8 +103,6 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
// getListing gets the information about a specific directory and its files.
func (i *file) getListing(c *requestContext, r *http.Request) error {
- baseURL := c.fm.RootURL() + r.URL.Path
-
// Gets the directory information using the Virtual File System of
// the user configuration.
f, err := c.us.fileSystem.OpenFile(context.TODO(), c.fi.VirtualPath, os.O_RDONLY, 0)
@@ -140,7 +138,7 @@ func (i *file) getListing(c *requestContext, r *http.Request) error {
}
// Absolute URL
- url := url.URL{Path: baseURL + name}
+ url := url.URL{Path: i.URL + name}
i := file{
Name: f.Name(),
@@ -165,7 +163,7 @@ func (i *file) getListing(c *requestContext, r *http.Request) error {
}
// getEditor gets the editor based on a Info struct
-func (i *file) getEditor(r *http.Request) error {
+func (i *file) getEditor() error {
i.Language = editorLanguage(i.Extension)
// If the editor will hold only content, leave now.
diff --git a/filemanager.go b/filemanager.go
index 18e4729f..1fb527f0 100644
--- a/filemanager.go
+++ b/filemanager.go
@@ -29,11 +29,6 @@ type FileManager struct {
// a trailing slash and mustn't contain prefixURL, if set.
baseURL string
- // webDavURL is the path where the WebDAV will be accessible. It can be set to ""
- // in order to override the GUI and only use the WebDAV. It musn't end with
- // a trailing slash.
- webDavURL string
-
// Users is a map with the different configurations for each user.
Users map[string]*User
@@ -43,8 +38,7 @@ type FileManager struct {
// AfterSave is a function that is called before saving a file.
AfterSave Command
- templates *rice.Box
- static http.Handler
+ assets *rice.Box
}
// Command is a command function.
@@ -53,15 +47,12 @@ type Command func(r *http.Request, m *FileManager, u *User) error
// User contains the configuration for each user. It should be created
// using NewUser on a File Manager instance.
type User struct {
- // scope is the physical path the user has access to.
- scope string
+ // Scope is the physical path the user has access to.
+ Scope string
// fileSystem is the virtual file system the user has access.
fileSystem webdav.FileSystem
- // handler handles incoming requests to the WebDAV backend.
- handler *webdav.Handler
-
// Rules is an array of access and deny rules.
Rules []*Rule `json:"-"`
@@ -106,13 +97,11 @@ func New(scope string) *FileManager {
Users: map[string]*User{},
BeforeSave: func(r *http.Request, m *FileManager, u *User) error { return nil },
AfterSave: func(r *http.Request, m *FileManager, u *User) error { return nil },
- static: http.FileServer(rice.MustFindBox("./_assets/dist_dev/_").HTTPBox()),
- templates: rice.MustFindBox("./_assets/dist_dev/templates"),
+ assets: rice.MustFindBox("./_assets/dist"),
}
m.SetScope(scope, "")
m.SetBaseURL("/")
- m.SetWebDavURL("/webdav")
return m
}
@@ -126,7 +115,7 @@ func (m FileManager) RootURL() string {
// WebDavURL returns the actual URL
// where WebDAV can be accessed.
func (m FileManager) WebDavURL() string {
- return m.prefixURL + m.baseURL + m.webDavURL
+ return m.prefixURL + m.baseURL + "/api/webdav"
}
// SetPrefixURL updates the prefixURL of a File
@@ -147,32 +136,6 @@ func (m *FileManager) SetBaseURL(url string) {
m.baseURL = strings.TrimSuffix(url, "/")
}
-// SetWebDavURL updates the webDavURL of a File Manager
-// object and updates it's main handler.
-func (m *FileManager) SetWebDavURL(url string) {
- url = strings.TrimPrefix(url, "/")
- url = strings.TrimSuffix(url, "/")
-
- m.webDavURL = "/" + url
-
- // update base user webdav handler
- m.handler = &webdav.Handler{
- Prefix: m.webDavURL,
- FileSystem: m.fileSystem,
- LockSystem: webdav.NewMemLS(),
- }
-
- // update other users' handlers to match
- // the new URL
- for _, u := range m.Users {
- u.handler = &webdav.Handler{
- Prefix: m.webDavURL,
- FileSystem: u.fileSystem,
- LockSystem: webdav.NewMemLS(),
- }
- }
-}
-
// SetScope updates a user scope and its virtual file system.
// If the user string is blank, it will change the base scope.
func (m *FileManager) SetScope(scope string, username string) error {
@@ -188,14 +151,8 @@ func (m *FileManager) SetScope(scope string, username string) error {
}
}
- u.scope = strings.TrimSuffix(scope, "/")
- u.fileSystem = webdav.Dir(u.scope)
-
- u.handler = &webdav.Handler{
- Prefix: m.webDavURL,
- FileSystem: u.fileSystem,
- LockSystem: webdav.NewMemLS(),
- }
+ u.Scope = strings.TrimSuffix(scope, "/")
+ u.fileSystem = webdav.Dir(u.Scope)
return nil
}
@@ -208,9 +165,8 @@ func (m *FileManager) NewUser(username string) error {
}
m.Users[username] = &User{
- scope: m.User.scope,
fileSystem: m.User.fileSystem,
- handler: m.User.handler,
+ Scope: m.User.Scope,
Rules: m.User.Rules,
AllowNew: m.User.AllowNew,
AllowEdit: m.User.AllowEdit,
@@ -252,8 +208,3 @@ func (u User) Allowed(url string) bool {
return true
}
-
-// Scope returns the user scope.
-func (u User) Scope() string {
- return u.scope
-}
diff --git a/http.go b/http.go
index 94af960d..402a0459 100644
--- a/http.go
+++ b/http.go
@@ -1,200 +1,98 @@
package filemanager
import (
- "errors"
+ "encoding/json"
+ "html/template"
"net/http"
"os"
- "path/filepath"
"strings"
)
-// assetsURL is the url where static assets are served.
-const assetsURL = "/_"
-
// requestContext contains the needed information to make handlers work.
type requestContext struct {
us *User
fm *FileManager
fi *file
- pg *page
}
+// serveHTTP is the main entry point of this HTML application.
func serveHTTP(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
- var (
- code int
- err error
- )
+ // Checks if the URL contains the baseURL and strips it. Otherwise, it just
+ // returns a 404 error because we're not supposed to be here!
+ p := strings.TrimPrefix(r.URL.Path, c.fm.baseURL)
- // Checks if the URL contains the baseURL. If so, it strips it. Otherwise,
- // it throws an error.
- if p := strings.TrimPrefix(r.URL.Path, c.fm.baseURL); len(p) < len(r.URL.Path) || len(c.fm.baseURL) == 0 {
- r.URL.Path = p
- } else {
+ if len(p) >= len(r.URL.Path) && c.fm.baseURL != "" {
return http.StatusNotFound, nil
}
- // Checks if the URL matches the Assets URL. Returns the asset if the
- // method is GET and Status Forbidden otherwise.
- if matchURL(r.URL.Path, assetsURL+"/") {
- if r.Method == http.MethodGet {
- r.URL.Path = strings.TrimPrefix(r.URL.Path, assetsURL)
- c.fm.static.ServeHTTP(w, r)
- return 0, nil
- }
+ r.URL.Path = p
- return http.StatusForbidden, nil
+ // Check if this request is made to the service worker. If so,
+ // pass it through a template to add the needed variables.
+ if r.URL.Path == "/sw.js" {
+ return renderFile(
+ w,
+ c.fm.assets.MustString(r.URL.Path),
+ "application/javascript",
+ c.fm.RootURL(),
+ )
}
- username, _, _ := r.BasicAuth()
- if _, ok := c.fm.Users[username]; ok {
- c.us = c.fm.Users[username]
- } else {
- c.us = c.fm.User
+ // 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
+ }
+
+ return staticHandler(c, w, r)
}
- // Checks if the request URL is for the WebDav server.
- if matchURL(r.URL.Path, c.fm.webDavURL) {
- return serveWebDAV(c, w, r)
+ // Checks if this request is made to the API and directs to the
+ // API handler if so.
+ if matchURL(r.URL.Path, "/api") {
+ r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
+ return serveAPI(c, w, r)
}
- 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 this request is made to the base path /files. If so,
+ // shows the index.html page.
+ if matchURL(r.URL.Path, "/files") {
+ w.Header().Set("x-frame-options", "SAMEORIGIN")
+ w.Header().Set("x-content-type", "nosniff")
+ w.Header().Set("x-xss-protection", "1; mode=block")
- // Checks if the User is allowed to access this file
- if !c.us.Allowed(r.URL.Path) {
- if r.Method == http.MethodGet {
- return htmlError(
- w, http.StatusForbidden,
- errors.New("You don't have permission to access this page"),
- )
- }
-
- return http.StatusForbidden, nil
- }
-
- if r.URL.Query().Get("search") != "" {
- return search(c, w, r)
- }
-
- if r.URL.Query().Get("command") != "" {
- return command(c, w, r)
- }
-
- if r.Method == http.MethodGet {
- var f *file
-
- // Obtains the information of the directory/file.
- f, err = getInfo(r.URL, c.fm, c.us)
- if err != nil {
- if r.Method == http.MethodGet {
- return htmlError(w, code, err)
- }
-
- code = errorToHTTP(err, false)
- return code, err
- }
-
- c.fi = f
-
- // If it's a dir and the path doesn't end with a trailing slash,
- // redirect the user.
- if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") {
- http.Redirect(w, r, c.fm.RootURL()+r.URL.Path+"/", http.StatusTemporaryRedirect)
- return 0, nil
- }
-
- switch {
- case r.URL.Query().Get("download") != "":
- code, err = serveDownload(c, w, r)
- case !f.IsDir && r.URL.Query().Get("checksum") != "":
- code, err = serveChecksum(c, w, r)
- case r.URL.Query().Get("raw") == "true" && !f.IsDir:
- http.ServeFile(w, r, f.Path)
- code, err = 0, nil
- default:
- code, err = serveDefault(c, w, r)
- }
-
- if err != nil {
- code, err = htmlError(w, code, err)
- }
-
- return code, err
- }
-
- return http.StatusNotImplemented, nil
-}
-
-// serveWebDAV handles the webDAV route of the File Manager.
-func serveWebDAV(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
- var err error
-
- // Checks for user permissions relatively to this path.
- if !c.us.Allowed(strings.TrimPrefix(r.URL.Path, c.fm.webDavURL)) {
- return http.StatusForbidden, nil
- }
-
- switch r.Method {
- case "GET", "HEAD":
- // Excerpt from RFC4918, section 9.4:
- //
- // GET, when applied to a collection, may return the contents of an
- // "index.html" resource, a human-readable view of the contents of
- // the collection, or something else altogether.
- //
- // It was decided on https://github.com/hacdias/caddy-filemanager/issues/85
- // that GET, for collections, will return the same as PROPFIND method.
- path := strings.Replace(r.URL.Path, c.fm.webDavURL, "", 1)
- path = c.us.scope + "/" + path
- path = filepath.Clean(path)
-
- var i os.FileInfo
- i, err = os.Stat(path)
- if err != nil {
- // Is there any error? WebDav will handle it... no worries.
- break
- }
-
- if i.IsDir() {
- r.Method = "PROPFIND"
-
- if r.Method == "HEAD" {
- w = newResponseWriterNoBody(w)
- }
- }
- case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
- if !c.us.AllowEdit {
- return http.StatusForbidden, nil
- }
- case "MKCOL", "COPY":
- if !c.us.AllowNew {
- return http.StatusForbidden, nil
- }
- }
-
- // Execute beforeSave if it is a PUT request.
- if r.Method == http.MethodPut {
- if err = c.fm.BeforeSave(r, c.fm, c.us); err != nil {
- return http.StatusInternalServerError, err
- }
- }
-
- c.fm.handler.ServeHTTP(w, r)
-
- // Execute afterSave if it is a PUT request.
- if r.Method == http.MethodPut {
- if err = c.fm.AfterSave(r, c.fm, c.us); err != nil {
- return http.StatusInternalServerError, err
- }
+ return renderFile(
+ w,
+ c.fm.assets.MustString("index.html"),
+ "text/html",
+ c.fm.RootURL(),
+ )
}
+ http.Redirect(w, r, c.fm.RootURL()+"/files"+r.URL.Path, http.StatusTemporaryRedirect)
return 0, nil
}
+// staticHandler handles the static assets path.
+func staticHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
+ if r.URL.Path != "/static/manifest.json" {
+ http.FileServer(c.fm.assets.HTTPBox()).ServeHTTP(w, r)
+ return 0, nil
+ }
+
+ return renderFile(
+ w,
+ c.fm.assets.MustString(r.URL.Path),
+ "application/json",
+ c.fm.RootURL(),
+ )
+}
+
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
-func serveChecksum(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
- query := r.URL.Query().Get("checksum")
+func checksumHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) {
+ query := r.URL.Query().Get("algo")
val, err := c.fi.Checksum(query)
if err == errInvalidOption {
@@ -207,30 +105,32 @@ func serveChecksum(c *requestContext, w http.ResponseWriter, r *http.Request) (i
return 0, nil
}
-// responseWriterNoBody is a wrapper used to suprress the body of the response
-// to a request. Mainly used for HEAD requests.
-type responseWriterNoBody struct {
- http.ResponseWriter
-}
+// renderFile renders a file using a template with some needed variables.
+func renderFile(w http.ResponseWriter, file string, contentType string, baseURL string) (int, error) {
+ tpl := template.Must(template.New("file").Parse(file))
+ w.Header().Set("Content-Type", contentType+"; charset=utf-8")
-// newResponseWriterNoBody creates a new responseWriterNoBody.
-func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
- return &responseWriterNoBody{w}
-}
+ err := tpl.Execute(w, map[string]string{"BaseURL": baseURL})
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
-// Header executes the Header method from the http.ResponseWriter.
-func (w responseWriterNoBody) Header() http.Header {
- return w.ResponseWriter.Header()
-}
-
-// Write suprresses the body.
-func (w responseWriterNoBody) Write(data []byte) (int, error) {
return 0, nil
}
-// WriteHeader writes the header to the http.ResponseWriter.
-func (w responseWriterNoBody) WriteHeader(statusCode int) {
- w.ResponseWriter.WriteHeader(statusCode)
+// renderJSON prints the JSON version of data to the browser.
+func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
+ marsh, err := json.Marshal(data)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ if _, err := w.Write(marsh); err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return 0, nil
}
// matchURL checks if the first URL matches the second.
@@ -244,6 +144,8 @@ func matchURL(first, second string) bool {
// errorToHTTP converts errors to HTTP Status Code.
func errorToHTTP(err error, gone bool) int {
switch {
+ case err == nil:
+ return http.StatusOK
case os.IsPermission(err):
return http.StatusForbidden
case os.IsNotExist(err):
diff --git a/page.go b/page.go
deleted file mode 100644
index 8d657a2d..00000000
--- a/page.go
+++ /dev/null
@@ -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 = `
-
-
-
TITLE
-
-
-
-
-
-
-
-
TITLE
-
-
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 hacdias/caddy-filemanager repository on GitHub with the code below.
-
-
CODE
-
-`
diff --git a/search.go b/search.go
index bee1c8c5..5ef14246 100644
--- a/search.go
+++ b/search.go
@@ -73,7 +73,7 @@ func search(c *requestContext, w http.ResponseWriter, r *http.Request) (int, err
search = parseSearch(value)
scope := strings.TrimPrefix(r.URL.Path, "/")
scope = "/" + scope
- scope = c.us.scope + scope
+ scope = c.us.Scope + scope
scope = strings.Replace(scope, "\\", "/", -1)
scope = filepath.Clean(scope)
diff --git a/serve.go b/serve.go
deleted file mode 100644
index 72264c36..00000000
--- a/serve.go
+++ /dev/null
@@ -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
-}