megauploader/conf.go

308 lines
8.3 KiB
Go

package megauploader
import (
"fmt"
"io"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/c2h5oh/datasize"
"github.com/pkg/errors"
glob "github.com/ryanuber/go-glob"
)
// helpers {{{
// Globs is an array of globs with convenience methods
type Globs []string
// Has check if any of the Glob matches with needle
func (gs Globs) Has(needle string) bool {
for _, g := range gs {
if glob.Glob(g, needle) {
return true
}
}
return false
}
// ExtensionList is a list of extensions; you MUST NOT include the dot. That is, it must be
// {"mp3", "ogg", "aac"}
// not
// {".docx", ".odt"}
type ExtensionList []string
// Matches check if the provided filename matches any of the extension
func (el *ExtensionList) Matches(needle string) bool {
if len(*el) == 0 {
return true
}
n := strings.ToLower(needle)
for _, ext := range *el {
if strings.HasSuffix(n, fmt.Sprintf(".%s", strings.ToLower(ext))) {
return true
}
}
return false
}
// }}}
// Config contains the whole configuration
type Config struct {
Global Global `yaml:"global"`
UserHome *UserHome `yaml:"userhome"`
Shares []Share `yaml:"shares"`
}
// Pretty returns a pretty-printed representation of the Config data
func (c Config) Pretty() string {
return fmt.Sprintf("GLOBAL\n%+v\nHOME\n%+v\nSHARES\n%+v\n", c.Global, *(c.UserHome), c.Shares)
}
// Validate checks the config for blocking inconsistencies
//
// while at the moment this does few checks, expect it to act on the conservative side
//
// returns a slice of errors; so check with
// if errs := conf.Validate(); len(errs) != 0
func (c Config) Validate() []error {
errs := make([]error, 0)
names := make([]string, len(c.Shares))
if c.Shares != nil {
for i, s := range c.Shares {
names[i] = s.Name
}
sort.Strings(names)
i := 0
for i < len(names)-1 {
if names[i] == names[i+1] {
errs = append(errs, fmt.Errorf("Duplicate '%s' shares", names[i]))
}
i++
}
for i, s := range c.Shares {
// shareRef is a human-understandable,
// 1-indexed reference to this share
shareRef := fmt.Sprintf("Share %d(%s)", i+1, s.Name)
if s.Name == "" {
errs = append(errs, fmt.Errorf("%s has no name", shareRef))
}
if s.Dir == "" {
errs = append(errs, fmt.Errorf("%s has no dir", shareRef))
} else {
var fi os.FileInfo
var err error
if fi, err = os.Stat(s.Dir); err != nil {
errs = append(errs, fmt.Errorf("error accessing dir %s for %s", s.Dir, shareRef))
} else {
if !fi.IsDir() {
errs = append(errs, fmt.Errorf("dir %s for %s is not a dir", s.Dir, shareRef))
}
}
}
}
}
if c.UserHome != nil && c.UserHome.BasePath == "" {
errs = append(errs, errors.New("UserHome requires a BasePath: fill it or omit it"))
}
if (c.Global.ViewPathBase == "") != (c.Global.ViewURLBase == "") {
errs = append(errs, errors.New("viewpathbase and viewurlbase must be both set, or none"))
}
return errs
}
// Global is the global section of the configuration
type Global struct {
Excluded Globs `yaml:"excluded"`
ViewURLBase string `yaml:"viewurlbase"`
ViewPathBase string `yaml:"viewpathbase"`
RoutePrefix string `yaml:"routeprefix"` // if you want to proxy megauploader as a sub-location, that's for you
}
// UserHome specifies rules for generating home shares on the fly
type UserHome struct {
BasePath string `yaml:"basepath"`
Create bool `yaml:"create"`
}
// A Share is a directory where users can upload files
type Share struct {
Name string `yaml:"name"`
Dir string `yaml:"dir"`
Description string `yaml:"description"`
Authorized Globs `yaml:"authorized"`
SizeLimit datasize.ByteSize `yaml:"sizelimit"`
Extensions ExtensionList `yaml:"extensions"`
}
// IsAuthorized checks whether username is autorized to access the Share
func (s Share) IsAuthorized(username string) bool {
return s.Authorized.Has(username)
}
// Upload let you create a new file in the share
//
// If a file with the same name exists, Upload will try to "increment"
// it. If no valid filename is found, an error will be returned.
//
// FIXME: distinguish between validation errors and server errors
func (s Share) Upload(buf io.Reader, filename string) (string, error) {
if !s.Extensions.Matches(filename) {
return "", fmt.Errorf("Extension not admitted for this share")
}
fpath := filepath.Join(s.Dir, filename)
incrementable := newFileIncrement(fpath)
var dst *os.File
var err error
var realFileName string
try := 0
for try < 10 {
realFileName = incrementable.Increment(try)
dst, err = os.OpenFile(realFileName,
os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err == nil {
break
}
try++
}
if try == 10 {
return "", fmt.Errorf("Can't create file %s", filename)
}
defer dst.Close()
_, err = io.Copy(dst, buf)
return realFileName, nil
}
// GetViewURL retrieves the view URL for the specified path
func (g *Global) GetViewURL(dir string) (url.URL, error) {
rel, err := filepath.Rel(g.ViewPathBase, dir)
if err != nil {
return url.URL{}, errors.Wrapf(err, "Error getting view URL for '%s'", dir)
}
if strings.HasPrefix(rel, "../") {
return url.URL{}, fmt.Errorf("Error getting view URL for '%s'", dir)
}
u, err := url.Parse(g.ViewURLBase)
if err != nil {
return url.URL{}, fmt.Errorf("Invalid view URL %s", g.ViewURLBase)
}
u.Path = path.Join(u.Path, rel)
return *u, nil
}
// GetShareURL retrieves the view URL for the share identified by sharename
func (c *Config) GetShareURL(sharename string) (url.URL, error) {
if c.Global.ViewPathBase == "" || c.Global.ViewURLBase == "" {
return url.URL{}, errors.New("View URL not configured")
}
s, err := c.GetShare(sharename)
if err != nil {
return url.URL{}, err
}
return c.Global.GetViewURL(s.Dir)
}
// GetAuthShares returns an array of all the shares the user is authorized to use, including its home.
func (c *Config) GetAuthShares(username string) []Share {
shares := make([]Share, 0)
if c.Global.Excluded.Has(username) {
return shares
}
for _, s := range c.Shares {
if s.IsAuthorized(username) {
shares = append(shares, s)
}
}
home, err := c.GetHome(username)
if err == nil {
shares = append(shares, *home)
}
return shares
}
// GetAuthShare returns a valid share for a certain user.
//
// It includes checking for permissions and dynamically-generated home.
//
// FIXME: can't tell 'unauthorized' from 'not found'
func (c *Config) GetAuthShare(sharename string, username string) (Share, error) {
share, err := c.GetShare(sharename)
if err == nil {
if share.IsAuthorized(username) {
return share, nil
}
return Share{}, errors.New("Unauthorized")
}
home, homeerr := c.GetHome(username)
if homeerr != nil || home.Name != sharename {
return Share{}, errors.New("Cannot find home")
}
// assuming user is always authorized on its dynamically-generated home
return *home, nil
}
func (c *Config) GetShare(name string) (Share, error) {
for _, share := range c.Shares {
if share.Name == name {
return share, nil
}
}
return Share{}, errors.New("share not found")
}
// GetHome returns the home share for a user, if available
// It implements authorization check
func (c *Config) GetHome(username string) (*Share, error) {
// explicit share gets precedence
sharename := fmt.Sprintf("home_%s", username)
share, err := c.GetShare(sharename)
if err == nil {
if share.IsAuthorized(username) {
return &share, nil
}
return nil, fmt.Errorf("Home share for %s is forbidden to its owner", username)
}
if c.Global.Excluded.Has(username) {
return nil, fmt.Errorf("User %s has no home", username)
}
s := new(Share)
s.Name = sharename
s.Authorized = Globs{username}
s.Dir = filepath.Join(c.UserHome.BasePath, username)
stat, err := os.Stat(s.Dir)
if err != nil {
if os.IsNotExist(err) { //doesn't exist
if c.UserHome.Create {
os.Mkdir(s.Dir, os.ModeDir)
} else {
return nil, fmt.Errorf("User %s home is non-existent (and we won't create it)", username)
}
} else {
return nil, errors.Wrapf(err, "access to %s'shome failed", username)
}
} else {
if !stat.IsDir() {
return nil, fmt.Errorf("%s's home exists but is not a directory", username)
}
}
s.Description = fmt.Sprintf("%s's home", username)
return s, nil
}
// URLFetcher is a WIP section of the config to define rules for fetching URLs instead of uploading
type URLFetcher struct {
Enabled bool
AllowedURLs Globs
SizeLimit datasize.ByteSize
}