feat: use cobra to provide subcommands, move sources to lib (#506)

- Use cobra in order to provide subcommands `serve` and `db`.
  - Subdir `cmd` is removed.
  - Subdir `cli` is created, which is a standard cobra structure.
- Sources related to the core are moved to subdir `lib`.
- #497 and #504 are merged.
- Deprecated flags are added. See https://github.com/filebrowser/filebrowser/pull/497#discussion_r209428120.
- [`viper.BindPFlags`](https://godoc.org/github.com/spf13/viper#BindPFlags) is used in order to reduce the verbosity in `serve.go`.


Former-commit-id: 4b37ad82e91e01f7718cd389469814674bdf7032 [formerly c84d7fcf9c362b2aa1f9e5b57196152f53835e61] [formerly 2fef43c0382f3cc7d13e0297ccb467e38fac6982 [formerly 69a3f853bd]]
Former-commit-id: 2f7dc1b8ee6735382cedae2053f40c546c21de45 [formerly b438417178b47ad5f7caf9cb728f4a5011a09f5e]
Former-commit-id: 07bc58ab2e1ab10c30be8d0a5e760288bfc4d4dc
This commit is contained in:
1138-4EB 2018-08-21 22:42:03 +01:00
parent 93acf4a6f7
commit 0536d8342c
35 changed files with 535 additions and 840 deletions

View file

@ -1,218 +0,0 @@
package http
import (
"encoding/json"
"net/http"
"net/url"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
fb "github.com/filebrowser/filebrowser"
)
const reCaptchaAPI = "/recaptcha/api/siteverify"
type cred struct {
Password string `json:"password"`
Username string `json:"username"`
ReCaptcha string `json:"recaptcha"`
}
// reCaptcha checks the reCaptcha code.
func reCaptcha(host, secret, response string) (bool, error) {
body := url.Values{}
body.Set("secret", secret)
body.Add("response", response)
client := &http.Client{}
resp, err := client.Post(host+reCaptchaAPI, "application/x-www-form-urlencoded", strings.NewReader(body.Encode()))
if err != nil {
return false, err
}
if resp.StatusCode != http.StatusOK {
return false, nil
}
var data struct {
Success bool `json:"success"`
}
err = json.NewDecoder(resp.Body).Decode(&data)
if err != nil {
return false, err
}
return data.Success, nil
}
// authHandler processes the authentication for the user.
func authHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if c.NoAuth {
// NoAuth instances shouldn't call this method.
return 0, nil
}
if c.AuthMethod == "proxy" {
// Receive the Username from the Header and check if it exists.
u, err := c.Store.Users.GetByUsername(r.Header.Get(c.LoginHeader), c.NewFS)
if err != nil {
return http.StatusForbidden, nil
}
c.User = u
return printToken(c, w)
}
// Receive the credentials from the request and unmarshal them.
var cred cred
if r.Body == nil {
return http.StatusForbidden, nil
}
err := json.NewDecoder(r.Body).Decode(&cred)
if err != nil {
return http.StatusForbidden, err
}
// If ReCaptcha is enabled, check the code.
if len(c.ReCaptchaSecret) > 0 {
ok, err := reCaptcha(c.ReCaptchaHost, c.ReCaptchaSecret, cred.ReCaptcha)
if err != nil {
return http.StatusForbidden, err
}
if !ok {
return http.StatusForbidden, nil
}
}
// Checks if the user exists.
u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
if err != nil {
return http.StatusForbidden, nil
}
// Checks if the password is correct.
if !fb.CheckPasswordHash(cred.Password, u.Password) {
return http.StatusForbidden, nil
}
c.User = u
return printToken(c, w)
}
// renewAuthHandler is used when the front-end already has a JWT token
// and is checking if it is up to date. If so, updates its info.
func renewAuthHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
ok, u := validateAuth(c, r)
if !ok {
return http.StatusForbidden, nil
}
c.User = u
return printToken(c, w)
}
// claims is the JWT claims.
type claims struct {
fb.User
jwt.StandardClaims
}
// printToken prints the final JWT token to the user.
func printToken(c *fb.Context, w http.ResponseWriter) (int, error) {
// Creates a copy of the user and removes it password
// hash so it never arrives to the user.
u := fb.User{}
u = *c.User
u.Password = ""
// Builds the claims.
claims := claims{
u,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
Issuer: "File Browser",
},
}
// Creates the token and signs it.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(c.Key)
if err != nil {
return http.StatusInternalServerError, err
}
// Writes the token.
w.Header().Set("Content-Type", "cty")
w.Write([]byte(signed))
return 0, nil
}
type extractor []string
func (e extractor) ExtractToken(r *http.Request) (string, error) {
token, _ := request.AuthorizationHeaderExtractor.ExtractToken(r)
// Checks if the token isn't empty and if it contains two dots.
// The former prevents incompatibility with URLs that previously
// used basic auth.
if token != "" && strings.Count(token, ".") == 2 {
return token, nil
}
cookie, err := r.Cookie("auth")
if err != nil {
return "", request.ErrNoTokenInRequest
}
return cookie.Value, nil
}
// validateAuth is used to validate the authentication and returns the
// User if it is valid.
func validateAuth(c *fb.Context, r *http.Request) (bool, *fb.User) {
if c.NoAuth {
c.User = c.DefaultUser
return true, c.User
}
// If proxy auth is used do not verify the JWT token if the header is provided.
if c.AuthMethod == "proxy" {
u, err := c.Store.Users.GetByUsername(r.Header.Get(c.LoginHeader), c.NewFS)
if err != nil {
return false, nil
}
c.User = u
return true, c.User
}
keyFunc := func(token *jwt.Token) (interface{}, error) {
return c.Key, nil
}
var claims claims
token, err := request.ParseFromRequestWithClaims(r,
extractor{},
&claims,
keyFunc,
)
if err != nil || !token.Valid {
return false, nil
}
u, err := c.Store.Users.Get(claims.User.ID, c.NewFS)
if err != nil {
return false, nil
}
c.User = u
return true, u
}

