conf.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. package megauploader
  2. import (
  3. "fmt"
  4. "io"
  5. "net/url"
  6. "os"
  7. "path"
  8. "path/filepath"
  9. "sort"
  10. "strings"
  11. "github.com/c2h5oh/datasize"
  12. "github.com/pkg/errors"
  13. glob "github.com/ryanuber/go-glob"
  14. )
  15. // helpers {{{
  16. // Globs is an array of globs with convenience methods
  17. type Globs []string
  18. // Has check if any of the Glob matches with needle
  19. func (gs Globs) Has(needle string) bool {
  20. for _, g := range gs {
  21. if glob.Glob(g, needle) {
  22. return true
  23. }
  24. }
  25. return false
  26. }
  27. // ExtensionList is a list of extensions; you MUST NOT include the dot. That is, it must be
  28. // {"mp3", "ogg", "aac"}
  29. // not
  30. // {".docx", ".odt"}
  31. type ExtensionList []string
  32. // Matches check if the provided filename matches any of the extension
  33. func (el *ExtensionList) Matches(needle string) bool {
  34. if len(*el) == 0 {
  35. return true
  36. }
  37. n := strings.ToLower(needle)
  38. for _, ext := range *el {
  39. if strings.HasSuffix(n, fmt.Sprintf(".%s", strings.ToLower(ext))) {
  40. return true
  41. }
  42. }
  43. return false
  44. }
  45. // }}}
  46. // Config contains the whole configuration
  47. type Config struct {
  48. Global Global `yaml:"global"`
  49. UserHome *UserHome `yaml:"userhome"`
  50. Shares []Share `yaml:"shares"`
  51. }
  52. // Pretty returns a pretty-printed representation of the Config data
  53. func (c Config) Pretty() string {
  54. return fmt.Sprintf("GLOBAL\n%+v\nHOME\n%+v\nSHARES\n%+v\n", c.Global, *(c.UserHome), c.Shares)
  55. }
  56. // Validate checks the config for blocking inconsistencies
  57. //
  58. // while at the moment this does few checks, expect it to act on the conservative side
  59. //
  60. // returns a slice of errors; so check with
  61. // if errs := conf.Validate(); len(errs) != 0
  62. func (c Config) Validate() []error {
  63. errs := make([]error, 0)
  64. names := make([]string, len(c.Shares))
  65. if c.Shares != nil {
  66. for i, s := range c.Shares {
  67. names[i] = s.Name
  68. }
  69. sort.Strings(names)
  70. i := 0
  71. for i < len(names)-1 {
  72. if names[i] == names[i+1] {
  73. errs = append(errs, fmt.Errorf("Duplicate '%s' shares", names[i]))
  74. }
  75. i++
  76. }
  77. for i, s := range c.Shares {
  78. // shareRef is a human-understandable,
  79. // 1-indexed reference to this share
  80. shareRef := fmt.Sprintf("Share %d(%s)", i+1, s.Name)
  81. if s.Name == "" {
  82. errs = append(errs, fmt.Errorf("%s has no name", shareRef))
  83. }
  84. if s.Dir == "" {
  85. errs = append(errs, fmt.Errorf("%s has no dir", shareRef))
  86. } else {
  87. var fi os.FileInfo
  88. var err error
  89. if fi, err = os.Stat(s.Dir); err != nil {
  90. errs = append(errs, fmt.Errorf("error accessing dir %s for %s", s.Dir, shareRef))
  91. } else {
  92. if !fi.IsDir() {
  93. errs = append(errs, fmt.Errorf("dir %s for %s is not a dir", s.Dir, shareRef))
  94. }
  95. }
  96. }
  97. }
  98. }
  99. if c.UserHome != nil && c.UserHome.BasePath == "" {
  100. errs = append(errs, errors.New("UserHome requires a BasePath: fill it or omit it"))
  101. }
  102. if (c.Global.ViewPathBase == "") != (c.Global.ViewURLBase == "") {
  103. errs = append(errs, errors.New("viewpathbase and viewurlbase must be both set, or none"))
  104. }
  105. return errs
  106. }
  107. // Global is the global section of the configuration
  108. type Global struct {
  109. Excluded Globs `yaml:"excluded"`
  110. ViewURLBase string `yaml:"viewurlbase"`
  111. ViewPathBase string `yaml:"viewpathbase"`
  112. RoutePrefix string `yaml:"routeprefix"` // if you want to proxy megauploader as a sub-location, that's for you
  113. }
  114. // UserHome specifies rules for generating home shares on the fly
  115. type UserHome struct {
  116. BasePath string `yaml:"basepath"`
  117. Create bool `yaml:"create"`
  118. }
  119. // A Share is a directory where users can upload files
  120. type Share struct {
  121. Name string `yaml:"name"`
  122. Dir string `yaml:"dir"`
  123. Description string `yaml:"description"`
  124. Authorized Globs `yaml:"authorized"`
  125. SizeLimit datasize.ByteSize `yaml:"sizelimit"`
  126. Extensions ExtensionList `yaml:"extensions"`
  127. }
  128. // IsAuthorized checks whether username is autorized to access the Share
  129. func (s Share) IsAuthorized(username string) bool {
  130. return s.Authorized.Has(username)
  131. }
  132. // Upload let you create a new file in the share
  133. //
  134. // If a file with the same name exists, Upload will try to "increment"
  135. // it. If no valid filename is found, an error will be returned.
  136. //
  137. // FIXME: distinguish between validation errors and server errors
  138. func (s Share) Upload(buf io.Reader, filename string) (string, error) {
  139. if !s.Extensions.Matches(filename) {
  140. return "", fmt.Errorf("Extension not admitted for this share")
  141. }
  142. fpath := filepath.Join(s.Dir, filename)
  143. incrementable := newFileIncrement(fpath)
  144. var dst *os.File
  145. var err error
  146. var realFileName string
  147. try := 0
  148. for try < 10 {
  149. realFileName = incrementable.Increment(try)
  150. dst, err = os.OpenFile(realFileName,
  151. os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
  152. if err == nil {
  153. break
  154. }
  155. try++
  156. }
  157. if try == 10 {
  158. return "", fmt.Errorf("Can't create file %s", filename)
  159. }
  160. defer dst.Close()
  161. _, err = io.Copy(dst, buf)
  162. return realFileName, nil
  163. }
  164. // GetViewURL retrieves the view URL for the specified path
  165. func (g *Global) GetViewURL(dir string) (url.URL, error) {
  166. rel, err := filepath.Rel(g.ViewPathBase, dir)
  167. if err != nil {
  168. return url.URL{}, errors.Wrapf(err, "Error getting view URL for '%s'", dir)
  169. }
  170. if strings.HasPrefix(rel, "../") {
  171. return url.URL{}, fmt.Errorf("Error getting view URL for '%s'", dir)
  172. }
  173. u, err := url.Parse(g.ViewURLBase)
  174. if err != nil {
  175. return url.URL{}, fmt.Errorf("Invalid view URL %s", g.ViewURLBase)
  176. }
  177. u.Path = path.Join(u.Path, rel)
  178. return *u, nil
  179. }
  180. // GetShareURL retrieves the view URL for the share identified by sharename
  181. func (c *Config) GetShareURL(sharename string) (url.URL, error) {
  182. if c.Global.ViewPathBase == "" || c.Global.ViewURLBase == "" {
  183. return url.URL{}, errors.New("View URL not configured")
  184. }
  185. s, err := c.GetShare(sharename)
  186. if err != nil {
  187. return url.URL{}, err
  188. }
  189. return c.Global.GetViewURL(s.Dir)
  190. }
  191. // GetAuthShares returns an array of all the shares the user is authorized to use, including its home.
  192. func (c *Config) GetAuthShares(username string) []Share {
  193. shares := make([]Share, 0)
  194. if c.Global.Excluded.Has(username) {
  195. return shares
  196. }
  197. for _, s := range c.Shares {
  198. if s.IsAuthorized(username) {
  199. shares = append(shares, s)
  200. }
  201. }
  202. home, err := c.GetHome(username)
  203. if err == nil {
  204. shares = append(shares, *home)
  205. }
  206. return shares
  207. }
  208. // GetAuthShare returns a valid share for a certain user.
  209. //
  210. // It includes checking for permissions and dynamically-generated home.
  211. //
  212. // FIXME: can't tell 'unauthorized' from 'not found'
  213. func (c *Config) GetAuthShare(sharename string, username string) (Share, error) {
  214. share, err := c.GetShare(sharename)
  215. if err == nil {
  216. if share.IsAuthorized(username) {
  217. return share, nil
  218. }
  219. return Share{}, errors.New("Unauthorized")
  220. }
  221. home, homeerr := c.GetHome(username)
  222. if homeerr != nil || home.Name != sharename {
  223. return Share{}, errors.New("Cannot find home")
  224. }
  225. // assuming user is always authorized on its dynamically-generated home
  226. return *home, nil
  227. }
  228. func (c *Config) GetShare(name string) (Share, error) {
  229. for _, share := range c.Shares {
  230. if share.Name == name {
  231. return share, nil
  232. }
  233. }
  234. return Share{}, errors.New("share not found")
  235. }
  236. // GetHome returns the home share for a user, if available
  237. // It implements authorization check
  238. func (c *Config) GetHome(username string) (*Share, error) {
  239. // explicit share gets precedence
  240. sharename := fmt.Sprintf("home_%s", username)
  241. share, err := c.GetShare(sharename)
  242. if err == nil {
  243. if share.IsAuthorized(username) {
  244. return &share, nil
  245. }
  246. return nil, fmt.Errorf("Home share for %s is forbidden to its owner", username)
  247. }
  248. if c.Global.Excluded.Has(username) {
  249. return nil, fmt.Errorf("User %s has no home", username)
  250. }
  251. s := new(Share)
  252. s.Name = sharename
  253. s.Authorized = Globs{username}
  254. s.Dir = filepath.Join(c.UserHome.BasePath, username)
  255. stat, err := os.Stat(s.Dir)
  256. if err != nil {
  257. if os.IsNotExist(err) { //doesn't exist
  258. if c.UserHome.Create {
  259. os.Mkdir(s.Dir, os.ModeDir)
  260. } else {
  261. return nil, fmt.Errorf("User %s home is non-existent (and we won't create it)", username)
  262. }
  263. } else {
  264. return nil, errors.Wrapf(err, "access to %s'shome failed", username)
  265. }
  266. } else {
  267. if !stat.IsDir() {
  268. return nil, fmt.Errorf("%s's home exists but is not a directory", username)
  269. }
  270. }
  271. s.Description = fmt.Sprintf("%s's home", username)
  272. return s, nil
  273. }
  274. // URLFetcher is a WIP section of the config to define rules for fetching URLs instead of uploading
  275. type URLFetcher struct {
  276. Enabled bool
  277. AllowedURLs Globs
  278. SizeLimit datasize.ByteSize
  279. }