fs.go 3.2 KB

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