conf.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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. }
  113. // UserHome specifies rules for generating home shares on the fly
  114. type UserHome struct {
  115. BasePath string `yaml:"basepath"`
  116. Create bool `yaml:"create"`
  117. }
  118. // A Share is a directory where users can upload files
  119. type Share struct {
  120. Name string `yaml:"name"`
  121. Dir string `yaml:"dir"`
  122. Description string `yaml:"description"`
  123. Authorized Globs `yaml:"authorized"`
  124. SizeLimit datasize.ByteSize `yaml:"sizelimit"`
  125. Extensions ExtensionList `yaml:"extensions"`
  126. }
  127. // IsAuthorized checks whether username is autorized to access the Share
  128. func (s Share) IsAuthorized(username string) bool {
  129. return s.Authorized.Has(username)
  130. }
  131. // Upload let you create a new file in the share
  132. //
  133. // If a file with the same name exists, Upload will try to "increment"
  134. // it. If no valid filename is found, an error will be returned.
  135. //
  136. // FIXME: distinguish between validation errors and server errors
  137. func (s Share) Upload(buf io.Reader, filename string) (string, error) {
  138. if !s.Extensions.Matches(filename) {
  139. return "", fmt.Errorf("Extension not admitted for this share")
  140. }
  141. fpath := filepath.Join(s.Dir, filename)
  142. incrementable := newFileIncrement(fpath)
  143. var dst *os.File
  144. var err error
  145. var realFileName string
  146. try := 0
  147. for try < 10 {
  148. realFileName = incrementable.Increment(try)
  149. dst, err = os.OpenFile(realFileName,
  150. os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
  151. if err == nil {
  152. break
  153. }
  154. try++
  155. }
  156. if try == 10 {
  157. return "", fmt.Errorf("Can't create file %s", filename)
  158. }
  159. defer dst.Close()
  160. _, err = io.Copy(dst, buf)
  161. return realFileName, nil
  162. }
  163. // GetViewURL retrieves the view URL for the specified path
  164. func (g *Global) GetViewURL(dir string) (url.URL, error) {
  165. rel, err := filepath.Rel(g.ViewPathBase, dir)
  166. if err != nil {
  167. return url.URL{}, errors.Wrapf(err, "Error getting view URL for '%s'", dir)
  168. }
  169. if strings.HasPrefix(rel, "../") {
  170. return url.URL{}, fmt.Errorf("Error getting view URL for '%s'", dir)
  171. }
  172. u, err := url.Parse(g.ViewURLBase)
  173. if err != nil {
  174. return url.URL{}, fmt.Errorf("Invalid view URL %s", g.ViewURLBase)
  175. }
  176. u.Path = path.Join(u.Path, rel)
  177. return *u, nil
  178. }
  179. // GetShareURL retrieves the view URL for the share identified by sharename
  180. func (c *Config) GetShareURL(sharename string) (url.URL, error) {
  181. if c.Global.ViewPathBase == "" || c.Global.ViewURLBase == "" {
  182. return url.URL{}, errors.New("View URL not configured")
  183. }
  184. s, err := c.GetShare(sharename)
  185. if err != nil {
  186. return url.URL{}, err
  187. }
  188. return c.Global.GetViewURL(s.Dir)
  189. }
  190. // GetAuthShares returns an array of all the shares the user is authorized to use, including its home.
  191. func (c *Config) GetAuthShares(username string) []Share {
  192. shares := make([]Share, 0)
  193. for _, s := range c.Shares {
  194. if s.IsAuthorized(username) {
  195. shares = append(shares, s)
  196. }
  197. }
  198. home, err := c.GetHome(username)
  199. if err == nil {
  200. shares = append(shares, *home)
  201. }
  202. return shares
  203. }
  204. // GetAuthShare returns a valid share for a certain user.
  205. //
  206. // It includes checking for permissions and dynamically-generated home.
  207. //
  208. // FIXME: can't tell 'unauthorized' from 'not found'
  209. func (c *Config) GetAuthShare(sharename string, username string) (Share, error) {
  210. share, err := c.GetShare(sharename)
  211. if err == nil {
  212. if share.IsAuthorized(username) {
  213. return share, nil
  214. }
  215. return Share{}, errors.New("Unauthorized")
  216. }
  217. home, homeerr := c.GetHome(username)
  218. if homeerr != nil || home.Name != sharename {
  219. return Share{}, errors.New("Cannot find home")
  220. }
  221. // assuming user is always authorized on its dynamically-generated home
  222. return *home, nil
  223. }
  224. func (c *Config) GetShare(name string) (Share, error) {
  225. for _, share := range c.Shares {
  226. if share.Name == name {
  227. return share, nil
  228. }
  229. }
  230. return Share{}, errors.New("share not found")
  231. }
  232. // GetHome returns the home share for a user, if available
  233. // It implements authorization check
  234. func (c *Config) GetHome(username string) (*Share, error) {
  235. // explicit share gets precedence
  236. sharename := fmt.Sprintf("home_%s", username)
  237. share, err := c.GetShare(sharename)
  238. if err == nil {
  239. if share.IsAuthorized(username) {
  240. return &share, nil
  241. }
  242. return nil, fmt.Errorf("Home share for %s is forbidden to its owner", username)
  243. }
  244. if c.Global.Excluded.Has(username) {
  245. return nil, fmt.Errorf("User %s has no home", username)
  246. }
  247. s := new(Share)
  248. s.Name = sharename
  249. s.Authorized = Globs{username}
  250. s.Dir = filepath.Join(c.UserHome.BasePath, username)
  251. stat, err := os.Stat(s.Dir)
  252. if err != nil {
  253. if os.IsNotExist(err) { //doesn't exist
  254. if c.UserHome.Create {
  255. os.Mkdir(s.Dir, os.ModeDir)
  256. } else {
  257. return nil, fmt.Errorf("User %s home is non-existent (and we won't create it)", username)
  258. }
  259. } else {
  260. return nil, errors.Wrapf(err, "access to %s'shome failed", username)
  261. }
  262. } else {
  263. if !stat.IsDir() {
  264. return nil, fmt.Errorf("%s's home exists but is not a directory", username)
  265. }
  266. }
  267. s.Description = fmt.Sprintf("%s's home", username)
  268. return s, nil
  269. }
  270. // URLFetcher is a WIP section of the config to define rules for fetching URLs instead of uploading
  271. type URLFetcher struct {
  272. Enabled bool
  273. AllowedURLs Globs
  274. SizeLimit datasize.ByteSize
  275. }