Former-commit-id: bbc11e551ed79fc41af61d67bfb9a882dfc8e655 [formerly 4ef42ee3251bdf613b7e24b50692629fbf8965ed] [formerly 10abcd7f0e34753069a80a683df94084f03fdb4b [formerly 1d4cbd622c783689457d3eaa854b0d33d97999b8]]
Former-commit-id: 7ac0c18ad61cd491f22b906e1efd87892d729cb2 [formerly b45c7e312058490ae90c8162e6cc1264613a9126]
Former-commit-id: 1c7e8ebd2f1b1dc5bd25df9f30a77c9b73ccad4d
This commit is contained in:
Henrique Dias 2017-10-30 15:47:59 +00:00
parent b96a4a63c0
commit 3bb3fdb956
16 changed files with 2300 additions and 2300 deletions

View File

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

View File

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

View File

@ -1,55 +1,55 @@
// Package filemanager provides middleware for managing files in a directory // Package filemanager provides middleware for managing files in a directory
// when directory path is requested instead of a specific file. Based on browse // when directory path is requested instead of a specific file. Based on browse
// middleware. // middleware.
package filemanager package filemanager
import ( import (
"net/http" "net/http"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http" h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
func init() { func init() {
caddy.RegisterPlugin("filemanager", caddy.Plugin{ caddy.RegisterPlugin("filemanager", caddy.Plugin{
ServerType: "http", ServerType: "http",
Action: setup, Action: setup,
}) })
} }
type plugin struct { type plugin struct {
Next httpserver.Handler Next httpserver.Handler
Configs []*filemanager.FileManager Configs []*filemanager.FileManager
} }
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. // 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) { func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range f.Configs { for i := range f.Configs {
// Checks if this Path should be handled by File Manager. // Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
continue continue
} }
h.Handler(f.Configs[i]).ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
return f.Next.ServeHTTP(w, r) return f.Next.ServeHTTP(w, r)
} }
// setup configures a new FileManager middleware instance. // setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
configs, err := parser.Parse(c, "") configs, err := parser.Parse(c, "")
if err != nil { if err != nil {
return err return err
} }
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next} return plugin{Configs: configs, Next: next}
}) })
return nil return nil
} }

View File

@ -1,52 +1,52 @@
package hugo package hugo
import ( import (
"net/http" "net/http"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http" h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
func init() { func init() {
caddy.RegisterPlugin("hugo", caddy.Plugin{ caddy.RegisterPlugin("hugo", caddy.Plugin{
ServerType: "http", ServerType: "http",
Action: setup, Action: setup,
}) })
} }
type plugin struct { type plugin struct {
Next httpserver.Handler Next httpserver.Handler
Configs []*filemanager.FileManager Configs []*filemanager.FileManager
} }
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. // 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) { func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range f.Configs { for i := range f.Configs {
// Checks if this Path should be handled by File Manager. // Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
continue continue
} }
h.Handler(f.Configs[i]).ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
return f.Next.ServeHTTP(w, r) return f.Next.ServeHTTP(w, r)
} }
// setup configures a new FileManager middleware instance. // setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
configs, err := parser.Parse(c, "hugo") configs, err := parser.Parse(c, "hugo")
if err != nil { if err != nil {
return err return err
} }
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next} return plugin{Configs: configs, Next: next}
}) })
return nil return nil
} }

View File

@ -1,52 +1,52 @@
package jekyll package jekyll
import ( import (
"net/http" "net/http"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http" h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
func init() { func init() {
caddy.RegisterPlugin("jekyll", caddy.Plugin{ caddy.RegisterPlugin("jekyll", caddy.Plugin{
ServerType: "http", ServerType: "http",
Action: setup, Action: setup,
}) })
} }
type plugin struct { type plugin struct {
Next httpserver.Handler Next httpserver.Handler
Configs []*filemanager.FileManager Configs []*filemanager.FileManager
} }
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. // 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) { func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range f.Configs { for i := range f.Configs {
// Checks if this Path should be handled by File Manager. // Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
continue continue
} }
h.Handler(f.Configs[i]).ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
return f.Next.ServeHTTP(w, r) return f.Next.ServeHTTP(w, r)
} }
// setup configures a new FileManager middleware instance. // setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
configs, err := parser.Parse(c, "jekyll") configs, err := parser.Parse(c, "jekyll")
if err != nil { if err != nil {
return err return err
} }
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next} return plugin{Configs: configs, Next: next}
}) })
return nil return nil
} }

