S3 Presigned PUT with file Blob example

Hey everyone,

We are trying to upload images via a put request using s3 presigned put urls.

We are not sure how to properly bind to the new Blob method to get the file into the body of this request.

So far this is what we are using to acquire the actual file:

module FileReader = {
  type fileReader
  type file = {"name": string, "lastModified": int, "size": int, "type__": string}

  @new external createFileReader: unit => fileReader = "FileReader"

  @bs.send
  external readAsDataURL: (fileReader, file) => unit = "readAsDataURL"

  let onload: (fileReader, string => unit) => unit = %raw(`
    function (reader, cb) {
      reader.onload = function (e) {
        cb(e.target.result);
      }
    }
  `)
}
let file = ReactEvent.Form.target(event)["files"][0]

if file["size"] / 1024 / 1024 < 10 {
  let reader = FileReader.createFileReader()
  FileReader.onload(reader, url => {
    setImageUrl(_ => url->Some) // Local preview
  })
  FileReader.readAsDataURL(reader, file)

  //TODO: fetch put to s3 presigned url with file blolb
} else {
  Js.log("Please choose an image less than 10 mb")
}

Javascript equivalent:

const fileBlob = new Blob([file], { type: file.type })

await fetch(data.shopAssetPresignUrl.url, {
  method: 'PUT',
  headers: { 'Content-Type': fileBlob.type },
  body: fileBlob,
})

Thanks!

(Playground) How about:

module File = {
  type t
  type type_

  @get
  external type_: t => type_ = "type"
}

module Blob = {
  type t
  type type_

  @new
  external make: (array<File.t>, {"type": File.type_}) => t = "Blob"

  @get
  external type_: t => type_ = "type"

  let make = file => make([file], {"type": file->File.type_})
}

type response

@scope("window") @val
external fetch: (
  string,
  {
    "method": [
      | #PUT
      | #GET
      | #POST
      | #DELETE
    ],
    "headers": 't,
    "body": Blob.t,
  },
) => response = "fetch"

let upload = (url, file) => {
  let blob = file->Blob.make

  fetch(
    url,
    {
      "method": #PUT,
      "headers": {
        "Content-Type": blob->Blob.type_,
      },
      "body": blob,
    },
  )
}

Which compiles to:

// Generated by ReScript, PLEASE EDIT WITH CARE

var $$File = {};

function make(file) {
  return new Blob([file], {
    type: file.type
  });
}

var $$Blob$1 = {
  make: make
};

function upload(url, file) {
  var blob = make(file);
  return window.fetch(url, {
    method: 'PUT',
    headers: {
      'Content-Type': blob.type
    },
    body: blob
  });
}

export { $$File, $$Blob$1 as $$Blob, upload };
/* No side effect */

1 Like

A File is already Blob, so you don’t need new Blob. Just put the file that you have, in the fetch request body. Assuming you’re using bs-fetch, here’s an example: https://github.com/reasonml-community/bs-fetch/blob/934389964e1005d4e37911c8324a6aeb7ce0a1b0/examples/reason_examples.re#L50

You can adapt that to put the file in the formdata object being uploaded using https://github.com/reasonml-community/bs-fetch/blob/934389964e1005d4e37911c8324a6aeb7ce0a1b0/src/Fetch.mli#L267

1 Like

Thanks so much @yawaramin and @oliverfencott - I ended figuring this out with a pretty simplistic implementation.

The main method I was missing was was from bs-fetch - All I really needed was the:

Fetch.BodyInit.makeWithBlob

Typically we always json stringify the body. With this helper we were able to proceed :slight_smile:

Thanks again!

2 Likes