Created
January 4, 2022 19:03
-
-
Save 0sc/64d8710cf17aa3894a22a039abc02777 to your computer and use it in GitHub Desktop.
Code snippets for using Imgproxy with Minio.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
IMGPROXY_KEY=$(xxd -g 2 -l 64 -p /dev/random | tr -d '\n') | |
IMGPROXY_SALT=$(xxd -g 2 -l 64 -p /dev/random | tr -d '\n') | |
ACCESS_SECRET=$(xxd -g 2 -l 5 -p /dev/random | tr -d '\n') | |
ACCESS_KEY=$(xxd -g 2 -l 5 -p /dev/random | tr -d '\n') | |
REGION=us-east-1 | |
DEFAULT_BUCKET=sample |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
services: | |
app: | |
image: golang:bullseye | |
command: /bin/sh -c "go mod init && go mod tidy && go run main.go" | |
ports: | |
- "50100:8080" | |
environment: | |
- ACCESS_KEY=${ACCESS_KEY:-aws-access-key} | |
- ACCESS_SECRET=${ACCESS_SECRET:-aws-access-secret} | |
- IMG_UPLOAD_BUCKET=${DEFAULT_BUCKET:-sample} | |
- IMGPROXY_PUBLIC_URL=http://localhost:50200 | |
- IMGPROXY_SIGNING_KEY=${IMGPROXY_KEY} | |
- IMGPROXY_SIGNING_SALT=${IMGPROXY_SALT} | |
- RESOURCE_ENDPOINT=http://minio:9000 | |
- RESOURCE_REGION=${REGION:-us-east-1} | |
volumes: | |
- ./main.go:/go/src/app/main.go | |
working_dir: /go/src/app | |
depends_on: | |
- minio | |
- imgproxy | |
imgproxy: | |
image: darthsim/imgproxy:latest | |
ports: | |
- "50200:8080" | |
environment: | |
- AWS_ACCESS_KEY_ID=${ACCESS_KEY:-aws-access-key} | |
- AWS_SECRET_ACCESS_KEY=${ACCESS_SECRET:-aws-access-secret} | |
- IMGPROXY_ALLOWED_SOURCES=s3:// | |
- IMGPROXY_DEVELOPMENT_ERRORS_MODE=true | |
- IMGPROXY_KEY=${IMGPROXY_KEY} | |
- IMGPROXY_S3_ENDPOINT=http://minio:9000 | |
- IMGPROXY_S3_REGION=${REGION:-us-east-1} | |
- IMGPROXY_SALT=${IMGPROXY_SALT} | |
- IMGPROXY_USE_S3=true | |
links: | |
- minio | |
minio: | |
image: minio/minio | |
command: server /data --console-address ":9001" | |
hostname: minio | |
ports: | |
- "50300:9000" | |
- "50400:9001" | |
environment: | |
- MINIO_DEFAULT_BUCKET=${DEFAULT_BUCKET:-sample} | |
- MINIO_ROOT_PASSWORD=${ACCESS_SECRET:-aws-access-secret} | |
- MINIO_ROOT_USER=${ACCESS_KEY:-aws-access-key} | |
- MINIO_SITE_REGION=${REGION:-us-east-1} | |
healthcheck: | |
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] | |
interval: 30s | |
timeout: 20s | |
retries: 3 | |
volumes: | |
- data:/data | |
volumes: | |
data: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"crypto/hmac" | |
"crypto/sha256" | |
"encoding/base64" | |
"encoding/hex" | |
"fmt" | |
"io" | |
"net/http" | |
"os" | |
"path" | |
"github.com/aws/aws-sdk-go/aws" | |
"github.com/aws/aws-sdk-go/aws/credentials" | |
"github.com/aws/aws-sdk-go/aws/session" | |
s3 "github.com/fclairamb/afero-s3" | |
"github.com/google/uuid" | |
"github.com/kelseyhightower/envconfig" | |
"github.com/pkg/errors" | |
"github.com/spf13/afero" | |
) | |
type config struct { | |
BaseURL string `envconfig:"IMGPROXY_PUBLIC_URL"` | |
SigningKeyHex string `envconfig:"IMGPROXY_SIGNING_KEY"` | |
SigningSaltHex string `envconfig:"IMGPROXY_SIGNING_SALT"` | |
} | |
type imageHandler struct { | |
baseURL string | |
signingKey []byte | |
signingSalt []byte | |
storage afero.Fs | |
} | |
func newimageHandler(cfg config, storage afero.Fs) (*imageHandler, error) { | |
// Prepare the url signing key and salt | |
key, err := hex.DecodeString(cfg.SigningKeyHex) | |
if err != nil { | |
return nil, errors.Wrap(err, "Key expected to be hex-encoded string") | |
} | |
salt, err := hex.DecodeString(cfg.SigningSaltHex) | |
if err != nil { | |
return nil, errors.Wrap(err, "Salt expected to be hex-encoded string") | |
} | |
img := &imageHandler{ | |
baseURL: cfg.BaseURL, | |
signingKey: key, | |
signingSalt: salt, | |
storage: storage, | |
} | |
return img, nil | |
} | |
func (ih *imageHandler) store(key string, image io.Reader) error { | |
file, err := ih.storage.OpenFile(key, os.O_WRONLY, 0777) | |
if err != nil { | |
return errors.Wrap(err, "failed to open image") | |
} | |
defer file.Close() | |
_, err = io.Copy(file, image) | |
if err != nil { | |
return errors.Wrap(err, "failed to store image") | |
} | |
return file.Close() | |
} | |
func (ih *imageHandler) sign(path string) string { | |
mac := hmac.New(sha256.New, ih.signingKey) | |
mac.Write(ih.signingSalt) // FIXME: possible error? | |
mac.Write([]byte(path)) // FIXME: possible error? | |
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) | |
} | |
func (ih *imageHandler) generateURL(imgURI string) string { | |
// TODO: The properties are set here for simplicity. | |
// but will be an argument to the function in a real application | |
resize := "fill" | |
width := 300 | |
height := 300 | |
gravity := "no" | |
enlarge := 1 | |
extension := "png" | |
imgURI = base64.RawURLEncoding.EncodeToString([]byte(imgURI)) | |
path := fmt.Sprintf("/rs:%s:%d:%d:%d/g:%s/%s.%s", resize, width, height, enlarge, gravity, imgURI, extension) | |
return fmt.Sprintf("%s/%s%s", ih.baseURL, ih.sign(path), path) | |
} | |
func (ih *imageHandler) handler(bucket string) http.HandlerFunc { | |
return func(w http.ResponseWriter, r *http.Request) { | |
// Maximum upload of 10 MB files | |
err := r.ParseMultipartForm(10 << 20) | |
if err != nil { | |
http.Error(w, err.Error(), http.StatusBadRequest) | |
return | |
} | |
// Get the file from the request | |
// Note that the argument to FormFile should match the name attribute | |
// of the upload input tag in the form. | |
file, handler, err := r.FormFile("image") | |
if err != nil { | |
http.Error(w, err.Error(), http.StatusBadRequest) | |
return | |
} | |
defer file.Close() | |
// Generate a unique key for the image | |
// We'll be using a uuid for this example | |
// but any unique string goes | |
// | |
// FYI this is not a 100% reliable way to determine file type | |
// but it's good enough for this example | |
imgKey := uuid.NewString() + path.Ext(handler.Filename) | |
// Store the image | |
err = ih.store(imgKey, file) | |
if err != nil { | |
http.Error(w, err.Error(), http.StatusInternalServerError) | |
return | |
} | |
// TODO: save the image key in the database as needed. | |
// Generate the URL for the image | |
imgURL := ih.generateURL(fmt.Sprintf("s3://%s/%s", bucket, imgKey)) | |
w.WriteHeader(http.StatusOK) | |
fmt.Fprintf(w, "Image URL: %s", imgURL) | |
} | |
} | |
func main() { | |
var cfg struct { | |
// Extra configuration for setting up connection to the file storage | |
AccessKey string `envconfig:"ACCESS_KEY"` | |
AccessSecret string `envconfig:"ACCESS_SECRET"` | |
Bucket string `envconfig:"IMG_UPLOAD_BUCKET"` | |
Endpoint string `envconfig:"RESOURCE_ENDPOINT"` | |
Region string `envconfig:"RESOURCE_REGION"` | |
ImgConfig config | |
} | |
err := envconfig.Process("", &cfg) | |
if err != nil { | |
panic(err) | |
} | |
// Create a file system for storing the images | |
sess, err := session.NewSession(&aws.Config{ | |
Region: aws.String(cfg.Region), | |
Endpoint: aws.String(cfg.Endpoint), | |
Credentials: credentials.NewStaticCredentials(cfg.AccessKey, cfg.AccessSecret, ""), | |
S3ForcePathStyle: aws.Bool(true), | |
}) | |
if err != nil { | |
panic(err) | |
} | |
img, err := newimageHandler(cfg.ImgConfig, s3.NewFs(cfg.Bucket, sess)) | |
if err != nil { | |
panic(err) | |
} | |
mux := http.NewServeMux() | |
mux.Handle("/", img.handler(cfg.Bucket)) | |
fmt.Println("Starting server on port", ":8080") | |
http.ListenAndServe(":8080", mux) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment