readdir.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. package s3util
  2. import (
  3. "bytes"
  4. "encoding/xml"
  5. "errors"
  6. "io"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "strconv"
  11. "strings"
  12. "time"
  13. )
  14. // File represents an S3 object or directory.
  15. type File struct {
  16. url string
  17. prefix string
  18. config *Config
  19. result *listObjectsResult
  20. }
  21. type fileInfo struct {
  22. name string
  23. size int64
  24. dir bool
  25. modTime time.Time
  26. sys *Stat
  27. }
  28. // Stat contains information about an S3 object or directory.
  29. // It is the "underlying data source" returned by method Sys
  30. // for each FileInfo produced by this package.
  31. // fi.Sys().(*s3util.Stat)
  32. // For the meaning of these fields, see
  33. // http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGET.html.
  34. type Stat struct {
  35. Key string
  36. LastModified string
  37. ETag string // ETag value, without double quotes.
  38. Size string
  39. StorageClass string
  40. OwnerID string `xml:"Owner>ID"`
  41. OwnerName string `xml:"Owner>DisplayName"`
  42. }
  43. type listObjectsResult struct {
  44. IsTruncated bool
  45. Contents []Stat
  46. Directories []string `xml:"CommonPrefixes>Prefix"` // Suffix "/" trimmed
  47. }
  48. func (f *fileInfo) Name() string { return f.name }
  49. func (f *fileInfo) Size() int64 { return f.size }
  50. func (f *fileInfo) Mode() os.FileMode {
  51. if f.dir {
  52. return 0755 | os.ModeDir
  53. }
  54. return 0644
  55. }
  56. func (f *fileInfo) ModTime() time.Time {
  57. if f.modTime.IsZero() && f.sys != nil {
  58. // we return the zero value if a parse error ever happens.
  59. f.modTime, _ = time.Parse(time.RFC3339Nano, f.sys.LastModified)
  60. }
  61. return f.modTime
  62. }
  63. func (f *fileInfo) IsDir() bool { return f.dir }
  64. func (f *fileInfo) Sys() interface{} { return f.sys }
  65. // NewFile returns a new File with the given URL and config.
  66. //
  67. // Set rawurl to a directory on S3, such as
  68. // https://mybucket.s3.amazonaws.com/myfolder.
  69. // The URL cannot have query parameters or a fragment.
  70. // If c is nil, DefaultConfig will be used.
  71. func NewFile(rawurl string, c *Config) (*File, error) {
  72. u, err := url.Parse(rawurl)
  73. if err != nil {
  74. return nil, err
  75. }
  76. if u.RawQuery != "" {
  77. return nil, errors.New("url cannot have raw query parameters.")
  78. }
  79. if u.Fragment != "" {
  80. return nil, errors.New("url cannot have a fragment.")
  81. }
  82. prefix := strings.TrimLeft(u.Path, "/")
  83. if prefix != "" && !strings.HasSuffix(prefix, "/") {
  84. prefix += "/"
  85. }
  86. u.Path = ""
  87. return &File{u.String(), prefix, c, nil}, nil
  88. }
  89. // Readdir requests a list of entries in the S3 directory
  90. // represented by f and returns a slice of up to n FileInfo
  91. // values, in alphabetical order. Subsequent calls
  92. // on the same File will yield further FileInfos.
  93. // Only direct children are returned, not deeper descendants.
  94. func (f *File) Readdir(n int) ([]os.FileInfo, error) {
  95. if f.result != nil && !f.result.IsTruncated {
  96. return make([]os.FileInfo, 0), io.EOF
  97. }
  98. reader, err := f.sendRequest(n)
  99. if err != nil {
  100. return nil, err
  101. }
  102. defer reader.Close()
  103. return f.parseResponse(reader)
  104. }
  105. func (f *File) sendRequest(count int) (io.ReadCloser, error) {
  106. c := f.config
  107. if c == nil {
  108. c = DefaultConfig
  109. }
  110. var buf bytes.Buffer
  111. buf.WriteString(f.url)
  112. buf.WriteString("?delimiter=%2F")
  113. if f.prefix != "" {
  114. buf.WriteString("&prefix=")
  115. buf.WriteString(url.QueryEscape(f.prefix))
  116. }
  117. if count > 0 {
  118. buf.WriteString("&max-keys=")
  119. buf.WriteString(strconv.Itoa(count))
  120. }
  121. if f.result != nil && f.result.IsTruncated {
  122. var lastDir, lastKey, marker string
  123. if len(f.result.Directories) > 0 {
  124. lastDir = f.result.Directories[len(f.result.Directories)-1]
  125. }
  126. if len(f.result.Contents) > 0 {
  127. lastKey = f.result.Contents[len(f.result.Contents)-1].Key
  128. }
  129. if lastKey > lastDir {
  130. marker = lastKey
  131. } else {
  132. marker = lastDir
  133. }
  134. if marker != "" {
  135. buf.WriteString("&marker=")
  136. buf.WriteString(url.QueryEscape(marker))
  137. }
  138. }
  139. u := buf.String()
  140. r, _ := http.NewRequest("GET", u, nil)
  141. r.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
  142. c.Sign(r, *c.Keys)
  143. resp, err := http.DefaultClient.Do(r)
  144. if err != nil {
  145. return nil, err
  146. }
  147. if resp.StatusCode != 200 {
  148. return nil, newRespError(resp)
  149. }
  150. return resp.Body, nil
  151. }
  152. func (f *File) parseResponse(reader io.Reader) ([]os.FileInfo, error) {
  153. decoder := xml.NewDecoder(reader)
  154. result := listObjectsResult{}
  155. var err error
  156. err = decoder.Decode(&result)
  157. if err != nil {
  158. return nil, err
  159. }
  160. infos := make([]os.FileInfo, len(result.Contents)+len(result.Directories))
  161. var size int64
  162. var name string
  163. var is_dir bool
  164. for i, content := range result.Contents {
  165. c := content
  166. c.ETag = strings.Trim(c.ETag, `"`)
  167. size, _ = strconv.ParseInt(c.Size, 10, 0)
  168. if size == 0 && strings.HasSuffix(c.Key, "/") {
  169. name = strings.TrimRight(c.Key, "/")
  170. is_dir = true
  171. } else {
  172. name = c.Key
  173. is_dir = false
  174. }
  175. infos[i] = &fileInfo{
  176. name: name,
  177. size: size,
  178. dir: is_dir,
  179. sys: &c,
  180. }
  181. }
  182. for i, dir := range result.Directories {
  183. infos[len(result.Contents)+i] = &fileInfo{
  184. name: strings.TrimRight(dir, "/"),
  185. size: 0,
  186. dir: true,
  187. }
  188. }
  189. f.result = &result
  190. return infos, nil
  191. }