| // 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. |
| |
| package fs |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "log" |
| "path/filepath" |
| "strings" |
| |
| "github.com/google/slothfs/cache" |
| "github.com/google/slothfs/gitiles" |
| "github.com/google/slothfs/manifest" |
| "github.com/hanwen/go-fuse/fuse" |
| "github.com/hanwen/go-fuse/fuse/nodefs" |
| git "github.com/libgit2/git2go" |
| ) |
| |
| type manifestFSRoot struct { |
| nodefs.Node |
| |
| service *gitiles.Service |
| |
| cache *cache.Cache |
| nodeCache *nodeCache |
| |
| // trees is Path => Tree map. |
| trees map[string]*gitiles.Tree |
| |
| options ManifestOptions |
| |
| // XML data for the manifest. |
| manifestXML []byte |
| } |
| |
| func (r *manifestFSRoot) Deletable() bool { return false } |
| |
| func (r *manifestFSRoot) GetXAttr(attribute string, context *fuse.Context) (data []byte, code fuse.Status) { |
| return nil, fuse.ENODATA |
| } |
| |
| // NewManifestFS creates a Manifest FS root node. |
| func NewManifestFS(service *gitiles.Service, cache *cache.Cache, opts ManifestOptions) (nodefs.Node, error) { |
| xml, err := opts.Manifest.MarshalXML() |
| if err != nil { |
| return nil, err |
| } |
| root := &manifestFSRoot{ |
| Node: newDirNode(), |
| nodeCache: newNodeCache(), |
| cache: cache, |
| service: service, |
| options: opts, |
| manifestXML: xml, |
| } |
| |
| for _, p := range opts.Manifest.Project { |
| if _, err := git.NewOid(p.Revision); err != nil { |
| return nil, fmt.Errorf("project %s revision %q does not parse: %v", p.Name, p.Revision, err) |
| } |
| } |
| |
| root.trees, err = fetchTreeMap(cache, service, opts.Manifest) |
| if err != nil { |
| return nil, err |
| } |
| return root, nil |
| } |
| |
| func (r *manifestFSRoot) OnMount(fsConn *nodefs.FileSystemConnector) { |
| if err := r.onMount(fsConn); err != nil { |
| log.Printf("onMount: %v", err) |
| for k := range r.Inode().Children() { |
| r.Inode().RmChild(k) |
| } |
| |
| r.Inode().NewChild("ERROR", false, newDataNode([]byte(err.Error()))) |
| } |
| |
| // Don't need the trees anymore. |
| r.trees = nil |
| } |
| |
| func (r *manifestFSRoot) onMount(fsConn *nodefs.FileSystemConnector) error { |
| var byDepth [][]string |
| for p := range r.trees { |
| d := len(strings.Split(p, "/")) |
| for len(byDepth) <= d { |
| byDepth = append(byDepth, nil) |
| } |
| |
| byDepth[d] = append(byDepth[d], p) |
| } |
| |
| clonablePaths := map[string]bool{} |
| revmap := map[string]*manifest.Project{} |
| for i, p := range r.options.Manifest.Project { |
| revmap[p.GetPath()] = &r.options.Manifest.Project[i] |
| |
| if p.CloneDepth == "" { |
| clonablePaths[p.GetPath()] = true |
| } |
| } |
| |
| // TODO(hanwen): use parallelism here. |
| |
| for _, ps := range byDepth { |
| for _, p := range ps { |
| dir, base := filepath.Split(p) |
| parent, left := fsConn.Node(r.Inode(), dir) |
| for _, c := range left { |
| ch := parent.NewChild(c, true, newDirNode()) |
| parent = ch |
| } |
| |
| clone, ok := clonablePaths[p] |
| if !ok { |
| for _, e := range r.options.RepoCloneOption { |
| if e.RE.FindString(p) != "" { |
| clone = e.Clone |
| break |
| } |
| } |
| } |
| |
| cloneURL := revmap[p].CloneURL |
| if !clone { |
| cloneURL = "" |
| } |
| |
| repoService := r.service.NewRepoService(revmap[p].Name) |
| |
| opts := GitilesRevisionOptions{ |
| Revision: revmap[p].Revision, |
| GitilesOptions: GitilesOptions{ |
| CloneURL: cloneURL, |
| CloneOption: r.options.FileCloneOption, |
| }, |
| } |
| |
| subRoot := NewGitilesRoot(r.cache, r.trees[p], repoService, opts) |
| subRoot.(*gitilesRoot).nodeCache = r.nodeCache |
| parent.NewChild(base, true, subRoot) |
| if err := subRoot.(*gitilesRoot).onMount(fsConn); err != nil { |
| return fmt.Errorf("onMount(%s): %v", p, err) |
| } |
| } |
| } |
| |
| // Do Linkfile, Copyfile after setting up the repos, so we |
| // have directories to attach the copy/link nodes to. |
| for _, p := range r.options.Manifest.Project { |
| for _, cp := range p.Copyfile { |
| srcNode, left := fsConn.Node(r.Inode(), filepath.Join(p.GetPath(), cp.Src)) |
| if len(left) > 0 { |
| return fmt.Errorf("Copyfile(%s): source %s does not exist", p.Name, cp.Src) |
| } |
| |
| dir, left := fsConn.Node(r.Inode(), cp.Dest) |
| switch len(left) { |
| case 0: |
| return fmt.Errorf("Copyfile(%s): dest %s already exists.", p.Name, cp.Dest) |
| case 1: |
| default: |
| return fmt.Errorf("Copyfile(%s): directory for dest %s does not exist.", p.Name, cp.Dest) |
| } |
| |
| dir.AddChild(left[0], srcNode) |
| } |
| |
| for _, lf := range p.Linkfile { |
| dir, left := fsConn.Node(r.Inode(), lf.Dest) |
| switch len(left) { |
| case 0: |
| return fmt.Errorf("Linkfile(%s): dest %s already exists.", p.Name, lf.Dest) |
| case 1: |
| default: |
| return fmt.Errorf("Linkfile(%s): directory for dest %s does not exist.", p.Name, lf.Dest) |
| } |
| |
| src := filepath.Join(p.GetPath(), lf.Src) |
| rel, err := filepath.Rel(filepath.Dir(lf.Dest), src) |
| if err != nil { |
| return err |
| } |
| |
| node := newLinkNode(filepath.Join(rel)) |
| dir.NewChild(left[0], false, node) |
| } |
| } |
| |
| metaNode := r.Inode().NewChild(".slothfs", true, newDirNode()) |
| metaNode.NewChild("manifest.xml", false, newDataNode(r.manifestXML)) |
| |
| var tree gitiles.Tree |
| treeContent, err := json.Marshal(tree) |
| if err != nil { |
| log.Panicf("json.Marshal: %v", err) |
| } |
| metaNode.NewChild("tree.json", false, newDataNode(treeContent)) |
| |
| return nil |
| } |
| |
| func fetchTreeMap(c *cache.Cache, service *gitiles.Service, mf *manifest.Manifest) (map[string]*gitiles.Tree, error) { |
| type resultT struct { |
| path string |
| resp *gitiles.Tree |
| err error |
| } |
| |
| // Fetch all the trees in parallel. |
| out := make(chan resultT, len(mf.Project)) |
| for _, p := range mf.Project { |
| go func(p manifest.Project) { |
| revID, err := git.NewOid(p.Revision) |
| if err != nil { |
| out <- resultT{p.GetPath(), nil, err} |
| return |
| } |
| |
| tree, err := c.Tree.Get(revID) |
| cached := (err == nil && tree != nil) |
| if err != nil { |
| if repo := c.Git.OpenLocal(p.CloneURL); repo != nil { |
| defer repo.Free() |
| tree, err = cache.GetTree(repo, revID) |
| } |
| } |
| |
| if err != nil { |
| repoService := service.NewRepoService(p.Name) |
| |
| tree, err = repoService.GetTree(p.Revision, "", true) |
| } |
| |
| if !cached && tree != nil && err == nil { |
| if err := c.Tree.Add(revID, tree); err != nil { |
| log.Printf("treeCache.Add: %v", err) |
| } |
| } |
| |
| out <- resultT{p.GetPath(), tree, err} |
| }(p) |
| } |
| |
| // drain goroutines |
| var result []resultT |
| for range mf.Project { |
| r := <-out |
| result = append(result, r) |
| } |
| |
| resmap := map[string]*gitiles.Tree{} |
| for _, r := range result { |
| if r.err != nil { |
| return nil, fmt.Errorf("Tree(%s): %v", r.path, r.err) |
| } |
| |
| resmap[r.path] = r.resp |
| } |
| return resmap, nil |
| } |