// +build linux darwin freebsd /* Copyright The containerd Authors. 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 continuityfs import ( "context" "errors" "fmt" "io" "os" "path/filepath" "syscall" "bazil.org/fuse" "bazil.org/fuse/fs" "github.com/containerd/continuity" "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" ) // File represents any file type (non directory) in the filesystem type File struct { inode uint64 uid uint32 gid uint32 provider FileContentProvider resource continuity.Resource } // NewFile creates a new file with the given inode and content provider func NewFile(inode uint64, provider FileContentProvider) *File { return &File{ inode: inode, provider: provider, } } func (f *File) setResource(r continuity.Resource) (err error) { // TODO: error out if uid excesses uint32? f.uid = uint32(r.UID()) f.gid = uint32(r.GID()) f.resource = r return } // Attr sets the fuse attribute for the file func (f *File) Attr(ctx context.Context, attr *fuse.Attr) (err error) { // Set attributes from resource metadata attr.Mode = f.resource.Mode() attr.Uid = f.uid attr.Gid = f.gid if rf, ok := f.resource.(continuity.RegularFile); ok { attr.Nlink = uint32(len(rf.Paths())) attr.Size = uint64(rf.Size()) } else { attr.Nlink = 1 } attr.Inode = f.inode return nil } // Open opens the file for read // currently only regular files can be opened func (f *File) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) { var dgst digest.Digest if rf, ok := f.resource.(continuity.RegularFile); ok { digests := rf.Digests() if len(digests) > 0 { dgst = digests[0] } } // TODO(dmcgowan): else check if device can be opened for read r, err := f.provider.Open(f.resource.Path(), dgst) if err != nil { logrus.Debugf("Error opening handle: %v", err) return nil, err } return &fileHandler{ reader: r, }, nil } func (f *File) getDirent(name string) (fuse.Dirent, error) { var t fuse.DirentType switch f.resource.(type) { case continuity.RegularFile: t = fuse.DT_File case continuity.SymLink: t = fuse.DT_Link case continuity.Device: t = fuse.DT_Block case continuity.NamedPipe: t = fuse.DT_FIFO default: t = fuse.DT_Unknown } return fuse.Dirent{ Inode: f.inode, Type: t, Name: name, }, nil } type fileHandler struct { offset int64 reader io.ReadCloser } func (h *fileHandler) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { if h.offset != req.Offset { if seeker, ok := h.reader.(io.Seeker); ok { if _, err := seeker.Seek(req.Offset, io.SeekStart); err != nil { logrus.Debugf("Error seeking: %v", err) return err } h.offset = req.Offset } else { return errors.New("unable to seek to offset") } } n, err := h.reader.Read(resp.Data[:req.Size]) if err != nil { logrus.Debugf("Read error: %v", err) return err } h.offset = h.offset + int64(n) resp.Data = resp.Data[:n] return nil } func (h *fileHandler) Release(ctx context.Context, req *fuse.ReleaseRequest) error { return h.reader.Close() } // Dir represents a file system directory type Dir struct { inode uint64 uid uint32 gid uint32 nodes map[string]fs.Node provider FileContentProvider resource continuity.Resource } // Attr sets the fuse attributes for the directory func (d *Dir) Attr(ctx context.Context, attr *fuse.Attr) (err error) { if d.resource == nil { attr.Mode = os.ModeDir | 0555 } else { attr.Mode = d.resource.Mode() } attr.Uid = d.uid attr.Gid = d.gid attr.Inode = d.inode return nil } func (d *Dir) getDirent(name string) (fuse.Dirent, error) { return fuse.Dirent{ Inode: d.inode, Type: fuse.DT_Dir, Name: name, }, nil } type direnter interface { getDirent(name string) (fuse.Dirent, error) } // Lookup looks up the filesystem node for the name within the directory func (d *Dir) Lookup(ctx context.Context, name string) (fs.Node, error) { node, ok := d.nodes[name] if !ok { return nil, fuse.ENOENT } return node, nil } // ReadDirAll reads all the directory entries func (d *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { ents := make([]fuse.Dirent, 0, len(d.nodes)) for name, node := range d.nodes { if nd, ok := node.(direnter); ok { de, err := nd.getDirent(name) if err != nil { return nil, err } ents = append(ents, de) } else { logrus.Errorf("%s does not have a directory entry", name) } } return ents, nil } func (d *Dir) setResource(r continuity.Resource) (err error) { d.uid = uint32(r.UID()) d.gid = uint32(r.GID()) d.resource = r return } // NewDir creates a new directory object func NewDir(inode uint64, provider FileContentProvider) *Dir { return &Dir{ inode: inode, nodes: map[string]fs.Node{}, provider: provider, } } var ( rootPath = fmt.Sprintf("%c", filepath.Separator) ) func addNode(path string, node fs.Node, cache map[string]*Dir, provider FileContentProvider) { dirPath, file := filepath.Split(path) d, ok := cache[dirPath] if !ok { d = NewDir(0, provider) cache[dirPath] = d addNode(filepath.Clean(dirPath), d, cache, provider) } d.nodes[file] = node logrus.Debugf("%s (%#v) added to %s", file, node, dirPath) } type treeRoot struct { root *Dir } func (t treeRoot) Root() (fs.Node, error) { logrus.Debugf("Returning root with %#v", t.root.nodes) return t.root, nil } // NewFSFromManifest creates a fuse filesystem using the given manifest // to create the node tree and the content provider to serve up // content for regular files. func NewFSFromManifest(manifest *continuity.Manifest, mountRoot string, provider FileContentProvider) (fs.FS, error) { tree := treeRoot{ root: NewDir(0, provider), } fi, err := os.Stat(mountRoot) if err != nil { return nil, err } st, ok := fi.Sys().(*syscall.Stat_t) if !ok { return nil, errors.New("could not access directory") } tree.root.uid = st.Uid tree.root.gid = st.Gid dirCache := map[string]*Dir{ rootPath: tree.root, } for i, resource := range manifest.Resources { inode := uint64(i) + 1 if _, ok := resource.(continuity.Directory); ok { cleanPath := filepath.Clean(resource.Path()) keyPath := fmt.Sprintf("%s%c", cleanPath, filepath.Separator) d, ok := dirCache[keyPath] if !ok { d = NewDir(inode, provider) dirCache[keyPath] = d addNode(cleanPath, d, dirCache, provider) } else { d.inode = inode } if err := d.setResource(resource); err != nil { return nil, err } continue } f := NewFile(inode, provider) if err := f.setResource(resource); err != nil { return nil, err } if rf, ok := resource.(continuity.RegularFile); ok { for _, p := range rf.Paths() { addNode(p, f, dirCache, provider) } } else { addNode(resource.Path(), f, dirCache, provider) } } return tree, nil }