diff --git a/.gitignore b/.gitignore index 89dd93e..20996fc 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ _testmain.go *.exe *.test *.prof +*.bak diff --git a/.travis.yml b/.travis.yml index 23e7f9e..a3c8671 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,12 +10,15 @@ branches: - /^v[0-9.]+$/ go: - - 1.9 - - "1.10" + - 1.9.x + - 1.x - tip go_import_path: aahframework.org/tools.v0/aah +before_install: + - bash <(curl -s https://aahframework.org/base-before-install) + install: - go get -t -v ./... diff --git a/README.md b/README.md index c58614f..e2a39d8 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,15 @@ -# tools - aah framework - -Home of all aah framework CLI tools. - -## aah CLI Tool -[![Build Status](https://travis-ci.org/go-aah/tools.svg?branch=master)](https://travis-ci.org/go-aah/tools) [![Go Report Card](https://goreportcard.com/badge/aahframework.org/tools.v0)](https://goreportcard.com/report/aahframework.org/tools.v0/aah) - [![Powered by Go](https://img.shields.io/badge/powered_by-go-blue.svg)](https://golang.org) - [![Version](https://img.shields.io/badge/version-0.10-blue.svg)](https://github.com/go-aah/tools/releases/latest) - [![License](https://img.shields.io/github/license/go-aah/tools.svg)](LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@aahframework-55acee.svg)](https://twitter.com/aahframework) - -***Release [v0.10](https://github.com/go-aah/tools/releases/latest) tagged on Mar 28, 2018*** +

+ +

Home of all CLI tools by aah framework

+

+

+

Build Status Go Report Card Godoc Twitter @aahframework

+

aah - A secure, flexible, rapid Go web framework. -Requires `go1.8` and above. +### News + + * aah CLI Release Version [released](https://github.com/go-aah/tools/releases/latest) and tagged on Jul 06, 2018. -Visit official website https://aahframework.org to learn more. +Visit official website https://aahframework.org to learn more about `aah` framework. diff --git a/aah/aah.go b/aah/aah.go index 1352b26..f7b953a 100644 --- a/aah/aah.go +++ b/aah/aah.go @@ -1,5 +1,5 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools/aah source code and usage is governed by a MIT style +// aahframework.org/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main @@ -7,44 +7,31 @@ package main import ( "errors" "fmt" - "io" "os" "os/exec" "path/filepath" - "runtime" "sort" - "strings" "gopkg.in/urfave/cli.v1" - "aahframework.org/aah.v0" - "aahframework.org/ahttp.v0" "aahframework.org/aruntime.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/i18n.v0" "aahframework.org/log.v0" - "aahframework.org/router.v0" - "aahframework.org/security.v0" - "aahframework.org/test.v0" - "aahframework.org/valpar.v0" - "aahframework.org/view.v0" ) const ( - permRWXRXRX = 0755 - permRWRWRW = 0666 - versionSeries = "v0" - importPrefix = "aahframework.org" + permRWXRXRX = 0755 + permRWRWRW = 0666 + importPrefix = "aahframework.org" ) var ( gopath string gocmd string gosrcDir string - - libNames = []string{"tools", "aah", "ahttp", "aruntime", "config", "essentials", "forge", "i18n", - "log", "router", "security", "test", "valpar", "view"} + gitcmd string + aahVer string // abstract it, so we can do unit test fatal = log.Fatal @@ -53,8 +40,13 @@ var ( // cli logger cliLog *log.Logger + + // CliPackaged is identify cli from go get or binary dist + CliPackaged string ) +var errStopHere = errors.New("stop here") + func checkPrerequisites() error { // check go is installed or not if !ess.LookExecutable("go") { @@ -68,12 +60,38 @@ func checkPrerequisites() error { return err } - if gocmd, err = exec.LookPath("go"); err != nil { + // Go executable + gocmdName := goCmdName() + if gocmd, err = exec.LookPath(gocmdName); err != nil { return err } gosrcDir = filepath.Join(gopath, "src") + // git + if gitcmd, err = exec.LookPath("git"); err != nil { + return err + } + + // aah + if aahVer, err = aahVersion(); err == errVersionNotExists { + if collectYesOrNo(reader, "aah framework is not installed in GOPATH, would you like to install [Y]es or [N]o") { + args := []string{"get"} + if gocmdName == "go" { + args = append(args, "-u") + } + args = append(args, "aahframework.org/aah.v0") + if _, err := execCmd(gocmd, args, false); err != nil { + return err + } + aahVer, _ = aahVersion() + fmt.Printf("\naah framework successfully installed in GOPATH\n\n") + return nil + } + fmt.Printf("\nOkay, you could do it manually, run '%s get aahframework.org/aah.v0'\n", gocmdName) + return errStopHere + } + return nil } @@ -82,14 +100,16 @@ func main() { // if panic happens, recover and abort nicely :) defer func() { if r := recover(); r != nil { - cfg, _ := config.ParseString(``) - strace := aruntime.NewStacktrace(r, cfg) + strace := aruntime.NewStacktrace(r, config.NewEmpty()) strace.Print(os.Stdout) exit(2) } }() - if err := checkPrerequisites(); err != nil { + err := checkPrerequisites() + if err == errStopHere { + return + } else if err != nil { logFatal(err) } @@ -112,6 +132,7 @@ func main() { switchCmd, updateCmd, generateCmd, + migrateCmd, } sort.Sort(cli.FlagsByName(app.Flags)) @@ -123,66 +144,39 @@ func main() { //___________________________________ func printHeader(c *cli.Context) error { - hdr := fmt.Sprintf("aah framework v%s - https://aahframework.org", aah.Version) - improveRpt := "# Report improvements/bugs at https://github.com/go-aah/aah/issues #" + hdrCont := fmt.Sprintf("aah framework v%s", aahVer) + improveRpt := "# Report improvements/bugs at https://aahframework.org/issues #" cnt := len(improveRpt) - sp := (cnt - len(hdr)) / 2 - - if !isWindowsOS() { - fmt.Fprintf(c.App.Writer, "\033[1;32m") - } + sp := (cnt - len(hdrCont)) / 2 - printChr(c.App.Writer, "‾", cnt) - fmt.Fprintf(c.App.Writer, "\n") - printChr(c.App.Writer, " ", sp) - fmt.Fprintf(c.App.Writer, hdr) - printChr(c.App.Writer, " ", sp) - fmt.Fprintf(c.App.Writer, "\n") - printChr(c.App.Writer, "_", cnt) - fmt.Fprintf(c.App.Writer, "\n") - - if !isWindowsOS() { - fmt.Fprintf(c.App.Writer, "\033[0m") - } + fmt.Println(chr2str("-", cnt)) + fmt.Println(chr2str(" ", sp) + hdrCont) + fmt.Println(chr2str("-", cnt)) + fmt.Printf(improveRpt + "\n\n") - fmt.Fprintf(c.App.Writer, improveRpt+"\n\n") return nil } -func printChr(w io.Writer, chr string, cnt int) { +func chr2str(chr string, cnt int) string { + var str string for idx := 0; idx < cnt; idx++ { - fmt.Fprintf(w, chr) + str += chr } + return str } func init() { cli.HelpFlag = cli.BoolFlag{ Name: "h, help", - Usage: "show help", + Usage: "Shows help", } cli.VersionFlag = cli.BoolFlag{ Name: "v, version", - Usage: "print aah framework version and go version", + Usage: "Prints cli, aah, go and aah libraries version", } - cli.VersionPrinter = func(c *cli.Context) { - _ = printHeader(c) - fmt.Fprint(c.App.Writer, "Version Info:\n") - fmt.Fprintf(c.App.Writer, "\t%-17s v%s\n", "aah framework", aah.Version) - fmt.Fprintf(c.App.Writer, "\t%-17s v%s\n", "aah cli tool", Version) - fmt.Fprintf(c.App.Writer, "\t%-17s %s\n", "Modules: ", strings.Join( - []string{ - "ahttp v" + ahttp.Version, "aruntime v" + aruntime.Version, "config v" + config.Version, - "essentials v" + ess.Version, "i18n v" + i18n.Version, "log v" + log.Version}, ", ")) - fmt.Fprintf(c.App.Writer, "\t%-17s %s\n", "", strings.Join( - []string{"router v" + router.Version, "security v" + security.Version, - "test v" + test.Version, "valpar v" + valpar.Version, "view v" + view.Version}, ", ")) - fmt.Println() - fmt.Fprintf(c.App.Writer, "\t%-17s %s\n", fmt.Sprintf("go[%s/%s]", - runtime.GOOS, runtime.GOARCH), runtime.Version()[2:]) - fmt.Println() - } + cli.VersionPrinter = VersionPrinter cli.AppHelpTemplate = `Usage: {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} diff --git a/aah/app-template/.gitignore b/aah/app-template/.gitignore deleted file mode 100644 index 0d8386d..0000000 --- a/aah/app-template/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# aah framework application - .gitignore - -aah.go -*.pid -build/ -vendor/*/ - -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof diff --git a/aah/app-template/aah.project.atmpl b/aah/app-template/aah.project.atmpl deleted file mode 100644 index 791d7bb..0000000 --- a/aah/app-template/aah.project.atmpl +++ /dev/null @@ -1,68 +0,0 @@ -############################################################## -# {{ .AppName }} - aah framework project -# -# Note: Add it to version control -############################################################## - -# Build section is used during aah application compile and build command. -build { - # Application binary name - # Default value is `name` attribute value from `aah.conf` - #binary_name = "{{ .AppName }}" - - # Used as fallback if - # - `git commit sha` or - # - `AAH_APP_VERSION` environment value is not available. - version = "0.0.1" - - # If application is missing any dependencies in `build import path` - # during a compile and build, aah CLI will try to get dependencies - # using 'go get '. - # Default value is `false`. - #dep_get = true - - flags = ["-i"] - - ldflags = "" - - tags = "" - - # AST excludes is used for `aah.Context` inspection and generating aah - # application main Go file. Valid exclude patterns - # refer: https://golang.org/pkg/path/filepath/#Match - ast_excludes = ["*_test.go", ".*", "*.bak", "*.tmp", "vendor"] - - # Packing excludes is used to exclude file/directory during aah application - # build archive. Valid exclude patterns - # refer: https://golang.org/pkg/path/filepath/#Match - excludes = ["*.go", "*_test.go", ".*", "*.bak", "*.tmp", "vendor", "app", "build", "tests", "logs"] -} - -# Logger configuration for aah CLI tool. -log { - # Log level - # Default value is `info`. - #level = "info" - - # Log colored output - # Default value is `true`. - #color = false -} - -# Hot-Reload is development purpose to help developer. -# Read more about implementation here - https://github.com/go-aah/aah/issues/4 -# -# NOTE: Do not use hot-reload feature for production purpose, it's not recommended. -hot_reload { - # Default value is `true`. - enable = true - - # Watch configuration - files/directories exclusion list. - watch { - # Note: static directory not required to be monitored, since server delivers - # up-to-date file on environment profile `dev`. - dir_excludes = [".*"] - - file_excludes = [".*", "_test.go", "LICENSE", "README.md"] - } -} diff --git a/aah/app-template/app/controllers/app.go.atmpl b/aah/app-template/app/controllers/app.go.atmpl deleted file mode 100644 index d1b6ede..0000000 --- a/aah/app-template/app/controllers/app.go.atmpl +++ /dev/null @@ -1,27 +0,0 @@ -package controllers - -import ( - "aahframework.org/aah.v0" - "{{.AppImportPath}}/app/models" -) - -// AppController struct application controller -type AppController struct { - *aah.Context -} - -// Index method is application {{ if eq .AppType "web" -}}home page.{{ else }}root API endpoint.{{- end }} -func (a *AppController) Index() { - {{- if eq .AppType "web" }} - data := aah.Data{ - "Greet": models.Greet{ - Message: "Welcome to aah framework - Web Application", - }, - } - - a.Reply().Ok().HTML(data) -{{- else }} - a.Reply().Ok().JSON(models.Greet{ - Message: "Welcome to aah framework - API application", - }){{ end }} -} diff --git a/aah/app-template/app/init.go.atmpl b/aah/app-template/app/init.go.atmpl deleted file mode 100644 index c8b83c6..0000000 --- a/aah/app-template/app/init.go.atmpl +++ /dev/null @@ -1,109 +0,0 @@ -// aah application initialization - configuration, server extensions, middleware's, etc. -// Customize it per your application needs. - -package main - -import ( - "aahframework.org/aah.v0"{{ if eq .AppType "web" }} - - // Registering HTML minifier for web application - _ "github.com/aah-cb/minify"{{ end }}{{ if eq .AppViewEngine "pug" }} - - // Registering Pug View Engine (formerly known as Jade) - _ "github.com/aah-cb/ve-pug"{{ end }} -) - -func init() { - - //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - // Server Extensions - // Doc: https://docs.aahframework.org/server-extension.html - // - // Best Practice: Define a function with meaningful name in a package and - // register it here. Extensions function name gets logged in the log, - // its very helpful to have meaningful log information. - // - // Such as: - // - Dedicated package for config loading - // - Dedicated package for datasource connections - // - etc - //__________________________________________________________________________ - - // Event: OnInit - // Published right after the `aah.AppConfig()` is loaded. - // - // aah.OnInit(config.LoadRemote) - - // Event: OnStart - // Published right before the start of aah go Server. - // - // aah.OnStart(db.Connect) - // aah.OnStart(cache.Load) - - // Event: OnShutdown - // Published on receiving OS Signals `SIGINT` or `SIGTERM`. - // - // aah.OnShutdown(cache.Flush) - // aah.OnShutdown(db.Disconnect) - - //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - // Middleware's - // Doc: https://docs.aahframework.org/middleware.html - // - // Executed in the order they are defined. It is recommended; NOT to change - // the order of pre-defined aah framework middleware's. - //__________________________________________________________________________ - aah.Middlewares( - aah.RouteMiddleware, - aah.CORSMiddleware, - aah.BindMiddleware,{{ if eq .AppType "web" }} - aah.AntiCSRFMiddleware,{{ end }} - aah.AuthcAuthzMiddleware, - - // - // NOTE: Register your Custom middleware's right here - // - - aah.ActionMiddleware, - ) - - //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - // Add Application Error Handler - // Doc: https://docs.aahframework.org/error-handling.html - //__________________________________________________________________________ - // aah.SetErrorHandler(AppErrorHandler) - - {{- if eq .AppType "web" }} - - //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - // Add Custom Template Functions - // Doc: https://docs.aahframework.org/template-funcs.html - //__________________________________________________________________________ - // aah.AddTemplateFunc(template.FuncMap{ - // // ... - // }) - - //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - // Add Custom Session Store - // Doc: https://docs.aahframework.org/session.html - //__________________________________________________________________________ - // aah.AddSessionStore("redis", &RedisSessionStore{}){{ end }} - - //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - // Add Custom value Parser - // Doc: https://docs.aahframework.org/request-parameters-auto-bind.html - //__________________________________________________________________________ - // if err := aah.AddValueParser(reflect.TypeOf(CustomType{}), customParser); err != nil { - // log.Error(err) - // } - - //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - // Add Custom Validation Functions - // Doc: https://godoc.org/gopkg.in/go-playground/validator.v9 - //__________________________________________________________________________ - // Obtain aah validator instance, then add yours - // validator := valpar.Validator() - // - // // Add your validation funcs - -} diff --git a/aah/app-template/app/models/greet.go b/aah/app-template/app/models/greet.go deleted file mode 100644 index 0479b35..0000000 --- a/aah/app-template/app/models/greet.go +++ /dev/null @@ -1,6 +0,0 @@ -package models - -// Greet holds the greeting message. -type Greet struct { - Message string `json:"message"` -} diff --git a/aah/app-template/app/security/authentication_provider.go b/aah/app-template/app/security/authentication_provider.go deleted file mode 100644 index 7c23ad2..0000000 --- a/aah/app-template/app/security/authentication_provider.go +++ /dev/null @@ -1,68 +0,0 @@ -package security - -import ( - "aahframework.org/aah.v0" - "aahframework.org/config.v0" - "aahframework.org/security.v0/authc" -) - -var _ authc.Authenticator = (*AuthenticationProvider)(nil) - -// AuthenticationProvider struct implements `authc.Authenticator` interface. -type AuthenticationProvider struct { -} - -// Init method initializes the AuthenticationProvider, this method gets called -// during server start up. -func (a *AuthenticationProvider) Init(cfg *config.Config) error { - - // NOTE: Init is called on application startup - - return nil -} - -// GetAuthenticationInfo method is `authc.Authenticator` interface -func (a *AuthenticationProvider) GetAuthenticationInfo(authcToken *authc.AuthenticationToken) (*authc.AuthenticationInfo, error) { - - //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - // This code snippet provided as a reference - // - // Call your appropriate datasource here (such as DB, API, LDAP, etc) - // to get the subject (aka user) authentication information. - // - // Form Auth Values from authcToken - // authcToken.Identity => username - // authcToken.Credential => passowrd - //_____________________________________________________________________ - - // user := models.FindUserByEmail(authcToken.Identity) - // if user == nil { - // // No subject exists, return nil and error - // return nil, authc.ErrSubjectNotExists - // } - - // User found, now create authentication info and return to the framework - authcInfo := authc.NewAuthenticationInfo() - // authcInfo.Principals = append(authcInfo.Principals, - // &authc.Principal{ - // Value: user.Email, - // IsPrimary: true, - // Realm: "inmemory", - // }) - // authcInfo.Credential = []byte(user.Password) - // authcInfo.IsLocked = user.IsLocked - // authcInfo.IsExpired = user.IsExpried - - return authcInfo, nil -} - -func postAuthEvent(e *aah.Event) { - ctx := e.Data.(*aah.Context) - - // Do post successful authentication actions... - _ = ctx -} - -func init() { - aah.OnPostAuth(postAuthEvent) -} diff --git a/aah/app-template/app/security/authorization_provider.go b/aah/app-template/app/security/authorization_provider.go deleted file mode 100644 index 6f45ac6..0000000 --- a/aah/app-template/app/security/authorization_provider.go +++ /dev/null @@ -1,43 +0,0 @@ -package security - -import ( - "aahframework.org/config.v0" - "aahframework.org/security.v0/authc" - "aahframework.org/security.v0/authz" -) - -var _ authz.Authorizer = (*AuthorizationProvider)(nil) - -// AuthorizationProvider struct implements `authz.Authorizer` interface. -type AuthorizationProvider struct { -} - -// Init method initializes the AuthorizationProvider, this method gets called -// during server start up. -func (a *AuthorizationProvider) Init(cfg *config.Config) error { - - // NOTE: Init is called on application startup - - return nil -} - -// GetAuthorizationInfo method is `authz.Authorizer` interface. -// -// GetAuthorizationInfo method gets called after authentication is successful -// to get Subject's (aka User) access control information such as roles and permissions. -func (a *AuthorizationProvider) GetAuthorizationInfo(authcInfo *authc.AuthenticationInfo) *authz.AuthorizationInfo { - //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - // This code snippet provided as a reference - // - // Call your appropriate datasource here (such as DB, API, etc) - // to get the subject (aka user) authorization details (roles, permissions) - //__________________________________________________________________________ - - // authorities := models.FindUserByEmail(authcInfo.PrimaryPrincipal().Value) - - authzInfo := authz.NewAuthorizationInfo() - // authzInfo.AddRole(authorities.Roles...) - // authzInfo.AddPermissionString(authorities.Permissions...) - - return authzInfo -} diff --git a/aah/app-template/config/aah.conf.atmpl b/aah/app-template/config/aah.conf.atmpl deleted file mode 100644 index 09c1ee1..0000000 --- a/aah/app-template/config/aah.conf.atmpl +++ /dev/null @@ -1,455 +0,0 @@ -################################################### -# {{ .AppName }} - aah framework application -# -# Complete configuration reference: -# https://docs.aahframework.org/app-config.html -################################################### - -# Application name (non-whitespace) -# Default value is `basename` of import path. -name = "{{ .AppName }}" - -# Friendly description of application -desc = "aah framework {{ .AppType }} application" - -# Application type, typically either Web or API. -type = "{{ .AppType }}" - -# Application instance name is used when you're running aah application cluster. -# This value is used in the context based logging, it distinguishes your instance -# log from other instances. -# -# Typically you can to pass `instance_name` value via aah external config -# support or Environment variable. -instance_name = $AAH_INSTANCE_NAME - -# Configure file path of application PID file to be written. -# Ensure application has appropriate permission and directory exists. -# Default value is `/.pid` -#pid_file = "/path/to/pidfile.pid" - -# ----------------------------------------------------------------- -# Server configuration - HTTP -# Doc: https://docs.aahframework.org/app-config.html#section-server -# ----------------------------------------------------------------- -server { - # For unix socket: unix:/tmp/aahframework.sock - # Default value is `empty` string. - #address = "" - - # For standard port `80` and `443`, put empty string or a value - # Default value is 8080. - #port = "" - - # Header value written as `Server` HTTP header. - # If you do not want to include `Server` header, comment it out. - header = "aah-go-server" - - # Valid time units are "s = seconds", "m = minutes" - timeout { - # Mapped to `http.Server.ReadTimeout`, is the maximum duration for reading - # the entire request, including the body. - # - # Because ReadTimeout does not let Handlers make per-request decisions on - # each request body's acceptable deadline or upload rate, most users will - # prefer to use ReadHeaderTimeout. It is valid to use them both. - # Default value is `90s`. - #read = "90s" - - # Mapped to `http.Server.WriteTimeout`, is the maximum duration before timing - # out writes of the response. It is reset whenever a new request's header is - # read. Like ReadTimeout, it does not let Handlers make decisions on a - # per-request basis. - # Default value is `90s`. - #write = "90s" - - # aah server graceful shutdown timeout - # Default value is `60s`. - #grace_shutdown = "60s" - } - - # Mapped to `http.Server.MaxHeaderBytes`. - # Default value is `1mb`. - #max_header_bytes = "1mb" - - # HTTP server keep alive option. - # Default value is `true`. - #keep_alive = true - - ssl { - # Default value is `false`. - #enable = false - - # Default value is `empty` string. - #cert = "" - - # Default value is `empty` string. - #key = "" - - # Disabling HTTP/2 set it true. - # Default value is `false`. - #disable_http2 = true - - # Redirect HTTP => HTTPS functionality does protocol switch, so it works - # with domain and subdomains. - # For example: - # http://aahframework.org => https://aahframework.org - # http://www.aahframework.org => https://www.aahframework.org - # http://docs.aahframework.org => https://docs.aahframework.org - redirect_http { - # Enabling HTTP => HTTPS redirects. - # Default is value is `false`. - #enable = true - - # Port no. of HTTP requests to listen. - # For standard port `80` put empty string or a value. - # It is required value, no default. - port = "8080" - - # Redirect code - # Default value is `307`. - #code = 307 - } - - lets_encrypt { - # To get SSL certificate from Let's Encrypt CA, enable it. - # Don't forget to enable `server.ssl.enable=true`. - # Default value is `false`. - #enable = false - - # Host policy controls which domains the autocert will attempt - # to retrieve new certificates for. It does not affect cached certs. - # It is required, no default value. - #host_policy = ["example.org", "docs.example.org"] - - # Renew before optionally specifies how early certificates should - # be renewed before they expire. - # Default value is `10` days. - #renew_before = 10 - - # Email optionally specifies a contact email address. This is used by CAs, - # such as Let's Encrypt, to notify about problems with issued certificates. - # If the Client's account key is already registered, Email is not used. - #email = "jeeva@myjeeva.com" - - # Force RSA makes the autocert generate certificates with 2048-bit RSA keys. - # If false, a default is used. Currently the default is - # EC-based keys using the P-256 curve. - #force_rsa = false - - # Cache optionally stores and retrieves previously-obtained certificates - # autocert manager. By default certs will only be cached for the lifetime - # of the autocert manager. - # - # autocert manager passes the Cache certificates data encoded in PEM, - # with private/public parts combined in a single Cache.Put call, - # private key first. - # Default value is `empty` string. - #cache_dir = "/Users/jeeva/autocert" - } - } - - # -------------------------------------------------------------------------- - # To manage aah server effectively it is necessary to know details about the - # request, response, processing time, client IP address, etc. aah framework - # provides the flexible and configurable access log capabilities. - # Doc: https://docs.aahframework.org/server-access-log.html - # -------------------------------------------------------------------------- - access_log { - # Enabling server access log - # Default value is `false`. - #enable = true - - # Absolute path to access log file or relative path. - # Default location is application logs directory - #file = "{{ .AppName }}-access.log" - - # Default server access log pattern - #pattern = "%clientip %custom:- %reqtime %reqmethod %requrl %reqproto %resstatus %ressize %restime %reqhdr:referer" - - # Access Log channel buffer size - # Default value is `500`. - #channel_buffer_size = 500 - - # Include static files access log too. - # Default value is `true`. - #static_file = false - } - - # ------------------------------------------------------- - # Dump Request & Response Details - # Such as URL, Proto, Headers, Body, etc. - # Note: Dump is not applicable for Static Files delivery. - # Doc: https://docs.aahframework.org/server-dump-log.html - # ------------------------------------------------------- - dump_log { - # Default value is `false`. - #enable = true - - # Absolute path to dump log file or relative path. - # Default location is application logs directory - #file = "{{ .AppName }}-dump.log" - - # Log Request body into dump log. aah dumps body for JSON, XML, Form - # HTML and Plain Text content types. - # Default value is `false`. - #request_body = true - - # Log Request body into dump log. aah dumps body for JSON, XML, Form - # HTML, and Plain Text content types. - # Default value is `false`. - #response_body = true - } -} - -# ------------------------------------------------------------------ -# Request configuration -# Doc: https://docs.aahframework.org/app-config.html#section-request -# ------------------------------------------------------------------ -request { - # aah framework encourages to have unique `Request Id` for each incoming - # request, it helps in traceability. If request has already `sX-Request-Id` - # HTTP header then it does not generate one. - id { - # Default value is `true`. - enable = true - - # Default value is `X-Request-Id`, change it if you have different one. - #header = "X-Request-Id" - } - - # Max request body size for all incoming HTTP requests except `MultipartForm`. - # Also you can override this size for individual route on specific cases - # in `routes.conf` if need be. - # Default value is `5mb`. - #max_body_size = "5mb" - - # Default value is `32mb`, choose your value based on your use case - #multipart_size = "32mb" - - # aah provides `Content Negotiation` feature for the incoming HTTP request. - # Read more about implementation and RFC details here GitHub #75. - # Perfect for REST API, also can be used for web application too if needed. - content_negotiation { - # To enable Content Negotiation for the application. - # Default value is `false`. - #enable = true - - # For example: Client sends Content-Type header as `application/xml`. - # However server only supports JSON payload as request body. - # Then server responds with 415 Unsupported Media Type. - # Default value is empty list and disabled. - #accepted = ["application/json", "text/json"] - - # For example: Client sends Accept header as `application/xml`. - # However server only supports serving JSON i.e. `application/json`. - # Then server responds with 406 Not Acceptable. - # Default value is empty list and disabled. - #offered = ["application/json", "text/json"] - } - - # Auto Bind configuration used to bind request parameters to controller - # action parameters. - auto_bind { - # Priority is used to select the bind source priority. - # P -> Path Parameter - # F -> Form Parameter - # Q -> Query Parameter - # - # For example: Let's say you have a controller action named `OrderInfo` and its has - # parameter called `orderId`. So framework tries to parse and bind based - # on the priority. The `orderId` present in `Path` and `Form`, framework - # binds the value from `Path`. Typically recommended to have unique names - # in the request parameter though :) - # Path -> Form -> Query - # If not found then it returns with default Go zero value. - # - # Default value is `PFQ`. - #priority = "PFQ" - - # Tag Name is used for bind values to struct exported fields. - # Default value is `bind`. - #tag_name = "bind" - } -} -{{ if eq .AppType "web" -}} -# --------------------------------------------------------------- -# i18n configuration -# Doc: https://docs.aahframework.org/app-config.html#section-i18n -# --------------------------------------------------------------- -i18n { - # It is used as fallback if framework is unable to determine the - # locale from HTTP Request. - # Default value is `en`. - #default = "en" - - # Overriding Request Locale `Accept-Language` header value via URL Path - # parameter or URL Query parameter. - param_name { - # Specify URL Path Param name i.e. `/:lang/home.html`, `/:lang/aboutus.html`, etc. - # For e.g.: `/en/home.html`, `/en/aboutus.html`, `/zh-CN/home.html`, `/zh-CN/aboutus.html` etc. - # Default value is `lang`. - #path = "locale" - - # Specify URL Query Param name i.e `?lang=en`, `?lang=zh-CN`, etc. - # Default value is `lang`. - #query = "locale" - } -}{{ end }} - -# ----------------------------------------------------------------- -# Format configuration -# Doc: https://docs.aahframework.org/app-config.html#section-format -# ----------------------------------------------------------------- -format { - # Time format for auto parse and bind. aah tries to parse the - # time value in the order they defined till it gets success - # otherwise returns the error. - time = [ - "2006-01-02T15:04:05Z07:00", - "2006-01-02T15:04:05Z", - "2006-01-02 15:04:05", - "2006-01-02" - ] -} - -# ------------------------------------------------------------------ -# Runtime configuration -# Doc: https://docs.aahframework.org/app-config.html#section-runtime -# ------------------------------------------------------------------ -runtime { - debug { - # Choose an appropriate buffer size for collecting all goroutines stack trace - # dump based on your case. - # Default value is `2mb`. - #stack_buffer_size = "2mb" - - # Whether to collect all the Go routines details or not. - # Default value is `false`. - #all_goroutines = true - - # Whether to strip source `src` base path from file path. - # Default value is `false`. - #strip_src_base = true - } -} - -# ----------------------------------------------------------------- -# Render configuration -# Doc: https://docs.aahframework.org/app-config.html#section-render -# ----------------------------------------------------------------- -render { - # aah framework chooses the `Content-Type` value automatically based on - # configuration if `aah.Reply()` builder value is not set. It selects in - # the order of: - # - Based on URL file extension, supported `.html`, `.htm`, `.json`, `.js`, `.xml` and `.txt` - # - Request Accept Header - Most Qualified one as per RFC7321 - # - Based `render.default` value supported types are `html`, `json`, `xml` and `text` - # - Finally aah framework uses `http.DetectContentType` API - # Default value is `empty` string. - default = "{{ if eq .AppType "web" }}html{{ else }}json{{ end }}" - - # Pretty print option is helpful in `dev` environment profile. - # It is only applicable to JSON and XML. - # Default value is `false`. - #pretty = true - - # Gzip compression configuration for HTTP response. - gzip { - # By default Gzip compression is enabled in aah framework, however - # framework ensures HTTP client accepts Gzip response otherwise - # it won't use Gzip compression. - # - # Tips: If you have nginx or apache web server enabled with gzip in-front - # of aah server the set this value to `false`. - # - # Default value is `true`. - #enable = true - - # Used to control Gzip compression levels. Valid levels are - # 1 = BestSpeed to 9 = BestCompression. - # Default value is `4`. - #level = 4 - } -} -{{ if eq .AppType "web" -}} -# ------------------------------------------------------------------ -# Cache configuration -# Doc: https://docs.aahframework.org/static-files.html#cache-control -# ------------------------------------------------------------------ -cache { - static { - # Default `Cache-Control` for all static files, - # if specific mime type is not defined. - default_cache_control = "public, max-age=31536000" - - # Define by mime types, if mime is not present then default is applied. - # Config is very flexible to define by mime type. - # - # Create a unique name and provide `mime` with comma separated value - # and `cache_control`. - # mime_types { - # css_js { - # mime = "text/css, application/javascript" - # cache_control = "public, max-age=604800, proxy-revalidate" - # } - # - # images { - # mime = "image/jpeg, image/png, image/gif, image/svg+xml, image/x-icon" - # cache_control = "public, max-age=2628000, proxy-revalidate" - # } - # } - } -} - -# --------------------------------------------------------------- -# View configuration -# Doc: https://docs.aahframework.org/app-config.html#section-view -# --------------------------------------------------------------- -view { - # Choosing view engine for application. You could implement - # on your own with simple interface `view.Enginer`. - # Default value is `go`. - engine = "{{ .AppViewEngine }}" - - # Choose your own view file extension. - # Default value is chosen based on `view.engine`, - # while creating a new app using command `aah new`. - ext = "{{ .AppViewFileExt }}" - - # Choose whether you need a case sensitive view file resolve or not. - # For e.g.: "/views/pages/app/login.tmpl" == "/views/pages/App/Login.tmpl" - # Default value is `false`. - #case_sensitive = false - - # To use custom Go template delimiters for view files. - # Default value is `{{ .TmplDemils }}`. - #delimiters = "{{ .TmplDemils }}" - - # Framework chooses the default app layout as `master.html` if you do not - # provide one. However you may have pages or template without layout too. - # So option to disable the default layout for HTML. - # Default value is `true`. Available since v0.6 - #default_layout = false -}{{ end }} - -# -------------------------------------------------------------- -# Application Security -# Doc: https://docs.aahframework.org/security-config.html -# -------------------------------------------------------------- -include "./security.conf" - -# -------------------------------------------------------------- -# Environment Profiles e.g.: dev, qa, prod -# Doc: https://docs.aahframework.org/app-config.html#section-env -# -------------------------------------------------------------- -env { - # Indicates active profile name for application configuration. - # Default value is `dev`. - #active = "dev" - - # ---------------------------------- - # Environment profile configurations - # ---------------------------------- - include "./env/*.conf" -} diff --git a/aah/app-template/config/env/dev.conf.atmpl b/aah/app-template/config/env/dev.conf.atmpl deleted file mode 100644 index 43598ab..0000000 --- a/aah/app-template/config/env/dev.conf.atmpl +++ /dev/null @@ -1,45 +0,0 @@ -# --------------------------------- -# Development Configuration Section -# --------------------------------- - -dev { - - # -------------------------------------------------- - # Log Configuration - # Doc: https://docs.aahframework.org/logging.html - # -------------------------------------------------- - log { - # Receiver is where is log values gets logged. aah - # supports `console` and `file` receivers. Hooks for extension. - # Default value is `console`. - #receiver = "file" - - # Level indicates the logging levels like `ERROR`, `WARN`, `INFO`, `DEBUG`, - # `TRACE`, FATAL and PANIC. Config value can be in lowercase or uppercase. - # Default value is `debug`. - level = "info" - - # Format to define log entry output format. Supported formats are `text` and `json`. - # Default value is `text`. - #format = "json" - - # Pattern config defines the message flags and formatting while logging - # into receivers. Customize it as per your need, learn more about flags - # and format - https://docs.aahframework.org/log-config.html#pattern - # Default value is `%time:2006-01-02 15:04:05.000 %level:-5 %appname %insname %reqid %principal %message %fields` - #pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %appname %insname %reqid %principal %message %fields" - - # Log colored output, applicable only to `console` receiver type. - # Default value is `true`. - #color = false - } - - # ------------------------- - # Render configuration - # ------------------------- - render { - # pretty is only applicable to JSON & XML rendering - pretty = true - } - -} diff --git a/aah/app-template/config/env/prod.conf.atmpl b/aah/app-template/config/env/prod.conf.atmpl deleted file mode 100644 index bc14244..0000000 --- a/aah/app-template/config/env/prod.conf.atmpl +++ /dev/null @@ -1,54 +0,0 @@ -# --------------------------------- -# Production Configuration Section -# --------------------------------- - -prod { - - # -------------------------------------------------- - # Log Configuration - # Doc: https://docs.aahframework.org/logging.html - # -------------------------------------------------- - log { - # Receiver is where is log values gets logged. aah - # supports `console` and `file` receivers. Hooks for extension. - # Default value is `console`. - receiver = "file" - - # Level indicates the logging levels like `ERROR`, `WARN`, `INFO`, `DEBUG`, - # `TRACE`, FATAL and PANIC. Config value can be in lowercase or uppercase. - # Default value is `debug`. - level = "warn" - - # Format to define log entry output format. Supported formats are `text` and `json`. - # Default value is `text`. - #format = "json" - - # Pattern config defines the message flags and formatting while logging - # into receivers. Customize it as per your need, learn more about flags - # and format - https://docs.aahframework.org/log-config.html#pattern - # Default value is `%time:2006-01-02 15:04:05.000 %level:-5 %appname %insname %reqid %principal %message %fields` - #pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %appname %insname %reqid %principal %message %fields" - - # File config attribute is applicable only to `file` receiver type. - # Default value is `aah-log-file.log`. - file = "{{ .AppName }}.log" - - # Rotate config section is applicable only to `file` receiver type. - # Default rotation is 'daily'. - rotate { - # Policy is used to determine rotate policy. aah supports `daily`, - # `lines` and `size` policies. - # Default value is `daily`. - #policy = "daily" - - # This is applicable only to if `mode` is `size`. - # Default value is 100MB. - #size = 500 - - # This is applicable only to if `mode` is `lines`. - # Default value is unlimited. - #lines = 100000 - } - } - -} diff --git a/aah/app-template/config/routes.conf.atmpl b/aah/app-template/config/routes.conf.atmpl deleted file mode 100644 index 8679517..0000000 --- a/aah/app-template/config/routes.conf.atmpl +++ /dev/null @@ -1,215 +0,0 @@ -#################################################### -# {{ .AppName }} - Application Routes Configuration -# -# Complete routes configuration reference: -# https://docs.aahframework.org/routes-config.html -#################################################### - -#------------------------------------------------------------------------ -# Domain and sub-domain configuration goes into section `domains { ... }` -#------------------------------------------------------------------------ -domains { - - # Pick your choice of an `unique keyname` to define your domain section - # in the routes configuration. - # For e.g.: Domain name/ip address with port no - localhost { - name = "{{ .AppName }} routes" - - # aah supports multi-domain routes configuration out-of-the-box. - # `host` used to determine domain routes for the incoming request. - # For e.g: example.org - host = "localhost" - - # Redirect trailing slash is to enable automatic redirection if the current - # route can't be matched but a `route` for the path with (without) - # the trailing slash exists. - # Default value is `true`. - redirect_trailing_slash = true - - # aah supports out-of-the-box `405 MethodNotAllowed` status with `Allow` - # header as per `RFC7231`. Perfect for RESTful APIs. - # Default value is `true`. - {{ if eq .AppType "web" }}#{{ end }}method_not_allowed = true - - # aah framework supports out-of-the-box `OPTIONS` request replies. - # User defined `OPTIONS` routes take priority over the automatic replies. - # Perfect for RESTful APIs. - # Default value is `true`. - {{ if eq .AppType "web" }}#{{ end }}auto_options = true - - # Default auth is used when route does not have attribute `auth` defined. - # If you don't define attribute `auth` then framework treats that route as - # `anonymous` auth scheme. - # Default value is empty string. - #default_auth = "" - - {{ if .AppCORSEnable -}} - #---------------------------------------------------------------------------- - # CORS (Cross-Origin Resource Sharing) - # Doc: https://docs.aahframework.org/cors.html - #---------------------------------------------------------------------------- - cors { - # Default value is `false`. - enable = true - - # Specify `Access-Control-Allow-Origin` header values. - # # Default value is `*`. - #allow_origins = [ - # "https://www.example.com", - # "http://sample.com" - #] - - # Specify `Access-Control-Allow-Headers` header values. - # Default values are `Accept`, `AcceptLanguage`, `Authorization`, `Origin`. - #allow_headers = [ - # "Accept", - # "Authorization", - # "Content-Type", - # "Origin" - #] - - # Specify `Access-Control-Allow-Methods` header values. - # Default values are `GET`, `POST`, `HEAD`. - #allow_methods = ["GET", "POST", "HEAD"] - - # Specify `Access-Control-Expose-Headers` header values. - # Default value - `cors.allow_headers` values are used if not provided. - #expose_headers = [ - # "X-Custom-Header1", - # "X-Custom-Header2" - #] - - # Specify `Access-Control-Max-Age` header value. - # Default value is `24h`. - #max_age = "48h" - - # Specify `Access-Control-Allow-Credentials` header value. - # Default value is `false`. - #allow_credentials = true - }{{ end }} - - {{ if eq .AppType "web" -}} - #---------------------------------------------------------------------------- - # Static Routes Configuration - # To serve static files, it can be directory or individual file. - # This section optional one, for e.g: RESTful APIs doesn't need this section. - # Static files are delivered via `http.ServeContent`. - # - # Supported features: - # * Serve directory - # * Serve individual file - # * Directory listing - # - # Pick your choice of `unique name` for each `directory` or `individual` file - # static route definition. It is called `route name`. - # Doc: https://docs.aahframework.org/routes-config.html#section-static - #---------------------------------------------------------------------------- - static { - # Static route name, pick a unique one - public_assets { - # URL 'path' for serving directory - # Below definition means '/static/**' - path = "/static" - - # Relative to application base directory or an absolute path - dir = "static" - - # list directory, default is 'false' - #list = false - } - - # serving single file - favicon { - path = "/favicon.ico" - - # Direct file mapping, It can be relative to application base directory - # or an absolute path. For relative path, it uses below `base_dir` config value. - file = "img/favicon.ico" - - # Default value for relative path file mapping is `public_assets.dir` - #base_dir = "assets" - } - - # Robots Configuration file. - # Know more: https://en.wikipedia.org/wiki/Robots_exclusion_standard - robots_txt { - path = "/robots.txt" - file = "robots.txt" - } - }{{ end }} - - #----------------------------------------------------------------------------- - # Application routes - # Doc: https://docs.aahframework.org/routes-config.html#section-routes - # Doc: https://docs.aahframework.org/routes-config.html#namespace-group-routes - #----------------------------------------------------------------------------- - routes { - - #------------------------------------------------------ - # Pick an unique name, it's called `route name`, - # used for reverse URL. - #------------------------------------------------------ - index { - # path is used to match incoming requests - # It can contain `:name` - Named parameter and - # `*name` - Catch-all parameter - path = "/" - - # HTTP method mapping, It can be multiple `HTTP` methods with comma separated - # Default value is `GET`, it can be lowercase or uppercase - #method = "GET" - - # The controller to be called for mapped URL path. - # * `controller` attribute supports with or without package prefix. For e.g.: `v1/User` or `User` - # * `controller` attribute supports both naming conventions. For e.g.: `User` or `UserController` - controller = "AppController" - - # The action/method name in the controller to be called for mapped URL path. - # Default values are mapped based on `HTTP` method. Refer doc for more info. - # Default action value for GET is 'Index'. - #action = "Index" - - # Auth config attribute is used to assign auth scheme for the route. - # If you do not this attribute then framework acquire value as follows. - # - # - Inherits the parent route `auth` attribute value if present. - # - Inherits the `default_auth` attribute config value if defined. - # - Otherwise it becomes not defined. - # - # When routes auth attribute is not defined; two possible actions are taken: - # - If one or more auth schemes are defined in security.auth_schemes { ... } - # and routes auth attribute is not defined then framework treats that route as 403 Forbidden. - # - Else framework treats that route as anonymous. - # - # When you want to define particular route as anonymous then define - # `auth` attribute as `anonymous`. - # Default value is empty string. - auth = "anonymous" - - # Max request body size for this route. If its happen to be `MultipartForm` - # then this value ignored since `request.multipart_size` config from `aah.conf` - # is applied. - # - # If this value is not provided then global `request.max_body_size` config - # from `aah.conf` is applied. So use it for specific cases. - # No default value, global value is applied. - #max_body_size = "5mb" - - # Optionally you can disable Anti-CSRF check for particular route. - # There are cases you might need this option. In-general don't disable the check. - # Default value is `true`. - #anti_csrf_check = false - } - - {{ if eq .AppAuthScheme "form" -}}login_submit { - path ="/login" - method = "POST" - controller = "VirtualFormController" - }{{ end }} - - } # end - routes - - } # end - localhost - -} # end - domains diff --git a/aah/app-template/config/security.conf.atmpl b/aah/app-template/config/security.conf.atmpl deleted file mode 100644 index f8a4dba..0000000 --- a/aah/app-template/config/security.conf.atmpl +++ /dev/null @@ -1,509 +0,0 @@ -###################################################### -# {{ .AppName }} - Application Security Configuration -# -# Complete routes configuration reference: -# https://docs.aahframework.org/security-config.html -###################################################### - -security { - # ------------------------------------------------------- - # Authentication & Authorization configuration - # Doc: https://docs.aahframework.org/security-design.html - # ------------------------------------------------------- - auth_schemes { - {{ if eq .AppAuthScheme "form" -}} - # HTTP Form Auth Scheme - # It is custom defined name, this is used in the routes `auth` attribute. - form_auth { - # Auth scheme name. - # Currently supported values are `form`, `basic` and `generic`. - # It is required value, no default. - scheme = "form" - - # Framework calls `Authenticator` to get the Subject's authentication - # information. Then framework validates the credential using password - # encoder. - # It is required value, no default. - authenticator = "security/AuthenticationProvider" - - # Framework calls `Authorizer` to get Subject's authorization information, - # such as Roles, Permissions. Then it populates the Subject instance. - # It is required value, no default. - authorizer = "security/AuthorizationProvider" - - # Password encoder is used to encode the given credential and then compares - # it with application provide credential. - # Doc: https://docs.aahframework.org/password-encoders.html - # Default value is `bcrypt`. - password_encoder = "{{ .AppPasswordEncoder }}" - - # Field names are used to extract `AuthenticationToken` from request. - field { - # Default value is `username` - #identity = "username" - - # Default value is `password` - #credential = "password" - } - - # URLs is used when appropriate. - url { - # Login page URL, implement your login and configure here. - # Default value is `/login.html`. - #login = "/login.html" - - # Login submit URL, used to submit login credential for authentication. - # Basically login form submits its values to this URL as POST request. - # Default value is `/login`. - #login_submit = "/login" - - # Login failure, any failure during authentication process. Framework - # sends user to this URL and logs detailed information in the log. - #login_failure = "/login.html?error=true" - - # Default page URL after the successful authentication. By default framework - # redirects the user to requested page after authentication. Otherwise it - # sends to this URL. - # Default value is `/`. - #default_target = "/" - - # Always redirect to default URL, regardless of the request page. - # Default value is `false`. - #always_to_default = false - } - }{{ end -}} - - {{ if eq .AppAuthScheme "basic" -}} - # HTTP Basic Auth Scheme - # It is custom defined name, this is used in the routes `auth` attribute. - basic_auth { - # Auth scheme name. - # Currently supported values are `form`, `basic` and `generic`. - # It is required value, no default. - scheme = "basic" - - # Realm name is used for `Www-Authenticate` HTTP header. - # Note: Nowadays, modern browsers are not respecting this values. - # However aah framework does its due diligence. - realm_name = "Protected" - - {{ if eq .AppBasicAuthMode "file-realm" -}} - # Basic auth realm file path. You can use absolute path or - # environment variable to provide path. - # It is required value, no default. - file_realm = "{{ .AppBasicAuthFileRealmPath }}"{{ else -}} - # Framework calls `Authenticator` to get the Subject's authentication - # information. Then framework validates the credential using password - # encoder. - # It is required value when `file_realm` not configured, no default. - authenticator = "security/AuthenticationProvider" - - # Framework calls `Authorizer` to get Subject's authorization information, - # such as Roles and Permissions. Then it populates the Subject instance. - # It is required value when `file_realm` not configured, no default. - authorizer = "security/AuthorizationProvider"{{ end }} - - # Password encoder is used to encode the given credential and then compares - # it with application provide credential. - # Doc: https://docs.aahframework.org/password-encoders.html - # Default value is `bcrypt`. - password_encoder = "{{ .AppPasswordEncoder }}" - }{{ end -}} - - {{ if eq .AppAuthScheme "generic" -}} - # Generic Auth Scheme - # It is custom defined name, this is used in the routes `auth` attribute. - generic_auth { - # Auth scheme name. - # Currently supported values are `form`, `basic` and `generic`. - # It is required value, no default. - scheme = "generic" - - # Framework calls `Authenticator` to get the Subject's authentication - # information. The credential validation is not done by framework, it is - # left to interface implementation. - # It is required value, no default. - authenticator = "security/AuthenticationProvider" - - # Framework calls `Authorizer` to get Subject's authorization information, - # such as Roles and Permissions. Then it populates the Subject instance. - # It is required value, no default. - authorizer = "security/AuthorizationProvider" - - # Header names are used to extract `AuthenticationToken` from request. - header { - # Default value is 'Authorization' - #identity = "Authorization" - - # Optional credential header - # Typically it's not used, however in the industry people do use it - # Default value is empty string - #credential = "X-AuthPass" - } - }{{ end }} - } - - # ------------------------------------------------------------ - # Password Encoders Configuration - # aah supports `bcrypt`, `scrypt`, `pbkdf2` password algorithm - # Doc: https://docs.aahframework.org/password-encoders.html - # ------------------------------------------------------------ - password_encoder { {{ if eq .AppPasswordEncoder "bcrypt" }} - # bcrypt algorithm - # - # Learn more: - # https://crackstation.net/hashing-security.htm - # https://security.stackexchange.com/a/6415 - # https://en.wikipedia.org/wiki/Bcrypt - bcrypt { - # Default value is `true` - enable = true - - # https://godoc.org/golang.org/x/crypto/bcrypt#pkg-constants - # Default value is `12`. - cost = 12 - }{{ end }} - {{ if eq .AppPasswordEncoder "scrypt" }} - # scrypt algorithm - # - # Learn more: - # https://crackstation.net/hashing-security.htm - # https://pthree.org/2016/06/28/lets-talk-password-hashing/ - # https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet - # Default values are chosen carefully to provide secure password. - scrypt { - # Default value is `false` - enable = true - - # CPU/Memory Cost - # Default value is `2^15` - #cpu_memory_cost = 32768 - - # Default value is `8` - #block_size = 8 - - # Default value is `1` - #parallelization = 1 - - # Default value is `32` - #derived_key_length = 32 - - # Default value is `24` - #salt_length = 24 - }{{ end }} - {{ if eq .AppPasswordEncoder "pbkdf2" }} - # pbkdf2 algorithm - # - # Learn more: - # https://crackstation.net/hashing-security.htm - # https://cryptosense.com/parameter-choice-for-pbkdf2/ - # https://pthree.org/2016/06/28/lets-talk-password-hashing/ - # https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet - # Default values are chosen carefully to provide secure password. - pbkdf2 { - # Default value is `false` - enable = true - - # Default value is `10000` - #iteration = 10000 - - # Default value is `32` - #derived_key_length = 32 - - # Default value is `24` - #salt_length = 24 - - # Supported SHA's are `sha-1`, `sha-224`, `sha-256`, `sha-384`, `sha-512`. - # Default value is `sha-512` - #hash_algorithm = "sha-512" - }{{ end }} - } - - {{ if eq .AppType "web" }}{{ if or (eq .AppAuthScheme "form") (eq .AppAuthScheme "basic") -}} - # ----------------------------------------------------------------------- - # Session configuration - # HTTP state management across multiple requests. - # Doc: https://docs.aahframework.org/security-config.html#section-session - # ----------------------------------------------------------------------- - session { - # Session mode to choose whether HTTP session should be persisted or - # destroyed at the end of the request. Supported values are `stateless` - # and `stateful`. - # Default value is `stateless` for API and `stateful` for Web app. - mode = "{{ if eq .AppAuthScheme "" }}stateless{{ else }}stateful{{ end }}" - - # Session store is to choose where session value should be persisted. - store { - # Currently aah framework supports `cookie` and `file` as store type. - # Also framework provide extensible `session.Storer` interface to - # add custom session store. - # Default value is `cookie`. - type = "{{ .AppSessionStore }}" - {{ if eq .AppSessionStore "file" -}} - # Filepath is used for file store to store session file in the file system. - # This is only applicable for `type = "file"`, make sure application has - # Read/Write access to the directory. Provide absolute path. - filepath = "{{ .AppSessionFileStorePath }}"{{ end }} - } - - # Session ID length - # Default value is `32`. - #id_length = 32 - - # Time-to-live for session data. Valid time units are "m = minutes", - # "h = hours" and 0. - # Default value is `0`, cookie is deleted when the browser is closed. - #ttl = "0" - - # Session cookie name prefix. - # Default value is `aah_` For e.g.: `aah_myapp_session` - prefix = "aah_{{ .AppName }}" - - # Default value is `empty` string. - #domain = "" - - # Default value is `/`. - #path = "/" - - # HTTP session cookie HTTPOnly value. This option prevents XSS - # (Cross Site Scripting) attacks. - # Default value is `true`. - #http_only = true - - # HTTP session cookie secure value. - # However if aah server is not configured with SSL then - # framework sets this value as false - # Default value is `true`. - #secure = true - - # HTTP session cookie value signing using `HMAC`. For server farm this - # should be same in all instance. For HMAC sign & verify it recommend to use - # key size is `32` or `64` bytes. - # Default value is `64` bytes (`aah new` generates strong one). - sign_key = "{{ .AppSessionSignKey }}" - - # HTTP session cookie value encryption and decryption using `AES`. For server - # farm this should be same in all instance. AES algorithm is used, valid - # lengths are `16`, `24`, or `32` bytes to select `AES-128`, `AES-192`, or `AES-256`. - # Default value is `32` bytes (`aah new` generates strong one). - enc_key = "{{ .AppSessionEncKey }}" - - # Cleanup Interval is used to clean the expired session objects from store. - # This is only applicable for non-cookie store type. - # Cleanup performed in dedicated goroutine. Valid time units are - # `m -> minutes`, `h -> hours`. - # Default value is `30m`. - #cleanup_interval = "30m" - }{{ end }}{{ end -}} - - {{ if eq .AppType "web" -}} - # ------------------------------------------------------------ - # Anti-CSRF Protection - # Doc: https://docs.aahframework.org/anti-csrf-protection.html - # ------------------------------------------------------------ - anti_csrf { - # Enabling Anti-CSRF Protection. - # Default value is `true`. - #enable = true - - # Anti-CSRF secret length - # Default value is `32`. - #secret_length = 32 - - # HTTP Header name for cipher token - # Default value is `X-Anti-CSRF-Token`. - #header_name = "X-Anti-CSRF-Token" - - # Form field name for cipher token - # Default value is `anti_csrf_token`. - #form_field_name = "anti_csrf_token" - - #Anti-CSRF secure cookie prefix - # Default value is `aah`. Cookie name would be `aah_anti_csrf`. - #prefix = "aah" - - # Default value is `empty` string. - #domain = "" - - # Default value is `/`. - #path = "/" - - # Time-to-live for Anti-CSRF secret. Valid time units are "m = minutes", - # "h = hours" and 0. - # Default value is `24h`. - #ttl = "24h" - - # Anti-CSRF cookie value signing using `HMAC`. For server farm this - # should be same in all instance. For HMAC sign & verify it recommend to use - # key size is `32` or `64` bytes. - # Default value is `64` bytes (`aah new` generates strong one). - sign_key = "{{ .AppAntiCSRFSignKey }}" - - # Anti-CSRF cookie value encryption and decryption using `AES`. For server - # farm this should be same in all instance. AES algorithm is used, valid - # lengths are `16`, `24`, or `32` bytes to select `AES-128`, `AES-192`, or `AES-256`. - # Default value is `32` bytes (`aah new` generates strong one). - enc_key = "{{ .AppAntiCSRFEncKey }}" - }{{ end }} - - # --------------------------------------------------------------------------- - # HTTP Secure Header(s) - # Application security headers with many safe defaults. - # Doc: https://docs.aahframework.org/security-config.html#section-http-header - # - # Tip: Quick way to verify secure headers - https://securityheaders.io - # --------------------------------------------------------------------------- - http_header { - # X-XSS-Protection - # Designed to enable the cross-site scripting (XSS) filter built into modern - # web browsers. This is usually enabled by default, but using this header - # will enforce it. - # - # Learn more: - # https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#xxxsp - # https://www.keycdn.com/blog/x-xss-protection/ - # - # Encouraged to make use of header `Content-Security-Policy` with enhanced - # policy to reduce XSS risk along with header `X-XSS-Protection`. - # Default values is `1; mode=block`. - {{ if eq .AppType "web" -}}#xxssp = "1; mode=block"{{ else }}xxssp = ""{{ end }} - - # X-Content-Type-Options - # Prevent Content Sniffing or MIME sniffing. - # - # Learn more: - # https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#xcto - # https://en.wikipedia.org/wiki/Content_sniffing - # Default value is `nosniff`. - #xcto = "nosniff" - - # X-Frame-Options - # Prevents Clickjacking. - # - # Learn more: - # https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#xfo - # https://www.keycdn.com/blog/x-frame-options/ - # Default value is `SAMEORIGIN`. - {{ if eq .AppType "web" -}}#xfo = "SAMEORIGIN"{{ else }}xfo = "DENY"{{ end }} - - # Referrer-Policy - # This header governs which referrer information, sent in the Referer header, should - # be included with requests made. - # Referrer Policy has been a W3C Candidate Recommendation since 26 January 2017. - # - # Learn more: - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy - # https://scotthelme.co.uk/a-new-security-header-referrer-policy/ - # https://www.w3.org/TR/referrer-policy/ - # Default value is `no-referrer-when-downgrade`. - {{ if eq .AppType "web" -}}#rp = "no-referrer-when-downgrade"{{ else }}rp = ""{{ end }} - - # Strict-Transport-Security (STS, aka HSTS) - # STS header that lets a web site tell browsers that it should only be communicated - # with using HTTPS, instead of using HTTP. - # - # Learn more: - # https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#hsts - # https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security - # - # Note: Framework checks that application uses SSL on startup then applies - # this header. Otherwise it does not apply. - sts { - # The time, in seconds, that the browser should remember that this site - # is only to be accessed using HTTPS. Valid time units are - # "s -> seconds", "m -> minutes", "h - hours". - # Default value is `30 days` in hours. - #max_age = "720h" - - # If enabled the STS rule applies to all of the site's subdomains as well. - # Default value is `false`. - #include_subdomains = true - - # Before enabling preload option, please read about pros and cons from above links. - # Default value is `false`. - #preload = false - } - - # Content-Security-Policy (CSP) - # Provides a rich set of policy directives that enable fairly granular control - # over the resources that a page is allowed. Prevents XSS risks. - # - # Learn more: - # https://content-security-policy.com/ - # https://developers.google.com/web/fundamentals/security/csp/ - # https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#csp - # - # Read above references and define your policy. - # - # Note: It is highly recommended to verify your policy directives in report - # only mode before enabling this header. Since its highly controls how your - # page is rendered. - # - # No default values, you have to provide it. - csp { - # Set of directives to govern the resources load on a page. - #directives = "" - - # By default, violation reports aren't sent. To enable violation reporting, - # you need to specify the report-uri policy directive. - report_uri = "" - - # Puts your `Content-Security-Policy` in report only mode, so that you can verify - # and then set `csp_report_only` value to false. - # Don't forget to set the `report-uri` for validation. - report_only = true - } - - # Public-Key-Pins PKP (aka HPKP) - # This header prevents the Man-in-the-Middle Attack (MITM) with forged certificates. - # - # Learn more: - # https://scotthelme.co.uk/hpkp-http-public-key-pinning/ - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning - # Read above references and define your keys. - # - # Note: - # - HPKP has the potential to lock out users for a long time if used incorrectly! - # The use of backup certificates and/or pinning the CA certificate is recommended. - # - It is highly recommended to verify your policy directives in report only mode - # before enabling this header - # - It is highly recommended to verify your PKP in report only mode before enabling this header. - # No default values, you have to provide it. - pkp { - # The Base64 encoded Subject Public Key Information (SPKI) fingerprint. - # These values gets added as `pin-sha256=; ...`. - #keys = [ - #"X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg=", - #"MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec=" - #] - - # The time that the browser should remember that this site is only to be - # accessed using one of the defined keys. - # Valid time units are "s -> seconds", "m -> minutes", "h - hours". - max_age = "720h" - - # If enabled the PKP keys applies to all of the site's subdomains as well. - # Default value is `false`. - include_subdomains = false - - # By default, Pin validation failure reports aren't sent. To enable Pin validation - # failure reporting, you need to specify the report-uri. - report_uri = "" - - # Puts your `Public-Key-Pins` in report only mode, so that you can verify - # and then set `pkp_report_only` value to false. - # Don't forget to set the `report-uri` for validation. - report_only = true - } - - # X-Permitted-Cross-Domain-Policies - # Restrict Adobe Flash Player's or PDF documents access via crossdomain.xml, - # and this header. - # - # Learn more: - # https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#xpcdp - # https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html - # Default value is `master-only`. - {{ if eq .AppType "web" -}}#xpcdp = "master-only"{{ else }}xpcdp = ""{{ end }} - } -} diff --git a/aah/app-template/i18n/messages.en.atmpl b/aah/app-template/i18n/messages.en.atmpl deleted file mode 100644 index 6a82d11..0000000 --- a/aah/app-template/i18n/messages.en.atmpl +++ /dev/null @@ -1,18 +0,0 @@ -############################################# -# i18n messages for {{.AppName}} application -# -# Complete configuration reference: -# https://docs.aahframework.org/i18n.html -############################################# - -# This structure is example purpose -# So choose your suitable structure for your application -label { - pages { - app { - index { - title = "aah framework application - Home" - } - } - } -} diff --git a/aah/app-template/static/css/aah.css b/aah/app-template/static/css/aah.css deleted file mode 100644 index fd463e9..0000000 --- a/aah/app-template/static/css/aah.css +++ /dev/null @@ -1,47 +0,0 @@ -/* -Minimal aah framework application template CSS. -Based on your need choose your CSS framework. -*/ - -html { - font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; -} - -body { - margin: 0; -} - -.container { - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; -} -@media (min-width: 992px) { - .container { - width: 970px; - } -} -@media (min-width: 1200px) { - .container { - width: 1170px; - } -} - -.row { - margin-right: -15px; - margin-left: -15px; -} - -.text-center { - text-align: center; -} - -.welcome-msg { - padding-top: 30px; - padding-bottom: 30px; - margin-bottom: 30px; - color: inherit; -} diff --git a/aah/app-template/static/img/aah-framework-logo.png b/aah/app-template/static/img/aah-framework-logo.png deleted file mode 100644 index 4287e4a..0000000 Binary files a/aah/app-template/static/img/aah-framework-logo.png and /dev/null differ diff --git a/aah/app-template/static/img/favicon.ico b/aah/app-template/static/img/favicon.ico deleted file mode 100644 index 8cc419e..0000000 Binary files a/aah/app-template/static/img/favicon.ico and /dev/null differ diff --git a/aah/app-template/static/js/aah.js b/aah/app-template/static/js/aah.js deleted file mode 100644 index e69de29..0000000 diff --git a/aah/app-template/static/robots.txt b/aah/app-template/static/robots.txt deleted file mode 100644 index 9d0006b..0000000 --- a/aah/app-template/static/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Prevents all robots visiting your site. -User-agent: * -Disallow: / diff --git a/aah/app-template/views/go/common/error_footer.html b/aah/app-template/views/go/common/error_footer.html deleted file mode 100644 index 8b13789..0000000 --- a/aah/app-template/views/go/common/error_footer.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/aah/app-template/views/go/common/error_header.html b/aah/app-template/views/go/common/error_header.html deleted file mode 100644 index ae05058..0000000 --- a/aah/app-template/views/go/common/error_header.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - {{ .Error.Code }} {{ .Error.Message }} - - - - diff --git a/aah/app-template/views/go/common/footer_scripts.html b/aah/app-template/views/go/common/footer_scripts.html deleted file mode 100644 index ce1fb71..0000000 --- a/aah/app-template/views/go/common/footer_scripts.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/aah/app-template/views/go/common/head_tags.html b/aah/app-template/views/go/common/head_tags.html deleted file mode 100644 index db19154..0000000 --- a/aah/app-template/views/go/common/head_tags.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/aah/app-template/views/go/errors/404.html b/aah/app-template/views/go/errors/404.html deleted file mode 100644 index 05174a7..0000000 --- a/aah/app-template/views/go/errors/404.html +++ /dev/null @@ -1,14 +0,0 @@ - - - {{ import "error_header.html" . -}} - -
{{ with .Error }} -
-
- {{ .Code }} {{ .Message }} -
-
{{ end }} -
- {{ import "error_footer.html" . -}} - - diff --git a/aah/app-template/views/go/errors/500.html b/aah/app-template/views/go/errors/500.html deleted file mode 100644 index 6bd0250..0000000 --- a/aah/app-template/views/go/errors/500.html +++ /dev/null @@ -1,14 +0,0 @@ - - -{{ import "error_header.html" . -}} - -
{{ with .Error }} -
-
- {{ .Code }} {{ .Message }} -
-
{{ end }} -
- {{ import "error_footer.html" . -}} - - diff --git a/aah/app-template/views/go/layouts/master.html b/aah/app-template/views/go/layouts/master.html deleted file mode 100644 index 9935987..0000000 --- a/aah/app-template/views/go/layouts/master.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - {{ template "title" . }} - {{ import "head_tags.html" . }} - - - {{ template "body" . -}} - {{ import "footer_scripts.html" . }} - - diff --git a/aah/app-template/views/go/pages/app/index.html b/aah/app-template/views/go/pages/app/index.html deleted file mode 100644 index cbe24ca..0000000 --- a/aah/app-template/views/go/pages/app/index.html +++ /dev/null @@ -1,12 +0,0 @@ -{{ define "title" -}} -{{ i18n . "label.pages.app.index.title" }} -{{- end }} - -{{ define "body" -}} -
-
- aah framework logo -

{{ .Greet.Message }}

-
-
-{{- end }} diff --git a/aah/app-template/views/pug/common/error_footer.pug b/aah/app-template/views/pug/common/error_footer.pug deleted file mode 100644 index e69de29..0000000 diff --git a/aah/app-template/views/pug/common/error_header.pug b/aah/app-template/views/pug/common/error_header.pug deleted file mode 100644 index bd1cc65..0000000 --- a/aah/app-template/views/pug/common/error_header.pug +++ /dev/null @@ -1,33 +0,0 @@ -//- error_header.pug -head - meta(charset='utf-8') - meta(http-equiv='X-UA-Compatible', content='IE=edge') - meta(name='viewport', content='width=device-width, initial-scale=1') - title {{ .Error.Code }} {{ .Error.Message }} - link(rel='icon', type='image/x-icon', href='/favicon.ico') - link(href='//fonts.googleapis.com/css?family=Open+Sans:300,400,700', rel='stylesheet', type='text/css') - style. - html {-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%} - html, body { - margin: 0; - background-color: #fff; - color: #636b6f; - font-family: 'Open Sans', sans-serif; - font-weight: 100; - height: 80vh; - } - .container { - align-items: center; - display: flex; - justify-content: center; - position: relative; - height: 80vh; - } - .content { - text-align: center; - } - .title { - font-size: 36px; - font-weight: bold; - padding: 20px; - } diff --git a/aah/app-template/views/pug/common/footer_scripts.pug b/aah/app-template/views/pug/common/footer_scripts.pug deleted file mode 100644 index 3246165..0000000 --- a/aah/app-template/views/pug/common/footer_scripts.pug +++ /dev/null @@ -1,2 +0,0 @@ -//- footer.pug -script(src='/static/js/aah.js') diff --git a/aah/app-template/views/pug/common/head_tags.pug b/aah/app-template/views/pug/common/head_tags.pug deleted file mode 100644 index 7d8641e..0000000 --- a/aah/app-template/views/pug/common/head_tags.pug +++ /dev/null @@ -1,5 +0,0 @@ -//- head_tags.pug, block keyword added for processing purpose. -//- jade library doesn't process this file. -block head - link(rel='icon', type='image/x-icon', href='/favicon.ico') - link(href='/static/css/aah.css', rel='stylesheet') diff --git a/aah/app-template/views/pug/errors/404.pug b/aah/app-template/views/pug/errors/404.pug deleted file mode 100644 index cc35901..0000000 --- a/aah/app-template/views/pug/errors/404.pug +++ /dev/null @@ -1,12 +0,0 @@ -//- 404.pug -doctype html -html - include "error_header.pug" . -body - .container - | {{ with .Error }} - .content - .title - | {{ .Code }} {{ .Message }} - | {{ end }} - include "error_footer.pug" . diff --git a/aah/app-template/views/pug/errors/500.pug b/aah/app-template/views/pug/errors/500.pug deleted file mode 100644 index 20e822b..0000000 --- a/aah/app-template/views/pug/errors/500.pug +++ /dev/null @@ -1,12 +0,0 @@ -//- 500.pug -doctype html -html - include "error_header.pug" . -body - .container - | {{ with .Error }} - .content - .title - | {{ .Code }} {{ .Message }} - | {{ end }} - include "error_footer.pug" . diff --git a/aah/app-template/views/pug/layouts/master.pug b/aah/app-template/views/pug/layouts/master.pug deleted file mode 100644 index 55dca52..0000000 --- a/aah/app-template/views/pug/layouts/master.pug +++ /dev/null @@ -1,12 +0,0 @@ -//- master.pug -doctype html -html - head - meta(charset='utf-8') - meta(http-equiv='X-UA-Compatible', content='IE=edge') - meta(name='viewport', content='width=device-width, initial-scale=1, viewport-fit=cover') - block title - include "head_tags.pug" . - body - block content - include "footer_scripts.pug" . diff --git a/aah/app-template/views/pug/pages/app/index.pug b/aah/app-template/views/pug/pages/app/index.pug deleted file mode 100644 index b95aa3e..0000000 --- a/aah/app-template/views/pug/pages/app/index.pug +++ /dev/null @@ -1,10 +0,0 @@ -//- pages/app/index.pug - -block title - title {{ i18n . "label.pages.app.index.title" }} - -block content - .container - .row.text-center.welcome-msg - img(src='/static/img/aah-framework-logo.png', alt='aah framework logo') - h1 {{ .Greet.Message }} diff --git a/aah/app_tmpl.go b/aah/app_tmpl.go new file mode 100644 index 0000000..0e4e940 --- /dev/null +++ b/aah/app_tmpl.go @@ -0,0 +1,119 @@ +package main + +import ( + "strings" + "text/template" + + "aahframework.org/essentials.v0" +) + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// appTmplData and its methods +//______________________________________________________________________________ + +const ( + typeWeb = "web" + typeAPI = "api" + typeWebSocket = "websocket" + storeCookie = "cookie" + storeFile = "file" + aahTmplExt = ".atmpl" + authForm = "form" + authBasic = "basic" + authOAuth2 = "oauth2" + authGeneric = "generic" + authNone = "none" + basicFileRealm = "file-realm" +) + +// appTmplData struct holds inputs collected from user for new aah creation +type appTmplData struct { + Name string + Type string + ImportPath string + BaseDir string + ViewEngine string + ViewFileExt string + AuthScheme string + BasicAuthMode string + PasswordEncoderAlgo string + SessionStore string + SessionFileStorePath string + BasicAuthFileRealmPath string + CORSEnable bool + TmplDelimLeft string + TmplDelimRight string + SubTypes []string +} + +func (a *appTmplData) IsWebApp() bool { + return a.Type == typeWeb +} + +func (a *appTmplData) IsAPIApp() bool { + return a.Type == typeAPI +} + +func (a *appTmplData) IsWebSocketApp() bool { + return a.Type == typeWebSocket +} + +func (a *appTmplData) DomainNameKey() string { + return strings.Replace(strings.Replace(a.Name, " ", "_", -1), "-", "_", -1) +} + +func (a *appTmplData) IsAuthSchemeForWeb() bool { + return a.Type == typeWeb && (a.AuthScheme == authForm || a.AuthScheme == authBasic) +} + +func (a *appTmplData) IsAuthSchemeForAPI() bool { + return a.Type == typeAPI && (a.AuthScheme == authGeneric || a.AuthScheme == authBasic) +} + +func (a *appTmplData) IsSecurityEnabled() bool { + return !ess.IsStrEmpty(a.AuthScheme) +} + +func (a *appTmplData) IsSubTypeAPI() bool { + return a.checkSubType(typeAPI) +} + +func (a *appTmplData) IsSubTypeWebSocket() bool { + return a.checkSubType(typeWebSocket) +} + +func (a *appTmplData) IsSessionConfigRequired() bool { + return a.AuthScheme == authForm || a.AuthScheme == authOAuth2 || a.AuthScheme == authBasic +} + +func (a *appTmplData) IsAuth(name string) bool { + return strings.Contains(a.AuthScheme, name) +} + +func (a *appTmplData) checkSubType(t string) bool { + for _, v := range a.SubTypes { + if v == t { + return true + } + } + return false +} + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Template funcs +//______________________________________________________________________________ + +var vreplace = strings.NewReplacer("_auth", "", "auth_", "") + +var appTemplateFuncs = template.FuncMap{ + "securerandomstring": func(length int) string { + return ess.SecureRandomString(length) + }, + "variablename": func(v string) string { + return toLowerCamelCase(vreplace.Replace(v)) + }, + "isauth": func(args map[string]interface{}, name string) bool { + app := args["App"].(*appTmplData) + return app.IsAuth(name) + }, +} diff --git a/aah/ast.go b/aah/ast.go deleted file mode 100644 index 18c834b..0000000 --- a/aah/ast.go +++ /dev/null @@ -1,621 +0,0 @@ -// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) -// go-aah/tools/aah source code and usage is governed by a MIT style -// license that can be found in the LICENSE file. - -package main - -import ( - "errors" - "fmt" - "go/ast" - "go/build" - "go/parser" - "go/scanner" - "go/token" - "os" - "path/filepath" - "strings" - - "aahframework.org/essentials.v0" -) - -var ( - buildImportCache = map[string]string{} - - // Reference: https://golang.org/pkg/builtin/ - builtInDataTypes = map[string]bool{ - "bool": true, - "byte": true, - "complex128": true, - "complex64": true, - "error": true, - "float32": true, - "float64": true, - "int": true, - "int16": true, - "int32": true, - "int64": true, - "int8": true, - "rune": true, - "string": true, - "uint": true, - "uint16": true, - "uint32": true, - "uint64": true, - "uint8": true, - "uintptr": true, - } - - errInvalidActionParam = errors.New("aah: invalid action parameter") - errInterfaceActionParam = errors.New("aah: 'interface{}' is not supported in the action parameter") - errMapActionParam = errors.New("aah: 'map' is not supported in the action parameter") -) - -type ( - // Program holds all details loaded from the Go source code for given Path. - program struct { - Path string - Packages []*packageInfo - RegisteredActions map[string]map[string]uint8 - } - - // PackageInfo holds the single paackge information. - packageInfo struct { - Fset *token.FileSet - Pkg *ast.Package - Types map[string]*typeInfo - ImportPath string - FilePath string - Files []string - } - - // TypeInfo holds the information about Controller Name, Methods, - // Embedded types etc. - typeInfo struct { - Name string - ImportPath string - Methods []*methodInfo - EmbeddedTypes []*typeInfo - } - - // MethodInfo holds the information of single method and it's Parameters. - methodInfo struct { - Name string - StructName string - Parameters []*parameterInfo - } - - // ParameterInfo holds the information of single Parameter in the method. - parameterInfo struct { - Name string - ImportPath string - Type *typeExpr - } - - // TypeExpr holds the information of single parameter data type. - typeExpr struct { - Expr string - IsBuiltIn bool - PackageName string - ImportPath string - PackageIndex uint8 - Valid bool - } -) - -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Package methods -//___________________________________ - -// LoadProgram method loads the Go source code for the given directory. -func loadProgram(path string, excludes ess.Excludes, registeredActions map[string]map[string]uint8) (*program, []error) { - if err := validateInput(path); err != nil { - return nil, append([]error{}, err) - } - - prg := &program{ - Path: path, - Packages: []*packageInfo{}, - RegisteredActions: registeredActions, - } - - var ( - pkgs map[string]*ast.Package - errs []error - ) - - err := ess.Walk(path, func(srcPath string, info os.FileInfo, err error) error { - if err != nil { - errs = append(errs, err) - } - - // Excludes - if excludes.Match(filepath.Base(srcPath)) { - if info.IsDir() { - return filepath.SkipDir - } - - return nil - } - - if !info.IsDir() { - return nil - } - - if info.IsDir() && ess.IsDirEmpty(srcPath) { - // skip directory if it's empty - return filepath.SkipDir - } - - pfset := token.NewFileSet() - pkgs, err = parser.ParseDir(pfset, srcPath, func(f os.FileInfo) bool { - return !f.IsDir() && !excludes.Match(f.Name()) - }, 0) - - if err != nil { - if errList, ok := err.(scanner.ErrorList); ok { - // TODO parsing error list - fmt.Println(errList) - } - - errs = append(errs, fmt.Errorf("error parsing dir[%s]: %s", srcPath, err)) - return nil - } - - pkg, err := validateAndGetPkg(pkgs, srcPath) - if err != nil { - errs = append(errs, err) - return nil - } - - if pkg != nil { - pkg.Fset = pfset - pkg.FilePath = srcPath - pkg.ImportPath = stripGoPath(srcPath) - prg.Packages = append(prg.Packages, pkg) - } - - return nil - }) - - if err != nil { - errs = append(errs, err) - } - - return prg, errs -} - -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Program methods -//___________________________________ - -// Process method processes all packages in the program for `Type`, -// `Embedded Type`, `Method`, etc. -func (prg *program) Process() { - for _, pkgInfo := range prg.Packages { - pkgInfo.Types = map[string]*typeInfo{} - - // Each source file - for name, file := range pkgInfo.Pkg.Files { - pkgInfo.Files = append(pkgInfo.Files, filepath.Base(name)) - fileImports := make(map[string]string) - - for _, decl := range file.Decls { - // Processing imports - pkgInfo.processImports(decl, fileImports) - - // Processing types - pkgInfo.processTypes(decl, fileImports) - - // Processing methods - processMethods(pkgInfo, prg.RegisteredActions, decl, fileImports) - } - } - } -} - -// FindTypeByEmbeddedType method returns all the typeInfo that has directly or -// indirectly embedded by given type name. Type name must be fully qualified -// type name. E.g.: aahframework.org/aah.Controller -func (prg *program) FindTypeByEmbeddedType(qualifiedTypeName string) []*typeInfo { - var ( - queue = []string{qualifiedTypeName} - processed []string - result []*typeInfo - ) - - for len(queue) > 0 { - typeName := queue[0] - queue = queue[1:] - processed = append(processed, typeName) - - // search within all packages in the program - for _, p := range prg.Packages { - // search within all struct type in the package - for _, t := range p.Types { - // If this one has been processed or is already in queue, then move on. - if ess.IsSliceContainsString(processed, t.FullyQualifiedName()) || - ess.IsSliceContainsString(queue, t.FullyQualifiedName()) { - continue - } - - // search through the embedded types to see if the current type is among them. - for _, et := range t.EmbeddedTypes { - // If so, add this type's FullyQualifiedName into queue, - // and it's typeInfo into result. - if typeName == et.FullyQualifiedName() { - queue = append(queue, t.FullyQualifiedName()) - result = append(result, t) - break - } - } - } - } - } - - return result -} - -// CreateImportPaths method returns unique package alias with import path. -func (prg *program) CreateImportPaths(types []*typeInfo) map[string]string { - importPaths := map[string]string{} - for _, t := range types { - createAlias(t.PackageName(), t.ImportPath, importPaths) - for _, m := range t.Methods { - for _, p := range m.Parameters { - if !p.Type.IsBuiltIn { - createAlias(p.Type.PackageName, p.ImportPath, importPaths) - } - } - } - } - - return importPaths -} - -func createAlias(packageName, importPath string, importPaths map[string]string) { - importPath = filepath.ToSlash(importPath) - if _, found := importPaths[importPath]; !found { - cnt := 0 - pkgAlias := packageName - - for isPkgAliasExists(importPaths, pkgAlias) { - pkgAlias = fmt.Sprintf("%s%d", packageName, cnt) - cnt++ - } - - if !ess.IsStrEmpty(pkgAlias) && !ess.IsStrEmpty(importPath) { - importPaths[importPath] = pkgAlias - } - } -} - -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// PackageInfo methods -//___________________________________ - -// Name method return package name -func (p *packageInfo) Name() string { - return filepath.Base(p.ImportPath) -} - -func (p *packageInfo) processTypes(decl ast.Decl, imports map[string]string) { - genDecl, ok := decl.(*ast.GenDecl) - if !ok || !isTypeTok(genDecl) || len(genDecl.Specs) == 0 { - return - } - - spec := genDecl.Specs[0].(*ast.TypeSpec) - st, ok := spec.Type.(*ast.StructType) - if !ok { - // Not a struct type - return - } - - typeName := spec.Name.Name - ty := &typeInfo{ - Name: typeName, - ImportPath: filepath.ToSlash(p.ImportPath), - Methods: make([]*methodInfo, 0), - EmbeddedTypes: make([]*typeInfo, 0), - } - - for _, field := range st.Fields.List { - // If field.Names is set, it's not an embedded type. - if field.Names != nil && len(field.Names) > 0 { - continue - } - - fPkgName, fTypeName := parseStructFieldExpr(field.Type) - if ess.IsStrEmpty(fTypeName) { - continue - } - - // Find the import path for embedded type. If it was referenced without - // a package name, use the current package import path otherwise - // get the import path by package name. - var eTypeImportPath string - if ess.IsStrEmpty(fPkgName) { - eTypeImportPath = ty.ImportPath - } else { - var found bool - if eTypeImportPath, found = imports[fPkgName]; !found { - logErrorf("AST: Unable to find import path for %s.%s", fPkgName, fTypeName) - continue - } - } - - ty.EmbeddedTypes = append(ty.EmbeddedTypes, &typeInfo{Name: fTypeName, ImportPath: eTypeImportPath}) - } - - p.Types[typeName] = ty -} - -func (p *packageInfo) processImports(decl ast.Decl, imports map[string]string) { - genDecl, ok := decl.(*ast.GenDecl) - if !ok || !isImportTok(genDecl) { - return - } - - for _, dspec := range genDecl.Specs { - spec := dspec.(*ast.ImportSpec) - var pkgAlias string - if spec.Name != nil { - if spec.Name.Name == "_" { - continue - } - - pkgAlias = spec.Name.Name - } - - importPath := spec.Path.Value[1 : len(spec.Path.Value)-1] - if ess.IsStrEmpty(pkgAlias) { - if alias, found := buildImportCache[importPath]; found { - pkgAlias = alias - } else { // build cache - pkg, err := build.Import(importPath, p.FilePath, 0) - if err != nil { - logErrorf("AST: Unable to find import path: %s", importPath) - continue - } - pkgAlias = pkg.Name - buildImportCache[importPath] = pkg.Name - } - } - - imports[pkgAlias] = importPath - } -} - -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// TypeInfo methods -//___________________________________ - -// FullyQualifiedName method returns the fully qualified type name. -func (t *typeInfo) FullyQualifiedName() string { - return fmt.Sprintf("%s.%s", t.ImportPath, t.Name) -} - -// PackageName method returns types package name from import path. -func (t *typeInfo) PackageName() string { - return filepath.Base(t.ImportPath) -} - -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// TypeExpr methods -//___________________________________ - -// Name method returns type name for expression. -func (te *typeExpr) Name() string { - if te.IsBuiltIn || ess.IsStrEmpty(te.PackageName) { - return te.Expr - } - - return fmt.Sprintf("%s%s.%s", te.Expr[:te.PackageIndex], te.PackageName, te.Expr[te.PackageIndex:]) -} - -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Unexported methods -//___________________________________ - -func validateInput(path string) error { - if ess.IsStrEmpty(path) { - return errors.New("path is required input") - } - - if !ess.IsFileExists(path) { - return fmt.Errorf("path is does not exists: %s", path) - } - - return nil -} - -func validateAndGetPkg(pkgs map[string]*ast.Package, path string) (*packageInfo, error) { - pkgCnt := len(pkgs) - - // no source code found in the directory - if pkgCnt == 0 { - return nil, nil - } - - // not permitted by Go lang spec - if pkgCnt > 1 { - var names []string - for k := range pkgs { - names = append(names, k) - } - return nil, fmt.Errorf("more than one package name [%s] found in single"+ - " directory: %s", strings.Join(names, ", "), path) - } - - pkg := &packageInfo{} - for _, v := range pkgs { - pkg.Pkg = v - } - - return pkg, nil -} - -func isImportTok(decl *ast.GenDecl) bool { - return token.IMPORT == decl.Tok -} - -func isTypeTok(decl *ast.GenDecl) bool { - return token.TYPE == decl.Tok -} - -func stripGoPath(pkgFilePath string) string { - idx := strings.Index(pkgFilePath, "src") - return filepath.Clean(pkgFilePath[idx+4:]) -} - -func isPkgAliasExists(importPaths map[string]string, pkgAlias string) bool { - _, found := importPaths[pkgAlias] - return found -} - -func processMethods(pkg *packageInfo, routeMethods map[string]map[string]uint8, decl ast.Decl, imports map[string]string) { - fn, ok := decl.(*ast.FuncDecl) - - // Do not process if these met: - // 1. does not have receiver, it means package function/method - // 2. method is not exported - // 3. method returns result - if !ok || fn.Recv == nil || !fn.Name.IsExported() || - fn.Type.Results != nil { - return - } - - actionName := fn.Name.Name - if isInterceptorActioName(actionName) { - return - } - - controllerName := getName(fn.Recv.List[0].Type) - method := &methodInfo{Name: actionName, StructName: controllerName, Parameters: []*parameterInfo{}} - - // processed so set to level 2, used to display unimplemented action details - // TODO for controller check too - for k, v := range routeMethods { - if strings.HasSuffix(k, controllerName) { - if _, found := v[actionName]; found { - v[actionName] = 2 - } - } - } - - // processing method parameters - for _, field := range fn.Type.Params.List { - for _, fieldName := range field.Names { - te, err := parseParamFieldExpr(pkg.Name(), field.Type) - if err != nil { - logErrorf("AST: %s, please fix the parameter '%s' on action '%s.%s'; "+ - "otherwise your action may not work properly", err, fieldName.Name, controllerName, actionName) - continue - } - - var importPath string - if !ess.IsStrEmpty(te.PackageName) { - var found bool - if importPath, found = imports[te.PackageName]; !found { - importPath = pkg.ImportPath - } - } - - method.Parameters = append(method.Parameters, ¶meterInfo{ - Name: fieldName.Name, - ImportPath: importPath, - Type: te, - }) - } - } - - if ty := pkg.Types[controllerName]; ty == nil { - pos := pkg.Fset.Position(decl.Pos()) - filename := stripGoPath(pos.Filename) - logErrorf("AST: Method '%s' has incorrect struct recevier '%s' on file [%s] at line #%d", - actionName, controllerName, filename, pos.Line) - } else { - ty.Methods = append(ty.Methods, method) - } -} - -func isInterceptorActioName(actionName string) bool { - return (strings.HasPrefix(actionName, "Before") || strings.HasPrefix(actionName, "After") || - strings.HasPrefix(actionName, "Panic") || strings.HasPrefix(actionName, "Finally")) -} - -func getName(expr ast.Expr) string { - switch t := expr.(type) { - case *ast.Ident: - return t.Name - case *ast.SelectorExpr: - return getName(t.X) - case *ast.StarExpr: - return getName(t.X) - default: - return "" - } -} - -func isBuiltInDataType(typeName string) bool { - _, found := builtInDataTypes[typeName] - return found -} - -// parseStructFieldExpr method to find a direct "embedded|sub-type". -// Struct ast.Field as follows: -// Ident { "type-name" } e.g. UserController -// SelectorExpr { "package-name", "type-name" } e.g. aah.Controller -// StarExpr { "*", "package-name", "type-name"} e.g. *aah.Controller -func parseStructFieldExpr(fieldType ast.Expr) (string, string) { - for { - if starExpr, ok := fieldType.(*ast.StarExpr); ok { - fieldType = starExpr.X - continue - } - break - } - - // type it's in the same package, it's an ast.Ident. - if ident, ok := fieldType.(*ast.Ident); ok { - return "", ident.Name - } - - // type it's in the different package, it's an ast.SelectorExpr. - if selectorExpr, ok := fieldType.(*ast.SelectorExpr); ok { - if pkgIdent, ok := selectorExpr.X.(*ast.Ident); ok { - return pkgIdent.Name, selectorExpr.Sel.Name - } - } - - return "", "" -} - -func parseParamFieldExpr(pkgName string, expr ast.Expr) (*typeExpr, error) { - switch t := expr.(type) { - case *ast.Ident: - if isBuiltInDataType(t.Name) { - return &typeExpr{Expr: t.Name, IsBuiltIn: true}, nil - } - return &typeExpr{Expr: t.Name, PackageName: pkgName}, nil - case *ast.SelectorExpr: - e, err := parseParamFieldExpr(pkgName, t.X) - return &typeExpr{Expr: t.Sel.Name, PackageName: e.Expr}, err - case *ast.StarExpr: - e, err := parseParamFieldExpr(pkgName, t.X) - return &typeExpr{Expr: "*" + e.Expr, PackageName: e.PackageName, PackageIndex: e.PackageIndex + uint8(1)}, err - case *ast.ArrayType: - e, err := parseParamFieldExpr(pkgName, t.Elt) - return &typeExpr{Expr: "[]" + e.Expr, PackageName: e.PackageName, PackageIndex: e.PackageIndex + uint8(2)}, err - case *ast.Ellipsis: - e, err := parseParamFieldExpr(pkgName, t.Elt) - return &typeExpr{Expr: "[]" + e.Expr, PackageName: e.PackageName, PackageIndex: e.PackageIndex + uint8(2)}, err - case *ast.InterfaceType: - return nil, errInterfaceActionParam - case *ast.MapType: - return nil, errMapActionParam - } - - return nil, errInvalidActionParam -} diff --git a/aah/build.go b/aah/build.go index e5b9732..0665841 100644 --- a/aah/build.go +++ b/aah/build.go @@ -1,11 +1,10 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools/aah source code and usage is governed by a MIT style +// aahframework.org/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main import ( - "bytes" "fmt" "io/ioutil" "path/filepath" @@ -22,47 +21,61 @@ import ( var buildCmd = cli.Command{ Name: "build", Aliases: []string{"b"}, - Usage: "Build aah application for deployment", - Description: `Build aah application by import path. + Usage: "Builds aah application for deployment (single or non-single)", + Description: `Builds aah application for deployment. It supports single and non-single + binary. It is a trade-off learn more https://docs.aahframework.org/vfs.html - Artifact naming convention: ---.zip + Artifact naming convention: ---.zip For e.g.: aahwebsite-381eaa8-darwin-amd64.zip Examples of short and long flags: - aah build - aah build -e dev - aah build -i github.com/user/appname -o /Users/jeeva -e qa - aah build -i github.com/user/appname -o /Users/jeeva/aahwebsite.zip - aah build --importpath github.com/user/appname --output /Users/jeeva --envprofile qa`, + aah build OR aah b + aah build --single OR aah b -s + aah build -i github.com/user/appname -o /Users/jeeva + aah build -i github.com/user/appname -o /Users/jeeva/aahwebsite.zip`, Action: buildAction, Flags: []cli.Flag{ cli.StringFlag{ Name: "i, importpath", Usage: "Import path of aah application", }, - cli.StringFlag{ - Name: "e, envprofile", - Usage: "Environment profile name to activate. e.g: dev, qa, prod", - }, cli.StringFlag{ Name: "o, output", - Usage: "Output of aah application build artifact. Default is '/build/---.zip'", + Usage: "Output of aah application build artifact; the default is '/build/---.zip'", + }, + cli.BoolFlag{ + Name: "s, single", + Usage: "Creates aah single application binary", }, }, } func buildAction(c *cli.Context) error { - importPath := getAppImportPath(c) + importPath := appImportPath(c) + if err := aah.Init(importPath); err != nil { + logFatal(err) + } - aah.Init(importPath) - appBaseDir := aah.AppBaseDir() - projectCfg := aahProjectCfg(appBaseDir) + projectCfg := aahProjectCfg(aah.AppBaseDir()) cliLog = initCLILogger(projectCfg) - cliLog.Infof("Loaded aah project file: %s", filepath.Join(appBaseDir, aahProjectIdentifier)) + cliLog.Infof("Loaded aah project file: %s", filepath.Join(aah.AppBaseDir(), aahProjectIdentifier)) cliLog.Infof("Build starts for '%s' [%s]", aah.AppName(), aah.AppImportPath()) - appBinay, err := compileApp(&compileArgs{ + if c.Bool("s") || c.Bool("single") { + buildSingleBinary(c, projectCfg) + } else { + buildBinary(c, projectCfg) + } + + return nil +} + +func buildBinary(c *cli.Context, projectCfg *config.Config) { + appBaseDir := aah.AppBaseDir() + processVFSConfig(projectCfg, false) + + appBinary, err := compileApp(&compileArgs{ Cmd: "BuildCmd", ProjectCfg: projectCfg, AppPack: true, @@ -71,45 +84,81 @@ func buildAction(c *cli.Context) error { logFatal(err) } - appProfile := firstNonEmpty(c.String("e"), c.String("envprofile"), "prod") - buildBaseDir, err := copyFilesToWorkingDir(projectCfg, appBaseDir, appBinay, appProfile) + buildBaseDir, err := copyFilesToWorkingDir(projectCfg, appBaseDir, appBinary) if err != nil { logFatal(err) } - outputFile := firstNonEmpty(c.String("o"), c.String("output")) - archiveName := ess.StripExt(filepath.Base(appBinay)) + "-" + getAppVersion(appBaseDir, projectCfg) - archiveName = addTargetBuildInfo(archiveName) + destArchiveFile := createZipArchiveName(c, projectCfg, appBaseDir, appBinary) - var destArchiveFile string - if ess.IsStrEmpty(outputFile) { - destArchiveFile = filepath.Join(appBaseDir, "build", archiveName) - } else { - destArchiveFile, err = filepath.Abs(outputFile) - if err != nil { - logFatal(err) - } - - if !strings.HasSuffix(destArchiveFile, ".zip") { - destArchiveFile = filepath.Join(destArchiveFile, archiveName) - } + // Creating app archive + if err = createZipArchive(buildBaseDir, destArchiveFile); err != nil { + logFatal(err) } - if !strings.HasSuffix(destArchiveFile, ".zip") { - destArchiveFile = destArchiveFile + ".zip" + cliLog.Infof("Build successful for '%s' [%s]", aah.AppName(), aah.AppImportPath()) + cliLog.Infof("Application artifact is here: %s\n", destArchiveFile) +} + +func buildSingleBinary(c *cli.Context, projectCfg *config.Config) { + cliLog.Infof("Embed starts for '%s' [%s]", aah.AppName(), aah.AppImportPath()) + processVFSConfig(projectCfg, true) + cliLog.Infof("Embed successful for '%s' [%s]", aah.AppName(), aah.AppImportPath()) + + appBinary, err := compileApp(&compileArgs{ + Cmd: "BuildCmd", + ProjectCfg: projectCfg, + AppPack: true, + AppEmbed: true, + }) + if err != nil { + logFatal(err) } // Creating app archive - if err = createZipArchive(buildBaseDir, destArchiveFile); err != nil { + destArchiveFile := createZipArchiveName(c, projectCfg, aah.AppBaseDir(), appBinary) + if err = createZipArchive(appBinary, destArchiveFile); err != nil { logFatal(err) } cliLog.Infof("Build successful for '%s' [%s]", aah.AppName(), aah.AppImportPath()) - cliLog.Infof("Your application artifact is here: %s\n", destArchiveFile) - return nil + cliLog.Infof("Application artifact is here: %s\n", destArchiveFile) +} + +func processVFSConfig(projectCfg *config.Config, mode bool) { + appBaseDir := aah.AppBaseDir() + cleanupAutoGenVFSFiles(appBaseDir) + + excludes, _ := projectCfg.StringList("build.excludes") + noGzipList, _ := projectCfg.StringList("vfs.no_gzip") + + if mode { + // Default mount point + if err := processMount(mode, appBaseDir, "/app", appBaseDir, ess.Excludes(excludes), noGzipList); err != nil { + logFatal(err) + } + } + + // Custom mount points + mountKeys := projectCfg.KeysByPath("vfs.mount") + for _, key := range mountKeys { + vroot := projectCfg.StringDefault("vfs.mount."+key+".mount_path", "") + proot := projectCfg.StringDefault("vfs.mount."+key+".physical_path", "") + + if !filepath.IsAbs(proot) { + logErrorf("vfs %s: physical_path is not absolute path, skip mount: %s", proot, vroot) + continue + } + + if !ess.IsStrEmpty(vroot) && !ess.IsStrEmpty(proot) { + if err := processMount(mode, appBaseDir, vroot, proot, ess.Excludes(excludes), noGzipList); err != nil { + logError(err) + } + } + } } -func copyFilesToWorkingDir(projectCfg *config.Config, appBaseDir, appBinary, appProfile string) (string, error) { +func copyFilesToWorkingDir(projectCfg *config.Config, appBaseDir, appBinary string) (string, error) { appBinaryName := filepath.Base(appBinary) tmpDir, err := ioutil.TempDir("", appBinaryName) if err != nil { @@ -154,26 +203,6 @@ func copyFilesToWorkingDir(projectCfg *config.Config, appBaseDir, appBinary, app } } - // startup files - data := map[string]string{ - "AppName": ess.StripExt(appBinaryName), - "AppProfile": appProfile, - "Backtick": "`", - } - buf := &bytes.Buffer{} - if err = renderTmpl(buf, aahBashStartupTemplate, data); err != nil { - return "", err - } - if err = ioutil.WriteFile(filepath.Join(buildBaseDir, "aah.sh"), buf.Bytes(), permRWXRXRX); err != nil { - return "", err - } - - buf.Reset() - if err = renderTmpl(buf, aahCmdStartupTemplate, data); err != nil { - return "", err - } - err = ioutil.WriteFile(filepath.Join(buildBaseDir, "aah.cmd"), buf.Bytes(), permRWXRXRX) - return buildBaseDir, err } @@ -187,228 +216,28 @@ func createZipArchive(buildBaseDir, destArchiveFile string) error { return ess.Zip(destArchiveFile, buildBaseDir) } -const aahBashStartupTemplate = `#!/usr/bin/env bash - -# The MIT License (MIT) -# -# Copyright (c) Jeevanandam M., https://myjeeva.com -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -########################################### -# Start and Stop script for aah application -########################################### - -APP_NAME="{{ .AppName }}" -APP_ENV_PROFILE="{{ .AppProfile }}" -APP_EXT_CONFIG="" - -if [ ! -z "$2" ]; then - APP_ENV_PROFILE=$2 -fi - -if [ ! -z "$3" ]; then - APP_EXT_CONFIG="-config=$3" -fi - -# resolve links - $0 may be a softlink -PRG="$0" -while [ -h "$PRG" ] ; do - ls={{ .Backtick }}ls -ld "$PRG"{{ .Backtick }} - link={{ .Backtick }}expr "$ls" : '.*-> \(.*\)$'{{ .Backtick }} - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG={{ .Backtick }}dirname "$PRG"{{ .Backtick }}/"$link" - fi -done - -# resolve APP_DIR and set executable -APP_DIR=$(cd "$(dirname $PRG)"; pwd) -APP_EXECUTABLE="$APP_DIR"/bin/"$APP_NAME" -APP_PID="$APP_DIR"/"$APP_NAME".pid - -if [ ! -x "$APP_EXECUTABLE" ]; then - echo "Cannot find aah application executable: $APP_EXECUTABLE" - exit 1 -fi - -# go to application base directory -cd "$APP_DIR" - -start() { - if [ ! -z "$APP_PID" ]; then # not empty - if [ -f "$APP_PID" ]; then # exists and regular file - if [ -s "$APP_PID" ]; then # not-empty - echo "Existing PID file found during start." - if [ -r "$APP_PID" ]; then - PID={{ .Backtick }}cat "$APP_PID"{{ .Backtick }} - ps -p $PID >/dev/null 2>&1 - if [ $? -eq 0 ] ; then - echo "$APP_NAME appears to still be running with PID $PID. Start aborted." - ps -f -p $PID - exit 1 - fi - fi - fi - fi - fi - - nohup "$APP_EXECUTABLE" -profile="$APP_ENV_PROFILE" "$APP_EXT_CONFIG" > appstart.log 2>&1 & - echo "$APP_NAME started." -} - -stop() { - if [ ! -z "$APP_PID" ]; then # not empty - if [ -f "$APP_PID" ]; then # exists and regular file - if [ -s "$APP_PID" ]; then # not-empty - PID={{ .Backtick }}cat "$APP_PID"{{ .Backtick }} - kill -15 "$PID" >/dev/null 2>&1 - if [ $? -gt 0 ]; then - echo "$APP_PID file found but no matching process was found. Stop aborted." - exit 1 - else - rm -f "$APP_PID" >/dev/null 2>&1 - echo "$APP_NAME stopped." - fi - else - echo "$APP_PID file is empty and has been ignored." - fi - else - echo "$APP_PID file does not exists. Stop aborted." - exit 1 - fi - fi -} - -version() { - "$APP_EXECUTABLE" -version - echo "" -} - -case "$1" in -start) - start - ;; -stop) - stop - ;; -restart) - stop - sleep 2 - start - ;; -version) - version - ;; -*) - echo "Usage: $0 {start|stop|restart|version}" - echo "" - exit 1 -esac - -exit 0 -` - -const aahCmdStartupTemplate = `@ECHO OFF - -REM The MIT License (MIT) -REM -REM Copyright (c) Jeevanandam M., https://myjeeva.com -REM -REM Permission is hereby granted, free of charge, to any person obtaining a copy -REM of this software and associated documentation files (the "Software"), to deal -REM in the Software without restriction, including without limitation the rights -REM to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -REM copies of the Software, and to permit persons to whom the Software is -REM furnished to do so, subject to the following conditions: -REM -REM The above copyright notice and this permission notice shall be included in all -REM copies or substantial portions of the Software. -REM -REM THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -REM IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -REM FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -REM AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -REM LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -REM OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -REM SOFTWARE. - -REM ########################################## -REM Start and Stop script for aah application -REM ########################################## - -SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION - -SET APP_NAME={{ .AppName }} -SET APP_ENV_PROFILE={{ .AppProfile }} -SET APP_EXT_CONFIG="" - -IF NOT "%2" == "" ( - SET APP_ENV_PROFILE="%2" -) - -IF NOT "%3" == "" ( - SET APP_EXT_CONFIG="-config %3" -) - -REM resolve APP_DIR and set executable -SET APP_DIR=%~dp0 -SET APP_EXECUTABLE=%APP_DIR%bin\%APP_NAME%.exe -SET APP_PID=%APP_DIR%%APP_NAME%.pid - -REM change directory -cd %APP_DIR% - -if ""%1"" == """" GOTO :cmdUsage -if ""%1"" == ""start"" GOTO :doStart -if ""%1"" == ""stop"" GOTO :doStop -if ""%1"" == ""version"" GOTO :doVersion - -:doStart -REM check app is running already -tasklist /FI "IMAGENAME eq %APP_NAME%.exe" 2>NUL | find /I /N "%APP_NAME%.exe">NUL -IF "%ERRORLEVEL%" == "0" ( - ECHO %APP_NAME% appears to still be running. Start aborted. - GOTO :end -) - -START "" /B "%APP_EXECUTABLE%" -profile "%APP_ENV_PROFILE%" "%APP_EXT_CONFIG%" > appstart.log 2>&1 -ECHO {{ .AppName }} started. -GOTO :end - -:doStop -SET /P PID= < %APP_PID% -IF NOT %PID% == "" ( - taskkill /pid %PID% /f - ECHO {{ .AppName }} stopped. -) -GOTO :end +func createZipArchiveName(c *cli.Context, projectCfg *config.Config, appBaseDir, appBinary string) string { + var err error + outputFile := firstNonEmpty(c.String("o"), c.String("output")) + archiveName := ess.StripExt(filepath.Base(appBinary)) + "-" + getAppVersion(appBaseDir, projectCfg) + archiveName = addTargetBuildInfo(archiveName) -:doVersion -%APP_EXECUTABLE% -version -GOTO :end + var destArchiveFile string + if ess.IsStrEmpty(outputFile) { + destArchiveFile = filepath.Join(appBaseDir, "build", archiveName) + } else { + destArchiveFile, err = filepath.Abs(outputFile) + if err != nil { + logFatal(err) + } -:cmdUsage -echo Usage: %0 {start or stop or version} -GOTO :end + if !strings.HasSuffix(destArchiveFile, ".zip") { + destArchiveFile = filepath.Join(destArchiveFile, archiveName) + } + } -:end -ENDLOCAL -` + if !strings.HasSuffix(destArchiveFile, ".zip") { + destArchiveFile = destArchiveFile + ".zip" + } + return destArchiveFile +} diff --git a/aah/clean.go b/aah/clean.go index 771e0e2..568a47c 100644 --- a/aah/clean.go +++ b/aah/clean.go @@ -5,19 +5,16 @@ package main import ( - "path/filepath" - "gopkg.in/urfave/cli.v1" "aahframework.org/aah.v0" - "aahframework.org/essentials.v0" ) var cleanCmd = cli.Command{ Name: "clean", Aliases: []string{"c"}, Usage: "Cleans the aah generated files and build directory", - Description: `aah clean command does cleanup of generated files and build directory. + Description: `Cleans the aah generated files and build directory. Such as aah.go and /build directory. @@ -35,17 +32,16 @@ var cleanCmd = cli.Command{ } func cleanAction(c *cli.Context) error { - importPath := getAppImportPath(c) + importPath := appImportPath(c) - aah.Init(importPath) + if err := aah.Init(importPath); err != nil { + logFatal(err) + } projectCfg := aahProjectCfg(aah.AppBaseDir()) cliLog = initCLILogger(projectCfg) - ess.DeleteFiles( - filepath.Join(aah.AppBaseDir(), "app", "aah.go"), - filepath.Join(aah.AppBaseDir(), "build"), - filepath.Join(aah.AppBaseDir(), aah.AppName()+".pid"), - ) + cleanupAutoGenFiles(aah.AppBaseDir()) + cleanupAutoGenVFSFiles(aah.AppBaseDir()) cliLog.Infof("Import Path '%v' clean successful.\n", importPath) diff --git a/aah/compile.go b/aah/compile.go index c01781b..bfcf807 100644 --- a/aah/compile.go +++ b/aah/compile.go @@ -1,19 +1,24 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools/aah source code and usage is governed by a MIT style +// aahframework.org/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main import ( + "bufio" "bytes" "errors" "fmt" + "go/format" "io/ioutil" + "os/exec" "path" "path/filepath" + "regexp" "strings" "aahframework.org/aah.v0" + "aahframework.org/ainsp.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/router.v0" @@ -24,6 +29,7 @@ type compileArgs struct { ProxyPort string ProjectCfg *config.Config AppPack bool + AppEmbed bool } //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ @@ -40,6 +46,7 @@ func compileApp(args *compileArgs) (string, error) { appImportPath := aah.AppImportPath() appCodeDir := filepath.Join(appBaseDir, "app") appControllersPath := filepath.Join(appCodeDir, "controllers") + appWebSocketsPath := filepath.Join(appCodeDir, "websockets") appBuildDir := filepath.Join(appBaseDir, "build") appName := projectCfg.StringDefault("name", aah.AppName()) @@ -52,37 +59,82 @@ func compileApp(args *compileArgs) (string, error) { registeredActions := aah.AppRouter().RegisteredActions() // Go AST processing for Controllers - prg, errs := loadProgram(appControllersPath, ess.Excludes(excludes), registeredActions) - if len(errs) > 0 { - errMsgs := []string{} - for _, e := range errs { - errMsgs = append(errMsgs, e.Error()) + acntlr, errs := ainsp.Inspect(appControllersPath, ess.Excludes(excludes), registeredActions) + if len(acntlr.Packages) > 0 { + if len(errs) > 0 { + errMsgs := []string{} + for _, e := range errs { + errMsgs = append(errMsgs, e.Error()) + } + return "", errors.New(strings.Join(errMsgs, "\n")) } - return "", errors.New(strings.Join(errMsgs, "\n")) - } - - // call the process - prg.Process() - // Print router configuration missing/error details - missingActions := []string{} - for c, m := range prg.RegisteredActions { - for a, v := range m { - if v == 1 && !router.IsDefaultAction(a) { - missingActions = append(missingActions, fmt.Sprintf("%s.%s", c, a)) + // Print router configuration missing/error details + missingActions := []string{} + for c, m := range acntlr.RegisteredActions { + for a, v := range m { + if v == 1 && !router.IsDefaultAction(a) { + missingActions = append(missingActions, fmt.Sprintf("%s.%s", c, a)) + } } } + if len(missingActions) > 0 { + logError("Following actions are configured in 'routes.conf', however not implemented in Controller:\n\t", + strings.Join(missingActions, "\n\t")) + } } - if len(missingActions) > 0 { - logError("Following actions are configured in 'routes.conf', however not implemented in Controller:\n\t", - strings.Join(missingActions, "\n\t")) + + appImportPaths := map[string]string{ + "aahframework.org/aah.v0": "aah", + "aahframework.org/aruntime.v0": "aruntime", + "aahframework.org/config.v0": "config", + "aahframework.org/essentials.v0": "ess", + "aahframework.org/log.v0": "log", } // get all the types info referred aah framework context embedded - appControllers := prg.FindTypeByEmbeddedType(fmt.Sprintf("%s.Context", libImportPath("aah"))) - appImportPaths := prg.CreateImportPaths(appControllers) + appControllers := acntlr.FindTypeByEmbeddedType(fmt.Sprintf("%s.Context", libImportPath("aah"))) + appImportPaths = acntlr.CreateImportPaths(appControllers, appImportPaths) appSecurity := appSecurity(aah.AppConfig(), appImportPaths) + // Go AST processing for WebSockets + registeredWSActions := aah.AppRouter().RegisteredWSActions() + wsc, errs := ainsp.Inspect(appWebSocketsPath, ess.Excludes(excludes), registeredWSActions) + if len(wsc.Packages) > 0 { + if len(errs) > 0 { + errMsgs := []string{} + for _, e := range errs { + errMsgs = append(errMsgs, e.Error()) + } + return "", errors.New(strings.Join(errMsgs, "\n")) + } + + // Print router configuration missing/error details + missingWSActions := []string{} + for c, m := range wsc.RegisteredActions { + for a, v := range m { + if v == 1 && !router.IsDefaultAction(a) { + missingWSActions = append(missingWSActions, fmt.Sprintf("%s.%s", c, a)) + } + } + } + if len(missingWSActions) > 0 { + logError("Following WebSocket actions are configured in 'routes.conf', however not implemented in WebSocket:\n\t", + strings.Join(missingWSActions, "\n\t")) + } + } + + appWebSockets := wsc.FindTypeByEmbeddedType(fmt.Sprintf("%s.Context", libImportPath("ws"))) + appImportPaths = wsc.CreateImportPaths(appWebSockets, appImportPaths) + + if len(appControllers) == 0 && len(appWebSockets) == 0 { + return "", fmt.Errorf("It seems your application have zero controller or websocket") + } + + if len(appControllers) > 0 || len(appWebSockets) > 0 { + appImportPaths[libImportPath("ainsp")] = "ainsp" + } + // prepare aah application version and build date appVersion := getAppVersion(appBaseDir, projectCfg) appBuildDate := getBuildDate() @@ -109,11 +161,8 @@ func compileApp(args *compileArgs) (string, error) { // main.go location e.g. path/to/import/app buildArgs = append(buildArgs, path.Join(appImportPath, "app")) - // clean previous main.go and binary file up before we start the build - appMainGoFile := filepath.Join(appCodeDir, "aah.go") - cliLog.Debugf("Cleaning %s", appMainGoFile) - cliLog.Debugf("Cleaning build directory %s", appBuildDir) - ess.DeleteFiles(appMainGoFile, appBuildDir) + // clean previously auto generated files + cleanupAutoGenFiles(appBaseDir) if err := generateSource(appCodeDir, "aah.go", aahMainTemplate, map[string]interface{}{ "AppTargetCmd": args.Cmd, @@ -124,9 +173,11 @@ func compileApp(args *compileArgs) (string, error) { "AppBuildDate": appBuildDate, "AppBinaryName": appBinaryName, "AppControllers": appControllers, + "AppWebSockets": appWebSockets, "AppImportPaths": appImportPaths, "AppSecurity": appSecurity, "AppIsPackaged": args.AppPack, + "AppIsEmbedded": args.AppEmbed, }); err != nil { return "", err } @@ -155,16 +206,26 @@ func generateSource(dir, filename, templateSource string, templateArgs map[strin file := filepath.Join(dir, filename) buf := &bytes.Buffer{} - if err := renderTmpl(buf, templateSource, templateArgs); err != nil { + err := renderTmpl(buf, templateSource, templateArgs) + if err != nil { return err } - if err := ioutil.WriteFile(file, buf.Bytes(), permRWXRXRX); err != nil { + b := buf.Bytes() + if strings.HasSuffix(filename, ".go") { + if b, err = format.Source(b); err != nil { + return fmt.Errorf("aah '%s' file format source error: %s", filename, err) + } + } + + if err := ioutil.WriteFile(file, b, permRWXRXRX); err != nil { return fmt.Errorf("aah '%s' file write error: %s", filename, err) } return nil } +var notExistRegex = regexp.MustCompile(`cannot find package "(.*)" in any of`) + // checkAndGetAppDeps method project dependencies is present otherwise // it tries to get it if any issues it will return error. It internally uses // go list command. @@ -173,39 +234,44 @@ func generateSource(dir, filename, templateSource string, templateArgs map[strin func checkAndGetAppDeps(appImportPath string, cfg *config.Config) error { importPath := path.Join(appImportPath, "app", "...") args := []string{"list", "-f", "{{.Imports}}", importPath} - output, err := execCmd(gocmd, args, false) if err != nil { return err } - lines := strings.Split(strings.TrimSpace(output), "\r\n") - for _, line := range lines { - line = strings.Replace(strings.Replace(line, "]", "", -1), "[", "", -1) - line = strings.Replace(strings.Replace(line, "\r", " ", -1), "\n", " ", -1) - if ess.IsStrEmpty(line) { - // all dependencies is available - return nil - } - - notExistsPkgs := []string{} - for _, pkg := range strings.Fields(line) { - if ess.IsStrEmpty(pkg) || ess.IsImportPathExists(pkg) { - continue + pkgList := make(map[string]string) + replacer := strings.NewReplacer("[", "", "]", "") + scanner := bufio.NewScanner(strings.NewReader(output)) + for scanner.Scan() { + if ln := replacer.Replace(strings.TrimSpace(scanner.Text())); ln != "" { + for _, p := range strings.Fields(ln) { + if p := strings.TrimSpace(p); p != "" { + pkgList[p] = p + } } - notExistsPkgs = append(notExistsPkgs, pkg) } + } - if cfg.BoolDefault("build.dep_get", false) && len(notExistsPkgs) > 0 { - cliLog.Info("Getting application dependencies ...") - if err := goGet(notExistsPkgs...); err != nil { - return err - } - } else if len(notExistsPkgs) > 0 { - return fmt.Errorf("Below application dependencies does not exist, "+ - "enable 'build.dep_get=true' in 'aah.project' for auto fetch\n---> %s", - strings.Join(notExistsPkgs, "\n---> ")) + args = []string{"list"} + for _, p := range pkgList { + args = append(args, p) + } + b, _ := exec.Command(gocmd, args...).CombinedOutput() + notExistsPkgs := []string{} + matches := notExistRegex.FindAllStringSubmatch(string(b), -1) + for _, m := range matches { + notExistsPkgs = append(notExistsPkgs, m[1]) + } + + if cfg.BoolDefault("build.dep_get", true) && len(notExistsPkgs) > 0 { + cliLog.Info("Getting application dependencies ...", notExistsPkgs) + if err := goGet(notExistsPkgs...); err != nil { + return err } + } else if len(notExistsPkgs) > 0 { + return fmt.Errorf("Below application dependencies does not exist, "+ + "enable 'build.dep_get=true' in 'aah.project' for auto fetch\n---> %s", + strings.Join(notExistsPkgs, "\n---> ")) } return nil @@ -230,6 +296,7 @@ func appSecurity(appCfg *config.Config, appImportPaths map[string]string) map[st isAuthSchemeCfg := false authSchemeInfo := struct { Authenticator string + Principal string Authorizer string }{} @@ -241,6 +308,14 @@ func appSecurity(appCfg *config.Config, appImportPaths map[string]string) map[st isAuthSchemeCfg = true } + // Principal Provider + principal := appCfg.StringDefault(keyPrefixAuthSchemeCfg+".principal", "") + if !ess.IsStrEmpty(principal) { + authSchemeInfo.Principal = prepareAuthAlias( + keyAuthScheme+"sec", principal, importPathPrefix, appImportPaths) + isAuthSchemeCfg = true + } + // Authorizer authorizer := appCfg.StringDefault(keyPrefixAuthSchemeCfg+".authorizer", "") if !ess.IsStrEmpty(authorizer) { @@ -281,7 +356,7 @@ func prepareAuthAlias(keyAuthAlias, auth, importPathPrefix string, appImportPath // Generate Templates //___________________________________ -const aahMainTemplate = `// GENERATED CODE - DO NOT EDIT +const aahMainTemplate = `// Code generated by aah CLI, DO NOT EDIT // // aah framework v{{.AahVersion}} - https://aahframework.org // FILE: aah.go @@ -290,54 +365,97 @@ const aahMainTemplate = `// GENERATED CODE - DO NOT EDIT package main import ( + "bytes" "flag" + "path/filepath" "fmt" "os" "os/signal" "reflect" + "regexp" "syscall" - - "aahframework.org/aah.v0" - "aahframework.org/config.v0" - "aahframework.org/essentials.v0" - "aahframework.org/log.v0"{{ range $k, $v := $.AppImportPaths }} + {{ if .AppSecurity }} + "aahframework.org/security.v0/authc" + "aahframework.org/security.v0/authz"{{ end }}{{ range $k, $v := $.AppImportPaths }} {{ $v }} "{{ $k }}"{{ end }} ) var ( - // Defining flags - version = flag.Bool("version", false, "Display application name, version and build date.") + // Define aah application binary flags configPath = flag.String("config", "", "Absolute path of external config file.") - profile = flag.String("profile", "", "Environment profile name to activate. e.g: dev, qa, prod.") + list = flag.String("list", "", "Prints the embedded file/directory path that matches the given regex pattern.") + profile = flag.String("profile", "", "Environment profile name to activate. For e.g.: dev, qa, prod, etc.") + version = flag.Bool("version", false, "Prints the aah application binary name, version and build timestamp.") _ = reflect.Invalid ) -func mergeExternalConfig(e *aah.Event) { - externalConfig, err := config.LoadFile(*configPath) +func MergeSuppliedConfig(_ *aah.Event) { + cpath, err := filepath.Abs(*configPath) if err != nil { - log.Fatalf("Unable to load external config: %s", *configPath) + log.Errorf("Unable to resolve external config: %s", *configPath) } - log.Debug("Merging external config into aah application config") + externalConfig, err := config.LoadFile(cpath) + if err != nil { + log.Errorf("Unable to load external config: %s", cpath) + } + + log.Infof("Merging external config[%s] into aah application[%s]", cpath, aah.AppName()) if err := aah.AppConfig().Merge(externalConfig); err != nil { log.Errorf("Unable to merge external config into aah application[%s]: %s", aah.AppName(), err) } } -func setAppEnvProfile(e *aah.Event) { +func ActivateAppEnvProfile(_ *aah.Event) { aah.AppConfig().SetString("env.active", *profile) } +func PrintFilepath(pattern string) { + if !aah.AppVFS().IsEmbeddedMode() { + fmt.Println("'"+aah.AppBuildInfo().BinaryName + "' binary does not have embedded files.") + return + } + + regex, err := regexp.Compile(pattern) + if err != nil { + log.Error(err) + return + } + + if err := aah.AppVFS().Walk(aah.AppVirtualBaseDir(), + func(fpath string, _ os.FileInfo, err error) error { + if err != nil { + return err + } + + if regex.MatchString(fpath) { + fmt.Println(fpath) + } + + return nil + }); err != nil { + log.Error(err) + } +} + {{ if eq .AppTargetCmd "RunCmd" -}} {{ if .AppProxyPort -}} -func setAppProxyPort(e *aah.Event) { +func RunCmdSetAppProxyPort(e *aah.Event) { aah.AppConfig().SetString("server.proxyport", "{{ .AppProxyPort }}") } {{- end }} {{- end }} func main() { - log.Infof("aah framework v%s, requires ≥ go1.8", aah.Version) + defer func() { + if r := recover(); r != nil { + st := aruntime.NewStacktrace(r, aah.AppConfig()) + buf := new(bytes.Buffer) + st.Print(buf) + log.Error(buf.String()) + } + }() + flag.Parse() aah.SetAppBuildInfo(&aah.BuildInfo{ @@ -346,66 +464,100 @@ func main() { Date: "{{ .AppBuildDate }}", }) - aah.SetAppPackaged({{ .AppIsPackaged }}) + {{ if .AppIsPackaged }}aah.SetAppPackaged({{ .AppIsPackaged }}){{ end }} // display application information if *version { - fmt.Printf("%-12s: %s\n", "Binary Name", aah.AppBuildInfo().BinaryName) - fmt.Printf("%-12s: %s\n", "Version", aah.AppBuildInfo().Version) - fmt.Printf("%-12s: %s\n", "Build Date", aah.AppBuildInfo().Date) + fmt.Printf("%-16s: %s\n", "Binary Name", aah.AppBuildInfo().BinaryName) + fmt.Printf("%-16s: %s\n", "Version", aah.AppBuildInfo().Version) + fmt.Printf("%-16s: %s\n", "Build Timestamp", aah.AppBuildInfo().Date) + fmt.Printf("%-16s: %s\n", "aah Version", aah.Version) + return + } + + if !ess.IsStrEmpty(*list) { + PrintFilepath(*list) return } // Apply supplied external config file if !ess.IsStrEmpty(*configPath) { - aah.OnInit(mergeExternalConfig) + aah.OnInit(MergeSuppliedConfig) } - // Apply environment profile + // Activate environment profile if !ess.IsStrEmpty(*profile) { - aah.OnInit(setAppEnvProfile) + aah.OnInit(ActivateAppEnvProfile) } - aah.Init("{{ .AppImportPath }}") + log.Infof("aah framework v%s, requires ≥ go1.8", aah.Version) - // Adding all the application controllers which refers 'aah.Context' directly - // or indirectly from app/controllers/** {{ range $i, $c := .AppControllers }} - aah.AddController( - (*{{ index $.AppImportPaths .ImportPath }}.{{ .Name }})(nil), - []*aah.MethodInfo{ - {{ range .Methods }}&aah.MethodInfo{ - Name: "{{ .Name }}", - Parameters: []*aah.ParameterInfo{ {{ range .Parameters -}} - &aah.ParameterInfo{Name: "{{ .Name }}", Type: reflect.TypeOf((*{{ .Type.Name }})(nil))},{{- end }} - }, - },{{ end }} - }, - ){{- end }} - - {{ if .AppSecurity -}} - // Initialize application security auth schemes - Authenticator & Authorizer - secMgr := aah.AppSecurityManager() - {{- range $k, $v := $.AppSecurity }} - {{ if $v.Authenticator -}} - log.Debugf("Calling authenticator Init for auth scheme '%s'", "{{ $k }}") - if err := secMgr.GetAuthScheme("{{ $k }}").SetAuthenticator(&{{ $v.Authenticator }}{}); err != nil { + if err := aah.Init("{{ .AppImportPath }}"); err != nil { log.Fatal(err) } + + {{ if gt (len .AppControllers) 0 -}} + // Adding all the application controllers which refers 'aah.Context' directly + // or indirectly from app/controllers/** {{ range $i, $c := .AppControllers }} + aah.AddController((*{{ index $.AppImportPaths .ImportPath }}.{{ .Name }})(nil), []*ainsp.Method{ {{ range .Methods }} + {Name: "{{ .Name }}"{{ if gt (len .Parameters) 0 }}, Parameters: []*ainsp.Parameter{ {{ range .Parameters }} + {Name: "{{ .Name }}", Type: reflect.TypeOf((*{{ .Type.Name }})(nil))},{{- end }} + }{{ end }}},{{ end }} + }){{- end }} {{ end -}} - {{ if $v.Authorizer -}} - log.Debugf("Calling authorizer Init for auth scheme '%s'", "{{ $k }}") - if err := secMgr.GetAuthScheme("{{ $k }}").SetAuthorizer(&{{ $v.Authorizer }}{}); err != nil { - log.Fatal(err) - } + + {{ if gt (len .AppWebSockets) 0 -}} + // Adding all the application websockets which refers 'ws.Context' directly + // or indirectly from app/websockets/** {{ range $i, $c := .AppWebSockets }} + aah.AddWebSocket((*{{ index $.AppImportPaths .ImportPath }}.{{ .Name }})(nil), []*ainsp.Method{ {{ range .Methods }} + {Name: "{{ .Name }}"{{ if gt (len .Parameters) 0 }}, Parameters: []*ainsp.Parameter{ {{ range .Parameters }} + {Name: "{{ .Name }}", Type: reflect.TypeOf((*{{ .Type.Name }})(nil))},{{- end }} + }{{ end }}},{{ end }} + }){{- end }} {{ end -}} + + {{ if .AppSecurity }} + type setprincipal interface { + SetPrincipalProvider(principal authc.PrincipalProvider) error + } + type setauthenticator interface { + SetAuthenticator(authenticator authc.Authenticator) error + } + type setauthorizer interface { + SetAuthorizer(authorizer authz.Authorizer) error + } + + // Initialize application security auth schemes - Authenticator, + // PrincipalProvider & Authorizer + secMgr := aah.AppSecurityManager() + {{- range $k, $v := $.AppSecurity }}{{ $vPrefix := (variablename $k) }} + {{ $vPrefix }}AuthScheme := secMgr.AuthScheme("{{ $k }}") + {{ if $v.Authenticator -}}if sauthc, ok := {{ $vPrefix }}AuthScheme.(setauthenticator); ok { + aah.AppLog().Debugf("Initializing authenticator for auth scheme '%s'", "{{ $k }}") + if err := sauthc.SetAuthenticator(&{{ $v.Authenticator }}{}); err != nil { + aah.AppLog().Fatal(err) + } + }{{ end }} + {{ if $v.Principal -}}if sprincipal, ok := {{ $vPrefix }}AuthScheme.(setprincipal); ok { + aah.AppLog().Debugf("Initializing principalprovider for auth scheme '%s'", "{{ $k }}") + if err := sprincipal.SetPrincipalProvider(&{{ $v.Principal }}{}); err != nil { + aah.AppLog().Fatal(err) + } + }{{ end }} + {{ if $v.Authorizer }}if sauthz, ok := {{ $vPrefix }}AuthScheme.(setauthorizer); ok { + aah.AppLog().Debugf("Initializing authorizer for auth scheme '%s'", "{{ $k }}") + if err := sauthz.SetAuthorizer(&{{ $v.Authorizer }}{}); err != nil { + aah.AppLog().Fatal(err) + } + }{{ end }} {{ end -}} {{ end }} - log.Info("aah application initialized successfully") + aah.AppLog().Info("aah application initialized successfully") {{ if eq .AppTargetCmd "RunCmd" -}} {{ if .AppProxyPort -}} - aah.OnStart(setAppProxyPort) + aah.OnStart(RunCmdSetAppProxyPort) {{- end }} {{- end }} @@ -417,14 +569,14 @@ func main() { sig := <-sc switch sig { case os.Interrupt: - log.Warn("Interrupt signal (SIGINT) received") + aah.AppLog().Warn("Interrupt signal (SIGINT) received") case syscall.SIGTERM: - log.Warn("Termination signal (SIGTERM) received") + aah.AppLog().Warn("Termination signal (SIGTERM) received") } // Call aah shutdown aah.Shutdown() - log.Info("aah application shutdown successful") + aah.AppLog().Info("aah application shutdown successful") // bye bye, see you later. os.Exit(0) diff --git a/aah/embed.go b/aah/embed.go new file mode 100644 index 0000000..424b47a --- /dev/null +++ b/aah/embed.go @@ -0,0 +1,275 @@ +// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) +// aahframework.org/tools/aah source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package main + +import ( + "bytes" + "compress/gzip" + "fmt" + "go/format" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + "text/template" + "time" + + "aahframework.org/aah.v0" + "aahframework.org/essentials.v0" + "aahframework.org/vfs.v0" +) + +// Standard frame type MTU size is 1500 bytes so 1400 bytes would make sense +// to Gzip by default. Read: https://en.wikipedia.org/wiki/Maximum_transmission_unit +var defaultGzipMinSize int64 = 1400 + +var vfsTmpl = template.Must(template.New("vfs").Funcs(vfsTmplFuncMap).Parse(vfsTmplStr)) + +func processMount(mode bool, appBaseDir, vroot, proot string, skipList ess.Excludes, noGzipList []string) error { + proot = filepath.ToSlash(proot) + if !ess.IsFileExists(proot) { + return &os.PathError{Op: "open", Path: proot, Err: os.ErrNotExist} + } + + if mode { + cliLog.Infof("|-- Processing mount: '%s' <== '%s'", vroot, proot) + } + b, err := generateVFSSource(mode, vroot, proot, skipList, noGzipList) + if err != nil { + return err + } + + // destination file + filename := fmt.Sprintf("aah%s_vfs.go", strings.Replace(vroot, "/", "_", -1)) + return ioutil.WriteFile(filepath.Join(appBaseDir, "app", filename), b, permRWXRXRX) +} + +// generateVFSSource method creates Virtual FileSystem (VFS) code +// to add files and directories within binary for configured Mount points +// on file aah.project. +func generateVFSSource(mode bool, vroot, proot string, skipList ess.Excludes, noGzipList []string) ([]byte, error) { + err := skipList.Validate() + if err != nil { + return nil, err + } + + buf := &bytes.Buffer{} + startTmpl := "vfs_start_embed" + if !mode { + startTmpl = "vfs_start_mount" + } + if err = vfsTmpl.ExecuteTemplate(buf, startTmpl, aah.Data{ + "Mode": mode, + "MountPath": vroot, + "PhysicalPath": proot, + }); err != nil { + return nil, err + } + + // non-single binary mode, exit here + if !mode { + _s(fmt.Fprint(buf, "\n}")) + return format.Source(buf.Bytes()) + } + + files := make(map[string]os.FileInfo) + if err := ess.Walk(proot, func(fpath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + fpath = filepath.ToSlash(fpath) + fname := path.Base(fpath) + if skipList.Match(fname) { + if fname == "app" && strings.Contains(fpath, "/pages/") { + goto sc + } + + cliLog.Debugf(" |-- Skipping: %s", fpath) + if info.IsDir() { + return filepath.SkipDir // skip directory + } + return nil // skip file + } + sc: + + if info.IsDir() { + mp := filepath.ToSlash(filepath.Join(vroot, strings.TrimPrefix(fpath, proot))) + + if err = vfsTmpl.ExecuteTemplate(buf, "vfs_dir", aah.Data{ + "Node": &vfs.NodeInfo{Dir: info.IsDir(), Path: mp, Time: info.ModTime()}, + }); err != nil { + return err + } + } else { + files[fpath] = info + } + + return nil + }); err != nil { + return nil, err + } + + _s(fmt.Fprintf(buf, "\n// Adding files into VFS\n")) + for fname, info := range files { + f, err := os.Open(fname) + if err != nil { + logError(err) + continue + } + + cliLog.Debugf(" |-- Processing: %s", fname) + mp := filepath.ToSlash(filepath.Join(vroot, strings.TrimPrefix(fname, proot))) + + if err = vfsTmpl.ExecuteTemplate(buf, "vfs_file", aah.Data{ + "Node": &vfs.NodeInfo{DataSize: info.Size(), Path: mp, Time: info.ModTime()}, + }); err != nil { + logError(err) + return nil, err + } + + if info.Size() > 0 { + if err = convertFile(buf, f, info, noGzip(noGzipList, info.Name())); err != nil { + logError(err) + return nil, err + } + } + _s(fmt.Fprint(buf, "\"))\n\n")) + ess.CloseQuietly(f) + } + + _s(fmt.Fprint(buf, "}")) + return format.Source(buf.Bytes()) +} + +func convertFile(buf *bytes.Buffer, r io.ReadSeeker, fi os.FileInfo, noGzip bool) error { + restorePoint := buf.Len() + w := &stringWriter{w: buf} + + // if its already less then MTU size or gzip not required + if fi.Size() <= defaultGzipMinSize || noGzip { + _, err := io.Copy(w, r) + return err + } + + gw := gzip.NewWriter(w) + _, err := io.Copy(gw, r) + if err != nil { + return err + } + + if err = gw.Close(); err != nil { + return err + } + + if int64(w.size) >= fi.Size() { + if _, err = r.Seek(0, io.SeekStart); err != nil { + return err + } + + buf.Truncate(restorePoint) + if _, err = io.Copy(w, r); err != nil { + return err + } + } + + return nil +} + +const lowerHex = "0123456789abcdef" + +// https://github.com/go-bindata/go-bindata/blob/master/stringwriter.go +type stringWriter struct { + w io.Writer + size int +} + +func (s *stringWriter) Write(p []byte) (n int, err error) { + buf := []byte(`\x00`) + for _, b := range p { + buf[2], buf[3] = lowerHex[b/16], lowerHex[b%16] + if _, err = s.w.Write(buf); err != nil { + return + } + n++ + s.size++ + } + return +} + +func timeStr(t time.Time) string { + if t.IsZero() { + return "time.Time{}" + } + t = t.UTC() // always go with UTC + return fmt.Sprintf("time.Date(%d, %d, %d, %d, %d, %d, %d, time.UTC)", + t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()) +} + +func _s(_ ...interface{}) {} + +func noGzip(noGzipList []string, name string) bool { + for _, t := range noGzipList { + if strings.HasSuffix(name, t) { + return true + } + } + return false +} + +var vfsTmplFuncMap = template.FuncMap{ + "timestr": timeStr, +} + +const vfsTmplStr = `{{ define "vfs_start_mount"}} // Code generated by aah CLI - VFS, DO NOT EDIT. + +package main + +import ({{ if .Mode }} + "time"{{ end }} + + "aahframework.org/aah.v0" + "aahframework.org/log.v0"{{ if .Mode }} + "aahframework.org/vfs.v0"{{ end }} +) + +func init() { + {{ if .Mode }}aah.AppVFS().SetEmbeddedMode(){{ end }} + + if err := aah.AppVFS().AddMount("{{ .MountPath }}", "{{ .PhysicalPath }}"); err != nil { + log.Fatal("vfs: ", err) + } +{{ end }} + +{{ define "vfs_start_embed" }}{{ template "vfs_start_mount" . }} + + // Find Mount point + m, err := aah.AppVFS().FindMount("{{ .MountPath }}") + if err != nil { + log.Fatal("vfs: ", err) + } + + // Adding directories into VFS +{{- end -}} + +{{ define "vfs_dir" }} + m.AddDir(&vfs.NodeInfo{ + Dir: {{ .Node.Dir }}, + Path: "{{ .Node.Path }}", + Time: {{ .Node.Time | timestr }}, + }) +{{ end }} + +{{ define "vfs_file" }} + m.AddFile(&vfs.NodeInfo{ + DataSize: {{ .Node.DataSize }}, + Path: "{{ .Node.Path }}", + Time: {{ .Node.Time | timestr }}, + }, + []byte(" +{{- end }} +` diff --git a/aah/generate.go b/aah/generate.go index 32e1daa..f744207 100644 --- a/aah/generate.go +++ b/aah/generate.go @@ -23,14 +23,14 @@ var generateCmd = cli.Command{ Name: "generate", Aliases: []string{"g"}, Usage: "Generates boilerplate code, configurations, complement scripts (systemd, docker), etc.", - Description: `Generate command increases productivity and helps developer on tedious tasks during application development. - It generates boilerplate code, configuration files, complement scripts (systemd, docker), etc. + Description: `Command generate increases productivity and helps developer on tedious tasks during application development. + Such as boilerplate code, configuration files, complement scripts (systemd, docker), etc. To know more about available 'generate' sub commands: aah h g aah help generate - To know more about individual sub command details: + To know more about individual sub-commands details: aah g h s aah generate help script `, @@ -93,9 +93,11 @@ func generateScriptsAction(c *cli.Context) error { //___________________________________ func generateSystemdScript(c *cli.Context) error { - importPath := getAppImportPath(c) + importPath := appImportPath(c) + if err := aah.Init(importPath); err != nil { + logFatal(err) + } - aah.Init(importPath) projectCfg := aahProjectCfg(aah.AppBaseDir()) cliLog = initCLILogger(projectCfg) @@ -129,9 +131,11 @@ func generateSystemdScript(c *cli.Context) error { } func generateDockerScript(c *cli.Context) error { - importPath := getAppImportPath(c) + importPath := appImportPath(c) - aah.Init(importPath) + if err := aah.Init(importPath); err != nil { + logFatal(err) + } projectCfg := aahProjectCfg(aah.AppBaseDir()) cliLog = initCLILogger(projectCfg) @@ -223,22 +227,20 @@ func checkAndConfirmOverwrite(destFile string) bool { // Script Templates //___________________________________ -const aahSystemdScriptTemplate = `// GENERATED BY aah CLI - Feel free to customization it. -// FILE: {{ .FileName }} -// DATE: {{ .CreateDate }} -// DESC: aah application systemd service file +const aahSystemdScriptTemplate = `# GENERATED BY aah CLI - Feel free to customization it. +# FILE: {{ .FileName }} +# DATE: {{ .CreateDate }} +# DESC: aah application systemd service file [Unit] Description={{ .Desc }} After=network.target [Service] +#User=aah +#Group=aah EnvironmentFile=/home/aah/{{ .AppName }}_env_values -User=aah -Group=aah -Type=forking -ExecStart=/home/aah/{{ .AppName }}/aah.sh start -ExecStop=/home/aah/{{ .AppName }}/aah.sh stop +ExecStart=/home/aah/{{ .AppName }}/bin/{{ .AppName }} -profile prod ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure diff --git a/aah/list.go b/aah/list.go index ebc1cff..30aa52e 100644 --- a/aah/list.go +++ b/aah/list.go @@ -20,8 +20,8 @@ const aahProjectIdentifier = "aah.project" var listCmd = cli.Command{ Name: "list", Aliases: []string{"l"}, - Usage: "List all aah projects in GOPATH", - Description: `List command allows you to view all projects that are making use of aah in your GOPATH. + Usage: "Lists all the aah projects on your GOPATH", + Description: `Command 'list' helps you to view all the aah application projects on your GOPATH. `, Action: listAction, } diff --git a/aah/migrate.conf b/aah/migrate.conf new file mode 100644 index 0000000..077fd53 --- /dev/null +++ b/aah/migrate.conf @@ -0,0 +1,62 @@ +# ------------------------------------------------------------ +# aah migrate grammar file +# +# Syntax format is same as aah config format +# ------------------------------------------------------------ + +file { + + # Grammar for Go source file (.go) + # Each line have "from" and "to" replacement + # + # Format: "oldsignature", "newsignature" + go { + upgrade_replacer = [ + "aah.AddServerTLSConfig(", "aah.SetTLSConfig(", + "aah.EventOnAfterReply", "aah.EventOnPostReply", + "aah.OnShutdown(", "aah.OnPostShutdown(", + "// Event: OnShutdown", "// Event: OnPostShutdown", + "aah.OnRequest(", "aah.AppHTTPEngine().OnRequest(", + "aah.OnPreReply(", "aah.AppHTTPEngine().OnPreReply(", + "aah.OnPostReply(", "aah.AppHTTPEngine().OnPostReply(", + "aah.OnAfterReply(", "aah.AppHTTPEngine().OnPostReply(", + "aah.OnPreAuth(", "aah.AppHTTPEngine().OnPreAuth(", + "aah.OnPostAuth(", "aah.AppHTTPEngine().OnPostAuth(", + "aah.Middlewares(", "aah.AppHTTPEngine().Middlewares(", + ".SubscribeEventf(", ".SubscribeEventFunc(", + ".UnsubscribeEventf(", ".UnsubscribeEventFunc(", + "aah.KeyViewArgRequestParams", "aah.KeyViewArgRequest", + "ahttp.GetResponseWriter(", "ahttp.AcquireResponseWriter(", + "ahttp.PutResponseWriter(", "ahttp.ReleaseResponseWriter(", + "ahttp.GetGzipResponseWriter(", "ahttp.WrapGzipWriter(", + "ahttp.PutGzipResponseWiriter(", "ahttp.ReleaseResponseWriter(", + "Req.Raw", "Req.Unwrap()", + "Req.Raw.URL", "Req.URL()", + "Req.Unwrap().URL", "Req.URL()", + ".Unwrap().FormValue(", ".Req.FormValue(", + "Req.AcceptContentType", "Req.AcceptContentType()", + "Req.AcceptEncoding", "Req.AcceptEncoding()", + "Req.ClientIP", "Req.ClientIP()", + "Req.ContentType", "Req.ContentType()", + "Req.Locale", "Req.Locale()", + ".Readfrom(", ".FromReader(", + ".RedirectSts(", ".RedirectWithStatus(", + ".FindDomain(", ".Lookup(", + ".ReverseURL(", ".RouteURL(", + ".ReverseURLm(", ".RouteURLNamedArgs(", + ".GetAuthScheme(", ".AuthScheme(" + ] + } + + # Grammar for View files + # Each line have "from" and "to" replacement + # + # Format: "oldsignature", "newsignature" + view { + upgrade_replacer = [ + "{{ anitcsrftoken . }}", "{{ anticsrftoken . }}", + "name=\"anit_csrf_token\"", "name=\"anti_csrf_token\"" + ] + } + +} diff --git a/aah/migrate.go b/aah/migrate.go new file mode 100644 index 0000000..7141d11 --- /dev/null +++ b/aah/migrate.go @@ -0,0 +1,210 @@ +// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) +// aahframework.org/tools/aah source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package main + +import ( + "bytes" + "go/format" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "aahframework.org/aah.v0" + "aahframework.org/config.v0" + "aahframework.org/essentials.v0" + "gopkg.in/urfave/cli.v1" +) + +const aahGrammarIdentifier = "migrate.conf" +const aahGrammarFetchLoc = "https://cdn.aahframework.org/" + aahGrammarIdentifier + +var migrateCmd = cli.Command{ + Name: "migrate", + Aliases: []string{"m"}, + Usage: "Migrates application codebase to current version of aah (currently beta)", + Description: `Command migrate is to house migration related sub-commands of aah. + Currently it supports Go source code migrate. + + To know more about available 'migrate' sub commands: + aah h m + aah help migrate + + To know more about individual sub-commands details: + aah m h c + aah migrate help code +`, + Subcommands: []cli.Command{ + cli.Command{ + Name: "code", + Aliases: []string{"c"}, + Usage: "Migrates application codebase by making it compatible with current version of aah", + Description: `Command code is to fix/upgrade aah's breaking changes and deprecated elements + in application codebase to the current version of aah. + + The goal of 'Code' command is to keep aah users always up-to-date with latest version of aah. + + Note: Migrate does not take file backup, assumes application use version control. + + Example of script command: + aah m c -i github.com/user/appname + aah migrate code --importpath github.com/user/appname + `, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "i, importpath", + Usage: "Import path of aah application", + }, + }, + Action: migrateCodeAction, + }, + }, +} + +func migrateCodeAction(c *cli.Context) error { + importPath := appImportPath(c) + if err := aah.Init(importPath); err != nil { + logFatal(err) + } + + projectCfg := aahProjectCfg(aah.AppBaseDir()) + cliLog = initCLILogger(projectCfg) + + cliLog.Warn("Migrate command does not take file backup. It assumes application use version control.") + if !collectYesOrNo(reader, "Would you like to continue ([Y]es or [N]o)? default is 'N'") { + cliLog.Info("Okay, I respect your choice. Bye.") + return nil + } + + grammarFile := filepath.Join(aahPath(), aahGrammarIdentifier) + if !ess.IsFileExists(grammarFile) { + cliLog.Info("Fetch migrate configuration from ", aahGrammarFetchLoc) + if err := fetchFile(grammarFile, aahGrammarFetchLoc); err != nil { + logFatal(err) + } + } + + grammarCfg, err := config.LoadFile(grammarFile) + if err != nil { + logFatal(err) + } + + cliLog.Info("\nNote:") + cliLog.Info("-----") + cliLog.Info("Command works based on `migrate.conf` file. If you identify a new grammar entry, \n" + + "create an issue at https://aahframework.org/issues.\n") + + cliLog.Infof("Loaded migrate configuration: %s", grammarFile) + cliLog.Infof("Loaded aah project file: %s", filepath.Join(aah.AppBaseDir(), aahProjectIdentifier)) + cliLog.Infof("Migrate starts for '%s' [%s]", aah.AppName(), aah.AppImportPath()) + + // Go Source files + cliLog.Infof("Go source code migrate starts ...") + if migrateGoSrcFiles(projectCfg, grammarCfg) == 0 { + cliLog.Info(" It seems application Go source code are up-to-date") + } + cliLog.Infof("Go source code migrate successful") + + if ess.IsFileExists(filepath.Join(aah.AppBaseDir(), "views")) { + // View files + cliLog.Infof("View file migrate starts ...") + if migrateViewFiles(projectCfg, grammarCfg) == 0 { + cliLog.Info(" It seems application view files are up-to-date") + } + cliLog.Infof("View file migrate successful") + } + + cliLog.Infof("Migrate successful for '%s' [%s]\n", aah.AppName(), aah.AppImportPath()) + return nil +} + +func migrateGoSrcFiles(projectCfg, grammarCfg *config.Config) int { + count := 0 + grammar, found := grammarCfg.StringList("file.go.upgrade_replacer") + if !found { + cliLog.Info("Config 'file.go.upgrades_replacer' not found in the grammar file") + return count + } + + fixer := strings.NewReplacer(grammar...) + excludes, _ := projectCfg.StringList("build.ast_excludes") + files, _ := ess.FilesPathExcludes(filepath.Join(aah.AppBaseDir(), "app"), true, ess.Excludes(excludes)) + for _, f := range files { + if filepath.Ext(f) != ".go" { + continue + } + if !migrateFile(f, fixer) { + continue + } + count++ + } + + return count +} + +func migrateViewFiles(projectCfg, grammarCfg *config.Config) int { + count := 0 + grammar, found := grammarCfg.StringList("file.view.upgrade_replacer") + if !found { + cliLog.Info("Config 'file.view.upgrades_replacer' not found in the grammar file") + return count + } + + fixer := strings.NewReplacer(grammar...) + files, _ := ess.FilesPath(filepath.Join(aah.AppBaseDir(), "views"), true) + fileExt := aah.AppConfig().StringDefault("view.ext", ".html") + for _, f := range files { + if filepath.Ext(f) != fileExt { + continue + } + if !migrateFile(f, fixer) { + continue + } + count++ + } + + return count +} + +func migrateFile(f string, fixer *strings.Replacer) bool { + df := strings.TrimPrefix(filepath.ToSlash(stripGoSrcPath(f)), aah.AppImportPath()+"/") + fileBytes, err := ioutil.ReadFile(f) + if err != nil { + logError(err) + cliLog.Infof(" |-- skipped: %s", df) + return false + } + + modFileBytes := []byte(fixer.Replace(string(fileBytes))) + if bytes.Equal(fileBytes, modFileBytes) { + // not modified + return false + } + + if filepath.Ext(f) == ".go" { + // format go src file + var err error + if modFileBytes, err = format.Source(modFileBytes); err != nil { + logErrorf("While formating: %s", err) + cliLog.Infof(" |-- skipped: %s", df) + return false + } + } + + if err = os.Truncate(f, 0); err != nil { + logErrorf("While truncate: %s", err) + cliLog.Infof(" |-- skipped: %s", df) + return false + } + + if err = ioutil.WriteFile(f, modFileBytes, permRWRWRW); err != nil { + logError(err) + cliLog.Infof(" |-- [ERROR] processed: %s", df) + } else { + cliLog.Infof(" |-- processed: %s", df) + } + + return true +} diff --git a/aah/new.go b/aah/new.go index cffcdf8..0e0eeb0 100644 --- a/aah/new.go +++ b/aah/new.go @@ -1,5 +1,5 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools/aah source code and usage is governed by a MIT style +// aahframework.org/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main @@ -17,33 +17,22 @@ import ( "gopkg.in/urfave/cli.v1" - aah "aahframework.org/aah.v0" + "aahframework.org/aah.v0" "aahframework.org/essentials.v0" ) -const ( - typeWeb = "web" - typeAPI = "api" - storeCookie = "cookie" - storeFile = "file" - aahTmplExt = ".atmpl" - authForm = "form" - authBasic = "basic" - authGeneric = "generic" - authNone = "none" - basicFileRealm = "file-realm" -) - var ( newCmd = cli.Command{ Name: "new", Aliases: []string{"n"}, - Usage: "Create new aah 'web' or 'api' application (interactive)", + Usage: "Creates new aah 'web', 'api' or 'websocket' application (interactive)", Description: `aah new command is an interactive program to assist you to quick start aah application. Just provide your inputs based on your use case to generate base structure to kick start your development. + Application templates are kept at '$HOME/.aah/app-templates' for CLI binary distribution. + Go to https://docs.aahframework.org to learn more and customize your aah application. `, Action: newAction, @@ -58,54 +47,47 @@ func newAction(c *cli.Context) error { fmt.Println() fmt.Println("Based on your inputs, aah CLI tool generates the aah application structure for you.") - // Collect data - importPath := getImportPath(reader) - appType := getAppType(reader) - viewEngineInfo := getViewEngine(reader, appType) - authScheme := getAuthScheme(reader, appType) - basicAuthMode := getBasicAuthMode(reader, authScheme) - passwordEncoder := getPasswordHashAlgorithm(reader, authScheme) - sessionStore := getSessionInfo(reader, appType, authScheme) - cors := getCORSInfo(reader) + // Collect inputs for aah app creation + importPath := collectImportPath(reader) + appType := collectAppType(reader) + + // Depends on application type choice, collect subsequent inputs + app := &appTmplData{ + ImportPath: importPath, + Type: appType, + TmplDelimLeft: "{{", + TmplDelimRight: "}}", + } + + switch appType { + case typeWeb: + collectInputsForWebApp(app) + case typeAPI: + collectInputsForAPIApp(app) + } // Process it - appDir := filepath.Join(gosrcDir, filepath.FromSlash(importPath)) - appName := filepath.Base(appDir) - appSessionFilepath := filepath.ToSlash(filepath.Join(appDir, "sessions")) - data := map[string]interface{}{ - "AppName": appName, - "AppType": appType, - "AppImportPath": importPath, - "AppViewEngine": viewEngineInfo[0], - "AppViewFileExt": viewEngineInfo[1], - "AppAuthScheme": authScheme, - "AppBasicAuthMode": basicAuthMode, - "AppPasswordEncoder": passwordEncoder, - "AppSessionStore": sessionStore, - "AppSessionFileStorePath": appSessionFilepath, - "AppSessionSignKey": ess.SecureRandomString(64), - "AppSessionEncKey": ess.SecureRandomString(32), - "AppAntiCSRFSignKey": ess.SecureRandomString(64), - "AppAntiCSRFEncKey": ess.SecureRandomString(32), - "AppCORSEnable": cors, - "TmplDemils": "{{.}}", - } - - if basicAuthMode == basicFileRealm { - data["AppBasicAuthFileRealmPath"] = filepath.Join(appDir, "config", "basic-realm.conf") + app.BaseDir = filepath.Join(gosrcDir, filepath.FromSlash(importPath)) + app.Name = filepath.Base(app.BaseDir) + app.SessionFileStorePath = filepath.ToSlash(filepath.Join(app.BaseDir, "sessions")) + + if app.BasicAuthMode == basicFileRealm { + app.BasicAuthFileRealmPath = filepath.Join(app.BaseDir, "config", "basic-realm.conf") } else { - data["AppBasicAuthFileRealmPath"] = "/path/to/basic-realm.conf" + app.BasicAuthFileRealmPath = "/path/to/basic-realm.conf" } - if err := createAahApp(appDir, data); err != nil { + if err := createAahApp(app.BaseDir, map[string]interface{}{ + "App": app, + }); err != nil { logFatal(err) } - fmt.Printf("\nYour aah %s application was created successfully at '%s'\n", appType, appDir) - fmt.Printf("You shall run your application via the command: 'aah run --importpath %s'\n", importPath) + fmt.Printf("\nYour aah %s application was created successfully at '%s'\n", app.Type, app.BaseDir) + fmt.Printf("You shall run your application via the command: 'aah run --importpath %s'\n", app.ImportPath) fmt.Println("\nGo to https://docs.aahframework.org to learn more and customize your aah application.") - if basicAuthMode == basicFileRealm { + if app.BasicAuthMode == basicFileRealm { fmt.Println("\nNext step:") fmt.Println("\tCreate basic auth realm file per your application requirements.") fmt.Println("\tRefer to 'https://docs.aahframework.org/authentication.html#basic-auth-file-realm-format' to create basic auth realm file.") @@ -124,7 +106,7 @@ func readInput(reader *bufio.Reader, prompt string) string { return strings.TrimSpace(input) } -func getImportPath(reader *bufio.Reader) string { +func collectImportPath(reader *bufio.Reader) string { var importPath string for { importPath = filepath.ToSlash(readInput(reader, "\nEnter your application import path: ")) @@ -140,14 +122,14 @@ func getImportPath(reader *bufio.Reader) string { return strings.Replace(importPath, " ", "-", -1) } -func getAppType(reader *bufio.Reader) string { +func collectAppType(reader *bufio.Reader) string { var appType string for { - appType = readInput(reader, "\nChoose your application type (web or api), default is 'web': ") - if ess.IsStrEmpty(appType) || appType == typeWeb || appType == typeAPI { + appType = strings.ToLower(readInput(reader, "\nChoose your application type (web, api or websocket), default is 'web': ")) + if ess.IsStrEmpty(appType) || appType == typeWeb || appType == typeAPI || appType == typeWebSocket { break } else { - logError("Unsupported new aah application type, choose either 'web or 'api'") + logError("Unsupported new aah application type, choose either 'web', 'api' or 'websocket'") appType = "" } } @@ -157,12 +139,63 @@ func getAppType(reader *bufio.Reader) string { return appType } -func getViewEngine(reader *bufio.Reader, appType string) []string { - if appType != typeWeb { - return []string{"go", ".html"} +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Collecting inputs for Web App +//______________________________________________________________________________ + +func collectInputsForWebApp(app *appTmplData) { + viewEngine(reader, app) + + authScheme(reader, app) + + if app.AuthScheme == authBasic { + basicAuthMode(reader, app) + } + + passwordHashAlgorithm(reader, app) + + sessionInfo(reader, app) + + // In the web application user may like to have API also WebSocket within it. + collectAppSubTypesChoice(reader, app) + + app.CORSEnable = collectYesOrNo(reader, "Would you like to enable CORS ([Y]es or [N]o)? default is 'N'") +} + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Collecting inputs for API App +//______________________________________________________________________________ + +func collectInputsForAPIApp(app *appTmplData) { + authScheme(reader, app) + + if app.AuthScheme == authBasic { + basicAuthMode(reader, app) + } + + passwordHashAlgorithm(reader, app) + + app.CORSEnable = collectYesOrNo(reader, "Would you like to enable CORS ([Y]es or [N]o)? default is 'N'") +} + +func collectAppSubTypesChoice(reader *bufio.Reader, app *appTmplData) { + app.SubTypes = make([]string, 0) + + // API choice + choice := collectYesOrNo(reader, "Would you like to add API [/api/v1/*] within your Web App ([Y]es or [N]o)? default is 'N'") + if choice { + app.SubTypes = append(app.SubTypes, typeAPI) } - builtInViewEngines := []string{"go", "pug"} + // WebSocket choice + choice = collectYesOrNo(reader, "Would you like to add WebSocket [/ws/*] within your Web App ([Y]es or [N]o)? default is 'N'") + if choice { + app.SubTypes = append(app.SubTypes, typeWebSocket) + } +} + +func viewEngine(reader *bufio.Reader, app *appTmplData) { + builtInViewEngines := []string{"go"} var engine string for { engine = strings.ToLower(readInput(reader, fmt.Sprintf("\nChoose your application View Engine (%s), default is 'go': ", @@ -170,127 +203,108 @@ func getViewEngine(reader *bufio.Reader, appType string) []string { if ess.IsStrEmpty(engine) || ess.IsSliceContainsString(builtInViewEngines, engine) { break } else { - logErrorf("Unsupported View Engine, choose either %s", strings.Join(builtInViewEngines, " or ")) + logErrorf("Unsupported View Engine") engine = "" } } switch engine { case "pug": - return []string{"pug", ".pug"} + app.ViewEngine = "pug" + app.ViewFileExt = ".pug" default: - return []string{"go", ".html"} + app.ViewEngine = "go" + app.ViewFileExt = ".html" } } -func getAuthScheme(reader *bufio.Reader, appType string) string { - var ( - authScheme string - schemeNames string - ) +func authScheme(reader *bufio.Reader, app *appTmplData) { + var schemeNames string - if appType == typeWeb { + if app.IsWebApp() { schemeNames = "form, basic" - } else if appType == typeAPI { + } else if app.IsAPIApp() { schemeNames = "basic, generic" } for { - authScheme = readInput(reader, fmt.Sprintf("\nChoose your application Auth Scheme (%v), default is 'none': ", schemeNames)) - if isAuthSchemeSupported(authScheme) { - if ess.IsStrEmpty(authScheme) || authScheme == authNone || - (appType == typeWeb && (authScheme == authForm || authScheme == authBasic)) || - (appType == typeAPI && (authScheme == authGeneric || authScheme == authBasic)) { + app.AuthScheme = strings.ToLower(readInput(reader, fmt.Sprintf("\nChoose your application Auth Scheme (%v), default is 'none': ", schemeNames))) + if isAuthSchemeSupported(app.AuthScheme) { + if ess.IsStrEmpty(app.AuthScheme) || app.AuthScheme == authNone || + app.IsAuthSchemeForWeb() || app.IsAuthSchemeForAPI() { break } else { - logErrorf("Application type '%v' is not applicable with auth scheme '%v'", appType, authScheme) - authScheme = "" + logErrorf("Application type '%v' is not applicable with auth scheme '%v'", app.Type, app.AuthScheme) + app.AuthScheme = "" } } else { - logErrorf("Unsupported Auth Scheme, choose either %v or 'none'", schemeNames) - authScheme = "" + logErrorf("Unsupported Auth Scheme") + app.AuthScheme = "" } } - if authScheme == authNone { - authScheme = "" + if app.AuthScheme == authNone { + app.AuthScheme = "" } - - return authScheme } -func getBasicAuthMode(reader *bufio.Reader, authScheme string) string { - var basicAuthMode string - if authScheme == authBasic { - for { - basicAuthMode = readInput(reader, "\nChoose your basic auth mode (file-realm, dynamic), default is 'file-realm': ") - if ess.IsStrEmpty(basicAuthMode) || basicAuthMode == "dynamic" { - break - } else { - logError("Unsupported Basic auth mode") - basicAuthMode = "" - } - } - - if ess.IsStrEmpty(basicAuthMode) { - basicAuthMode = basicFileRealm +func basicAuthMode(reader *bufio.Reader, app *appTmplData) { + for { + app.BasicAuthMode = strings.ToLower(readInput(reader, "\nChoose your basic auth mode (file-realm, dynamic), default is 'file-realm': ")) + if ess.IsStrEmpty(app.BasicAuthMode) || app.BasicAuthMode == "dynamic" { + break + } else { + logError("Unsupported Basic auth mode") + app.BasicAuthMode = "" } } - return basicAuthMode + if ess.IsStrEmpty(app.BasicAuthMode) { + app.BasicAuthMode = basicFileRealm + } } -func getPasswordHashAlgorithm(reader *bufio.Reader, authScheme string) string { - var authPasswordAlgorithm string - if authScheme == authForm || authScheme == authBasic { +func passwordHashAlgorithm(reader *bufio.Reader, app *appTmplData) { + if app.AuthScheme == authForm || app.AuthScheme == authBasic { for { - authPasswordAlgorithm = readInput(reader, "\nChoose your password hash algorithm (bcrypt, scrypt, pbkdf2), default is 'bcrypt': ") - - if ess.IsStrEmpty(authPasswordAlgorithm) || authPasswordAlgorithm == "bcrypt" || - authPasswordAlgorithm == "scrypt" || authPasswordAlgorithm == "pbkdf2" { + app.PasswordEncoderAlgo = strings.ToLower(readInput(reader, "\nChoose your password hash algorithm (bcrypt, scrypt, pbkdf2), default is 'bcrypt': ")) + if ess.IsStrEmpty(app.PasswordEncoderAlgo) || app.PasswordEncoderAlgo == "bcrypt" || + app.PasswordEncoderAlgo == "scrypt" || app.PasswordEncoderAlgo == "pbkdf2" { break } else { logError("Unsupported Password hash algorithm") - authPasswordAlgorithm = "" + app.PasswordEncoderAlgo = "" } } - if ess.IsStrEmpty(authPasswordAlgorithm) { - authPasswordAlgorithm = "bcrypt" + if ess.IsStrEmpty(app.PasswordEncoderAlgo) { + app.PasswordEncoderAlgo = "bcrypt" } } - return authPasswordAlgorithm } -func getSessionInfo(reader *bufio.Reader, appType, authScheme string) string { - sessionStore := storeCookie - - if appType == typeWeb && (authScheme == authForm || authScheme == authBasic) { - // Session Store +func sessionInfo(reader *bufio.Reader, app *appTmplData) { + if app.IsAuthSchemeForWeb() { for { - sessionStore = readInput(reader, "\nChoose your session store (cookie or file), default is 'cookie': ") - if ess.IsStrEmpty(sessionStore) || sessionStore == storeCookie || sessionStore == storeFile { + app.SessionStore = strings.ToLower(readInput(reader, "\nChoose your session store (cookie or file), default is 'cookie': ")) + if ess.IsStrEmpty(app.SessionStore) || app.SessionStore == storeCookie || app.SessionStore == storeFile { break } else { - logError("Unsupported session store type, choose either 'cookie or 'file") - sessionStore = "" + logError("Unsupported session store type") + app.SessionStore = "" } } - if ess.IsStrEmpty(sessionStore) { - sessionStore = storeCookie + if ess.IsStrEmpty(app.SessionStore) { + app.SessionStore = storeCookie } } - - return sessionStore } -func getCORSInfo(reader *bufio.Reader) bool { - enable := false +func collectYesOrNo(reader *bufio.Reader, msg string) bool { var input string for { - input = readInput(reader, "\nWould you like to enable CORS ([Y]es or [N]o), default is 'N': ") - input = strings.ToLower(strings.TrimSpace(input)) + input = strings.ToLower(readInput(reader, "\n"+msg+": ")) if ess.IsStrEmpty(input) { input = "n" } @@ -302,116 +316,177 @@ func getCORSInfo(reader *bufio.Reader) bool { input = "" } } + return input == "y" +} - if input == "yes" { - enable = true - } - - return enable +type file struct { + src, dst string } func createAahApp(appDir string, data map[string]interface{}) error { - appType := data["AppType"].(string) - viewEngine := data["AppViewEngine"].(string) - aahToolsPath := getAahToolsPath() - appTemplatePath := filepath.Join(aahToolsPath.Dir, "app-template") + app := data["App"].(*appTmplData) + appBaseDir := app.BaseDir + appTmplBaseDir := inferAppTmplBaseDir() + if ess.IsStrEmpty(appTmplBaseDir) { + logFatal("Unable to find aah app template at $HOME/.aah/app-templates") + } // app directory creation if err := ess.MkDirAll(appDir, permRWXRXRX); err != nil { logFatal(err) } + files := make([]file, 0) + // aah.project - processFile(appDir, appTemplatePath, filepath.Join(appTemplatePath, "aah.project.atmpl"), data) + files = append(files, file{ + src: filepath.Join(appTmplBaseDir, "aah.project.atmpl"), + dst: filepath.Join(appBaseDir, "aah.project.atmpl"), + }) - // gitignore - processFile(appDir, appTemplatePath, filepath.Join(appTemplatePath, ".gitignore"), data) + // .gitignore + files = append(files, file{ + src: filepath.Join(appTmplBaseDir, ".gitignore"), + dst: filepath.Join(appBaseDir, ".gitignore"), + }) // source - processSection(appDir, appTemplatePath, "app", data) + files = append(files, sourceTmplFiles(app, appTmplBaseDir, appBaseDir)...) // config - processSection(appDir, appTemplatePath, "config", data) + files = append(files, configTmplFiles(app.Type, appTmplBaseDir, appBaseDir)...) - if typeWeb == appType { + if app.IsWebApp() { // i18n - processSection(appDir, appTemplatePath, "i18n", data) + files = append(files, tmplFiles(filepath.Join(appTmplBaseDir, "i18n"), appTmplBaseDir, appBaseDir, true)...) // static - processSection(appDir, appTemplatePath, "static", data) + files = append(files, tmplFiles(filepath.Join(appTmplBaseDir, "static"), appTmplBaseDir, appBaseDir, true)...) // views - switch viewEngine { - case "pug": - processSection(appDir, appTemplatePath, filepath.Join("views", "pug"), data) - default: // go - processSection(appDir, appTemplatePath, filepath.Join("views", "go"), data) - } + files = append(files, viewTmplFiles(app.ViewEngine, appTmplBaseDir, appBaseDir)...) + } + + // processing app template files + for _, f := range files { + processFile(appBaseDir, f, data) } return nil } -func processSection(destDir, srcDir, dir string, data map[string]interface{}) { - files, _ := ess.FilesPath(filepath.Join(srcDir, dir), true) - for _, v := range files { - if strings.Contains(v, "/app/security/") { - authScheme := data["AppAuthScheme"].(string) - if !ess.IsStrEmpty(authScheme) && authScheme != authNone { - if authScheme == authBasic { - basicAuthMode := data["AppBasicAuthMode"].(string) - if basicAuthMode == "dynamic" { - processFile(destDir, srcDir, v, data) - } - } else { - processFile(destDir, srcDir, v, data) - } - } - } else { - processFile(destDir, srcDir, v, data) +func configTmplFiles(appType, appTmplBaseDir, appBaseDir string) []file { + srcDir := filepath.Join(appTmplBaseDir, "config") + flist, _ := ess.FilesPath(srcDir, true) + files := []file{} + for _, f := range flist { + if appType == typeWebSocket && strings.HasSuffix(f, "security.conf.atmpl") { + continue } + files = append(files, file{src: f, dst: filepath.Join(appBaseDir, f[len(appTmplBaseDir):])}) } + return files } -func processFile(destDir, srcDir, f string, data map[string]interface{}) { - dfPath := getDestPath(destDir, srcDir, f) - dfDir := filepath.Dir(dfPath) - if !ess.IsFileExists(dfDir) { - _ = ess.MkDirAll(dfDir, permRWXRXRX) +func sourceTmplFiles(app *appTmplData, appTmplBaseDir, appBaseDir string) []file { + files := []file{} + + fn := func(srcDir string, recur bool) { + flist, _ := ess.FilesPath(srcDir, recur) + for _, f := range flist { + files = append(files, file{src: f, dst: filepath.Join(appBaseDir, f[len(appTmplBaseDir):])}) + } } - sf, _ := os.Open(f) - df, _ := os.Create(dfPath) + // /app + fn(filepath.Join(appTmplBaseDir, "app"), false) + + // /app/controllers + if app.IsWebApp() || app.IsAPIApp() { + fn(filepath.Join(appTmplBaseDir, "app", "controllers"), false) - if strings.HasSuffix(f, aahTmplExt) { - sfbytes, _ := ioutil.ReadAll(sf) - if err := renderTmpl(df, string(sfbytes), data); err != nil { - logFatalf("Unable to process file '%s': %s", dfPath, err) - } - } else { - _, _ = io.Copy(df, sf) } - _ = ess.ApplyFileMode(dfPath, permRWRWRW) - ess.CloseQuietly(sf, df) + if app.IsAPIApp() { + fn(filepath.Join(appTmplBaseDir, "app", "controllers", "v1"), false) + } + + if app.IsSubTypeAPI() { + files = append(files, file{ + src: filepath.Join(appTmplBaseDir, filepath.FromSlash("app/controllers/v1/value.go.atmpl")), + dst: filepath.Join(appBaseDir, filepath.FromSlash("app/controllers/api/v1/value.go")), + }) + } + + // /app/websockets + if app.IsWebSocketApp() || app.IsSubTypeWebSocket() { + fn(filepath.Join(appTmplBaseDir, "app", "websockets"), true) + } + + // /app/models + files = append(files, file{ + src: filepath.Join(appTmplBaseDir, filepath.FromSlash("app/models/greet.go")), + dst: filepath.Join(appBaseDir, filepath.FromSlash("app/models/greet.go")), + }) + if app.IsAPIApp() || app.IsSubTypeAPI() { + files = append(files, file{ + src: filepath.Join(appTmplBaseDir, filepath.FromSlash("app/models/value.go")), + dst: filepath.Join(appBaseDir, filepath.FromSlash("app/models/value.go")), + }) + } + + // /app/security + if app.IsSecurityEnabled() && app.BasicAuthMode != basicFileRealm { + fn(filepath.Join(appTmplBaseDir, "app", "security"), true) + } + + return files } -func getDestPath(destDir, srcDir, v string) string { - dpath := v[len(srcDir):] +func viewTmplFiles(engName, appTmplBaseDir, appBaseDir string) []file { + srcDir := filepath.Join(appTmplBaseDir, "views", engName) + flist, _ := ess.FilesPath(srcDir, true) + files := []file{} + for _, f := range flist { + files = append(files, file{src: f, dst: filepath.Join(appBaseDir, "views", f[len(srcDir):])}) + } + return files +} - // Handle specific - views files for multiple engine - if strings.HasPrefix(dpath[1:], "views") { - r := strings.SplitAfterN(dpath, string(filepath.Separator), 4) - dpath = filepath.Join(r[1], r[3]) +func tmplFiles(srcDir, appTmplBaseDir, appBaseDir string, recur bool) []file { + flist, _ := ess.FilesPath(srcDir, recur) + files := []file{} + for _, f := range flist { + files = append(files, file{src: f, dst: filepath.Join(appBaseDir, f[len(appTmplBaseDir):])}) } + return files +} + +func processFile(appBaseDir string, f file, data map[string]interface{}) { + dst := strings.TrimSuffix(f.dst, aahTmplExt) - dpath = filepath.Join(destDir, dpath) + // create dst dir if not exists + dstDir := filepath.Dir(dst) + if !ess.IsFileExists(dstDir) { + _ = ess.MkDirAll(dstDir, permRWXRXRX) + } - if strings.HasSuffix(v, aahTmplExt) { - dpath = dpath[:len(dpath)-len(aahTmplExt)] + // open src and create dst + sf, _ := os.Open(f.src) + df, _ := os.Create(dst) + + // render or write it directly + if strings.HasSuffix(f.src, aahTmplExt) { + sfbytes, _ := ioutil.ReadAll(sf) + if err := renderTmpl(df, string(sfbytes), data); err != nil { + logFatalf("Unable to process file '%s': %s", dst, err) + } + } else { + _, _ = io.Copy(df, sf) } - return dpath + _ = ess.ApplyFileMode(dst, permRWRWRW) + ess.CloseQuietly(sf, df) } func isAuthSchemeSupported(authScheme string) bool { @@ -425,22 +500,27 @@ func checkAndGenerateInitgoFile(importPath, baseDir string) { cliLog.Warn("***** In v0.10 'init.go' file introduced to evolve aah framework." + " Since its not found, generating 'init.go' file. Please add it to your version control. *****\n") - aahToolsPath := getAahToolsPath() + aahToolsPath := aahToolsPath() appTemplatePath := filepath.Join(aahToolsPath.Dir, "app-template") appType := typeAPI if ess.IsFileExists(filepath.Join(baseDir, "views")) { appType = typeWeb } data := map[string]interface{}{ - "AppType": appType, - "AppViewEngine": aah.AppConfig().StringDefault("view.engine", "go"), + "App": &appTmplData{ + Type: appType, + ViewEngine: aah.AppConfig().StringDefault("view.engine", "go"), + }, } - processFile(baseDir, appTemplatePath, filepath.Join(appTemplatePath, "app", "init.go"+aahTmplExt), data) + processFile(baseDir, file{ + src: filepath.Join(appTemplatePath, "app", "init.go.atmpl"), + dst: filepath.Join(baseDir, "app", "init.go"), + }, data) } } -func getAahToolsPath() *build.Package { +func aahToolsPath() *build.Package { aahToolsPath, err := build.Import(path.Join(libImportPath("tools"), "aah"), "", build.FindOnly) if err != nil { logFatal(err) diff --git a/aah/publish_cli.sh b/aah/publish_cli.sh new file mode 100755 index 0000000..03aab10 --- /dev/null +++ b/aah/publish_cli.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# Creator: Jeevanandam M. (https://github.com/jeevatkm) jeeva@myjeeva.com +# License: MIT + +# currently this script focused on mac os + +BASEDIR=$(dirname "$0") +cd $BASEDIR + +# Inputs +version=$(cat version.go | perl -pe '($_)=/([0-9]+([.][0-9]+)+(-edge)?)/') +echo "Publish for version: $version" + +# Build for macOS +echo "Starting aah CLI build for macOS" +build_dir="/tmp/aah_cli_mac_$version" +mkdir -p $build_dir +env GOOS=darwin GOARCH=amd64 go build -o $build_dir/aah -ldflags="-s -w -X main.CliPackaged=true" +cd $build_dir && zip aah_darwin_amd64.zip aah + +# sha256 and upload to aah server +sha256=$(/usr/bin/shasum -a 256 $build_dir/aah_darwin_amd64.zip | cut -d " " -f 1) +echo "sha256 $sha256" +echo "Uploading aah CLI macOS binary to aah server" +ssh root@aahframework.org "mkdir -p /srv/www/aahframework.org/public/releases/cli/$version" +scp $build_dir/aah_darwin_amd64.zip root@aahframework.org:/srv/www/aahframework.org/public/releases/cli/$version + +# update homebrew tap +echo "Updating Homebrew Tap for macOS" +if [ ! -d "$GOPATH/src/github.com/go-aah/homebrew-tap" ]; then + git clone git@github.com:go-aah/homebrew-tap.git $GOPATH/src/github.com/go-aah/homebrew-tap +fi +cd $GOPATH/src/github.com/go-aah/homebrew-tap +sed -i '' -e 's/cli\/.*\/aah_darwin_amd64.zip/cli\/'"$version"'\/aah_darwin_amd64.zip/g' ./Formula/aah.rb +sed -i '' -e 's/sha256 ".*"/sha256 "'"$sha256"'"/g' ./Formula/aah.rb +sed -i '' -e 's/version ".*"/version "'"$version"'"/g' ./Formula/aah.rb +git add -u && git commit -m "brew tap update with $version release" && git push + +# Cleanup +echo "Cleanup after macOS build" +rm -rf $build_dir + +# .. next upcoming OS support \ No newline at end of file diff --git a/aah/run.go b/aah/run.go index 666113b..8361c16 100644 --- a/aah/run.go +++ b/aah/run.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "io/ioutil" + "net" "net/http" "net/http/httputil" "net/url" @@ -33,9 +34,9 @@ import ( var runCmd = cli.Command{ Name: "run", Aliases: []string{"r"}, - Usage: "Run aah framework application (supports hot-reload)", - Description: `Run the aah framework web/api application. It supports hot-reload, just code and refresh the browser - to see your updates. + Usage: "Runs aah application (supports hot-reload)", + Description: `Runs aah application. It supports hot-reload (just code and refresh the browser + to see your updates). Examples of short and long flags: aah run @@ -45,12 +46,12 @@ var runCmd = cli.Command{ aah run -i github.com/user/appname -e qa aah run -i github.com/user/appname -e qa -c /path/to/config/external.conf - aah run --importpath github.com/username/name - aah run --importpath github.com/username/name --envprofile qa - aah run --importpath github.com/username/name --envprofile qa --config /path/to/config/external.conf + aah run --importpath github.com/user/appname + aah run --importpath github.com/user/appname --envprofile qa + aah run --importpath github.com/user/appname --envprofile qa --config /path/to/config/external.conf - Note: It is recommended to use build and deploy approach instead of - using 'aah run' for production use.`, + Note: For production use, it is recommended to follow build and deploy approach instead of + using 'aah run'.`, Flags: []cli.Flag{ cli.StringFlag{ Name: "i, importpath", @@ -58,7 +59,7 @@ var runCmd = cli.Command{ }, cli.StringFlag{ Name: "e, envprofile", - Usage: "Environment profile name to activate. e.g: dev, qa, prod"}, + Usage: "Environment profile name to activate (e.g: dev, qa, prod)"}, cli.StringFlag{ Name: "c, config", Usage: "External config file for overriding aah.conf values", @@ -69,19 +70,19 @@ var runCmd = cli.Command{ type ( hotReload struct { - ProxyURL *url.URL + ChangedOrError bool + IsSSL bool ProxyPort string BaseDir string Addr string Port string - IsSSL bool SSLCert string SSLKey string Args []string + ProxyURL *url.URL Proxy *httputil.ReverseProxy Process *process ProjectConfig *config.Config - ChangedOrError bool Watcher *watcher.Watcher } @@ -98,7 +99,7 @@ type ( ) func runAction(c *cli.Context) error { - importPath := getAppImportPath(c) + importPath := appImportPath(c) appStartArgs := []string{} configPath := getNonEmptyAbsPath(c.String("c"), c.String("config")) @@ -111,7 +112,9 @@ func runAction(c *cli.Context) error { appStartArgs = append(appStartArgs, "-profile", envProfile) } - aah.Init(importPath) + if err := aah.Init(importPath); err != nil { + logFatal(err) + } projectCfg := aahProjectCfg(aah.AppBaseDir()) cliLog = initCLILogger(projectCfg) @@ -159,6 +162,7 @@ func runAction(c *cli.Context) error { Cmd: "RunCmd", ProjectCfg: projectCfg, AppPack: false, + AppEmbed: false, }) if err != nil { logFatal(err) @@ -189,6 +193,7 @@ func (hr *hotReload) Start() { server.ErrorLog = hr.Proxy.ErrorLog if hr.IsSSL { + /* #nosec Its required for development activity */ hr.Proxy.Transport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} err = server.ListenAndServeTLS(hr.SSLCert, hr.SSLKey) } else { @@ -215,12 +220,14 @@ func (hr *hotReload) CompileAndStart() error { ProxyPort: hr.ProxyPort, ProjectCfg: hr.ProjectConfig, AppPack: false, + AppEmbed: false, }) if err != nil { return err } hr.Process = &process{ + // #nosec cmd: exec.Command(appBinary, hr.Args...), nw: ¬ifyWriter{ w: os.Stdout, @@ -257,7 +264,7 @@ func (hr *hotReload) ServeHTTP(w http.ResponseWriter, r *http.Request) { if hr.ChangedOrError { cliLog.Info("Application file change(s) detected") hr.ChangedOrError = false - hr.Watcher.Close() + ess.CloseQuietly(hr.Watcher) hr.Stop() if err := hr.CompileAndStart(); err != nil { logError(err) @@ -267,7 +274,68 @@ func (hr *hotReload) ServeHTTP(w http.ResponseWriter, r *http.Request) { } waitForConnReady(hr.ProxyPort) } - hr.Proxy.ServeHTTP(w, r) + hr.ProxyServe(w, r) +} + +// Typically for HTTP method: CONNECT and WebSocket needs tunneling, we cannot +// use `httputil.ReverseProxy` since it handles Hop-By-Hop headers on proxy +// connection - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#hbh +func (hr *hotReload) needTunneling(r *http.Request) bool { + return r.Method == http.MethodConnect || + strings.EqualFold(strings.ToLower(r.Header.Get("Upgrade")), "websocket") +} + +func (hr *hotReload) ProxyServe(w http.ResponseWriter, r *http.Request) { + if hr.needTunneling(r) { + hr.tunnel(w, r) + } else { + hr.Proxy.ServeHTTP(w, r) + } +} + +func (hr *hotReload) tunnel(w http.ResponseWriter, r *http.Request) { + var peer net.Conn + var err error + address := fmt.Sprintf("%s:%s", hr.Addr, hr.ProxyPort) + if hr.IsSSL { + /* #nosec Its required for development activity */ + peer, err = tls.Dial("tcp", address, &tls.Config{InsecureSkipVerify: true}) + } else { + peer, err = net.DialTimeout("tcp", address, 10*time.Second) + } + + if err != nil { + http.Error(w, "Error tunneling with peer", http.StatusBadGateway) + return + } + + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Error hijacking is not supported", http.StatusInternalServerError) + return + } + + conn, _, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + + if err = r.Write(peer); err != nil { + logErrorf("Error tunneling data to peer: %s", err) + return + } + + go func() { + defer ess.CloseQuietly(peer) + defer ess.CloseQuietly(conn) + _, _ = io.Copy(peer, conn) + }() + go func() { + defer ess.CloseQuietly(conn) + defer ess.CloseQuietly(peer) + _, _ = io.Copy(conn, peer) + }() } func startWatcher(projectCfg *config.Config, baseDir string, w *watcher.Watcher, watch chan<- bool) { @@ -282,10 +350,12 @@ func startWatcher(projectCfg *config.Config, baseDir string, w *watcher.Watcher, for { select { case e := <-w.Event: - if e.Op == watcher.Create { - _ = w.Add(e.Path) + if !e.IsDir() { + watch <- true + if e.Op == watcher.Create { + _ = w.Add(e.Path) + } } - watch <- true case err := <-w.Error: if err == watcher.ErrWatchedFileDeleted { // treat as trace information, not an error @@ -300,7 +370,7 @@ func startWatcher(projectCfg *config.Config, baseDir string, w *watcher.Watcher, if cliLog.IsLevelTrace() { var fileList []string for path := range w.WatchedFiles() { - fileList = append(fileList, stripGoPath(path)) + fileList = append(fileList, stripGoSrcPath(path)) } cliLog.Trace("Watched files:\n\t", strings.Join(fileList, "\n\t")) } @@ -329,11 +399,9 @@ func loadWatchFiles(projectCfg *config.Config, baseDir string, w *watcher.Watche } // standard dir ignore list for aah project - dirExcludes = append(dirExcludes, "build", "static", "vendor", "tests", "logs") + dirExcludes = append(dirExcludes, "build", "static", "vendor", "views", "tests", "logs") dirs, _ := ess.DirsPathExcludes(baseDir, true, dirExcludes) - // dirs = excludeAndCreateSlice(dirs, baseDir) - // dirs = excludeAndCreateSlice(dirs, filepath.Join(baseDir, "app")) for _, d := range dirs { if err := w.Add(d); err != nil { logErrorf("Unable add watch for '%v'", d) diff --git a/aah/switch.go b/aah/switch.go index a36fc21..7bcd7da 100644 --- a/aah/switch.go +++ b/aah/switch.go @@ -5,6 +5,7 @@ package main import ( + "fmt" "strings" "gopkg.in/urfave/cli.v1" @@ -13,13 +14,14 @@ import ( const ( releaseBranchName = "master" edgeBranchName = "v0-edge" + emojiThumpsUp = `👍` ) var switchCmd = cli.Command{ Name: "switch", Aliases: []string{"s"}, - Usage: "Switch between aah release and edge version", - Description: `Provides an ability to switch between aah release (currently on your GOPATH) and latest edge version. + Usage: "Switches between aah release and edge version", + Description: `Provides an ability to switch between aah release and latest edge version. Examples of short and long flags: aah s @@ -34,7 +36,7 @@ var switchCmd = cli.Command{ aah switch --refresh Note: - - Currently it works with only GOPATH. Gradually I will add vendorize support too. + - Currently it works with only GOPATH. - It always operates on latest edge version and current release version on your GOPATH, specific version is not supported.`, Flags: []cli.Flag{ cli.StringFlag{ @@ -80,21 +82,25 @@ func whoami(branchName string) error { func doRefresh(branchName string) error { fname := friendlyName(branchName) if branchName == releaseBranchName { - cliLog.Infof("Refresh is only applicable to edge version, currently you're on '%s' version.\n", fname) + cliLog.Infof("Refresh option is for 'edge' version only, currently you're on '%s' version.\n", fname) cliLog.Infof("Use 'aah update' command to update your aah to the latest release version on your GOPATH.\n") return nil } cliLog.Infof("Refreshing aah '%s' version ...\n", fname) + aahLibDirs := aahLibraryDirs() + // Refresh to latest edge codebase - refreshCodebase(libNames...) + refreshLibCode(aahLibDirs) // Refresh dependencies in grace mode - fetchAahDeps() + fetchLibDeps() + + checkoutBranch(aahLibDirs, edgeBranchName) - // Install aah CLI for the currently version - installAahCLI() + // Install aah CLI for current version + installCLI() cliLog.Infof("You have successfully refreshed aah '%s' version.\n", fname) return nil @@ -103,8 +109,17 @@ func doRefresh(branchName string) error { func doSwitch(branchName, target string) error { fname := friendlyName(branchName) if target == fname { - cliLog.Infof("You're already on '%s' version.\n", fname) - cliLog.Infof("To switch to latest release version. Run 'aah switch -v release'\n") + cliLog.Infof("Currently you're on aah '%s' version.\n", fname) + cliLog.Infof("To switch to release version. Run 'aah s -v release'\n") + + if fname == "edge" { + ans := collectYesOrNo(reader, "Would you like to refresh 'edge' to latest updates? ([Y]es or [N]o), default is 'N'") + fmt.Println() + if ans { + doRefresh(branchName) + } + } + return nil } @@ -115,30 +130,29 @@ func doSwitch(branchName, target string) error { toBranch = releaseBranchName } - cliLog.Infof("Switching aah version to '%s' ...\n", friendlyName(toBranch)) + cliLog.Infof("Switching aah to '%s' version ...\n", friendlyName(toBranch)) - // Checkout the branch - for _, lib := range libNames { - if err := gitCheckout(libDir(lib), toBranch); err != nil { - logFatalf("Error occurred which switching aah version: %s", err) - } - } + // // Checkout the branch + aahLibDirs := aahLibraryDirs() + checkoutBranch(aahLibDirs, toBranch) if toBranch == edgeBranchName { - cliLog.Infof("Refreshing aah version to latest '%s' ...\n", friendlyName(toBranch)) - refreshCodebase(libNames...) + cliLog.Infof("Refreshing aah to latest '%s' updates ...\n", friendlyName(toBranch)) + refreshLibCode(aahLibDirs) } // Refresh dependencies in grace mode - fetchAahDeps() + fetchLibDeps() + + checkoutBranch(aahLibDirs, toBranch) - // Install aah CLI for the currently version - installAahCLI() + // Install aah CLI for current version + installCLI() if toBranch == releaseBranchName { - cliLog.Infof("You have successfully switched to aah 'release' version.\n") + cliLog.Infof("You have successfully switched %s.\n", emojiThumpsUp) } else { - cliLog.Infof("You have successfully switched to aah 'edge' version, your feedback is appreciated.\n") + cliLog.Infof("You have successfully switched %s, your feedback is appreciated.\n", emojiThumpsUp) } return nil } diff --git a/aah/test.cfg b/aah/test.cfg deleted file mode 100644 index 500c45a..0000000 --- a/aah/test.cfg +++ /dev/null @@ -1,60 +0,0 @@ -env { - #active = "prod" - - dev { - http { - port = 5000 - } - } - - - # -------------------------------- - # Production Configuration Section - # -------------------------------- - prod { - - # -------------------- - # Logger configuration - # -------------------- - log { - receiver = "file" - file = "testing-name.log" - } - - # ------------------------- - # Render/View configuration - # ------------------------- - render { - default = "html" - pretty = false - gzip = true - - multipart { - enable = true - size = "32mb" - } - } - - # ------------------------ - # DataSource configuration - # ------------------------ - datasource { - default = "mydb1" - - mydb1 { - driver = "" - url = "" - max_idle_conns = 5 - max_open_conns = 10 - } - - mydb2 { - driver = "" - url = "" - max_idle_conns = 5 - max_open_conns = 10 - } - } - - } -} diff --git a/aah/update.go b/aah/update.go index 78fe81e..c3f1d95 100644 --- a/aah/update.go +++ b/aah/update.go @@ -14,15 +14,16 @@ import ( var updateCmd = cli.Command{ Name: "update", Aliases: []string{"u"}, - Usage: "Update your aah to the latest release version on your GOPATH", - Description: `Provides an easy and convenient way to update your aah to the latest release version on your GOPATH. + Usage: "Updates aah to the latest release version on your GOPATH", + Description: `Provides an easy and convenient way to update your aah framework version +to the latest release version on your GOPATH. Examples of short and long flags: aah u aah update Note: - - Currently it works with only GOPATH. Gradually I will add vendorize support too. + - Currently it works with only GOPATH. - It always operates on aah latest release version, specific version is not supported. `, Action: updateAction, @@ -33,13 +34,17 @@ func updateAction(c *cli.Context) error { branchName := gitBranchName(libDir("aah")) if branchName != releaseBranchName { fmt.Printf("Update command only applicable to aah release version.\n") - fmt.Printf("Currently you're on aah 'edge' version, use 'aah switch --refresh' command to get latest edge version.\n\n") + fmt.Printf("Currently you're on aah 'edge' version, use 'aah s -r' command to refresh edge version.\n\n") return nil } fmt.Printf("Update aah version to the latest release ...\n\n") - - args := []string{"get", "-u", path.Join(importPrefix, "tools.v0", "aah")} + gocmdName := goCmdName() + args := []string{"get"} + if gocmdName == "go" { + args = append(args, "-u") + } + args = append(args, path.Join(importPrefix, "tools.v0", "aah")) if _, err := execCmd(gocmd, args, false); err != nil { logFatalf("Unable to update aah to the latest release version: %s", err) } diff --git a/aah/util.go b/aah/util.go index 16d212a..334bf2e 100644 --- a/aah/util.go +++ b/aah/util.go @@ -1,5 +1,5 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools/aah source code and usage is governed by a MIT style +// aahframework.org/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main @@ -7,14 +7,16 @@ package main import ( "fmt" "io" + "io/ioutil" "net" + "net/http" "os" "os/exec" - "path" "path/filepath" "runtime" "strconv" "strings" + "sync" "text/template" "time" @@ -26,7 +28,7 @@ import ( ) func importPathRelwd() string { - pwd, _ := os.Getwd() + pwd, _ := os.Getwd() // #nosec var importPath string if idx := strings.Index(pwd, "src"); idx > 0 { @@ -103,21 +105,17 @@ func getAppVersion(appBaseDir string, cfg *config.Config) string { version := cfg.StringDefault("build.version", "") // git describe - if gitcmd, err := exec.LookPath("git"); err == nil { - if !ess.IsFileExists(filepath.Join(appBaseDir, ".git")) { - return version - } - - gitArgs := []string{"-C", appBaseDir, "describe", "--always", "--dirty"} - output, err := execCmd(gitcmd, gitArgs, false) - if err != nil { - return version - } + if !ess.IsFileExists(filepath.Join(appBaseDir, ".git")) { + return version + } - version = strings.TrimSpace(output) + gitArgs := []string{"-C", appBaseDir, "describe", "--always", "--dirty"} + output, err := execCmd(gitcmd, gitArgs, false) + if err != nil { + return version } - return version + return strings.TrimSpace(output) } // getBuildDate method returns application build date, which used to display @@ -137,7 +135,8 @@ func getBuildDate() string { } func execCmd(cmdName string, args []string, stdout bool) (string, error) { - cmd := exec.Command(cmdName, args...) + cmd := exec.Command(cmdName, args...) // #nosec + cliLog = initCLILogger(nil) cliLog.Trace("Executing ", strings.Join(cmd.Args, " ")) if stdout { @@ -159,7 +158,7 @@ func execCmd(cmdName string, args []string, stdout bool) (string, error) { } func renderTmpl(w io.Writer, text string, data interface{}) error { - tmpl := template.Must(template.New("").Parse(text)) + tmpl := template.Must(template.New("").Funcs(appTemplateFuncs).Parse(text)) return tmpl.Execute(w, data) } @@ -220,7 +219,7 @@ func isAahProject(file string) bool { } func findAvailablePort() string { - lstn, err := net.Listen("tcp", ":0") + lstn, err := net.Listen("tcp", ":0") // #nosec if err != nil { logError(err) return "0" @@ -231,6 +230,9 @@ func findAvailablePort() string { } func initCLILogger(cfg *config.Config) *log.Logger { + if cliLog != nil { + return cliLog + } if cfg == nil { cfg, _ = config.ParseString("") } @@ -258,16 +260,15 @@ func initCLILogger(cfg *config.Config) *log.Logger { } func gitCheckout(dir, branch string) error { - if gitcmd, err := exec.LookPath("git"); err == nil { - gitArgs := []string{"-C", dir, "checkout", branch} - _, err := execCmd(gitcmd, gitArgs, false) + if ess.IsFileExists(filepath.Join(dir, ".git")) { + _, err := execCmd(gitcmd, []string{"-C", dir, "checkout", branch}, false) return err } return nil } func libImportPath(name string) string { - return fmt.Sprintf("%s/%s.%s", importPrefix, name, versionSeries) + return fmt.Sprintf("%s/%s.%s", importPrefix, name, inferVersionSeries()) } func libDir(name string) string { @@ -281,18 +282,18 @@ func gitBranchName(dir string) string { return "" } - if gitcmd, err := exec.LookPath("git"); err == nil { - gitArgs := []string{"-C", dir, "rev-parse", "--abbrev-ref", "HEAD"} - output, _ := execCmd(gitcmd, gitArgs, false) - return strings.TrimSpace(output) + if !ess.IsFileExists(filepath.Join(dir, ".git")) { + return "" } - return "" + + gitArgs := []string{"-C", dir, "rev-parse", "--abbrev-ref", "HEAD"} + output, _ := execCmd(gitcmd, gitArgs, false) + return strings.TrimSpace(output) } func gitPull(dir string) error { - if gitcmd, err := exec.LookPath("git"); err == nil { - gitArgs := []string{"-C", dir, "pull"} - _, err := execCmd(gitcmd, gitArgs, false) + if ess.IsFileExists(filepath.Join(dir, ".git")) { + _, err := execCmd(gitcmd, []string{"-C", dir, "pull"}, false) return err } return nil @@ -300,8 +301,7 @@ func gitPull(dir string) error { func goGet(pkgs ...string) error { for _, pkg := range pkgs { - args := []string{"get", pkg} - if _, err := execCmd(gocmd, args, false); err != nil { + if _, err := execCmd(gocmd, []string{"get", pkg}, false); err != nil { return err } } @@ -324,28 +324,54 @@ func waitForConnReady(port string) { } } -func installAahCLI() { - args := []string{"install", path.Join(importPrefix, "tools.v0", "aah")} +func installCLI() { + if CliPackaged != "" { + return + } + verser := inferVersionSeries() + args := []string{"install", fmt.Sprintf("%s/tools.%s/aah", importPrefix, verser)} if _, err := execCmd(gocmd, args, false); err != nil { - logFatalf("Unable to compile CLI tool: %s", err) + logFatalf("Unable to compile aah CLI: %s", err) } } -func fetchAahDeps() { - if err := goGet(path.Join(importPrefix, "tools.v0", "aah", "...")); err != nil { - logFatalf("Unable to refresh dependencies: %s", err) +func fetchLibDeps() { + var notEixstsList []string + var wg sync.WaitGroup + for _, i := range aahImportPaths() { + wg.Add(1) + go func(p string) { + defer wg.Done() + if neList := inferNotExistsDeps(libDependencyImports(p)); len(neList) > 0 { + notEixstsList = append(notEixstsList, neList...) + } + }(i) } -} + wg.Wait() -func refreshCodebase(names ...string) { - for _, lib := range names { - if err := gitPull(libDir(lib)); err != nil { - logFatalf("Unable to refresh library: %s.%s", lib, versionSeries) + // infer not exists libraries on GOPATH using importpath + if len(notEixstsList) > 0 { + if err := goGet(notEixstsList...); err != nil { + logFatalf("Error during go get: %s", err) } } } -func getAppImportPath(c *cli.Context) string { +func refreshLibCode(libDirs []string) { + var wg sync.WaitGroup + for _, dir := range libDirs { + wg.Add(1) + go func(d string) { + defer wg.Done() + if err := gitPull(d); err != nil { + logErrorf("Unable to refresh library, possibliy you may have local changes: %s", filepath.Base(d)) + } + }(dir) + } + wg.Wait() +} + +func appImportPath(c *cli.Context) string { importPath := firstNonEmpty(c.String("i"), c.String("importpath")) if ess.IsStrEmpty(importPath) { importPath = importPathRelwd() @@ -359,15 +385,23 @@ func getAppImportPath(c *cli.Context) string { } func logFatal(v ...interface{}) { - _ = log.SetPattern("%level %message") - fatal(v...) - _ = log.SetPattern(log.DefaultPattern) + if cliLog == nil { + _ = log.SetPattern("%level %message") + fatal(v...) + _ = log.SetPattern(log.DefaultPattern) + } else { + cliLog.Fatal(append([]interface{}{"FATAL"}, v...)) + } } func logFatalf(format string, v ...interface{}) { - _ = log.SetPattern("%level %message") - fatalf(format, v...) - _ = log.SetPattern(log.DefaultPattern) + if cliLog == nil { + _ = log.SetPattern("%level %message") + fatalf(format, v...) + _ = log.SetPattern(log.DefaultPattern) + } else { + cliLog.Fatalf("FATAL "+format, v...) + } } func logError(v ...interface{}) { @@ -377,3 +411,199 @@ func logError(v ...interface{}) { func logErrorf(format string, v ...interface{}) { cliLog.Errorf("ERROR "+format, v...) } + +func stripGoSrcPath(pkgFilePath string) string { + idx := strings.Index(pkgFilePath, "src") + return filepath.Clean(pkgFilePath[idx+4:]) +} + +func inferVersionSeries() string { + verser := "v0" + for _, d := range aahLibraryDirs() { + baseName := filepath.Base(d) + if strings.HasPrefix(baseName, "aah") { + return strings.Split(baseName, ".")[1] + } + } + return verser +} + +func aahLibraryDirs() []string { + dirs, err := ess.DirsPathExcludes(filepath.Join(gosrcDir, importPrefix), false, ess.Excludes{"examples"}) + if err != nil { + return []string{} + } + return dirs +} + +func aahImportPaths() []string { + var importPaths []string + gsLen := len(gosrcDir) + for _, d := range aahLibraryDirs() { + p := d[gsLen+1:] + if strings.Contains(p, "tools") { + p += "/aah" // Note: this import path so always forward slash + } + importPaths = append(importPaths, p) + } + return importPaths +} + +func checkoutBranch(aahLibDirs []string, branchName string) { + var wg sync.WaitGroup + for _, dir := range aahLibDirs { + wg.Add(1) + go func(d string) { + defer wg.Done() + baseName := filepath.Base(d) + if err := gitCheckout(d, branchName); err != nil { + logErrorf("Unable to switch library version, possibliy you may have local changes[%s]: %s", baseName, err) + } + cliLog.Tracef("Library '%s' have been switched to '%s' successfully", baseName, branchName) + }(dir) + } + wg.Wait() +} + +func libDependencyImports(importPath string) []string { + var depList []string + str, err := execCmd(gocmd, []string{"list", "-f", "{{.Imports}}", importPath}, false) + if err != nil { + logErrorf("Unable to infer dependency imports for %s", importPath) + return []string{} + } + + str = strings.TrimSpace(str) + for _, i := range strings.Fields(str[1 : len(str)-1]) { + depList = append(depList, strings.TrimSpace(i)) + } + + return depList +} + +func inferNotExistsDeps(depList []string) []string { + var notExistsList []string + for _, d := range depList { + if !ess.IsImportPathExists(d) && !ess.IsSliceContainsString(notExistsList, d) { + notExistsList = append(notExistsList, d) + } + } + return notExistsList +} + +func readVersionNo(baseDir string) (string, error) { + versionFile := filepath.Join(baseDir, "version.go") + if !ess.IsFileExists(versionFile) { + return "", errVersionNotExists + } + + bytes, err := ioutil.ReadFile(versionFile) + if err != nil { + return "", err + } + + result := verRegex.FindStringSubmatch(string(bytes)) + if len(result) >= 2 { + return result[1], nil + } + + return "Unknown", nil +} + +func cleanupAutoGenFiles(appBaseDir string) { + appMainGoFile := filepath.Join(appBaseDir, "app", "aah.go") + appBuildDir := filepath.Join(appBaseDir, "build") + cliLog.Debugf("Cleaning %s", appMainGoFile) + cliLog.Debugf("Cleaning build directory %s", appBuildDir) + ess.DeleteFiles(appMainGoFile, appBuildDir) +} + +func cleanupAutoGenVFSFiles(appBaseDir string) { + vfsFiles, _ := filepath.Glob(filepath.Join(appBaseDir, "app", "aah_*_vfs.go")) + if len(vfsFiles) > 0 { + cliLog.Debugf("Cleaning embed files %s", strings.Join(vfsFiles, "\n\t")) + ess.DeleteFiles(vfsFiles...) + } +} + +func toLowerCamelCase(v string) string { + var st []byte + for idx := 0; idx < len(v); idx++ { + c := v[idx] + if c == '_' || c == ' ' { + idx++ + st = append(st, []byte(strings.ToUpper(string(v[idx])))...) + } else { + st = append(st, c) + } + } + return string(st) +} + +func inferAppTmplBaseDir() string { + aahBasePath := aahPath() + baseDir := filepath.Join(aahBasePath, "app-templates", "generic") + if !ess.IsFileExists(baseDir) { + tmplRepo := "https://github.com/go-aah/app-templates.git" + cliLog.Debugf("Downloading aah quick start app templates from %s", tmplRepo) + gitArgs := []string{"clone", tmplRepo, filepath.Dir(baseDir)} + if _, err := execCmd(gitcmd, gitArgs, false); err != nil { + logErrorf("Unable to download aah app-template from %s", tmplRepo) + return "" + } + } + return baseDir +} + +func aahPath() string { + s := os.Getenv("AAHPATH") + if s == "" { + return filepath.Join(userHomeDir(), ".aah") + } + return s +} + +func userHomeDir() string { + if isWindowsOS() { + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if ess.IsStrEmpty(home) { + home = os.Getenv("USERPROFILE") + } + return filepath.Clean(home) + } + + env := "HOME" + if runtime.GOOS == "plan9" { + env = "home" + } + if home := os.Getenv(env); home != "" { + return filepath.Clean(home) + } + + return "" +} + +func goCmdName() string { + if name := os.Getenv("AAHVGO"); name != "" { + return "vgo" + } + return "go" +} + +func fetchFile(dst, src string) error { + resp, err := http.Get(src) + if err != nil { + return err + } + defer ess.CloseQuietly(resp.Body) + + _ = ess.MkDirAll(filepath.Dir(dst), permRWXRXRX) + f, err := os.Create(dst) + if err != nil { + return err + } + defer ess.CloseQuietly(f) + + _, err = io.Copy(f, resp.Body) + return err +} diff --git a/aah/version.go b/aah/version.go index 70409ee..585e5c8 100644 --- a/aah/version.go +++ b/aah/version.go @@ -1,8 +1,58 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools/aah source code and usage is governed by a MIT style +// aahframework.org/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main +import ( + "errors" + "fmt" + "path/filepath" + "regexp" + "strings" + + "gopkg.in/urfave/cli.v1" +) + // Version no. of aah framework CLI tool -const Version = "0.10.2" +const Version = "0.11.0" + +var ( + errVersionNotExists = errors.New("version not exists") + verRegex = regexp.MustCompile(`Version = "([\d.]+(\-edge)?)"`) +) + +// VersionPrinter method prints the versions info. +func VersionPrinter(c *cli.Context) { + cliLog = initCLILogger(nil) + fmt.Printf("%-3s v%s\n", "cli", Version) + fmt.Printf("%-3s v%s\n", "aah", aahVer) + if goVer := goVersion(); len(goVer) > 0 { + fmt.Printf("%-3s v%s\n", "go", goVer) + } + + if c.Bool("all") { // currently not-executed intentionally + fmt.Printf("\nLibraries:\n") + for _, bd := range aahLibraryDirs() { + bn := filepath.Base(bd) + if strings.HasPrefix(bn, "aah") || strings.HasPrefix(bn, "tools") { + continue + } + if verNo, err := readVersionNo(bd); err == nil { + fmt.Printf(" %s v%s\n", bn[:len(bn)-3], verNo) + } + } + } + fmt.Println() +} + +func aahVersion() (string, error) { + return readVersionNo(libDir("aah")) +} + +func goVersion() string { + if ver, err := execCmd(gocmd, []string{"version"}, false); err == nil { + return strings.TrimLeft(strings.Fields(ver)[2], "go") + } + return "" +}