github.com/wiselike/revel-cmd@v1.2.1/model/revel_container.go (about)

     1  // This package will be shared between Revel and Revel CLI eventually
     2  package model
     3  
     4  import (
     5  	"bufio"
     6  	"fmt"
     7  	"go/build"
     8  	"io"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  	"unicode"
    15  
    16  	"github.com/wiselike/revel-cmd/utils"
    17  	"github.com/revel/config"
    18  	"golang.org/x/tools/go/packages"
    19  )
    20  
    21  // Error is used for constant errors.
    22  type Error string
    23  
    24  // Error implements the error interface.
    25  func (e Error) Error() string {
    26  	return string(e)
    27  }
    28  
    29  const (
    30  	ErrNoApp       Error = "no app found at path"
    31  	ErrNoConfig    Error = "no config found at path"
    32  	ErrNotFound    Error = "not found"
    33  	ErrMissingCert Error = "no http.sslcert provided"
    34  	ErrMissingKey  Error = "no http.sslkey provided"
    35  	ErrNoFiles     Error = "no files found in import path"
    36  	ErrNoPackages  Error = "no packages found for import"
    37  )
    38  
    39  type (
    40  	// The container object for describing all Revels variables.
    41  	RevelContainer struct {
    42  		BuildPaths struct {
    43  			Revel string
    44  		}
    45  		Paths struct {
    46  			Import   string
    47  			Source   string
    48  			Base     string
    49  			App      string
    50  			Views    string
    51  			Code     []string
    52  			Template []string
    53  			Config   []string
    54  		}
    55  		PackageInfo struct {
    56  			Config   config.Context
    57  			Packaged bool
    58  			DevMode  bool
    59  			Vendor   bool
    60  		}
    61  		Application struct {
    62  			Name string
    63  			Root string
    64  		}
    65  
    66  		ImportPath    string                 // The import path
    67  		SourcePath    string                 // The full source path
    68  		RunMode       string                 // The current run mode
    69  		RevelPath     string                 // The path to the Revel source code
    70  		BasePath      string                 // The base path to the application
    71  		AppPath       string                 // The application path (BasePath + "/app")
    72  		ViewsPath     string                 // The application views path
    73  		CodePaths     []string               // All the code paths
    74  		TemplatePaths []string               // All the template paths
    75  		ConfPaths     []string               // All the configuration paths
    76  		Config        *config.Context        // The global config object
    77  		Packaged      bool                   // True if packaged
    78  		DevMode       bool                   // True if running in dev mode
    79  		HTTPPort      int                    // The http port
    80  		HTTPAddr      string                 // The http address
    81  		HTTPSsl       bool                   // True if running https
    82  		HTTPSslCert   string                 // The SSL certificate
    83  		HTTPSslKey    string                 // The SSL key
    84  		AppName       string                 // The application name
    85  		AppRoot       string                 // The application root from the config `app.root`
    86  		CookiePrefix  string                 // The cookie prefix
    87  		CookieDomain  string                 // The cookie domain
    88  		CookieSecure  bool                   // True if cookie is secure
    89  		SecretStr     string                 // The secret string
    90  		MimeConfig    *config.Context        // The mime configuration
    91  		ModulePathMap map[string]*ModuleInfo // The module path map
    92  	}
    93  	ModuleInfo struct {
    94  		ImportPath string
    95  		Path       string
    96  	}
    97  
    98  	WrappedRevelCallback struct {
    99  		FireEventFunction func(key Event, value interface{}) (response EventResponse)
   100  		ImportFunction    func(pkgName string) error
   101  	}
   102  	Mod struct {
   103  		ImportPath    string
   104  		SourcePath    string
   105  		Version       string
   106  		SourceVersion string
   107  		Dir           string   // full path, $GOPATH/pkg/mod/
   108  		Pkgs          []string // sub-pkg import paths
   109  		VendorList    []string // files to vendor
   110  	}
   111  )
   112  
   113  // Simple Wrapped RevelCallback.
   114  func NewWrappedRevelCallback(fe func(key Event, value interface{}) (response EventResponse), ie func(pkgName string) error) RevelCallback {
   115  	return &WrappedRevelCallback{fe, ie}
   116  }
   117  
   118  // Function to implement the FireEvent.
   119  func (w *WrappedRevelCallback) FireEvent(key Event, value interface{}) (response EventResponse) {
   120  	if w.FireEventFunction != nil {
   121  		response = w.FireEventFunction(key, value)
   122  	}
   123  	return
   124  }
   125  
   126  func (w *WrappedRevelCallback) PackageResolver(pkgName string) error {
   127  	return w.ImportFunction(pkgName)
   128  }
   129  
   130  // RevelImportPath Revel framework import path.
   131  var (
   132  	RevelImportPath        = "github.com/wiselike/revel"
   133  	RevelModulesImportPath = "github.com/wiselike/revel-modules"
   134  )
   135  
   136  // This function returns a container object describing the revel application
   137  // eventually this type of function will replace the global variables.
   138  func NewRevelPaths(mode, importPath, appSrcPath string, callback RevelCallback) (rp *RevelContainer, err error) {
   139  	rp = &RevelContainer{ModulePathMap: map[string]*ModuleInfo{}}
   140  	// Ignore trailing slashes.
   141  	rp.ImportPath = strings.TrimRight(importPath, "/")
   142  	rp.SourcePath = appSrcPath
   143  	rp.RunMode = mode
   144  
   145  	// We always need to determine the paths for files
   146  	pathMap, err := utils.FindSrcPaths(appSrcPath, []string{importPath + "/app", RevelImportPath}, callback.PackageResolver)
   147  	if err != nil {
   148  		return
   149  	}
   150  	rp.AppPath, rp.RevelPath = pathMap[importPath], pathMap[RevelImportPath]
   151  	// Setup paths for application
   152  	rp.BasePath = rp.SourcePath
   153  	rp.PackageInfo.Vendor = utils.Exists(filepath.Join(rp.BasePath, "go.mod"))
   154  	rp.AppPath = filepath.Join(rp.BasePath, "app")
   155  
   156  	// Sanity check , ensure app and conf paths exist
   157  	if !utils.DirExists(rp.AppPath) {
   158  		return rp, fmt.Errorf("%w: %s", ErrNoApp, rp.AppPath)
   159  	}
   160  	if !utils.DirExists(filepath.Join(rp.BasePath, "conf")) {
   161  		return rp, fmt.Errorf("%w: %s", ErrNoConfig, filepath.Join(rp.BasePath, "conf"))
   162  	}
   163  
   164  	rp.ViewsPath = filepath.Join(rp.AppPath, "views")
   165  	rp.CodePaths = []string{rp.AppPath}
   166  	rp.TemplatePaths = []string{}
   167  
   168  	if rp.ConfPaths == nil {
   169  		rp.ConfPaths = []string{}
   170  	}
   171  
   172  	// Config load order
   173  	// 1. framework (revel/conf/*)
   174  	// 2. application (conf/*)
   175  	// 3. user supplied configs (...) - User configs can override/add any from above
   176  	rp.ConfPaths = append(
   177  		[]string{
   178  			filepath.Join(rp.RevelPath, "conf"),
   179  			filepath.Join(rp.BasePath, "conf"),
   180  		},
   181  		rp.ConfPaths...)
   182  
   183  	rp.Config, err = config.LoadContext("app.conf", rp.ConfPaths)
   184  	if err != nil {
   185  		return rp, fmt.Errorf("unable to load configuration file %w", err)
   186  	}
   187  
   188  	// Ensure that the selected runmode appears in app.conf.
   189  	// If empty string is passed as the mode, treat it as "DEFAULT"
   190  	if mode == "" {
   191  		mode = config.DefaultSection
   192  	}
   193  	if !rp.Config.HasSection(mode) {
   194  		return rp, fmt.Errorf("app.conf: %w %s %s", ErrNotFound, "run-mode", mode)
   195  	}
   196  	rp.Config.SetSection(mode)
   197  
   198  	// Configure properties from app.conf
   199  	rp.DevMode = rp.Config.BoolDefault("mode.dev", false)
   200  	rp.HTTPPort = rp.Config.IntDefault("http.port", 9000)
   201  	rp.HTTPAddr = rp.Config.StringDefault("http.addr", "")
   202  	rp.HTTPSsl = rp.Config.BoolDefault("http.ssl", false)
   203  	rp.HTTPSslCert = rp.Config.StringDefault("http.sslcert", "")
   204  	rp.HTTPSslKey = rp.Config.StringDefault("http.sslkey", "")
   205  	if rp.HTTPSsl {
   206  		if rp.HTTPSslCert == "" {
   207  			return rp, ErrMissingCert
   208  		}
   209  
   210  		if rp.HTTPSslKey == "" {
   211  			return rp, ErrMissingKey
   212  		}
   213  	}
   214  
   215  	rp.AppName = rp.Config.StringDefault("app.name", "(not set)")
   216  	rp.AppRoot = rp.Config.StringDefault("app.root", "")
   217  	rp.CookiePrefix = rp.Config.StringDefault("cookie.prefix", "REVEL")
   218  	rp.CookieDomain = rp.Config.StringDefault("cookie.domain", "")
   219  	rp.CookieSecure = rp.Config.BoolDefault("cookie.secure", rp.HTTPSsl)
   220  	rp.SecretStr = rp.Config.StringDefault("app.secret", "")
   221  
   222  	callback.FireEvent(REVEL_BEFORE_MODULES_LOADED, nil)
   223  	utils.Logger.Info("Loading modules")
   224  	if err := rp.loadModules(callback); err != nil {
   225  		return rp, err
   226  	}
   227  
   228  	callback.FireEvent(REVEL_AFTER_MODULES_LOADED, nil)
   229  
   230  	return
   231  }
   232  
   233  // LoadMimeConfig load mime-types.conf on init.
   234  func (rp *RevelContainer) LoadMimeConfig() (err error) {
   235  	rp.MimeConfig, err = config.LoadContext("mime-types.conf", rp.ConfPaths)
   236  	if err != nil {
   237  		return fmt.Errorf("failed to load mime type config: %s %w", "error", err)
   238  	}
   239  	return
   240  }
   241  
   242  // Loads modules based on the configuration setup.
   243  // This will fire the REVEL_BEFORE_MODULE_LOADED, REVEL_AFTER_MODULE_LOADED
   244  // for each module loaded. The callback will receive the RevelContainer, name, moduleImportPath and modulePath
   245  // It will automatically add in the code paths for the module to the
   246  // container object.
   247  func (rp *RevelContainer) loadModules(callback RevelCallback) (err error) {
   248  	keys := []string{}
   249  	keys = append(keys, rp.Config.Options("module.")...)
   250  
   251  	// Reorder module order by key name, a poor mans sort but at least it is consistent
   252  	sort.Strings(keys)
   253  	modtxtPath := filepath.Join(rp.SourcePath, "vendor", "modules.txt")
   254  	if utils.Exists(modtxtPath) {
   255  		// Parse out require sections of module.txt
   256  		modules := rp.vendorInitilizeLocal(modtxtPath, keys)
   257  		for _, mod := range modules {
   258  			for _, vendorFile := range mod.VendorList {
   259  				x := strings.Index(vendorFile, mod.Dir)
   260  				if x < 0 {
   261  					utils.Logger.Crit("Error! vendor file doesn't belong to mod, strange.", "vendorFile", "mod.Dir", mod.Dir)
   262  				}
   263  
   264  				localPath := fmt.Sprintf("%s%s", mod.ImportPath, vendorFile[len(mod.Dir):])
   265  				localFile := filepath.Join(rp.SourcePath, "vendor", localPath)
   266  
   267  				utils.Logger.Infof("vendoring %s\n", localPath)
   268  
   269  				os.MkdirAll(filepath.Dir(localFile), os.ModePerm)
   270  				if _, err := copyFile(vendorFile, localFile); err != nil {
   271  					fmt.Printf("Error! %s - unable to copy file %s\n", err.Error(), vendorFile)
   272  					os.Exit(1)
   273  				}
   274  			}
   275  		}
   276  	}
   277  
   278  	for _, key := range keys {
   279  		moduleImportPath := rp.Config.StringDefault(key, "")
   280  		if moduleImportPath == "" {
   281  			continue
   282  		}
   283  
   284  		modulePath, err := rp.ResolveImportPath(moduleImportPath)
   285  		utils.Logger.Info("Resolving import path ", "modulePath", modulePath, "module_import_path", moduleImportPath, "error", err)
   286  		if err != nil {
   287  
   288  			if err := callback.PackageResolver(moduleImportPath); err != nil {
   289  				return fmt.Errorf("failed to resolve package %w", err)
   290  			}
   291  
   292  			modulePath, err = rp.ResolveImportPath(moduleImportPath)
   293  			if err != nil {
   294  				return fmt.Errorf("failed to load module.  Import of path failed %s:%s %s:%w ", "modulePath", moduleImportPath, "error", err)
   295  			}
   296  		}
   297  		// Drop anything between module.???.<name of module>
   298  		name := key[len("module."):]
   299  		if index := strings.Index(name, "."); index > -1 {
   300  			name = name[index+1:]
   301  		}
   302  		callback.FireEvent(REVEL_BEFORE_MODULE_LOADED, []interface{}{rp, name, moduleImportPath, modulePath})
   303  		rp.addModulePaths(name, moduleImportPath, modulePath)
   304  		callback.FireEvent(REVEL_AFTER_MODULE_LOADED, []interface{}{rp, name, moduleImportPath, modulePath})
   305  	}
   306  	return
   307  }
   308  
   309  // Adds a module paths to the container object.
   310  func (rp *RevelContainer) vendorInitilizeLocal(modtxtPath string, revel_modules_keys []string) []*Mod {
   311  	revel_modules := []string{"github.com/wiselike/revel"}
   312  	for _, key := range revel_modules_keys {
   313  		moduleImportPath := rp.Config.StringDefault(key, "")
   314  		if moduleImportPath == "" {
   315  			continue
   316  		}
   317  		revel_modules = append(revel_modules, moduleImportPath)
   318  	}
   319  	f, _ := os.Open(modtxtPath)
   320  	defer f.Close()
   321  	scanner := bufio.NewScanner(f)
   322  	scanner.Split(bufio.ScanLines)
   323  
   324  	var (
   325  		mod *Mod
   326  		err error
   327  	)
   328  	modules := []*Mod{}
   329  
   330  	for scanner.Scan() {
   331  		line := scanner.Text()
   332  
   333  		// Look for # character
   334  		if line[0] == 35 {
   335  			s := strings.Split(line, " ")
   336  			if (len(s) != 6 && len(s) != 3) || s[1] == "explicit" {
   337  				continue
   338  			}
   339  
   340  			mod = &Mod{
   341  				ImportPath: s[1],
   342  				Version:    s[2],
   343  			}
   344  			if s[2] == "=>" {
   345  				// issue https://github.com/golang/go/issues/33848 added these,
   346  				// see comments. I think we can get away with ignoring them.
   347  				continue
   348  			}
   349  			// Handle "replace" in module file if any
   350  			if len(s) > 3 && s[3] == "=>" {
   351  				mod.SourcePath = s[4]
   352  
   353  				// Handle replaces with a relative target. For example:
   354  				// "replace github.com/status-im/status-go/protocol => ./protocol"
   355  				if strings.HasPrefix(s[4], ".") || strings.HasPrefix(s[4], "/") {
   356  					mod.Dir, err = filepath.Abs(s[4])
   357  					if err != nil {
   358  						fmt.Printf("invalid relative path: %v", err)
   359  						os.Exit(1)
   360  					}
   361  				} else {
   362  					mod.SourceVersion = s[5]
   363  					mod.Dir = pkgModPath(mod.SourcePath, mod.SourceVersion)
   364  				}
   365  			} else {
   366  				mod.Dir = pkgModPath(mod.ImportPath, mod.Version)
   367  			}
   368  
   369  			if _, err := os.Stat(mod.Dir); os.IsNotExist(err) {
   370  				utils.Logger.Critf("Error! %q module path does not exist, check $GOPATH/pkg/mod\n", mod.Dir)
   371  			}
   372  
   373  			// Determine if we need to examine this mod, based on the list of modules being imported
   374  			for _, importPath := range revel_modules {
   375  				if strings.HasPrefix(importPath, mod.ImportPath) {
   376  					updateModVendorList(mod, importPath)
   377  				}
   378  			}
   379  
   380  			// Build list of files to module path source to project vendor folder
   381  			modules = append(modules, mod)
   382  
   383  			continue
   384  		}
   385  
   386  		mod.Pkgs = append(mod.Pkgs, line)
   387  	}
   388  	return modules
   389  }
   390  
   391  // Adds a module paths to the container object.
   392  func (rp *RevelContainer) addModulePaths(name, importPath, modulePath string) {
   393  	utils.Logger.Info("Adding module path", "name", name, "import path", importPath, "system path", modulePath)
   394  	if codePath := filepath.Join(modulePath, "app"); utils.DirExists(codePath) {
   395  		rp.CodePaths = append(rp.CodePaths, codePath)
   396  		rp.ModulePathMap[name] = &ModuleInfo{importPath, modulePath}
   397  		if viewsPath := filepath.Join(modulePath, "app", "views"); utils.DirExists(viewsPath) {
   398  			rp.TemplatePaths = append(rp.TemplatePaths, viewsPath)
   399  		}
   400  	}
   401  
   402  	// Hack: There is presently no way for the testrunner module to add the
   403  	// "test" subdirectory to the CodePaths.  So this does it instead.
   404  	if importPath == rp.Config.StringDefault("module.testrunner", "github.com/wiselike/revel-modules/testrunner") {
   405  		joinedPath := filepath.Join(rp.BasePath, "tests")
   406  		rp.CodePaths = append(rp.CodePaths, joinedPath)
   407  	}
   408  	if testsPath := filepath.Join(modulePath, "tests"); utils.DirExists(testsPath) {
   409  		rp.CodePaths = append(rp.CodePaths, testsPath)
   410  	}
   411  }
   412  
   413  // ResolveImportPath returns the filesystem path for the given import path.
   414  // Returns an error if the import path could not be found.
   415  func (rp *RevelContainer) ResolveImportPath(importPath string) (string, error) {
   416  	if rp.Packaged {
   417  		return filepath.Join(rp.SourcePath, importPath), nil
   418  	}
   419  	config := &packages.Config{
   420  		Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports |
   421  			packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo,
   422  		Dir: rp.AppPath,
   423  	}
   424  	config.Env = utils.ReducedEnv(false)
   425  	pkgs, err := packages.Load(config, importPath)
   426  	if len(pkgs) == 0 {
   427  		return "", fmt.Errorf("%w %s using app path %s", ErrNoPackages, importPath, rp.AppPath)
   428  	}
   429  	//	modPkg, err := build.Import(importPath, rp.AppPath, build.FindOnly)
   430  	if err != nil {
   431  		return "", err
   432  	}
   433  	if len(pkgs[0].GoFiles) > 0 {
   434  		return filepath.Dir(pkgs[0].GoFiles[0]), nil
   435  	}
   436  	return pkgs[0].PkgPath, fmt.Errorf("%w: %s", ErrNoFiles, importPath)
   437  }
   438  
   439  func normString(str string) (normStr string) {
   440  	for _, char := range str {
   441  		if unicode.IsUpper(char) {
   442  			normStr += "!" + string(unicode.ToLower(char))
   443  		} else {
   444  			normStr += string(char)
   445  		}
   446  	}
   447  	return
   448  }
   449  
   450  func pkgModPath(importPath, version string) string {
   451  	goPath := build.Default.GOPATH
   452  	if goPath == "" {
   453  		if goPath = os.Getenv("GOPATH"); goPath == "" {
   454  			// the default GOPATH for go v1.11
   455  			goPath = filepath.Join(os.Getenv("HOME"), "go")
   456  		}
   457  	}
   458  
   459  	normPath := normString(importPath)
   460  	normVersion := normString(version)
   461  
   462  	return filepath.Join(goPath, "pkg", "mod", fmt.Sprintf("%s@%s", normPath, normVersion))
   463  }
   464  
   465  func copyFile(src, dst string) (int64, error) {
   466  	srcStat, err := os.Stat(src)
   467  	if err != nil {
   468  		return 0, err
   469  	}
   470  
   471  	if !srcStat.Mode().IsRegular() {
   472  		return 0, fmt.Errorf("%s is not a regular file", src)
   473  	}
   474  
   475  	srcFile, err := os.Open(src)
   476  	if err != nil {
   477  		return 0, err
   478  	}
   479  	defer srcFile.Close()
   480  
   481  	dstFile, err := os.Create(dst)
   482  	if err != nil {
   483  		return 0, err
   484  	}
   485  	defer dstFile.Close()
   486  
   487  	return io.Copy(dstFile, srcFile)
   488  }
   489  
   490  func updateModVendorList(mod *Mod, importPath string) {
   491  	vendorList := []string{}
   492  	pathPrefix := filepath.Join(mod.Dir, importPath[len(mod.ImportPath):])
   493  
   494  	filepath.WalkDir(pathPrefix, func(path string, d fs.DirEntry, err error) (e error) {
   495  		if d.IsDir() {
   496  			return
   497  		}
   498  
   499  		if err != nil {
   500  			utils.Logger.Crit("Failed to walk vendor dir")
   501  		}
   502  		utils.Logger.Info("Adding to file in vendor list", "path", path)
   503  		vendorList = append(vendorList, path)
   504  		return
   505  	})
   506  
   507  	utils.Logger.Info("For module", "module", mod.ImportPath, "files", len(vendorList))
   508  
   509  	mod.VendorList = append(mod.VendorList, vendorList...)
   510  }