fuzzyworld

template / files / internal / core / profile.go

profile.go

9.5 KB · history · blame

1package core
2
3import (
4 "bufio"
5 "bytes"
6 "errors"
7 "fmt"
8 "os"
9 "os/user"
10 "path/filepath"
11 "runtime"
12 "strings"
13 "time"
14
15 "slices"
16)
17
18type ProfileType string
19
20const (
21 FileType ProfileType = "file"
22 FolderType ProfileType = "folder"
23 ScriptType ProfileType = "script"
24)
25
26var allProfileTypes = []ProfileType{
27 FileType,
28 FolderType,
29 ScriptType,
30}
31
32type OSType string
33
34const (
35 Windows OSType = "Windows"
36 Mac OSType = "Mac"
37 All OSType = "All"
38)
39
40type ParameterizedText string
41
42type FileInFolder struct {
43 Name ParameterizedText
44 FileTemplate ParameterizedText `yaml:"file,omitempty"`
45}
46
47type Profile struct {
48 Name string
49 Description string `yaml:"description,omitempty"`
50 IsDefault bool `yaml:"isdefault,omitempty"`
51 IsDisabled bool `yaml:"isdisabled,omitempty"`
52 Type ProfileType
53 OS OSType `yaml:"os,omitempty"`
54 RunPostscriptOnFailure bool `yaml:"runpostscriptonfailure,omitempty"`
55 PostScripts []ParameterizedText `yaml:"postscripts,omitempty"`
56
57 NamePatterns []ParameterizedText
58
59 TargetDirectory string `yaml:"targetdirectory,omitempty"`
60 FileTemplate ParameterizedText `yaml:"filetemplate,omitempty"`
61 Files []FileInFolder `yaml:"files,omitempty"`
62}
63
64var forbiddenProfileNames = []string{
65 "list",
66 "get",
67}
68
69func processProfile(profile Profile, projectName string, extras map[string]string, usr user.User, pipeInputScanner *bufio.Scanner) []error {
70 if profile.IsDisabled {
71 return []error{fmt.Errorf("Profile is disabled, doing nothing")}
72 }
73
74 // decide if the profile should run on this OS
75 profileOs := profile.OS
76 isWindows := runtime.GOOS == "windows"
77 shouldRun := profileOs == "" ||
78 profileOs == "All" ||
79 (profileOs == "Windows" && isWindows) ||
80 (profileOs == "Mac" && !isWindows)
81 if !shouldRun {
82 return []error{
83 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),
84 }
85 }
86
87 // determine if we're dealing with files or directories
88 profileType := profile.Type
89
90 cwd, err := monkey.OSGetwd()
91 if err != nil {
92 return []error{fmt.Errorf("unable to get current working directory: %v", err)}
93 }
94 if profile.TargetDirectory != "" {
95 path := getFullPath(profile.TargetDirectory, usr)
96
97 if _, err := monkey.OSStat(path); errors.Is(err, os.ErrNotExist) {
98 err = monkey.OSMkdir(path, 0755)
99 if err != nil {
100 return []error{fmt.Errorf("unable to create target directory: %v", err)}
101 }
102 }
103
104 // change cwd to the profile's path, if one is specified
105 err = monkey.OSChdir(path)
106 if err != nil {
107 return []error{fmt.Errorf("unable to change directory (1) to %v: %v", path, err)}
108 }
109 cwd = path
110 }
111
112 errors := []error{}
113 fullPath := ""
114 now := time.Now()
115 prePatternData := createPrePatternData(now, projectName, extras)
116
117 if profileType == FileType || profileType == FolderType {
118 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
123 // get all entities in current directory
124 files, err := monkey.OSReadDir(cwd)
125 if err != nil {
126 return []error{err}
127 }
128
129 // 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 }
136 }
137
138 // generate a new name based on time, profile, and existing entities
139 validator := func(newName string) bool { return !slices.Contains(entities, newName) }
140 newName, err := createNewNameForPatterns(now, prePatternData, projectName, profile.NamePatterns, extras, validator)
141 swallowPrePostScriptErrors := profile.RunPostscriptOnFailure && profile.PostScripts != nil && len(profile.PostScripts) > 0
142
143 // check newName is created
144 if newName == "" {
145 if swallowPrePostScriptErrors {
146 fmt.Println("No new name was created, but will attempt to run post scripts")
147 } else {
148 errors = append(errors, fmt.Errorf("couldn't find a good new name"))
149 return errors
150 }
151 }
152
153 if err == nil && newName != "" {
154 fullPath, err = createEntity(now, profile, cwd, newName, projectName, extras, prePatternData, pipeInputScanner)
155 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 }
164 }
165
166 // execute PostScript, if present
167 if len(profile.PostScripts) > 0 {
168 postPatternData := createPostPatternData(fullPath, prePatternData)
169 postScripts, err := createPostScriptArrays(profile, postPatternData)
170 if err != nil {
171 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
190
191 fmt.Printf("Error executing post script '%v': %v\n",
192 fmt.Sprintf("%v %v", shell, strings.Trim(fmt.Sprint(args), "[]")), err)
193 }
194 }
195 }
196
197 return nil
198}
199
200func 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
212func createEntity(
213 now time.Time,
214 profile Profile,
215 cwd string,
216 newName string,
217 projectName string,
218 extras map[string]string,
219 prePatternData PrePatternData,
220 pipeInputScanner *bufio.Scanner) (string, error) {
221
222 profileType := profile.Type
223 fullPath := filepath.Join(cwd, newName)
224 switch profileType {
225 case FolderType:
226 fmt.Printf("Creating folder: %v\n", fullPath)
227 err := monkey.OSMkdir(fullPath, os.ModePerm)
228 if err != nil {
229 return "", fmt.Errorf("error creating directory: %v", err)
230 }
231 err = monkey.OSChdir(fullPath)
232 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 {
238 fmt.Printf("Creating %v files in folder: %v\n", len(profile.Files), fullPath)
239 existingFiles := []string{}
240 for index, file := range profile.Files {
241 filePrePatternData := createPrePatternData(now, projectName, extras)
242 validator := func(newName string) bool { return !slices.Contains(existingFiles, newName) }
243 fileName, err := createNewNameForPatterns(now, filePrePatternData, projectName, []ParameterizedText{file.Name}, extras, validator)
244 if err != nil {
245 return "", fmt.Errorf("error creating file name for file index %v : %v", index, err)
246 }
247 existingFiles = append(existingFiles, fileName)
248 subFilePath := filepath.Join(fullPath, fileName)
249 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 }
258 fmt.Printf("Creating sub file: %v\n", subFilePath)
259 err = createFile(filePrePatternData, file.FileTemplate, subFilePath, nil)
260 if err != nil {
261 return "", fmt.Errorf("error creating file %v: %v", subFilePath, err)
262 }
263 }
264 }
265 case FileType:
266 fmt.Printf("Creating file: %v\n", fullPath)
267 err := createFile(prePatternData, profile.FileTemplate, fullPath, pipeInputScanner)
268 if err != nil {
269 return "", err
270 }
271 default:
272 fmt.Printf("Profile type is script, doing nothing during main phase\n")
273 }
274 return fullPath, nil
275}
276
277func validateProfile(profile *Profile) error {
278 // check name
279 if profile.Name == "" {
280 return fmt.Errorf("profile name is required")
281 }
282 if slices.Contains(forbiddenProfileNames, profile.Name) {
283 return fmt.Errorf("forbidden profile name '%s'", profile.Name)
284 }
285
286 // check name patterns
287 if len(profile.NamePatterns) == 0 {
288 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 }
295
296 // check type
297 if profile.Type == "" || !slices.Contains(allProfileTypes, profile.Type) {
298 return fmt.Errorf("invalid profile type %s for profile %s", profile.Type, profile.Name)
299 }
300
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 }
306 }
307
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 }
313 }
314
315 // check ScriptType profile
316 if profile.Type == ScriptType {
317 if len(profile.NamePatterns) > 1 {
318 return fmt.Errorf("multiple name patterns are not supported for profile %s, which has Type=%s", profile.Name, profile.Type)
319 }
320 }
321
322 // set files to empty array if nil
323 if profile.Files == nil {
324 profile.Files = []FileInFolder{}
325 }
326
327 return nil
328}