package models import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "github.com/rs/zerolog/log" ) const ( MainDir string = ".homeserver" ComposeFile string = "docker-compose.deploy.yml" EnvFile string = ".env" NginxFile = "nginx.conf" ConfFile = "hmdeploy.json" ) var ErrProjectConfFile = errors.New("project error") type filepathOption struct { isDir bool } type fnFilepathOption func(*filepathOption) func IsDir() fnFilepathOption { return func(fo *filepathOption) { fo.isDir = true } } func getFilepath(baseDir, filePath string, options ...fnFilepathOption) (string, error) { var opts filepathOption for _, opt := range options { opt(&opts) } if !filepath.IsAbs(filePath) { filePath = filepath.Join(baseDir, filePath) filePath, err := filepath.Abs(filePath) //nolint: govet if err != nil { return filePath, fmt.Errorf( "%w, file=%s, err=%v", ErrProjectConfFile, filePath, err, ) } } fileInfo, err := os.Stat(filePath) if err != nil { return filePath, fmt.Errorf( "%w, file=%s, err=%v", ErrProjectConfFile, filePath, err, ) } if fileInfo.IsDir() && !opts.isDir { return filePath, fmt.Errorf( "%w, file=%s, err=%s", ErrProjectConfFile, filePath, "must be a file", ) } if !fileInfo.IsDir() && opts.isDir { return filePath, fmt.Errorf( "%w, file=%s, err=%s", ErrProjectConfFile, filePath, "must be a dir", ) } return filePath, nil } // Project handles the details and file informations of your project. type Project struct { Name string `json:"name"` Dir string Deps struct { Nginx struct { Conf string `json:"conf"` Assets string `json:"assets"` } Swarm struct { EnvFile string `json:"env"` ComposeFile string `json:"compose"` ImageNames []string `json:"images"` } `json:"swarm"` } `json:"dependencies"` } func (p *Project) validate() error { if compf := p.Deps.Swarm.ComposeFile; compf != "" { cpath, err := getFilepath(p.Dir, compf) if err != nil { return err } p.Deps.Swarm.ComposeFile = cpath } else { log.Warn().Msg("no docker-compose file provided, Swarm deployment discards") } if env := p.Deps.Swarm.EnvFile; env != "" { epath, err := getFilepath(p.Dir, env) if err != nil { return err } p.Deps.Swarm.EnvFile = epath } if conf := p.Deps.Nginx.Conf; conf != "" { npath, err := getFilepath(p.Dir, conf) if err != nil { return err } p.Deps.Nginx.Conf = npath } if assets := p.Deps.Nginx.Assets; assets != "" { apath, err := getFilepath(p.Dir, assets, IsDir()) if err != nil { return err } p.Deps.Nginx.Assets = apath } return nil } func (p Project) GetComposePath() string { return p.Deps.Swarm.ComposeFile } func (p Project) GetEnvPath() string { return p.Deps.Swarm.EnvFile } func (p Project) GetNginxConfPath() string { return p.Deps.Nginx.Conf } func (p Project) GetNginxAssetsPath() string { return p.Deps.Nginx.Assets } // ProjectFromDir instantiates a new project from a directory path. // // The directory path must refers to the path including the `.homeserver` dir not // the `.homeserver` path itself. func ProjectFromDir(dir string) (Project, error) { var p Project dir = filepath.Join(dir, MainDir) p.Dir = dir content, err := os.ReadFile(filepath.Join(dir, ConfFile)) if err != nil { return p, fmt.Errorf( "%w, unable to read conf file=%s, err=%v", ErrProjectConfFile, ConfFile, err, ) } if err := json.Unmarshal(content, &p); err != nil { return p, fmt.Errorf( "%w, unable to parse conf file=%s, err=%v", ErrProjectConfFile, ConfFile, err, ) } if err := p.validate(); err != nil { return p, fmt.Errorf( "%w, unable to validate project, name=%s, dir=%s, err=%v", ErrProjectConfFile, p.Name, p.Dir, err, ) } return p, nil }