fuzzyworld

template / files / internal / core / profile.go

profile.go

Blame: the commit that last changed each line · file · history

37160b89 2026-05-01 Pavel Safronov1package core
d026838a 2022-09-22 Pavel Safronov2
3import (
bbd011d0 2022-12-10 Pavel Safronov4 "bufio"
ae89a42a 2022-11-02 Pavel Safronov5 "bytes"
6543e947 2022-12-29 Pavel Safronov6 "errors"
d026838a 2022-09-22 Pavel Safronov7 "fmt"
8 "os"
05f9e8ef 2022-10-06 Pavel Safronov9 "os/user"
319b8b3f 2022-09-22 Pavel Safronov10 "path/filepath"
2bc4bd60 2022-09-23 Pavel Safronov11 "runtime"
230da3be 2022-09-22 Pavel Safronov12 "strings"
13 "time"
763c260d 2022-10-06 Pavel Safronov14
37160b89 2026-05-01 Pavel Safronov15 "slices"
763c260d 2022-10-06 Pavel Safronov16)
17
18type ProfileType string
19
20const (
21 FileType ProfileType = "file"
22 FolderType ProfileType = "folder"
23 ScriptType ProfileType = "script"
24)
25
873846b9 2022-12-09 Pavel Safronov26var allProfileTypes = []ProfileType{
27 FileType,
28 FolderType,
29 ScriptType,
30}
31
763c260d 2022-10-06 Pavel Safronov32type OSType string
33
34const (
35 Windows OSType = "Windows"
36 Mac OSType = "Mac"
37 All OSType = "All"
d026838a 2022-09-22 Pavel Safronov38)
39
1a02ef4e 2022-10-21 Pavel Safronov40type ParameterizedText string
41
5e7795dc 2022-10-21 Pavel Safronov42type FileInFolder struct {
873846b9 2022-12-09 Pavel Safronov43 Name ParameterizedText
44 FileTemplate ParameterizedText `yaml:"file,omitempty"`
5e7795dc 2022-10-21 Pavel Safronov45}
46
b0586433 2022-10-21 Pavel Safronov47type Profile struct {
687d5626 2022-10-28 Pavel Safronov48 Name string
e86c26ae 2022-11-02 Pavel Safronov49 Description string `yaml:"description,omitempty"`
51fece6b 2026-05-01 Pavel Safronov50 IsDefault bool `yaml:"isdefault,omitempty"`
51 IsDisabled bool `yaml:"isdisabled,omitempty"`
687d5626 2022-10-28 Pavel Safronov52 Type ProfileType
873846b9 2022-12-09 Pavel Safronov53 OS OSType `yaml:"os,omitempty"`
e86c26ae 2022-11-02 Pavel Safronov54 RunPostscriptOnFailure bool `yaml:"runpostscriptonfailure,omitempty"`
55 PostScripts []ParameterizedText `yaml:"postscripts,omitempty"`
873846b9 2022-12-09 Pavel Safronov56
57 NamePatterns []ParameterizedText
58
59 TargetDirectory string `yaml:"targetdirectory,omitempty"`
60 FileTemplate ParameterizedText `yaml:"filetemplate,omitempty"`
61 Files []FileInFolder `yaml:"files,omitempty"`
763c260d 2022-10-06 Pavel Safronov62}
63
64var forbiddenProfileNames = []string{
65 "list",
66 "get",
67}
68
bbd011d0 2022-12-10 Pavel Safronov69func processProfile(profile Profile, projectName string, extras map[string]string, usr user.User, pipeInputScanner *bufio.Scanner) []error {
10676da3 2022-11-06 Pavel Safronov70 if profile.IsDisabled {
9573ca02 2022-11-16 Pavel Safronov71 return []error{fmt.Errorf("Profile is disabled, doing nothing")}
10676da3 2022-11-06 Pavel Safronov72 }
73
da15cf49 2022-09-23 Pavel Safronov74 // decide if the profile should run on this OS
2bc4bd60 2022-09-23 Pavel Safronov75 profileOs := profile.OS
76 isWindows := runtime.GOOS == "windows"
b65fb5e9 2022-09-23 Pavel Safronov77 shouldRun := profileOs == "" ||
78 profileOs == "All" ||
2bc4bd60 2022-09-23 Pavel Safronov79 (profileOs == "Windows" && isWindows) ||
80 (profileOs == "Mac" && !isWindows)
81 if !shouldRun {
9573ca02 2022-11-16 Pavel Safronov82 return []error{
873846b9 2022-12-09 Pavel Safronov83 fmt.Errorf("this profile is not supported on this OS, doing nothing. Profile OS: %s, system OS: %s. Supported OS values: Windows, Mac, All", profileOs, runtime.GOOS),
9573ca02 2022-11-16 Pavel Safronov84 }
2bc4bd60 2022-09-23 Pavel Safronov85 }
86
873846b9 2022-12-09 Pavel Safronov87 // determine if we're dealing with files or directories
88 profileType := profile.Type
89
c3fc7a22 2022-10-30 Pavel Safronov90 cwd, err := monkey.OSGetwd()
bbd011d0 2022-12-10 Pavel Safronov91 if err != nil {
687d5626 2022-10-28 Pavel Safronov92 return []error{fmt.Errorf("unable to get current working directory: %v", err)}
da15cf49 2022-09-23 Pavel Safronov93 }
873846b9 2022-12-09 Pavel Safronov94 if profile.TargetDirectory != "" {
2ef800f4 2023-01-09 Pavel Safronov95 path := getFullPath(profile.TargetDirectory, usr)
96
97 if _, err := monkey.OSStat(path); errors.Is(err, os.ErrNotExist) {
98 err = monkey.OSMkdir(path, 0755)
6543e947 2022-12-29 Pavel Safronov99 if err != nil {
100 return []error{fmt.Errorf("unable to create target directory: %v", err)}
101 }
102 }
103
da15cf49 2022-09-23 Pavel Safronov104 // change cwd to the profile's path, if one is specified
c3fc7a22 2022-10-30 Pavel Safronov105 err = monkey.OSChdir(path)
da15cf49 2022-09-23 Pavel Safronov106 if err != nil {
687d5626 2022-10-28 Pavel Safronov107 return []error{fmt.Errorf("unable to change directory (1) to %v: %v", path, err)}
da15cf49 2022-09-23 Pavel Safronov108 }
109 cwd = path
319b8b3f 2022-09-22 Pavel Safronov110 }
111
687d5626 2022-10-28 Pavel Safronov112 errors := []error{}
8c15a714 2022-11-04 Pavel Safronov113 fullPath := ""
114 now := time.Now()
115 prePatternData := createPrePatternData(now, projectName, extras)
873846b9 2022-12-09 Pavel Safronov116
8c15a714 2022-11-04 Pavel Safronov117 if profileType == FileType || profileType == FolderType {
bbd011d0 2022-12-10 Pavel Safronov118 if pipeInputScanner != nil && profileType != FileType {
119 errors = append(errors, fmt.Errorf("cannot use pipe input with non-file type profile"))
120 return errors
121 }
122
8c15a714 2022-11-04 Pavel Safronov123 // get all entities in current directory
124 files, err := monkey.OSReadDir(cwd)
125 if err != nil {
126 return []error{err}
127 }
319b8b3f 2022-09-22 Pavel Safronov128
8c15a714 2022-11-04 Pavel Safronov129 // filter entities to just files or directories
130 var entities []string
131 for _, file := range files {
132 isDir := file.IsDir()
133 if isDir == (profileType == FolderType) {
134 entities = append(entities, file.Name())
135 }
687d5626 2022-10-28 Pavel Safronov136 }
2bc4bd60 2022-09-23 Pavel Safronov137
8c15a714 2022-11-04 Pavel Safronov138 // generate a new name based on time, profile, and existing entities
10676da3 2022-11-06 Pavel Safronov139 validator := func(newName string) bool { return !slices.Contains(entities, newName) }
140 newName, err := createNewNameForPatterns(now, prePatternData, projectName, profile.NamePatterns, extras, validator)
8c15a714 2022-11-04 Pavel Safronov141 swallowPrePostScriptErrors := profile.RunPostscriptOnFailure && profile.PostScripts != nil && len(profile.PostScripts) > 0
142
143 // check newName is created
144 if newName == "" {
e86c26ae 2022-11-02 Pavel Safronov145 if swallowPrePostScriptErrors {
8c15a714 2022-11-04 Pavel Safronov146 fmt.Println("No new name was created, but will attempt to run post scripts")
e86c26ae 2022-11-02 Pavel Safronov147 } else {
8c15a714 2022-11-04 Pavel Safronov148 errors = append(errors, fmt.Errorf("couldn't find a good new name"))
687d5626 2022-10-28 Pavel Safronov149 return errors
fe139e41 2022-09-25 Pavel Safronov150 }
5e7795dc 2022-10-21 Pavel Safronov151 }
8c15a714 2022-11-04 Pavel Safronov152
153 if err == nil && newName != "" {
bbd011d0 2022-12-10 Pavel Safronov154 fullPath, err = createEntity(now, profile, cwd, newName, projectName, extras, prePatternData, pipeInputScanner)
8c15a714 2022-11-04 Pavel Safronov155 if err != nil {
156 if swallowPrePostScriptErrors {
157 fmt.Printf("Error creating entity, but will attempt to run post scripts: %v", err)
158 } else {
159 errors = append(errors, fmt.Errorf("unable to create entity: %v", err))
160 return errors
161 }
162 }
163 }
319b8b3f 2022-09-22 Pavel Safronov164 }
165
166 // execute PostScript, if present
83f12389 2026-01-15 Pavel Safronov167 if len(profile.PostScripts) > 0 {
9573ca02 2022-11-16 Pavel Safronov168 postPatternData := createPostPatternData(fullPath, prePatternData)
169 postScripts, err := createPostScriptArrays(profile, postPatternData)
ae89a42a 2022-11-02 Pavel Safronov170 if err != nil {
9573ca02 2022-11-16 Pavel Safronov171 errors = append(errors, fmt.Errorf("unable to create post scripts: %v", err))
172 return errors
173 }
174 for _, postScript := range postScripts {
175
176 shell, args := shellCommandAndArgs(postScript)
177 var out bytes.Buffer
178 fmt.Printf("Executing post script: %v %v\n", shell, strings.Join(args, " "))
179 cmd := monkey.ExecCommand(shell, args...)
180 cmd.Stdout = &out
181 err := cmd.Run()
182 output := out.String()
183 if output != "" {
184 fmt.Printf("Post script output: %v\n", output)
185 } else {
186 fmt.Printf("Post script had no output\n")
187 }
188 if err != nil {
189 // post scripts can fail (TODO: investigate why), so for now we'll just log the error
2bc4bd60 2022-09-23 Pavel Safronov190
9573ca02 2022-11-16 Pavel Safronov191 fmt.Printf("Error executing post script '%v': %v\n",
192 fmt.Sprintf("%v %v", shell, strings.Trim(fmt.Sprint(args), "[]")), err)
193 }
319b8b3f 2022-09-22 Pavel Safronov194 }
195 }
196
197 return nil
198}
763c260d 2022-10-06 Pavel Safronov199
52e9ad7c 2022-11-08 Pavel Safronov200func shellCommandAndArgs(command string) (shell string, args []string) {
201 if runtime.GOOS == "windows" {
202 shell = "cmd"
203 args = []string{"/c", command}
204 return
205 } else {
206 shell = "sh"
207 args = []string{"-c", command}
208 return
209 }
210}
211
687d5626 2022-10-28 Pavel Safronov212func createEntity(
213 now time.Time,
214 profile Profile,
215 cwd string,
216 newName string,
217 projectName string,
218 extras map[string]string,
bbd011d0 2022-12-10 Pavel Safronov219 prePatternData PrePatternData,
220 pipeInputScanner *bufio.Scanner) (string, error) {
687d5626 2022-10-28 Pavel Safronov221
c3fc7a22 2022-10-30 Pavel Safronov222 profileType := profile.Type
687d5626 2022-10-28 Pavel Safronov223 fullPath := filepath.Join(cwd, newName)
83f12389 2026-01-15 Pavel Safronov224 switch profileType {
225 case FolderType:
e86c26ae 2022-11-02 Pavel Safronov226 fmt.Printf("Creating folder: %v\n", fullPath)
c3fc7a22 2022-10-30 Pavel Safronov227 err := monkey.OSMkdir(fullPath, os.ModePerm)
687d5626 2022-10-28 Pavel Safronov228 if err != nil {
229 return "", fmt.Errorf("error creating directory: %v", err)
230 }
c3fc7a22 2022-10-30 Pavel Safronov231 err = monkey.OSChdir(fullPath)
687d5626 2022-10-28 Pavel Safronov232 if err != nil {
233 return "", fmt.Errorf("unable to change directory (2) to %v: %v", fullPath, err)
234 }
235
236 // create files in the new folder
237 if len(profile.Files) > 0 {
e86c26ae 2022-11-02 Pavel Safronov238 fmt.Printf("Creating %v files in folder: %v\n", len(profile.Files), fullPath)
10676da3 2022-11-06 Pavel Safronov239 existingFiles := []string{}
687d5626 2022-10-28 Pavel Safronov240 for index, file := range profile.Files {
8c15a714 2022-11-04 Pavel Safronov241 filePrePatternData := createPrePatternData(now, projectName, extras)
10676da3 2022-11-06 Pavel Safronov242 validator := func(newName string) bool { return !slices.Contains(existingFiles, newName) }
243 fileName, err := createNewNameForPatterns(now, filePrePatternData, projectName, []ParameterizedText{file.Name}, extras, validator)
687d5626 2022-10-28 Pavel Safronov244 if err != nil {
245 return "", fmt.Errorf("error creating file name for file index %v : %v", index, err)
246 }
10676da3 2022-11-06 Pavel Safronov247 existingFiles = append(existingFiles, fileName)
687d5626 2022-10-28 Pavel Safronov248 subFilePath := filepath.Join(fullPath, fileName)
5e424f1b 2022-11-12 Pavel Safronov249 subDirPath := filepath.Dir(subFilePath)
250
251 if _, err := os.Stat(subDirPath); os.IsNotExist(err) {
252 fmt.Printf("Creating subdirectory: %v\n", subDirPath)
253 err = monkey.OSMkdir(subDirPath, os.ModePerm)
254 if err != nil {
255 return "", fmt.Errorf("error creating sub directory for file index %v : %v", index, err)
256 }
257 }
e86c26ae 2022-11-02 Pavel Safronov258 fmt.Printf("Creating sub file: %v\n", subFilePath)
bbd011d0 2022-12-10 Pavel Safronov259 err = createFile(filePrePatternData, file.FileTemplate, subFilePath, nil)
687d5626 2022-10-28 Pavel Safronov260 if err != nil {
886f7fa2 2022-11-02 Pavel Safronov261 return "", fmt.Errorf("error creating file %v: %v", subFilePath, err)
687d5626 2022-10-28 Pavel Safronov262 }
263 }
264 }
83f12389 2026-01-15 Pavel Safronov265 case FileType:
e86c26ae 2022-11-02 Pavel Safronov266 fmt.Printf("Creating file: %v\n", fullPath)
bbd011d0 2022-12-10 Pavel Safronov267 err := createFile(prePatternData, profile.FileTemplate, fullPath, pipeInputScanner)
5e7795dc 2022-10-21 Pavel Safronov268 if err != nil {
687d5626 2022-10-28 Pavel Safronov269 return "", err
5e7795dc 2022-10-21 Pavel Safronov270 }
83f12389 2026-01-15 Pavel Safronov271 default:
e86c26ae 2022-11-02 Pavel Safronov272 fmt.Printf("Profile type is script, doing nothing during main phase\n")
5e7795dc 2022-10-21 Pavel Safronov273 }
687d5626 2022-10-28 Pavel Safronov274 return fullPath, nil
5e7795dc 2022-10-21 Pavel Safronov275}
276
b0586433 2022-10-21 Pavel Safronov277func validateProfile(profile *Profile) error {
873846b9 2022-12-09 Pavel Safronov278 // check name
763c260d 2022-10-06 Pavel Safronov279 if profile.Name == "" {
873846b9 2022-12-09 Pavel Safronov280 return fmt.Errorf("profile name is required")
763c260d 2022-10-06 Pavel Safronov281 }
282 if slices.Contains(forbiddenProfileNames, profile.Name) {
283 return fmt.Errorf("forbidden profile name '%s'", profile.Name)
284 }
873846b9 2022-12-09 Pavel Safronov285
286 // check name patterns
83f12389 2026-01-15 Pavel Safronov287 if len(profile.NamePatterns) == 0 {
10676da3 2022-11-06 Pavel Safronov288 return fmt.Errorf("namePatterns is required")
289 }
290 for index, namePattern := range profile.NamePatterns {
291 if namePattern == "" {
292 return fmt.Errorf("name pattern is required for pattern index %v of profile %v", index, profile.Name)
293 }
294 }
873846b9 2022-12-09 Pavel Safronov295
296 // check type
297 if profile.Type == "" || !slices.Contains(allProfileTypes, profile.Type) {
763c260d 2022-10-06 Pavel Safronov298 return fmt.Errorf("invalid profile type %s for profile %s", profile.Type, profile.Name)
299 }
873846b9 2022-12-09 Pavel Safronov300
301 // check FileType profile
302 if profile.Type == FileType {
303 if profile.TargetDirectory == "" {
304 return fmt.Errorf("targetDirectory is not specified for profile %s, which has Type=%s", profile.Name, profile.Type)
305 }
05f9e8ef 2022-10-06 Pavel Safronov306 }
873846b9 2022-12-09 Pavel Safronov307
308 // check FolderType profile
309 if profile.Type == FolderType {
310 if profile.TargetDirectory == "" {
311 return fmt.Errorf("targetDirectory is not specified for profile %s, which has Type=%s", profile.Name, profile.Type)
312 }
8c15a714 2022-11-04 Pavel Safronov313 }
873846b9 2022-12-09 Pavel Safronov314
315 // check ScriptType profile
316 if profile.Type == ScriptType {
83f12389 2026-01-15 Pavel Safronov317 if len(profile.NamePatterns) > 1 {
873846b9 2022-12-09 Pavel Safronov318 return fmt.Errorf("multiple name patterns are not supported for profile %s, which has Type=%s", profile.Name, profile.Type)
319 }
8c15a714 2022-11-04 Pavel Safronov320 }
873846b9 2022-12-09 Pavel Safronov321
322 // set files to empty array if nil
b0586433 2022-10-21 Pavel Safronov323 if profile.Files == nil {
5e7795dc 2022-10-21 Pavel Safronov324 profile.Files = []FileInFolder{}
b0586433 2022-10-21 Pavel Safronov325 }
873846b9 2022-12-09 Pavel Safronov326
763c260d 2022-10-06 Pavel Safronov327 return nil
328}