fs.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  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/config"
  15. "github.com/imgproxy/imgproxy/v3/httprange"
  16. "github.com/imgproxy/imgproxy/v3/transport/common"
  17. "github.com/imgproxy/imgproxy/v3/transport/notmodified"
  18. )
  19. type transport struct {
  20. fs http.Dir
  21. }
  22. func New() transport {
  23. return transport{fs: http.Dir(config.LocalFileSystemRoot)}
  24. }
  25. func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
  26. header := make(http.Header)
  27. _, path, _ := common.GetBucketAndKey(req.URL)
  28. path = "/" + path
  29. f, err := t.fs.Open(path)
  30. if err != nil {
  31. if os.IsNotExist(err) {
  32. return respNotFound(req, fmt.Sprintf("%s doesn't exist", path)), nil
  33. }
  34. return nil, err
  35. }
  36. fi, err := f.Stat()
  37. if err != nil {
  38. return nil, err
  39. }
  40. if fi.IsDir() {
  41. return respNotFound(req, fmt.Sprintf("%s is directory", path)), nil
  42. }
  43. statusCode := 200
  44. size := fi.Size()
  45. body := io.ReadCloser(f)
  46. if mimetype := detectContentType(f, fi); len(mimetype) > 0 {
  47. header.Set("Content-Type", mimetype)
  48. }
  49. f.Seek(0, io.SeekStart)
  50. start, end, err := httprange.Parse(req.Header.Get("Range"))
  51. switch {
  52. case err != nil:
  53. f.Close()
  54. return httprange.InvalidHTTPRangeResponse(req), nil
  55. case end != 0:
  56. if end < 0 {
  57. end = size - 1
  58. }
  59. f.Seek(start, io.SeekStart)
  60. statusCode = http.StatusPartialContent
  61. size = end - start + 1
  62. body = &fileLimiter{f: f, left: int(size)}
  63. header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fi.Size()))
  64. default:
  65. if config.ETagEnabled {
  66. etag := BuildEtag(path, fi)
  67. header.Set("ETag", etag)
  68. }
  69. if config.LastModifiedEnabled {
  70. lastModified := fi.ModTime().Format(http.TimeFormat)
  71. header.Set("Last-Modified", lastModified)
  72. }
  73. }
  74. if resp := notmodified.Response(req, header); resp != nil {
  75. f.Close()
  76. return resp, nil
  77. }
  78. header.Set("Accept-Ranges", "bytes")
  79. header.Set("Content-Length", 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{"Content-Type": {"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. }