How to get a file or blob from an object URL?

Viewed 284658

I am allowing the user to load images into a page via drag&drop and other methods. When an image is dropped, I'm using URL.createObjectURL to convert to an object URL to display the image. I am not revoking the url, as I do reuse it.

So, when it comes time to create a FormData object so I can allow them to upload a form with one of those images in it, is there some way I can then reverse that Object URL back into a Blob or File so I can then append it to a FormData object?

9 Answers

Modern solution:

let blob = await fetch(url).then(r => r.blob());

The url can be an object url or a normal url.

Maybe someone finds this useful when working with React/Node/Axios. I used this for my Cloudinary image upload feature with react-dropzone on the UI.

    axios({
        method: 'get',
        url: file[0].preview, // blob url eg. blob:http://127.0.0.1:8000/e89c5d87-a634-4540-974c-30dc476825cc
        responseType: 'blob'
    }).then(function(response){
         var reader = new FileReader();
         reader.readAsDataURL(response.data); 
         reader.onloadend = function() {
             var base64data = reader.result;
             self.props.onMainImageDrop(base64data)
         }

    })

Using fetch for example like below:

 fetch(<"yoururl">, {
    method: 'GET',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + <your access token if need>
    },
       })
.then((response) => response.blob())
.then((blob) => {
// 2. Create blob link to download
 const url = window.URL.createObjectURL(new Blob([blob]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `sample.xlsx`);
 // 3. Append to html page
 document.body.appendChild(link);
 // 4. Force download
 link.click();
 // 5. Clean up and remove the link
 link.parentNode.removeChild(link);
})

You can paste in on Chrome console to test. the file with download with 'sample.xlsx' Hope it can help!

The problem with fetching the blob URL again is that this will create a full copy of the Blob's data, and so instead of having it only once in memory, you'll have it twice. With big Blobs this can blow your memory usage quite quickly.

It's rather unfortunate that the File API doesn't give us access to the currently linked Blobs, certainly they thought web-authors should store that Blob themselves at creation time anyway, which is true:

The best here is to store the object you used when creating the blob:// URL.

If you are afraid this would prevent the Blob from being Garbage Collected, you're right, but so does the blob:// URL in the first place, until you revoke it. So holding yourself a pointer to that Blob won't change a thing.

But for those who aren't responsible for the creation of the blob:// URI (e.g because a library made it), we can still fill that API hole ourselves by overriding the default URL.createObjectURL and URL.revokeObjectURL methods so that they do store references to the object passed.

Be sure to call this function before the code that does generate the blob:// URI is called.

// Adds an URL.getFromObjectURL( <blob:// URI> ) method
// returns the original object (<Blob> or <MediaSource>) the URI points to or null
(() => {
  // overrides URL methods to be able to retrieve the original blobs later on
  const old_create = URL.createObjectURL;
  const old_revoke = URL.revokeObjectURL;
  Object.defineProperty(URL, 'createObjectURL', {
    get: () => storeAndCreate
  });
  Object.defineProperty(URL, 'revokeObjectURL', {
    get: () => forgetAndRevoke
  });
  Object.defineProperty(URL, 'getFromObjectURL', {
    get: () => getBlob
  });
  const dict = {};

  function storeAndCreate(blob) {
    const url = old_create(blob); // let it throw if it has to
    dict[url] = blob;
    return url
  }

  function forgetAndRevoke(url) {
    old_revoke(url);
    try {
      if(new URL(url).protocol === 'blob:') {
        delete dict[url];
      }
    } catch(e){}
  }

  function getBlob(url) {
    return dict[url] || null;
  }
})();

//  Usage:
const blob = new Blob( ["foo"] );
const url = URL.createObjectURL( blob );
console.log( url );
const retrieved = URL.getFromObjectURL( url );
console.log( "retrieved Blob is Same Object?", retrieved === blob );
fetch( url ).then( (resp) => resp.blob() )
  .then( (fetched) => console.log( "fetched Blob is Same Object?", fetched === blob ) );

And an other advantage is that it can even retrieve MediaSource objects, while the fetching solutions would just err in that case.

Following @Kaiido answer, another way to overload URL without messing with URL is to extend the URL class like this:

export class URLwithStore extends URL {
  static createObjectURL(blob) {
    const url = super.createObjectURL(blob);
    URLwithStore.store = { ...(URLwithStore.store ?? {}), [url]: blob };
    return url;
  }

  static getFromObjectURL(url) {
    return (URLwithStore.store ?? {})[url] ?? null;
  }

  static revokeObjectURL(url) {
    super.revokeObjectURL(url);
    if (
      new URL(url).protocol === "blob:" &&
      URLwithStore.store &&
      url in URLwithStore.store
    )
      delete URLwithStore.store[url];
  }
}

Usage

const blob = new Blob( ["foo"] );
const url = URLwithStore.createObjectURL( blob );
const retrieved = URLwithStore.getFromObjectURL( url );
console.log( "retrieved Blob is Same Object?", retrieved === blob );
Related