Merge branch 'spl'

pull/212/head
Henrique Dias 2017-08-20 10:27:09 +01:00
commit bbffd10e0b
No known key found for this signature in database
GPG Key ID: 936F5EB68D786730
27 changed files with 10482 additions and 1202 deletions

View File

@ -1,92 +0,0 @@
package filemanager
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)
}
}

26
bolt/config.go Normal file
View File

@ -0,0 +1,26 @@
package bolt
import (
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
// ConfigStore is a configuration store.
type ConfigStore struct {
DB *storm.DB
}
// Get gets a configuration from the database to an interface.
func (c ConfigStore) Get(name string, to interface{}) error {
err := c.DB.Get("config", name, to)
if err == storm.ErrNotFound {
return fm.ErrNotExist
}
return err
}
// Save saves a configuration from an interface to the database.
func (c ConfigStore) Save(name string, from interface{}) error {
return c.DB.Set("config", name, from)
}

62
bolt/share.go Normal file
View File

@ -0,0 +1,62 @@
package bolt
import (
"github.com/asdine/storm"
"github.com/asdine/storm/q"
fm "github.com/hacdias/filemanager"
)
// ShareStore is a shareable links store.
type ShareStore struct {
DB *storm.DB
}
// Get gets a Share Link from an hash.
func (s ShareStore) Get(hash string) (*fm.ShareLink, error) {
var v *fm.ShareLink
err := s.DB.One("Hash", hash, &v)
if err == storm.ErrNotFound {
return v, fm.ErrNotExist
}
return v, err
}
// GetPermanent gets the permanent link from a path.
func (s ShareStore) GetPermanent(path string) (*fm.ShareLink, error) {
var v *fm.ShareLink
err := s.DB.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&v)
if err == storm.ErrNotFound {
return v, fm.ErrNotExist
}
return v, err
}
// GetByPath gets all the links for a specific path.
func (s ShareStore) GetByPath(hash string) ([]*fm.ShareLink, error) {
var v []*fm.ShareLink
err := s.DB.Find("Path", hash, &v)
if err == storm.ErrNotFound {
return v, fm.ErrNotExist
}
return v, err
}
// Gets retrieves all the shareable links.
func (s ShareStore) Gets() ([]*fm.ShareLink, error) {
var v []*fm.ShareLink
err := s.DB.All(&v)
return v, err
}
// Save stores a Share Link on the database.
func (s ShareStore) Save(l *fm.ShareLink) error {
return s.DB.Save(l)
}
// Delete deletes a Share Link from the database.
func (s ShareStore) Delete(hash string) error {
return s.DB.DeleteStruct(&fm.ShareLink{Hash: hash})
}

90
bolt/users.go Normal file
View File

@ -0,0 +1,90 @@
package bolt
import (
"reflect"
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
// UsersStore is a users store.
type UsersStore struct {
DB *storm.DB
}
// Get gets a user with a certain id from the database.
func (u UsersStore) Get(id int, builder fm.FSBuilder) (*fm.User, error) {
var us fm.User
err := u.DB.One("ID", id, &us)
if err == storm.ErrNotFound {
return nil, fm.ErrNotExist
}
if err != nil {
return nil, err
}
us.FileSystem = builder(us.Scope)
return &us, nil
}
// GetByUsername gets a user with a certain username from the database.
func (u UsersStore) GetByUsername(username string, builder fm.FSBuilder) (*fm.User, error) {
var us fm.User
err := u.DB.One("Username", username, &us)
if err == storm.ErrNotFound {
return nil, fm.ErrNotExist
}
if err != nil {
return nil, err
}
us.FileSystem = builder(us.Scope)
return &us, nil
}
// Gets gets all the users from the database.
func (u UsersStore) Gets(builder fm.FSBuilder) ([]*fm.User, error) {
var us []*fm.User
err := u.DB.All(&us)
if err == storm.ErrNotFound {
return nil, fm.ErrNotExist
}
if err != nil {
return us, err
}
for _, user := range us {
user.FileSystem = builder(user.Scope)
}
return us, err
}
// Update updates the whole user object or only certain fields.
func (u UsersStore) Update(us *fm.User, fields ...string) error {
if len(fields) == 0 {
return u.Save(us)
}
for _, field := range fields {
val := reflect.ValueOf(us).Elem().FieldByName(field).Interface()
if err := u.DB.UpdateField(us, field, val); err != nil {
return err
}
}
return nil
}
// Save saves a user to the database.
func (u UsersStore) Save(us *fm.User) error {
return u.DB.Save(us)
}
// Delete deletes a user from the database.
func (u UsersStore) Delete(id int) error {
return u.DB.DeleteStruct(&fm.User{ID: id})
}

View File

@ -8,6 +8,7 @@ import (
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
@ -32,7 +33,7 @@ func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
continue continue
} }
f.Configs[i].ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }

View File

@ -5,6 +5,7 @@ import (
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
@ -29,7 +30,7 @@ func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
continue continue
} }
f.Configs[i].ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }

View File

@ -5,6 +5,7 @@ import (
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
@ -29,7 +30,7 @@ func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
continue continue
} }
f.Configs[i].ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }

View File

@ -10,12 +10,18 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/asdine/storm"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt"
"github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/spf13/viper"
) )
var databases = map[string]*storm.DB{}
// Parse ... // Parse ...
func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) { func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) {
var ( var (
@ -24,7 +30,7 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
) )
for c.Next() { for c.Next() {
u := filemanager.User{ u := &filemanager.User{
Locale: "en", Locale: "en",
AllowCommands: true, AllowCommands: true,
AllowEdit: true, AllowEdit: true,
@ -183,13 +189,45 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n") ". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
} }
u.Scope = scope
u.FileSystem = fileutils.Dir(scope) u.FileSystem = fileutils.Dir(scope)
m, err := filemanager.New(database, u)
var db *storm.DB
if stored, ok := databases[database]; ok {
db = stored
} else {
db, err = storm.Open(database)
databases[database] = db
}
if err != nil {
return nil, err
}
m := &filemanager.FileManager{
NoAuth: viper.GetBool("NoAuth"),
BaseURL: "",
PrefixURL: "",
DefaultUser: u,
Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db},
},
NewFS: func(scope string) filemanager.FileSystem {
return fileutils.Dir(scope)
},
}
err = m.Setup()
if err != nil {
return nil, err
}
switch plugin { switch plugin {
case "hugo": case "hugo":
// Initialize the default settings for Hugo. // Initialize the default settings for Hugo.
hugo := &filemanager.Hugo{ hugo := &staticgen.Hugo{
Root: scope, Root: scope,
Public: filepath.Join(scope, "public"), Public: filepath.Join(scope, "public"),
Args: []string{}, Args: []string{},
@ -197,13 +235,13 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
} }
// Attaches Hugo plugin to this file manager instance. // Attaches Hugo plugin to this file manager instance.
err = m.EnableStaticGen(hugo) err = m.Attach(hugo)
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "jekyll": case "jekyll":
// Initialize the default settings for Jekyll. // Initialize the default settings for Jekyll.
jekyll := &filemanager.Jekyll{ jekyll := &staticgen.Jekyll{
Root: scope, Root: scope,
Public: filepath.Join(scope, "_site"), Public: filepath.Join(scope, "_site"),
Args: []string{}, Args: []string{},
@ -211,7 +249,7 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
} }
// Attaches Hugo plugin to this file manager instance. // Attaches Hugo plugin to this file manager instance.
err = m.EnableStaticGen(jekyll) err = m.Attach(jekyll)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -10,9 +10,14 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/asdine/storm"
lumberjack "gopkg.in/natefinch/lumberjack.v2" lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt"
h "github.com/hacdias/filemanager/http"
"github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -25,7 +30,7 @@ var (
scope string scope string
commands string commands string
logfile string logfile string
staticgen string staticg string
locale string locale string
port int port int
noAuth bool noAuth bool
@ -51,7 +56,7 @@ func init() {
flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users") flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users")
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication") flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
flag.StringVar(&locale, "locale", "en", "Default locale for new users") flag.StringVar(&locale, "locale", "en", "Default locale for new users")
flag.StringVar(&staticgen, "staticgen", "", "Static Generator you want to enable") flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable")
flag.BoolVarP(&showVer, "version", "v", false, "Show version") flag.BoolVarP(&showVer, "version", "v", false, "Show version")
} }
@ -148,52 +153,6 @@ func main() {
}) })
} }
// Create a File Manager instance.
fm, err := filemanager.New(viper.GetString("Database"), filemanager.User{
AllowCommands: viper.GetBool("AllowCommands"),
AllowEdit: viper.GetBool("AllowEdit"),
AllowNew: viper.GetBool("AllowNew"),
AllowPublish: viper.GetBool("AllowPublish"),
Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"),
CSS: "",
FileSystem: fileutils.Dir(viper.GetString("Scope")),
})
if viper.GetBool("NoAuth") {
fm.NoAuth = true
}
if err != nil {
log.Fatal(err)
}
switch viper.GetString("StaticGen") {
case "hugo":
hugo := &filemanager.Hugo{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "public"),
Args: []string{},
CleanPublic: true,
}
if err = fm.EnableStaticGen(hugo); err != nil {
log.Fatal(err)
}
case "jekyll":
jekyll := &filemanager.Jekyll{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "_site"),
Args: []string{"build"},
CleanPublic: true,
}
if err = fm.EnableStaticGen(jekyll); err != nil {
log.Fatal(err)
}
}
// Builds the address and a listener. // Builds the address and a listener.
laddr := viper.GetString("Address") + ":" + viper.GetString("Port") laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
listener, err := net.Listen("tcp", laddr) listener, err := net.Listen("tcp", laddr)
@ -205,7 +164,72 @@ func main() {
fmt.Println("Listening on", listener.Addr().String()) fmt.Println("Listening on", listener.Addr().String())
// Starts the server. // Starts the server.
if err := http.Serve(listener, fm); err != nil { if err := http.Serve(listener, handler()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func handler() http.Handler {
db, err := storm.Open(viper.GetString("Database"))
if err != nil {
log.Fatal(err)
}
fm := &filemanager.FileManager{
NoAuth: viper.GetBool("NoAuth"),
BaseURL: "",
PrefixURL: "",
DefaultUser: &filemanager.User{
AllowCommands: viper.GetBool("AllowCommands"),
AllowEdit: viper.GetBool("AllowEdit"),
AllowNew: viper.GetBool("AllowNew"),
AllowPublish: viper.GetBool("AllowPublish"),
Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"),
CSS: "",
Scope: viper.GetString("Scope"),
FileSystem: fileutils.Dir(viper.GetString("Scope")),
},
Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db},
},
NewFS: func(scope string) filemanager.FileSystem {
return fileutils.Dir(scope)
},
}
err = fm.Setup()
if err != nil {
log.Fatal(err)
}
switch viper.GetString("StaticGen") {
case "hugo":
hugo := &staticgen.Hugo{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "public"),
Args: []string{},
CleanPublic: true,
}
if err = fm.Attach(hugo); err != nil {
log.Fatal(err)
}
case "jekyll":
jekyll := &staticgen.Jekyll{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "_site"),
Args: []string{"build"},
CleanPublic: true,
}
if err = fm.Attach(jekyll); err != nil {
log.Fatal(err)
}
}
return h.Handler(fm)
}

