github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/service/runtime/source/git/git.go (about)

     1  // Licensed under the Apache License, Version 2.0 (the "License");
     2  // you may not use this file except in compliance with the License.
     3  // You may obtain a copy of the License at
     4  //
     5  //     https://www.apache.org/licenses/LICENSE-2.0
     6  //
     7  // Unless required by applicable law or agreed to in writing, software
     8  // distributed under the License is distributed on an "AS IS" BASIS,
     9  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    10  // See the License for the specific language governing permissions and
    11  // limitations under the License.
    12  //
    13  // Original source: github.com/micro/go-micro/v3/runtime/local/source/git/git.go
    14  
    15  package git
    16  
    17  import (
    18  	"archive/tar"
    19  	"archive/zip"
    20  	"compress/gzip"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"net/http"
    25  	"os"
    26  	"os/exec"
    27  	"path"
    28  	"path/filepath"
    29  	"regexp"
    30  	"strings"
    31  
    32  	"github.com/teris-io/shortid"
    33  )
    34  
    35  const credentialsKey = "GIT_CREDENTIALS"
    36  
    37  type Gitter interface {
    38  	Checkout(repo, branchOrCommit string) error
    39  	RepoDir() string
    40  }
    41  
    42  type binaryGitter struct {
    43  	folder  string
    44  	secrets map[string]string
    45  	client  *http.Client
    46  }
    47  
    48  func (g *binaryGitter) Checkout(repo, branchOrCommit string) error {
    49  	// The implementation of this method is questionable.
    50  	// We use archives from github/gitlab etc which doesnt require the user to have got
    51  	// and probably is faster than downloading the whole repo history,
    52  	// but it comes with a bit of custom code for EACH host.
    53  	// @todo probably we should fall back to git in case the archives are not available.
    54  	doCheckout := func(repo, branchOrCommit string) error {
    55  		if strings.HasPrefix(repo, "https://github.com") {
    56  			return g.checkoutGithub(repo, branchOrCommit)
    57  		} else if strings.HasPrefix(repo, "https://gitlab.com") {
    58  			err := g.checkoutGitLabPublic(repo, branchOrCommit)
    59  			if err != nil && len(g.secrets[credentialsKey]) > 0 {
    60  				// If the public download fails, try getting it with tokens.
    61  				// Private downloads needs a token for api project listing, hence
    62  				// the weird structure of this code.
    63  				return g.checkoutGitLabPrivate(repo, branchOrCommit)
    64  			}
    65  			return err
    66  		}
    67  		if len(g.secrets[credentialsKey]) > 0 {
    68  			return g.checkoutAnyRemote(repo, branchOrCommit, true)
    69  		}
    70  		return g.checkoutAnyRemote(repo, branchOrCommit, false)
    71  	}
    72  
    73  	if branchOrCommit != "latest" {
    74  		return doCheckout(repo, branchOrCommit)
    75  	}
    76  	// default branches
    77  	defaults := []string{"latest", "master", "main", "trunk"}
    78  	var err error
    79  	for _, ref := range defaults {
    80  		err = doCheckout(repo, ref)
    81  		if err == nil {
    82  			return nil
    83  		}
    84  	}
    85  	return err
    86  }
    87  
    88  // This aims to be a generic checkout method. Currently only tested for bitbucket,
    89  // see tests
    90  func (g *binaryGitter) checkoutAnyRemote(repo, branchOrCommit string, useCredentials bool) error {
    91  	repoFolder := strings.ReplaceAll(strings.ReplaceAll(repo, "/", "-"), "https:--", "")
    92  	g.folder = filepath.Join(os.TempDir(),
    93  		repoFolder+"-"+shortid.MustGenerate())
    94  	err := os.MkdirAll(g.folder, 0755)
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	// Assumes remote address format is git@gitlab.com:micro-test/monorepo-test.git
   100  	remoteAddr := fmt.Sprintf("https://%v", strings.TrimPrefix(repo, "https://"))
   101  	if useCredentials {
   102  		remoteAddr = fmt.Sprintf("https://%v@%v", g.secrets[credentialsKey], repo)
   103  	}
   104  
   105  	cmd := exec.Command("git", "clone", remoteAddr, "--depth=1", ".")
   106  	cmd.Dir = g.folder
   107  	outp, err := cmd.CombinedOutput()
   108  	if err != nil {
   109  		return fmt.Errorf("Git clone failed: %v", string(outp))
   110  	}
   111  
   112  	cmd = exec.Command("git", "fetch", "origin", branchOrCommit, "--depth=1")
   113  	cmd.Dir = g.folder
   114  	outp, err = cmd.CombinedOutput()
   115  	if err != nil {
   116  		return fmt.Errorf("Git fetch failed: %v", string(outp))
   117  	}
   118  
   119  	cmd = exec.Command("git", "checkout", "FETCH_HEAD")
   120  	cmd.Dir = g.folder
   121  	outp, err = cmd.CombinedOutput()
   122  	if err != nil {
   123  		return fmt.Errorf("Git  checkout failed: %v", string(outp))
   124  	}
   125  	return nil
   126  }
   127  
   128  func (g *binaryGitter) checkoutGithub(repo, branchOrCommit string) error {
   129  	// @todo if it's a commit it must not be checked out all the time
   130  	repoFolder := strings.ReplaceAll(strings.ReplaceAll(repo, "/", "-"), "https:--", "")
   131  	g.folder = filepath.Join(os.TempDir(),
   132  		repoFolder+"-"+shortid.MustGenerate())
   133  
   134  	url := fmt.Sprintf("%v/archive/%v.zip", repo, branchOrCommit)
   135  	if !strings.HasPrefix(url, "https://") {
   136  		url = "https://" + url
   137  	}
   138  	req, _ := http.NewRequest("GET", url, nil)
   139  	if len(g.secrets[credentialsKey]) > 0 {
   140  		req.Header.Set("Authorization", "token "+g.secrets[credentialsKey])
   141  	}
   142  	resp, err := g.client.Do(req)
   143  	if err != nil {
   144  		return fmt.Errorf("Can't get zip: %v", err)
   145  	}
   146  
   147  	defer resp.Body.Close()
   148  	// Github returns 404 for tar.gz files...
   149  	// but still gives back a proper file so ignoring status code
   150  	// for now.
   151  	//if resp.StatusCode != 200 {
   152  	//	return errors.New("Status code was not 200")
   153  	//}
   154  
   155  	src := g.folder + ".zip"
   156  	// Create the file
   157  	out, err := os.Create(src)
   158  	if err != nil {
   159  		return fmt.Errorf("Can't create source file %v src: %v", src, err)
   160  	}
   161  	defer out.Close()
   162  
   163  	// Write the body to file
   164  	_, err = io.Copy(out, resp.Body)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	return unzip(src, g.folder, true)
   169  }
   170  
   171  func (g *binaryGitter) checkoutGitLabPublic(repo, branchOrCommit string) error {
   172  	// Example: https://gitlab.com/micro-test/basic-micro-service/-/archive/master/basic-micro-service-master.tar.gz
   173  	// @todo if it's a commit it must not be checked out all the time
   174  	repoFolder := strings.ReplaceAll(strings.ReplaceAll(repo, "/", "-"), "https:--", "")
   175  	g.folder = filepath.Join(os.TempDir(),
   176  		repoFolder+"-"+shortid.MustGenerate())
   177  
   178  	tarName := strings.ReplaceAll(strings.ReplaceAll(repo, "gitlab.com/", ""), "/", "-")
   179  	url := fmt.Sprintf("%v/-/archive/%v/%v.tar.gz", repo, branchOrCommit, tarName)
   180  	if !strings.HasPrefix(url, "https://") {
   181  		url = "https://" + url
   182  	}
   183  	req, _ := http.NewRequest("GET", url, nil)
   184  	resp, err := g.client.Do(req)
   185  	if err != nil {
   186  		return fmt.Errorf("Can't get zip: %v", err)
   187  	}
   188  
   189  	defer resp.Body.Close()
   190  
   191  	src := g.folder + ".tar.gz"
   192  	// Create the file
   193  	out, err := os.Create(src)
   194  	if err != nil {
   195  		return fmt.Errorf("Can't create source file %v src: %v", src, err)
   196  	}
   197  	defer out.Close()
   198  
   199  	// Write the body to file
   200  	_, err = io.Copy(out, resp.Body)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	err = Uncompress(src, g.folder)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	// Gitlab zip/tar has contents inside a folder
   209  	// It has the format of eg. basic-micro-service-master-314b4a494ed472793e0a8bce8babbc69359aed7b
   210  	// Since we don't have the commit at this point we must list the dir
   211  	files, err := ioutil.ReadDir(g.folder)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	if len(files) == 0 {
   216  		return fmt.Errorf("No contents in dir downloaded from gitlab: %v", g.folder)
   217  	}
   218  	g.folder = filepath.Join(g.folder, files[0].Name())
   219  	return nil
   220  }
   221  
   222  func (g *binaryGitter) checkoutGitLabPrivate(repo, branchOrCommit string) error {
   223  
   224  	repoFolder := strings.ReplaceAll(strings.ReplaceAll(repo, "/", "-"), "https:--", "")
   225  	g.folder = filepath.Join(os.TempDir(),
   226  		repoFolder+"-"+shortid.MustGenerate())
   227  	tarName := strings.ReplaceAll(strings.ReplaceAll(repo, "gitlab.com/", ""), "/", "-")
   228  
   229  	url := fmt.Sprintf("%v/-/archive/%v/%v.tar.gz?private_token=%v", repo, branchOrCommit, tarName, g.secrets[credentialsKey])
   230  
   231  	if !strings.HasPrefix(url, "https://") {
   232  		url = "https://" + url
   233  	}
   234  	req, _ := http.NewRequest("GET", url, nil)
   235  	resp, err := g.client.Do(req)
   236  	if err != nil {
   237  		return fmt.Errorf("Can't get zip: %v", err)
   238  	}
   239  
   240  	defer resp.Body.Close()
   241  
   242  	src := g.folder + ".tar.gz"
   243  	// Create the file
   244  	out, err := os.Create(src)
   245  	if err != nil {
   246  		return fmt.Errorf("Can't create source file %v src: %v", src, err)
   247  	}
   248  	defer out.Close()
   249  
   250  	// Write the body to file
   251  	_, err = io.Copy(out, resp.Body)
   252  	if err != nil {
   253  		return err
   254  	}
   255  	err = Uncompress(src, g.folder)
   256  	if err != nil {
   257  		return err
   258  	}
   259  	// Gitlab zip/tar has contents inside a folder
   260  	// It has the format of eg. basic-micro-service-master-314b4a494ed472793e0a8bce8babbc69359aed7b
   261  	// Since we don't have the commit at this point we must list the dir
   262  	files, err := ioutil.ReadDir(g.folder)
   263  	if err != nil {
   264  		return err
   265  	}
   266  	if len(files) == 0 {
   267  		return fmt.Errorf("No contents in dir downloaded from gitlab: %v", g.folder)
   268  	}
   269  	g.folder = filepath.Join(g.folder, files[0].Name())
   270  	return nil
   271  }
   272  
   273  func (g *binaryGitter) RepoDir() string {
   274  	return g.folder
   275  }
   276  
   277  func NewGitter(secrets map[string]string) Gitter {
   278  	tmpdir, _ := ioutil.TempDir(os.TempDir(), "git-src-*")
   279  
   280  	return &binaryGitter{
   281  		folder:  tmpdir,
   282  		secrets: secrets,
   283  		client:  &http.Client{},
   284  	}
   285  }
   286  
   287  func commandExists(cmd string) bool {
   288  	_, err := exec.LookPath(cmd)
   289  	return err == nil
   290  }
   291  
   292  func dirifyRepo(s string) string {
   293  	s = strings.ReplaceAll(s, "https://", "")
   294  	s = strings.ReplaceAll(s, "/", "-")
   295  	return s
   296  }
   297  
   298  // exists returns whether the given file or directory exists
   299  func pathExists(path string) (bool, error) {
   300  	_, err := os.Stat(path)
   301  	if err == nil {
   302  		return true, nil
   303  	}
   304  	if os.IsNotExist(err) {
   305  		return false, nil
   306  	}
   307  	return true, err
   308  }
   309  
   310  // GetRepoRoot determines the repo root from a full path.
   311  // Returns empty string and no error if not found
   312  func GetRepoRoot(fullPath string) (string, error) {
   313  	// traverse parent directories
   314  	prev := fullPath
   315  	for {
   316  		current := prev
   317  
   318  		// check for a go.mod
   319  		goExists, err := pathExists(filepath.Join(current, "go.mod"))
   320  		if err != nil {
   321  			return "", err
   322  		}
   323  		if goExists {
   324  			return current, nil
   325  		}
   326  
   327  		// check for .git
   328  		gitExists, err := pathExists(filepath.Join(current, ".git"))
   329  		if err != nil {
   330  			return "", err
   331  		}
   332  		if gitExists {
   333  			return current, nil
   334  		}
   335  
   336  		prev = filepath.Dir(current)
   337  		// reached top level, see:
   338  		// https://play.golang.org/p/rDgVdk3suzb
   339  		if current == prev {
   340  			break
   341  		}
   342  	}
   343  	return "", nil
   344  }
   345  
   346  // Source is not just git related @todo move
   347  type Source struct {
   348  	// is it a local folder intended for a local runtime?
   349  	Local bool
   350  	// absolute path to service folder in local mode
   351  	FullPath string
   352  	// path of folder to repo root
   353  	// be it local or github repo
   354  	Folder string
   355  	// github ref
   356  	Ref string
   357  	// for cloning purposes
   358  	// blank for local
   359  	Repo string
   360  	// dir to repo root
   361  	// blank for non local
   362  	LocalRepoRoot string
   363  }
   364  
   365  // Name to be passed to RPC call runtime.Create Update Delete
   366  // eg: `helloworld/api`, `crufter/myrepo/helloworld/api`, `localfolder`
   367  func (s *Source) RuntimeName() string {
   368  	if len(s.Folder) == 0 {
   369  		// This is the case for top level url source ie. gitlab.com/micro-test/basic-micro-service
   370  		return path.Base(s.Repo)
   371  	}
   372  	return path.Base(s.Folder)
   373  }
   374  
   375  // Source to be passed to RPC call runtime.Create Update Delete
   376  // eg: `helloworld`, `github.com/crufter/myrepo/helloworld`, `/path/to/localrepo/localfolder`
   377  func (s *Source) RuntimeSource() string {
   378  	if s.Local && s.LocalRepoRoot != s.FullPath {
   379  		relpath, _ := filepath.Rel(s.LocalRepoRoot, s.FullPath)
   380  		return relpath
   381  	}
   382  	if s.Local {
   383  		return s.FullPath
   384  	}
   385  	if len(s.Folder) == 0 {
   386  		return s.Repo
   387  	}
   388  	return fmt.Sprintf("%v/%v", s.Repo, s.Folder)
   389  }
   390  
   391  // ParseSource parses a `micro run/update/kill` source.
   392  func ParseSource(source string) (*Source, error) {
   393  	if !strings.Contains(source, "@") {
   394  		source += "@latest"
   395  	}
   396  	ret := &Source{}
   397  	refs := strings.Split(source, "@")
   398  	ret.Ref = refs[1]
   399  	parts := strings.Split(refs[0], "/")
   400  
   401  	max := 3
   402  	if len(parts) < 3 {
   403  		max = len(parts)
   404  	}
   405  	ret.Repo = strings.Join(parts[0:max], "/")
   406  
   407  	if len(parts) > 1 {
   408  		ret.Folder = strings.Join(parts[3:], "/")
   409  	}
   410  
   411  	return ret, nil
   412  }
   413  
   414  // ParseSourceLocal a version of ParseSource that detects and handles local paths.
   415  // Workdir should be used only from the CLI @todo better interface for this function.
   416  // PathExistsFunc exists only for testing purposes, to make the function side effect free.
   417  func ParseSourceLocal(workDir, source string, pathExistsFunc ...func(path string) (bool, error)) (*Source, error) {
   418  	var pexists func(string) (bool, error)
   419  	if len(pathExistsFunc) == 0 {
   420  		pexists = pathExists
   421  	} else {
   422  		pexists = pathExistsFunc[0]
   423  	}
   424  	isLocal, localFullPath := IsLocal(workDir, source, pexists)
   425  	if isLocal {
   426  		localRepoRoot, err := GetRepoRoot(localFullPath)
   427  		if err != nil {
   428  			return nil, err
   429  		}
   430  		var folder string
   431  		// If the local repo root is a top level folder, we are not in a git repo.
   432  		// In this case, we should take the last folder as folder name.
   433  		if localRepoRoot == "" {
   434  			folder = filepath.Base(localFullPath)
   435  		} else {
   436  			folder = strings.ReplaceAll(localFullPath, localRepoRoot+string(filepath.Separator), "")
   437  		}
   438  
   439  		return &Source{
   440  			Local:         true,
   441  			Folder:        folder,
   442  			FullPath:      localFullPath,
   443  			LocalRepoRoot: localRepoRoot,
   444  			Ref:           "latest", // @todo consider extracting branch from git here
   445  		}, nil
   446  	}
   447  	return ParseSource(source)
   448  }
   449  
   450  // IsLocal tries returns true and full path of directory if the path is a local one, and
   451  // false and empty string if not.
   452  func IsLocal(workDir, source string, pathExistsFunc ...func(path string) (bool, error)) (bool, string) {
   453  	var pexists func(string) (bool, error)
   454  	if len(pathExistsFunc) == 0 {
   455  		pexists = pathExists
   456  	} else {
   457  		pexists = pathExistsFunc[0]
   458  	}
   459  	// Check for absolute path
   460  	// @todo "/" won't work for Windows
   461  	if exists, err := pexists(source); strings.HasPrefix(source, "/") && err == nil && exists {
   462  		return true, source
   463  		// Check for path relative to workdir
   464  	} else if exists, err := pexists(filepath.Join(workDir, source)); err == nil && exists {
   465  		return true, filepath.Join(workDir, source)
   466  	}
   467  	return false, ""
   468  }
   469  
   470  // CheckoutSource checks out a git repo (source) into a local temp directory. It will return the
   471  // source of the local repo an an error if one occured. Secrets can optionally be passed if the repo
   472  // is private.
   473  func CheckoutSource(source *Source, secrets map[string]string) (string, error) {
   474  	gitter := NewGitter(secrets)
   475  	repo := source.Repo
   476  	if !strings.Contains(repo, "https://") {
   477  		repo = "https://" + repo
   478  	}
   479  	if err := gitter.Checkout(repo, source.Ref); err != nil {
   480  		return "", err
   481  	}
   482  	return gitter.RepoDir(), nil
   483  }
   484  
   485  // code below is not used yet
   486  
   487  var nameExtractRegexp = regexp.MustCompile(`((micro|web)\.Name\(")(.*)("\))`)
   488  
   489  func extractServiceName(fileContent []byte) string {
   490  	hits := nameExtractRegexp.FindAll(fileContent, 1)
   491  	if len(hits) == 0 {
   492  		return ""
   493  	}
   494  	hit := string(hits[0])
   495  	return strings.Split(hit, "\"")[1]
   496  }
   497  
   498  // Uncompress is a modified version of: https://gist.github.com/mimoo/25fc9716e0f1353791f5908f94d6e726
   499  func Uncompress(src string, dst string) error {
   500  	file, err := os.OpenFile(src, os.O_RDWR|os.O_CREATE, 0666)
   501  	defer file.Close()
   502  	if err != nil {
   503  		return err
   504  	}
   505  	// ungzip
   506  	zr, err := gzip.NewReader(file)
   507  	if err != nil {
   508  		return err
   509  	}
   510  	// untar
   511  	tr := tar.NewReader(zr)
   512  
   513  	// uncompress each element
   514  	for {
   515  		header, err := tr.Next()
   516  		if err == io.EOF {
   517  			break // End of archive
   518  		}
   519  		if err != nil {
   520  			return err
   521  		}
   522  		target := header.Name
   523  
   524  		// validate name against path traversal
   525  		if !validRelPath(header.Name) {
   526  			return fmt.Errorf("tar contained invalid name error %q\n", target)
   527  		}
   528  
   529  		// add dst + re-format slashes according to system
   530  		target = filepath.Join(dst, header.Name)
   531  		// if no join is needed, replace with ToSlash:
   532  		// target = filepath.ToSlash(header.Name)
   533  
   534  		// check the type
   535  		switch header.Typeflag {
   536  
   537  		// if its a dir and it doesn't exist create it (with 0755 permission)
   538  		case tar.TypeDir:
   539  			if _, err := os.Stat(target); err != nil {
   540  				// @todo think about this:
   541  				// if we don't nuke the folder, we might end up with files from
   542  				// the previous decompress.
   543  				if err := os.MkdirAll(target, 0755); err != nil {
   544  					return err
   545  				}
   546  			}
   547  		// if it's a file create it (with same permission)
   548  		case tar.TypeReg:
   549  			// the truncating is probably unnecessary due to the `RemoveAll` of folders
   550  			// above
   551  			fileToWrite, err := os.OpenFile(target, os.O_TRUNC|os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
   552  			if err != nil {
   553  				return err
   554  			}
   555  			// copy over contents
   556  			if _, err := io.Copy(fileToWrite, tr); err != nil {
   557  				return err
   558  			}
   559  			// manually close here after each file operation; defering would cause each file close
   560  			// to wait until all operations have completed.
   561  			fileToWrite.Close()
   562  		}
   563  	}
   564  	return nil
   565  }
   566  
   567  // check for path traversal and correct forward slashes
   568  func validRelPath(p string) bool {
   569  	if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") {
   570  		return false
   571  	}
   572  	return true
   573  }
   574  
   575  // taken from https://stackoverflow.com/questions/20357223/easy-way-to-unzip-file-with-golang
   576  func unzip(src, dest string, skipTopFolder bool) error {
   577  	r, err := zip.OpenReader(src)
   578  	if err != nil {
   579  		return err
   580  	}
   581  	defer func() {
   582  		r.Close()
   583  	}()
   584  
   585  	os.MkdirAll(dest, 0755)
   586  
   587  	// Closure to address file descriptors issue with all the deferred .Close() methods
   588  	extractAndWriteFile := func(f *zip.File) error {
   589  		rc, err := f.Open()
   590  		if err != nil {
   591  			return err
   592  		}
   593  		defer func() {
   594  			rc.Close()
   595  		}()
   596  		if skipTopFolder {
   597  			f.Name = strings.Join(strings.Split(f.Name, string(filepath.Separator))[1:], string(filepath.Separator))
   598  		}
   599  		// zip slip https://snyk.io/research/zip-slip-vulnerability
   600  		destpath, err := zipSafeFilePath(dest, f.Name)
   601  		if err != nil {
   602  			return err
   603  		}
   604  		path := destpath
   605  		if f.FileInfo().IsDir() {
   606  			os.MkdirAll(path, f.Mode())
   607  		} else {
   608  			os.MkdirAll(filepath.Dir(path), f.Mode())
   609  			f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
   610  			if err != nil {
   611  				return err
   612  			}
   613  			defer func() {
   614  				f.Close()
   615  			}()
   616  
   617  			_, err = io.Copy(f, rc)
   618  			if err != nil {
   619  				return err
   620  			}
   621  		}
   622  		return nil
   623  	}
   624  
   625  	for _, f := range r.File {
   626  		err := extractAndWriteFile(f)
   627  		if err != nil {
   628  			return err
   629  		}
   630  	}
   631  
   632  	return nil
   633  }
   634  
   635  // zipSafeFilePath checks whether the file path is safe to use or is a zip slip attack https://snyk.io/research/zip-slip-vulnerability
   636  func zipSafeFilePath(destination, filePath string) (string, error) {
   637  	if len(filePath) == 0 {
   638  		return filepath.Join(destination, filePath), nil
   639  	}
   640  	destination, _ = filepath.Abs(destination) //explicit the destination folder to prevent that 'string.HasPrefix' check can be 'bypassed' when no destination folder is supplied in input
   641  	destpath := filepath.Join(destination, filePath)
   642  	if !strings.HasPrefix(destpath, filepath.Clean(destination)+string(os.PathSeparator)) {
   643  		return "", fmt.Errorf("%s: illegal file path", filePath)
   644  	}
   645  	return destpath, nil
   646  }