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 @@ + File Manager - - + + - - + + - + - - + + <% for (var chunk of webpack.chunks) { for (var file of chunk.files) { if (file.match(/\.(js|css)$/)) { %> - <% }}} %> + <% }}} %> - - {{- if ne .User.StyleSheet "" -}} - - {{- end -}} -
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 -}