73
doc.go Normal file
View File

@ -0,0 +1,73 @@
/*
Package filemanager provides a web interface to access your files
wherever you are. To use this package as a middleware for your app,
you'll need to import both File Manager and File Manager HTTP packages.
import (
fm "github.com/hacdias/filemanager"
h "github.com/hacdias/filemanager/http"
)
Then, you should create a new FileManager object with your options. In this
case, I'm using BoltDB (via Storm package) as a Store. So, you'll also need
to import "github.com/hacdias/filemanager/bolt".
db, _ := storm.Open("bolt.db")
m := &fm.FileManager{
NoAuth: false,
DefaultUser: &fm.User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{"git"},
Rules: []*fm.Rule{},
Locale: "en",
CSS: "",
Scope: ".",
FileSystem: fileutils.Dir("."),
},
Store: &fm.Store{
Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db},
},
NewFS: func(scope string) fm.FileSystem {
return fileutils.Dir(scope)
},
}
The credentials for the first user are always 'admin' for both the user and
the password, and they can be changed later through the settings. The first
user is always an Admin and has all of the permissions set to 'true'.
Then, you should set the Prefix URL and the Base URL, using the following
functions:
m.SetBaseURL("/")
m.SetPrefixURL("/")
The Prefix URL is a part of the path that is already stripped from the
r.URL.Path variable before the request arrives to File Manager's handler.
This is a function that will rarely be used. You can see one example on Caddy
filemanager plugin.
The Base URL is the URL path where you want File Manager to be available in. If
you want to be available at the root path, you should call:
m.SetBaseURL("/")
But if you want to access it at '/admin', you would call:
m.SetBaseURL("/admin")
Now, that you already have a File Manager instance created, you just need to
add it to your handlers using m.ServeHTTP which is compatible to http.Handler.
We also have a m.ServeWithErrorsHTTP that returns the status code and an error.
One simple implementation for this, at port 80, in the root of the domain, would be:
http.ListenAndServe(":80", h.Handler(m))
*/
package filemanager

70
file.go
View File

