Merge branch 'spl'

Former-commit-id: 22d8f06e8a8f25af502d28a913fe83fa8aaa3970 [formerly f93cb05d08b4ca935aad07262f3898bf2fce87ef] [formerly 3562ac890aa6a5ab7f5514b5f9c5617bcf16c844 [formerly bbffd10e0b]]
Former-commit-id: 301bbb736a3003ef4b9ac0398e6c3b311a9281bc [formerly 9ed7c705279356410e9861c2100b0d0911acc54f]
Former-commit-id: df715dab654dc2116dfba8bbea3365d940ceecb8
This commit is contained in:
Henrique Dias 2017-08-20 10:27:09 +01:00
commit 21156bf24f
27 changed files with 10207 additions and 927 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/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
@ -32,7 +33,7 @@ func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
continue
}
f.Configs[i].ServeHTTP(w, r)
h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil
}

View File

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

View File

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

View File

@ -10,12 +10,18 @@ import (
"strconv"
"strings"
"github.com/asdine/storm"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt"
"github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/spf13/viper"
)
var databases = map[string]*storm.DB{}
// Parse ...
func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) {
var (
@ -24,7 +30,7 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
)
for c.Next() {
u := filemanager.User{
u := &filemanager.User{
Locale: "en",
AllowCommands: 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")
}
u.Scope = 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 {
case "hugo":
// Initialize the default settings for Hugo.
hugo := &filemanager.Hugo{
hugo := &staticgen.Hugo{
Root: scope,
Public: filepath.Join(scope, "public"),
Args: []string{},
@ -197,13 +235,13 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
}
// Attaches Hugo plugin to this file manager instance.
err = m.EnableStaticGen(hugo)
err = m.Attach(hugo)
if err != nil {
return nil, err
}
case "jekyll":
// Initialize the default settings for Jekyll.
jekyll := &filemanager.Jekyll{
jekyll := &staticgen.Jekyll{
Root: scope,
Public: filepath.Join(scope, "_site"),
Args: []string{},
@ -211,7 +249,7 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
}
// Attaches Hugo plugin to this file manager instance.
err = m.EnableStaticGen(jekyll)
err = m.Attach(jekyll)
if err != nil {
return nil, err
}

View File

@ -10,9 +10,14 @@ import (
"path/filepath"
"strings"
"github.com/asdine/storm"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt"
h "github.com/hacdias/filemanager/http"
"github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
@ -25,7 +30,7 @@ var (
scope string
commands string
logfile string
staticgen string
staticg string
locale string
port int
noAuth bool
@ -51,7 +56,7 @@ func init() {
flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users")
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
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")
}
@ -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.
laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
listener, err := net.Listen("tcp", laddr)
@ -205,7 +164,72 @@ func main() {
fmt.Println("Listening on", listener.Addr().String())
// Starts the server.
if err := http.Serve(listener, fm); err != nil {
if err := http.Serve(listener, handler()); err != nil {
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/sha512"
"encoding/hex"
"errors"
"hash"
"io"
"io/ioutil"
@ -23,13 +22,9 @@ import (
"github.com/gohugoio/hugo/parser"
)
var (
errInvalidOption = errors.New("Invalid option")
)
// 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).
// File contains the information about a particular file or directory.
type File struct {
// Indicates the Kind of view on the front-end (Listing, editor or preview).
Kind string `json:"kind"`
// The name of the file.
Name string `json:"name"`
@ -54,19 +49,19 @@ type file struct {
// Stores the content of a text file.
Content string `json:"content,omitempty"`
*listing `json:",omitempty"`
*Listing `json:",omitempty"`
Metadata string `json:"metadata,omitempty"`
Language string `json:"language,omitempty"`
}
// A listing is the context used to fill out a template.
type listing struct {
// A Listing is the context used to fill out a template.
type Listing struct {
// The items (files and folders) in the path.
Items []*file `json:"items"`
// The number of directories in the listing.
Items []*File `json:"items"`
// The number of directories in the Listing.
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"`
// Which sorting order is used.
Sort string `json:"sort"`
@ -76,15 +71,15 @@ type listing struct {
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
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
i := &file{
i := &File{
URL: "/files" + url.String(),
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)
@ -106,11 +101,11 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
return i, nil
}
// getListing gets the information about a specific directory and its files.
func (i *file) getListing(c *RequestContext, r *http.Request) error {
// GetListing gets the information about a specific directory and its files.
func (i *File) GetListing(u *User, r *http.Request) error {
// Gets the directory information using the Virtual File System of
// 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 {
return err
}
@ -123,7 +118,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
}
var (
fileinfos []*file
fileinfos []*File
dirCount, fileCount int
)
@ -134,7 +129,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
for _, f := range files {
name := f.Name()
allowed := c.User.Allowed("/" + name)
allowed := u.Allowed("/" + name)
if !allowed {
continue
@ -150,7 +145,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
// Absolute URL
url := url.URL{Path: baseurl + name}
i := &file{
i := &File{
Name: f.Name(),
Size: f.Size(),
ModTime: f.ModTime(),
@ -166,7 +161,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
fileinfos = append(fileinfos, i)
}
i.listing = &listing{
i.Listing = &Listing{
Items: fileinfos,
NumDirs: dirCount,
NumFiles: fileCount,
@ -175,8 +170,8 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
return nil
}
// getEditor gets the editor based on a Info struct
func (i *file) getEditor() error {
// GetEditor gets the editor based on a Info struct
func (i *File) GetEditor() error {
i.Language = editorLanguage(i.Extension)
// If the editor will hold only content, leave now.
if editorMode(i.Language) == "content" {
@ -205,7 +200,7 @@ func (i *file) getEditor() error {
// GetFileType obtains the mimetype and converts it to a simple
// type nomenclature.
func (i *file) GetFileType(checkContent bool) error {
func (i *File) GetFileType(checkContent bool) error {
var content []byte
var err error
@ -283,7 +278,8 @@ End:
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)
if err != nil {
return "", err
@ -293,7 +289,7 @@ func (i file) Checksum(kind string) (string, error) {
var h hash.Hash
switch kind {
switch algo {
case "md5":
h = md5.New()
case "sha1":
@ -303,7 +299,7 @@ func (i file) Checksum(kind string) (string, error) {
case "sha512":
h = sha512.New()
default:
return "", errInvalidOption
return "", ErrInvalidOption
}
_, 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
func (i file) CanBeEdited() bool {
func (i File) CanBeEdited() bool {
return i.Type == "text"
}
// ApplySort applies the sort order using .Order and .Sort
func (l listing) ApplySort() {
func (l Listing) ApplySort() {
// Check '.Order' to know how to sort
if l.Order == "desc" {
switch l.Sort {
@ -349,10 +345,10 @@ func (l listing) ApplySort() {
}
}
// Implement sorting for listing
type byName listing
type bySize listing
type byModified listing
// Implement sorting for Listing
type byName Listing
type bySize Listing
type byModified Listing
// By Name
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
import (
"crypto/rand"
"errors"
"log"
"net/http"
@ -64,38 +12,41 @@ import (
"strings"
"time"
"golang.org/x/crypto/bcrypt"
rice "github.com/GeertJohan/go.rice"
"github.com/asdine/storm"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/robfig/cron"
)
var (
errUserExist = errors.New("user already exists")
errUserNotExist = errors.New("user does not exist")
errEmptyRequest = errors.New("request body is empty")
errEmptyPassword = errors.New("password is empty")
errEmptyUsername = errors.New("username is empty")
errEmptyScope = errors.New("scope is empty")
errWrongDataType = errors.New("wrong data type")
errInvalidUpdateField = errors.New("invalid field to update")
ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist")
ErrEmptyRequest = errors.New("request body is empty")
ErrEmptyPassword = errors.New("password is empty")
ErrEmptyUsername = errors.New("username is empty")
ErrEmptyScope = errors.New("scope is empty")
ErrWrongDataType = errors.New("wrong data type")
ErrInvalidUpdateField = errors.New("invalid field to update")
ErrInvalidOption = errors.New("Invalid option")
)
// FileManager is a file manager instance. It should be creating using the
// 'New' function and not directly.
type FileManager struct {
// The BoltDB database for this instance.
db *storm.DB
// Cron job to manage schedulings.
Cron *cron.Cron
// The key used to sign the JWT tokens.
key []byte
Key []byte
// The static assets.
assets *rice.Box
Assets *rice.Box
// Job cron.
cron *cron.Cron
// The Store is used to manage users, shareable links and
// 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
// 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".
NoAuth bool
// staticgen is the name of the current static website generator.
staticgen string
// StaticGen is the static websit generator handler.
StaticGen StaticGen
// The Default User needed to build the New User page.
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.
Commands map[string][]string
// NewFS should build a new file system for a given path.
NewFS FSBuilder
}
// Command is a command function.
type Command func(r *http.Request, m *FileManager, u *User) error
// User contains the configuration for each user.
type User struct {
// ID is the required primary key with auto increment0
ID int `storm:"id,increment"`
// FSBuilder is the File System Builder.
type FSBuilder func(scope string) FileSystem
// 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"`
// 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) {
// Setup loads the configuration from the database and configures
// the Assets and the Cron job. It must always be run after
// creating a File Manager object.
func (m *FileManager) Setup() error {
// Creates a new File Manager instance with the Users
// map and Assets box.
m := &FileManager{
Users: map[string]*User{},
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
}
m.Assets = rice.MustFindBox("./assets/dist")
m.Cron = cron.New()
// Tries to get the encryption key from the database.
// If it doesn't exist, create a new one of 256 bits.
err = db.Get("config", "key", &m.key)
if err != nil && err == storm.ErrNotFound {
err := m.Store.Config.Get("key", &m.Key)
if err != nil && err == ErrNotExist {
var bytes []byte
bytes, err = generateRandomBytes(64)
bytes, err = GenerateRandomBytes(64)
if err != nil {
return nil, err
return err
}
m.key = bytes
err = db.Set("config", "key", m.key)
m.Key = bytes
err = m.Store.Config.Save("key", m.Key)
}
if err != nil {
return nil, err
return err
}
// Tries to get the event commands from the database.
// If they don't exist, initialize them.
err = db.Get("config", "commands", &m.Commands)
if err != nil && err == storm.ErrNotFound {
err = m.Store.Config.Get("commands", &m.Commands)
if err != nil && err == ErrNotExist {
m.Commands = map[string][]string{
"before_save": {},
"after_save": {},
"before_publish": {},
"after_publish": {},
}
err = db.Set("config", "commands", m.Commands)
err = m.Store.Config.Save("commands", m.Commands)
}
if err != nil {
return nil, err
return err
}
// Tries to fetch the users from the database and if there are
// any, add them to the current File Manager instance.
var users []User
err = db.All(&users)
if err != nil {
return nil, err
}
for i := range users {
m.Users[users[i].Username] = &users[i]
// Tries to fetch the users from the database.
users, err := m.Store.Users.Gets(m.NewFS)
if err != nil && err != ErrNotExist {
return err
}
// 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.
if len(users) == 0 {
u := base
u := *m.DefaultUser
u.Username = "admin"
// Hashes the password.
u.Password, err = hashPassword("admin")
u.Password, err = HashPassword("admin")
if err != nil {
return nil, err
return err
}
// The first user must be an administrator.
@ -289,25 +151,18 @@ func New(database string, base User) (*FileManager, error) {
u.AllowPublish = true
// Saves the user to the database.
if err := db.Save(&u); err != nil {
return nil, err
if err := m.Store.Users.Save(&u); err != nil {
return err
}
m.Users[u.Username] = &u
}
// Attaches db to this File Manager instance.
m.db = db
m.DefaultUser.Username = ""
m.DefaultUser.Password = ""
// Create the default user, making a copy of the base.
base.Username = ""
base.Password = ""
m.DefaultUser = &base
m.Cron.AddFunc("@hourly", m.ShareCleaner)
m.Cron.Start()
m.cron.AddFunc("@hourly", m.shareCleaner)
m.cron.Start()
return m, nil
return nil
}
// RootURL returns the actual URL where
@ -334,95 +189,32 @@ func (m *FileManager) SetBaseURL(url string) {
m.BaseURL = strings.TrimSuffix(url, "/")
}
// ServeHTTP handles the request.
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code, err := serveHTTP(&RequestContext{
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 {
// Attach attaches a static generator to the current File Manager.
func (m *FileManager) Attach(s StaticGen) error {
if reflect.TypeOf(s).Kind() != reflect.Ptr {
return errors.New("data should be a pointer to interface, not interface")
}
if h, ok := data.(*Hugo); ok {
return m.enableHugo(h)
}
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 {
err := s.Setup()
if err != nil {
return err
}
m.staticgen = "hugo"
m.StaticGen = h
m.StaticGen = s
err := m.db.Get("staticgen", "hugo", h)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("staticgen", "hugo", *h)
err = m.Store.Config.Get("staticgen_"+s.Name(), s)
if err == ErrNotExist {
return m.Store.Config.Save("staticgen_"+s.Name(), s)
}
return nil
return err
}
func (m *FileManager) enableJekyll(j *Jekyll) error {
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.
// ShareCleaner removes sharing links that are no longer active.
// This function is set to run periodically.
func (m FileManager) shareCleaner() {
var links []shareLink
func (m FileManager) ShareCleaner() {
// Get all links.
err := m.db.All(&links)
links, err := m.Store.Share.Gets()
if err != nil {
log.Print(err)
return
@ -431,7 +223,7 @@ func (m FileManager) shareCleaner() {
// Find the expired ones.
for i := range links {
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 {
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.
func (m FileManager) Runner(event string, path string) error {
commands := []string{}
@ -518,3 +279,209 @@ func (m FileManager) Runner(event string, path string) error {
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 (
"crypto/rand"
"encoding/json"
"net/http"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
fm "github.com/hacdias/filemanager"
)
// authHandler proccesses the authentication for the user.
func authHandler(c *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.
if c.NoAuth {
return 0, nil
}
// Receive the credentials from the request and unmarshal them.
var cred User
var cred fm.User
if r.Body == 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.
u, ok := c.Users[cred.Username]
if !ok {
u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
if err != nil {
return http.StatusForbidden, nil
}
// Checks if the password is correct.
if !checkPasswordHash(cred.Password, u.Password) {
if !fm.CheckPasswordHash(cred.Password, u.Password) {
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
// 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)
if !ok {
return http.StatusForbidden, nil
@ -60,16 +58,16 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// claims is the JWT claims.
type claims struct {
User
fm.User
NoAuth bool `json:"noAuth"`
jwt.StandardClaims
}
// 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
// hash so it never arrives to the user.
u := User{}
u := fm.User{}
u = *c.User
u.Password = ""
@ -85,7 +83,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
// Creates the token and signs it.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(c.key)
signed, err := token.SignedString(c.Key)
if err != nil {
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
// 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 {
c.User = c.DefaultUser
return true, c.User
}
keyFunc := func(token *jwt.Token) (interface{}, error) {
return c.key, nil
return c.Key, nil
}
var claims claims
token, err := request.ParseFromRequestWithClaims(r,
@ -139,38 +137,11 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
return false, nil
}
u, ok := c.Users[claims.User.Username]
if !ok {
u, err := c.Store.Users.Get(claims.User.ID, c.NewFS)
if err != nil {
return false, nil
}
c.User = u
return true, u
}
// hashPassword generates an hash from a password using bcrypt.
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// checkPasswordHash compares a password with an hash to check if they match.
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// generateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func generateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"io"
@ -9,13 +9,14 @@ import (
"path/filepath"
"strings"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
"github.com/mholt/archiver"
)
// downloadHandler creates an archive in one of the supported formats (zip, tar,
// tar.gz or tar.bz2) and sends it to be downloaded.
func downloadHandler(c *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")
// 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 (
"encoding/json"
"html/template"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
// RequestContext contains the needed information to make handlers work.
type RequestContext struct {
*FileManager
User *User
File *file
// On API handlers, Router is the APi handler we want.
Router string
// Handler returns a function compatible with http.HandleFunc.
func Handler(m *fm.FileManager) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
code, err := serve(&fm.Context{
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()))
}
})
}
// serveHTTP is the main entry point of this HTML application.
func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// serve is the main entry point of this HTML application.
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
// 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)
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" {
return renderFile(
c, w,
c.assets.MustString("sw.js"),
c.Assets.MustString("sw.js"),
"application/javascript",
)
}
@ -79,27 +98,27 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return renderFile(
c, w,
c.assets.MustString("index.html"),
c.Assets.MustString("index.html"),
"text/html",
)
}
// 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" {
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r)
http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil
}
return renderFile(
c, w,
c.assets.MustString("static/manifest.json"),
c.Assets.MustString("static/manifest.json"),
"application/json",
)
}
// 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" {
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" {
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 {
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.
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")
val, err := c.File.Checksum(query)
if err == errInvalidOption {
if err == fm.ErrInvalidOption {
return http.StatusBadRequest, err
} else if err != nil {
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.
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))
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.RootURL(),
"StaticGen": c.staticgen,
})
data := map[string]interface{}{"BaseURL": c.RootURL()}
if c.StaticGen != nil {
data["StaticGen"] = c.StaticGen.Name()
}
err := tpl.Execute(w, data)
if err != nil {
return http.StatusInternalServerError, err
}
@ -216,13 +239,13 @@ func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentTy
return 0, nil
}
func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink
err := c.db.One("Hash", r.URL.Path, &s)
// sharePage build the share page.
func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
s, err := c.Store.Share.Get(r.URL.Path)
if err == storm.ErrNotFound {
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
c.Assets.MustString("static/share/404.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()) {
c.db.DeleteStruct(&s)
c.Store.Share.Delete(s.Hash)
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
c.Assets.MustString("static/share/404.html"),
"text/html",
)
}
@ -244,10 +267,10 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
info, err := os.Stat(s.Path)
if err != nil {
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
c.File = &file{
c.File = &fm.File{
Path: s.Path,
Name: info.Name(),
ModTime: info.ModTime(),
@ -259,7 +282,7 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
dl := r.URL.Query().Get("dl")
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")
err := tpl.Execute(w, map[string]interface{}{
@ -299,8 +322,8 @@ func matchURL(first, second string) bool {
return strings.HasPrefix(first, second)
}
// errorToHTTP converts errors to HTTP Status Code.
func errorToHTTP(err error, gone bool) int {
// ErrorToHTTP converts errors to HTTP Status Code.
func ErrorToHTTP(err error, gone bool) int {
switch {
case err == nil:
return http.StatusOK

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"errors"
@ -13,6 +13,7 @@ import (
"strings"
"time"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
)
@ -26,7 +27,7 @@ func sanitizeURL(url string) string {
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)
switch r.Method {
@ -36,7 +37,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return resourceDeleteHandler(c, w, r)
case http.MethodPut:
// 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 {
return http.StatusInternalServerError, err
}
@ -61,11 +62,11 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
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.
f, err := getInfo(r.URL, c.FileManager, c.User)
f, err := fm.GetInfo(r.URL, c.FileManager, c.User)
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,
@ -82,7 +83,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
// Tries to get the file type.
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
@ -96,23 +97,23 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
f.Kind = "editor"
// Tries to get the editor data.
if err = f.getEditor(); err != nil {
if err = f.GetEditor(); err != nil {
return http.StatusInternalServerError, err
}
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.Kind = "listing"
// Tries to get the listing data.
if err := f.getListing(c, r); err != nil {
return errorToHTTP(err, true), err
if err := f.GetListing(c.User, r); err != nil {
return ErrorToHTTP(err, true), err
}
listing := f.listing
listing := f.Listing
// Defines the cookie scope.
cookieScope := c.RootURL()
@ -134,7 +135,7 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
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.
if r.URL.Path == "/" || !c.User.AllowEdit {
return http.StatusForbidden, nil
@ -143,13 +144,13 @@ func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Req
// Remove the file or folder.
err := c.User.FileSystem.RemoveAll(r.URL.Path)
if err != nil {
return errorToHTTP(err, true), err
return ErrorToHTTP(err, true), err
}
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 {
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
// reset error.
// reset fm.Error.
defer func() {
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.
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
// 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 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 {
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.
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
if err != nil {
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
defer f.Close()
// Copies the new content for the file.
_, err = io.Copy(f, r.Body)
if err != nil {
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
// Gets the info about the file.
fi, err := f.Stat()
if err != nil {
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
// 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
}
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")
schedule := r.Header.Get("Schedule")
@ -241,7 +242,7 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
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)
if err != nil {
log.Print(err)
@ -251,8 +252,8 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
return http.StatusOK, nil
}
func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(c.User.Scope, r.URL.Path)
// Before save command handler.
if err := c.Runner("before_publish", path); err != nil {
@ -273,7 +274,7 @@ func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// 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 {
return http.StatusForbidden, nil
}
@ -282,7 +283,7 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
action := r.Header.Get("Action")
dst, err := url.QueryUnescape(dst)
if err != nil {
return errorToHTTP(err, true), err
return ErrorToHTTP(err, true), err
}
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)
}
return errorToHTTP(err, true), err
return ErrorToHTTP(err, true), err
}
// displayMode obtains the display mode from the Cookie.

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"bytes"
@ -6,6 +6,7 @@ import (
"net/http"
"reflect"
fm "github.com/hacdias/filemanager"
"github.com/mitchellh/mapstructure"
)
@ -26,7 +27,7 @@ type option struct {
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, errEmptyRequest
return nil, fm.ErrEmptyRequest
}
// 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.
if mod.What != "settings" {
return nil, errWrongDataType
return nil, fm.ErrWrongDataType
}
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 != "/" {
return http.StatusNotFound, nil
}
@ -64,7 +65,7 @@ type settingsGetRequest struct {
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 {
return http.StatusForbidden, nil
}
@ -93,7 +94,7 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
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 {
return http.StatusForbidden, nil
}
@ -102,9 +103,10 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
if err != nil {
return http.StatusBadRequest, err
}
// Update the commands.
if mod.Which == "commands" {
if err := c.db.Set("config", "commands", mod.Data.Commands); err != nil {
if err := c.Store.Config.Save("commands", mod.Data.Commands); err != nil {
return http.StatusInternalServerError, err
}
@ -119,7 +121,7 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
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 {
return http.StatusInternalServerError, err
}

View File

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

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"encoding/json"
@ -9,7 +9,7 @@ import (
"strconv"
"strings"
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
type modifyRequest struct {
@ -19,12 +19,12 @@ type modifyRequest struct {
type modifyUserRequest struct {
*modifyRequest
Data *User `json:"data"`
Data *fm.User `json:"data"`
}
// usersHandler is the entry point of the users API. It's just a router
// to send the request to its
func usersHandler(c *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
// request, then return forbidden.
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
// 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) {
// Obtains the ID in string from the URL and converts
// 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
// body. If the body is empty or the JSON is invalid, it
// returns an error.
func getUser(r *http.Request) (*User, string, error) {
// returns an fm.Error.
func getUser(c *fm.Context, r *http.Request) (*fm.User, string, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, "", errEmptyRequest
return nil, "", fm.ErrEmptyRequest
}
// 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.
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
}
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.
if r.URL.Path == "/base" {
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.
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 {
// Copies the user info and removes its
// password so it won't be sent to the
// front-end.
u := *user
for _, u := range users {
// Removes the user password so it won't
// be sent to the front-end.
u.Password = ""
users = append(users, u)
}
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
}
// Searches for the user and prints the one who matches.
for _, user := range c.Users {
if user.ID != id {
continue
}
u := *user
u.Password = ""
return renderJSON(w, u)
u, err := c.Store.Users.Get(id, c.NewFS)
if err == fm.ErrExist {
return http.StatusNotFound, err
}
// If there aren't any matches, return not found.
return http.StatusNotFound, errUserNotExist
if err != nil {
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 != "/" {
return http.StatusMethodNotAllowed, nil
}
u, _, err := getUser(r)
u, _, err := getUser(c, r)
if err != nil {
return http.StatusBadRequest, err
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername
return http.StatusBadRequest, fm.ErrEmptyUsername
}
// Checks if filesystem isn't empty.
if u.FileSystem == "" {
return http.StatusBadRequest, errEmptyScope
// Checks if scope isn't empty.
if u.Scope == "" {
return http.StatusBadRequest, fm.ErrEmptyScope
}
// Checks if password isn't empty.
if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword
}
// The username, password and scope cannot be empty.
if u.Username == "" || u.Password == "" || u.FileSystem == "" {
return http.StatusBadRequest, errors.New("username, password or scope is empty")
return http.StatusBadRequest, fm.ErrEmptyPassword
}
// Initialize rules if they're not initialized.
if u.Rules == nil {
u.Rules = []*Rule{}
u.Rules = []*fm.Rule{}
}
// Initialize commands if not initialized.
@ -177,12 +171,12 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// 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
}
// Hashes the password.
pw, err := hashPassword(u.Password)
pw, err := fm.HashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
@ -190,18 +184,15 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
u.Password = pw
// Saves the user to the database.
err = c.db.Save(u)
if err == storm.ErrAlreadyExists {
return http.StatusConflict, errUserExist
err = c.Store.Users.Save(u)
if err == fm.ErrExist {
return http.StatusConflict, err
}
if err != nil {
return http.StatusInternalServerError, err
}
// Saves the user to the memory.
c.Users[u.Username] = u
// Set the Location header and return.
w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
w.WriteHeader(http.StatusCreated)
@ -231,7 +222,7 @@ func checkFS(path string) (int, error) {
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 == "/" {
return http.StatusMethodNotAllowed, nil
}
@ -242,27 +233,19 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
}
// Deletes the user from the database.
err = c.db.DeleteStruct(&User{ID: id})
if err == storm.ErrNotFound {
return http.StatusNotFound, errUserNotExist
err = c.Store.Users.Delete(id)
if err == fm.ErrNotExist {
return http.StatusNotFound, fm.ErrNotExist
}
if err != nil {
return http.StatusInternalServerError, err
}
// Delete the user from the in-memory users map.
for _, user := range c.Users {
if user.ID == id {
delete(c.Users, user.Username)
break
}
}
return http.StatusOK, nil
}
func usersPutHandler(c *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.
if r.URL.Path == "/" {
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.
u, which, err := getUser(r)
u, which, err := getUser(c, r)
if err != nil {
return http.StatusBadRequest, err
}
@ -289,12 +272,8 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
if which == "partial" {
c.User.CSS = u.CSS
c.User.Locale = u.Locale
err = c.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
if err != nil {
return http.StatusInternalServerError, err
}
err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale)
err = c.Store.Users.Update(c.User, "CSS", "Locale")
if err != nil {
return http.StatusInternalServerError, err
}
@ -305,16 +284,15 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// Updates the Password.
if which == "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 {
return http.StatusInternalServerError, err
}
c.User.Password = pw
err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
err = c.Store.Users.Update(c.User, "Password")
if err != nil {
return http.StatusInternalServerError, err
}
@ -324,27 +302,27 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// If can only be all.
if which != "all" {
return http.StatusBadRequest, errInvalidUpdateField
return http.StatusBadRequest, fm.ErrInvalidUpdateField
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername
return http.StatusBadRequest, fm.ErrEmptyUsername
}
// Checks if filesystem isn't empty.
if u.FileSystem == "" {
return http.StatusBadRequest, errEmptyScope
if u.Scope == "" {
return http.StatusBadRequest, fm.ErrEmptyScope
}
// 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
}
// Initialize rules if they're not initialized.
if u.Rules == nil {
u.Rules = []*Rule{}
u.Rules = []*fm.Rule{}
}
// 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.
var suser *User
for _, user := range c.Users {
if user.ID == id {
suser = user
break
}
}
if suser == nil {
suser, err := c.Store.Users.Get(id, c.NewFS)
if err == fm.ErrNotExist {
return http.StatusNotFound, nil
}
if err != nil {
return http.StatusInternalServerError, err
}
u.ID = id
// Changes the password if the request wants it.
if u.Password != "" {
pw, err := hashPassword(u.Password)
pw, err := fm.HashPassword(u.Password)
if err != nil {
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
// to send a new entire object.
err = c.db.Save(u)
err = c.Store.Users.Update(u)
if err != nil {
return http.StatusInternalServerError, err
}
// If the user changed the username, delete the old user
// from the in-memory user map.
if suser.Username != u.Username {
delete(c.Users, suser.Username)
}
c.Users[u.Username] = u
return http.StatusOK, nil
}

View File

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

9156
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
e65b8ed3dc64488f693f30f22bfd4605dd0c440c
1e22abcaf7c77746c69d510cabcbec8cc6de0bbc

View File

@ -1,4 +1,4 @@
package filemanager
package staticgen
import (
"errors"
@ -10,6 +10,7 @@ import (
"path/filepath"
"strings"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/varutils"
)
@ -17,15 +18,6 @@ var (
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.
type Hugo struct {
// Website root
@ -66,8 +58,13 @@ func (h Hugo) SettingsPath() string {
return "/config." + frontmatter
}
// Name is the plugin's name.
func (h Hugo) Name() string {
return "hugo"
}
// 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
// since we are only working with this method.
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
}
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
filename := filepath.Join(c.User.Scope, r.URL.Path)
archetype := r.Header.Get("archetype")
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.
func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
func (h Hugo) 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 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.
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.
if h.previewPath == "" {
path, err := ioutil.TempDir("", "")
@ -186,7 +183,8 @@ func (h Hugo) undraft(file string) error {
return nil
}
func (h *Hugo) find() error {
// Setup sets up the plugin.
func (h *Hugo) Setup() error {
var err error
if h.Exe, err = exec.LookPath("hugo"); err != nil {
return err
@ -194,114 +192,3 @@ func (h *Hugo) find() error {
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