| // Copyright 2016 Google Inc. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // populate holds the code to augment a partial R/W checkout with a |
| // symlink forest into a SlothFS workspace. |
| package populate |
| |
| import ( |
| "bytes" |
| "fmt" |
| "os" |
| "path/filepath" |
| "sort" |
| "strings" |
| ) |
| |
| // symlinkRepo creates symlinks for all the files in `child`. |
| func symlinkRepo(name string, child *repoTree, roRoot, rwRoot string) error { |
| fi, err := os.Stat(filepath.Join(rwRoot, name)) |
| if err == nil && fi.IsDir() { |
| return nil |
| } |
| |
| for e := range child.entries { |
| dest := filepath.Join(rwRoot, name, e) |
| |
| if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil { |
| return err |
| } |
| if err := os.Symlink(filepath.Join(roRoot, name, e), dest); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // createTreeLinks tries to short-cut symlinks for whole trees by |
| // symlinking to the root of a repository in the RO tree. |
| func createTreeLinks(ro, rw *repoTree, roRoot, rwRoot string) error { |
| allRW := rw.allChildren() |
| |
| outer: |
| for nm, ch := range ro.children { |
| foundCheckout := false |
| foundRecurse := false |
| for k := range allRW { |
| if k == "" { |
| continue |
| } |
| if nm == k { |
| foundRecurse = true |
| break |
| } |
| rel, err := filepath.Rel(nm, k) |
| if err != nil { |
| return err |
| } |
| |
| if strings.HasPrefix(rel, "..") { |
| continue |
| } |
| |
| // we have a checkout below "nm". |
| foundCheckout = true |
| break |
| } |
| |
| switch { |
| case foundRecurse: |
| if err := createTreeLinks(ch, rw.children[nm], filepath.Join(roRoot, nm), filepath.Join(rwRoot, nm)); err != nil { |
| return err |
| } |
| continue outer |
| case !foundCheckout: |
| dest := filepath.Join(rwRoot, nm) |
| if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil { |
| return err |
| } |
| if err := os.Symlink(filepath.Join(roRoot, nm), dest); err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| |
| // createLinks will populate a RW tree with symlinks to the RO tree. |
| func createLinks(ro, rw *repoTree, roRoot, rwRoot string) error { |
| if err := createTreeLinks(ro, rw, roRoot, rwRoot); err != nil { |
| return err |
| } |
| |
| rwc := rw.allChildren() |
| for nm, ch := range ro.allChildren() { |
| if _, ok := rwc[nm]; !ok { |
| if err := symlinkRepo(nm, ch, roRoot, rwRoot); err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| |
| // clearLinks removes all symlinks to the RO tree. It returns the workspace name that was linked before. |
| func clearLinks(mount, dir string) (string, error) { |
| mount = filepath.Clean(mount) |
| |
| var prefix string |
| var dirs []string |
| if err := filepath.Walk(dir, func(n string, fi os.FileInfo, err error) error { |
| if fi == nil { |
| return fmt.Errorf("Walk %s: nil fileinfo for %s", dir, n) |
| } |
| if fi.Mode()&os.ModeSymlink != 0 { |
| target, err := os.Readlink(n) |
| if err != nil { |
| return err |
| } |
| if strings.HasPrefix(target, mount) { |
| prefix = target |
| if err := os.Remove(n); err != nil { |
| return err |
| } |
| } |
| } |
| if fi.IsDir() && n != dir { |
| dirs = append(dirs, n) |
| } |
| return nil |
| }); err != nil { |
| return "", fmt.Errorf("Walk %s: %v", dir, err) |
| } |
| |
| // Reverse the ordering, so we get the deepest subdirs first. |
| sort.Strings(dirs) |
| for i := range dirs { |
| d := dirs[len(dirs)-1-i] |
| // Ignore error: dir may still contain entries. |
| os.Remove(d) |
| } |
| |
| prefix = strings.TrimPrefix(prefix, mount+"/") |
| if i := strings.Index(prefix, "/"); i != -1 { |
| prefix = prefix[:i] |
| } |
| return prefix, nil |
| } |
| |
| // Returns the filenames (as relative paths) in newDir that have |
| // changed relative to the files in oldDir. |
| func changedFiles(oldInfos map[string]*fileInfo, newInfos map[string]*fileInfo) ([]string, error) { |
| var changed []string |
| for path, info := range newInfos { |
| old, ok := oldInfos[path] |
| if !ok { |
| changed = append(changed, path) |
| continue |
| } |
| if info.isLink { |
| // TODO(hanwen): maybe we should we deref the link? |
| continue |
| } |
| |
| if old.sha1 == nil || info.sha1 == nil { |
| changed = append(changed, path) |
| continue |
| } |
| if bytes.Compare(old.sha1[:], info.sha1[:]) != 0 { |
| changed = append(changed, path) |
| continue |
| } |
| } |
| sort.Strings(changed) |
| return changed, nil |
| } |
| |
| // Checkout updates a RW dir with new symlinks to the given RO dir. |
| // Returns the files that should be touched. |
| func Checkout(ro, rw string) ([]string, error) { |
| ro = filepath.Clean(ro) |
| wsName, err := clearLinks(filepath.Dir(ro), rw) |
| if err != nil { |
| return nil, err |
| } |
| oldRoot := filepath.Join(filepath.Dir(ro), wsName) |
| |
| // Do the file system traversals in parallel. |
| errs := make(chan error, 3) |
| var rwTree, roTree *repoTree |
| var oldInfos map[string]*fileInfo |
| |
| if wsName != "" { |
| go func() { |
| t, err := repoTreeFromSlothFS(oldRoot) |
| if t != nil { |
| oldInfos = t.allFiles() |
| } |
| errs <- err |
| }() |
| } else { |
| oldInfos = map[string]*fileInfo{} |
| errs <- nil |
| } |
| |
| go func() { |
| t, err := newRepoTree(rw) |
| rwTree = t |
| errs <- err |
| }() |
| go func() { |
| t, err := repoTreeFromSlothFS(ro) |
| roTree = t |
| errs <- err |
| }() |
| |
| for i := 0; i < cap(errs); i++ { |
| err := <-errs |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| if err := createLinks(roTree, rwTree, ro, rw); err != nil { |
| return nil, err |
| } |
| |
| newInfos := roTree.allFiles() |
| changed, err := changedFiles(oldInfos, newInfos) |
| if err != nil { |
| return nil, fmt.Errorf("changedFiles: %v", err) |
| } |
| |
| for i, p := range changed { |
| changed[i] = filepath.Join(ro, p) |
| } |
| |
| return changed, nil |
| } |