blob: 4977164eaba7564250a6eca890d853c807fa19c8 [file] [log] [blame]
// Copyright 2016 The Upspin Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package filesystem provides a DirServer and StoreServer that serve
// files from a local file system.
package filesystem // import "upspin.io/exp/filesystem"
import (
"io/ioutil"
"os"
gPath "path"
"path/filepath"
"upspin.io/access"
"upspin.io/cache"
"upspin.io/errors"
"upspin.io/path"
"upspin.io/upspin"
)
const (
packing = upspin.EEIntegrityPack
maxCacheEntries = 10000
)
var errReadOnly = errors.Str("read-only name space")
// Server provides DirServer and StoreServer implementations
// that serve files from a local file system.
type Server struct {
// Set by New.
server upspin.Config
root string
defaultAccess *access.Access
dirEntries *cache.LRU
// Set by Dial.
user upspin.Config
}
// New creates a new filesystem Server instance serving the
// given root with the provided server configuration.
func New(cfg upspin.Config, root string) (*Server, error) {
const op = "exp/filesystem.New"
root = filepath.Clean(root)
if !filepath.IsAbs(root) {
return nil, errors.E(op, errors.Invalid, errors.Str("root must be an absolute path"))
}
if fi, err := os.Stat(root); os.IsNotExist(err) {
return nil, errors.E(op, errors.NotExist, err)
} else if err != nil {
return nil, errors.E(op, errors.IO, err)
} else if !fi.IsDir() {
return nil, errors.E(op, errors.Str("root must be a directory"))
}
defaultAccess, err := access.New(upspin.PathName(cfg.UserName()) + "/Access")
if err != nil {
return nil, errors.E(op, err)
}
return &Server{
server: cfg,
root: root,
defaultAccess: defaultAccess,
dirEntries: cache.NewLRU(maxCacheEntries),
}, nil
}
func (s *Server) Ping() bool {
return true
}
func (s *Server) Close() {
}
func (s *Server) Endpoint() upspin.Endpoint {
return upspin.Endpoint{} // No endpoint.
}
// can reports whether the calling user has
// the given right to access the given path.
func (s *Server) can(right access.Right, parsed path.Parsed) (bool, error) {
a := s.defaultAccess
afn, err := s.whichAccess(parsed)
if err != nil {
return false, err
}
if afn != "" {
data, err := s.readFile(afn)
if err != nil {
return false, err
}
a, err = access.Parse(afn, data)
if err != nil {
return false, err
}
}
return a.Can(s.user.UserName(), right, parsed.Path(), s.readFile)
}
// whichAccess is the core of the WhichAccess method,
// factored out so it can be called from other locations.
func (s *Server) whichAccess(parsed path.Parsed) (upspin.PathName, error) {
// Look for Access file starting at end of local path.
for i := 0; i <= parsed.NElem(); i++ {
dir := filepath.Join(s.root, filepath.FromSlash(parsed.Drop(i).FilePath()))
if fi, err := os.Stat(dir); err != nil {
return "", err
} else if !fi.IsDir() {
continue
}
name := filepath.Join(dir, "Access")
fi, err := os.Stat(name)
// Must exist and be a plain file.
if os.IsNotExist(err) {
continue
}
if err != nil {
return "", err
}
// File exists. Is it a regular file?
accessFile := gPath.Join(parsed.Drop(i).String(), "Access")
if !fi.Mode().IsRegular() {
return "", errors.Errorf("%q is not a regular file", accessFile)
}
fd, err := os.Open(name)
if err != nil {
// File exists but cannot be read.
return "", err
}
fd.Close()
return upspin.PathName(accessFile), nil
}
return "", nil
}
// readFile returns the contents of the named file relative to the server root.
// The file must be world-readable, or readFile returns a permissoin error.
func (s *Server) readFile(name upspin.PathName) ([]byte, error) {
parsed, err := path.Parse(name)
if err != nil {
return nil, err
}
localName := filepath.Join(s.root, parsed.FilePath())
info, err := os.Stat(localName)
if err != nil {
return nil, err
}
if info.IsDir() {
return nil, errors.E(errors.IsDir, name)
}
// Require world-readability on the local file system
// to prevent accidental information leakage (e.g. $HOME/.ssh).
// TODO(r,adg): find a less conservative policy for this.
if info.Mode()&04 == 0 {
return nil, errors.E(errors.Permission, errors.Str("not world-readable"), name)
}
// TODO(r, adg): think about symbolic links.
return ioutil.ReadFile(localName)
}