github.com/latiif/helm@v2.15.0+incompatible/pkg/downloader/manager.go (about) 1 /* 2 Copyright The Helm Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package downloader 17 18 import ( 19 "errors" 20 "fmt" 21 "io" 22 "io/ioutil" 23 "net/url" 24 "os" 25 "path" 26 "path/filepath" 27 "strings" 28 "sync" 29 30 "github.com/Masterminds/semver" 31 "github.com/ghodss/yaml" 32 33 "k8s.io/helm/pkg/chartutil" 34 "k8s.io/helm/pkg/getter" 35 "k8s.io/helm/pkg/helm/helmpath" 36 "k8s.io/helm/pkg/proto/hapi/chart" 37 "k8s.io/helm/pkg/repo" 38 "k8s.io/helm/pkg/resolver" 39 "k8s.io/helm/pkg/urlutil" 40 ) 41 42 // Manager handles the lifecycle of fetching, resolving, and storing dependencies. 43 type Manager struct { 44 // Out is used to print warnings and notifications. 45 Out io.Writer 46 // ChartPath is the path to the unpacked base chart upon which this operates. 47 ChartPath string 48 // HelmHome is the $HELM_HOME directory 49 HelmHome helmpath.Home 50 // Verification indicates whether the chart should be verified. 51 Verify VerificationStrategy 52 // Debug is the global "--debug" flag 53 Debug bool 54 // Keyring is the key ring file. 55 Keyring string 56 // SkipUpdate indicates that the repository should not be updated first. 57 SkipUpdate bool 58 // Getter collection for the operation 59 Getters []getter.Provider 60 } 61 62 // Build rebuilds a local charts directory from a lockfile. 63 // 64 // If the lockfile is not present, this will run a Manager.Update() 65 // 66 // If SkipUpdate is set, this will not update the repository. 67 func (m *Manager) Build() error { 68 c, err := m.loadChartDir() 69 if err != nil { 70 return err 71 } 72 73 // If a lock file is found, run a build from that. Otherwise, just do 74 // an update. 75 lock, err := chartutil.LoadRequirementsLock(c) 76 if err != nil { 77 return m.Update() 78 } 79 80 // A lock must accompany a requirements.yaml file. 81 req, err := chartutil.LoadRequirements(c) 82 if err != nil { 83 return fmt.Errorf("requirements.yaml cannot be opened: %s", err) 84 } 85 if sum, err := resolver.HashReq(req); err != nil || sum != lock.Digest { 86 return fmt.Errorf("requirements.lock is out of sync with requirements.yaml") 87 } 88 89 // Check that all of the repos we're dependent on actually exist. 90 if err := m.hasAllRepos(lock.Dependencies); err != nil { 91 return err 92 } 93 94 if !m.SkipUpdate { 95 // For each repo in the file, update the cached copy of that repo 96 if err := m.UpdateRepositories(); err != nil { 97 return err 98 } 99 } 100 101 // Now we need to fetch every package here into charts/ 102 return m.downloadAll(lock.Dependencies) 103 } 104 105 // Update updates a local charts directory. 106 // 107 // It first reads the requirements.yaml file, and then attempts to 108 // negotiate versions based on that. It will download the versions 109 // from remote chart repositories unless SkipUpdate is true. 110 func (m *Manager) Update() error { 111 c, err := m.loadChartDir() 112 if err != nil { 113 return err 114 } 115 116 // If no requirements file is found, we consider this a successful 117 // completion. 118 req, err := chartutil.LoadRequirements(c) 119 if err != nil { 120 if err == chartutil.ErrRequirementsNotFound { 121 fmt.Fprintf(m.Out, "No requirements found in %s/charts.\n", m.ChartPath) 122 return nil 123 } 124 return err 125 } 126 127 // Hash requirements.yaml 128 hash, err := resolver.HashReq(req) 129 if err != nil { 130 return err 131 } 132 133 // Check that all of the repos we're dependent on actually exist and 134 // the repo index names. 135 repoNames, err := m.getRepoNames(req.Dependencies) 136 if err != nil { 137 return err 138 } 139 140 // For each repo in the file, update the cached copy of that repo 141 if !m.SkipUpdate { 142 if err := m.UpdateRepositories(); err != nil { 143 return err 144 } 145 } 146 147 // Now we need to find out which version of a chart best satisfies the 148 // requirements the requirements.yaml 149 lock, err := m.resolve(req, repoNames, hash) 150 if err != nil { 151 return err 152 } 153 154 // Now we need to fetch every package here into charts/ 155 if err := m.downloadAll(lock.Dependencies); err != nil { 156 return err 157 } 158 159 // If the lock file hasn't changed, don't write a new one. 160 oldLock, err := chartutil.LoadRequirementsLock(c) 161 if err == nil && oldLock.Digest == lock.Digest { 162 return nil 163 } 164 165 // Finally, we need to write the lockfile. 166 return writeLock(m.ChartPath, lock) 167 } 168 169 func (m *Manager) loadChartDir() (*chart.Chart, error) { 170 if fi, err := os.Stat(m.ChartPath); err != nil { 171 return nil, fmt.Errorf("could not find %s: %s", m.ChartPath, err) 172 } else if !fi.IsDir() { 173 return nil, errors.New("only unpacked charts can be updated") 174 } 175 return chartutil.LoadDir(m.ChartPath) 176 } 177 178 // resolve takes a list of requirements and translates them into an exact version to download. 179 // 180 // This returns a lock file, which has all of the requirements normalized to a specific version. 181 func (m *Manager) resolve(req *chartutil.Requirements, repoNames map[string]string, hash string) (*chartutil.RequirementsLock, error) { 182 res := resolver.New(m.ChartPath, m.HelmHome) 183 return res.Resolve(req, repoNames, hash) 184 } 185 186 // downloadAll takes a list of dependencies and downloads them into charts/ 187 // 188 // It will delete versions of the chart that exist on disk and might cause 189 // a conflict. 190 func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { 191 repos, err := m.loadChartRepositories() 192 if err != nil { 193 return err 194 } 195 196 destPath := filepath.Join(m.ChartPath, "charts") 197 tmpPath := filepath.Join(m.ChartPath, "tmpcharts") 198 199 // Create 'charts' directory if it doesn't already exist. 200 if fi, err := os.Stat(destPath); err != nil { 201 if err := os.MkdirAll(destPath, 0755); err != nil { 202 return err 203 } 204 } else if !fi.IsDir() { 205 return fmt.Errorf("%q is not a directory", destPath) 206 } 207 208 if err := os.Rename(destPath, tmpPath); err != nil { 209 return fmt.Errorf("Unable to move current charts to tmp dir: %v", err) 210 } 211 212 if err := os.MkdirAll(destPath, 0755); err != nil { 213 return err 214 } 215 216 fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) 217 var saveError error 218 for _, dep := range deps { 219 // No repository means the chart is in charts directory 220 if dep.Repository == "" { 221 fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name) 222 chartPath := filepath.Join(tmpPath, dep.Name) 223 ch, err := chartutil.LoadDir(chartPath) 224 if err != nil { 225 return fmt.Errorf("Unable to load chart: %v", err) 226 } 227 228 constraint, err := semver.NewConstraint(dep.Version) 229 if err != nil { 230 return fmt.Errorf("Dependency %s has an invalid version/constraint format: %s", dep.Name, err) 231 } 232 233 v, err := semver.NewVersion(ch.Metadata.Version) 234 if err != nil { 235 return fmt.Errorf("Invalid version %s for dependency %s: %s", dep.Version, dep.Name, err) 236 } 237 238 if !constraint.Check(v) { 239 saveError = fmt.Errorf("Dependency %s at version %s does not satisfy the constraint %s", dep.Name, ch.Metadata.Version, dep.Version) 240 break 241 } 242 continue 243 } 244 if strings.HasPrefix(dep.Repository, "file://") { 245 if m.Debug { 246 fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository) 247 } 248 ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version) 249 if err != nil { 250 saveError = err 251 break 252 } 253 dep.Version = ver 254 continue 255 } 256 257 fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) 258 259 // Any failure to resolve/download a chart should fail: 260 // https://github.com/kubernetes/helm/issues/1439 261 churl, username, password, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos) 262 if err != nil { 263 saveError = fmt.Errorf("could not find %s: %s", churl, err) 264 break 265 } 266 267 dl := ChartDownloader{ 268 Out: m.Out, 269 Verify: m.Verify, 270 Keyring: m.Keyring, 271 HelmHome: m.HelmHome, 272 Getters: m.Getters, 273 Username: username, 274 Password: password, 275 } 276 277 if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil { 278 saveError = fmt.Errorf("could not download %s: %s", churl, err) 279 break 280 } 281 } 282 283 if saveError == nil { 284 fmt.Fprintln(m.Out, "Deleting outdated charts") 285 for _, dep := range deps { 286 // Chart from local charts directory stays in place 287 if dep.Repository != "" { 288 if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil { 289 return err 290 } 291 } 292 } 293 if err := move(tmpPath, destPath); err != nil { 294 return err 295 } 296 if err := os.RemoveAll(tmpPath); err != nil { 297 return fmt.Errorf("Failed to remove %v: %v", tmpPath, err) 298 } 299 } else { 300 fmt.Fprintln(m.Out, "Save error occurred: ", saveError) 301 fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state") 302 for _, dep := range deps { 303 if err := m.safeDeleteDep(dep.Name, destPath); err != nil { 304 return err 305 } 306 } 307 if err := os.RemoveAll(destPath); err != nil { 308 return fmt.Errorf("Failed to remove %v: %v", destPath, err) 309 } 310 if err := os.Rename(tmpPath, destPath); err != nil { 311 return fmt.Errorf("Unable to move current charts to tmp dir: %v", err) 312 } 313 return saveError 314 } 315 return nil 316 } 317 318 // safeDeleteDep deletes any versions of the given dependency in the given directory. 319 // 320 // It does this by first matching the file name to an expected pattern, then loading 321 // the file to verify that it is a chart with the same name as the given name. 322 // 323 // Because it requires tar file introspection, it is more intensive than a basic delete. 324 // 325 // This will only return errors that should stop processing entirely. Other errors 326 // will emit log messages or be ignored. 327 func (m *Manager) safeDeleteDep(name, dir string) error { 328 files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz")) 329 if err != nil { 330 // Only for ErrBadPattern 331 return err 332 } 333 for _, fname := range files { 334 ch, err := chartutil.LoadFile(fname) 335 if err != nil { 336 fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err) 337 continue 338 } 339 if ch.Metadata.Name != name { 340 // This is not the file you are looking for. 341 continue 342 } 343 if err := os.Remove(fname); err != nil { 344 fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err) 345 continue 346 } 347 } 348 return nil 349 } 350 351 // hasAllRepos ensures that all of the referenced deps are in the local repo cache. 352 func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error { 353 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 354 if err != nil { 355 return err 356 } 357 repos := rf.Repositories 358 359 // Verify that all repositories referenced in the deps are actually known 360 // by Helm. 361 missing := []string{} 362 for _, dd := range deps { 363 // If repo is from local path, continue 364 if strings.HasPrefix(dd.Repository, "file://") { 365 continue 366 } 367 368 found := false 369 if dd.Repository == "" { 370 found = true 371 } else { 372 for _, repo := range repos { 373 if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) { 374 found = true 375 } 376 } 377 } 378 if !found { 379 missing = append(missing, dd.Repository) 380 } 381 } 382 if len(missing) > 0 { 383 return fmt.Errorf("no repository definition for %s. Please add the missing repos via 'helm repo add'", strings.Join(missing, ", ")) 384 } 385 return nil 386 } 387 388 // getRepoNames returns the repo names of the referenced deps which can be used to fetch the cached index file. 389 func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string, error) { 390 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 391 if err != nil { 392 return nil, err 393 } 394 repos := rf.Repositories 395 396 reposMap := make(map[string]string) 397 398 // Verify that all repositories referenced in the deps are actually known 399 // by Helm. 400 missing := []string{} 401 for _, dd := range deps { 402 // Don't map the repository, we don't need to download chart from charts directory 403 if dd.Repository == "" { 404 continue 405 } 406 // if dep chart is from local path, verify the path is valid 407 if strings.HasPrefix(dd.Repository, "file://") { 408 if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil { 409 return nil, err 410 } 411 412 if m.Debug { 413 fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository) 414 } 415 reposMap[dd.Name] = dd.Repository 416 continue 417 } 418 419 found := false 420 421 for _, repo := range repos { 422 if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) || 423 (strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) { 424 found = true 425 dd.Repository = repo.URL 426 reposMap[dd.Name] = repo.Name 427 break 428 } else if urlutil.Equal(repo.URL, dd.Repository) { 429 found = true 430 reposMap[dd.Name] = repo.Name 431 break 432 } 433 } 434 if !found { 435 repository := dd.Repository 436 // Add if URL 437 _, err := url.ParseRequestURI(repository) 438 if err == nil { 439 reposMap[repository] = repository 440 continue 441 } 442 missing = append(missing, repository) 443 } 444 } 445 446 if len(missing) > 0 { 447 errorMessage := fmt.Sprintf("no repository definition for %s. Please add them via 'helm repo add'", strings.Join(missing, ", ")) 448 // It is common for people to try to enter "stable" as a repository instead of the actual URL. 449 // For this case, let's give them a suggestion. 450 containsNonURL := false 451 for _, repo := range missing { 452 if !strings.Contains(repo, "//") && !strings.HasPrefix(repo, "@") && !strings.HasPrefix(repo, "alias:") { 453 containsNonURL = true 454 } 455 } 456 if containsNonURL { 457 errorMessage += ` 458 Note that repositories must be URLs or aliases. For example, to refer to the stable 459 repository, use "https://kubernetes-charts.storage.googleapis.com/" or "@stable" instead of 460 "stable". Don't forget to add the repo, too ('helm repo add').` 461 } 462 return nil, errors.New(errorMessage) 463 } 464 465 return reposMap, nil 466 } 467 468 // UpdateRepositories updates all of the local repos to the latest. 469 func (m *Manager) UpdateRepositories() error { 470 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 471 if err != nil { 472 return err 473 } 474 if repos := rf.Repositories; len(repos) > 0 { 475 // This prints warnings straight to out. 476 if err := m.parallelRepoUpdate(repos); err != nil { 477 return err 478 } 479 } 480 return nil 481 } 482 483 func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { 484 out := m.Out 485 fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") 486 var wg sync.WaitGroup 487 for _, c := range repos { 488 r, err := repo.NewChartRepository(c, m.Getters) 489 if err != nil { 490 return err 491 } 492 wg.Add(1) 493 go func(r *repo.ChartRepository) { 494 if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil { 495 fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", r.Config.Name, r.Config.URL, err) 496 } else { 497 fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name) 498 } 499 wg.Done() 500 }(r) 501 } 502 wg.Wait() 503 fmt.Fprintln(out, "Update Complete.") 504 return nil 505 } 506 507 // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified. 508 // 509 // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the 510 // newest version will be returned. 511 // 512 // repoURL is the repository to search 513 // 514 // If it finds a URL that is "relative", it will prepend the repoURL. 515 func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) { 516 for _, cr := range repos { 517 if urlutil.Equal(repoURL, cr.Config.URL) { 518 var entry repo.ChartVersions 519 entry, err = findEntryByName(name, cr) 520 if err != nil { 521 return 522 } 523 var ve *repo.ChartVersion 524 ve, err = findVersionedEntry(version, entry) 525 if err != nil { 526 return 527 } 528 url, err = normalizeURL(repoURL, ve.URLs[0]) 529 if err != nil { 530 return 531 } 532 username = cr.Config.Username 533 password = cr.Config.Password 534 return 535 } 536 } 537 url, err = repo.FindChartInRepoURL(repoURL, name, version, "", "", "", m.Getters) 538 if err == nil { 539 return 540 } 541 err = fmt.Errorf("chart %s not found in %s", name, repoURL) 542 return 543 } 544 545 // findEntryByName finds an entry in the chart repository whose name matches the given name. 546 // 547 // It returns the ChartVersions for that entry. 548 func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) { 549 for ename, entry := range cr.IndexFile.Entries { 550 if ename == name { 551 return entry, nil 552 } 553 } 554 return nil, errors.New("entry not found") 555 } 556 557 // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints. 558 // 559 // If version is empty, the first chart found is returned. 560 func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) { 561 for _, verEntry := range vers { 562 if len(verEntry.URLs) == 0 { 563 // Not a legit entry. 564 continue 565 } 566 567 if version == "" || versionEquals(version, verEntry.Version) { 568 return verEntry, nil 569 } 570 } 571 return nil, errors.New("no matching version") 572 } 573 574 func versionEquals(v1, v2 string) bool { 575 sv1, err := semver.NewVersion(v1) 576 if err != nil { 577 // Fallback to string comparison. 578 return v1 == v2 579 } 580 sv2, err := semver.NewVersion(v2) 581 if err != nil { 582 return false 583 } 584 return sv1.Equal(sv2) 585 } 586 587 func normalizeURL(baseURL, urlOrPath string) (string, error) { 588 u, err := url.Parse(urlOrPath) 589 if err != nil { 590 return urlOrPath, err 591 } 592 if u.IsAbs() { 593 return u.String(), nil 594 } 595 u2, err := url.Parse(baseURL) 596 if err != nil { 597 return urlOrPath, fmt.Errorf("Base URL failed to parse: %s", err) 598 } 599 600 u2.Path = path.Join(u2.Path, urlOrPath) 601 return u2.String(), nil 602 } 603 604 // loadChartRepositories reads the repositories.yaml, and then builds a map of 605 // ChartRepositories. 606 // 607 // The key is the local name (which is only present in the repositories.yaml). 608 func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) { 609 indices := map[string]*repo.ChartRepository{} 610 repoyaml := m.HelmHome.RepositoryFile() 611 612 // Load repositories.yaml file 613 rf, err := repo.LoadRepositoriesFile(repoyaml) 614 if err != nil { 615 return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err) 616 } 617 618 for _, re := range rf.Repositories { 619 lname := re.Name 620 cacheindex := m.HelmHome.CacheIndex(lname) 621 index, err := repo.LoadIndexFile(cacheindex) 622 if err != nil { 623 return indices, err 624 } 625 626 // TODO: use constructor 627 cr := &repo.ChartRepository{ 628 Config: re, 629 IndexFile: index, 630 } 631 indices[lname] = cr 632 } 633 return indices, nil 634 } 635 636 // writeLock writes a lockfile to disk 637 func writeLock(chartpath string, lock *chartutil.RequirementsLock) error { 638 data, err := yaml.Marshal(lock) 639 if err != nil { 640 return err 641 } 642 dest := filepath.Join(chartpath, "requirements.lock") 643 return ioutil.WriteFile(dest, data, 0644) 644 } 645 646 // tarFromLocalDir archive a dep chart from local directory and save it into charts/ 647 func tarFromLocalDir(chartpath, name, repo, version string) (string, error) { 648 destPath := filepath.Join(chartpath, "charts") 649 650 if !strings.HasPrefix(repo, "file://") { 651 return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo) 652 } 653 654 origPath, err := resolver.GetLocalPath(repo, chartpath) 655 if err != nil { 656 return "", err 657 } 658 659 ch, err := chartutil.LoadDir(origPath) 660 if err != nil { 661 return "", err 662 } 663 664 constraint, err := semver.NewConstraint(version) 665 if err != nil { 666 return "", fmt.Errorf("dependency %s has an invalid version/constraint format: %s", name, err) 667 } 668 669 v, err := semver.NewVersion(ch.Metadata.Version) 670 if err != nil { 671 return "", err 672 } 673 674 if constraint.Check(v) { 675 _, err = chartutil.Save(ch, destPath) 676 return ch.Metadata.Version, err 677 } 678 679 return "", fmt.Errorf("can't get a valid version for dependency %s", name) 680 } 681 682 // move files from tmppath to destpath 683 func move(tmpPath, destPath string) error { 684 files, _ := ioutil.ReadDir(tmpPath) 685 for _, file := range files { 686 filename := file.Name() 687 tmpfile := filepath.Join(tmpPath, filename) 688 destfile := filepath.Join(destPath, filename) 689 if err := os.Rename(tmpfile, destfile); err != nil { 690 return fmt.Errorf("Unable to move local charts to charts dir: %v", err) 691 } 692 } 693 return nil 694 } 695 696 func copyFile(source string, destination string) (err error) { 697 sourceFile, err := os.Open(source) 698 if err != nil { 699 return err 700 } 701 defer sourceFile.Close() 702 destinationFile, err := os.Create(destination) 703 if err != nil { 704 return err 705 } 706 defer destinationFile.Close() 707 _, err = io.Copy(destinationFile, sourceFile) 708 if err == nil { 709 stats, err := os.Stat(source) 710 if err == nil { 711 return os.Chmod(destination, stats.Mode()) 712 } 713 } 714 return err 715 } 716 717 func copyDir(source string, destination string) (err error) { 718 fi, err := os.Stat(source) 719 if err != nil { 720 return err 721 } 722 if !fi.IsDir() { 723 return fmt.Errorf("Source is not a directory") 724 } 725 _, err = os.Open(destination) 726 if !os.IsNotExist(err) { 727 return fmt.Errorf("Destination already exists") 728 } 729 err = os.MkdirAll(destination, fi.Mode()) 730 if err != nil { 731 return err 732 } 733 734 entries, err := ioutil.ReadDir(source) 735 for _, entry := range entries { 736 sourceFile := source + "/" + entry.Name() 737 destinationFile := destination + "/" + entry.Name() 738 if entry.IsDir() { 739 err = copyDir(sourceFile, destinationFile) 740 if err != nil { 741 return err 742 } 743 } else { 744 err = copyFile(sourceFile, destinationFile) 745 if err != nil { 746 return err 747 } 748 } 749 } 750 return 751 }