github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/fileutil/files.go (about) 1 package fileutil 2 3 import ( 4 "bytes" 5 "crypto/rand" 6 "encoding/hex" 7 "fmt" 8 "io" 9 "io/fs" 10 "os" 11 "path/filepath" 12 "regexp" 13 "strings" 14 "text/template" 15 16 "runtime" 17 18 "github.com/drud/ddev/pkg/output" 19 "github.com/drud/ddev/pkg/util" 20 ) 21 22 // CopyFile copies the contents of the file named src to the file named 23 // by dst. The file will be created if it does not already exist. If the 24 // destination file exists, all its contents will be replaced by the contents 25 // of the source file. The file mode will be copied from the source and 26 // the copied data is synced/flushed to stable storage. Credit @m4ng0squ4sh https://gist.github.com/m4ng0squ4sh/92462b38df26839a3ca324697c8cba04 27 func CopyFile(src string, dst string) error { 28 in, err := os.Open(src) 29 if err != nil { 30 return err 31 } 32 defer util.CheckClose(in) 33 out, err := os.Create(dst) 34 if err != nil { 35 return fmt.Errorf("Failed to create file %v, err: %v", src, err) 36 } 37 defer util.CheckClose(out) 38 _, err = io.Copy(out, in) 39 if err != nil { 40 return fmt.Errorf("Failed to copy file from %v to %v err: %v", src, dst, err) 41 } 42 43 err = out.Sync() 44 if err != nil { 45 return err 46 } 47 48 // os.Chmod fails on long path (> 256 characters) on windows. 49 // A description of this problem with golang is at https://github.com/golang/dep/issues/774#issuecomment-311560825 50 // It could end up fixed in a future version of golang. 51 if runtime.GOOS != "windows" { 52 si, err := os.Stat(src) 53 if err != nil { 54 return err 55 } 56 57 err = os.Chmod(dst, si.Mode()) 58 if err != nil { 59 return fmt.Errorf("Failed to chmod file %v to mode %v, err=%v", dst, si.Mode(), err) 60 } 61 } 62 63 return nil 64 } 65 66 // CopyDir recursively copies a directory tree, attempting to preserve permissions. 67 // Source directory must exist, destination directory must *not* exist. 68 // Symlinks are ignored and skipped. Credit @r0l1 https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04 69 func CopyDir(src string, dst string) error { 70 src = filepath.Clean(src) 71 dst = filepath.Clean(dst) 72 73 si, err := os.Stat(src) 74 if err != nil { 75 return err 76 } 77 if !si.IsDir() { 78 return fmt.Errorf("CopyDir: source directory %s is not a directory", src) 79 } 80 81 _, err = os.Stat(dst) 82 if err != nil && !os.IsNotExist(err) { 83 return err 84 } 85 if err == nil { 86 return fmt.Errorf("CopyDir: destination %s already exists", dst) 87 } 88 89 err = os.MkdirAll(dst, si.Mode()) 90 if err != nil { 91 return err 92 } 93 94 dirEntrySlice, err := os.ReadDir(src) 95 if err != nil { 96 return err 97 } 98 99 for _, de := range dirEntrySlice { 100 101 srcPath := filepath.Join(src, de.Name()) 102 dstPath := filepath.Join(dst, de.Name()) 103 104 if de.IsDir() { 105 err = CopyDir(srcPath, dstPath) 106 if err != nil { 107 return err 108 } 109 } else { 110 deInfo, err := de.Info() 111 if err != nil { 112 return err 113 } 114 err = CopyFile(srcPath, dstPath) 115 if err != nil && deInfo.Mode()&os.ModeSymlink != 0 { 116 output.UserOut.Warnf("failed to copy symlink %s, skipping...\n", srcPath) 117 continue 118 } 119 if err != nil { 120 return err 121 } 122 } 123 } 124 125 return nil 126 } 127 128 // IsDirectory returns true if path is a dir, false on error or not directory 129 func IsDirectory(path string) bool { 130 fileInfo, err := os.Stat(path) 131 if err != nil { 132 return false 133 } 134 return fileInfo.IsDir() 135 } 136 137 // FileIsReadable checks to make sure a file exists and is readable 138 func FileIsReadable(name string) bool { 139 file, err := os.OpenFile(name, os.O_RDONLY, 0666) 140 if err != nil { 141 return false 142 } 143 file.Close() 144 return true 145 } 146 147 // PurgeDirectory removes all of the contents of a given 148 // directory, leaving the directory itself intact. 149 func PurgeDirectory(path string) error { 150 dir, err := os.Open(path) 151 if err != nil { 152 return err 153 } 154 155 defer util.CheckClose(dir) 156 157 files, err := dir.Readdirnames(-1) 158 if err != nil { 159 return err 160 } 161 162 for _, file := range files { 163 err = os.Chmod(filepath.Join(path, file), 0777) 164 if err != nil { 165 return err 166 } 167 err = os.RemoveAll(filepath.Join(path, file)) 168 if err != nil { 169 return err 170 } 171 } 172 return nil 173 } 174 175 // FgrepStringInFile is a small hammer for looking for a literal string in a file. 176 // It should only be used against very modest sized files, as the entire file is read 177 // into a string. 178 func FgrepStringInFile(fullPath string, needle string) (bool, error) { 179 fullFileBytes, err := os.ReadFile(fullPath) 180 if err != nil { 181 return false, err 182 } 183 fullFileString := string(fullFileBytes) 184 return strings.Contains(fullFileString, needle), nil 185 } 186 187 // GrepStringInFile is a small hammer for looking for a regex in a file. 188 // It should only be used against very modest sized files, as the entire file is read 189 // into a string. 190 func GrepStringInFile(fullPath string, needle string) (bool, error) { 191 fullFileBytes, err := os.ReadFile(fullPath) 192 if err != nil { 193 return false, fmt.Errorf("failed to open file %s, err:%v ", fullPath, err) 194 } 195 fullFileString := string(fullFileBytes) 196 re := regexp.MustCompile(needle) 197 matches := re.FindStringSubmatch(fullFileString) 198 return len(matches) > 0, nil 199 } 200 201 // ListFilesInDir returns an array of files or directories found in a directory 202 func ListFilesInDir(path string) ([]string, error) { 203 var fileList []string 204 dirEntrySlice, err := os.ReadDir(path) 205 if err != nil { 206 return fileList, err 207 } 208 209 for _, de := range dirEntrySlice { 210 fileList = append(fileList, de.Name()) 211 } 212 return fileList, nil 213 } 214 215 // ListFilesInDirFullPath returns an array of full path of files found in a directory 216 func ListFilesInDirFullPath(path string) ([]string, error) { 217 var fileList []string 218 dirEntrySlice, err := os.ReadDir(path) 219 if err != nil { 220 return fileList, err 221 } 222 223 for _, de := range dirEntrySlice { 224 fileList = append(fileList, filepath.Join(path, de.Name())) 225 } 226 return fileList, nil 227 } 228 229 // RandomFilenameBase generates a temporary filename for use in testing or whatever. 230 // From https://stackoverflow.com/a/28005931/215713 231 func RandomFilenameBase() string { 232 randBytes := make([]byte, 16) 233 _, _ = rand.Read(randBytes) 234 return hex.EncodeToString(randBytes) 235 } 236 237 // ReplaceStringInFile takes search and replace strings, an original path, and a dest path, returns error 238 func ReplaceStringInFile(searchString string, replaceString string, origPath string, destPath string) error { 239 input, err := os.ReadFile(origPath) 240 if err != nil { 241 return err 242 } 243 244 output := bytes.Replace(input, []byte(searchString), []byte(replaceString), -1) 245 246 // nolint: revive 247 if err = os.WriteFile(destPath, output, 0666); err != nil { 248 return err 249 } 250 return nil 251 } 252 253 // IsSameFile determines whether two paths refer to the same file/dir 254 func IsSameFile(path1 string, path2 string) (bool, error) { 255 path1fi, err := os.Stat(path1) 256 if err != nil { 257 return false, err 258 } 259 path2fi, err := os.Stat(path2) 260 if err != nil { 261 return false, err 262 } 263 return os.SameFile(path1fi, path2fi), nil 264 } 265 266 // ReadFileIntoString just gets the contents of file into string 267 func ReadFileIntoString(path string) (string, error) { 268 bytes, err := os.ReadFile(path) 269 if err != nil { 270 return "", err 271 } 272 return string(bytes), err 273 } 274 275 // AppendStringToFile takes a path to a file and a string to append 276 // and it appends it, returning err 277 func AppendStringToFile(path string, appendString string) error { 278 f, err := os.OpenFile(path, 279 os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 280 if err != nil { 281 return err 282 } 283 defer f.Close() 284 if _, err := f.WriteString(appendString); err != nil { 285 return err 286 } 287 return nil 288 } 289 290 type XSymContents struct { 291 LinkLocation string 292 LinkTarget string 293 } 294 295 // FindSimulatedXsymSymlinks searches the basePath provided for files 296 // whose first line is XSym, which is used in cifs filesystem for simulated 297 // symlinks. 298 func FindSimulatedXsymSymlinks(basePath string) ([]XSymContents, error) { 299 symLinks := make([]XSymContents, 0) 300 err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error { 301 if err != nil { 302 return err 303 } 304 //TODO: Skip a directory named .git? Skip other arbitrary dirs or files? 305 if !info.IsDir() { 306 if info.Size() == 1067 { 307 contents, err := os.ReadFile(path) 308 if err != nil { 309 return err 310 } 311 lines := strings.Split(string(contents), "\n") 312 if lines[0] != "XSym" { 313 return nil 314 } 315 if len(lines) < 4 { 316 return fmt.Errorf("Apparent XSym doesn't have enough lines: %s", path) 317 } 318 // target is 4th line 319 linkTarget := filepath.Clean(lines[3]) 320 symLinks = append(symLinks, XSymContents{LinkLocation: path, LinkTarget: linkTarget}) 321 } 322 } 323 return nil 324 }) 325 return symLinks, err 326 } 327 328 // ReplaceSimulatedXsymSymlinks walks a list of XSymContents and makes real symlinks 329 // in their place. This is only valid on Windows host, only works with Docker for Windows 330 // (cifs filesystem) 331 func ReplaceSimulatedXsymSymlinks(links []XSymContents) error { 332 for _, item := range links { 333 err := os.Remove(item.LinkLocation) 334 if err != nil { 335 return err 336 } 337 err = os.Symlink(item.LinkTarget, item.LinkLocation) 338 if err != nil { 339 return err 340 } 341 } 342 return nil 343 } 344 345 // CanCreateSymlinks tests to see if it's possible to create a symlink 346 func CanCreateSymlinks() bool { 347 tmpdir := os.TempDir() 348 linkPath := filepath.Join(tmpdir, RandomFilenameBase()) 349 // This doesn't attempt to create the real file; we don't need it. 350 err := os.Symlink(filepath.Join(tmpdir, "realfile.txt"), linkPath) 351 //nolint: errcheck 352 defer os.Remove(linkPath) 353 if err != nil { 354 return false 355 } 356 return true 357 } 358 359 // ReplaceSimulatedLinks walks the path provided and tries to replace XSym links with real ones. 360 func ReplaceSimulatedLinks(path string) { 361 links, err := FindSimulatedXsymSymlinks(path) 362 if err != nil { 363 util.Warning("Error finding XSym Symlinks: %v", err) 364 } 365 if len(links) == 0 { 366 return 367 } 368 369 if !CanCreateSymlinks() { 370 util.Warning("This host computer is unable to create real symlinks, please see the docs to enable developer mode:\n%s\nNote that the simulated symlinks created inside the container will work fine for most projects.", "https://ddev.readthedocs.io/en/stable/users/basics/developer-tools/#windows-os-and-ddev-composer") 371 return 372 } 373 374 err = ReplaceSimulatedXsymSymlinks(links) 375 if err != nil { 376 util.Warning("Failed replacing simulated symlinks: %v", err) 377 } 378 replacedLinks := make([]string, 0) 379 for _, l := range links { 380 replacedLinks = append(replacedLinks, l.LinkLocation) 381 } 382 util.Success("Replaced these simulated symlinks with real symlinks: %v", replacedLinks) 383 return 384 } 385 386 // RemoveContents removes contents of passed directory 387 // From https://stackoverflow.com/questions/33450980/how-to-remove-all-contents-of-a-directory-using-golang 388 func RemoveContents(dir string) error { 389 d, err := os.Open(dir) 390 if err != nil { 391 return err 392 } 393 defer d.Close() 394 names, err := d.Readdirnames(-1) 395 if err != nil { 396 return err 397 } 398 for _, name := range names { 399 err = os.RemoveAll(filepath.Join(dir, name)) 400 if err != nil { 401 return err 402 } 403 } 404 return nil 405 } 406 407 // TemplateStringToFile takes a template string, runs templ.Execute on it, and writes it out to file 408 func TemplateStringToFile(content string, vars map[string]interface{}, targetFilePath string) error { 409 410 templ := template.New("templateStringToFile:" + targetFilePath) 411 templ, err := templ.Parse(content) 412 if err != nil { 413 return err 414 } 415 416 var doc bytes.Buffer 417 err = templ.Execute(&doc, vars) 418 if err != nil { 419 return err 420 } 421 422 f, err := os.Create(targetFilePath) 423 if err != nil { 424 return err 425 } 426 defer util.CheckClose(f) 427 428 _, err = f.WriteString(doc.String()) 429 if err != nil { 430 return nil 431 } 432 return nil 433 } 434 435 // CheckSignatureOrNoFile checks to make sure that a file or directory either doesn't exist 436 // or has #ddev-generated in its contents (so it can be overwritten) 437 // returns nil if overwrite is OK (if sig found or no file existing) 438 func CheckSignatureOrNoFile(path string, signature string) error { 439 var err error 440 switch { 441 case !FileExists(path): 442 return nil 443 444 case FileExists(path) && !IsDirectory(path): 445 found, err := FgrepStringInFile(path, signature) 446 // It's unlikely that we'll get an error, but report it if we do. 447 if err != nil { 448 return err 449 } 450 // We found the file and it has the signature in it. 451 if !found { 452 return fmt.Errorf("signature was not found in file %s", path) 453 } 454 return nil 455 456 case IsDirectory(path): 457 err = filepath.WalkDir(path, func(path string, info fs.DirEntry, err error) error { 458 if err != nil { 459 return err 460 } 461 // If a directory, nothing to do, continue traversing 462 if info.IsDir() { 463 return nil 464 } 465 // If file doesn't exist, nothing to do, continue traversing 466 if !FileExists(path) { 467 return nil 468 } 469 // Now check to see if file has signature. 470 found, err := FgrepStringInFile(path, signature) 471 // It's unlikely that we'll get an error, but report it if we do. 472 if err != nil { 473 return err 474 } 475 // We have the file and it does not have the signature in it. 476 // that means it's not safe to overwrite it. 477 if !found { 478 return fmt.Errorf("signature was not found in file %s", path) 479 } 480 return nil 481 }) 482 } 483 return err 484 }