diff options
Diffstat (limited to 'cmd/ponzu')
-rw-r--r-- | cmd/ponzu/main.go | 119 | ||||
-rw-r--r-- | cmd/ponzu/options.go | 242 |
2 files changed, 361 insertions, 0 deletions
diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go new file mode 100644 index 0000000..5043753 --- /dev/null +++ b/cmd/ponzu/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + + "github.com/bosssauce/ponzu/system/admin" + "github.com/bosssauce/ponzu/system/api" + "github.com/bosssauce/ponzu/system/db" +) + +var usage = ` +$ cms <option> <params> + +Options + +new <directory>: + + Creates a new 'cms' in the current directory, or one supplied + as a parameter immediately following the 'new' option. Note: 'new' + depends on the program 'git' and possibly a network connection. If there is + no local repository to clone from at the local machine's $GOPATH, 'new' will + attempt to clone the 'cms' package from over the network. + + Example: + $ cms new ~/Projects/my-project.dev + + + +generate, gen, g <type>: + + Generate a content type file with boilerplate code to implement + the editor.Editable interface. Must be given one (1) parameter of + the name of the type for the new content. + + Example: + $ cms gen review + + + +serve, s <service> <port> <tls>: + + Starts the 'cms' HTTP server for the JSON API, Admin System, or both. + Must be given at least one (1) parameter. The segments describe + which services to start, either 'admin' (Admin System / CMS + backend) or 'api' (JSON API), and, optionally, if the server(s) should + utilize TLS encryption (served over HTTPS), which is automatically managed + using Let's Encrypt (https://letsencrypt.org) + + Example: + $ cms serve admin|api 8080 tls + (or) + $ cms serve admin + (or) + $ cms serve api 8888 + + Defaults to 'admin|api 8080' (running Admin & API on port 8080, without TLS) + + Note: + Admin and API cannot run on separate processes unless you use a copy of the + database, since the first process to open it recieves a lock. If you intend + to run the Admin and API on separate processes, you must call them with the + 'cms' command independently. +` + +func init() { + flag.Usage = func() { + fmt.Println(usage) + } +} + +func main() { + flag.Parse() + + args := flag.Args() + + if len(args) < 1 { + flag.Usage() + os.Exit(0) + } + + switch args[0] { + case "new": + if len(args) < 2 { + flag.PrintDefaults() + os.Exit(0) + } + + err := newProjectInDir(args[1]) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + case "generate", "gen", "g": + if len(args) < 2 { + flag.PrintDefaults() + os.Exit(0) + } + + err := generateContentType(args[1]) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + case "serve", "s": + db.Init() + admin.Run() + api.Run() + log.Fatal(http.ListenAndServe(":8080", nil)) + case "": + flag.PrintDefaults() + default: + flag.PrintDefaults() + } +} diff --git a/cmd/ponzu/options.go b/cmd/ponzu/options.go new file mode 100644 index 0000000..e893753 --- /dev/null +++ b/cmd/ponzu/options.go @@ -0,0 +1,242 @@ +package main + +import ( + "bytes" + "fmt" + "html/template" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func generateContentType(name string) error { + fileName := strings.ToLower(name) + ".go" + typeName := strings.ToUpper(string(name[0])) + string(name[1:]) + + // contain processed name an info for template + data := map[string]string{ + "name": typeName, + "initial": string(fileName[0]), + } + + // open file in ./content/ dir + // if exists, alert user of conflict + pwd, err := os.Getwd() + if err != nil { + return err + } + + contentDir := filepath.Join(pwd, "content") + filePath := filepath.Join(contentDir, fileName) + + if _, err := os.Stat(filePath); !os.IsNotExist(err) { + return fmt.Errorf("Please remove '%s' before executing this command.", fileName) + } + + // no file exists.. ok to write new one + file, err := os.Create(filePath) + defer file.Close() + if err != nil { + return err + } + + // execute template + tmpl := template.Must(template.New("content").Parse(contentTypeTmpl)) + err = tmpl.Execute(file, data) + if err != nil { + return err + } + + return nil +} + +const contentTypeTmpl = ` +package content + +import ( + "fmt" + + "github.com/bosssauce/ponzu/management/editor" +) + +// {{ .name }} is the generic content struct +type {{ .name }} struct { + Item + editor editor.Editor + + // required: all maintained {{ .name }} fields must have json tags! + Title string ` + "`json:" + `"title"` + "`" + ` + Content string ` + "`json:" + `"content"` + "`" + ` + Author string ` + "`json:" + `"author"` + "`" + ` + Category []string ` + "`json:" + `"category"` + "`" + ` + ThemeStyle string ` + "`json:" + `"theme"` + "`" + ` +} + +func init() { + Types["{{ .name }}"] = func() interface{} { return new({{ .name }}) } +} + +// SetContentID partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) SetContentID(id int) { {{ .initial }}.ID = id } + +// ContentID partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) ContentID() int { return {{ .initial }}.ID } + +// ContentName partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) ContentName() string { return {{ .initial }}.Title } + +// SetSlug partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) SetSlug(slug string) { {{ .initial }}.Slug = slug } + +// Editor partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) Editor() *editor.Editor { return &{{ .initial }}.editor } + +// MarshalEditor writes a buffer of html to edit a {{ .name }} and partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) MarshalEditor() ([]byte, error) { +/* EXAMPLE CODE (from post.go, the default content type) */ + view, err := editor.Form({{ .initial }}, + editor.Field{ + // Take careful note that the first argument to these Input-like methods + // is the string version of each {{ .name }} struct tag, and must follow this pattern + // for auto-decoding and -encoding reasons. + View: editor.Input("Title", {{ .initial }}, map[string]string{ + "label": "{{ .name }} Title", + "type": "text", + "placeholder": "Enter your {{ .name }} Title here", + }), + }, + editor.Field{ + View: editor.Textarea("Content", {{ .initial }}, map[string]string{ + "label": "Content", + "placeholder": "Add the content of your {{ .name }} here", + }), + }, + editor.Field{ + View: editor.Input("Author", {{ .initial }}, map[string]string{ + "label": "Author", + "type": "text", + "placeholder": "Enter the author name here", + }), + }, + editor.Field{ + View: editor.Checkbox("Category", {{ .initial }}, map[string]string{ + "label": "{{ .name }} Category", + }, map[string]string{ + "important": "Important", + "active": "Active", + "unplanned": "Unplanned", + }), + }, + editor.Field{ + View: editor.Select("ThemeStyle", {{ .initial }}, map[string]string{ + "label": "Theme Style", + }, map[string]string{ + "dark": "Dark", + "light": "Light", + }), + }, + ) + + if err != nil { + return nil, fmt.Errorf("Failed to render {{ .name }} editor view: %s", err.Error()) + } + + return view, nil +} +` + +func newProjectInDir(path string) error { + // check if anything exists at the path, ask if it should be overwritten + if _, err := os.Stat(path); !os.IsNotExist(err) { + fmt.Println("Path exists, overwrite contents? (y/N):") + // input := bufio.NewReader(os.Stdin) + // answer, err := input.ReadString('\n') + + var answer string + _, err := fmt.Scanf("%s\n", &answer) + if err != nil { + return err + } + + answer = strings.ToLower(answer) + + switch answer { + case "n", "no", "": + fmt.Println("") + + case "y", "yes": + err := os.RemoveAll(path) + if err != nil { + return fmt.Errorf("Failed to overwrite %s. \n%s", path, err) + } + + return createProjInDir(path) + + default: + fmt.Println("Input not recognized. No files overwritten. Answer as 'y' or 'n' only.") + } + + return nil + } + + return createProjInDir(path) +} + +func createProjInDir(path string) error { + var buf = &bytes.Buffer{} + echo := exec.Command("echo", os.Getenv("GOPATH")) + echo.Stdout = buf + err := echo.Run() + if err != nil { + return err + } + + gopath := buf.String() + gopath = gopath[:len(gopath)-1] + gopath = filepath.Join(gopath, "src") + + repo := []string{"github.com", "nilslice", "cms"} + local := filepath.Join(gopath, filepath.Join(repo...)) + network := "https://" + strings.Join(repo, "/") + ".git" + + // create the directory or overwrite it + err = os.MkdirAll(path, os.ModeDir|os.ModePerm) + if err != nil { + return err + } + + // try to git clone the repository from the local machine's $GOPATH + localClone := exec.Command("git", "clone", local, path) + localClone.Stdout = os.Stdout + localClone.Stderr = os.Stderr + + err = localClone.Start() + if err != nil { + return err + } + err = localClone.Wait() + if err != nil { + fmt.Println("Couldn't clone from", local, ". Trying network...") + + // try to git clone the repository over the network + networkClone := exec.Command("git", "clone", network, path) + networkClone.Stdout = os.Stdout + networkClone.Stderr = os.Stderr + + err = networkClone.Start() + if err != nil { + fmt.Println("Network clone failed to start. Try again and make sure you have a network connection.") + return err + } + err = networkClone.Wait() + if err != nil { + fmt.Println("Network clone failure.") + // failed + return fmt.Errorf("Failed to clone files from local machine [%s] and over the network [%s].\n%s", local, network, err) + } + } + + fmt.Println("New project created at", path) + return nil +} |