stash/pkg/utils/image.go
SmallCoccinelle 214a15bc40
Fixups + enable the commentFormatting linter (#1866)
* Add a space after // comments

For consistency, the commentFormatting lint checker suggests a space
after each // comment block. This commit handles all the spots in
the code where that is needed.

* Rewrite documentation on functions

Use the Go idiom of commenting:

* First sentence declares the purpose.
* First word is the name being declared

The reason this style is preferred is such that grep is able to find
names the user might be interested in. Consider e.g.,

    go doc -all pkg/ffmpeg | grep -i transcode

in which case a match will tell you the name of the function you are
interested in.

* Remove old code comment-blocks

There are some commented out old code blocks in the code base. These are
either 3 years old, or 2 years old. By now, I don't think their use is
going to come back any time soon, and Git will track old pieces of
deleted code anyway.

Opt for deletion.

* Reorder imports

Split stdlib imports from non-stdlib imports in files we are touching.

* Use a range over an iteration variable

Probably more go-idiomatic, and the code needed comment-fixing anyway.

* Use time.After rather than rolling our own

The idiom here is common enough that the stdlib contains a function for
it. Use the stdlib function over our own variant.

* Enable the commentFormatting linter
2021-10-20 16:10:46 +11:00

131 lines
3.3 KiB
Go

package utils
import (
"context"
"crypto/md5"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
)
// Timeout to get the image. Includes transfer time. May want to make this
// configurable at some point.
const imageGetTimeout = time.Second * 60
const base64RE = `^data:.+\/(.+);base64,(.*)$`
// ProcessImageInput transforms an image string either from a base64 encoded
// string, or from a URL, and returns the image as a byte slice
func ProcessImageInput(ctx context.Context, imageInput string) ([]byte, error) {
regex := regexp.MustCompile(base64RE)
if regex.MatchString(imageInput) {
_, d, err := ProcessBase64Image(imageInput)
return d, err
}
// assume input is a URL. Read it.
return ReadImageFromURL(ctx, imageInput)
}
// ReadImageFromURL returns image data from a URL
func ReadImageFromURL(ctx context.Context, url string) ([]byte, error) {
client := &http.Client{
Transport: &http.Transport{ // ignore insecure certificates
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
Timeout: imageGetTimeout,
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
// assume is a URL for now
// set the host of the URL as the referer
if req.URL.Scheme != "" {
req.Header.Set("Referer", req.URL.Scheme+"://"+req.Host+"/")
}
req.Header.Set("User-Agent", GetUserAgent())
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("http error %d", resp.StatusCode)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
// ProcessBase64Image transforms a base64 encoded string from a form post and returns the MD5 hash of the data and the
// image itself as a byte slice.
func ProcessBase64Image(imageString string) (string, []byte, error) {
if imageString == "" {
return "", nil, fmt.Errorf("empty image string")
}
regex := regexp.MustCompile(base64RE)
matches := regex.FindStringSubmatch(imageString)
var encodedString string
if len(matches) > 2 {
encodedString = regex.FindStringSubmatch(imageString)[2]
} else {
encodedString = imageString
}
imageData, err := GetDataFromBase64String(encodedString)
if err != nil {
return "", nil, err
}
return MD5FromBytes(imageData), imageData, nil
}
// GetDataFromBase64String returns the given base64 encoded string as a byte slice
func GetDataFromBase64String(encodedString string) ([]byte, error) {
return base64.StdEncoding.DecodeString(encodedString)
}
// GetBase64StringFromData returns the given byte slice as a base64 encoded string
func GetBase64StringFromData(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
func ServeImage(image []byte, w http.ResponseWriter, r *http.Request) error {
etag := fmt.Sprintf("%x", md5.Sum(image))
if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, etag) {
w.WriteHeader(http.StatusNotModified)
return nil
}
}
contentType := http.DetectContentType(image)
if contentType == "text/xml; charset=utf-8" || contentType == "text/plain; charset=utf-8" {
contentType = "image/svg+xml"
}
w.Header().Set("Content-Type", contentType)
w.Header().Add("Etag", etag)
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
_, err := w.Write(image)
return err
}