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  }