diff --git a/bolt/config.go b/bolt/config.go index f74e90ee..e3d7fb81 100644 --- a/bolt/config.go +++ b/bolt/config.go @@ -1,26 +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) -} +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) +} diff --git a/bolt/users.go b/bolt/users.go index 4ee3a0e1..6189016b 100644 --- a/bolt/users.go +++ b/bolt/users.go @@ -1,90 +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}) -} +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}) +} diff --git a/caddy/filemanager/filemanager.go b/caddy/filemanager/filemanager.go index cf02ca80..979854cc 100644 --- a/caddy/filemanager/filemanager.go +++ b/caddy/filemanager/filemanager.go @@ -1,55 +1,55 @@ -// Package filemanager provides middleware for managing files in a directory -// when directory path is requested instead of a specific file. Based on browse -// middleware. -package filemanager - -import ( - "net/http" - - "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" -) - -func init() { - caddy.RegisterPlugin("filemanager", caddy.Plugin{ - ServerType: "http", - Action: setup, - }) -} - -type plugin struct { - Next httpserver.Handler - Configs []*filemanager.FileManager -} - -// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. -func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - for i := range f.Configs { - // Checks if this Path should be handled by File Manager. - if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { - continue - } - - h.Handler(f.Configs[i]).ServeHTTP(w, r) - return 0, nil - } - - return f.Next.ServeHTTP(w, r) -} - -// setup configures a new FileManager middleware instance. -func setup(c *caddy.Controller) error { - configs, err := parser.Parse(c, "") - if err != nil { - return err - } - - httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { - return plugin{Configs: configs, Next: next} - }) - - return nil -} +// Package filemanager provides middleware for managing files in a directory +// when directory path is requested instead of a specific file. Based on browse +// middleware. +package filemanager + +import ( + "net/http" + + "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" +) + +func init() { + caddy.RegisterPlugin("filemanager", caddy.Plugin{ + ServerType: "http", + Action: setup, + }) +} + +type plugin struct { + Next httpserver.Handler + Configs []*filemanager.FileManager +} + +// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. +func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + for i := range f.Configs { + // Checks if this Path should be handled by File Manager. + if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { + continue + } + + h.Handler(f.Configs[i]).ServeHTTP(w, r) + return 0, nil + } + + return f.Next.ServeHTTP(w, r) +} + +// setup configures a new FileManager middleware instance. +func setup(c *caddy.Controller) error { + configs, err := parser.Parse(c, "") + if err != nil { + return err + } + + httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return plugin{Configs: configs, Next: next} + }) + + return nil +} diff --git a/caddy/hugo/hugo.go b/caddy/hugo/hugo.go index 3badc83d..dedb24ce 100644 --- a/caddy/hugo/hugo.go +++ b/caddy/hugo/hugo.go @@ -1,52 +1,52 @@ -package hugo - -import ( - "net/http" - - "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" -) - -func init() { - caddy.RegisterPlugin("hugo", caddy.Plugin{ - ServerType: "http", - Action: setup, - }) -} - -type plugin struct { - Next httpserver.Handler - Configs []*filemanager.FileManager -} - -// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. -func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - for i := range f.Configs { - // Checks if this Path should be handled by File Manager. - if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { - continue - } - - h.Handler(f.Configs[i]).ServeHTTP(w, r) - return 0, nil - } - - return f.Next.ServeHTTP(w, r) -} - -// setup configures a new FileManager middleware instance. -func setup(c *caddy.Controller) error { - configs, err := parser.Parse(c, "hugo") - if err != nil { - return err - } - - httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { - return plugin{Configs: configs, Next: next} - }) - - return nil -} +package hugo + +import ( + "net/http" + + "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" +) + +func init() { + caddy.RegisterPlugin("hugo", caddy.Plugin{ + ServerType: "http", + Action: setup, + }) +} + +type plugin struct { + Next httpserver.Handler + Configs []*filemanager.FileManager +} + +// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. +func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + for i := range f.Configs { + // Checks if this Path should be handled by File Manager. + if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { + continue + } + + h.Handler(f.Configs[i]).ServeHTTP(w, r) + return 0, nil + } + + return f.Next.ServeHTTP(w, r) +} + +// setup configures a new FileManager middleware instance. +func setup(c *caddy.Controller) error { + configs, err := parser.Parse(c, "hugo") + if err != nil { + return err + } + + httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return plugin{Configs: configs, Next: next} + }) + + return nil +} diff --git a/caddy/jekyll/jekyll.go b/caddy/jekyll/jekyll.go index e79948d8..b41d4f5e 100644 --- a/caddy/jekyll/jekyll.go +++ b/caddy/jekyll/jekyll.go @@ -1,52 +1,52 @@ -package jekyll - -import ( - "net/http" - - "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" -) - -func init() { - caddy.RegisterPlugin("jekyll", caddy.Plugin{ - ServerType: "http", - Action: setup, - }) -} - -type plugin struct { - Next httpserver.Handler - Configs []*filemanager.FileManager -} - -// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. -func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - for i := range f.Configs { - // Checks if this Path should be handled by File Manager. - if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { - continue - } - - h.Handler(f.Configs[i]).ServeHTTP(w, r) - return 0, nil - } - - return f.Next.ServeHTTP(w, r) -} - -// setup configures a new FileManager middleware instance. -func setup(c *caddy.Controller) error { - configs, err := parser.Parse(c, "jekyll") - if err != nil { - return err - } - - httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { - return plugin{Configs: configs, Next: next} - }) - - return nil -} +package jekyll + +import ( + "net/http" + + "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" +) + +func init() { + caddy.RegisterPlugin("jekyll", caddy.Plugin{ + ServerType: "http", + Action: setup, + }) +} + +type plugin struct { + Next httpserver.Handler + Configs []*filemanager.FileManager +} + +// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. +func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + for i := range f.Configs { + // Checks if this Path should be handled by File Manager. + if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { + continue + } + + h.Handler(f.Configs[i]).ServeHTTP(w, r) + return 0, nil + } + + return f.Next.ServeHTTP(w, r) +} + +// setup configures a new FileManager middleware instance. +func setup(c *caddy.Controller) error { + configs, err := parser.Parse(c, "jekyll") + if err != nil { + return err + } + + httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return plugin{Configs: configs, Next: next} + }) + + return nil +} diff --git a/caddy/parser/parser.go b/caddy/parser/parser.go index 6a36c9b1..aa295c28 100644 --- a/caddy/parser/parser.go +++ b/caddy/parser/parser.go @@ -1,294 +1,294 @@ -package parser - -import ( - "crypto/md5" - "encoding/hex" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "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" -) - -var databases = map[string]*storm.DB{} - -// Parse ... -func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) { - var ( - configs []*filemanager.FileManager - err error - ) - - for c.Next() { - u := &filemanager.User{ - Locale: "en", - AllowCommands: true, - AllowEdit: true, - AllowNew: true, - AllowPublish: true, - Commands: []string{"git", "svn", "hg"}, - CSS: "", - Rules: []*filemanager.Rule{{ - Regex: true, - Allow: false, - Regexp: &filemanager.Regexp{Raw: "\\/\\..+"}, - }}, - } - - baseURL := "/" - scope := "." - database := "" - noAuth := false - reCaptchaKey := "" - reCaptchaSecret := "" - - if plugin != "" { - baseURL = "/admin" - } - - // Get the baseURL and scope - args := c.RemainingArgs() - - if plugin == "" { - if len(args) >= 1 { - baseURL = args[0] - } - - if len(args) > 1 { - scope = args[1] - } - } else { - if len(args) >= 1 { - scope = args[0] - } - - if len(args) > 1 { - baseURL = args[1] - } - } - - for c.NextBlock() { - switch c.Val() { - case "database": - if !c.NextArg() { - return nil, c.ArgErr() - } - - database = c.Val() - case "locale": - if !c.NextArg() { - return nil, c.ArgErr() - } - - u.Locale = c.Val() - case "allow_commands": - if !c.NextArg() { - u.AllowCommands = true - continue - } - - u.AllowCommands, err = strconv.ParseBool(c.Val()) - if err != nil { - return nil, err - } - case "allow_edit": - if !c.NextArg() { - u.AllowEdit = true - continue - } - - u.AllowEdit, err = strconv.ParseBool(c.Val()) - if err != nil { - return nil, err - } - case "allow_new": - if !c.NextArg() { - u.AllowNew = true - continue - } - - u.AllowNew, err = strconv.ParseBool(c.Val()) - if err != nil { - return nil, err - } - case "allow_publish": - if !c.NextArg() { - u.AllowPublish = true - continue - } - - u.AllowPublish, err = strconv.ParseBool(c.Val()) - if err != nil { - return nil, err - } - case "commands": - if !c.NextArg() { - return nil, c.ArgErr() - } - - u.Commands = strings.Split(c.Val(), " ") - case "css": - if !c.NextArg() { - return nil, c.ArgErr() - } - - file := c.Val() - css, err := ioutil.ReadFile(file) - if err != nil { - return nil, err - } - - u.CSS = string(css) - case "view_mode": - if !c.NextArg() { - return nil, c.ArgErr() - } - - u.ViewMode = c.Val() - if u.ViewMode != filemanager.MosaicViewMode && u.ViewMode != filemanager.ListViewMode { - return nil, c.ArgErr() - } - case "recaptcha_key": - if !c.NextArg() { - return nil, c.ArgErr() - } - - reCaptchaKey = c.Val() - case "recaptcha_secret": - if !c.NextArg() { - return nil, c.ArgErr() - } - - reCaptchaSecret = c.Val() - case "no_auth": - if !c.NextArg() { - noAuth = true - continue - } - - noAuth, err = strconv.ParseBool(c.Val()) - if err != nil { - return nil, err - } - } - } - - caddyConf := httpserver.GetConfig(c) - - path := filepath.Join(caddy.AssetsPath(), "filemanager") - err := os.MkdirAll(path, 0700) - if err != nil { - return nil, err - } - - // if there is a database path and it is not absolute, - // it will be relative to Caddy folder. - if !filepath.IsAbs(database) && database != "" { - database = filepath.Join(path, database) - } - - // If there is no database path on the settings, - // store one in .caddy/filemanager/name.db. - if database == "" { - // The name of the database is the hashed value of a string composed - // by the host, address path and the baseurl of this File Manager - // instance. - hasher := md5.New() - hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL)) - sha := hex.EncodeToString(hasher.Sum(nil)) - database = filepath.Join(path, sha+".db") - - fmt.Println("[WARNING] A database is going to be created for your File Manager instance at " + database + - ". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n") - } - - u.Scope = scope - u.FileSystem = fileutils.Dir(scope) - - 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: noAuth, - BaseURL: "", - PrefixURL: "", - ReCaptchaKey: reCaptchaKey, - ReCaptchaSecret: reCaptchaSecret, - 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 := &staticgen.Hugo{ - Root: scope, - Public: filepath.Join(scope, "public"), - Args: []string{}, - CleanPublic: true, - } - - // Attaches Hugo plugin to this file manager instance. - err = m.Attach(hugo) - if err != nil { - return nil, err - } - case "jekyll": - // Initialize the default settings for Jekyll. - jekyll := &staticgen.Jekyll{ - Root: scope, - Public: filepath.Join(scope, "_site"), - Args: []string{}, - CleanPublic: true, - } - - // Attaches Hugo plugin to this file manager instance. - err = m.Attach(jekyll) - if err != nil { - return nil, err - } - } - - if err != nil { - return nil, err - } - - m.NoAuth = noAuth - m.SetBaseURL(baseURL) - m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/")) - - configs = append(configs, m) - } - - return configs, nil -} +package parser + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "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" +) + +var databases = map[string]*storm.DB{} + +// Parse ... +func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) { + var ( + configs []*filemanager.FileManager + err error + ) + + for c.Next() { + u := &filemanager.User{ + Locale: "en", + AllowCommands: true, + AllowEdit: true, + AllowNew: true, + AllowPublish: true, + Commands: []string{"git", "svn", "hg"}, + CSS: "", + Rules: []*filemanager.Rule{{ + Regex: true, + Allow: false, + Regexp: &filemanager.Regexp{Raw: "\\/\\..+"}, + }}, + } + + baseURL := "/" + scope := "." + database := "" + noAuth := false + reCaptchaKey := "" + reCaptchaSecret := "" + + if plugin != "" { + baseURL = "/admin" + } + + // Get the baseURL and scope + args := c.RemainingArgs() + + if plugin == "" { + if len(args) >= 1 { + baseURL = args[0] + } + + if len(args) > 1 { + scope = args[1] + } + } else { + if len(args) >= 1 { + scope = args[0] + } + + if len(args) > 1 { + baseURL = args[1] + } + } + + for c.NextBlock() { + switch c.Val() { + case "database": + if !c.NextArg() { + return nil, c.ArgErr() + } + + database = c.Val() + case "locale": + if !c.NextArg() { + return nil, c.ArgErr() + } + + u.Locale = c.Val() + case "allow_commands": + if !c.NextArg() { + u.AllowCommands = true + continue + } + + u.AllowCommands, err = strconv.ParseBool(c.Val()) + if err != nil { + return nil, err + } + case "allow_edit": + if !c.NextArg() { + u.AllowEdit = true + continue + } + + u.AllowEdit, err = strconv.ParseBool(c.Val()) + if err != nil { + return nil, err + } + case "allow_new": + if !c.NextArg() { + u.AllowNew = true + continue + } + + u.AllowNew, err = strconv.ParseBool(c.Val()) + if err != nil { + return nil, err + } + case "allow_publish": + if !c.NextArg() { + u.AllowPublish = true + continue + } + + u.AllowPublish, err = strconv.ParseBool(c.Val()) + if err != nil { + return nil, err + } + case "commands": + if !c.NextArg() { + return nil, c.ArgErr() + } + + u.Commands = strings.Split(c.Val(), " ") + case "css": + if !c.NextArg() { + return nil, c.ArgErr() + } + + file := c.Val() + css, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + u.CSS = string(css) + case "view_mode": + if !c.NextArg() { + return nil, c.ArgErr() + } + + u.ViewMode = c.Val() + if u.ViewMode != filemanager.MosaicViewMode && u.ViewMode != filemanager.ListViewMode { + return nil, c.ArgErr() + } + case "recaptcha_key": + if !c.NextArg() { + return nil, c.ArgErr() + } + + reCaptchaKey = c.Val() + case "recaptcha_secret": + if !c.NextArg() { + return nil, c.ArgErr() + } + + reCaptchaSecret = c.Val() + case "no_auth": + if !c.NextArg() { + noAuth = true + continue + } + + noAuth, err = strconv.ParseBool(c.Val()) + if err != nil { + return nil, err + } + } + } + + caddyConf := httpserver.GetConfig(c) + + path := filepath.Join(caddy.AssetsPath(), "filemanager") + err := os.MkdirAll(path, 0700) + if err != nil { + return nil, err + } + + // if there is a database path and it is not absolute, + // it will be relative to Caddy folder. + if !filepath.IsAbs(database) && database != "" { + database = filepath.Join(path, database) + } + + // If there is no database path on the settings, + // store one in .caddy/filemanager/name.db. + if database == "" { + // The name of the database is the hashed value of a string composed + // by the host, address path and the baseurl of this File Manager + // instance. + hasher := md5.New() + hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL)) + sha := hex.EncodeToString(hasher.Sum(nil)) + database = filepath.Join(path, sha+".db") + + fmt.Println("[WARNING] A database is going to be created for your File Manager instance at " + database + + ". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n") + } + + u.Scope = scope + u.FileSystem = fileutils.Dir(scope) + + 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: noAuth, + BaseURL: "", + PrefixURL: "", + ReCaptchaKey: reCaptchaKey, + ReCaptchaSecret: reCaptchaSecret, + 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 := &staticgen.Hugo{ + Root: scope, + Public: filepath.Join(scope, "public"), + Args: []string{}, + CleanPublic: true, + } + + // Attaches Hugo plugin to this file manager instance. + err = m.Attach(hugo) + if err != nil { + return nil, err + } + case "jekyll": + // Initialize the default settings for Jekyll. + jekyll := &staticgen.Jekyll{ + Root: scope, + Public: filepath.Join(scope, "_site"), + Args: []string{}, + CleanPublic: true, + } + + // Attaches Hugo plugin to this file manager instance. + err = m.Attach(jekyll) + if err != nil { + return nil, err + } + } + + if err != nil { + return nil, err + } + + m.NoAuth = noAuth + m.SetBaseURL(baseURL) + m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/")) + + configs = append(configs, m) + } + + return configs, nil +} diff --git a/cmd/filemanager/main.go b/cmd/filemanager/main.go index ba03eef6..a54cdfad 100644 --- a/cmd/filemanager/main.go +++ b/cmd/filemanager/main.go @@ -1,249 +1,249 @@ -package main - -import ( - "fmt" - "io/ioutil" - "log" - "net" - "net/http" - "os" - "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" -) - -var ( - addr string - config string - database string - scope string - commands string - logfile string - staticg string - locale string - baseurl string - prefixurl string - viewMode string - recaptchakey string - recaptchasecret string - port int - noAuth bool - allowCommands bool - allowEdit bool - allowNew bool - allowPublish bool - showVer bool -) - -func init() { - flag.StringVarP(&config, "config", "c", "", "Configuration file") - flag.IntVarP(&port, "port", "p", 0, "HTTP Port (default is random)") - flag.StringVarP(&addr, "address", "a", "", "Address to listen to (default is all of them)") - flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file") - flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file") - flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users") - flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL") - flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users") - flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL") - flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users") - flag.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key") - flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret") - flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users") - flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users") - flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish 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.StringVar(&locale, "locale", "", "Default locale for new users, set it empty to enable auto detect from browser") - flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable") - flag.BoolVarP(&showVer, "version", "v", false, "Show version") -} - -func setupViper() { - viper.SetDefault("Address", "") - viper.SetDefault("Port", "0") - viper.SetDefault("Database", "./filemanager.db") - viper.SetDefault("Scope", ".") - viper.SetDefault("Logger", "stdout") - viper.SetDefault("Commands", []string{"git", "svn", "hg"}) - viper.SetDefault("AllowCommmands", true) - viper.SetDefault("AllowEdit", true) - viper.SetDefault("AllowNew", true) - viper.SetDefault("AllowPublish", true) - viper.SetDefault("StaticGen", "") - viper.SetDefault("Locale", "") - viper.SetDefault("NoAuth", false) - viper.SetDefault("BaseURL", "") - viper.SetDefault("PrefixURL", "") - viper.SetDefault("ViewMode", filemanager.MosaicViewMode) - viper.SetDefault("ReCaptchaKey", "") - viper.SetDefault("ReCaptchaSecret", "") - - viper.BindPFlag("Port", flag.Lookup("port")) - viper.BindPFlag("Address", flag.Lookup("address")) - viper.BindPFlag("Database", flag.Lookup("database")) - viper.BindPFlag("Scope", flag.Lookup("scope")) - viper.BindPFlag("Logger", flag.Lookup("log")) - viper.BindPFlag("Commands", flag.Lookup("commands")) - viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands")) - viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit")) - viper.BindPFlag("AlowNew", flag.Lookup("allow-new")) - viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish")) - viper.BindPFlag("Locale", flag.Lookup("locale")) - viper.BindPFlag("StaticGen", flag.Lookup("staticgen")) - viper.BindPFlag("NoAuth", flag.Lookup("no-auth")) - viper.BindPFlag("BaseURL", flag.Lookup("baseurl")) - viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl")) - viper.BindPFlag("ViewMode", flag.Lookup("view-mode")) - viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key")) - viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret")) - - viper.SetConfigName("filemanager") - viper.AddConfigPath(".") -} - -func printVersion() { - fmt.Println("filemanager version", filemanager.Version) - os.Exit(0) -} - -func main() { - setupViper() - flag.Parse() - - if showVer { - printVersion() - } - - // Add a configuration file if set. - if config != "" { - ext := filepath.Ext(config) - dir := filepath.Dir(config) - config = strings.TrimSuffix(config, ext) - - if dir != "" { - viper.AddConfigPath(dir) - config = strings.TrimPrefix(config, dir) - } - - viper.SetConfigName(config) - } - - // Read configuration from a file if exists. - err := viper.ReadInConfig() - if err != nil { - if _, ok := err.(viper.ConfigParseError); ok { - panic(err) - } - } - - // Set up process log before anything bad happens. - switch viper.GetString("Logger") { - case "stdout": - log.SetOutput(os.Stdout) - case "stderr": - log.SetOutput(os.Stderr) - case "": - log.SetOutput(ioutil.Discard) - default: - log.SetOutput(&lumberjack.Logger{ - Filename: logfile, - MaxSize: 100, - MaxAge: 14, - MaxBackups: 10, - }) - } - - // Builds the address and a listener. - laddr := viper.GetString("Address") + ":" + viper.GetString("Port") - listener, err := net.Listen("tcp", laddr) - if err != nil { - log.Fatal(err) - } - - // Tell the user the port in which is listening. - fmt.Println("Listening on", listener.Addr().String()) - - // Starts the server. - 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: viper.GetString("BaseURL"), - PrefixURL: viper.GetString("PrefixURL"), - ReCaptchaKey: viper.GetString("ReCaptchaKey"), - ReCaptchaSecret: viper.GetString("ReCaptchaSecret"), - 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")), - ViewMode: viper.GetString("ViewMode"), - }, - 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) -} +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/asdine/storm" + + "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" +) + +var ( + addr string + config string + database string + scope string + commands string + logfile string + staticg string + locale string + baseurl string + prefixurl string + viewMode string + recaptchakey string + recaptchasecret string + port int + noAuth bool + allowCommands bool + allowEdit bool + allowNew bool + allowPublish bool + showVer bool +) + +func init() { + flag.StringVarP(&config, "config", "c", "", "Configuration file") + flag.IntVarP(&port, "port", "p", 0, "HTTP Port (default is random)") + flag.StringVarP(&addr, "address", "a", "", "Address to listen to (default is all of them)") + flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file") + flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file") + flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users") + flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL") + flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users") + flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL") + flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users") + flag.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key") + flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret") + flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users") + flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users") + flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish 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.StringVar(&locale, "locale", "", "Default locale for new users, set it empty to enable auto detect from browser") + flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable") + flag.BoolVarP(&showVer, "version", "v", false, "Show version") +} + +func setupViper() { + viper.SetDefault("Address", "") + viper.SetDefault("Port", "0") + viper.SetDefault("Database", "./filemanager.db") + viper.SetDefault("Scope", ".") + viper.SetDefault("Logger", "stdout") + viper.SetDefault("Commands", []string{"git", "svn", "hg"}) + viper.SetDefault("AllowCommmands", true) + viper.SetDefault("AllowEdit", true) + viper.SetDefault("AllowNew", true) + viper.SetDefault("AllowPublish", true) + viper.SetDefault("StaticGen", "") + viper.SetDefault("Locale", "") + viper.SetDefault("NoAuth", false) + viper.SetDefault("BaseURL", "") + viper.SetDefault("PrefixURL", "") + viper.SetDefault("ViewMode", filemanager.MosaicViewMode) + viper.SetDefault("ReCaptchaKey", "") + viper.SetDefault("ReCaptchaSecret", "") + + viper.BindPFlag("Port", flag.Lookup("port")) + viper.BindPFlag("Address", flag.Lookup("address")) + viper.BindPFlag("Database", flag.Lookup("database")) + viper.BindPFlag("Scope", flag.Lookup("scope")) + viper.BindPFlag("Logger", flag.Lookup("log")) + viper.BindPFlag("Commands", flag.Lookup("commands")) + viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands")) + viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit")) + viper.BindPFlag("AlowNew", flag.Lookup("allow-new")) + viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish")) + viper.BindPFlag("Locale", flag.Lookup("locale")) + viper.BindPFlag("StaticGen", flag.Lookup("staticgen")) + viper.BindPFlag("NoAuth", flag.Lookup("no-auth")) + viper.BindPFlag("BaseURL", flag.Lookup("baseurl")) + viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl")) + viper.BindPFlag("ViewMode", flag.Lookup("view-mode")) + viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key")) + viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret")) + + viper.SetConfigName("filemanager") + viper.AddConfigPath(".") +} + +func printVersion() { + fmt.Println("filemanager version", filemanager.Version) + os.Exit(0) +} + +func main() { + setupViper() + flag.Parse() + + if showVer { + printVersion() + } + + // Add a configuration file if set. + if config != "" { + ext := filepath.Ext(config) + dir := filepath.Dir(config) + config = strings.TrimSuffix(config, ext) + + if dir != "" { + viper.AddConfigPath(dir) + config = strings.TrimPrefix(config, dir) + } + + viper.SetConfigName(config) + } + + // Read configuration from a file if exists. + err := viper.ReadInConfig() + if err != nil { + if _, ok := err.(viper.ConfigParseError); ok { + panic(err) + } + } + + // Set up process log before anything bad happens. + switch viper.GetString("Logger") { + case "stdout": + log.SetOutput(os.Stdout) + case "stderr": + log.SetOutput(os.Stderr) + case "": + log.SetOutput(ioutil.Discard) + default: + log.SetOutput(&lumberjack.Logger{ + Filename: logfile, + MaxSize: 100, + MaxAge: 14, + MaxBackups: 10, + }) + } + + // Builds the address and a listener. + laddr := viper.GetString("Address") + ":" + viper.GetString("Port") + listener, err := net.Listen("tcp", laddr) + if err != nil { + log.Fatal(err) + } + + // Tell the user the port in which is listening. + fmt.Println("Listening on", listener.Addr().String()) + + // Starts the server. + 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: viper.GetString("BaseURL"), + PrefixURL: viper.GetString("PrefixURL"), + ReCaptchaKey: viper.GetString("ReCaptchaKey"), + ReCaptchaSecret: viper.GetString("ReCaptchaSecret"), + 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")), + ViewMode: viper.GetString("ViewMode"), + }, + 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) +} diff --git a/doc.go b/doc.go index 71177fe5..d6cc768f 100644 --- a/doc.go +++ b/doc.go @@ -1,73 +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 +/* +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 diff --git a/filemanager.go b/filemanager.go index 22d4116e..57149d03 100644 --- a/filemanager.go +++ b/filemanager.go @@ -15,7 +15,7 @@ import ( "golang.org/x/crypto/bcrypt" - rice "github.com/GeertJohan/go.rice" + "github.com/GeertJohan/go.rice" "github.com/hacdias/fileutils" "github.com/mholt/caddy" "github.com/robfig/cron" diff --git a/http/auth.go b/http/auth.go index f61eff64..03ec09f3 100644 --- a/http/auth.go +++ b/http/auth.go @@ -9,7 +9,7 @@ import ( "strings" "time" - jwt "github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go/request" fm "github.com/hacdias/filemanager" ) diff --git a/http/http.go b/http/http.go index 6431fdb2..1aaa6a74 100644 --- a/http/http.go +++ b/http/http.go @@ -1,344 +1,344 @@ -package http - -import ( - "encoding/json" - "html/template" - "log" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - fm "github.com/hacdias/filemanager" -) - -// 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) - - txt := http.StatusText(code) - log.Printf("%v: %v %v\n", r.URL.Path, code, txt) - w.Write([]byte(txt + "\n")) - } - - if err != nil { - log.Print(err) - } - }) -} - -// serve is the main entry point of this HTML application. -func serve(c *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 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 != "" { - return http.StatusNotFound, nil - } - - r.URL.Path = p - - // Check if this request is made to the service worker. If so, - // pass it through a template to add the needed variables. - if r.URL.Path == "/sw.js" { - return renderFile(c, w, "sw.js") - } - - // Checks if this request is made to the static assets folder. If so, and - // if it is a GET request, returns with the asset. Otherwise, returns - // a status not implemented. - if matchURL(r.URL.Path, "/static") { - if r.Method != http.MethodGet { - return http.StatusNotImplemented, nil - } - - return staticHandler(c, w, r) - } - - // Checks if this request is made to the API and directs to the - // API handler if so. - if matchURL(r.URL.Path, "/api") { - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api") - return apiHandler(c, w, r) - } - - // If it is a request to the preview and a static website generator is - // active, build the preview. - if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil { - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview") - return c.StaticGen.Preview(c, w, r) - } - - if strings.HasPrefix(r.URL.Path, "/share/") { - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/") - return sharePage(c, w, r) - } - - // Any other request should show the index.html file. - w.Header().Set("x-frame-options", "SAMEORIGIN") - w.Header().Set("x-content-type", "nosniff") - w.Header().Set("x-xss-protection", "1; mode=block") - - return renderFile(c, w, "index.html") -} - -// staticHandler handles the static assets path. -func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - if r.URL.Path != "/static/manifest.json" { - http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r) - return 0, nil - } - - return renderFile(c, w, "static/manifest.json") -} - -// apiHandler is the main entry point for the /api endpoint. -func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - if r.URL.Path == "/auth/get" { - return authHandler(c, w, r) - } - - if r.URL.Path == "/auth/renew" { - return renewAuthHandler(c, w, r) - } - - valid, _ := validateAuth(c, r) - if !valid { - return http.StatusForbidden, nil - } - - c.Router, r.URL.Path = splitURL(r.URL.Path) - - if !c.User.Allowed(r.URL.Path) { - return http.StatusForbidden, nil - } - - if c.StaticGen != nil { - // If we are using the 'magic url' for the settings, - // we should redirect the request for the acutual path. - if r.URL.Path == "/settings" { - r.URL.Path = c.StaticGen.SettingsPath() - } - - // Executes the Static website generator hook. - code, err := c.StaticGen.Hook(c, w, r) - if code != 0 || err != nil { - return code, err - } - } - - if c.Router == "checksum" || c.Router == "download" { - var err error - c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User) - if err != nil { - return ErrorToHTTP(err, false), err - } - } - - var code int - var err error - - switch c.Router { - case "download": - code, err = downloadHandler(c, w, r) - case "checksum": - code, err = checksumHandler(c, w, r) - case "command": - code, err = command(c, w, r) - case "search": - code, err = search(c, w, r) - case "resource": - code, err = resourceHandler(c, w, r) - case "users": - code, err = usersHandler(c, w, r) - case "settings": - code, err = settingsHandler(c, w, r) - case "share": - code, err = shareHandler(c, w, r) - default: - code = http.StatusNotFound - } - - return code, err -} - -// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512. -func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - query := r.URL.Query().Get("algo") - - val, err := c.File.Checksum(query) - if err == fm.ErrInvalidOption { - return http.StatusBadRequest, err - } else if err != nil { - return http.StatusInternalServerError, err - } - - w.Write([]byte(val)) - return 0, nil -} - -// splitURL splits the path and returns everything that stands -// before the first slash and everything that goes after. -func splitURL(path string) (string, string) { - if path == "" { - return "", "" - } - - path = strings.TrimPrefix(path, "/") - - i := strings.Index(path, "/") - if i == -1 { - return "", path - } - - return path[0:i], path[i:] -} - -// renderFile renders a file using a template with some needed variables. -func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) { - tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file))) - - var contentType string - switch filepath.Ext(file) { - case ".html": - contentType = "text/html" - case ".js": - contentType = "application/javascript" - case ".json": - contentType = "application/json" - default: - contentType = "text" - } - - w.Header().Set("Content-Type", contentType+"; charset=utf-8") - - data := map[string]interface{}{ - "BaseURL": c.RootURL(), - "NoAuth": c.NoAuth, - "Version": fm.Version, - "CSS": template.CSS(c.CSS), - "ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "", - "ReCaptchaKey": c.ReCaptchaKey, - "ReCaptchaSecret": c.ReCaptchaSecret, - } - - if c.StaticGen != nil { - data["StaticGen"] = c.StaticGen.Name() - } - - err := tpl.Execute(w, data) - - if err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil -} - -// sharePage build the share page. -func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - s, err := c.Store.Share.Get(r.URL.Path) - if err == fm.ErrNotExist { - w.WriteHeader(http.StatusNotFound) - return renderFile(c, w, "static/share/404.html") - } - - if err != nil { - return http.StatusInternalServerError, err - } - - if s.Expires && s.ExpireDate.Before(time.Now()) { - c.Store.Share.Delete(s.Hash) - w.WriteHeader(http.StatusNotFound) - return renderFile(c, w, "static/share/404.html") - } - - r.URL.Path = s.Path - - info, err := os.Stat(s.Path) - if err != nil { - c.Store.Share.Delete(s.Hash) - return ErrorToHTTP(err, false), err - } - - c.File = &fm.File{ - Path: s.Path, - Name: info.Name(), - ModTime: info.ModTime(), - Mode: info.Mode(), - IsDir: info.IsDir(), - Size: info.Size(), - } - - dl := r.URL.Query().Get("dl") - - if dl == "" || dl == "0" { - tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html"))) - w.Header().Set("Content-Type", "text/html; charset=utf-8") - - err := tpl.Execute(w, map[string]interface{}{ - "BaseURL": c.RootURL(), - "File": c.File, - }) - - if err != nil { - return http.StatusInternalServerError, err - } - return 0, nil - } - - return downloadHandler(c, w, r) -} - -// renderJSON prints the JSON version of data to the browser. -func renderJSON(w http.ResponseWriter, data interface{}) (int, error) { - marsh, err := json.Marshal(data) - if err != nil { - return http.StatusInternalServerError, err - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - if _, err := w.Write(marsh); err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil -} - -// matchURL checks if the first URL matches the second. -func matchURL(first, second string) bool { - first = strings.ToLower(first) - second = strings.ToLower(second) - - return strings.HasPrefix(first, second) -} - -// ErrorToHTTP converts errors to HTTP Status Code. -func ErrorToHTTP(err error, gone bool) int { - switch { - case err == nil: - return http.StatusOK - case os.IsPermission(err): - return http.StatusForbidden - case os.IsNotExist(err): - if !gone { - return http.StatusNotFound - } - - return http.StatusGone - case os.IsExist(err): - return http.StatusConflict - default: - return http.StatusInternalServerError - } -} +package http + +import ( + "encoding/json" + "html/template" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + fm "github.com/hacdias/filemanager" +) + +// 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) + + txt := http.StatusText(code) + log.Printf("%v: %v %v\n", r.URL.Path, code, txt) + w.Write([]byte(txt + "\n")) + } + + if err != nil { + log.Print(err) + } + }) +} + +// serve is the main entry point of this HTML application. +func serve(c *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 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 != "" { + return http.StatusNotFound, nil + } + + r.URL.Path = p + + // Check if this request is made to the service worker. If so, + // pass it through a template to add the needed variables. + if r.URL.Path == "/sw.js" { + return renderFile(c, w, "sw.js") + } + + // Checks if this request is made to the static assets folder. If so, and + // if it is a GET request, returns with the asset. Otherwise, returns + // a status not implemented. + if matchURL(r.URL.Path, "/static") { + if r.Method != http.MethodGet { + return http.StatusNotImplemented, nil + } + + return staticHandler(c, w, r) + } + + // Checks if this request is made to the API and directs to the + // API handler if so. + if matchURL(r.URL.Path, "/api") { + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api") + return apiHandler(c, w, r) + } + + // If it is a request to the preview and a static website generator is + // active, build the preview. + if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil { + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview") + return c.StaticGen.Preview(c, w, r) + } + + if strings.HasPrefix(r.URL.Path, "/share/") { + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/") + return sharePage(c, w, r) + } + + // Any other request should show the index.html file. + w.Header().Set("x-frame-options", "SAMEORIGIN") + w.Header().Set("x-content-type", "nosniff") + w.Header().Set("x-xss-protection", "1; mode=block") + + return renderFile(c, w, "index.html") +} + +// staticHandler handles the static assets path. +func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + if r.URL.Path != "/static/manifest.json" { + http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r) + return 0, nil + } + + return renderFile(c, w, "static/manifest.json") +} + +// apiHandler is the main entry point for the /api endpoint. +func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + if r.URL.Path == "/auth/get" { + return authHandler(c, w, r) + } + + if r.URL.Path == "/auth/renew" { + return renewAuthHandler(c, w, r) + } + + valid, _ := validateAuth(c, r) + if !valid { + return http.StatusForbidden, nil + } + + c.Router, r.URL.Path = splitURL(r.URL.Path) + + if !c.User.Allowed(r.URL.Path) { + return http.StatusForbidden, nil + } + + if c.StaticGen != nil { + // If we are using the 'magic url' for the settings, + // we should redirect the request for the acutual path. + if r.URL.Path == "/settings" { + r.URL.Path = c.StaticGen.SettingsPath() + } + + // Executes the Static website generator hook. + code, err := c.StaticGen.Hook(c, w, r) + if code != 0 || err != nil { + return code, err + } + } + + if c.Router == "checksum" || c.Router == "download" { + var err error + c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User) + if err != nil { + return ErrorToHTTP(err, false), err + } + } + + var code int + var err error + + switch c.Router { + case "download": + code, err = downloadHandler(c, w, r) + case "checksum": + code, err = checksumHandler(c, w, r) + case "command": + code, err = command(c, w, r) + case "search": + code, err = search(c, w, r) + case "resource": + code, err = resourceHandler(c, w, r) + case "users": + code, err = usersHandler(c, w, r) + case "settings": + code, err = settingsHandler(c, w, r) + case "share": + code, err = shareHandler(c, w, r) + default: + code = http.StatusNotFound + } + + return code, err +} + +// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512. +func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + query := r.URL.Query().Get("algo") + + val, err := c.File.Checksum(query) + if err == fm.ErrInvalidOption { + return http.StatusBadRequest, err + } else if err != nil { + return http.StatusInternalServerError, err + } + + w.Write([]byte(val)) + return 0, nil +} + +// splitURL splits the path and returns everything that stands +// before the first slash and everything that goes after. +func splitURL(path string) (string, string) { + if path == "" { + return "", "" + } + + path = strings.TrimPrefix(path, "/") + + i := strings.Index(path, "/") + if i == -1 { + return "", path + } + + return path[0:i], path[i:] +} + +// renderFile renders a file using a template with some needed variables. +func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) { + tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file))) + + var contentType string + switch filepath.Ext(file) { + case ".html": + contentType = "text/html" + case ".js": + contentType = "application/javascript" + case ".json": + contentType = "application/json" + default: + contentType = "text" + } + + w.Header().Set("Content-Type", contentType+"; charset=utf-8") + + data := map[string]interface{}{ + "BaseURL": c.RootURL(), + "NoAuth": c.NoAuth, + "Version": fm.Version, + "CSS": template.CSS(c.CSS), + "ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "", + "ReCaptchaKey": c.ReCaptchaKey, + "ReCaptchaSecret": c.ReCaptchaSecret, + } + + if c.StaticGen != nil { + data["StaticGen"] = c.StaticGen.Name() + } + + err := tpl.Execute(w, data) + + if err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil +} + +// sharePage build the share page. +func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + s, err := c.Store.Share.Get(r.URL.Path) + if err == fm.ErrNotExist { + w.WriteHeader(http.StatusNotFound) + return renderFile(c, w, "static/share/404.html") + } + + if err != nil { + return http.StatusInternalServerError, err + } + + if s.Expires && s.ExpireDate.Before(time.Now()) { + c.Store.Share.Delete(s.Hash) + w.WriteHeader(http.StatusNotFound) + return renderFile(c, w, "static/share/404.html") + } + + r.URL.Path = s.Path + + info, err := os.Stat(s.Path) + if err != nil { + c.Store.Share.Delete(s.Hash) + return ErrorToHTTP(err, false), err + } + + c.File = &fm.File{ + Path: s.Path, + Name: info.Name(), + ModTime: info.ModTime(), + Mode: info.Mode(), + IsDir: info.IsDir(), + Size: info.Size(), + } + + dl := r.URL.Query().Get("dl") + + if dl == "" || dl == "0" { + tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html"))) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + err := tpl.Execute(w, map[string]interface{}{ + "BaseURL": c.RootURL(), + "File": c.File, + }) + + if err != nil { + return http.StatusInternalServerError, err + } + return 0, nil + } + + return downloadHandler(c, w, r) +} + +// renderJSON prints the JSON version of data to the browser. +func renderJSON(w http.ResponseWriter, data interface{}) (int, error) { + marsh, err := json.Marshal(data) + if err != nil { + return http.StatusInternalServerError, err + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if _, err := w.Write(marsh); err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil +} + +// matchURL checks if the first URL matches the second. +func matchURL(first, second string) bool { + first = strings.ToLower(first) + second = strings.ToLower(second) + + return strings.HasPrefix(first, second) +} + +// ErrorToHTTP converts errors to HTTP Status Code. +func ErrorToHTTP(err error, gone bool) int { + switch { + case err == nil: + return http.StatusOK + case os.IsPermission(err): + return http.StatusForbidden + case os.IsNotExist(err): + if !gone { + return http.StatusNotFound + } + + return http.StatusGone + case os.IsExist(err): + return http.StatusConflict + default: + return http.StatusInternalServerError + } +} diff --git a/http/resource.go b/http/resource.go index 01132993..a312de90 100644 --- a/http/resource.go +++ b/http/resource.go @@ -1,386 +1,386 @@ -package http - -import ( - "errors" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" - "time" - - fm "github.com/hacdias/filemanager" - "github.com/hacdias/fileutils" -) - -// sanitizeURL sanitizes the URL to prevent path transversal -// using fileutils.SlashClean and adds the trailing slash bar. -func sanitizeURL(url string) string { - path := fileutils.SlashClean(url) - if strings.HasSuffix(url, "/") && path != "/" { - return path + "/" - } - return path -} - -func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - r.URL.Path = sanitizeURL(r.URL.Path) - - switch r.Method { - case http.MethodGet: - return resourceGetHandler(c, w, r) - case http.MethodDelete: - return resourceDeleteHandler(c, w, r) - case http.MethodPut: - // Before save command handler. - path := filepath.Join(c.User.Scope, r.URL.Path) - if err := c.Runner("before_save", path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - code, err := resourcePostPutHandler(c, w, r) - if code != http.StatusOK { - return code, err - } - - // After save command handler. - if err := c.Runner("after_save", path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - return code, err - case http.MethodPatch: - return resourcePatchHandler(c, w, r) - case http.MethodPost: - return resourcePostPutHandler(c, w, r) - } - - return http.StatusNotImplemented, nil -} - -func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Gets the information of the directory/file. - f, err := fm.GetInfo(r.URL, c.FileManager, c.User) - if err != nil { - return ErrorToHTTP(err, false), err - } - - // If it's a dir and the path doesn't end with a trailing slash, - // add a trailing slash to the path. - if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") { - r.URL.Path = r.URL.Path + "/" - } - - // If it is a dir, go and serve the listing. - if f.IsDir { - c.File = f - return listingHandler(c, w, r) - } - - // Tries to get the file type. - if err = f.GetFileType(true); err != nil { - return ErrorToHTTP(err, true), err - } - - // Serve a preview if the file can't be edited or the - // user has no permission to edit this file. Otherwise, - // just serve the editor. - if !f.CanBeEdited() || !c.User.AllowEdit { - f.Kind = "preview" - return renderJSON(w, f) - } - - f.Kind = "editor" - - // Tries to get the editor data. - if err = f.GetEditor(); err != nil { - return http.StatusInternalServerError, err - } - - return renderJSON(w, f) -} - -func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - f := c.File - f.Kind = "listing" - - // Tries to get the listing data. - if err := f.GetListing(c.User, r); err != nil { - return ErrorToHTTP(err, true), err - } - - listing := f.Listing - - // Defines the cookie scope. - cookieScope := c.RootURL() - if cookieScope == "" { - cookieScope = "/" - } - - // Copy the query values into the Listing struct - if sort, order, err := handleSortOrder(w, r, cookieScope); err == nil { - listing.Sort = sort - listing.Order = order - } else { - return http.StatusBadRequest, err - } - - listing.ApplySort() - return renderJSON(w, f) -} - -func resourceDeleteHandler(c *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 - } - - // Fire the before trigger. - if err := c.Runner("before_delete", r.URL.Path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - // Remove the file or folder. - err := c.User.FileSystem.RemoveAll(r.URL.Path) - if err != nil { - return ErrorToHTTP(err, true), err - } - - // Fire the after trigger. - if err := c.Runner("after_delete", r.URL.Path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - return http.StatusOK, nil -} - -func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - if !c.User.AllowNew && r.Method == http.MethodPost { - return http.StatusForbidden, nil - } - - if !c.User.AllowEdit && r.Method == http.MethodPut { - return http.StatusForbidden, nil - } - - // Discard any invalid upload before returning to avoid connection - // reset error. - defer func() { - io.Copy(ioutil.Discard, r.Body) - }() - - // Checks if the current request is for a directory and not a file. - if strings.HasSuffix(r.URL.Path, "/") { - // If the method is PUT, we return 405 Method not Allowed, because - // POST should be used instead. - if r.Method == http.MethodPut { - return http.StatusMethodNotAllowed, nil - } - - // Otherwise we try to create the directory. - err := c.User.FileSystem.Mkdir(r.URL.Path, 0776) - return ErrorToHTTP(err, false), err - } - - // If using POST method, we are trying to create a new file so it is not - // desirable to override an already existent file. Thus, we check - // if the file already exists. If so, we just return a 409 Conflict. - if r.Method == http.MethodPost && r.Header.Get("Action") != "override" { - if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil { - return http.StatusConflict, errors.New("There is already a file on that path") - } - } - - // Fire the before trigger. - if err := c.Runner("before_upload", r.URL.Path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - // Create/Open the file. - f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776) - if err != nil { - return ErrorToHTTP(err, false), err - } - defer f.Close() - - // Copies the new content for the file. - _, err = io.Copy(f, r.Body) - if err != nil { - return ErrorToHTTP(err, false), err - } - - // Gets the info about the file. - fi, err := f.Stat() - if err != nil { - return ErrorToHTTP(err, false), err - } - - // Check if this instance has a Static Generator and handles publishing - // or scheduling if it's the case. - if c.StaticGen != nil { - code, err := resourcePublishSchedule(c, w, r) - if code != 0 { - return code, err - } - } - - // Writes the ETag Header. - etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()) - w.Header().Set("ETag", etag) - - // Fire the after trigger. - if err := c.Runner("after_upload", r.URL.Path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - return http.StatusOK, nil -} - -func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - publish := r.Header.Get("Publish") - schedule := r.Header.Get("Schedule") - - if publish != "true" && schedule == "" { - return 0, nil - } - - if !c.User.AllowPublish { - return http.StatusForbidden, nil - } - - if publish == "true" { - return resourcePublish(c, w, r) - } - - t, err := time.Parse("2006-01-02T15:04", schedule) - if err != nil { - return http.StatusInternalServerError, err - } - - c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() { - _, err := resourcePublish(c, w, r) - if err != nil { - log.Print(err) - } - }) - - return http.StatusOK, nil -} - -func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - path := filepath.Join(c.User.Scope, r.URL.Path) - - // Before save command handler. - if err := c.Runner("before_publish", path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - code, err := c.StaticGen.Publish(c, w, r) - if err != nil { - return code, err - } - - // Executed the before publish command. - if err := c.Runner("before_publish", path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - return code, nil -} - -// resourcePatchHandler is the entry point for resource handler. -func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - if !c.User.AllowEdit { - return http.StatusForbidden, nil - } - - dst := r.Header.Get("Destination") - action := r.Header.Get("Action") - dst, err := url.QueryUnescape(dst) - if err != nil { - return ErrorToHTTP(err, true), err - } - - src := r.URL.Path - - if dst == "/" || src == "/" { - return http.StatusForbidden, nil - } - - if action == "copy" { - // Fire the after trigger. - if err := c.Runner("before_copy", src, dst, c.User); err != nil { - return http.StatusInternalServerError, err - } - - // Copy the file. - err = c.User.FileSystem.Copy(src, dst) - - // Fire the after trigger. - if err := c.Runner("after_copy", src, dst, c.User); err != nil { - return http.StatusInternalServerError, err - } - } else { - // Fire the after trigger. - if err := c.Runner("before_rename", src, dst, c.User); err != nil { - return http.StatusInternalServerError, err - } - - // Rename the file. - err = c.User.FileSystem.Rename(src, dst) - - // Fire the after trigger. - if err := c.Runner("after_rename", src, dst, c.User); err != nil { - return http.StatusInternalServerError, err - } - } - - return ErrorToHTTP(err, true), err -} - -// handleSortOrder gets and stores for a Listing the 'sort' and 'order', -// and reads 'limit' if given. The latter is 0 if not given. Sets cookies. -func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, err error) { - sort = r.URL.Query().Get("sort") - order = r.URL.Query().Get("order") - - // If the query 'sort' or 'order' is empty, use defaults or any values - // previously saved in Cookies. - switch sort { - case "": - sort = "name" - if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { - sort = sortCookie.Value - } - case "name", "size": - http.SetCookie(w, &http.Cookie{ - Name: "sort", - Value: sort, - MaxAge: 31536000, - Path: scope, - Secure: r.TLS != nil, - }) - } - - switch order { - case "": - order = "asc" - if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { - order = orderCookie.Value - } - case "asc", "desc": - http.SetCookie(w, &http.Cookie{ - Name: "order", - Value: order, - MaxAge: 31536000, - Path: scope, - Secure: r.TLS != nil, - }) - } - - return -} +package http + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + fm "github.com/hacdias/filemanager" + "github.com/hacdias/fileutils" +) + +// sanitizeURL sanitizes the URL to prevent path transversal +// using fileutils.SlashClean and adds the trailing slash bar. +func sanitizeURL(url string) string { + path := fileutils.SlashClean(url) + if strings.HasSuffix(url, "/") && path != "/" { + return path + "/" + } + return path +} + +func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + r.URL.Path = sanitizeURL(r.URL.Path) + + switch r.Method { + case http.MethodGet: + return resourceGetHandler(c, w, r) + case http.MethodDelete: + return resourceDeleteHandler(c, w, r) + case http.MethodPut: + // Before save command handler. + path := filepath.Join(c.User.Scope, r.URL.Path) + if err := c.Runner("before_save", path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + code, err := resourcePostPutHandler(c, w, r) + if code != http.StatusOK { + return code, err + } + + // After save command handler. + if err := c.Runner("after_save", path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + return code, err + case http.MethodPatch: + return resourcePatchHandler(c, w, r) + case http.MethodPost: + return resourcePostPutHandler(c, w, r) + } + + return http.StatusNotImplemented, nil +} + +func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + // Gets the information of the directory/file. + f, err := fm.GetInfo(r.URL, c.FileManager, c.User) + if err != nil { + return ErrorToHTTP(err, false), err + } + + // If it's a dir and the path doesn't end with a trailing slash, + // add a trailing slash to the path. + if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") { + r.URL.Path = r.URL.Path + "/" + } + + // If it is a dir, go and serve the listing. + if f.IsDir { + c.File = f + return listingHandler(c, w, r) + } + + // Tries to get the file type. + if err = f.GetFileType(true); err != nil { + return ErrorToHTTP(err, true), err + } + + // Serve a preview if the file can't be edited or the + // user has no permission to edit this file. Otherwise, + // just serve the editor. + if !f.CanBeEdited() || !c.User.AllowEdit { + f.Kind = "preview" + return renderJSON(w, f) + } + + f.Kind = "editor" + + // Tries to get the editor data. + if err = f.GetEditor(); err != nil { + return http.StatusInternalServerError, err + } + + return renderJSON(w, f) +} + +func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + f := c.File + f.Kind = "listing" + + // Tries to get the listing data. + if err := f.GetListing(c.User, r); err != nil { + return ErrorToHTTP(err, true), err + } + + listing := f.Listing + + // Defines the cookie scope. + cookieScope := c.RootURL() + if cookieScope == "" { + cookieScope = "/" + } + + // Copy the query values into the Listing struct + if sort, order, err := handleSortOrder(w, r, cookieScope); err == nil { + listing.Sort = sort + listing.Order = order + } else { + return http.StatusBadRequest, err + } + + listing.ApplySort() + return renderJSON(w, f) +} + +func resourceDeleteHandler(c *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 + } + + // Fire the before trigger. + if err := c.Runner("before_delete", r.URL.Path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + // Remove the file or folder. + err := c.User.FileSystem.RemoveAll(r.URL.Path) + if err != nil { + return ErrorToHTTP(err, true), err + } + + // Fire the after trigger. + if err := c.Runner("after_delete", r.URL.Path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil +} + +func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + if !c.User.AllowNew && r.Method == http.MethodPost { + return http.StatusForbidden, nil + } + + if !c.User.AllowEdit && r.Method == http.MethodPut { + return http.StatusForbidden, nil + } + + // Discard any invalid upload before returning to avoid connection + // reset error. + defer func() { + io.Copy(ioutil.Discard, r.Body) + }() + + // Checks if the current request is for a directory and not a file. + if strings.HasSuffix(r.URL.Path, "/") { + // If the method is PUT, we return 405 Method not Allowed, because + // POST should be used instead. + if r.Method == http.MethodPut { + return http.StatusMethodNotAllowed, nil + } + + // Otherwise we try to create the directory. + err := c.User.FileSystem.Mkdir(r.URL.Path, 0776) + return ErrorToHTTP(err, false), err + } + + // If using POST method, we are trying to create a new file so it is not + // desirable to override an already existent file. Thus, we check + // if the file already exists. If so, we just return a 409 Conflict. + if r.Method == http.MethodPost && r.Header.Get("Action") != "override" { + if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil { + return http.StatusConflict, errors.New("There is already a file on that path") + } + } + + // Fire the before trigger. + if err := c.Runner("before_upload", r.URL.Path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + // Create/Open the file. + f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776) + if err != nil { + return ErrorToHTTP(err, false), err + } + defer f.Close() + + // Copies the new content for the file. + _, err = io.Copy(f, r.Body) + if err != nil { + return ErrorToHTTP(err, false), err + } + + // Gets the info about the file. + fi, err := f.Stat() + if err != nil { + return ErrorToHTTP(err, false), err + } + + // Check if this instance has a Static Generator and handles publishing + // or scheduling if it's the case. + if c.StaticGen != nil { + code, err := resourcePublishSchedule(c, w, r) + if code != 0 { + return code, err + } + } + + // Writes the ETag Header. + etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()) + w.Header().Set("ETag", etag) + + // Fire the after trigger. + if err := c.Runner("after_upload", r.URL.Path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil +} + +func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + publish := r.Header.Get("Publish") + schedule := r.Header.Get("Schedule") + + if publish != "true" && schedule == "" { + return 0, nil + } + + if !c.User.AllowPublish { + return http.StatusForbidden, nil + } + + if publish == "true" { + return resourcePublish(c, w, r) + } + + t, err := time.Parse("2006-01-02T15:04", schedule) + if err != nil { + return http.StatusInternalServerError, err + } + + c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() { + _, err := resourcePublish(c, w, r) + if err != nil { + log.Print(err) + } + }) + + return http.StatusOK, nil +} + +func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + path := filepath.Join(c.User.Scope, r.URL.Path) + + // Before save command handler. + if err := c.Runner("before_publish", path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + code, err := c.StaticGen.Publish(c, w, r) + if err != nil { + return code, err + } + + // Executed the before publish command. + if err := c.Runner("before_publish", path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + return code, nil +} + +// resourcePatchHandler is the entry point for resource handler. +func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + if !c.User.AllowEdit { + return http.StatusForbidden, nil + } + + dst := r.Header.Get("Destination") + action := r.Header.Get("Action") + dst, err := url.QueryUnescape(dst) + if err != nil { + return ErrorToHTTP(err, true), err + } + + src := r.URL.Path + + if dst == "/" || src == "/" { + return http.StatusForbidden, nil + } + + if action == "copy" { + // Fire the after trigger. + if err := c.Runner("before_copy", src, dst, c.User); err != nil { + return http.StatusInternalServerError, err + } + + // Copy the file. + err = c.User.FileSystem.Copy(src, dst) + + // Fire the after trigger. + if err := c.Runner("after_copy", src, dst, c.User); err != nil { + return http.StatusInternalServerError, err + } + } else { + // Fire the after trigger. + if err := c.Runner("before_rename", src, dst, c.User); err != nil { + return http.StatusInternalServerError, err + } + + // Rename the file. + err = c.User.FileSystem.Rename(src, dst) + + // Fire the after trigger. + if err := c.Runner("after_rename", src, dst, c.User); err != nil { + return http.StatusInternalServerError, err + } + } + + return ErrorToHTTP(err, true), err +} + +// handleSortOrder gets and stores for a Listing the 'sort' and 'order', +// and reads 'limit' if given. The latter is 0 if not given. Sets cookies. +func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, err error) { + sort = r.URL.Query().Get("sort") + order = r.URL.Query().Get("order") + + // If the query 'sort' or 'order' is empty, use defaults or any values + // previously saved in Cookies. + switch sort { + case "": + sort = "name" + if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { + sort = sortCookie.Value + } + case "name", "size": + http.SetCookie(w, &http.Cookie{ + Name: "sort", + Value: sort, + MaxAge: 31536000, + Path: scope, + Secure: r.TLS != nil, + }) + } + + switch order { + case "": + order = "asc" + if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { + order = orderCookie.Value + } + case "asc", "desc": + http.SetCookie(w, &http.Cookie{ + Name: "order", + Value: order, + MaxAge: 31536000, + Path: scope, + Secure: r.TLS != nil, + }) + } + + return +} diff --git a/http/websockets.go b/http/websockets.go index 8d73ab06..8f6d867f 100644 --- a/http/websockets.go +++ b/http/websockets.go @@ -1,339 +1,339 @@ -package http - -import ( - "bytes" - "encoding/json" - "mime" - "net/http" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - "time" - - "github.com/gorilla/websocket" - fm "github.com/hacdias/filemanager" -) - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, -} - -var ( - cmdNotImplemented = []byte("Command not implemented.") - cmdNotAllowed = []byte("Command not allowed.") -) - -// command handles the requests for VCS related commands: git, svn and mercurial -func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Upgrades the connection to a websocket and checks for fm.Errors. - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return 0, err - } - defer conn.Close() - - var ( - message []byte - command []string - ) - - // Starts an infinite loop until a valid command is captured. - for { - _, message, err = conn.ReadMessage() - if err != nil { - return http.StatusInternalServerError, err - } - - command = strings.Split(string(message), " ") - if len(command) != 0 { - break - } - } - - // Check if the command is allowed - allowed := false - - for _, cmd := range c.User.Commands { - if cmd == command[0] { - allowed = true - } - } - - if !allowed { - err = conn.WriteMessage(websocket.BinaryMessage, cmdNotAllowed) - if err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil - } - - // Check if the program is talled is installed on the computer. - if _, err = exec.LookPath(command[0]); err != nil { - err = conn.WriteMessage(websocket.BinaryMessage, cmdNotImplemented) - if err != nil { - return http.StatusInternalServerError, err - } - - return http.StatusNotImplemented, nil - } - - // Gets the path and initializes a buffer. - path := c.User.Scope + "/" + r.URL.Path - path = filepath.Clean(path) - buff := new(bytes.Buffer) - - // Sets up the command executation. - cmd := exec.Command(command[0], command[1:]...) - cmd.Dir = path - cmd.Stderr = buff - cmd.Stdout = buff - - // Starts the command and checks for fm.Errors. - err = cmd.Start() - if err != nil { - return http.StatusInternalServerError, err - } - - // Set a 'done' variable to check whetever the command has already finished - // running or not. This verification is done using a goroutine that uses the - // method .Wait() from the command. - done := false - go func() { - err = cmd.Wait() - done = true - }() - - // Function to print the current information on the buffer to the connection. - print := func() error { - by := buff.Bytes() - if len(by) > 0 { - err = conn.WriteMessage(websocket.TextMessage, by) - if err != nil { - return err - } - } - - return nil - } - - // While the command hasn't finished running, continue sending the output - // to the client in intervals of 100 milliseconds. - for !done { - if err = print(); err != nil { - return http.StatusInternalServerError, err - } - - time.Sleep(100 * time.Millisecond) - } - - // After the command is done executing, send the output one more time to the - // browser to make sure it gets the latest information. - if err = print(); err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil -} - -var ( - typeRegexp = regexp.MustCompile(`type:(\w+)`) -) - -type condition func(path string) bool - -type searchOptions struct { - CaseInsensitive bool - Conditions []condition - Terms []string -} - -func extensionCondition(extension string) condition { - return func(path string) bool { - return filepath.Ext(path) == "."+extension - } -} - -func imageCondition(path string) bool { - extension := filepath.Ext(path) - mimetype := mime.TypeByExtension(extension) - - return strings.HasPrefix(mimetype, "image") -} - -func audioCondition(path string) bool { - extension := filepath.Ext(path) - mimetype := mime.TypeByExtension(extension) - - return strings.HasPrefix(mimetype, "audio") -} - -func videoCondition(path string) bool { - extension := filepath.Ext(path) - mimetype := mime.TypeByExtension(extension) - - return strings.HasPrefix(mimetype, "video") -} - -func parseSearch(value string) *searchOptions { - opts := &searchOptions{ - CaseInsensitive: strings.Contains(value, "case:insensitive"), - Conditions: []condition{}, - Terms: []string{}, - } - - // removes the options from the value - value = strings.Replace(value, "case:insensitive", "", -1) - value = strings.Replace(value, "case:sensitive", "", -1) - value = strings.TrimSpace(value) - - types := typeRegexp.FindAllStringSubmatch(value, -1) - for _, t := range types { - if len(t) == 1 { - continue - } - - switch t[1] { - case "image": - opts.Conditions = append(opts.Conditions, imageCondition) - case "audio", "music": - opts.Conditions = append(opts.Conditions, audioCondition) - case "video": - opts.Conditions = append(opts.Conditions, videoCondition) - default: - opts.Conditions = append(opts.Conditions, extensionCondition(t[1])) - } - } - - if len(types) > 0 { - // Remove the fields from the search value. - value = typeRegexp.ReplaceAllString(value, "") - } - - // If it's canse insensitive, put everything in lowercase. - if opts.CaseInsensitive { - value = strings.ToLower(value) - } - - // Remove the spaces from the search value. - value = strings.TrimSpace(value) - - if value == "" { - return opts - } - - // if the value starts with " and finishes what that character, we will - // only search for that term - if value[0] == '"' && value[len(value)-1] == '"' { - unique := strings.TrimPrefix(value, "\"") - unique = strings.TrimSuffix(unique, "\"") - - opts.Terms = []string{unique} - return opts - } - - opts.Terms = strings.Split(value, " ") - return opts -} - -// search searches for a file or directory. -func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Upgrades the connection to a websocket and checks for fm.Errors. - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return 0, err - } - defer conn.Close() - - var ( - value string - search *searchOptions - message []byte - ) - - // Starts an infinite loop until a valid command is captured. - for { - _, message, err = conn.ReadMessage() - if err != nil { - return http.StatusInternalServerError, err - } - - if len(message) != 0 { - value = string(message) - break - } - } - - search = parseSearch(value) - scope := strings.TrimPrefix(r.URL.Path, "/") - scope = "/" + scope - scope = c.User.Scope + scope - scope = strings.Replace(scope, "\\", "/", -1) - scope = filepath.Clean(scope) - - err = filepath.Walk(scope, func(path string, f os.FileInfo, err error) error { - if search.CaseInsensitive { - path = strings.ToLower(path) - } - - path = strings.TrimPrefix(path, scope) - path = strings.TrimPrefix(path, "/") - path = strings.Replace(path, "\\", "/", -1) - - // Only execute if there are conditions to meet. - if len(search.Conditions) > 0 { - match := false - - for _, t := range search.Conditions { - if t(path) { - match = true - break - } - } - - // If doesn't meet the condition, go to the next. - if !match { - return nil - } - } - - if len(search.Terms) > 0 { - is := false - - // Checks if matches the terms and if it is allowed. - for _, term := range search.Terms { - if is { - break - } - - if strings.Contains(path, term) { - if !c.User.Allowed(path) { - return nil - } - - is = true - } - } - - if !is { - return nil - } - } - - response, _ := json.Marshal(map[string]interface{}{ - "dir": f.IsDir(), - "path": path, - }) - - return conn.WriteMessage(websocket.TextMessage, response) - }) - - if err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil -} +package http + +import ( + "bytes" + "encoding/json" + "mime" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/gorilla/websocket" + fm "github.com/hacdias/filemanager" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +var ( + cmdNotImplemented = []byte("Command not implemented.") + cmdNotAllowed = []byte("Command not allowed.") +) + +// command handles the requests for VCS related commands: git, svn and mercurial +func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + // Upgrades the connection to a websocket and checks for fm.Errors. + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return 0, err + } + defer conn.Close() + + var ( + message []byte + command []string + ) + + // Starts an infinite loop until a valid command is captured. + for { + _, message, err = conn.ReadMessage() + if err != nil { + return http.StatusInternalServerError, err + } + + command = strings.Split(string(message), " ") + if len(command) != 0 { + break + } + } + + // Check if the command is allowed + allowed := false + + for _, cmd := range c.User.Commands { + if cmd == command[0] { + allowed = true + } + } + + if !allowed { + err = conn.WriteMessage(websocket.BinaryMessage, cmdNotAllowed) + if err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil + } + + // Check if the program is talled is installed on the computer. + if _, err = exec.LookPath(command[0]); err != nil { + err = conn.WriteMessage(websocket.BinaryMessage, cmdNotImplemented) + if err != nil { + return http.StatusInternalServerError, err + } + + return http.StatusNotImplemented, nil + } + + // Gets the path and initializes a buffer. + path := c.User.Scope + "/" + r.URL.Path + path = filepath.Clean(path) + buff := new(bytes.Buffer) + + // Sets up the command executation. + cmd := exec.Command(command[0], command[1:]...) + cmd.Dir = path + cmd.Stderr = buff + cmd.Stdout = buff + + // Starts the command and checks for fm.Errors. + err = cmd.Start() + if err != nil { + return http.StatusInternalServerError, err + } + + // Set a 'done' variable to check whetever the command has already finished + // running or not. This verification is done using a goroutine that uses the + // method .Wait() from the command. + done := false + go func() { + err = cmd.Wait() + done = true + }() + + // Function to print the current information on the buffer to the connection. + print := func() error { + by := buff.Bytes() + if len(by) > 0 { + err = conn.WriteMessage(websocket.TextMessage, by) + if err != nil { + return err + } + } + + return nil + } + + // While the command hasn't finished running, continue sending the output + // to the client in intervals of 100 milliseconds. + for !done { + if err = print(); err != nil { + return http.StatusInternalServerError, err + } + + time.Sleep(100 * time.Millisecond) + } + + // After the command is done executing, send the output one more time to the + // browser to make sure it gets the latest information. + if err = print(); err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil +} + +var ( + typeRegexp = regexp.MustCompile(`type:(\w+)`) +) + +type condition func(path string) bool + +type searchOptions struct { + CaseInsensitive bool + Conditions []condition + Terms []string +} + +func extensionCondition(extension string) condition { + return func(path string) bool { + return filepath.Ext(path) == "."+extension + } +} + +func imageCondition(path string) bool { + extension := filepath.Ext(path) + mimetype := mime.TypeByExtension(extension) + + return strings.HasPrefix(mimetype, "image") +} + +func audioCondition(path string) bool { + extension := filepath.Ext(path) + mimetype := mime.TypeByExtension(extension) + + return strings.HasPrefix(mimetype, "audio") +} + +func videoCondition(path string) bool { + extension := filepath.Ext(path) + mimetype := mime.TypeByExtension(extension) + + return strings.HasPrefix(mimetype, "video") +} + +func parseSearch(value string) *searchOptions { + opts := &searchOptions{ + CaseInsensitive: strings.Contains(value, "case:insensitive"), + Conditions: []condition{}, + Terms: []string{}, + } + + // removes the options from the value + value = strings.Replace(value, "case:insensitive", "", -1) + value = strings.Replace(value, "case:sensitive", "", -1) + value = strings.TrimSpace(value) + + types := typeRegexp.FindAllStringSubmatch(value, -1) + for _, t := range types { + if len(t) == 1 { + continue + } + + switch t[1] { + case "image": + opts.Conditions = append(opts.Conditions, imageCondition) + case "audio", "music": + opts.Conditions = append(opts.Conditions, audioCondition) + case "video": + opts.Conditions = append(opts.Conditions, videoCondition) + default: + opts.Conditions = append(opts.Conditions, extensionCondition(t[1])) + } + } + + if len(types) > 0 { + // Remove the fields from the search value. + value = typeRegexp.ReplaceAllString(value, "") + } + + // If it's canse insensitive, put everything in lowercase. + if opts.CaseInsensitive { + value = strings.ToLower(value) + } + + // Remove the spaces from the search value. + value = strings.TrimSpace(value) + + if value == "" { + return opts + } + + // if the value starts with " and finishes what that character, we will + // only search for that term + if value[0] == '"' && value[len(value)-1] == '"' { + unique := strings.TrimPrefix(value, "\"") + unique = strings.TrimSuffix(unique, "\"") + + opts.Terms = []string{unique} + return opts + } + + opts.Terms = strings.Split(value, " ") + return opts +} + +// search searches for a file or directory. +func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + // Upgrades the connection to a websocket and checks for fm.Errors. + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return 0, err + } + defer conn.Close() + + var ( + value string + search *searchOptions + message []byte + ) + + // Starts an infinite loop until a valid command is captured. + for { + _, message, err = conn.ReadMessage() + if err != nil { + return http.StatusInternalServerError, err + } + + if len(message) != 0 { + value = string(message) + break + } + } + + search = parseSearch(value) + scope := strings.TrimPrefix(r.URL.Path, "/") + scope = "/" + scope + scope = c.User.Scope + scope + scope = strings.Replace(scope, "\\", "/", -1) + scope = filepath.Clean(scope) + + err = filepath.Walk(scope, func(path string, f os.FileInfo, err error) error { + if search.CaseInsensitive { + path = strings.ToLower(path) + } + + path = strings.TrimPrefix(path, scope) + path = strings.TrimPrefix(path, "/") + path = strings.Replace(path, "\\", "/", -1) + + // Only execute if there are conditions to meet. + if len(search.Conditions) > 0 { + match := false + + for _, t := range search.Conditions { + if t(path) { + match = true + break + } + } + + // If doesn't meet the condition, go to the next. + if !match { + return nil + } + } + + if len(search.Terms) > 0 { + is := false + + // Checks if matches the terms and if it is allowed. + for _, term := range search.Terms { + if is { + break + } + + if strings.Contains(path, term) { + if !c.User.Allowed(path) { + return nil + } + + is = true + } + } + + if !is { + return nil + } + } + + response, _ := json.Marshal(map[string]interface{}{ + "dir": f.IsDir(), + "path": path, + }) + + return conn.WriteMessage(websocket.TextMessage, response) + }) + + if err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil +} diff --git a/staticgen/hugo.go b/staticgen/hugo.go index bde72754..4a12ea77 100644 --- a/staticgen/hugo.go +++ b/staticgen/hugo.go @@ -1,194 +1,194 @@ -package staticgen - -import ( - "errors" - "io/ioutil" - "log" - "net/http" - "os" - "os/exec" - "path/filepath" - "strings" - - fm "github.com/hacdias/filemanager" - "github.com/hacdias/varutils" -) - -var ( - errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action") -) - -// Hugo is the Hugo static website generator. -type Hugo struct { - // Website root - Root string `name:"Website Root"` - // Public folder - Public string `name:"Public Directory"` - // Hugo executable path - Exe string `name:"Hugo Executable"` - // Hugo arguments - Args []string `name:"Hugo 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 (h Hugo) SettingsPath() string { - var frontmatter string - var err error - - if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil { - frontmatter = "yaml" - } - - if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil { - frontmatter = "json" - } - - if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil { - frontmatter = "toml" - } - - if frontmatter == "" { - return "/settings" - } - - 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 *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 { - return 0, nil - } - - if c.Router != "resource" { - return 0, nil - } - - // We only care about creating new files from archetypes here. So... - if r.Header.Get("Archetype") == "" { - return 0, nil - } - - if !c.User.AllowNew { - return http.StatusForbidden, nil - } - - filename := filepath.Join(c.User.Scope, r.URL.Path) - archetype := r.Header.Get("archetype") - - ext := filepath.Ext(filename) - - // If the request isn't for a markdown file, we can't - // handle it. - if ext != ".markdown" && ext != ".md" { - return http.StatusBadRequest, errUnsupportedFileType - } - - // Tries to create a new file based on this archetype. - args := []string{"new", filename, "--kind", archetype} - if err := runCommand(h.Exe, args, h.Root); err != nil { - return http.StatusInternalServerError, err - } - - // Writes the location of the new file to the Header. - w.Header().Set("Location", "/files/content/"+filename) - return http.StatusCreated, nil -} - -// Publish publishes a post. -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") { - if err := h.undraft(filename); err != nil { - return http.StatusInternalServerError, err - } - } - - // Regenerates the file - h.run(false) - - return 0, nil -} - -// Preview handles the preview path. -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("", "") - if err != nil { - return http.StatusInternalServerError, err - } - - h.previewPath = path - } - - // Build the arguments to execute Hugo: change the base URL, - // build the drafts and update the destination. - args := h.Args - args = append(args, "--baseURL", c.RootURL()+"/preview/") - args = append(args, "--buildDrafts") - args = append(args, "--destination", h.previewPath) - - // Builds the preview. - if err := runCommand(h.Exe, args, h.Root); err != nil { - return http.StatusInternalServerError, err - } - - // Serves the temporary path with the preview. - http.FileServer(http.Dir(h.previewPath)).ServeHTTP(w, r) - return 0, nil -} - -func (h Hugo) run(force bool) { - // If the CleanPublic option is enabled, clean it. - if h.CleanPublic { - os.RemoveAll(h.Public) - } - - // Prevent running if watching is enabled - if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force { - if len(h.Args) > pos && h.Args[pos+1] != "false" { - return - } - - if len(h.Args) == pos+1 { - return - } - } - - if err := runCommand(h.Exe, h.Args, h.Root); err != nil { - log.Println(err) - } -} - -func (h Hugo) undraft(file string) error { - args := []string{"undraft", file} - if err := runCommand(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") { - return err - } - - return nil -} - -// Setup sets up the plugin. -func (h *Hugo) Setup() error { - var err error - if h.Exe, err = exec.LookPath("hugo"); err != nil { - return err - } - - return nil -} +package staticgen + +import ( + "errors" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + + fm "github.com/hacdias/filemanager" + "github.com/hacdias/varutils" +) + +var ( + errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action") +) + +// Hugo is the Hugo static website generator. +type Hugo struct { + // Website root + Root string `name:"Website Root"` + // Public folder + Public string `name:"Public Directory"` + // Hugo executable path + Exe string `name:"Hugo Executable"` + // Hugo arguments + Args []string `name:"Hugo 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 (h Hugo) SettingsPath() string { + var frontmatter string + var err error + + if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil { + frontmatter = "yaml" + } + + if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil { + frontmatter = "json" + } + + if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil { + frontmatter = "toml" + } + + if frontmatter == "" { + return "/settings" + } + + 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 *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 { + return 0, nil + } + + if c.Router != "resource" { + return 0, nil + } + + // We only care about creating new files from archetypes here. So... + if r.Header.Get("Archetype") == "" { + return 0, nil + } + + if !c.User.AllowNew { + return http.StatusForbidden, nil + } + + filename := filepath.Join(c.User.Scope, r.URL.Path) + archetype := r.Header.Get("archetype") + + ext := filepath.Ext(filename) + + // If the request isn't for a markdown file, we can't + // handle it. + if ext != ".markdown" && ext != ".md" { + return http.StatusBadRequest, errUnsupportedFileType + } + + // Tries to create a new file based on this archetype. + args := []string{"new", filename, "--kind", archetype} + if err := runCommand(h.Exe, args, h.Root); err != nil { + return http.StatusInternalServerError, err + } + + // Writes the location of the new file to the Header. + w.Header().Set("Location", "/files/content/"+filename) + return http.StatusCreated, nil +} + +// Publish publishes a post. +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") { + if err := h.undraft(filename); err != nil { + return http.StatusInternalServerError, err + } + } + + // Regenerates the file + h.run(false) + + return 0, nil +} + +// Preview handles the preview path. +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("", "") + if err != nil { + return http.StatusInternalServerError, err + } + + h.previewPath = path + } + + // Build the arguments to execute Hugo: change the base URL, + // build the drafts and update the destination. + args := h.Args + args = append(args, "--baseURL", c.RootURL()+"/preview/") + args = append(args, "--buildDrafts") + args = append(args, "--destination", h.previewPath) + + // Builds the preview. + if err := runCommand(h.Exe, args, h.Root); err != nil { + return http.StatusInternalServerError, err + } + + // Serves the temporary path with the preview. + http.FileServer(http.Dir(h.previewPath)).ServeHTTP(w, r) + return 0, nil +} + +func (h Hugo) run(force bool) { + // If the CleanPublic option is enabled, clean it. + if h.CleanPublic { + os.RemoveAll(h.Public) + } + + // Prevent running if watching is enabled + if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force { + if len(h.Args) > pos && h.Args[pos+1] != "false" { + return + } + + if len(h.Args) == pos+1 { + return + } + } + + if err := runCommand(h.Exe, h.Args, h.Root); err != nil { + log.Println(err) + } +} + +func (h Hugo) undraft(file string) error { + args := []string{"undraft", file} + if err := runCommand(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") { + return err + } + + return nil +} + +// Setup sets up the plugin. +func (h *Hugo) Setup() error { + var err error + if h.Exe, err = exec.LookPath("hugo"); err != nil { + return err + } + + return nil +} diff --git a/staticgen/jekyll.go b/staticgen/jekyll.go index 308ac1dd..fea4dd1a 100644 --- a/staticgen/jekyll.go +++ b/staticgen/jekyll.go @@ -1,125 +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 -} +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 +} diff --git a/staticgen/staticgen.go b/staticgen/staticgen.go index 64f3d694..ef862223 100644 --- a/staticgen/staticgen.go +++ b/staticgen/staticgen.go @@ -1,19 +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 -} +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 +}