@ -7,7 +7,6 @@ import (
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
"errors"
"hash" "hash"
"io" "io"
"io/ioutil" "io/ioutil"
@ -23,13 +22,9 @@ import (
"github.com/gohugoio/hugo/parser" "github.com/gohugoio/hugo/parser"
) )
var ( // File contains the information about a particular file or directory.
errInvalidOption = errors.New("Invalid option") type File struct {
) // Indicates the Kind of view on the front-end (Listing, editor or preview).
// file contains the information about a particular file or directory.
type file struct {
// Indicates the Kind of view on the front-end (listing, editor or preview).
Kind string `json:"kind"` Kind string `json:"kind"`
// The name of the file. // The name of the file.
Name string `json:"name"` Name string `json:"name"`
@ -54,19 +49,19 @@ type file struct {
// Stores the content of a text file. // Stores the content of a text file.
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
*listing `json:",omitempty"` *Listing `json:",omitempty"`
Metadata string `json:"metadata,omitempty"` Metadata string `json:"metadata,omitempty"`
Language string `json:"language,omitempty"` Language string `json:"language,omitempty"`
} }
// A listing is the context used to fill out a template. // A Listing is the context used to fill out a template.
type listing struct { type Listing struct {
// The items (files and folders) in the path. // The items (files and folders) in the path.
Items []*file `json:"items"` Items []*File `json:"items"`
// The number of directories in the listing. // The number of directories in the Listing.
NumDirs int `json:"numDirs"` NumDirs int `json:"numDirs"`
// The number of files (items that aren't directories) in the listing. // The number of files (items that aren't directories) in the Listing.
NumFiles int `json:"numFiles"` NumFiles int `json:"numFiles"`
// Which sorting order is used. // Which sorting order is used.
Sort string `json:"sort"` Sort string `json:"sort"`
@ -76,15 +71,15 @@ type listing struct {
Display string `json:"display"` Display string `json:"display"`
} }
// getInfo gets the file information and, in case of error, returns the // GetInfo gets the file information and, in case of error, returns the
// respective HTTP error code // respective HTTP error code
func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) { func GetInfo(url *url.URL, c *FileManager, u *User) (*File, error) {
var err error var err error
i := &file{ i := &File{
URL: "/files" + url.String(), URL: "/files" + url.String(),
VirtualPath: url.Path, VirtualPath: url.Path,
Path: filepath.Join(string(u.FileSystem), url.Path), Path: filepath.Join(u.Scope, url.Path),
} }
info, err := u.FileSystem.Stat(url.Path) info, err := u.FileSystem.Stat(url.Path)
@ -106,11 +101,11 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
return i, nil return i, nil
} }
// getListing gets the information about a specific directory and its files. // GetListing gets the information about a specific directory and its files.
func (i *file) getListing(c *RequestContext, r *http.Request) error { func (i *File) GetListing(u *User, r *http.Request) error {
// Gets the directory information using the Virtual File System of // Gets the directory information using the Virtual File System of
// the user configuration. // the user configuration.
f, err := c.User.FileSystem.OpenFile(c.File.VirtualPath, os.O_RDONLY, 0) f, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0)
if err != nil { if err != nil {
return err return err
} }
@ -123,7 +118,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
} }
var ( var (
fileinfos []*file fileinfos []*File
dirCount, fileCount int dirCount, fileCount int
) )
@ -134,7 +129,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
for _, f := range files { for _, f := range files {
name := f.Name() name := f.Name()
allowed := c.User.Allowed("/" + name) allowed := u.Allowed("/" + name)
if !allowed { if !allowed {
continue continue
@ -150,7 +145,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
// Absolute URL // Absolute URL
url := url.URL{Path: baseurl + name} url := url.URL{Path: baseurl + name}
i := &file{ i := &File{
Name: f.Name(), Name: f.Name(),
Size: f.Size(), Size: f.Size(),
ModTime: f.ModTime(), ModTime: f.ModTime(),
@ -166,7 +161,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
fileinfos = append(fileinfos, i) fileinfos = append(fileinfos, i)
} }
i.listing = &listing{ i.Listing = &Listing{
Items: fileinfos, Items: fileinfos,
NumDirs: dirCount, NumDirs: dirCount,
NumFiles: fileCount, NumFiles: fileCount,
@ -175,8 +170,8 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
return nil return nil
} }
// getEditor gets the editor based on a Info struct // GetEditor gets the editor based on a Info struct
func (i *file) getEditor() error { func (i *File) GetEditor() error {
i.Language = editorLanguage(i.Extension) i.Language = editorLanguage(i.Extension)
// If the editor will hold only content, leave now. // If the editor will hold only content, leave now.
if editorMode(i.Language) == "content" { if editorMode(i.Language) == "content" {
@ -205,7 +200,7 @@ func (i *file) getEditor() error {
// GetFileType obtains the mimetype and converts it to a simple // GetFileType obtains the mimetype and converts it to a simple
// type nomenclature. // type nomenclature.
func (i *file) GetFileType(checkContent bool) error { func (i *File) GetFileType(checkContent bool) error {
var content []byte var content []byte
var err error var err error
@ -283,7 +278,8 @@ End:
return nil return nil
} }
func (i file) Checksum(kind string) (string, error) { // Checksum retrieves the checksum of a file.
func (i File) Checksum(algo string) (string, error) {
file, err := os.Open(i.Path) file, err := os.Open(i.Path)
if err != nil { if err != nil {
return "", err return "", err
@ -293,7 +289,7 @@ func (i file) Checksum(kind string) (string, error) {
var h hash.Hash var h hash.Hash
switch kind { switch algo {
case "md5": case "md5":
h = md5.New() h = md5.New()
case "sha1": case "sha1":
@ -303,7 +299,7 @@ func (i file) Checksum(kind string) (string, error) {
case "sha512": case "sha512":
h = sha512.New() h = sha512.New()
default: default:
return "", errInvalidOption return "", ErrInvalidOption
} }
_, err = io.Copy(h, file) _, err = io.Copy(h, file)
@ -315,12 +311,12 @@ func (i file) Checksum(kind string) (string, error) {
} }
// CanBeEdited checks if the extension of a file is supported by the editor // CanBeEdited checks if the extension of a file is supported by the editor
func (i file) CanBeEdited() bool { func (i File) CanBeEdited() bool {
return i.Type == "text" return i.Type == "text"
} }
// ApplySort applies the sort order using .Order and .Sort // ApplySort applies the sort order using .Order and .Sort
func (l listing) ApplySort() { func (l Listing) ApplySort() {
// Check '.Order' to know how to sort // Check '.Order' to know how to sort
if l.Order == "desc" { if l.Order == "desc" {
switch l.Sort { switch l.Sort {
@ -349,10 +345,10 @@ func (l listing) ApplySort() {
} }
} }
// Implement sorting for listing // Implement sorting for Listing
type byName listing type byName Listing
type bySize listing type bySize Listing
type byModified listing type byModified Listing
// By Name // By Name
func (l byName) Len() int { func (l byName) Len() int {

View File

@ -1,59 +1,7 @@
// Package filemanager provides a web interface to access your files
// wherever you are. To use this package as a middleware for your app,
// you'll need to create a filemanager instance:
//
// m, err := filemanager.New(database, user)
//
// Where 'user' contains the default options for new users. You can just
// use 'filemanager.DefaultUser' or create yourself a default user:
//
// m, err := filemanager.New(database, filemanager.User{
// Admin: false,
// AllowCommands: false,
// AllowEdit: true,
// AllowNew: true,
// Commands: []string{
// "git",
// },
// Rules: []*filemanager.Rule{},
// CSS: "",
// FileSystem: webdav.Dir("/path/to/files"),
// })
//
// The credentials for the first user are always 'admin' for both the user and
// the password, and they can be changed later through the settings. The first
// user is always an Admin and has all of the permissions set to 'true'.
//
// Then, you should set the Prefix URL and the Base URL, using the following
// functions:
//
// m.SetBaseURL("/")
// m.SetPrefixURL("/")
//
// The Prefix URL is a part of the path that is already stripped from the
// r.URL.Path variable before the request arrives to File Manager's handler.
// This is a function that will rarely be used. You can see one example on Caddy
// filemanager plugin.
//
// The Base URL is the URL path where you want File Manager to be available in. If
// you want to be available at the root path, you should call:
//
// m.SetBaseURL("/")
//
// But if you want to access it at '/admin', you would call:
//
// m.SetBaseURL("/admin")
//
// Now, that you already have a File Manager instance created, you just need to
// add it to your handlers using m.ServeHTTP which is compatible to http.Handler.
// We also have a m.ServeWithErrorsHTTP that returns the status code and an error.
//
// One simple implementation for this, at port 80, in the root of the domain, would be:
//
// http.ListenAndServe(":80", m)
package filemanager package filemanager
import ( import (
"crypto/rand"
"errors" "errors"
"log" "log"
"net/http" "net/http"
@ -64,38 +12,41 @@ import (
"strings" "strings"
"time" "time"
"golang.org/x/crypto/bcrypt"
rice "github.com/GeertJohan/go.rice" rice "github.com/GeertJohan/go.rice"
"github.com/asdine/storm"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/robfig/cron" "github.com/robfig/cron"
) )
var ( var (
errUserExist = errors.New("user already exists") ErrExist = errors.New("the resource already exists")
errUserNotExist = errors.New("user does not exist") ErrNotExist = errors.New("the resource does not exist")
errEmptyRequest = errors.New("request body is empty") ErrEmptyRequest = errors.New("request body is empty")
errEmptyPassword = errors.New("password is empty") ErrEmptyPassword = errors.New("password is empty")
errEmptyUsername = errors.New("username is empty") ErrEmptyUsername = errors.New("username is empty")
errEmptyScope = errors.New("scope is empty") ErrEmptyScope = errors.New("scope is empty")
errWrongDataType = errors.New("wrong data type") ErrWrongDataType = errors.New("wrong data type")
errInvalidUpdateField = errors.New("invalid field to update") ErrInvalidUpdateField = errors.New("invalid field to update")
ErrInvalidOption = errors.New("Invalid option")
) )
// FileManager is a file manager instance. It should be creating using the // FileManager is a file manager instance. It should be creating using the
// 'New' function and not directly. // 'New' function and not directly.
type FileManager struct { type FileManager struct {
// The BoltDB database for this instance. // Cron job to manage schedulings.
db *storm.DB Cron *cron.Cron
// The key used to sign the JWT tokens. // The key used to sign the JWT tokens.
key []byte Key []byte
// The static assets. // The static assets.
assets *rice.Box Assets *rice.Box
// Job cron. // The Store is used to manage users, shareable links and
cron *cron.Cron // other stuff that is saved on the database.
Store *Store
// PrefixURL is a part of the URL that is already trimmed from the request URL before it // PrefixURL is a part of the URL that is already trimmed from the request URL before it
// arrives to our handlers. It may be useful when using File Manager as a middleware // arrives to our handlers. It may be useful when using File Manager as a middleware
@ -111,174 +62,85 @@ type FileManager struct {
// there will only exist one user, called "admin". // there will only exist one user, called "admin".
NoAuth bool NoAuth bool
// staticgen is the name of the current static website generator.
staticgen string
// StaticGen is the static websit generator handler. // StaticGen is the static websit generator handler.
StaticGen StaticGen StaticGen StaticGen
// The Default User needed to build the New User page. // The Default User needed to build the New User page.
DefaultUser *User DefaultUser *User
// Users is a map with the different configurations for each user.
Users map[string]*User
// A map of events to a slice of commands. // A map of events to a slice of commands.
Commands map[string][]string Commands map[string][]string
// NewFS should build a new file system for a given path.
NewFS FSBuilder
} }
// Command is a command function. // Command is a command function.
type Command func(r *http.Request, m *FileManager, u *User) error type Command func(r *http.Request, m *FileManager, u *User) error
// User contains the configuration for each user. // FSBuilder is the File System Builder.
type User struct { type FSBuilder func(scope string) FileSystem
// ID is the required primary key with auto increment0
ID int `storm:"id,increment"`
// Username is the user username used to login. // Setup loads the configuration from the database and configures
Username string `json:"username" storm:"index,unique"` // the Assets and the Cron job. It must always be run after
// creating a File Manager object.
// The hashed password. This never reaches the front-end because it's temporarily func (m *FileManager) Setup() error {
// emptied during JSON marshall.
Password string `json:"password"`
// Tells if this user is an admin.
Admin bool `json:"admin"`
// FileSystem is the virtual file system the user has access.
FileSystem fileutils.Dir `json:"filesystem"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
// Custom styles for this user.
CSS string `json:"css"`
// Locale is the language of the user.
Locale string `json:"locale"`
// These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
// Commands is the list of commands the user can execute.
Commands []string `json:"commands"`
}
// Rule is a dissalow/allow rule.
type Rule struct {
// Regex indicates if this rule uses Regular Expressions or not.
Regex bool `json:"regex"`
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
Allow bool `json:"allow"`
// Path is the corresponding URL path for this rule.
Path string `json:"path"`
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
Regexp *Regexp `json:"regexp"`
}
// Regexp is a regular expression wrapper around native regexp.
type Regexp struct {
Raw string `json:"raw"`
regexp *regexp.Regexp
}
// DefaultUser is used on New, when no 'base' user is provided.
var DefaultUser = User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{},
Rules: []*Rule{},
CSS: "",
Admin: true,
Locale: "en",
FileSystem: fileutils.Dir("."),
}
// New creates a new File Manager instance. If 'database' file already
// exists, it will load the users from there. Otherwise, a new user
// will be created using the 'base' variable. The 'base' User should
// not have the Password field hashed.
func New(database string, base User) (*FileManager, error) {
// Creates a new File Manager instance with the Users // Creates a new File Manager instance with the Users
// map and Assets box. // map and Assets box.
m := &FileManager{ m.Assets = rice.MustFindBox("./assets/dist")
Users: map[string]*User{}, m.Cron = cron.New()
cron: cron.New(),
assets: rice.MustFindBox("./assets/dist"),
}
// Tries to open a database on the location provided. This
// function will automatically create a new one if it doesn't
// exist.
db, err := storm.Open(database)
if err != nil {
return nil, err
}
// Tries to get the encryption key from the database. // Tries to get the encryption key from the database.
// If it doesn't exist, create a new one of 256 bits. // If it doesn't exist, create a new one of 256 bits.
err = db.Get("config", "key", &m.key) err := m.Store.Config.Get("key", &m.Key)
if err != nil && err == storm.ErrNotFound { if err != nil && err == ErrNotExist {
var bytes []byte var bytes []byte
bytes, err = generateRandomBytes(64) bytes, err = GenerateRandomBytes(64)
if err != nil { if err != nil {
return nil, err return err
} }
m.key = bytes m.Key = bytes
err = db.Set("config", "key", m.key) err = m.Store.Config.Save("key", m.Key)
} }
if err != nil { if err != nil {
return nil, err return err
} }
// Tries to get the event commands from the database. // Tries to get the event commands from the database.
// If they don't exist, initialize them. // If they don't exist, initialize them.
err = db.Get("config", "commands", &m.Commands) err = m.Store.Config.Get("commands", &m.Commands)
if err != nil && err == storm.ErrNotFound { if err != nil && err == ErrNotExist {
m.Commands = map[string][]string{ m.Commands = map[string][]string{
"before_save": {}, "before_save": {},
"after_save": {}, "after_save": {},
"before_publish": {}, "before_publish": {},
"after_publish": {}, "after_publish": {},
} }
err = db.Set("config", "commands", m.Commands) err = m.Store.Config.Save("commands", m.Commands)
} }
if err != nil { if err != nil {
return nil, err return err
} }
// Tries to fetch the users from the database and if there are // Tries to fetch the users from the database.
// any, add them to the current File Manager instance. users, err := m.Store.Users.Gets(m.NewFS)
var users []User if err != nil && err != ErrNotExist {
err = db.All(&users) return err
if err != nil {
return nil, err
}
for i := range users {
m.Users[users[i].Username] = &users[i]
} }
// If there are no users in the database, it creates a new one // If there are no users in the database, it creates a new one
// based on 'base' User that must be provided by the function caller. // based on 'base' User that must be provided by the function caller.
if len(users) == 0 { if len(users) == 0 {
u := base u := *m.DefaultUser
u.Username = "admin" u.Username = "admin"
// Hashes the password. // Hashes the password.
u.Password, err = hashPassword("admin") u.Password, err = HashPassword("admin")
if err != nil { if err != nil {
return nil, err return err
} }
// The first user must be an administrator. // The first user must be an administrator.
@ -289,25 +151,18 @@ func New(database string, base User) (*FileManager, error) {
u.AllowPublish = true u.AllowPublish = true
// Saves the user to the database. // Saves the user to the database.
if err := db.Save(&u); err != nil { if err := m.Store.Users.Save(&u); err != nil {
return nil, err return err
} }
m.Users[u.Username] = &u
} }
// Attaches db to this File Manager instance. m.DefaultUser.Username = ""
m.db = db m.DefaultUser.Password = ""
// Create the default user, making a copy of the base. m.Cron.AddFunc("@hourly", m.ShareCleaner)
base.Username = "" m.Cron.Start()
base.Password = ""
m.DefaultUser = &base
m.cron.AddFunc("@hourly", m.shareCleaner) return nil
m.cron.Start()
return m, nil
} }
// RootURL returns the actual URL where // RootURL returns the actual URL where
@ -334,95 +189,32 @@ func (m *FileManager) SetBaseURL(url string) {
m.BaseURL = strings.TrimSuffix(url, "/") m.BaseURL = strings.TrimSuffix(url, "/")
} }
// ServeHTTP handles the request. // Attach attaches a static generator to the current File Manager.
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (m *FileManager) Attach(s StaticGen) error {
code, err := serveHTTP(&RequestContext{ if reflect.TypeOf(s).Kind() != reflect.Ptr {
FileManager: m,
User: nil,
File: nil,
}, w, r)
if code >= 400 {
w.WriteHeader(code)
if err == nil {
txt := http.StatusText(code)
log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
w.Write([]byte(txt))
}
}
if err != nil {
log.Print(err)
w.Write([]byte(err.Error()))
}
}
// EnableStaticGen attaches a static generator to the current File Manager
// instance.
func (m *FileManager) EnableStaticGen(data StaticGen) error {
if reflect.TypeOf(data).Kind() != reflect.Ptr {
return errors.New("data should be a pointer to interface, not interface") return errors.New("data should be a pointer to interface, not interface")
} }
if h, ok := data.(*Hugo); ok { err := s.Setup()
return m.enableHugo(h) if err != nil {
}
if j, ok := data.(*Jekyll); ok {
return m.enableJekyll(j)
}
return errors.New("unknown static website generator")
}
func (m *FileManager) enableHugo(h *Hugo) error {
if err := h.find(); err != nil {
return err return err
} }
m.staticgen = "hugo" m.StaticGen = s
m.StaticGen = h
err := m.db.Get("staticgen", "hugo", h) err = m.Store.Config.Get("staticgen_"+s.Name(), s)
if err != nil && err == storm.ErrNotFound { if err == ErrNotExist {
err = m.db.Set("staticgen", "hugo", *h) return m.Store.Config.Save("staticgen_"+s.Name(), s)
} }
return nil return err
} }
func (m *FileManager) enableJekyll(j *Jekyll) error { // ShareCleaner removes sharing links that are no longer active.
if err := j.find(); err != nil {
return err
}
if len(j.Args) == 0 {
j.Args = []string{"build"}
}
if j.Args[0] != "build" {
j.Args = append([]string{"build"}, j.Args...)
}
m.staticgen = "jekyll"
m.StaticGen = j
err := m.db.Get("staticgen", "jekyll", j)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("staticgen", "jekyll", *j)
}
return nil
}
// shareCleaner removes sharing links that are no longer active.
// This function is set to run periodically. // This function is set to run periodically.
func (m FileManager) shareCleaner() { func (m FileManager) ShareCleaner() {
var links []shareLink
// Get all links. // Get all links.
err := m.db.All(&links) links, err := m.Store.Share.Gets()
if err != nil { if err != nil {
log.Print(err) log.Print(err)
return return
@ -431,7 +223,7 @@ func (m FileManager) shareCleaner() {
// Find the expired ones. // Find the expired ones.
for i := range links { for i := range links {
if links[i].Expires && links[i].ExpireDate.Before(time.Now()) { if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
err = m.db.DeleteStruct(&links[i]) err = m.Store.Share.Delete(links[i].Hash)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
} }
@ -439,37 +231,6 @@ func (m FileManager) shareCleaner() {
} }
} }
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule
i := len(u.Rules) - 1
for i >= 0 {
rule = u.Rules[i]
if rule.Regex {
if rule.Regexp.MatchString(url) {
return rule.Allow
}
} else if strings.HasPrefix(url, rule.Path) {
return rule.Allow
}
i--
}
return true
}
// MatchString checks if this string matches the regular expression.
func (r *Regexp) MatchString(s string) bool {
if r.regexp == nil {
r.regexp = regexp.MustCompile(r.Raw)
}
return r.regexp.MatchString(s)
}
// Runner runs the commands for a certain event type. // Runner runs the commands for a certain event type.
func (m FileManager) Runner(event string, path string) error { func (m FileManager) Runner(event string, path string) error {
commands := []string{} commands := []string{}
@ -518,3 +279,209 @@ func (m FileManager) Runner(event string, path string) error {
return nil return nil
} }
// DefaultUser is used on New, when no 'base' user is provided.
var DefaultUser = User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{},
Rules: []*Rule{},
CSS: "",
Admin: true,
Locale: "en",
Scope: ".",
FileSystem: fileutils.Dir("."),
}
// User contains the configuration for each user.
type User struct {
// ID is the required primary key with auto increment0
ID int `storm:"id,increment"`
// Username is the user username used to login.
Username string `json:"username" storm:"index,unique"`
// The hashed password. This never reaches the front-end because it's temporarily
// emptied during JSON marshall.
Password string `json:"password"`
// Tells if this user is an admin.
Admin bool `json:"admin"`
// Scope is the path the user has access to.
Scope string `json:"filesystem"`
// FileSystem is the virtual file system the user has access.
FileSystem FileSystem `json:"-"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
// Custom styles for this user.
CSS string `json:"css"`
// Locale is the language of the user.
Locale string `json:"locale"`
// These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
// Commands is the list of commands the user can execute.
Commands []string `json:"commands"`
}
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule
i := len(u.Rules) - 1
for i >= 0 {
rule = u.Rules[i]
if rule.Regex {
if rule.Regexp.MatchString(url) {
return rule.Allow
}
} else if strings.HasPrefix(url, rule.Path) {
return rule.Allow
}
i--
}
return true
}
// Rule is a dissalow/allow rule.
type Rule struct {
// Regex indicates if this rule uses Regular Expressions or not.
Regex bool `json:"regex"`
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
Allow bool `json:"allow"`
// Path is the corresponding URL path for this rule.
Path string `json:"path"`
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
Regexp *Regexp `json:"regexp"`
}
// Regexp is a regular expression wrapper around native regexp.
type Regexp struct {
Raw string `json:"raw"`
regexp *regexp.Regexp
}
// MatchString checks if this string matches the regular expression.
func (r *Regexp) MatchString(s string) bool {
if r.regexp == nil {
r.regexp = regexp.MustCompile(r.Raw)
}
return r.regexp.MatchString(s)
}
// ShareLink is the information needed to build a shareable link.
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"`
}
// Store is a collection of the stores needed to get
// and save information.
type Store struct {
Users UsersStore
Config ConfigStore
Share ShareStore
}
// UsersStore is the interface to manage users.
type UsersStore interface {
Get(id int, builder FSBuilder) (*User, error)
GetByUsername(username string, builder FSBuilder) (*User, error)
Gets(builder FSBuilder) ([]*User, error)
Save(u *User) error
Update(u *User, fields ...string) error
Delete(id int) error
}
// ConfigStore is the interface to manage configuration.
type ConfigStore interface {
Get(name string, to interface{}) error
Save(name string, from interface{}) error
}
// ShareStore is the interface to manage share links.
type ShareStore interface {
Get(hash string) (*ShareLink, error)
GetPermanent(path string) (*ShareLink, error)
GetByPath(path string) ([]*ShareLink, error)
Gets() ([]*ShareLink, error)
Save(s *ShareLink) error
Delete(hash string) error
}
// StaticGen is a static website generator.
type StaticGen interface {
SettingsPath() string
Name() string
Setup() error
Hook(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
Preview(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
Publish(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
}
// FileSystem is the interface to work with the file system.
type FileSystem interface {
Mkdir(name string, perm os.FileMode) error
OpenFile(name string, flag int, perm os.FileMode) (*os.File, error)
RemoveAll(name string) error
Rename(oldName, newName string) error
Stat(name string) (os.FileInfo, error)
Copy(src, dst string) error
}
// Context contains the needed information to make handlers work.
type Context struct {
*FileManager
User *User
File *File
// On API handlers, Router is the APi handler we want.
Router string
}
// 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 fm.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
}

View File

@ -1,49 +0,0 @@
package filemanager
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/hacdias/fileutils"
)
type test struct {
*FileManager
Temp string
}
func (t test) Clean() {
t.db.Close()
os.RemoveAll(t.Temp)
}
func newTest(t *testing.T) *test {
temp, err := ioutil.TempDir("", t.Name())
if err != nil {
t.Fatalf("Error creating temporary directory: %v", err)
}
scope := filepath.Join(temp, "scope")
database := filepath.Join(temp, "database.db")
err = fileutils.CopyDir("./testdata", scope)
if err != nil {
t.Fatalf("Error copying the test data: %v", err)
}
user := DefaultUser
user.FileSystem = fileutils.Dir(scope)
fm, err := New(database, user)
if err != nil {
t.Fatalf("Error creating a file manager instance: %v", err)
}
return &test{
FileManager: fm,
Temp: temp,
}
}

View File

@ -1,27 +1,25 @@
package filemanager package http
import ( import (
"crypto/rand"
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"golang.org/x/crypto/bcrypt"
jwt "github.com/dgrijalva/jwt-go" jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request" "github.com/dgrijalva/jwt-go/request"
fm "github.com/hacdias/filemanager"
) )
// authHandler proccesses the authentication for the user. // authHandler proccesses the authentication for the user.
func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// NoAuth instances shouldn't call this method. // NoAuth instances shouldn't call this method.
if c.NoAuth { if c.NoAuth {
return 0, nil return 0, nil
} }
// Receive the credentials from the request and unmarshal them. // Receive the credentials from the request and unmarshal them.
var cred User var cred fm.User
if r.Body == nil { if r.Body == nil {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -32,13 +30,13 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
} }
// Checks if the user exists. // Checks if the user exists.
u, ok := c.Users[cred.Username] u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
if !ok { if err != nil {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
// Checks if the password is correct. // Checks if the password is correct.
if !checkPasswordHash(cred.Password, u.Password) { if !fm.CheckPasswordHash(cred.Password, u.Password) {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -48,7 +46,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
// renewAuthHandler is used when the front-end already has a JWT token // 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. // and is checking if it is up to date. If so, updates its info.
func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func renewAuthHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
ok, u := validateAuth(c, r) ok, u := validateAuth(c, r)
if !ok { if !ok {
return http.StatusForbidden, nil return http.StatusForbidden, nil
@ -60,16 +58,16 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// claims is the JWT claims. // claims is the JWT claims.
type claims struct { type claims struct {
User fm.User
NoAuth bool `json:"noAuth"` NoAuth bool `json:"noAuth"`
jwt.StandardClaims jwt.StandardClaims
} }
// printToken prints the final JWT token to the user. // printToken prints the final JWT token to the user.
func printToken(c *RequestContext, w http.ResponseWriter) (int, error) { func printToken(c *fm.Context, w http.ResponseWriter) (int, error) {
// Creates a copy of the user and removes it password // Creates a copy of the user and removes it password
// hash so it never arrives to the user. // hash so it never arrives to the user.
u := User{} u := fm.User{}
u = *c.User u = *c.User
u.Password = "" u.Password = ""
@ -85,7 +83,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
// Creates the token and signs it. // Creates the token and signs it.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(c.key) signed, err := token.SignedString(c.Key)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
@ -119,14 +117,14 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
// validateAuth is used to validate the authentication and returns the // validateAuth is used to validate the authentication and returns the
// User if it is valid. // User if it is valid.
func validateAuth(c *RequestContext, r *http.Request) (bool, *User) { func validateAuth(c *fm.Context, r *http.Request) (bool, *fm.User) {
if c.NoAuth { if c.NoAuth {
c.User = c.DefaultUser c.User = c.DefaultUser
return true, c.User return true, c.User
} }
keyFunc := func(token *jwt.Token) (interface{}, error) { keyFunc := func(token *jwt.Token) (interface{}, error) {
return c.key, nil return c.Key, nil
} }
var claims claims var claims claims
token, err := request.ParseFromRequestWithClaims(r, token, err := request.ParseFromRequestWithClaims(r,
@ -139,38 +137,11 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
return false, nil return false, nil
} }
u, ok := c.Users[claims.User.Username] u, err := c.Store.Users.Get(claims.User.ID, c.NewFS)
if !ok { if err != nil {
return false, nil return false, nil
} }
c.User = u c.User = u
return true, 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
}

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"io" "io"
@ -9,13 +9,14 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
"github.com/mholt/archiver" "github.com/mholt/archiver"
) )
// downloadHandler creates an archive in one of the supported formats (zip, tar, // downloadHandler creates an archive in one of the supported formats (zip, tar,
// tar.gz or tar.bz2) and sends it to be downloaded. // tar.gz or tar.bz2) and sends it to be downloaded.
func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func downloadHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("format") query := r.URL.Query().Get("format")
// If the file isn't a directory, serve it using http.ServeFile. We display it // If the file isn't a directory, serve it using http.ServeFile. We display it

View File

@ -1,29 +1,48 @@
package filemanager package http
import ( import (
"encoding/json" "encoding/json"
"html/template" "html/template"
"log"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/asdine/storm" "github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
) )
// RequestContext contains the needed information to make handlers work. // Handler returns a function compatible with http.HandleFunc.
type RequestContext struct { func Handler(m *fm.FileManager) http.Handler {
*FileManager return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
User *User code, err := serve(&fm.Context{
File *file FileManager: m,
// On API handlers, Router is the APi handler we want. User: nil,
Router string File: nil,
}, w, r)
if code >= 400 {
w.WriteHeader(code)
if err == nil {
txt := http.StatusText(code)
log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
w.Write([]byte(txt))
}
}
if err != nil {
log.Print(err)
w.Write([]byte(err.Error()))
}
})
} }
// serveHTTP is the main entry point of this HTML application. // serve is the main entry point of this HTML application.
func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func serve(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Checks if the URL contains the baseURL and strips it. Otherwise, it just // 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! // returns a 404 fm.Error because we're not supposed to be here!
p := strings.TrimPrefix(r.URL.Path, c.BaseURL) p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
if len(p) >= len(r.URL.Path) && c.BaseURL != "" { if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
@ -37,7 +56,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
if r.URL.Path == "/sw.js" { if r.URL.Path == "/sw.js" {
return renderFile( return renderFile(
c, w, c, w,
c.assets.MustString("sw.js"), c.Assets.MustString("sw.js"),
"application/javascript", "application/javascript",
) )
} }
@ -79,27 +98,27 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return renderFile( return renderFile(
c, w, c, w,
c.assets.MustString("index.html"), c.Assets.MustString("index.html"),
"text/html", "text/html",
) )
} }
// staticHandler handles the static assets path. // staticHandler handles the static assets path.
func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/static/manifest.json" { if r.URL.Path != "/static/manifest.json" {
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r) http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
return renderFile( return renderFile(
c, w, c, w,
c.assets.MustString("static/manifest.json"), c.Assets.MustString("static/manifest.json"),
"application/json", "application/json",
) )
} }
// apiHandler is the main entry point for the /api endpoint. // apiHandler is the main entry point for the /api endpoint.
func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/auth/get" { if r.URL.Path == "/auth/get" {
return authHandler(c, w, r) return authHandler(c, w, r)
} }
@ -135,9 +154,9 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
if c.Router == "checksum" || c.Router == "download" { if c.Router == "checksum" || c.Router == "download" {
var err error var err error
c.File, err = getInfo(r.URL, c.FileManager, c.User) c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User)
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
} }
@ -169,11 +188,11 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
} }
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512. // serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo") query := r.URL.Query().Get("algo")
val, err := c.File.Checksum(query) val, err := c.File.Checksum(query)
if err == errInvalidOption { if err == fm.ErrInvalidOption {
return http.StatusBadRequest, err return http.StatusBadRequest, err
} else if err != nil { } else if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
@ -201,14 +220,18 @@ func splitURL(path string) (string, string) {
} }
// renderFile renders a file using a template with some needed variables. // renderFile renders a file using a template with some needed variables.
func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentType string) (int, error) { func renderFile(c *fm.Context, w http.ResponseWriter, file string, contentType string) (int, error) {
tpl := template.Must(template.New("file").Parse(file)) tpl := template.Must(template.New("file").Parse(file))
w.Header().Set("Content-Type", contentType+"; charset=utf-8") w.Header().Set("Content-Type", contentType+"; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{ data := map[string]interface{}{"BaseURL": c.RootURL()}
"BaseURL": c.RootURL(),
"StaticGen": c.staticgen, if c.StaticGen != nil {
}) data["StaticGen"] = c.StaticGen.Name()
}
err := tpl.Execute(w, data)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -216,13 +239,13 @@ func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentTy
return 0, nil return 0, nil
} }
func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { // sharePage build the share page.
var s shareLink func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
err := c.db.One("Hash", r.URL.Path, &s) s, err := c.Store.Share.Get(r.URL.Path)
if err == storm.ErrNotFound { if err == storm.ErrNotFound {
return renderFile( return renderFile(
c, w, c, w,
c.assets.MustString("static/share/404.html"), c.Assets.MustString("static/share/404.html"),
"text/html", "text/html",
) )
} }
@ -232,10 +255,10 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
} }
if s.Expires && s.ExpireDate.Before(time.Now()) { if s.Expires && s.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&s) c.Store.Share.Delete(s.Hash)
return renderFile( return renderFile(
c, w, c, w,
c.assets.MustString("static/share/404.html"), c.Assets.MustString("static/share/404.html"),
"text/html", "text/html",
) )
} }
@ -244,10 +267,10 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
info, err := os.Stat(s.Path) info, err := os.Stat(s.Path)
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
c.File = &file{ c.File = &fm.File{
Path: s.Path, Path: s.Path,
Name: info.Name(), Name: info.Name(),
ModTime: info.ModTime(), ModTime: info.ModTime(),
@ -259,7 +282,7 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
dl := r.URL.Query().Get("dl") dl := r.URL.Query().Get("dl")
if dl == "" || dl == "0" { if dl == "" || dl == "0" {
tpl := template.Must(template.New("file").Parse(c.assets.MustString("static/share/index.html"))) tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html")))
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{ err := tpl.Execute(w, map[string]interface{}{
@ -299,8 +322,8 @@ func matchURL(first, second string) bool {
return strings.HasPrefix(first, second) return strings.HasPrefix(first, second)
} }
// errorToHTTP converts errors to HTTP Status Code. // ErrorToHTTP converts errors to HTTP Status Code.
func errorToHTTP(err error, gone bool) int { func ErrorToHTTP(err error, gone bool) int {
switch { switch {
case err == nil: case err == nil:
return http.StatusOK return http.StatusOK

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"errors" "errors"
@ -13,6 +13,7 @@ import (
"strings" "strings"
"time" "time"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
) )
@ -26,7 +27,7 @@ func sanitizeURL(url string) string {
return path return path
} }
func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path) r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method { switch r.Method {
@ -36,7 +37,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return resourceDeleteHandler(c, w, r) return resourceDeleteHandler(c, w, r)
case http.MethodPut: case http.MethodPut:
// Before save command handler. // Before save command handler.
path := filepath.Join(string(c.User.FileSystem), r.URL.Path) path := filepath.Join(c.User.Scope, r.URL.Path)
if err := c.Runner("before_save", path); err != nil { if err := c.Runner("before_save", path); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -61,11 +62,11 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return http.StatusNotImplemented, nil return http.StatusNotImplemented, nil
} }
func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Gets the information of the directory/file. // Gets the information of the directory/file.
f, err := getInfo(r.URL, c.FileManager, c.User) f, err := fm.GetInfo(r.URL, c.FileManager, c.User)
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// If it's a dir and the path doesn't end with a trailing slash, // If it's a dir and the path doesn't end with a trailing slash,
@ -82,7 +83,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
// Tries to get the file type. // Tries to get the file type.
if err = f.GetFileType(true); err != nil { if err = f.GetFileType(true); err != nil {
return errorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
// Serve a preview if the file can't be edited or the // Serve a preview if the file can't be edited or the
@ -96,23 +97,23 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
f.Kind = "editor" f.Kind = "editor"
// Tries to get the editor data. // Tries to get the editor data.
if err = f.getEditor(); err != nil { if err = f.GetEditor(); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return renderJSON(w, f) return renderJSON(w, f)
} }
func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
f := c.File f := c.File
f.Kind = "listing" f.Kind = "listing"
// Tries to get the listing data. // Tries to get the listing data.
if err := f.getListing(c, r); err != nil { if err := f.GetListing(c.User, r); err != nil {
return errorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
listing := f.listing listing := f.Listing
// Defines the cookie scope. // Defines the cookie scope.
cookieScope := c.RootURL() cookieScope := c.RootURL()
@ -134,7 +135,7 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
return renderJSON(w, f) return renderJSON(w, f)
} }
func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Prevent the removal of the root directory. // Prevent the removal of the root directory.
if r.URL.Path == "/" || !c.User.AllowEdit { if r.URL.Path == "/" || !c.User.AllowEdit {
return http.StatusForbidden, nil return http.StatusForbidden, nil
@ -143,13 +144,13 @@ func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Req
// Remove the file or folder. // Remove the file or folder.
err := c.User.FileSystem.RemoveAll(r.URL.Path) err := c.User.FileSystem.RemoveAll(r.URL.Path)
if err != nil { if err != nil {
return errorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
return http.StatusOK, nil return http.StatusOK, nil
} }
func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowNew && r.Method == http.MethodPost { if !c.User.AllowNew && r.Method == http.MethodPost {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -159,7 +160,7 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
} }
// Discard any invalid upload before returning to avoid connection // Discard any invalid upload before returning to avoid connection
// reset error. // reset fm.Error.
defer func() { defer func() {
io.Copy(ioutil.Discard, r.Body) io.Copy(ioutil.Discard, r.Body)
}() }()
@ -174,13 +175,13 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
// Otherwise we try to create the directory. // Otherwise we try to create the directory.
err := c.User.FileSystem.Mkdir(r.URL.Path, 0776) err := c.User.FileSystem.Mkdir(r.URL.Path, 0776)
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// If using POST method, we are trying to create a new file so it is not // 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 // desirable to ovfm.Erride an already existent file. Thus, we check
// if the file already exists. If so, we just return a 409 Conflict. // if the file already exists. If so, we just return a 409 Conflict.
if r.Method == http.MethodPost && r.Header.Get("Action") != "override" { if r.Method == http.MethodPost && r.Header.Get("Action") != "ovfm.Erride" {
if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil { if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil {
return http.StatusConflict, errors.New("There is already a file on that path") return http.StatusConflict, errors.New("There is already a file on that path")
} }
@ -189,20 +190,20 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
// Create/Open the file. // Create/Open the file.
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776) f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
defer f.Close() defer f.Close()
// Copies the new content for the file. // Copies the new content for the file.
_, err = io.Copy(f, r.Body) _, err = io.Copy(f, r.Body)
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// Gets the info about the file. // Gets the info about the file.
fi, err := f.Stat() fi, err := f.Stat()
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// Check if this instance has a Static Generator and handles publishing // Check if this instance has a Static Generator and handles publishing
@ -220,7 +221,7 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
return http.StatusOK, nil return http.StatusOK, nil
} }
func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
publish := r.Header.Get("Publish") publish := r.Header.Get("Publish")
schedule := r.Header.Get("Schedule") schedule := r.Header.Get("Schedule")
@ -241,7 +242,7 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
c.cron.AddFunc(t.Format("05 04 15 02 01 *"), func() { c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
_, err := resourcePublish(c, w, r) _, err := resourcePublish(c, w, r)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@ -251,8 +252,8 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
return http.StatusOK, nil return http.StatusOK, nil
} }
func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path) path := filepath.Join(c.User.Scope, r.URL.Path)
// Before save command handler. // Before save command handler.
if err := c.Runner("before_publish", path); err != nil { if err := c.Runner("before_publish", path); err != nil {
@ -273,7 +274,7 @@ func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
// resourcePatchHandler is the entry point for resource handler. // resourcePatchHandler is the entry point for resource handler.
func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowEdit { if !c.User.AllowEdit {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -282,7 +283,7 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
action := r.Header.Get("Action") action := r.Header.Get("Action")
dst, err := url.QueryUnescape(dst) dst, err := url.QueryUnescape(dst)
if err != nil { if err != nil {
return errorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
src := r.URL.Path src := r.URL.Path
@ -297,7 +298,7 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
err = c.User.FileSystem.Rename(src, dst) err = c.User.FileSystem.Rename(src, dst)
} }
return errorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
// displayMode obtains the display mode from the Cookie. // displayMode obtains the display mode from the Cookie.

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"bytes" "bytes"
@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"reflect" "reflect"
fm "github.com/hacdias/filemanager"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
@ -26,7 +27,7 @@ type option struct {
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) { func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
// Checks if the request body is empty. // Checks if the request body is empty.
if r.Body == nil { if r.Body == nil {
return nil, errEmptyRequest return nil, fm.ErrEmptyRequest
} }
// Parses the request body and checks if it's well formed. // Parses the request body and checks if it's well formed.
@ -38,13 +39,13 @@ func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
// Checks if the request type is right. // Checks if the request type is right.
if mod.What != "settings" { if mod.What != "settings" {
return nil, errWrongDataType return nil, fm.ErrWrongDataType
} }
return mod, nil return mod, nil
} }
func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func settingsHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "" && r.URL.Path != "/" { if r.URL.Path != "" && r.URL.Path != "/" {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
@ -64,7 +65,7 @@ type settingsGetRequest struct {
StaticGen []option `json:"staticGen"` StaticGen []option `json:"staticGen"`
} }
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func settingsGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin { if !c.User.Admin {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -93,7 +94,7 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return renderJSON(w, result) return renderJSON(w, result)
} }
func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func settingsPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin { if !c.User.Admin {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -102,9 +103,10 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
if err != nil { if err != nil {
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
// Update the commands. // Update the commands.
if mod.Which == "commands" { if mod.Which == "commands" {
if err := c.db.Set("config", "commands", mod.Data.Commands); err != nil { if err := c.Store.Config.Save("commands", mod.Data.Commands); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -119,7 +121,7 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
err = c.db.Set("staticgen", c.staticgen, c.StaticGen) err = c.Store.Config.Save("staticgen_"+c.StaticGen.Name(), c.StaticGen)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"encoding/hex" "encoding/hex"
@ -9,17 +9,10 @@ import (
"time" "time"
"github.com/asdine/storm" "github.com/asdine/storm"
"github.com/asdine/storm/q" fm "github.com/hacdias/filemanager"
) )
type shareLink struct { func shareHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
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 *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path) r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method { switch r.Method {
@ -34,13 +27,9 @@ func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (in
return http.StatusNotImplemented, nil return http.StatusNotImplemented, nil
} }
func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func shareGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
var ( path := filepath.Join(c.User.Scope, r.URL.Path)
s []*shareLink s, err := c.Store.Share.GetByPath(path)
path = filepath.Join(string(c.User.FileSystem), r.URL.Path)
)
err := c.db.Find("Path", path, &s)
if err == storm.ErrNotFound { if err == storm.ErrNotFound {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
@ -51,7 +40,7 @@ func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
for i, link := range s { for i, link := range s {
if link.Expires && link.ExpireDate.Before(time.Now()) { if link.Expires && link.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&shareLink{Hash: link.Hash}) c.Store.Share.Delete(link.Hash)
s = append(s[:i], s[i+1:]...) s = append(s[:i], s[i+1:]...)
} }
} }
@ -59,29 +48,30 @@ func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return renderJSON(w, s) return renderJSON(w, s)
} }
func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path) path := filepath.Join(c.User.Scope, r.URL.Path)
var s shareLink var s *fm.ShareLink
expire := r.URL.Query().Get("expires") expire := r.URL.Query().Get("expires")
unit := r.URL.Query().Get("unit") unit := r.URL.Query().Get("unit")
if expire == "" { if expire == "" {
err := c.db.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&s) var err error
s, err = c.Store.Share.GetPermanent(path)
if err == nil { if err == nil {
w.Write([]byte(c.RootURL() + "/share/" + s.Hash)) w.Write([]byte(c.RootURL() + "/share/" + s.Hash))
return 0, nil return 0, nil
} }
} }
bytes, err := generateRandomBytes(32) bytes, err := fm.GenerateRandomBytes(32)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
str := hex.EncodeToString(bytes) str := hex.EncodeToString(bytes)
s = shareLink{ s = &fm.ShareLink{
Path: path, Path: path,
Hash: str, Hash: str,
Expires: expire != "", Expires: expire != "",
@ -108,18 +98,15 @@ func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
s.ExpireDate = time.Now().Add(add) s.ExpireDate = time.Now().Add(add)
} }
err = c.db.Save(&s) if err := c.Store.Share.Save(s); err != nil {
if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return renderJSON(w, s) return renderJSON(w, s)
} }
func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func shareDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink s, err := c.Store.Share.Get(strings.TrimPrefix(r.URL.Path, "/"))
err := c.db.One("Hash", strings.TrimPrefix(r.URL.Path, "/"), &s)
if err == storm.ErrNotFound { if err == storm.ErrNotFound {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
@ -128,7 +115,7 @@ func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
err = c.db.DeleteStruct(&s) err = c.Store.Share.Delete(s.Hash)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"encoding/json" "encoding/json"
@ -9,7 +9,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/asdine/storm" fm "github.com/hacdias/filemanager"
) )
type modifyRequest struct { type modifyRequest struct {
@ -19,12 +19,12 @@ type modifyRequest struct {
type modifyUserRequest struct { type modifyUserRequest struct {
*modifyRequest *modifyRequest
Data *User `json:"data"` Data *fm.User `json:"data"`
} }
// usersHandler is the entry point of the users API. It's just a router // usersHandler is the entry point of the users API. It's just a router
// to send the request to its // to send the request to its
func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { 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 // If the user isn't admin and isn't making a PUT
// request, then return forbidden. // request, then return forbidden.
if !c.User.Admin && r.Method != http.MethodPut { if !c.User.Admin && r.Method != http.MethodPut {
@ -47,7 +47,7 @@ func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (in
// getUserID returns the id from the user which is present // getUserID returns the id from the user which is present
// in the request url. If the url is invalid and doesn't // in the request url. If the url is invalid and doesn't
// contain a valid ID, it returns an error. // contain a valid ID, it returns an fm.Error.
func getUserID(r *http.Request) (int, error) { func getUserID(r *http.Request) (int, error) {
// Obtains the ID in string from the URL and converts // Obtains the ID in string from the URL and converts
// it into an integer. // it into an integer.
@ -63,11 +63,11 @@ func getUserID(r *http.Request) (int, error) {
// getUser returns the user which is present in the request // getUser returns the user which is present in the request
// body. If the body is empty or the JSON is invalid, it // body. If the body is empty or the JSON is invalid, it
// returns an error. // returns an fm.Error.
func getUser(r *http.Request) (*User, string, error) { func getUser(c *fm.Context, r *http.Request) (*fm.User, string, error) {
// Checks if the request body is empty. // Checks if the request body is empty.
if r.Body == nil { if r.Body == nil {
return nil, "", errEmptyRequest return nil, "", fm.ErrEmptyRequest
} }
// Parses the request body and checks if it's well formed. // Parses the request body and checks if it's well formed.
@ -79,13 +79,14 @@ func getUser(r *http.Request) (*User, string, error) {
// Checks if the request type is right. // Checks if the request type is right.
if mod.What != "user" { if mod.What != "user" {
return nil, "", errWrongDataType return nil, "", fm.ErrWrongDataType
} }
mod.Data.FileSystem = c.NewFS(mod.Data.Scope)
return mod.Data, mod.Which, nil return mod.Data, mod.Which, nil
} }
func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func usersGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Request for the default user data. // Request for the default user data.
if r.URL.Path == "/base" { if r.URL.Path == "/base" {
return renderJSON(w, c.DefaultUser) return renderJSON(w, c.DefaultUser)
@ -93,15 +94,15 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// Request for the listing of users. // Request for the listing of users.
if r.URL.Path == "/" { if r.URL.Path == "/" {
users := []User{} users, err := c.Store.Users.Gets(c.NewFS)
if err != nil {
return http.StatusInternalServerError, err
}
for _, user := range c.Users { for _, u := range users {
// Copies the user info and removes its // Removes the user password so it won't
// password so it won't be sent to the // be sent to the front-end.
// front-end.
u := *user
u.Password = "" u.Password = ""
users = append(users, u)
} }
sort.Slice(users, func(i, j int) bool { sort.Slice(users, func(i, j int) bool {
@ -116,54 +117,47 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Searches for the user and prints the one who matches. u, err := c.Store.Users.Get(id, c.NewFS)
for _, user := range c.Users { if err == fm.ErrExist {
if user.ID != id { return http.StatusNotFound, err
continue
}
u := *user
u.Password = ""
return renderJSON(w, u)
} }
// If there aren't any matches, return not found. if err != nil {
return http.StatusNotFound, errUserNotExist return http.StatusInternalServerError, err
}
u.Password = ""
return renderJSON(w, u)
} }
func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/" { if r.URL.Path != "/" {
return http.StatusMethodNotAllowed, nil return http.StatusMethodNotAllowed, nil
} }
u, _, err := getUser(r) u, _, err := getUser(c, r)
if err != nil { if err != nil {
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
// Checks if username isn't empty. // Checks if username isn't empty.
if u.Username == "" { if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername return http.StatusBadRequest, fm.ErrEmptyUsername
} }
// Checks if filesystem isn't empty. // Checks if scope isn't empty.
if u.FileSystem == "" { if u.Scope == "" {
return http.StatusBadRequest, errEmptyScope return http.StatusBadRequest, fm.ErrEmptyScope
} }
// Checks if password isn't empty. // Checks if password isn't empty.
if u.Password == "" { if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword return http.StatusBadRequest, fm.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. // Initialize rules if they're not initialized.
if u.Rules == nil { if u.Rules == nil {
u.Rules = []*Rule{} u.Rules = []*fm.Rule{}
} }
// Initialize commands if not initialized. // Initialize commands if not initialized.
@ -177,12 +171,12 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
// Checks if the scope exists. // Checks if the scope exists.
if code, err := checkFS(string(u.FileSystem)); err != nil { if code, err := checkFS(u.Scope); err != nil {
return code, err return code, err
} }
// Hashes the password. // Hashes the password.
pw, err := hashPassword(u.Password) pw, err := fm.HashPassword(u.Password)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -190,18 +184,15 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
u.Password = pw u.Password = pw
// Saves the user to the database. // Saves the user to the database.
err = c.db.Save(u) err = c.Store.Users.Save(u)
if err == storm.ErrAlreadyExists { if err == fm.ErrExist {
return http.StatusConflict, errUserExist return http.StatusConflict, err
} }
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Saves the user to the memory.
c.Users[u.Username] = u
// Set the Location header and return. // Set the Location header and return.
w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID)) w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
@ -231,7 +222,7 @@ func checkFS(path string) (int, error) {
return 0, nil return 0, nil
} }
func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func usersDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/" { if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil return http.StatusMethodNotAllowed, nil
} }
@ -242,27 +233,19 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
} }
// Deletes the user from the database. // Deletes the user from the database.
err = c.db.DeleteStruct(&User{ID: id}) err = c.Store.Users.Delete(id)
if err == storm.ErrNotFound { if err == fm.ErrNotExist {
return http.StatusNotFound, errUserNotExist return http.StatusNotFound, fm.ErrNotExist
} }
if err != nil { if err != nil {
return http.StatusInternalServerError, err 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 return http.StatusOK, nil
} }
func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// New users should be created on /api/users. // New users should be created on /api/users.
if r.URL.Path == "/" { if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil return http.StatusMethodNotAllowed, nil
@ -280,7 +263,7 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
// Gets the user from the request body. // Gets the user from the request body.
u, which, err := getUser(r) u, which, err := getUser(c, r)
if err != nil { if err != nil {
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
@ -289,12 +272,8 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
if which == "partial" { if which == "partial" {
c.User.CSS = u.CSS c.User.CSS = u.CSS
c.User.Locale = u.Locale 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) err = c.Store.Users.Update(c.User, "CSS", "Locale")
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -305,16 +284,15 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// Updates the Password. // Updates the Password.
if which == "password" { if which == "password" {
if u.Password == "" { if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword return http.StatusBadRequest, fm.ErrEmptyPassword
} }
pw, err := hashPassword(u.Password) c.User.Password, err = fm.HashPassword(u.Password)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
c.User.Password = pw err = c.Store.Users.Update(c.User, "Password")
err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -324,27 +302,27 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// If can only be all. // If can only be all.
if which != "all" { if which != "all" {
return http.StatusBadRequest, errInvalidUpdateField return http.StatusBadRequest, fm.ErrInvalidUpdateField
} }
// Checks if username isn't empty. // Checks if username isn't empty.
if u.Username == "" { if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername return http.StatusBadRequest, fm.ErrEmptyUsername
} }
// Checks if filesystem isn't empty. // Checks if filesystem isn't empty.
if u.FileSystem == "" { if u.Scope == "" {
return http.StatusBadRequest, errEmptyScope return http.StatusBadRequest, fm.ErrEmptyScope
} }
// Checks if the scope exists. // Checks if the scope exists.
if code, err := checkFS(string(u.FileSystem)); err != nil { if code, err := checkFS(u.Scope); err != nil {
return code, err return code, err
} }
// Initialize rules if they're not initialized. // Initialize rules if they're not initialized.
if u.Rules == nil { if u.Rules == nil {
u.Rules = []*Rule{} u.Rules = []*fm.Rule{}
} }
// Initialize commands if not initialized. // Initialize commands if not initialized.
@ -353,22 +331,20 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
// Gets the current saved user from the in-memory map. // Gets the current saved user from the in-memory map.
var suser *User suser, err := c.Store.Users.Get(id, c.NewFS)
for _, user := range c.Users { if err == fm.ErrNotExist {
if user.ID == id {
suser = user
break
}
}
if suser == nil {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
if err != nil {
return http.StatusInternalServerError, err
}
u.ID = id u.ID = id
// Changes the password if the request wants it. // Changes the password if the request wants it.
if u.Password != "" { if u.Password != "" {
pw, err := hashPassword(u.Password) pw, err := fm.HashPassword(u.Password)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -380,17 +356,10 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// Updates the whole User struct because we always are supposed // Updates the whole User struct because we always are supposed
// to send a new entire object. // to send a new entire object.
err = c.db.Save(u) err = c.Store.Users.Update(u)
if err != nil { if err != nil {
return http.StatusInternalServerError, err 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 return http.StatusOK, nil
} }

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"bytes" "bytes"
@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
fm "github.com/hacdias/filemanager"
) )
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
@ -26,8 +27,8 @@ var (
) )
// command handles the requests for VCS related commands: git, svn and mercurial // command handles the requests for VCS related commands: git, svn and mercurial
func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for errors. // Upgrades the connection to a websocket and checks for fm.Errors.
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
return 0, err return 0, err
@ -81,7 +82,7 @@ func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, er
} }
// Gets the path and initializes a buffer. // Gets the path and initializes a buffer.
path := string(c.User.FileSystem) + "/" + r.URL.Path path := c.User.Scope + "/" + r.URL.Path
path = filepath.Clean(path) path = filepath.Clean(path)
buff := new(bytes.Buffer) buff := new(bytes.Buffer)
@ -91,7 +92,7 @@ func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, er
cmd.Stderr = buff cmd.Stderr = buff
cmd.Stdout = buff cmd.Stdout = buff
// Starts the command and checks for errors. // Starts the command and checks for fm.Errors.
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
@ -239,8 +240,8 @@ func parseSearch(value string) *searchOptions {
} }
// search searches for a file or directory. // search searches for a file or directory.
func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for errors. // Upgrades the connection to a websocket and checks for fm.Errors.
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
return 0, err return 0, err
@ -269,7 +270,7 @@ func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, err
search = parseSearch(value) search = parseSearch(value)
scope := strings.TrimPrefix(r.URL.Path, "/") scope := strings.TrimPrefix(r.URL.Path, "/")
scope = "/" + scope scope = "/" + scope
scope = string(c.User.FileSystem) + scope scope = c.User.Scope + scope
scope = strings.Replace(scope, "\\", "/", -1) scope = strings.Replace(scope, "\\", "/", -1)
scope = filepath.Clean(scope) scope = filepath.Clean(scope)

9156
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
package filemanager package staticgen
import ( import (
"errors" "errors"
@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/varutils" "github.com/hacdias/varutils"
) )
@ -17,15 +18,6 @@ var (
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action") errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
) )
// StaticGen is a static website generator.
type StaticGen interface {
SettingsPath() string
Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
}
// Hugo is the Hugo static website generator. // Hugo is the Hugo static website generator.
type Hugo struct { type Hugo struct {
// Website root // Website root
@ -66,8 +58,13 @@ func (h Hugo) SettingsPath() string {
return "/config." + frontmatter return "/config." + frontmatter
} }
// Name is the plugin's name.
func (h Hugo) Name() string {
return "hugo"
}
// Hook is the pre-api handler. // Hook is the pre-api handler.
func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func (h Hugo) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// If we are not using HTTP Post, we shall return Method Not Allowed // If we are not using HTTP Post, we shall return Method Not Allowed
// since we are only working with this method. // since we are only working with this method.
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
@ -87,7 +84,7 @@ func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path) filename := filepath.Join(c.User.Scope, r.URL.Path)
archetype := r.Header.Get("archetype") archetype := r.Header.Get("archetype")
ext := filepath.Ext(filename) ext := filepath.Ext(filename)
@ -110,8 +107,8 @@ func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
} }
// Publish publishes a post. // Publish publishes a post.
func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func (h Hugo) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path) filename := filepath.Join(c.User.Scope, r.URL.Path)
// We only run undraft command if it is a file. // We only run undraft command if it is a file.
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") { if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
@ -127,7 +124,7 @@ func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
// Preview handles the preview path. // Preview handles the preview path.
func (h *Hugo) Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func (h *Hugo) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none. // Get a new temporary path if there is none.
if h.previewPath == "" { if h.previewPath == "" {
path, err := ioutil.TempDir("", "") path, err := ioutil.TempDir("", "")
@ -186,7 +183,8 @@ func (h Hugo) undraft(file string) error {
return nil return nil
} }
func (h *Hugo) find() error { // Setup sets up the plugin.
func (h *Hugo) Setup() error {
var err error var err error
if h.Exe, err = exec.LookPath("hugo"); err != nil { if h.Exe, err = exec.LookPath("hugo"); err != nil {
return err return err
@ -194,114 +192,3 @@ func (h *Hugo) find() error {
return nil return nil
} }
// Jekyll is the Jekyll static website generator.
type Jekyll struct {
// Website root
Root string `name:"Website Root"`
// Public folder
Public string `name:"Public Directory"`
// Jekyll executable path
Exe string `name:"Executable"`
// Jekyll arguments
Args []string `name:"Arguments"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview
previewPath string
}
// SettingsPath retrieves the correct settings path.
func (j Jekyll) SettingsPath() string {
return "/_config.yml"
}
// Hook is the pre-api handler.
func (j Jekyll) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}
// Publish publishes a post.
func (j Jekyll) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
// We only run undraft command if it is a file.
if err := j.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
// Regenerates the file
j.run()
return 0, nil
}
// Preview handles the preview path.
func (j *Jekyll) Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none.
if j.previewPath == "" {
path, err := ioutil.TempDir("", "")
if err != nil {
return http.StatusInternalServerError, err
}
j.previewPath = path
}
// Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination.
args := j.Args
args = append(args, "--baseurl", c.RootURL()+"/preview/")
args = append(args, "--drafts")
args = append(args, "--destination", j.previewPath)
// Builds the preview.
if err := runCommand(j.Exe, args, j.Root); err != nil {
return http.StatusInternalServerError, err
}
// Serves the temporary path with the preview.
http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r)
return 0, nil
}
func (j Jekyll) run() {
// If the CleanPublic option is enabled, clean it.
if j.CleanPublic {
os.RemoveAll(j.Public)
}
if err := runCommand(j.Exe, j.Args, j.Root); err != nil {
log.Println(err)
}
}
func (j Jekyll) undraft(file string) error {
if !strings.Contains(file, "_drafts") {
return nil
}
return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1))
}
func (j *Jekyll) find() error {
var err error
if j.Exe, err = exec.LookPath("jekyll"); err != nil {
return err
}
return nil
}
// runCommand executes an external command
func runCommand(command string, args []string, path string) error {
cmd := exec.Command(command, args...)
cmd.Dir = path
out, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(out))
}
return nil
}

125
staticgen/jekyll.go Normal file
View File

@ -0,0 +1,125 @@
package staticgen
import (
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
fm "github.com/hacdias/filemanager"
)
// Jekyll is the Jekyll static website generator.
type Jekyll struct {
// Website root
Root string `name:"Website Root"`
// Public folder
Public string `name:"Public Directory"`
// Jekyll executable path
Exe string `name:"Executable"`
// Jekyll arguments
Args []string `name:"Arguments"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview
previewPath string
}
// Name is the plugin's name.
func (j Jekyll) Name() string {
return "jekyll"
}
// SettingsPath retrieves the correct settings path.
func (j Jekyll) SettingsPath() string {
return "/_config.yml"
}
// Hook is the pre-api handler.
func (j Jekyll) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}
// Publish publishes a post.
func (j Jekyll) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(c.User.Scope, r.URL.Path)
// We only run undraft command if it is a file.
if err := j.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
// Regenerates the file
j.run()
return 0, nil
}
// Preview handles the preview path.
func (j *Jekyll) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none.
if j.previewPath == "" {
path, err := ioutil.TempDir("", "")
if err != nil {
return http.StatusInternalServerError, err
}
j.previewPath = path
}
// Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination.
args := j.Args
args = append(args, "--baseurl", c.RootURL()+"/preview/")
args = append(args, "--drafts")
args = append(args, "--destination", j.previewPath)
// Builds the preview.
if err := runCommand(j.Exe, args, j.Root); err != nil {
return http.StatusInternalServerError, err
}
// Serves the temporary path with the preview.
http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r)
return 0, nil
}
func (j Jekyll) run() {
// If the CleanPublic option is enabled, clean it.
if j.CleanPublic {
os.RemoveAll(j.Public)
}
if err := runCommand(j.Exe, j.Args, j.Root); err != nil {
log.Println(err)
}
}
func (j Jekyll) undraft(file string) error {
if !strings.Contains(file, "_drafts") {
return nil
}
return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1))
}
// Setup sets up the plugin.
func (j *Jekyll) Setup() error {
var err error
if j.Exe, err = exec.LookPath("jekyll"); err != nil {
return err
}
if len(j.Args) == 0 {
j.Args = []string{"build"}
}
if j.Args[0] != "build" {
j.Args = append([]string{"build"}, j.Args...)
}
return nil
}

19
staticgen/staticgen.go Normal file
View File

@ -0,0 +1,19 @@
package staticgen
import (
"errors"
"os/exec"
)
// runCommand executes an external command
func runCommand(command string, args []string, path string) error {
cmd := exec.Command(command, args...)
cmd.Dir = path
out, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(out))
}
return nil
}

0
testdata/.gitkeep vendored
View File