View file

@ -1,102 +0,0 @@
package http
import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
fb "github.com/filebrowser/filebrowser"
"github.com/hacdias/fileutils"
"github.com/mholt/archiver"
)
// downloadHandler creates an archive in one of the supported formats (zip, tar,
// tar.gz or tar.bz2) and sends it to be downloaded.
func downloadHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// If the file isn't a directory, serve it using http.ServeFile. We display it
// inline if it is requested.
if !c.File.IsDir {
return downloadFileHandler(c, w, r)
}
query := r.URL.Query().Get("format")
files := []string{}
names := strings.Split(r.URL.Query().Get("files"), ",")
// If there are files in the query, sanitize their names.
// Otherwise, just append the current path.
if len(names) != 0 {
for _, name := range names {
// Unescape the name.
name, err := url.QueryUnescape(name)
if err != nil {
return http.StatusInternalServerError, err
}
// Clean the slashes.
name = fileutils.SlashClean(name)
files = append(files, filepath.Join(c.File.Path, name))
}
} else {
files = append(files, c.File.Path)
}
var (
extension string
ar archiver.Archiver
)
switch query {
// If the format is true, just set it to "zip".
case "zip", "true", "":
extension, ar = ".zip", archiver.Zip
case "tar":
extension, ar = ".tar", archiver.Tar
case "targz":
extension, ar = ".tar.gz", archiver.TarGz
case "tarbz2":
extension, ar = ".tar.bz2", archiver.TarBz2
case "tarxz":
extension, ar = ".tar.xz", archiver.TarXZ
default:
return http.StatusNotImplemented, nil
}
// Defines the file name.
name := c.File.Name
if name == "." || name == "" {
name = "archive"
}
name += extension
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(name))
err := ar.Write(w, files)
return 0, err
}
func downloadFileHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
file, err := os.Open(c.File.Path)
if err != nil {
return http.StatusInternalServerError, err
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return http.StatusInternalServerError, err
}
if r.URL.Query().Get("inline") == "true" {
w.Header().Set("Content-Disposition", "inline")
} else {
// As per RFC6266 section 4.3
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(c.File.Name))
}
http.ServeContent(w, r, stat.Name(), stat.ModTime(), file)
return 0, nil
}

View file

@ -1,348 +0,0 @@
package http
import (
"encoding/json"
"html/template"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
fb "github.com/filebrowser/filebrowser"
)
// Handler returns a function compatible with http.HandleFunc.
func Handler(m *fb.FileBrowser) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
code, err := serve(&fb.Context{
FileBrowser: m,
User: nil,
File: nil,
}, w, r)
if code >= 400 {
w.WriteHeader(code)
txt := http.StatusText(code)
log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
w.Write([]byte(txt + "\n"))
}
if err != nil {
log.Print(err)
}
})
}
// serve is the main entry point of this HTML application.
func serve(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Checks if the URL contains the baseURL and strips it. Otherwise, it just
// returns a 404 fb.Error because we're not supposed to be here!
p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
return http.StatusNotFound, nil
}
r.URL.Path = p
// Check if this request is made to the service worker. If so,
// pass it through a template to add the needed variables.
if r.URL.Path == "/sw.js" {
return renderFile(c, w, "sw.js")
}
// 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 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 apiHandler(c, w, r)
}
// If it is a request to the preview and a static website generator is
// active, build the preview.
if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview")
return c.StaticGen.Preview(c, w, r)
}
if strings.HasPrefix(r.URL.Path, "/share/") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
return sharePage(c, w, r)
}
// Any other request should show the index.html file.
w.Header().Set("x-frame-options", "SAMEORIGIN")
w.Header().Set("x-content-type-options", "nosniff")
w.Header().Set("x-xss-protection", "1; mode=block")
return renderFile(c, w, "index.html")
}
// staticHandler handles the static assets path.
func staticHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/static/manifest.json" {
http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil
}
return renderFile(c, w, "static/manifest.json")
}
// apiHandler is the main entry point for the /api endpoint.
func apiHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/auth/get" {
return authHandler(c, w, r)
}
if r.URL.Path == "/auth/renew" {
return renewAuthHandler(c, w, r)
}
valid, _ := validateAuth(c, r)
if !valid {
return http.StatusForbidden, nil
}
c.Router, r.URL.Path = splitURL(r.URL.Path)
if !c.User.Allowed(r.URL.Path) {
return http.StatusForbidden, nil
}
if c.StaticGen != nil {
// If we are using the 'magic url' for the settings,
// we should redirect the request for the acutual path.
if r.URL.Path == "/settings" {
r.URL.Path = c.StaticGen.SettingsPath()
}
// Executes the Static website generator hook.
code, err := c.StaticGen.Hook(c, w, r)
if code != 0 || err != nil {
return code, err
}
}
if c.Router == "checksum" || c.Router == "download" || c.Router == "subtitle" || c.Router == "subtitles" {
var err error
c.File, err = fb.GetInfo(r.URL, c.FileBrowser, c.User)
if err != nil {
return ErrorToHTTP(err, false), err
}
}
var code int
var err error
switch c.Router {
case "download":
code, err = downloadHandler(c, w, r)
case "checksum":
code, err = checksumHandler(c, w, r)
case "command":
code, err = command(c, w, r)
case "search":
code, err = search(c, w, r)
case "resource":
code, err = resourceHandler(c, w, r)
case "users":
code, err = usersHandler(c, w, r)
case "settings":
code, err = settingsHandler(c, w, r)
case "share":
code, err = shareHandler(c, w, r)
case "subtitles":
code, err = subtitlesHandler(c, w, r)
case "subtitle":
code, err = subtitleHandler(c, w, r)
default:
code = http.StatusNotFound
}
return code, err
}
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
func checksumHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo")
val, err := c.File.Checksum(query)
if err == fb.ErrInvalidOption {
return http.StatusBadRequest, err
} else if err != nil {
return http.StatusInternalServerError, err
}
w.Write([]byte(val))
return 0, nil
}
// splitURL splits the path and returns everything that stands
// before the first slash and everything that goes after.
func splitURL(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:]
}
// renderFile renders a file using a template with some needed variables.
func renderFile(c *fb.Context, w http.ResponseWriter, file string) (int, error) {
tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file)))
var contentType string
switch filepath.Ext(file) {
case ".html":
contentType = "text/html"
case ".js":
contentType = "application/javascript"
case ".json":
contentType = "application/json"
default:
contentType = "text"
}
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
data := map[string]interface{}{
"BaseURL": c.RootURL(),
"NoAuth": c.NoAuth,
"Version": fb.Version,
"CSS": template.CSS(c.CSS),
"ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "",
"ReCaptchaHost": c.ReCaptchaHost,
"ReCaptchaKey": c.ReCaptchaKey,
}
if c.StaticGen != nil {
data["StaticGen"] = c.StaticGen.Name()
}
err := tpl.Execute(w, data)
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
// sharePage build the share page.
func sharePage(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
s, err := c.Store.Share.Get(r.URL.Path)
if err == fb.ErrNotExist {
w.WriteHeader(http.StatusNotFound)
return renderFile(c, w, "static/share/404.html")
}
if err != nil {
return http.StatusInternalServerError, err
}
if s.Expires && s.ExpireDate.Before(time.Now()) {
c.Store.Share.Delete(s.Hash)
w.WriteHeader(http.StatusNotFound)
return renderFile(c, w, "static/share/404.html")
}
r.URL.Path = s.Path
info, err := os.Stat(s.Path)
if err != nil {
c.Store.Share.Delete(s.Hash)
return ErrorToHTTP(err, false), err
}
c.File = &fb.File{
Path: s.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
}
dl := r.URL.Query().Get("dl")
if dl == "" || dl == "0" {
tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html")))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.RootURL(),
"File": c.File,
})
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
return downloadHandler(c, w, r)
}
// 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.
func matchURL(first, second string) bool {
first = strings.ToLower(first)
second = strings.ToLower(second)
return strings.HasPrefix(first, second)
}
// ErrorToHTTP converts errors to HTTP Status Code.
func ErrorToHTTP(err error, gone bool) int {
switch {
case err == nil:
return http.StatusOK
case os.IsPermission(err):
return http.StatusForbidden
case os.IsNotExist(err):
if !gone {
return http.StatusNotFound
}
return http.StatusGone
case os.IsExist(err):
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}

View file

@ -1,386 +0,0 @@
package http
import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
fb "github.com/filebrowser/filebrowser"
"github.com/hacdias/fileutils"
)
// sanitizeURL sanitizes the URL to prevent path transversal
// using fileutils.SlashClean and adds the trailing slash bar.
func sanitizeURL(url string) string {
path := fileutils.SlashClean(url)
if strings.HasSuffix(url, "/") && path != "/" {
return path + "/"
}
return path
}
func resourceHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method {
case http.MethodGet:
return resourceGetHandler(c, w, r)
case http.MethodDelete:
return resourceDeleteHandler(c, w, r)
case http.MethodPut:
// Before save command handler.
path := filepath.Join(c.User.Scope, r.URL.Path)
if err := c.Runner("before_save", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
code, err := resourcePostPutHandler(c, w, r)
if code != http.StatusOK {
return code, err
}
// After save command handler.
if err := c.Runner("after_save", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
return code, err
case http.MethodPatch:
return resourcePatchHandler(c, w, r)
case http.MethodPost:
return resourcePostPutHandler(c, w, r)
}
return http.StatusNotImplemented, nil
}
func resourceGetHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Gets the information of the directory/file.
f, err := fb.GetInfo(r.URL, c.FileBrowser, c.User)
if err != nil {
return ErrorToHTTP(err, false), err
}
// If it's a dir and the path doesn't end with a trailing slash,
// add a trailing slash to the path.
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.File = f
return listingHandler(c, w, r)
}
// Tries to get the file type.
if err = f.GetFileType(true); err != nil {
return ErrorToHTTP(err, true), err
}
// Serve a preview if the file can't be edited or the
// user has no permission to edit this file. Otherwise,
// just serve the editor.
if !f.CanBeEdited() || !c.User.AllowEdit {
f.Kind = "preview"
return renderJSON(w, f)
}
f.Kind = "editor"
// Tries to get the editor data.
if err = f.GetEditor(); err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, f)
}
func listingHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
f := c.File
f.Kind = "listing"
// Tries to get the listing data.
if err := f.GetListing(c.User, r); err != nil {
return ErrorToHTTP(err, true), err
}
listing := f.Listing
// Defines the cookie scope.
cookieScope := c.RootURL()
if cookieScope == "" {
cookieScope = "/"
}
// Copy the query values into the Listing struct
if sort, order, err := handleSortOrder(w, r, cookieScope); err == nil {
listing.Sort = sort
listing.Order = order
} else {
return http.StatusBadRequest, err
}
listing.ApplySort()
return renderJSON(w, f)
}
func resourceDeleteHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Prevent the removal of the root directory.
if r.URL.Path == "/" || !c.User.AllowEdit {
return http.StatusForbidden, nil
}
// Fire the before trigger.
if err := c.Runner("before_delete", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
// Remove the file or folder.
err := c.User.FileSystem.RemoveAll(r.URL.Path)
if err != nil {
return ErrorToHTTP(err, true), err
}
// Fire the after trigger.
if err := c.Runner("after_delete", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func resourcePostPutHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowNew && r.Method == http.MethodPost {
return http.StatusForbidden, nil
}
if !c.User.AllowEdit && r.Method == http.MethodPut {
return http.StatusForbidden, nil
}
// Discard any invalid upload before returning to avoid connection
// reset error.
defer func() {
io.Copy(ioutil.Discard, r.Body)
}()
// Checks if the current request is for a directory and not a file.
if strings.HasSuffix(r.URL.Path, "/") {
// If the method is PUT, we return 405 Method not Allowed, because
// POST should be used instead.
if r.Method == http.MethodPut {
return http.StatusMethodNotAllowed, nil
}
// Otherwise we try to create the directory.
err := c.User.FileSystem.Mkdir(r.URL.Path, 0775)
return ErrorToHTTP(err, false), err
}
// If using POST method, we are trying to create a new file so it is not
// desirable to override an already existent file. Thus, we check
// if the file already exists. If so, we just return a 409 Conflict.
if r.Method == http.MethodPost && r.Header.Get("Action") != "override" {
if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil {
return http.StatusConflict, errors.New("There is already a file on that path")
}
}
// Fire the before trigger.
if err := c.Runner("before_upload", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
// Create/Open the file.
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil {
return ErrorToHTTP(err, false), err
}
defer f.Close()
// Copies the new content for the file.
_, err = io.Copy(f, r.Body)
if err != nil {
return ErrorToHTTP(err, false), err
}
// Gets the info about the file.
fi, err := f.Stat()
if err != nil {
return ErrorToHTTP(err, false), err
}
// Check if this instance has a Static Generator and handles publishing
// or scheduling if it's the case.
if c.StaticGen != nil {
code, err := resourcePublishSchedule(c, w, r)
if code != 0 {
return code, err
}
}
// Writes the ETag Header.
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
w.Header().Set("ETag", etag)
// Fire the after trigger.
if err := c.Runner("after_upload", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func resourcePublishSchedule(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
publish := r.Header.Get("Publish")
schedule := r.Header.Get("Schedule")
if publish != "true" && schedule == "" {
return 0, nil
}
if !c.User.AllowPublish {
return http.StatusForbidden, nil
}
if publish == "true" {
return resourcePublish(c, w, r)
}
t, err := time.Parse("2006-01-02T15:04", schedule)
if err != nil {
return http.StatusInternalServerError, err
}
c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
_, err := resourcePublish(c, w, r)
if err != nil {
log.Print(err)
}
})
return http.StatusOK, nil
}
func resourcePublish(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(c.User.Scope, r.URL.Path)
// Before save command handler.
if err := c.Runner("before_publish", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
code, err := c.StaticGen.Publish(c, w, r)
if err != nil {
return code, err
}
// Executed the before publish command.
if err := c.Runner("before_publish", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
return code, nil
}
// resourcePatchHandler is the entry point for resource handler.
func resourcePatchHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowEdit {
return http.StatusForbidden, nil
}
dst := r.Header.Get("Destination")
action := r.Header.Get("Action")
dst, err := url.QueryUnescape(dst)
if err != nil {
return ErrorToHTTP(err, true), err
}
src := r.URL.Path
if dst == "/" || src == "/" {
return http.StatusForbidden, nil
}
if action == "copy" {
// Fire the after trigger.
if err := c.Runner("before_copy", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
// Copy the file.
err = c.User.FileSystem.Copy(src, dst)
// Fire the after trigger.
if err := c.Runner("after_copy", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
} else {
// Fire the after trigger.
if err := c.Runner("before_rename", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
// Rename the file.
err = c.User.FileSystem.Rename(src, dst)
// Fire the after trigger.
if err := c.Runner("after_rename", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
}
return ErrorToHTTP(err, true), err
}
// 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":
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
}

View file

@ -1,146 +0,0 @@
package http
import (
"bytes"
"encoding/json"
"net/http"
"reflect"
fb "github.com/filebrowser/filebrowser"
"github.com/mitchellh/mapstructure"
)
type modifySettingsRequest struct {
modifyRequest
Data struct {
CSS string `json:"css"`
Commands map[string][]string `json:"commands"`
StaticGen map[string]interface{} `json:"staticGen"`
} `json:"data"`
}
type option struct {
Variable string `json:"variable"`
Name string `json:"name"`
Value interface{} `json:"value"`
}
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, fb.ErrEmptyRequest
}
// Parses the request body and checks if it's well formed.
mod := &modifySettingsRequest{}
err := json.NewDecoder(r.Body).Decode(mod)
if err != nil {
return nil, err
}
// Checks if the request type is right.
if mod.What != "settings" {
return nil, fb.ErrWrongDataType
}
return mod, nil
}
func settingsHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "" && r.URL.Path != "/" {
return http.StatusNotFound, nil
}
switch r.Method {
case http.MethodGet:
return settingsGetHandler(c, w, r)
case http.MethodPut:
return settingsPutHandler(c, w, r)
}
return http.StatusMethodNotAllowed, nil
}
type settingsGetRequest struct {
CSS string `json:"css"`
Commands map[string][]string `json:"commands"`
StaticGen []option `json:"staticGen"`
}
func settingsGetHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
result := &settingsGetRequest{
Commands: c.Commands,
StaticGen: []option{},
CSS: c.CSS,
}
if c.StaticGen != nil {
t := reflect.TypeOf(c.StaticGen).Elem()
for i := 0; i < t.NumField(); i++ {
if t.Field(i).Name[0] == bytes.ToLower([]byte{t.Field(i).Name[0]})[0] {
continue
}
result.StaticGen = append(result.StaticGen, option{
Variable: t.Field(i).Name,
Name: t.Field(i).Tag.Get("name"),
Value: reflect.ValueOf(c.StaticGen).Elem().FieldByName(t.Field(i).Name).Interface(),
})
}
}
return renderJSON(w, result)
}
func settingsPutHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
mod, err := parsePutSettingsRequest(r)
if err != nil {
return http.StatusBadRequest, err
}
// Update the commands.
if mod.Which == "commands" {
if err := c.Store.Config.Save("commands", mod.Data.Commands); err != nil {
return http.StatusInternalServerError, err
}
c.Commands = mod.Data.Commands
return http.StatusOK, nil
}
// Update the global CSS.
if mod.Which == "css" {
if err := c.Store.Config.Save("css", mod.Data.CSS); err != nil {
return http.StatusInternalServerError, err
}
c.CSS = mod.Data.CSS
return http.StatusOK, nil
}
// Update the static generator options.
if mod.Which == "staticGen" {
err = mapstructure.Decode(mod.Data.StaticGen, c.StaticGen)
if err != nil {
return http.StatusInternalServerError, err
}
err = c.Store.Config.Save("staticgen_"+c.StaticGen.Name(), c.StaticGen)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
return http.StatusMethodNotAllowed, nil
}

View file

@ -1,127 +0,0 @@
package http
import (
"encoding/base64"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
fb "github.com/filebrowser/filebrowser"
)
func shareHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method {
case http.MethodGet:
return shareGetHandler(c, w, r)
case http.MethodDelete:
return shareDeleteHandler(c, w, r)
case http.MethodPost:
return sharePostHandler(c, w, r)
}
return http.StatusNotImplemented, nil
}
func shareGetHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(c.User.Scope, r.URL.Path)
s, err := c.Store.Share.GetByPath(path)
if err == fb.ErrNotExist {
return http.StatusNotFound, nil
}
if err != nil {
return http.StatusInternalServerError, err
}
for i, link := range s {
if link.Expires && link.ExpireDate.Before(time.Now()) {
c.Store.Share.Delete(link.Hash)
s = append(s[:i], s[i+1:]...)
}
}
if len(s) == 0 {
return http.StatusNotFound, nil
}
return renderJSON(w, s)
}
func sharePostHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(c.User.Scope, r.URL.Path)
var s *fb.ShareLink
expire := r.URL.Query().Get("expires")
unit := r.URL.Query().Get("unit")
if expire == "" {
var err error
s, err = c.Store.Share.GetPermanent(path)
if err == nil {
w.Write([]byte(c.RootURL() + "/share/" + s.Hash))
return 0, nil
}
}
bytes, err := fb.GenerateRandomBytes(6)
if err != nil {
return http.StatusInternalServerError, err
}
str := base64.URLEncoding.EncodeToString(bytes)
s = &fb.ShareLink{
Path: path,
Hash: str,
Expires: expire != "",
}
if expire != "" {
num, err := strconv.Atoi(expire)
if err != nil {
return http.StatusInternalServerError, err
}
var add time.Duration
switch unit {
case "seconds":
add = time.Second * time.Duration(num)
case "minutes":
add = time.Minute * time.Duration(num)
case "days":
add = time.Hour * 24 * time.Duration(num)
default:
add = time.Hour * time.Duration(num)
}
s.ExpireDate = time.Now().Add(add)
}
if err := c.Store.Share.Save(s); err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, s)
}
func shareDeleteHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
s, err := c.Store.Share.Get(strings.TrimPrefix(r.URL.Path, "/"))
if err == fb.ErrNotExist {
return http.StatusNotFound, nil
}
if err != nil {
return http.StatusInternalServerError, err
}
err = c.Store.Share.Delete(s.Hash)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}

View file

@ -1,83 +0,0 @@
package http
import (
"bytes"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"regexp"
fb "github.com/filebrowser/filebrowser"
)
func subtitlesHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
files, err := ReadDir(filepath.Dir(c.File.Path))
if err != nil {
return http.StatusInternalServerError, err
}
var subtitles = make([]map[string]string, 0)
for _, file := range files {
ext := filepath.Ext(file.Name())
if ext == ".vtt" || ext == ".srt" {
var sub map[string]string = make(map[string]string)
sub["src"] = filepath.Dir(c.File.Path) + "/" + file.Name()
sub["kind"] = "subtitles"
sub["label"] = file.Name()
subtitles = append(subtitles, sub)
}
}
return renderJSON(w, subtitles)
}
func subtitleHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
str, err := CleanSubtitle(c.File.Path)
if err != nil {
return http.StatusInternalServerError, err
}
file, err := os.Open(c.File.Path)
if err != nil {
return http.StatusInternalServerError, err
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Disposition", "inline")
w.Header().Set("Content-Type", "text/vtt")
http.ServeContent(w, r, stat.Name(), stat.ModTime(), bytes.NewReader([]byte(str)))
return 0, nil
}
func CleanSubtitle(filename string) (string, error) {
b, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
str := string(b) // convert content to a 'string'
ext := filepath.Ext(filename)
if ext == ".srt" {
re := regexp.MustCompile("([0-9]{2}:[0-9]{2}:[0-9]{2}),([0-9]{3})")
str = "WEBVTT\n\n" + re.ReplaceAllString(str, "$1.$2")
}
return str, err
}
func ReadDir(dirname string) ([]os.FileInfo, error) {
f, err := os.Open(dirname)
if err != nil {
return nil, err
}
list, err := f.Readdir(-1)
f.Close()
if err != nil {
return nil, err
}
return list, nil
}