View File

@ -1,294 +1,294 @@
package parser package parser
import ( import (
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"github.com/asdine/storm" "github.com/asdine/storm"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt" "github.com/hacdias/filemanager/bolt"
"github.com/hacdias/filemanager/staticgen" "github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
var databases = map[string]*storm.DB{} var databases = map[string]*storm.DB{}
// Parse ... // Parse ...
func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) { func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) {
var ( var (
configs []*filemanager.FileManager configs []*filemanager.FileManager
err error err error
) )
for c.Next() { for c.Next() {
u := &filemanager.User{ u := &filemanager.User{
Locale: "en", Locale: "en",
AllowCommands: true, AllowCommands: true,
AllowEdit: true, AllowEdit: true,
AllowNew: true, AllowNew: true,
AllowPublish: true, AllowPublish: true,
Commands: []string{"git", "svn", "hg"}, Commands: []string{"git", "svn", "hg"},
CSS: "", CSS: "",
Rules: []*filemanager.Rule{{ Rules: []*filemanager.Rule{{
Regex: true, Regex: true,
Allow: false, Allow: false,
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"}, Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
}}, }},
} }
baseURL := "/" baseURL := "/"
scope := "." scope := "."
database := "" database := ""
noAuth := false noAuth := false
reCaptchaKey := "" reCaptchaKey := ""
reCaptchaSecret := "" reCaptchaSecret := ""
if plugin != "" { if plugin != "" {
baseURL = "/admin" baseURL = "/admin"
} }
// Get the baseURL and scope // Get the baseURL and scope
args := c.RemainingArgs() args := c.RemainingArgs()
if plugin == "" { if plugin == "" {
if len(args) >= 1 { if len(args) >= 1 {
baseURL = args[0] baseURL = args[0]
} }
if len(args) > 1 { if len(args) > 1 {
scope = args[1] scope = args[1]
} }
} else { } else {
if len(args) >= 1 { if len(args) >= 1 {
scope = args[0] scope = args[0]
} }
if len(args) > 1 { if len(args) > 1 {
baseURL = args[1] baseURL = args[1]
} }
} }
for c.NextBlock() { for c.NextBlock() {
switch c.Val() { switch c.Val() {
case "database": case "database":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
database = c.Val() database = c.Val()
case "locale": case "locale":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
u.Locale = c.Val() u.Locale = c.Val()
case "allow_commands": case "allow_commands":
if !c.NextArg() { if !c.NextArg() {
u.AllowCommands = true u.AllowCommands = true
continue continue
} }
u.AllowCommands, err = strconv.ParseBool(c.Val()) u.AllowCommands, err = strconv.ParseBool(c.Val())
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "allow_edit": case "allow_edit":
if !c.NextArg() { if !c.NextArg() {
u.AllowEdit = true u.AllowEdit = true
continue continue
} }
u.AllowEdit, err = strconv.ParseBool(c.Val()) u.AllowEdit, err = strconv.ParseBool(c.Val())
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "allow_new": case "allow_new":
if !c.NextArg() { if !c.NextArg() {
u.AllowNew = true u.AllowNew = true
continue continue
} }
u.AllowNew, err = strconv.ParseBool(c.Val()) u.AllowNew, err = strconv.ParseBool(c.Val())
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "allow_publish": case "allow_publish":
if !c.NextArg() { if !c.NextArg() {
u.AllowPublish = true u.AllowPublish = true
continue continue
} }
u.AllowPublish, err = strconv.ParseBool(c.Val()) u.AllowPublish, err = strconv.ParseBool(c.Val())
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "commands": case "commands":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
u.Commands = strings.Split(c.Val(), " ") u.Commands = strings.Split(c.Val(), " ")
case "css": case "css":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
file := c.Val() file := c.Val()
css, err := ioutil.ReadFile(file) css, err := ioutil.ReadFile(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
u.CSS = string(css) u.CSS = string(css)
case "view_mode": case "view_mode":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
u.ViewMode = c.Val() u.ViewMode = c.Val()
if u.ViewMode != filemanager.MosaicViewMode && u.ViewMode != filemanager.ListViewMode { if u.ViewMode != filemanager.MosaicViewMode && u.ViewMode != filemanager.ListViewMode {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
case "recaptcha_key": case "recaptcha_key":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
reCaptchaKey = c.Val() reCaptchaKey = c.Val()
case "recaptcha_secret": case "recaptcha_secret":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
reCaptchaSecret = c.Val() reCaptchaSecret = c.Val()
case "no_auth": case "no_auth":
if !c.NextArg() { if !c.NextArg() {
noAuth = true noAuth = true
continue continue
} }
noAuth, err = strconv.ParseBool(c.Val()) noAuth, err = strconv.ParseBool(c.Val())
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
} }
caddyConf := httpserver.GetConfig(c) caddyConf := httpserver.GetConfig(c)
path := filepath.Join(caddy.AssetsPath(), "filemanager") path := filepath.Join(caddy.AssetsPath(), "filemanager")
err := os.MkdirAll(path, 0700) err := os.MkdirAll(path, 0700)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// if there is a database path and it is not absolute, // if there is a database path and it is not absolute,
// it will be relative to Caddy folder. // it will be relative to Caddy folder.
if !filepath.IsAbs(database) && database != "" { if !filepath.IsAbs(database) && database != "" {
database = filepath.Join(path, database) database = filepath.Join(path, database)
} }
// If there is no database path on the settings, // If there is no database path on the settings,
// store one in .caddy/filemanager/name.db. // store one in .caddy/filemanager/name.db.
if database == "" { if database == "" {
// The name of the database is the hashed value of a string composed // 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 // by the host, address path and the baseurl of this File Manager
// instance. // instance.
hasher := md5.New() hasher := md5.New()
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL)) hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL))
sha := hex.EncodeToString(hasher.Sum(nil)) sha := hex.EncodeToString(hasher.Sum(nil))
database = filepath.Join(path, sha+".db") database = filepath.Join(path, sha+".db")
fmt.Println("[WARNING] A database is going to be created for your File Manager instance at " + database + 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") ". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
} }
u.Scope = scope u.Scope = scope
u.FileSystem = fileutils.Dir(scope) u.FileSystem = fileutils.Dir(scope)
var db *storm.DB var db *storm.DB
if stored, ok := databases[database]; ok { if stored, ok := databases[database]; ok {
db = stored db = stored
} else { } else {
db, err = storm.Open(database) db, err = storm.Open(database)
databases[database] = db databases[database] = db
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
m := &filemanager.FileManager{ m := &filemanager.FileManager{
NoAuth: noAuth, NoAuth: noAuth,
BaseURL: "", BaseURL: "",
PrefixURL: "", PrefixURL: "",
ReCaptchaKey: reCaptchaKey, ReCaptchaKey: reCaptchaKey,
ReCaptchaSecret: reCaptchaSecret, ReCaptchaSecret: reCaptchaSecret,
DefaultUser: u, DefaultUser: u,
Store: &filemanager.Store{ Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db}, Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db}, Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db}, Share: bolt.ShareStore{DB: db},
}, },
NewFS: func(scope string) filemanager.FileSystem { NewFS: func(scope string) filemanager.FileSystem {
return fileutils.Dir(scope) return fileutils.Dir(scope)
}, },
} }
err = m.Setup() err = m.Setup()
if err != nil { if err != nil {
return nil, err return nil, err
} }
switch plugin { switch plugin {
case "hugo": case "hugo":
// Initialize the default settings for Hugo. // Initialize the default settings for Hugo.
hugo := &staticgen.Hugo{ hugo := &staticgen.Hugo{
Root: scope, Root: scope,
Public: filepath.Join(scope, "public"), Public: filepath.Join(scope, "public"),
Args: []string{}, Args: []string{},
CleanPublic: true, CleanPublic: true,
} }
// Attaches Hugo plugin to this file manager instance. // Attaches Hugo plugin to this file manager instance.
err = m.Attach(hugo) err = m.Attach(hugo)
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "jekyll": case "jekyll":
// Initialize the default settings for Jekyll. // Initialize the default settings for Jekyll.
jekyll := &staticgen.Jekyll{ jekyll := &staticgen.Jekyll{
Root: scope, Root: scope,
Public: filepath.Join(scope, "_site"), Public: filepath.Join(scope, "_site"),
Args: []string{}, Args: []string{},
CleanPublic: true, CleanPublic: true,
} }
// Attaches Hugo plugin to this file manager instance. // Attaches Hugo plugin to this file manager instance.
err = m.Attach(jekyll) err = m.Attach(jekyll)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.NoAuth = noAuth m.NoAuth = noAuth
m.SetBaseURL(baseURL) m.SetBaseURL(baseURL)
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/")) m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
configs = append(configs, m) configs = append(configs, m)
} }
return configs, nil return configs, nil
} }

View File

@ -1,249 +1,249 @@
package main package main
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/asdine/storm" "github.com/asdine/storm"
lumberjack "gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt" "github.com/hacdias/filemanager/bolt"
h "github.com/hacdias/filemanager/http" h "github.com/hacdias/filemanager/http"
"github.com/hacdias/filemanager/staticgen" "github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var ( var (
addr string addr string
config string config string
database string database string
scope string scope string
commands string commands string
logfile string logfile string
staticg string staticg string
locale string locale string
baseurl string baseurl string
prefixurl string prefixurl string
viewMode string viewMode string
recaptchakey string recaptchakey string
recaptchasecret string recaptchasecret string
port int port int
noAuth bool noAuth bool
allowCommands bool allowCommands bool
allowEdit bool allowEdit bool
allowNew bool allowNew bool
allowPublish bool allowPublish bool
showVer bool showVer bool
) )
func init() { func init() {
flag.StringVarP(&config, "config", "c", "", "Configuration file") flag.StringVarP(&config, "config", "c", "", "Configuration file")
flag.IntVarP(&port, "port", "p", 0, "HTTP Port (default is random)") 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(&addr, "address", "a", "", "Address to listen to (default is all of them)")
flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file") 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(&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(&scope, "scope", "s", ".", "Default scope option for new users")
flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL") flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL")
flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users") flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL") flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL")
flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users") flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users")
flag.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key") flag.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key")
flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret") flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret")
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users") 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(&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(&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(&allowNew, "allow-new", true, "Default allow new option for new users")
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication") flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
flag.StringVar(&locale, "locale", "", "Default locale for new users, set it empty to enable auto detect from browser") 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.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable")
flag.BoolVarP(&showVer, "version", "v", false, "Show version") flag.BoolVarP(&showVer, "version", "v", false, "Show version")
} }
func setupViper() { func setupViper() {
viper.SetDefault("Address", "") viper.SetDefault("Address", "")
viper.SetDefault("Port", "0") viper.SetDefault("Port", "0")
viper.SetDefault("Database", "./filemanager.db") viper.SetDefault("Database", "./filemanager.db")
viper.SetDefault("Scope", ".") viper.SetDefault("Scope", ".")
viper.SetDefault("Logger", "stdout") viper.SetDefault("Logger", "stdout")
viper.SetDefault("Commands", []string{"git", "svn", "hg"}) viper.SetDefault("Commands", []string{"git", "svn", "hg"})
viper.SetDefault("AllowCommmands", true) viper.SetDefault("AllowCommmands", true)
viper.SetDefault("AllowEdit", true) viper.SetDefault("AllowEdit", true)
viper.SetDefault("AllowNew", true) viper.SetDefault("AllowNew", true)
viper.SetDefault("AllowPublish", true) viper.SetDefault("AllowPublish", true)
viper.SetDefault("StaticGen", "") viper.SetDefault("StaticGen", "")
viper.SetDefault("Locale", "") viper.SetDefault("Locale", "")
viper.SetDefault("NoAuth", false) viper.SetDefault("NoAuth", false)
viper.SetDefault("BaseURL", "") viper.SetDefault("BaseURL", "")
viper.SetDefault("PrefixURL", "") viper.SetDefault("PrefixURL", "")
viper.SetDefault("ViewMode", filemanager.MosaicViewMode) viper.SetDefault("ViewMode", filemanager.MosaicViewMode)
viper.SetDefault("ReCaptchaKey", "") viper.SetDefault("ReCaptchaKey", "")
viper.SetDefault("ReCaptchaSecret", "") viper.SetDefault("ReCaptchaSecret", "")
viper.BindPFlag("Port", flag.Lookup("port")) viper.BindPFlag("Port", flag.Lookup("port"))
viper.BindPFlag("Address", flag.Lookup("address")) viper.BindPFlag("Address", flag.Lookup("address"))
viper.BindPFlag("Database", flag.Lookup("database")) viper.BindPFlag("Database", flag.Lookup("database"))
viper.BindPFlag("Scope", flag.Lookup("scope")) viper.BindPFlag("Scope", flag.Lookup("scope"))
viper.BindPFlag("Logger", flag.Lookup("log")) viper.BindPFlag("Logger", flag.Lookup("log"))
viper.BindPFlag("Commands", flag.Lookup("commands")) viper.BindPFlag("Commands", flag.Lookup("commands"))
viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands")) viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands"))
viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit")) viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit"))
viper.BindPFlag("AlowNew", flag.Lookup("allow-new")) viper.BindPFlag("AlowNew", flag.Lookup("allow-new"))
viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish")) viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish"))
viper.BindPFlag("Locale", flag.Lookup("locale")) viper.BindPFlag("Locale", flag.Lookup("locale"))
viper.BindPFlag("StaticGen", flag.Lookup("staticgen")) viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
viper.BindPFlag("NoAuth", flag.Lookup("no-auth")) viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
viper.BindPFlag("BaseURL", flag.Lookup("baseurl")) viper.BindPFlag("BaseURL", flag.Lookup("baseurl"))
viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl")) viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl"))
viper.BindPFlag("ViewMode", flag.Lookup("view-mode")) viper.BindPFlag("ViewMode", flag.Lookup("view-mode"))
viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key")) viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key"))
viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret")) viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret"))
viper.SetConfigName("filemanager") viper.SetConfigName("filemanager")
viper.AddConfigPath(".") viper.AddConfigPath(".")
} }
func printVersion() { func printVersion() {
fmt.Println("filemanager version", filemanager.Version) fmt.Println("filemanager version", filemanager.Version)
os.Exit(0) os.Exit(0)
} }
func main() { func main() {
setupViper() setupViper()
flag.Parse() flag.Parse()
if showVer { if showVer {
printVersion() printVersion()
} }
// Add a configuration file if set. // Add a configuration file if set.
if config != "" { if config != "" {
ext := filepath.Ext(config) ext := filepath.Ext(config)
dir := filepath.Dir(config) dir := filepath.Dir(config)
config = strings.TrimSuffix(config, ext) config = strings.TrimSuffix(config, ext)
if dir != "" { if dir != "" {
viper.AddConfigPath(dir) viper.AddConfigPath(dir)
config = strings.TrimPrefix(config, dir) config = strings.TrimPrefix(config, dir)
} }
viper.SetConfigName(config) viper.SetConfigName(config)
} }
// Read configuration from a file if exists. // Read configuration from a file if exists.
err := viper.ReadInConfig() err := viper.ReadInConfig()
if err != nil { if err != nil {
if _, ok := err.(viper.ConfigParseError); ok { if _, ok := err.(viper.ConfigParseError); ok {
panic(err) panic(err)
} }
} }
// Set up process log before anything bad happens. // Set up process log before anything bad happens.
switch viper.GetString("Logger") { switch viper.GetString("Logger") {
case "stdout": case "stdout":
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
case "stderr": case "stderr":
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
case "": case "":
log.SetOutput(ioutil.Discard) log.SetOutput(ioutil.Discard)
default: default:
log.SetOutput(&lumberjack.Logger{ log.SetOutput(&lumberjack.Logger{
Filename: logfile, Filename: logfile,
MaxSize: 100, MaxSize: 100,
MaxAge: 14, MaxAge: 14,
MaxBackups: 10, MaxBackups: 10,
}) })
} }
// Builds the address and a listener. // Builds the address and a listener.
laddr := viper.GetString("Address") + ":" + viper.GetString("Port") laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
listener, err := net.Listen("tcp", laddr) listener, err := net.Listen("tcp", laddr)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Tell the user the port in which is listening. // Tell the user the port in which is listening.
fmt.Println("Listening on", listener.Addr().String()) fmt.Println("Listening on", listener.Addr().String())
// Starts the server. // Starts the server.
if err := http.Serve(listener, handler()); err != nil { if err := http.Serve(listener, handler()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func handler() http.Handler { func handler() http.Handler {
db, err := storm.Open(viper.GetString("Database")) db, err := storm.Open(viper.GetString("Database"))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
fm := &filemanager.FileManager{ fm := &filemanager.FileManager{
NoAuth: viper.GetBool("NoAuth"), NoAuth: viper.GetBool("NoAuth"),
BaseURL: viper.GetString("BaseURL"), BaseURL: viper.GetString("BaseURL"),
PrefixURL: viper.GetString("PrefixURL"), PrefixURL: viper.GetString("PrefixURL"),
ReCaptchaKey: viper.GetString("ReCaptchaKey"), ReCaptchaKey: viper.GetString("ReCaptchaKey"),
ReCaptchaSecret: viper.GetString("ReCaptchaSecret"), ReCaptchaSecret: viper.GetString("ReCaptchaSecret"),
DefaultUser: &filemanager.User{ DefaultUser: &filemanager.User{
AllowCommands: viper.GetBool("AllowCommands"), AllowCommands: viper.GetBool("AllowCommands"),
AllowEdit: viper.GetBool("AllowEdit"), AllowEdit: viper.GetBool("AllowEdit"),
AllowNew: viper.GetBool("AllowNew"), AllowNew: viper.GetBool("AllowNew"),
AllowPublish: viper.GetBool("AllowPublish"), AllowPublish: viper.GetBool("AllowPublish"),
Commands: viper.GetStringSlice("Commands"), Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{}, Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"), Locale: viper.GetString("Locale"),
CSS: "", CSS: "",
Scope: viper.GetString("Scope"), Scope: viper.GetString("Scope"),
FileSystem: fileutils.Dir(viper.GetString("Scope")), FileSystem: fileutils.Dir(viper.GetString("Scope")),
ViewMode: viper.GetString("ViewMode"), ViewMode: viper.GetString("ViewMode"),
}, },
Store: &filemanager.Store{ Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db}, Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db}, Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db}, Share: bolt.ShareStore{DB: db},
}, },
NewFS: func(scope string) filemanager.FileSystem { NewFS: func(scope string) filemanager.FileSystem {
return fileutils.Dir(scope) return fileutils.Dir(scope)
}, },
} }
err = fm.Setup() err = fm.Setup()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
switch viper.GetString("StaticGen") { switch viper.GetString("StaticGen") {
case "hugo": case "hugo":
hugo := &staticgen.Hugo{ hugo := &staticgen.Hugo{
Root: viper.GetString("Scope"), Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "public"), Public: filepath.Join(viper.GetString("Scope"), "public"),
Args: []string{}, Args: []string{},
CleanPublic: true, CleanPublic: true,
} }
if err = fm.Attach(hugo); err != nil { if err = fm.Attach(hugo); err != nil {
log.Fatal(err) log.Fatal(err)
} }
case "jekyll": case "jekyll":
jekyll := &staticgen.Jekyll{ jekyll := &staticgen.Jekyll{
Root: viper.GetString("Scope"), Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "_site"), Public: filepath.Join(viper.GetString("Scope"), "_site"),
Args: []string{"build"}, Args: []string{"build"},
CleanPublic: true, CleanPublic: true,
} }
if err = fm.Attach(jekyll); err != nil { if err = fm.Attach(jekyll); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
return h.Handler(fm) return h.Handler(fm)
} }

146
doc.go
View File

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

View File

@ -15,7 +15,7 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
rice "github.com/GeertJohan/go.rice" "github.com/GeertJohan/go.rice"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/robfig/cron" "github.com/robfig/cron"

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"time" "time"
jwt "github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request" "github.com/dgrijalva/jwt-go/request"
fm "github.com/hacdias/filemanager" fm "github.com/hacdias/filemanager"
) )

