From 2462346e5682991367963400be81574b7a47c2ad Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Thu, 10 Aug 2017 12:18:51 +0100 Subject: [PATCH] Jekyll; progresses on #108 Former-commit-id: 05caf8c6dd2df09d064a14c034e221151a2dd341 [formerly df5cbcf51aa8a2e76e39d73f30f4e63f222bd70f] [formerly 3fbf8281b93fed6c16cd7c8f810dccdf1f7347d6 [formerly fe80e98f85c47bbd6c6b545e216f1210ebd300e7]] Former-commit-id: b1d034b6c7ee1ce445bd53039ce0306f2b2b7651 [formerly 7b3c5571c7647ae4d4f25f46e66df49aa0e3320c] Former-commit-id: 0f57d274deaeae52b9da52d8f6495070caba6ee7 --- assa | 0 caddy/hugo/hugo.go | 1 + caddy/jekyll/jekyll.go | 177 +++++++++++++++++++++++++++++++++++++ cmd/filemanager/main.go | 20 ++++- filemanager.go | 39 ++++++-- rice-box.go.REMOVED.git-id | 2 +- staticgen.go | 136 +++++++++++++++++++++++++++- users.go | 1 + 8 files changed, 364 insertions(+), 12 deletions(-) create mode 100644 assa create mode 100644 caddy/jekyll/jekyll.go diff --git a/assa b/assa new file mode 100644 index 00000000..e69de29b diff --git a/caddy/hugo/hugo.go b/caddy/hugo/hugo.go index dad91fca..3453861a 100644 --- a/caddy/hugo/hugo.go +++ b/caddy/hugo/hugo.go @@ -111,6 +111,7 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) { AllowCommands: true, AllowEdit: true, AllowNew: true, + AllowPublish: true, Commands: []string{"git", "svn", "hg"}, Rules: []*filemanager.Rule{{ Regex: true, diff --git a/caddy/jekyll/jekyll.go b/caddy/jekyll/jekyll.go new file mode 100644 index 00000000..a9fcd163 --- /dev/null +++ b/caddy/jekyll/jekyll.go @@ -0,0 +1,177 @@ +package jekyll + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/hacdias/filemanager" + "github.com/hacdias/fileutils" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +// setup configures a new FileManager middleware instance. +func setup(c *caddy.Controller) error { + configs, err := parse(c) + if err != nil { + return err + } + + httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return plugin{Configs: configs, Next: next} + }) + + return nil +} + +func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) { + var ( + configs []*filemanager.FileManager + ) + + for c.Next() { + // jekyll [directory] [admin] { + // database path + // } + directory := "." + admin := "/admin" + database := "" + noAuth := false + + // Get the baseURL and baseScope + args := c.RemainingArgs() + + if len(args) >= 1 { + directory = args[0] + } + + if len(args) > 1 { + admin = args[1] + } + + for c.NextBlock() { + switch c.Val() { + case "database": + if !c.NextArg() { + return nil, c.ArgErr() + } + + database = c.Val() + case "no_auth": + if !c.NextArg() { + noAuth = true + continue + } + + var err error + noAuth, err = strconv.ParseBool(c.Val()) + if err != nil { + return nil, err + } + } + } + + caddyConf := httpserver.GetConfig(c) + + path := filepath.Join(caddy.AssetsPath(), "jekyll") + err := os.MkdirAll(path, 0700) + if err != nil { + return nil, err + } + + // if there is a database path and it is not absolute, + // it will be relative to ".caddy" folder. + if !filepath.IsAbs(database) && database != "" { + database = filepath.Join(path, database) + } + + // If there is no database path on the settings, + // store one in .caddy/jekyll/{name}.db. + if database == "" { + // The name of the database is the hashed value of a string composed + // by the host, address path and the baseurl of this File Manager + // instance. + hasher := md5.New() + hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + admin)) + sha := hex.EncodeToString(hasher.Sum(nil)) + database = filepath.Join(path, sha+".db") + + fmt.Println("[WARNING] A database is going to be created for your Jekyll instace at " + database + + ". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n") + } + + m, err := filemanager.New(database, filemanager.User{ + Locale: "en", + AllowCommands: true, + AllowEdit: true, + AllowNew: true, + AllowPublish: true, + Commands: []string{"git", "svn", "hg"}, + Rules: []*filemanager.Rule{{ + Regex: true, + Allow: false, + Regexp: &filemanager.Regexp{Raw: "\\/\\..+"}, + }}, + CSS: "", + FileSystem: fileutils.Dir(directory), + }) + + if err != nil { + return nil, err + } + + // Initialize the default settings for Jekyll. + jekyll := &filemanager.Jekyll{ + Root: directory, + Public: filepath.Join(directory, "_site"), + Args: []string{}, + CleanPublic: true, + } + + // Attaches Hugo plugin to this file manager instance. + err = m.EnableStaticGen(jekyll) + if err != nil { + return nil, err + } + + m.NoAuth = noAuth + m.SetBaseURL(admin) + m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/")) + configs = append(configs, m) + } + + return configs, nil +} + +// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. +func (p plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + for i := range p.Configs { + // Checks if this Path should be handled by File Manager. + if !httpserver.Path(r.URL.Path).Matches(p.Configs[i].BaseURL) { + continue + } + + p.Configs[i].ServeHTTP(w, r) + return 0, nil + } + + return p.Next.ServeHTTP(w, r) +} + +func init() { + caddy.RegisterPlugin("jekyll", caddy.Plugin{ + ServerType: "http", + Action: setup, + }) +} + +type plugin struct { + Next httpserver.Handler + Configs []*filemanager.FileManager +} diff --git a/cmd/filemanager/main.go b/cmd/filemanager/main.go index a83f34c4..684fe53c 100644 --- a/cmd/filemanager/main.go +++ b/cmd/filemanager/main.go @@ -32,6 +32,7 @@ var ( allowCommands bool allowEdit bool allowNew bool + allowPublish bool showVer bool version = "master" ) @@ -46,6 +47,7 @@ func init() { flag.StringVar(&commands, "commands", "git svn hg", "Default 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(&allowPublish, "allow-publish", true, "Default allow publish option for new users") flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users") flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication") flag.StringVar(&locale, "locale", "en", "Default locale for new users") @@ -63,6 +65,7 @@ func setupViper() { viper.SetDefault("AllowCommmands", true) viper.SetDefault("AllowEdit", true) viper.SetDefault("AllowNew", true) + viper.SetDefault("AllowPublish", true) viper.SetDefault("StaticGen", "") viper.SetDefault("Locale", "en") viper.SetDefault("NoAuth", false) @@ -76,6 +79,7 @@ func setupViper() { viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands")) viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit")) viper.BindPFlag("AlowNew", flag.Lookup("allow-new")) + viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish")) viper.BindPFlag("Locale", flag.Lookup("locale")) viper.BindPFlag("StaticGen", flag.Lookup("staticgen")) viper.BindPFlag("NoAuth", flag.Lookup("no-auth")) @@ -149,6 +153,7 @@ func main() { AllowCommands: viper.GetBool("AllowCommands"), AllowEdit: viper.GetBool("AllowEdit"), AllowNew: viper.GetBool("AllowNew"), + AllowPublish: viper.GetBool("AllowPublish"), Commands: viper.GetStringSlice("Commands"), Rules: []*filemanager.Rule{}, Locale: viper.GetString("Locale"), @@ -164,8 +169,8 @@ func main() { log.Fatal(err) } - if viper.GetString("StaticGen") == "hugo" { - // Initialize the default settings for Hugo. + switch viper.GetString("StaticGen") { + case "hugo": hugo := &filemanager.Hugo{ Root: viper.GetString("Scope"), Public: filepath.Join(viper.GetString("Scope"), "public"), @@ -176,6 +181,17 @@ func main() { if err = fm.EnableStaticGen(hugo); err != nil { log.Fatal(err) } + case "jekyll": + jekyll := &filemanager.Jekyll{ + Root: viper.GetString("Scope"), + Public: filepath.Join(viper.GetString("Scope"), "_site"), + Args: []string{"build"}, + CleanPublic: true, + } + + if err = fm.EnableStaticGen(jekyll); err != nil { + log.Fatal(err) + } } // Builds the address and a listener. diff --git a/filemanager.go b/filemanager.go index 0e42f3f6..2a729bcb 100644 --- a/filemanager.go +++ b/filemanager.go @@ -289,6 +289,7 @@ func New(database string, base User) (*FileManager, error) { u.AllowCommands = true u.AllowNew = true u.AllowEdit = true + u.AllowPublish = true // Saves the user to the database. if err := db.Save(&u); err != nil { @@ -365,20 +366,48 @@ func (m *FileManager) EnableStaticGen(data StaticGen) error { return m.enableHugo(h) } + if j, ok := data.(*Jekyll); ok { + return m.enableJekyll(j) + } + return errors.New("unknown static website generator") } -func (m *FileManager) enableHugo(hugo *Hugo) error { - if err := hugo.find(); err != nil { +func (m *FileManager) enableHugo(h *Hugo) error { + if err := h.find(); err != nil { return err } m.staticgen = "hugo" - m.StaticGen = hugo + m.StaticGen = h - err := m.db.Get("staticgen", "hugo", hugo) + err := m.db.Get("staticgen", "hugo", h) if err != nil && err == storm.ErrNotFound { - err = m.db.Set("staticgen", "hugo", *hugo) + err = m.db.Set("staticgen", "hugo", *h) + } + + return nil +} + +func (m *FileManager) enableJekyll(j *Jekyll) error { + if err := j.find(); err != nil { + return err + } + + if len(j.Args) == 0 { + j.Args = []string{"build"} + } + + if j.Args[0] != "build" { + j.Args = append([]string{"build"}, j.Args...) + } + + m.staticgen = "jekyll" + m.StaticGen = j + + err := m.db.Get("staticgen", "jekyll", j) + if err != nil && err == storm.ErrNotFound { + err = m.db.Set("staticgen", "jekyll", *j) } return nil diff --git a/rice-box.go.REMOVED.git-id b/rice-box.go.REMOVED.git-id index 54c77b09..849f7292 100644 --- a/rice-box.go.REMOVED.git-id +++ b/rice-box.go.REMOVED.git-id @@ -1 +1 @@ -fb7f000455844aaf5021aa2a43f0fcd05f67fa35 \ No newline at end of file +d12113f698d3e44cb873181f6e7d7c9e17de30ac \ No newline at end of file diff --git a/staticgen.go b/staticgen.go index b11f6f55..5aa0a1bd 100644 --- a/staticgen.go +++ b/staticgen.go @@ -16,8 +16,6 @@ import ( ) var ( - // ErrHugoNotFound ... - ErrHugoNotFound = errors.New("It seems that tou don't have 'hugo' on your PATH") // ErrUnsupportedFileType ... ErrUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action") ) @@ -119,7 +117,6 @@ func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) if err := h.undraft(filename); err != nil { return http.StatusInternalServerError, err } - } // Regenerates the file @@ -219,7 +216,138 @@ func (h Hugo) undraft(file string) error { func (h *Hugo) find() error { var err error if h.Exe, err = exec.LookPath("hugo"); err != nil { - return ErrHugoNotFound + return err + } + + return nil +} + +// Jekyll is the Jekyll static website generator. +type Jekyll struct { + // Website root + Root string `name:"Website Root"` + // Public folder + Public string `name:"Public Directory"` + // Jekyll executable path + Exe string `name:"Executable"` + // Jekyll arguments + Args []string `name:"Arguments"` + // Indicates if we should clean public before a new publish. + CleanPublic bool `name:"Clean Public"` + // previewPath is the temporary path for a preview + previewPath string +} + +// SettingsPath retrieves the correct settings path. +func (j Jekyll) SettingsPath() string { + return "/_config.yml" +} + +// Hook is the pre-api handler. +func (j Jekyll) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { + return 0, nil +} + +// Publish publishes a post. +func (j Jekyll) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { + filename := filepath.Join(string(c.User.FileSystem), r.URL.Path) + + // Before save command handler. + if err := c.Runner("before_publish", filename); err != nil { + return http.StatusInternalServerError, err + } + + // We only run undraft command if it is a file. + if err := j.undraft(filename); err != nil { + return http.StatusInternalServerError, err + } + + // Regenerates the file + j.run() + + // Executed the before publish command. + if err := c.Runner("before_publish", filename); err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil +} + +// Schedule schedules a post. +func (j Jekyll) Schedule(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { + t, err := time.Parse("2006-01-02T15:04", r.Header.Get("Schedule")) + path := filepath.Join(string(c.User.FileSystem), r.URL.Path) + path = filepath.Clean(path) + + if err != nil { + return http.StatusInternalServerError, err + } + + scheduler := cron.New() + scheduler.AddFunc(t.Format("05 04 15 02 01 *"), func() { + if err := j.undraft(path); err != nil { + log.Printf(err.Error()) + } + + j.run() + }) + + scheduler.Start() + return http.StatusOK, nil +} + +// Preview handles the preview path. +func (j *Jekyll) Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { + // Get a new temporary path if there is none. + if j.previewPath == "" { + path, err := ioutil.TempDir("", "") + if err != nil { + return http.StatusInternalServerError, err + } + + j.previewPath = path + } + + // Build the arguments to execute Hugo: change the base URL, + // build the drafts and update the destination. + args := j.Args + args = append(args, "--baseurl", c.RootURL()+"/preview/") + args = append(args, "--drafts") + args = append(args, "--destination", j.previewPath) + + // Builds the preview. + if err := runCommand(j.Exe, args, j.Root); err != nil { + return http.StatusInternalServerError, err + } + + // Serves the temporary path with the preview. + http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r) + return 0, nil +} + +func (j Jekyll) run() { + // If the CleanPublic option is enabled, clean it. + if j.CleanPublic { + os.RemoveAll(j.Public) + } + + if err := runCommand(j.Exe, j.Args, j.Root); err != nil { + log.Println(err) + } +} + +func (j Jekyll) undraft(file string) error { + if !strings.Contains(file, "_drafts") { + return nil + } + + return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1)) +} + +func (j *Jekyll) find() error { + var err error + if j.Exe, err = exec.LookPath("jekyll"); err != nil { + return err } return nil diff --git a/users.go b/users.go index 60a12e5d..152d9e94 100644 --- a/users.go +++ b/users.go @@ -221,6 +221,7 @@ func checkFS(path string) (int, error) { return http.StatusInternalServerError, err } + return 0, nil } if !info.IsDir() {