diff --git a/caddy/hugo/hugo.go b/caddy/hugo/hugo.go index ba2a5b94..df9e0742 100644 --- a/caddy/hugo/hugo.go +++ b/caddy/hugo/hugo.go @@ -1,7 +1,6 @@ package hugo import ( - "errors" "log" "net/http" "os" @@ -17,21 +16,18 @@ import ( type hugo struct { // Website root - Root string + Root string `description:"The relative or absolute path to the place where your website is located."` // Public folder - Public string + Public string `description:"The relative or absolute path to the public folder."` // Hugo executable path - Exe string + Exe string `description:"The absolute path to the Hugo executable or the command to execute."` // Hugo arguments - Args []string + Args []string `description:"The arguments to run when running Hugo"` // Indicates if we should clean public before a new publish. - CleanPublic bool - // A map of events to a slice of commands. - Commands map[string][]string + CleanPublic bool `description:"Indicates if the public folder should be cleaned before publishing the website."` - // AllowPublish - - javascript string + // TODO: AllowPublish + // TODO: admin interface to cgange options } func (h hugo) BeforeAPI(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { @@ -63,45 +59,60 @@ func (h hugo) BeforeAPI(c *filemanager.RequestContext, w http.ResponseWriter, r return 0, nil } + // If we are not using HTTP Post, we shall return Method Not Allowed + // since we are only working with this method. if r.Method != http.MethodPost { return http.StatusMethodNotAllowed, nil } + // If we are creating a file built from an archetype. if r.Header.Get("Archetype") != "" { filename := filepath.Join(string(c.User.FileSystem), r.URL.Path) - filename = filepath.Clean(filename) filename = strings.TrimPrefix(filename, "/") archetype := r.Header.Get("archetype") - if !strings.HasSuffix(filename, ".md") && !strings.HasSuffix(filename, ".markdown") { - return http.StatusBadRequest, errors.New("Your file must be markdown") + ext := filepath.Ext(filename) + + // If the request isn't for a markdown file, we can't + // handle it. + if ext != ".markdown" && ext != ".md" { + return http.StatusBadRequest, errUnsupportedFileType } + // Tries to create a new file based on this archetype. args := []string{"new", filename, "--kind", archetype} - if err := Run(h.Exe, args, h.Root); err != nil { return http.StatusInternalServerError, err } + // Writes the location of the new file to the Header. w.Header().Set("Location", "/files/content/"+filename) return http.StatusCreated, nil } + // If we are trying to regenerate the website. if r.Header.Get("Regenerate") == "true" { + filename := filepath.Join(string(c.User.FileSystem), r.URL.Path) + filename = strings.TrimPrefix(filename, "/") + // Before save command handler. - path := filepath.Clean(filepath.Join(string(c.User.FileSystem), r.URL.Path)) - if err := c.FM.Runner("before_publish", path); err != nil { + if err := c.FM.Runner("before_publish", filename); err != nil { return http.StatusInternalServerError, err } - args := []string{"undraft", path} - if err := Run(h.Exe, args, h.Root); err != nil { - return http.StatusInternalServerError, err + // We only run undraft command if it is a file. + if !strings.HasSuffix(filename, "/") { + args := []string{"undraft", filename} + if err := Run(h.Exe, args, h.Root); err != nil { + return http.StatusInternalServerError, err + } } + // Regenerates the file h.run(false) - if err := c.FM.Runner("before_publish", path); err != nil { + // Executed the before publish command. + if err := c.FM.Runner("before_publish", filename); err != nil { return http.StatusInternalServerError, err } diff --git a/caddy/hugo/hugo.js b/caddy/hugo/hugo.js index 780119d9..e25096e6 100644 --- a/caddy/hugo/hugo.js +++ b/caddy/hugo/hugo.js @@ -27,14 +27,36 @@ }) } - let newArchetype = function (data, file, type) { + let newArchetype = function (data, url, type) { + url = data.api.removePrefix(url) + + return new Promise((resolve, reject) => { + let request = new window.XMLHttpRequest() + request.open('POST', `${data.store.state.baseURL}/api/hugo${url}`, true) + request.setRequestHeader('Authorization', `Bearer ${data.store.state.jwt}`) + request.setRequestHeader('Archetype', encodeURIComponent(type)) + + request.onload = () => { + if (request.status === 200) { + resolve(request.getResponseHeader('Location')) + } else { + reject(request.responseText) + } + } + + request.onerror = (error) => reject(error) + request.send() + }) + } + + let schedule = function (data, file, date) { file = data.api.removePrefix(file) return new Promise((resolve, reject) => { let request = new window.XMLHttpRequest() request.open('POST', `${data.store.state.baseURL}/api/hugo${file}`, true) request.setRequestHeader('Authorization', `Bearer ${data.store.state.jwt}`) - request.setRequestHeader('Archetype', encodeURIComponent(type)) + request.setRequestHeader('Schedule', date) request.onload = () => { if (request.status === 200) { @@ -93,7 +115,8 @@ data.store.state.req.metadata !== null) }, click: function (event, data, route) { - console.log('Schedule') + document.getElementById('save-button').click() + data.store.commit('showHover', 'schedule') }, id: 'schedule-button', icon: 'alarm', @@ -145,8 +168,6 @@ submit: function (event, data, route) { event.preventDefault() - console.log(event) - let file = event.currentTarget.querySelector('[name="file"]').value let type = event.currentTarget.querySelector('[name="archetype"]').value if (type === '') type = 'default' @@ -161,6 +182,40 @@ data.store.commit('showError', error) }) } + }, + { + name: 'schedule', + title: 'Schedule', + description: 'Pick a date and time to schedule the publication of this post.', + inputs: [ + { + type: 'datetime-local', + name: 'date', + placeholder: 'Date' + } + ], + ok: 'Schedule', + submit: function (event, data, route) { + event.preventDefault() + data.buttons.loading('schedule') + + let date = event.currentTarget.querySelector('[name="date"]').value + if (date === '') { + data.buttons.done('schedule') + data.store.commit('showError', 'The date must not be empty.') + return + } + + schedule(data, route.path, date) + .then(() => { + data.buttons.done('schedule') + data.store.commit('setReload', true) + }) + .catch((error) => { + data.buttons.done('schedule') + data.store.commit('showError', error) + }) + } } ] }) diff --git a/caddy/hugo/setup.go b/caddy/hugo/setup.go index 55b79067..4c94cb6a 100644 --- a/caddy/hugo/setup.go +++ b/caddy/hugo/setup.go @@ -18,7 +18,8 @@ import ( ) var ( - errHugoNotFound = errors.New("It seems that tou don't have 'hugo' on your PATH") + errHugoNotFound = errors.New("It seems that tou don't have 'hugo' on your PATH") + errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action") ) // setup configures a new FileManager middleware instance. @@ -125,10 +126,6 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) { Public: filepath.Join(directory, "public"), Args: []string{}, CleanPublic: true, - Commands: map[string][]string{ - "before_publish": []string{}, - "after_publish": []string{}, - }, } // Try to find the Hugo executable path. @@ -141,6 +138,16 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) { return nil, err } + err = m.RegisterEventType("before_publish") + if err != nil { + return nil, err + } + + err = m.RegisterEventType("after_publish") + if err != nil { + return nil, err + } + m.SetBaseURL(admin) m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/")) configs = append(configs, m) diff --git a/caddy/hugo/utils.go b/caddy/hugo/utils.go index 5d3a88e6..a8bb900b 100644 --- a/caddy/hugo/utils.go +++ b/caddy/hugo/utils.go @@ -1,7 +1,7 @@ package hugo import ( - "os" + "errors" "os/exec" ) @@ -9,7 +9,11 @@ import ( func Run(command string, args []string, path string) error { cmd := exec.Command(command, args...) cmd.Dir = path - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() + out, err := cmd.CombinedOutput() + + if err != nil { + return errors.New(string(out)) + } + + return nil } diff --git a/filemanager.go b/filemanager.go index bbaec489..627279e7 100644 --- a/filemanager.go +++ b/filemanager.go @@ -114,6 +114,7 @@ type Regexp struct { type Plugin interface { // The JavaScript that will be injected into the main page. JavaScript() string + // If the Plugin returns (0, nil), the executation of File Manager will procced as usual. // Otherwise it will stop. BeforeAPI(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) @@ -264,6 +265,17 @@ func (m *FileManager) RegisterPlugin(name string, plugin Plugin) error { return nil } +// RegisterEventType registers a new event type which can be triggered using Runner +// function. +func (m *FileManager) RegisterEventType(name string) error { + if _, ok := m.Commands[name]; ok { + return nil + } + + m.Commands[name] = []string{} + return m.db.Set("config", "commands", m.Commands) +} + // ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { // TODO: Handle errors here and make it compatible with http.Handler @@ -322,7 +334,15 @@ func (r *Regexp) MatchString(s string) bool { // Runner runs the commands for a certain event type. func (m FileManager) Runner(event string, path string) error { - for _, command := range m.Commands[event] { + commands := []string{} + + // Get the commands from the File Manager instance itself. + if val, ok := m.Commands[event]; ok { + commands = append(commands, val...) + } + + // Execute the commands. + for _, command := range commands { args := strings.Split(command, " ") nonblock := false