View File

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

View File

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

View File

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

View File

@ -1,194 +1,194 @@
package staticgen package staticgen
import ( import (
"errors" "errors"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
fm "github.com/hacdias/filemanager" fm "github.com/hacdias/filemanager"
"github.com/hacdias/varutils" "github.com/hacdias/varutils"
) )
var ( var (
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action") errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
) )
// Hugo is the Hugo static website generator. // Hugo is the Hugo static website generator.
type Hugo struct { type Hugo struct {
// Website root // Website root
Root string `name:"Website Root"` Root string `name:"Website Root"`
// Public folder // Public folder
Public string `name:"Public Directory"` Public string `name:"Public Directory"`
// Hugo executable path // Hugo executable path
Exe string `name:"Hugo Executable"` Exe string `name:"Hugo Executable"`
// Hugo arguments // Hugo arguments
Args []string `name:"Hugo Arguments"` Args []string `name:"Hugo Arguments"`
// Indicates if we should clean public before a new publish. // Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"` CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview // previewPath is the temporary path for a preview
previewPath string previewPath string
} }
// SettingsPath retrieves the correct settings path. // SettingsPath retrieves the correct settings path.
func (h Hugo) SettingsPath() string { func (h Hugo) SettingsPath() string {
var frontmatter string var frontmatter string
var err error var err error
if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil { if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil {
frontmatter = "yaml" frontmatter = "yaml"
} }
if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil { if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil {
frontmatter = "json" frontmatter = "json"
} }
if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil { if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil {
frontmatter = "toml" frontmatter = "toml"
} }
if frontmatter == "" { if frontmatter == "" {
return "/settings" return "/settings"
} }
return "/config." + frontmatter return "/config." + frontmatter
} }
// Name is the plugin's name. // Name is the plugin's name.
func (h Hugo) Name() string { func (h Hugo) Name() string {
return "hugo" return "hugo"
} }
// Hook is the pre-api handler. // Hook is the pre-api handler.
func (h Hugo) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func (h Hugo) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// If we are not using HTTP Post, we shall return Method Not Allowed // If we are not using HTTP Post, we shall return Method Not Allowed
// since we are only working with this method. // since we are only working with this method.
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
return 0, nil return 0, nil
} }
if c.Router != "resource" { if c.Router != "resource" {
return 0, nil return 0, nil
} }
// We only care about creating new files from archetypes here. So... // We only care about creating new files from archetypes here. So...
if r.Header.Get("Archetype") == "" { if r.Header.Get("Archetype") == "" {
return 0, nil return 0, nil
} }
if !c.User.AllowNew { if !c.User.AllowNew {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
filename := filepath.Join(c.User.Scope, r.URL.Path) filename := filepath.Join(c.User.Scope, r.URL.Path)
archetype := r.Header.Get("archetype") archetype := r.Header.Get("archetype")
ext := filepath.Ext(filename) ext := filepath.Ext(filename)
// If the request isn't for a markdown file, we can't // If the request isn't for a markdown file, we can't
// handle it. // handle it.
if ext != ".markdown" && ext != ".md" { if ext != ".markdown" && ext != ".md" {
return http.StatusBadRequest, errUnsupportedFileType return http.StatusBadRequest, errUnsupportedFileType
} }
// Tries to create a new file based on this archetype. // Tries to create a new file based on this archetype.
args := []string{"new", filename, "--kind", archetype} args := []string{"new", filename, "--kind", archetype}
if err := runCommand(h.Exe, args, h.Root); err != nil { if err := runCommand(h.Exe, args, h.Root); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Writes the location of the new file to the Header. // Writes the location of the new file to the Header.
w.Header().Set("Location", "/files/content/"+filename) w.Header().Set("Location", "/files/content/"+filename)
return http.StatusCreated, nil return http.StatusCreated, nil
} }
// Publish publishes a post. // Publish publishes a post.
func (h Hugo) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func (h Hugo) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(c.User.Scope, r.URL.Path) filename := filepath.Join(c.User.Scope, r.URL.Path)
// We only run undraft command if it is a file. // We only run undraft command if it is a file.
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") { if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
if err := h.undraft(filename); err != nil { if err := h.undraft(filename); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
} }
// Regenerates the file // Regenerates the file
h.run(false) h.run(false)
return 0, nil return 0, nil
} }
// Preview handles the preview path. // Preview handles the preview path.
func (h *Hugo) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func (h *Hugo) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none. // Get a new temporary path if there is none.
if h.previewPath == "" { if h.previewPath == "" {
path, err := ioutil.TempDir("", "") path, err := ioutil.TempDir("", "")
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
h.previewPath = path h.previewPath = path
} }
// Build the arguments to execute Hugo: change the base URL, // Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination. // build the drafts and update the destination.
args := h.Args args := h.Args
args = append(args, "--baseURL", c.RootURL()+"/preview/") args = append(args, "--baseURL", c.RootURL()+"/preview/")
args = append(args, "--buildDrafts") args = append(args, "--buildDrafts")
args = append(args, "--destination", h.previewPath) args = append(args, "--destination", h.previewPath)
// Builds the preview. // Builds the preview.
if err := runCommand(h.Exe, args, h.Root); err != nil { if err := runCommand(h.Exe, args, h.Root); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Serves the temporary path with the preview. // Serves the temporary path with the preview.
http.FileServer(http.Dir(h.previewPath)).ServeHTTP(w, r) http.FileServer(http.Dir(h.previewPath)).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
func (h Hugo) run(force bool) { func (h Hugo) run(force bool) {
// If the CleanPublic option is enabled, clean it. // If the CleanPublic option is enabled, clean it.
if h.CleanPublic { if h.CleanPublic {
os.RemoveAll(h.Public) os.RemoveAll(h.Public)
} }
// Prevent running if watching is enabled // Prevent running if watching is enabled
if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force { if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force {
if len(h.Args) > pos && h.Args[pos+1] != "false" { if len(h.Args) > pos && h.Args[pos+1] != "false" {
return return
} }
if len(h.Args) == pos+1 { if len(h.Args) == pos+1 {
return return
} }
} }
if err := runCommand(h.Exe, h.Args, h.Root); err != nil { if err := runCommand(h.Exe, h.Args, h.Root); err != nil {
log.Println(err) log.Println(err)
} }
} }
func (h Hugo) undraft(file string) error { func (h Hugo) undraft(file string) error {
args := []string{"undraft", file} args := []string{"undraft", file}
if err := runCommand(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") { if err := runCommand(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") {
return err return err
} }
return nil return nil
} }
// Setup sets up the plugin. // Setup sets up the plugin.
func (h *Hugo) Setup() error { func (h *Hugo) Setup() error {
var err error var err error
if h.Exe, err = exec.LookPath("hugo"); err != nil { if h.Exe, err = exec.LookPath("hugo"); err != nil {
return err return err
} }
return nil return nil
} }

View File

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

View File

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