pinoyyid
pinoyyid

Reputation: 22306

How do I save and restore a File object in local storage

I have an HTML5/javscript app which uses

<input type="file" accept="image/*;capture=camera" onchange="gotPhoto(this)">

to capture a camera image. Because my app wants to be runnable offline, how do I save the File (https://developer.mozilla.org/en-US/docs/Web/API/File) object in local storage, such that it can be retrieved later for an ajax upload?

I'm grabbing the file object from the using ...

function gotPhoto(element) { 
     var file = element.files[0];
     //I want to save 'file' to local storage here :-(
}

I can Stringify the object and save it, but when I restore it, it is no longer recognised as a File object, and thus can't be used to grab the file content.

I have a feeling it can't be done, but am open to suggestions.

FWIW My workaround is to read the file contents at store time and save the full contents to local storage. This works, but quickly consumes local storage since each file is a 1MB plus photograph.

Upvotes: 74

Views: 125670

Answers (8)

rorymichele
rorymichele

Reputation: 61

Update 2024:

While it is still not possible to serialize the actual File Object, a way more efficient solution to store the content for later upload (compared to serializing to base64) is possible thanks to the widely avaible origin private file system* (OPFS).

Below is a minimal Typescript example to store an array of File objects with cacheFiles and retrieving them with retrieveFiles:

public async cacheFiles(files: File[]) Promise<void> {
  const opfs = await navigator.storage.getDirectory();
  for (let file of files) {
    const handle = await opfs.getFileHandle(file.name, {create: true});
    const stream = await handle.createWritable();
    await stream.write(file);
    await stream.close();
  }
}

private async retrieveFiles(): Promise<File[]> {
  const opfs = await navigator.storage.getDirectory();
  const files: File[] = [];
  for await (const fileEntry of opfs.values()) {
    const fileHandle = await opfs.getFileHandle(fileEntry.name);
    files.push(await fileHandle.getFile());
  }
  return files;
}

The retrieved array of Files can then be used just like the FileList returned by the files attribute of HTMLInputElement with type=file. Since 2023 the methods used in above example are available in all major browsers (MDN Baseline 2023).

An extensive guide with examples can be found at https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system .

Working with a File reference without copying the content, as described at https://developer.chrome.com/docs/capabilities/web-apis/file-system-access , should become possible once the File System Access API (https://wicg.github.io/file-system-access/#api-showopenfilepicker) is widely supported, namely the methods

As of December 2024, this is not the case!

Upvotes: 3

George Mavritsakis
George Mavritsakis

Reputation: 7093

You cannot serialize file API object.

Not that it helps with the specific problem, but ... Although I haven't used this, if you look at the article it seems that there are ways (although not supported yet by most browsers) to store the offline image data to some files so as to restore them afterward when the user is online (and not to use localStorage)

Upvotes: 31

Convert it to base64 and then save it.

function gotPhoto(element) {
   var file = element.files[0];
   var reader = new FileReader()
   reader.onload = function(base64) {
      localStorage["file"] = base64;
   }
   reader.readAsDataURL(file);
}
// Saved to localstorage

function getPhoto() {
   var base64 = localStorage["file"];
   var base64Parts = base64.split(",");
   var fileFormat = base64Parts[0].split(";")[1];
   var fileContent = base64Parts[1];
   var file = new File([fileContent], "file name here", {type: fileFormat});
   return file;
}
// Retreived file object

Upvotes: 31

Dryden Williams
Dryden Williams

Reputation: 1305

You could do something like this:

// fileObj = new File(); from file input
const buffer = Buffer.from(await new Response(fileObj).arrayBuffer());
const dataUrl = `data:${fileObj.type};base64,${buffer.toString("base64")}`;

localStorage.setItem('dataUrl', dataUrl);

then you can do:

document.getElementById('image').src = localStorage.getItem('dataUrl');

Upvotes: 0

umithuckan
umithuckan

Reputation: 111

create an object on the global scope

exp: var attmap = new Object();

after you are done with file selection, put your files in attmap variable as below,

attmap[file.name] = attachmentBody;
JSON.stringify(attmap)

Then you can send it to controller via input hidden or etc. and use it after deserializing.

(Map<String, String>)JSON.deserialize(attachments, Map<String,String>.class);

You can create your files with those values in a for loop or etc.

EncodingUtil.base64Decode(CurrentMapValue);

FYI:This solution will also cover multiple file selection

Upvotes: 1

I don't think that there is a direct way to Stringify and then deserialize the string object into the object of your interest. But as a work around you can store the image paths in your local storage and load the images by retrieving the URL for the images. Advantages would be, you will never run out of storage space and you can store 1000 times more files there.. Saving an image or any other file as a string in local storage is never a wise decision..

Upvotes: 1

Joseph Astrahan
Joseph Astrahan

Reputation: 9082

Here is a workaround that I got working with the code below. I'm aware with your edit you talked about localStorage but I wanted to share how I actually implemented that workaround. I like to put the functions on body so that even if the class is added afterwards via AJAX the "change" command will still trigger the event.

See my example here: http://jsfiddle.net/x11joex11/9g8NN/

If you run the JSFiddle example twice you will see it remembers the image.

My approach does use jQuery. This approach also demonstrates the image is actually there to prove it worked.

HTML:

<input class="classhere" type="file" name="logo" id="logo" />
<div class="imagearea"></div>

JS:

$(document).ready(function(){
  //You might want to do if check to see if localstorage set for theImage here
  var img = new Image();                
  img.src = localStorage.theImage;

  $('.imagearea').html(img);

  $("body").on("change",".classhere",function(){
      //Equivalent of getElementById
      var fileInput = $(this)[0];//returns a HTML DOM object by putting the [0] since it's really an associative array.
      var file = fileInput.files[0]; //there is only '1' file since they are not multiple type.

      var reader = new FileReader();
      reader.onload = function(e) {
           // Create a new image.
           var img = new Image();

           img.src = reader.result;
           localStorage.theImage = reader.result; //stores the image to localStorage
           $(".imagearea").html(img);
       }

       reader.readAsDataURL(file);//attempts to read the file in question.
    });
});

This approach uses the HTML5 File System API's to read the image and put it into a new javascript img object. The key here is readAsDataURL. If you use chrome inspector you will notice the images are stored in base64 encoding.

The reader is Asynchronous, this is why it uses the callback function onload. So make sure any important code that requires the image is inside the onLoad or else you may get unexpected results.

Upvotes: 14

Philip G
Philip G

Reputation: 4104

You could use this lib:

https://github.com/carlo/jquery-base64

then do something similar to this:

//Set file
var baseFile = $.base64.encode(fileObject);
window.localStorage.setItem("file",basefile);

//get file
var outFile = window.localStorage.getItem("file");

an other solution would be using json (I prefer this method) using: http://code.google.com/p/jquery-json/

//Set file
window.localStorage.setItem("file",$.toJSON(fileobject));

//get file
var outFile = $.evalJSON(window.localStorage.getItem("file"));

Upvotes: 1

Related Questions