fs.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. package fs
  2. import (
  3. "crypto/md5"
  4. "encoding/base64"
  5. "fmt"
  6. "io"
  7. "io/fs"
  8. "mime"
  9. "net/http"
  10. "os"
  11. "path/filepath"
  12. "strconv"
  13. "strings"
  14. "github.com/imgproxy/imgproxy/v3/fetcher/transport/common"
  15. "github.com/imgproxy/imgproxy/v3/fetcher/transport/notmodified"
  16. "github.com/imgproxy/imgproxy/v3/httpheaders"
  17. "github.com/imgproxy/imgproxy/v3/httprange"
  18. )
  19. type transport struct {
  20. fs http.Dir
  21. querySeparator string
  22. }
  23. func New(config *Config, querySeparator string) (transport, error) {
  24. if err := config.Validate(); err != nil {
  25. return transport{}, err
  26. }
  27. return transport{fs: http.Dir(config.Root), querySeparator: querySeparator}, nil
  28. }
  29. func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
  30. header := make(http.Header)
  31. _, path, _ := common.GetBucketAndKey(req.URL, t.querySeparator)
  32. path = "/" + path
  33. f, err := t.fs.Open(path)
  34. if err != nil {
  35. if os.IsNotExist(err) {
  36. return respNotFound(req, fmt.Sprintf("%s doesn't exist", path)), nil
  37. }
  38. return nil, err
  39. }
  40. fi, err := f.Stat()
  41. if err != nil {
  42. return nil, err
  43. }
  44. if fi.IsDir() {
  45. return respNotFound(req, fmt.Sprintf("%s is directory", path)), nil
  46. }
  47. statusCode := 200
  48. size := fi.Size()
  49. body := io.ReadCloser(f)
  50. if mimetype := detectContentType(f, fi); len(mimetype) > 0 {
  51. header.Set(httpheaders.ContentType, mimetype)
  52. }
  53. f.Seek(0, io.SeekStart)
  54. start, end, err := httprange.Parse(req.Header.Get(httpheaders.Range))
  55. switch {
  56. case err != nil:
  57. f.Close()
  58. return httprange.InvalidHTTPRangeResponse(req), nil
  59. case end != 0:
  60. if end < 0 {
  61. end = size - 1
  62. }
  63. f.Seek(start, io.SeekStart)
  64. statusCode = http.StatusPartialContent
  65. size = end - start + 1
  66. body = &fileLimiter{f: f, left: int(size)}
  67. header.Set(httpheaders.ContentRange, fmt.Sprintf("bytes %d-%d/%d", start, end, fi.Size()))
  68. default:
  69. etag := BuildEtag(path, fi)
  70. header.Set(httpheaders.Etag, etag)
  71. lastModified := fi.ModTime().Format(http.TimeFormat)
  72. header.Set(httpheaders.LastModified, lastModified)
  73. }
  74. if resp := notmodified.Response(req, header); resp != nil {
  75. f.Close()
  76. return resp, nil
  77. }
  78. header.Set(httpheaders.AcceptRanges, "bytes")
  79. header.Set(httpheaders.ContentLength, strconv.Itoa(int(size)))
  80. return &http.Response{
  81. StatusCode: statusCode,
  82. Proto: "HTTP/1.0",
  83. ProtoMajor: 1,
  84. ProtoMinor: 0,
  85. Header: header,
  86. ContentLength: size,
  87. Body: body,
  88. Close: true,
  89. Request: req,
  90. }, nil
  91. }
  92. func BuildEtag(path string, fi fs.FileInfo) string {
  93. tag := fmt.Sprintf("%s__%d__%d", path, fi.Size(), fi.ModTime().UnixNano())
  94. hash := md5.Sum([]byte(tag))
  95. return `"` + string(base64.RawURLEncoding.EncodeToString(hash[:])) + `"`
  96. }
  97. func respNotFound(req *http.Request, msg string) *http.Response {
  98. return &http.Response{
  99. StatusCode: http.StatusNotFound,
  100. Proto: "HTTP/1.0",
  101. ProtoMajor: 1,
  102. ProtoMinor: 0,
  103. Header: http.Header{httpheaders.ContentType: {"text/plain"}},
  104. ContentLength: int64(len(msg)),
  105. Body: io.NopCloser(strings.NewReader(msg)),
  106. Close: false,
  107. Request: req,
  108. }
  109. }
  110. func detectContentType(f http.File, fi fs.FileInfo) string {
  111. var (
  112. tmp [512]byte
  113. mimetype string
  114. )
  115. if n, err := io.ReadFull(f, tmp[:]); err == nil {
  116. mimetype = http.DetectContentType(tmp[:n])
  117. }
  118. if len(mimetype) == 0 || strings.HasPrefix(mimetype, "text/plain") || strings.HasPrefix(mimetype, "application/octet-stream") {
  119. if m := mime.TypeByExtension(filepath.Ext(fi.Name())); len(m) > 0 {
  120. mimetype = m
  121. }
  122. }
  123. return mimetype
  124. }