Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save dillonstreator/2075fcc633225ad1172135ee1ca33cb4 to your computer and use it in GitHub Desktop.
Save dillonstreator/2075fcc633225ad1172135ee1ca33cb4 to your computer and use it in GitHub Desktop.
Golang http server arbitrary image count upload with `browser-image-compression` and concurrent streaming
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"

	"golang.org/x/sync/errgroup"
)

const (
	maxUploadSize = 1024 * 1024 * 5
	maxImageCount = 5
)

func main() {

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		_, err := w.Write(indexHTML)
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
	})
	http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
		r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)

		err := r.ParseMultipartForm(1024 * 32)
		if err != nil {
			fmt.Println(err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		imageCount, err := strconv.Atoi(r.FormValue("imageCount"))
		if err != nil {
			fmt.Println(err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		if imageCount > maxImageCount {
			fmt.Printf("too many images %d\n", imageCount)
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		eg := errgroup.Group{}
		for i := 0; i < imageCount; i++ {
			key := fmt.Sprintf("image%d", i)

			eg.Go(func() error {
				imgF, fh, err := r.FormFile(key)
				if err != nil {
					return err
				}
				defer imgF.Close()

				// adjust this to fit your accepted content types
				ct := fh.Header.Get("Content-Type")
				parts := strings.Split(ct, "/")
				if len(parts) < 2 || parts[0] != "image" {
					return fmt.Errorf("invalid content type %s for %s", ct, key)
				}
				ext := parts[len(parts)-1]

				f, err := os.Create(fmt.Sprintf("%s.%s", key, ext))
				if err != nil {
					return err
				}
				defer f.Close()

				_, err = io.Copy(f, imgF)
				return err
			})
		}

		err = eg.Wait()
		if err != nil {
			fmt.Println(err)
			// TODO: better distinguish bad request and internal server error
			w.WriteHeader(http.StatusBadRequest)
			return
		}
	})

	http.ListenAndServe(":4321", nil)
}

var indexHTML = []byte(`
<body>
	<h1>image upload</h1>
	<input type="file" multiple accept="image/*" onchange="handleImageUpload(event);" />
	<script>
		async function handleImageUpload(event) {
			const options = {
				maxSizeMB: .5,
				useWebWorker: true,
			};
			console.time("time")
			try {
				const files = Array.from(event.target.files);

				const formData = new FormData();
				formData.append("imageCount", files.length);

				console.time("compress")
				const compressedFiles = await Promise.all(files.map(f => imageCompression(f, options)))
				console.timeEnd("compress")

				compressedFiles.forEach((cf, idx) => formData.append(` + "`image${idx}`" + `, cf))

				await fetch("/upload", {
					method: "POST",
					body: formData,
				})
			} catch (error) {
				console.log(error);
			}
			console.timeEnd("time")
		}
	</script>
	<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.1/dist/browser-image-compression.js"></script>
</body>
`)

https://www.npmjs.com/package/browser-image-compression

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment