mirror of
https://github.com/filebrowser/filebrowser.git
synced 2026-01-23 02:35:10 +00:00
updates
Former-commit-id: 54b88552d11f2151a165dba9debb4657dfa56cf8 [formerly 0ce53651a8e9660f9d5f977295f553b5b1d1e93a] [formerly 7ebca3a8896222091c95af86a9cf1d12550b8b76 [formerly 174330929a]]
Former-commit-id: 993d0cdb239f9969587d13a11ee8469fa8b91287 [formerly c22c911f944dd8d6597ab95589842d3c68d34869]
Former-commit-id: 44ed259fe50a085e8bcace3f1f14caafec97ce66
This commit is contained in:
parent
e4144ad2b2
commit
4b602be5e3
19 changed files with 9860 additions and 661 deletions
177
http/auth.go
Normal file
177
http/auth.go
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/dgrijalva/jwt-go/request"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
// authHandler proccesses the authentication for the user.
|
||||
func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// NoAuth instances shouldn't call this method.
|
||||
if c.NoAuth {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Receive the credentials from the request and unmarshal them.
|
||||
var cred fm.User
|
||||
if r.Body == nil {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&cred)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Checks if the user exists.
|
||||
u, ok := c.Users[cred.Username]
|
||||
if !ok {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Checks if the password is correct.
|
||||
if !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 *fm.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 {
|
||||
fm.User
|
||||
NoAuth bool `json:"noAuth"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
// printToken prints the final JWT token to the user.
|
||||
func printToken(c *fm.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 := fm.User{}
|
||||
u = *c.User
|
||||
u.Password = ""
|
||||
|
||||
// Builds the claims.
|
||||
claims := claims{
|
||||
u,
|
||||
c.NoAuth,
|
||||
jwt.StandardClaims{
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
|
||||
Issuer: "File Manager",
|
||||
},
|
||||
}
|
||||
|
||||
// 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 *fm.Context, r *http.Request) (bool, *fm.User) {
|
||||
if c.NoAuth {
|
||||
c.User = c.DefaultUser
|
||||
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, ok := c.Users[claims.User.Username]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
c.User = u
|
||||
return true, u
|
||||
}
|
||||
|
||||
// hashPassword generates an hash from a password using bcrypt.
|
||||
func hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// checkPasswordHash compares a password with an hash to check if they match.
|
||||
func checkPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// generateRandomBytes returns securely generated random bytes.
|
||||
// It will return an error if the system's secure random
|
||||
// number generator fails to function correctly, in which
|
||||
// case the caller should not continue.
|
||||
func generateRandomBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
// Note that err == nil only if we read len(b) bytes.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
92
http/auth_test.go
Normal file
92
http/auth_test.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var defaultCredentials = "{\"username\":\"admin\",\"password\":\"admin\"}"
|
||||
|
||||
var authHandlerTests = []struct {
|
||||
Data string
|
||||
Expected int
|
||||
}{
|
||||
{defaultCredentials, http.StatusOK},
|
||||
{"{\"username\":\"admin\",\"password\":\"wrong\"}", http.StatusForbidden},
|
||||
{"{\"username\":\"wrong\",\"password\":\"admin\"}", http.StatusForbidden},
|
||||
}
|
||||
|
||||
func TestAuthHandler(t *testing.T) {
|
||||
fm := newTest(t)
|
||||
defer fm.Clean()
|
||||
|
||||
for _, test := range authHandlerTests {
|
||||
req, err := http.NewRequest("POST", "/api/auth/get", strings.NewReader(test.Data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
fm.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != test.Expected {
|
||||
t.Errorf("Wrong status code: got %v want %v", w.Code, test.Expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewHandler(t *testing.T) {
|
||||
fm := newTest(t)
|
||||
defer fm.Clean()
|
||||
|
||||
// First, we have to make an auth request to get the user authenticated,
|
||||
r, err := http.NewRequest("POST", "/api/auth/get", strings.NewReader(defaultCredentials))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
fm.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Couldn't authenticate: got %v", w.Code)
|
||||
}
|
||||
|
||||
token := w.Body.String()
|
||||
|
||||
// Test renew authorization via Authorization Header.
|
||||
r, err = http.NewRequest("GET", "/api/auth/renew", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r.Header.Set("Authorization", "Bearer "+token)
|
||||
w = httptest.NewRecorder()
|
||||
fm.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Can't renew auth via header: got %v", w.Code)
|
||||
}
|
||||
|
||||
// Test renew authorization via cookie field.
|
||||
r, err = http.NewRequest("GET", "/api/auth/renew", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r.AddCookie(&http.Cookie{
|
||||
Value: token,
|
||||
Name: "auth",
|
||||
Expires: time.Now().Add(1 * time.Hour),
|
||||
})
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
fm.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Can't renew auth via cookie: got %v", w.Code)
|
||||
}
|
||||
}
|
||||
113
http/download.go
Normal file
113
http/download.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
fm "github.com/hacdias/filemanager"
|
||||
"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 *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
query := r.URL.Query().Get("format")
|
||||
|
||||
// If the file isn't a directory, serve it using http.ServeFile. We display it
|
||||
// inline if it is requested.
|
||||
if !c.File.IsDir {
|
||||
if r.URL.Query().Get("inline") == "true" {
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
} else {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+c.File.Name)
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, c.File.Path)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// If the format is true, just set it to "zip".
|
||||
if query == "true" || query == "" {
|
||||
query = "zip"
|
||||
}
|
||||
|
||||
var (
|
||||
extension string
|
||||
temp string
|
||||
err error
|
||||
tempfile string
|
||||
)
|
||||
|
||||
// Create a temporary directory.
|
||||
temp, err = ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer os.RemoveAll(temp)
|
||||
|
||||
tempfile = filepath.Join(temp, "temp")
|
||||
|
||||
switch query {
|
||||
case "zip":
|
||||
extension, err = ".zip", archiver.Zip.Make(tempfile, files)
|
||||
case "tar":
|
||||
extension, err = ".tar", archiver.Tar.Make(tempfile, files)
|
||||
case "targz":
|
||||
extension, err = ".tar.gz", archiver.TarGz.Make(tempfile, files)
|
||||
case "tarbz2":
|
||||
extension, err = ".tar.bz2", archiver.TarBz2.Make(tempfile, files)
|
||||
case "tarxz":
|
||||
extension, err = ".tar.xz", archiver.TarXZ.Make(tempfile, files)
|
||||
default:
|
||||
return http.StatusNotImplemented, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Defines the file name.
|
||||
name := c.File.Name
|
||||
if name == "." || name == "" {
|
||||
name = "download"
|
||||
}
|
||||
name += extension
|
||||
|
||||
// Opens the file so it can be downloaded.
|
||||
file, err := os.Open(temp + "/temp")
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+name)
|
||||
_, err = io.Copy(w, file)
|
||||
return 0, err
|
||||
}
|
||||
324
http/http.go
Normal file
324
http/http.go
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
var (
|
||||
errUserExist = errors.New("user already exists")
|
||||
errUserNotExist = errors.New("user does not exist")
|
||||
errEmptyRequest = errors.New("request body is empty")
|
||||
errEmptyPassword = errors.New("password is empty")
|
||||
errEmptyUsername = errors.New("username is empty")
|
||||
errEmptyScope = errors.New("scope is empty")
|
||||
errWrongDataType = errors.New("wrong data type")
|
||||
errInvalidUpdateField = errors.New("invalid field to update")
|
||||
)
|
||||
|
||||
// ServeHTTP is the main entry point of this HTML application.
|
||||
func ServeHTTP(c *fm.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 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,
|
||||
c.assets.MustString("sw.js"),
|
||||
"application/javascript",
|
||||
)
|
||||
}
|
||||
|
||||
// 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", "nosniff")
|
||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.assets.MustString("index.html"),
|
||||
"text/html",
|
||||
)
|
||||
}
|
||||
|
||||
// staticHandler handles the static assets path.
|
||||
func staticHandler(c *fm.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,
|
||||
c.assets.MustString("static/manifest.json"),
|
||||
"application/json",
|
||||
)
|
||||
}
|
||||
|
||||
// apiHandler is the main entry point for the /api endpoint.
|
||||
func apiHandler(c *fm.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" {
|
||||
var err error
|
||||
c.File, err = fm.GetInfo(r.URL, c.FileManager, 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)
|
||||
default:
|
||||
code = http.StatusNotFound
|
||||
}
|
||||
|
||||
return code, err
|
||||
}
|
||||
|
||||
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
|
||||
func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
query := r.URL.Query().Get("algo")
|
||||
|
||||
val, err := c.File.Checksum(query)
|
||||
if err == 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 *fm.Context, w http.ResponseWriter, file string, contentType string) (int, error) {
|
||||
tpl := template.Must(template.New("file").Parse(file))
|
||||
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
||||
|
||||
err := tpl.Execute(w, map[string]interface{}{
|
||||
"BaseURL": c.RootURL(),
|
||||
"StaticGen": c.staticgen,
|
||||
})
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var s shareLink
|
||||
err := c.db.One("Hash", r.URL.Path, &s)
|
||||
if err == storm.ErrNotFound {
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.assets.MustString("static/share/404.html"),
|
||||
"text/html",
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
if s.Expires && s.ExpireDate.Before(time.Now()) {
|
||||
c.db.DeleteStruct(&s)
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.assets.MustString("static/share/404.html"),
|
||||
"text/html",
|
||||
)
|
||||
}
|
||||
|
||||
r.URL.Path = s.Path
|
||||
|
||||
info, err := os.Stat(s.Path)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, false), err
|
||||
}
|
||||
|
||||
c.File = &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
|
||||
}
|
||||
}
|
||||
371
http/resource.go
Normal file
371
http/resource.go
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
fm "github.com/hacdias/filemanager"
|
||||
"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 *fm.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(string(c.User.FileSystem), r.URL.Path)
|
||||
if err := c.Runner("before_save", path); 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); 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 *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Gets the information of the directory/file.
|
||||
f, err := getInfo(r.URL, c.FileManager, 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 *fm.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, 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()
|
||||
listing.Display = displayMode(w, r, cookieScope)
|
||||
|
||||
return renderJSON(w, f)
|
||||
}
|
||||
|
||||
func resourceDeleteHandler(c *fm.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
|
||||
}
|
||||
|
||||
// Remove the file or folder.
|
||||
err := c.User.FileSystem.RemoveAll(r.URL.Path)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func resourcePostPutHandler(c *fm.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, 0776)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// Create/Open the file.
|
||||
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
|
||||
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)
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func resourcePublishSchedule(c *fm.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 *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
|
||||
// Before save command handler.
|
||||
if err := c.Runner("before_publish", path); 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); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// resourcePatchHandler is the entry point for resource handler.
|
||||
func resourcePatchHandler(c *fm.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" {
|
||||
err = c.User.FileSystem.Copy(src, dst)
|
||||
} else {
|
||||
err = c.User.FileSystem.Rename(src, dst)
|
||||
}
|
||||
|
||||
return errorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
// displayMode obtains the display mode from the Cookie.
|
||||
func displayMode(w http.ResponseWriter, r *http.Request, scope string) string {
|
||||
var displayMode string
|
||||
|
||||
// Checks the cookie.
|
||||
if displayCookie, err := r.Cookie("display"); err == nil {
|
||||
displayMode = displayCookie.Value
|
||||
}
|
||||
|
||||
// If it's invalid, set it to mosaic, which is the default.
|
||||
if displayMode == "" || (displayMode != "mosaic" && displayMode != "list") {
|
||||
displayMode = "mosaic"
|
||||
}
|
||||
|
||||
// Set the cookie.
|
||||
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":
|
||||
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
|
||||
}
|
||||
132
http/settings.go
Normal file
132
http/settings.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
fm "github.com/hacdias/filemanager"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type modifySettingsRequest struct {
|
||||
*modifyRequest
|
||||
Data struct {
|
||||
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, 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, errWrongDataType
|
||||
}
|
||||
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
func settingsHandler(c *fm.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 {
|
||||
Commands map[string][]string `json:"commands"`
|
||||
StaticGen []option `json:"staticGen"`
|
||||
}
|
||||
|
||||
func settingsGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
result := &settingsGetRequest{
|
||||
Commands: c.Commands,
|
||||
StaticGen: []option{},
|
||||
}
|
||||
|
||||
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 *fm.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.db.Set("config", "commands", mod.Data.Commands); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
c.Commands = mod.Data.Commands
|
||||
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.db.Set("staticgen", c.staticgen, c.StaticGen)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
138
http/share.go
Normal file
138
http/share.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
"github.com/asdine/storm/q"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
type shareLink struct {
|
||||
Hash string `json:"hash" storm:"id,index"`
|
||||
Path string `json:"path" storm:"index"`
|
||||
Expires bool `json:"expires"`
|
||||
ExpireDate time.Time `json:"expireDate"`
|
||||
}
|
||||
|
||||
func shareHandler(c *fm.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 *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var (
|
||||
s []*shareLink
|
||||
path = filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
)
|
||||
|
||||
err := c.db.Find("Path", path, &s)
|
||||
if err == storm.ErrNotFound {
|
||||
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.db.DeleteStruct(&shareLink{Hash: link.Hash})
|
||||
s = append(s[:i], s[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
return renderJSON(w, s)
|
||||
}
|
||||
|
||||
func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
|
||||
var s shareLink
|
||||
expire := r.URL.Query().Get("expires")
|
||||
unit := r.URL.Query().Get("unit")
|
||||
|
||||
if expire == "" {
|
||||
err := c.db.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&s)
|
||||
if err == nil {
|
||||
w.Write([]byte(c.RootURL() + "/share/" + s.Hash))
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
bytes, err := generateRandomBytes(32)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
str := hex.EncodeToString(bytes)
|
||||
|
||||
s = 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)
|
||||
}
|
||||
|
||||
err = c.db.Save(&s)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return renderJSON(w, s)
|
||||
}
|
||||
|
||||
func shareDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var s shareLink
|
||||
|
||||
err := c.db.One("Hash", strings.TrimPrefix(r.URL.Path, "/"), &s)
|
||||
if err == storm.ErrNotFound {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
err = c.db.DeleteStruct(&s)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
397
http/users.go
Normal file
397
http/users.go
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
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 *fm.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 *fm.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 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 error.
|
||||
func getUser(r *http.Request) (*fm.User, string, error) {
|
||||
// Checks if the request body is empty.
|
||||
if r.Body == nil {
|
||||
return nil, "", 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, "", errWrongDataType
|
||||
}
|
||||
|
||||
return mod.Data, mod.Which, nil
|
||||
}
|
||||
|
||||
func usersGetHandler(c *fm.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 := []User{}
|
||||
|
||||
for _, user := range c.Users {
|
||||
// Copies the user info and removes its
|
||||
// password so it won't be sent to the
|
||||
// front-end.
|
||||
u := *user
|
||||
u.Password = ""
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Searches for the user and prints the one who matches.
|
||||
for _, user := range c.Users {
|
||||
if user.ID != id {
|
||||
continue
|
||||
}
|
||||
|
||||
u := *user
|
||||
u.Password = ""
|
||||
return renderJSON(w, u)
|
||||
}
|
||||
|
||||
// If there aren't any matches, return not found.
|
||||
return http.StatusNotFound, errUserNotExist
|
||||
}
|
||||
|
||||
func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if r.URL.Path != "/" {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
u, _, err := getUser(r)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
// Checks if username isn't empty.
|
||||
if u.Username == "" {
|
||||
return http.StatusBadRequest, errEmptyUsername
|
||||
}
|
||||
|
||||
// Checks if filesystem isn't empty.
|
||||
if u.FileSystem == "" {
|
||||
return http.StatusBadRequest, errEmptyScope
|
||||
}
|
||||
|
||||
// Checks if password isn't empty.
|
||||
if u.Password == "" {
|
||||
return http.StatusBadRequest, errEmptyPassword
|
||||
}
|
||||
|
||||
// The username, password and scope cannot be empty.
|
||||
if u.Username == "" || u.Password == "" || u.FileSystem == "" {
|
||||
return http.StatusBadRequest, errors.New("username, password or scope is empty")
|
||||
}
|
||||
|
||||
// Initialize rules if they're not initialized.
|
||||
if u.Rules == nil {
|
||||
u.Rules = []*Rule{}
|
||||
}
|
||||
|
||||
// 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(string(u.FileSystem)); err != nil {
|
||||
return code, err
|
||||
}
|
||||
|
||||
// Hashes the password.
|
||||
pw, err := hashPassword(u.Password)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
u.Password = pw
|
||||
|
||||
// Saves the user to the database.
|
||||
err = c.db.Save(u)
|
||||
if err == storm.ErrAlreadyExists {
|
||||
return http.StatusConflict, errUserExist
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Saves the user to the memory.
|
||||
c.Users[u.Username] = u
|
||||
|
||||
// Set the Location header and return.
|
||||
w.Header().Set("Location", "/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 *fm.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.db.DeleteStruct(&User{ID: id})
|
||||
if err == storm.ErrNotFound {
|
||||
return http.StatusNotFound, errUserNotExist
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Delete the user from the in-memory users map.
|
||||
for _, user := range c.Users {
|
||||
if user.ID == id {
|
||||
delete(c.Users, user.Username)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func usersPutHandler(c *fm.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(r)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
// Updates the CSS and locale.
|
||||
if which == "partial" {
|
||||
c.User.CSS = u.CSS
|
||||
c.User.Locale = u.Locale
|
||||
err = c.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// Updates the Password.
|
||||
if which == "password" {
|
||||
if u.Password == "" {
|
||||
return http.StatusBadRequest, errEmptyPassword
|
||||
}
|
||||
|
||||
pw, err := hashPassword(u.Password)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
c.User.Password = pw
|
||||
err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// If can only be all.
|
||||
if which != "all" {
|
||||
return http.StatusBadRequest, errInvalidUpdateField
|
||||
}
|
||||
|
||||
// Checks if username isn't empty.
|
||||
if u.Username == "" {
|
||||
return http.StatusBadRequest, errEmptyUsername
|
||||
}
|
||||
|
||||
// Checks if filesystem isn't empty.
|
||||
if u.FileSystem == "" {
|
||||
return http.StatusBadRequest, errEmptyScope
|
||||
}
|
||||
|
||||
// Checks if the scope exists.
|
||||
if code, err := checkFS(string(u.FileSystem)); err != nil {
|
||||
return code, err
|
||||
}
|
||||
|
||||
// Initialize rules if they're not initialized.
|
||||
if u.Rules == nil {
|
||||
u.Rules = []*Rule{}
|
||||
}
|
||||
|
||||
// Initialize commands if not initialized.
|
||||
if u.Commands == nil {
|
||||
u.Commands = []string{}
|
||||
}
|
||||
|
||||
// Gets the current saved user from the in-memory map.
|
||||
var suser *User
|
||||
for _, user := range c.Users {
|
||||
if user.ID == id {
|
||||
suser = user
|
||||
break
|
||||
}
|
||||
}
|
||||
if suser == nil {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
u.ID = id
|
||||
|
||||
// Changes the password if the request wants it.
|
||||
if u.Password != "" {
|
||||
pw, err := 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.db.Save(u)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// If the user changed the username, delete the old user
|
||||
// from the in-memory user map.
|
||||
if suser.Username != u.Username {
|
||||
delete(c.Users, suser.Username)
|
||||
}
|
||||
|
||||
c.Users[u.Username] = u
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
339
http/websockets.go
Normal file
339
http/websockets.go
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
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 *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Upgrades the connection to a websocket and checks for 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 cmd == command[0] {
|
||||
allowed = true
|
||||
}
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
err = conn.WriteMessage(websocket.BinaryMessage, cmdNotAllowed)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Check if the program is talled is installed on the computer.
|
||||
if _, err = exec.LookPath(command[0]); err != nil {
|
||||
err = conn.WriteMessage(websocket.BinaryMessage, cmdNotImplemented)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusNotImplemented, nil
|
||||
}
|
||||
|
||||
// Gets the path and initializes a buffer.
|
||||
path := string(c.User.FileSystem) + "/" + 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 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 {
|
||||
CaseInsensitive 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{
|
||||
CaseInsensitive: strings.Contains(value, "case:insensitive"),
|
||||
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.CaseInsensitive {
|
||||
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 *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Upgrades the connection to a websocket and checks for 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 = string(c.User.FileSystem) + 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.CaseInsensitive {
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue