kcl-lang.io/kpm@v0.8.7-0.20240520061008-9fc4c5efc8c7/pkg/utils/utils.go (about) 1 package utils 2 3 import ( 4 "archive/tar" 5 "bufio" 6 "compress/gzip" 7 "crypto/sha256" 8 "encoding/base64" 9 goerrors "errors" 10 "fmt" 11 "io" 12 "log" 13 "net/url" 14 "os" 15 "path/filepath" 16 "regexp" 17 "strings" 18 19 "github.com/docker/distribution/reference" 20 "github.com/moby/term" 21 22 "kcl-lang.io/kpm/pkg/constants" 23 "kcl-lang.io/kpm/pkg/errors" 24 "kcl-lang.io/kpm/pkg/reporter" 25 ) 26 27 // HashDir computes the checksum of a directory by concatenating all files and 28 // hashing them by sha256. 29 func HashDir(dir string) (string, error) { 30 hasher := sha256.New() 31 32 err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 33 if err != nil { 34 return err 35 } 36 37 if info.IsDir() { 38 return nil 39 } 40 41 // files in the ".git "directory will cause the same repository, cloned at different times, 42 // has different checksum. 43 for _, ignore := range ignores { 44 if strings.Contains(path, ignore) { 45 return nil 46 } 47 } 48 49 f, err := os.Open(path) 50 if err != nil { 51 return err 52 } 53 defer f.Close() 54 55 if _, err := io.Copy(hasher, f); err != nil { 56 return err 57 } 58 59 return nil 60 }) 61 62 if err != nil { 63 return "", err 64 } 65 66 return base64.StdEncoding.EncodeToString(hasher.Sum(nil)), nil 67 } 68 69 // StoreToFile will store 'data' into toml file under 'filePath'. 70 func StoreToFile(filePath string, dataStr string) error { 71 file, err := os.Create(filePath) 72 if err != nil { 73 reporter.ExitWithReport("failed to create file: ", filePath, err) 74 return err 75 } 76 defer file.Close() 77 78 file, err = os.Create(filePath) 79 80 if err != nil { 81 return err 82 } 83 defer file.Close() 84 85 if _, err := io.WriteString(file, dataStr); err != nil { 86 return err 87 } 88 return nil 89 } 90 91 // ParseRepoNameFromGitUrl get the repo name from git url, 92 // the repo name in 'https://github.com/xxx/kcl1.git' is 'kcl1'. 93 func ParseRepoNameFromGitUrl(gitUrl string) string { 94 name := filepath.Base(gitUrl) 95 return name[:len(name)-len(filepath.Ext(name))] 96 } 97 98 // CreateFileIfNotExist will check whether the file under a certain path 'filePath/fileName' exists, 99 // and return an error if it exists, and call the method 'storeFunc' to save the file if it does not exist. 100 func CreateFileIfNotExist(filePath string, storeFunc func() error) error { 101 _, err := os.Stat(filePath) 102 if os.IsNotExist(err) { 103 err := storeFunc() 104 if err != nil { 105 return reporter.NewErrorEvent(reporter.FailedCreateFile, err, fmt.Sprintf("failed to create '%s'", filePath)) 106 } 107 } else { 108 return reporter.NewErrorEvent(reporter.FileExists, err, fmt.Sprintf("'%s' already exists", filePath)) 109 } 110 return nil 111 } 112 113 // Whether the file exists 114 func Exists(path string) (bool, error) { 115 _, err := os.Stat(path) 116 if os.IsNotExist(err) { 117 return false, nil 118 } 119 if err != nil { 120 return false, err 121 } 122 123 return true, nil 124 } 125 126 // todo: Consider using the OCI tarball as the standard tar format. 127 var ignores = []string{".git", ".tar"} 128 129 func TarDir(srcDir string, tarPath string, include []string, exclude []string) error { 130 fw, err := os.Create(tarPath) 131 if err != nil { 132 log.Fatal(err) 133 } 134 defer fw.Close() 135 136 tw := tar.NewWriter(fw) 137 defer tw.Close() 138 139 err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { 140 if err != nil { 141 return err 142 } 143 144 for _, ignore := range ignores { 145 if strings.Contains(path, ignore) { 146 return nil 147 } 148 } 149 150 getNewPattern := func(ex string) string { 151 newPath := ex 152 if !strings.HasPrefix(ex, srcDir+string(filepath.Separator)) { 153 newPath = filepath.Join(srcDir, ex) 154 } 155 return newPath 156 } 157 158 for _, ex := range exclude { 159 if matched, _ := filepath.Match(getNewPattern(ex), path); matched { 160 return nil 161 } 162 } 163 164 for _, inc := range include { 165 if matched, _ := filepath.Match(getNewPattern(inc), path); !matched { 166 return nil 167 } 168 } 169 170 relPath, _ := filepath.Rel(srcDir, path) 171 relPath = filepath.ToSlash(relPath) 172 173 hdr, err := tar.FileInfoHeader(info, "") 174 if err != nil { 175 return err 176 } 177 hdr.Name = relPath 178 179 if err := tw.WriteHeader(hdr); err != nil { 180 return err 181 } 182 183 if info.IsDir() || info.Mode()&os.ModeSymlink != 0 { 184 return nil 185 } 186 187 fr, err := os.Open(path) 188 if err != nil { 189 return err 190 } 191 defer fr.Close() 192 193 if _, err := io.Copy(tw, fr); err != nil { 194 return err 195 } 196 197 return nil 198 }) 199 200 return err 201 } 202 203 // UnTarDir will extract tar from 'tarPath' to 'destDir'. 204 func UnTarDir(tarPath string, destDir string) error { 205 file, err := os.Open(tarPath) 206 if err != nil { 207 return reporter.NewErrorEvent(reporter.FailedCreateFile, err, fmt.Sprintf("failed to open '%s'", tarPath)) 208 } 209 defer file.Close() 210 211 tarReader := tar.NewReader(file) 212 213 for { 214 header, err := tarReader.Next() 215 if err == io.EOF { 216 break 217 } 218 if err != nil { 219 return reporter.NewErrorEvent(reporter.FailedCreateFile, err, fmt.Sprintf("failed to open '%s'", tarPath)) 220 } 221 222 destFilePath := filepath.Join(destDir, header.Name) 223 switch header.Typeflag { 224 case tar.TypeDir: 225 if err := os.MkdirAll(destFilePath, 0755); err != nil { 226 return errors.FailedUnTarKclPackage 227 } 228 case tar.TypeReg: 229 err := os.MkdirAll(filepath.Dir(destFilePath), 0755) 230 if err != nil { 231 return err 232 } 233 outFile, err := os.Create(destFilePath) 234 if err != nil { 235 return reporter.NewErrorEvent(reporter.FailedCreateFile, err, fmt.Sprintf("failed to open '%s'", tarPath)) 236 } 237 defer outFile.Close() 238 239 if _, err := io.Copy(outFile, tarReader); err != nil { 240 return reporter.NewErrorEvent(reporter.FailedCreateFile, err, fmt.Sprintf("failed to open '%s'", tarPath)) 241 } 242 default: 243 return errors.UnknownTarFormat 244 } 245 } 246 return nil 247 } 248 249 // ExtractTarball support extracting tarball with '.tgz' format. 250 func ExtractTarball(tarPath, destDir string) error { 251 f, err := os.Open(tarPath) 252 if err != nil { 253 return reporter.NewErrorEvent(reporter.FailedCreateFile, err, fmt.Sprintf("failed to open '%s'", tarPath)) 254 } 255 defer f.Close() 256 257 zip, err := gzip.NewReader(f) 258 if err != nil { 259 return reporter.NewErrorEvent(reporter.FailedCreateFile, err, fmt.Sprintf("failed to open '%s'", tarPath)) 260 } 261 tarReader := tar.NewReader(zip) 262 263 for { 264 header, err := tarReader.Next() 265 if err == io.EOF { 266 break 267 } 268 if err != nil { 269 return reporter.NewErrorEvent(reporter.FailedCreateFile, err, fmt.Sprintf("failed to open '%s'", tarPath)) 270 } 271 272 destFilePath := filepath.Join(destDir, header.Name) 273 switch header.Typeflag { 274 case tar.TypeDir: 275 if err := os.MkdirAll(destFilePath, 0755); err != nil { 276 return errors.FailedUnTarKclPackage 277 } 278 case tar.TypeReg: 279 err := os.MkdirAll(filepath.Dir(destFilePath), 0755) 280 if err != nil { 281 return err 282 } 283 outFile, err := os.Create(destFilePath) 284 if err != nil { 285 return reporter.NewErrorEvent(reporter.FailedCreateFile, err, fmt.Sprintf("failed to open '%s'", tarPath)) 286 } 287 defer outFile.Close() 288 289 if _, err := io.Copy(outFile, tarReader); err != nil { 290 return reporter.NewErrorEvent(reporter.FailedCreateFile, err, fmt.Sprintf("failed to open '%s'", tarPath)) 291 } 292 default: 293 return errors.UnknownTarFormat 294 } 295 } 296 297 return nil 298 } 299 300 // DirExists will check whether the directory 'path' exists. 301 func DirExists(path string) bool { 302 _, err := os.Stat(path) 303 return err == nil 304 } 305 306 // IsSymlinkValidAndExists will check whether the symlink exists and points to a valid target 307 // return three values: whether the symlink exists, whether it points to a valid target, and any error encountered 308 // Note: IsSymlinkValidAndExists is only useful on unix-like systems. 309 func IsSymlinkValidAndExists(symlinkPath string) (bool, bool, error) { 310 // check if the symlink exists 311 info, err := os.Lstat(symlinkPath) 312 if err != nil && os.IsNotExist(err) { 313 // symlink does not exist 314 return false, false, nil 315 } else if err != nil { 316 // other error 317 return false, false, err 318 } 319 320 // check if the file is a symlink 321 if info.Mode()&os.ModeSymlink == os.ModeSymlink { 322 // get the target of the symlink 323 target, err := os.Readlink(symlinkPath) 324 if err != nil { 325 // can not read the target 326 return true, false, err 327 } 328 329 // check if the target exists 330 _, err = os.Stat(target) 331 if err == nil { 332 // target exists 333 return true, true, nil 334 } 335 if os.IsNotExist(err) { 336 // target does not exist 337 return true, false, nil 338 } 339 return true, false, err 340 } 341 342 return false, false, fmt.Errorf("%s exists but is not a symlink", symlinkPath) 343 } 344 345 // DefaultKpmHome create the '.kpm' in the user home and return the path of ".kpm". 346 func CreateSubdirInUserHome(subdir string) (string, error) { 347 homeDir, err := os.UserHomeDir() 348 if err != nil { 349 return "", reporter.NewErrorEvent(reporter.Bug, err, "internal bugs, failed to load user home directory") 350 } 351 352 dirPath := filepath.Join(homeDir, subdir) 353 if !DirExists(dirPath) { 354 err = os.MkdirAll(dirPath, 0755) 355 if err != nil { 356 return "", reporter.NewErrorEvent(reporter.Bug, err, "internal bugs, failed to create directory") 357 } 358 } 359 360 return dirPath, nil 361 } 362 363 // CreateSymlink will create symbolic link named 'newName' for 'oldName', 364 // and if the symbolic link already exists, it will be deleted and recreated. 365 // Note: CreateSymlink is only useful on unix-like systems. 366 func CreateSymlink(oldName, newName string) error { 367 symExist, _, err := IsSymlinkValidAndExists(newName) 368 369 if err != nil { 370 return err 371 } 372 373 if symExist { 374 err := os.Remove(newName) 375 if err != nil { 376 return err 377 } 378 } 379 380 err = os.Symlink(oldName, newName) 381 if err != nil { 382 return err 383 } 384 return nil 385 } 386 387 // Copied/Adapted from https://github.com/helm/helm 388 func GetUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) { 389 var err error 390 username := usernameOpt 391 password := passwordOpt 392 393 if password == "" { 394 if username == "" { 395 username, err = readLine("Username: ", false) 396 if err != nil { 397 return "", "", err 398 } 399 username = strings.TrimSpace(username) 400 } 401 if username == "" { 402 password, err = readLine("Token: ", true) 403 if err != nil { 404 return "", "", err 405 } else if password == "" { 406 return "", "", goerrors.New("token required") 407 } 408 } else { 409 password, err = readLine("Password: ", true) 410 if err != nil { 411 return "", "", err 412 } else if password == "" { 413 return "", "", goerrors.New("password required") 414 } 415 } 416 } 417 418 return username, password, nil 419 } 420 421 // Copied/adapted from https://github.com/helm/helm 422 func readLine(prompt string, silent bool) (string, error) { 423 fmt.Print(prompt) 424 if silent { 425 fd := os.Stdin.Fd() 426 state, err := term.SaveState(fd) 427 if err != nil { 428 return "", err 429 } 430 err = term.DisableEcho(fd, state) 431 if err != nil { 432 return "", err 433 } 434 435 defer func() { 436 restoreErr := term.RestoreTerminal(fd, state) 437 if err == nil { 438 err = restoreErr 439 } 440 }() 441 } 442 443 reader := bufio.NewReader(os.Stdin) 444 line, _, err := reader.ReadLine() 445 if err != nil { 446 return "", err 447 } 448 if silent { 449 fmt.Println() 450 } 451 452 return string(line), nil 453 } 454 455 // FindKFiles will find all the '.k' files in the 'path' directory. 456 func FindKFiles(path string) ([]string, error) { 457 var files []string 458 info, err := os.Stat(path) 459 if err != nil { 460 return nil, err 461 } 462 if !info.IsDir() { 463 if strings.HasSuffix(path, ".k") { 464 files = append(files, path) 465 } 466 return files, nil 467 } 468 469 entries, err := os.ReadDir(path) 470 if err != nil { 471 return nil, err 472 } 473 for _, entry := range entries { 474 if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".k") { 475 files = append(files, filepath.Join(path, entry.Name())) 476 } 477 } 478 return files, nil 479 } 480 481 // RmNewline will remove all the '\r\n' and '\n' in the string 's'. 482 func RmNewline(s string) string { 483 return strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", ""), "\n", "") 484 } 485 486 // JoinPath will join the 'elem' to the 'base' with '/'. 487 func JoinPath(base, elem string) string { 488 base = strings.TrimSuffix(base, "/") 489 elem = strings.TrimPrefix(elem, "/") 490 return base + "/" + elem 491 } 492 493 // IsUrl will check whether the string 'str' is a url. 494 func IsURL(str string) bool { 495 u, err := url.Parse(str) 496 return err == nil && u.Scheme != "" && u.Host != "" 497 } 498 499 // IsGitRepoUrl will check whether the string 'str' is a git repo url 500 func IsGitRepoUrl(str string) bool { 501 r := regexp.MustCompile(`((git|ssh|http(s)?)|(git@[\w\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)?(/)?`) 502 return r.MatchString(str) 503 } 504 505 // IsRef will check whether the string 'str' is a reference. 506 func IsRef(str string) bool { 507 _, err := reference.ParseNormalizedNamed(str) 508 return err == nil 509 } 510 511 // IsTar will check whether the string 'str' is a tar path. 512 func IsTar(str string) bool { 513 return strings.HasSuffix(str, constants.TarPathSuffix) 514 } 515 516 // IsKfile will check whether the string 'str' is a k file path. 517 func IsKfile(str string) bool { 518 return strings.HasSuffix(str, constants.KFilePathSuffix) 519 } 520 521 // CheckPackageSum will check whether the 'checkedSum' is equal 522 // to the hash of the package under 'localPath'. 523 func CheckPackageSum(checkedSum, localPath string) bool { 524 if checkedSum == "" { 525 return false 526 } 527 528 sum, err := HashDir(localPath) 529 530 if err != nil { 531 return false 532 } 533 534 return checkedSum == sum 535 } 536 537 // AbsTarPath checks whether path 'tarPath' exists and whether path 'tarPath' ends with '.tar' 538 // And after checking, absTarPath return the abs path for 'tarPath'. 539 func AbsTarPath(tarPath string) (string, error) { 540 absTarPath, err := filepath.Abs(tarPath) 541 if err != nil { 542 return "", err 543 } 544 545 if filepath.Ext(absTarPath) != ".tar" { 546 return "", errors.InvalidKclPacakgeTar 547 } else if !DirExists(absTarPath) { 548 return "", errors.KclPacakgeTarNotFound 549 } 550 551 return absTarPath, nil 552 }