123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- 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"`
- }
- // 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)
- 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
- }
|