View file

@ -1,383 +0,0 @@
package http
import (
"encoding/json"
"errors"
"net/http"
"os"
"sort"
"strconv"
"strings"
fb "github.com/filebrowser/filebrowser"
)
type modifyRequest struct {
What string `json:"what"` // Answer to: what data type?
Which string `json:"which"` // Answer to: which field?
}
type modifyUserRequest struct {
modifyRequest
Data *fb.User `json:"data"`
}
// usersHandler is the entry point of the users API. It's just a router
// to send the request to its
func usersHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// If the user isn't admin and isn't making a PUT
// request, then return forbidden.
if !c.User.Admin && r.Method != http.MethodPut {
return http.StatusForbidden, nil
}
switch r.Method {
case http.MethodGet:
return usersGetHandler(c, w, r)
case http.MethodPost:
return usersPostHandler(c, w, r)
case http.MethodDelete:
return usersDeleteHandler(c, w, r)
case http.MethodPut:
return usersPutHandler(c, w, r)
}
return http.StatusNotImplemented, nil
}
// getUserID returns the id from the user which is present
// in the request url. If the url is invalid and doesn't
// contain a valid ID, it returns an fb.Error.
func getUserID(r *http.Request) (int, error) {
// Obtains the ID in string from the URL and converts
// it into an integer.
sid := strings.TrimPrefix(r.URL.Path, "/")
sid = strings.TrimSuffix(sid, "/")
id, err := strconv.Atoi(sid)
if err != nil {
return http.StatusBadRequest, err
}
return id, nil
}
// getUser returns the user which is present in the request
// body. If the body is empty or the JSON is invalid, it
// returns an fb.Error.
func getUser(c *fb.Context, r *http.Request) (*fb.User, string, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, "", fb.ErrEmptyRequest
}
// Parses the request body and checks if it's well formed.
mod := &modifyUserRequest{}
err := json.NewDecoder(r.Body).Decode(mod)
if err != nil {
return nil, "", err
}
// Checks if the request type is right.
if mod.What != "user" {
return nil, "", fb.ErrWrongDataType
}
mod.Data.FileSystem = c.NewFS(mod.Data.Scope)
return mod.Data, mod.Which, nil
}
func usersGetHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Request for the default user data.
if r.URL.Path == "/base" {
return renderJSON(w, c.DefaultUser)
}
// Request for the listing of users.
if r.URL.Path == "/" {
users, err := c.Store.Users.Gets(c.NewFS)
if err != nil {
return http.StatusInternalServerError, err
}
for _, u := range users {
// Removes the user password so it won't
// be sent to the front-end.
u.Password = ""
}
sort.Slice(users, func(i, j int) bool {
return users[i].ID < users[j].ID
})
return renderJSON(w, users)
}
id, err := getUserID(r)
if err != nil {
return http.StatusInternalServerError, err
}
u, err := c.Store.Users.Get(id, c.NewFS)
if err == fb.ErrExist {
return http.StatusNotFound, err
}
if err != nil {
return http.StatusInternalServerError, err
}
u.Password = ""
return renderJSON(w, u)
}
func usersPostHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/" {
return http.StatusMethodNotAllowed, nil
}
u, _, err := getUser(c, r)
if err != nil {
return http.StatusBadRequest, err
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, fb.ErrEmptyUsername
}
// Checks if scope isn't empty.
if u.Scope == "" {
return http.StatusBadRequest, fb.ErrEmptyScope
}
// Checks if password isn't empty.
if u.Password == "" {
return http.StatusBadRequest, fb.ErrEmptyPassword
}
// Initialize rules if they're not initialized.
if u.Rules == nil {
u.Rules = []*fb.Rule{}
}
// If the view mode is empty, initialize with the default one.
if u.ViewMode == "" {
u.ViewMode = c.DefaultUser.ViewMode
}
// Initialize commands if not initialized.
if u.Commands == nil {
u.Commands = []string{}
}
// It's a new user so the ID will be auto created.
if u.ID != 0 {
u.ID = 0
}
// Checks if the scope exists.
if code, err := checkFS(u.Scope); err != nil {
return code, err
}
// Hashes the password.
pw, err := fb.HashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
u.Password = pw
u.ViewMode = fb.MosaicViewMode
// Saves the user to the database.
err = c.Store.Users.Save(u)
if err == fb.ErrExist {
return http.StatusConflict, err
}
if err != nil {
return http.StatusInternalServerError, err
}
// Set the Location header and return.
w.Header().Set("Location", "/settings/users/"+strconv.Itoa(u.ID))
w.WriteHeader(http.StatusCreated)
return 0, nil
}
func checkFS(path string) (int, error) {
info, err := os.Stat(path)
if err != nil {
if !os.IsNotExist(err) {
return http.StatusInternalServerError, err
}
err = os.MkdirAll(path, 0666)
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
if !info.IsDir() {
return http.StatusBadRequest, errors.New("Scope is not a dir")
}
return 0, nil
}
func usersDeleteHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil
}
id, err := getUserID(r)
if err != nil {
return http.StatusInternalServerError, err
}
// Deletes the user from the database.
err = c.Store.Users.Delete(id)
if err == fb.ErrNotExist {
return http.StatusNotFound, fb.ErrNotExist
}
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func usersPutHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// New users should be created on /api/users.
if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil
}
// Gets the user ID from the URL and checks if it's valid.
id, err := getUserID(r)
if err != nil {
return http.StatusInternalServerError, err
}
// Checks if the user has permission to access this page.
if !c.User.Admin && id != c.User.ID {
return http.StatusForbidden, nil
}
// Gets the user from the request body.
u, which, err := getUser(c, r)
if err != nil {
return http.StatusBadRequest, err
}
// If we're updating the default user. Only for NoAuth
// implementations. Used to change the viewMode.
if id == 0 && c.NoAuth {
c.DefaultUser.ViewMode = u.ViewMode
return http.StatusOK, nil
}
// Updates the CSS and locale.
if which == "partial" {
c.User.CSS = u.CSS
c.User.Locale = u.Locale
c.User.ViewMode = u.ViewMode
err = c.Store.Users.Update(c.User, "CSS", "Locale", "ViewMode")
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
// Updates the Password.
if which == "password" {
if u.Password == "" {
return http.StatusBadRequest, fb.ErrEmptyPassword
}
if id == c.User.ID && c.User.LockPassword {
return http.StatusForbidden, nil
}
c.User.Password, err = fb.HashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
err = c.Store.Users.Update(c.User, "Password")
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
// If can only be all.
if which != "all" {
return http.StatusBadRequest, fb.ErrInvalidUpdateField
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, fb.ErrEmptyUsername
}
// Checks if filesystem isn't empty.
if u.Scope == "" {
return http.StatusBadRequest, fb.ErrEmptyScope
}
// Checks if the scope exists.
if code, err := checkFS(u.Scope); err != nil {
return code, err
}
// Initialize rules if they're not initialized.
if u.Rules == nil {
u.Rules = []*fb.Rule{}
}
// Initialize commands if not initialized.
if u.Commands == nil {
u.Commands = []string{}
}
// Gets the current saved user from the in-memory map.
suser, err := c.Store.Users.Get(id, c.NewFS)
if err == fb.ErrNotExist {
return http.StatusNotFound, nil
}
if err != nil {
return http.StatusInternalServerError, err
}
u.ID = id
// Changes the password if the request wants it.
if u.Password != "" {
pw, err := fb.HashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
u.Password = pw
} else {
u.Password = suser.Password
}
// Updates the whole User struct because we always are supposed
// to send a new entire object.
err = c.Store.Users.Update(u)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}

View file

@ -1,340 +0,0 @@
package http
import (
"bytes"
"encoding/json"
"mime"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
fb "github.com/filebrowser/filebrowser"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
var (
cmdNotImplemented = []byte("Command not implemented.")
cmdNotAllowed = []byte("Command not allowed.")
)
// command handles the requests for VCS related commands: git, svn and mercurial
func command(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for fb.Errors.
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return 0, err
}
defer conn.Close()
var (
message []byte
command []string
)
// Starts an infinite loop until a valid command is captured.
for {
_, message, err = conn.ReadMessage()
if err != nil {
return http.StatusInternalServerError, err
}
command = strings.Split(string(message), " ")
if len(command) != 0 {
break
}
}
// Check if the command is allowed
allowed := false
for _, cmd := range c.User.Commands {
if regexp.MustCompile(cmd).MatchString(command[0]) {
allowed = true
break
}
}
if !allowed {
err = conn.WriteMessage(websocket.TextMessage, cmdNotAllowed)
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
// Check if the program is installed on the computer.
if _, err = exec.LookPath(command[0]); err != nil {
err = conn.WriteMessage(websocket.TextMessage, cmdNotImplemented)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusNotImplemented, nil
}
// Gets the path and initializes a buffer.
path := c.User.Scope + "/" + r.URL.Path
path = filepath.Clean(path)
buff := new(bytes.Buffer)
// Sets up the command executation.
cmd := exec.Command(command[0], command[1:]...)
cmd.Dir = path
cmd.Stderr = buff
cmd.Stdout = buff
// Starts the command and checks for fb.Errors.
err = cmd.Start()
if err != nil {
return http.StatusInternalServerError, err
}
// Set a 'done' variable to check whetever the command has already finished
// running or not. This verification is done using a goroutine that uses the
// method .Wait() from the command.
done := false
go func() {
err = cmd.Wait()
done = true
}()
// Function to print the current information on the buffer to the connection.
print := func() error {
by := buff.Bytes()
if len(by) > 0 {
err = conn.WriteMessage(websocket.TextMessage, by)
if err != nil {
return err
}
}
return nil
}
// While the command hasn't finished running, continue sending the output
// to the client in intervals of 100 milliseconds.
for !done {
if err = print(); err != nil {
return http.StatusInternalServerError, err
}
time.Sleep(100 * time.Millisecond)
}
// After the command is done executing, send the output one more time to the
// browser to make sure it gets the latest information.
if err = print(); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
var (
typeRegexp = regexp.MustCompile(`type:(\w+)`)
)
type condition func(path string) bool
type searchOptions struct {
CaseSensitive bool
Conditions []condition
Terms []string
}
func extensionCondition(extension string) condition {
return func(path string) bool {
return filepath.Ext(path) == "."+extension
}
}
func imageCondition(path string) bool {
extension := filepath.Ext(path)
mimetype := mime.TypeByExtension(extension)
return strings.HasPrefix(mimetype, "image")
}
func audioCondition(path string) bool {
extension := filepath.Ext(path)
mimetype := mime.TypeByExtension(extension)
return strings.HasPrefix(mimetype, "audio")
}
func videoCondition(path string) bool {
extension := filepath.Ext(path)
mimetype := mime.TypeByExtension(extension)
return strings.HasPrefix(mimetype, "video")
}
func parseSearch(value string) *searchOptions {
opts := &searchOptions{
CaseSensitive: strings.Contains(value, "case:sensitive"),
Conditions: []condition{},
Terms: []string{},
}
// removes the options from the value
value = strings.Replace(value, "case:insensitive", "", -1)
value = strings.Replace(value, "case:sensitive", "", -1)
value = strings.TrimSpace(value)
types := typeRegexp.FindAllStringSubmatch(value, -1)
for _, t := range types {
if len(t) == 1 {
continue
}
switch t[1] {
case "image":
opts.Conditions = append(opts.Conditions, imageCondition)
case "audio", "music":
opts.Conditions = append(opts.Conditions, audioCondition)
case "video":
opts.Conditions = append(opts.Conditions, videoCondition)
default:
opts.Conditions = append(opts.Conditions, extensionCondition(t[1]))
}
}
if len(types) > 0 {
// Remove the fields from the search value.
value = typeRegexp.ReplaceAllString(value, "")
}
// If it's canse insensitive, put everything in lowercase.
if !opts.CaseSensitive {
value = strings.ToLower(value)
}
// Remove the spaces from the search value.
value = strings.TrimSpace(value)
if value == "" {
return opts
}
// if the value starts with " and finishes what that character, we will
// only search for that term
if value[0] == '"' && value[len(value)-1] == '"' {
unique := strings.TrimPrefix(value, "\"")
unique = strings.TrimSuffix(unique, "\"")
opts.Terms = []string{unique}
return opts
}
opts.Terms = strings.Split(value, " ")
return opts
}
// search searches for a file or directory.
func search(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for fb.Errors.
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return 0, err
}
defer conn.Close()
var (
value string
search *searchOptions
message []byte
)
// Starts an infinite loop until a valid command is captured.
for {
_, message, err = conn.ReadMessage()
if err != nil {
return http.StatusInternalServerError, err
}
if len(message) != 0 {
value = string(message)
break
}
}
search = parseSearch(value)
scope := strings.TrimPrefix(r.URL.Path, "/")
scope = "/" + scope
scope = c.User.Scope + scope
scope = strings.Replace(scope, "\\", "/", -1)
scope = filepath.Clean(scope)
err = filepath.Walk(scope, func(path string, f os.FileInfo, err error) error {
if !search.CaseSensitive {
path = strings.ToLower(path)
}
path = strings.TrimPrefix(path, scope)
path = strings.TrimPrefix(path, "/")
path = strings.Replace(path, "\\", "/", -1)
// Only execute if there are conditions to meet.
if len(search.Conditions) > 0 {
match := false
for _, t := range search.Conditions {
if t(path) {
match = true
break
}
}
// If doesn't meet the condition, go to the next.
if !match {
return nil
}
}
if len(search.Terms) > 0 {
is := false
// Checks if matches the terms and if it is allowed.
for _, term := range search.Terms {
if is {
break
}
if strings.Contains(path, term) {
if !c.User.Allowed(path) {
return nil
}
is = true
}
}
if !is {
return nil
}
}
response, _ := json.Marshal(map[string]interface{}{
"dir": f.IsDir(),
"path": path,
})
return conn.WriteMessage(websocket.TextMessage, response)
})
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}