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 }