Browser File Storage
Abstracts the complexity of the IndexedDB Api so that a user can easily save files on the browser.
While there are some local database like libraries to store json objects, advanced queries, replication, etc, this is very focused on a single use case: saving and loading files locally.
Important
- Browser must support the IndexedDB API as it cannot be polyfilled: https://caniuse.com/#feat=indexeddb
- Browser must support promises or include a Promise polyfill: https://www.npmjs.com/package/promise-polyfill
- Files saved locally, just like IndexedDB entries, are namespaced by domain by default, don't expect to save a file on one domain and be able to access it from another just because it is the same user's computer accessing your websites.
Installation
Installing Package
npm install browser-file-storage
Importing in Webpack
import browserFileStorage from 'browser-file-storage'
Importing through script tag
To be found in the builds folder or the dist folder
<script type="text/javascript" src="browser-file-storage.min.js">
Usage
Logging
By default browserFileStorage will only log errors, if you want to see logs in more detail you can change the mode.
browserFileStorage.logLevel('info')
Initialization
Before using browserFileStorage, it is important to initialize the instance.
Although a namespace parameter is not needed, it is safer to namespace the instance with the name of your app.
The app name will be appended to the inner database name and create a unique identifier in case another library within the same domain, also uses indexedDB internally.
The same namespace will have to be used always when running init as it will re open the same inner database.
browserFileStorage.init('MY_AMAZING_APP').then((status) => {
if(status.initial) {
}
}).catch((error) => {
if(!error.supported) {
}
if(error.alreadyInit) {
}
if(error.dbError) {
console.error(error.error)
}
})
Asking for persistency
Since browser-file-storage uses indexed DB to store files internally, we can ask for user/browser permission for the data to stay on the computer permanently or until the user manually deletes it (through browser or filesystem). Default persistency means the browser can technically clear the data whenever it wants to in order to fit other data.
browserFileStorage.persist().then((status) => {
if(status.persistent) {
} else {
if(status.canPersist) {
} else {
}
}
}).catch((error) => {
})
Saving a file
Save takes two mandatory parameters: filename and contents
Save also takes an optional third parameter: mimetype
filename is a string and will be used as identifier to save and load the file from the inner database. the extension in the filename will also be used if possible to auto-detect the mimetype of the file, unless the content we are storing is a blob with an already existing mimetype, or if the third optional parameter mimetype is set (In which case mimetype will be forced to whatever was specified on that third parameter)
contents can be a raw string, a blob, a BrowserFile (Type used internally to unify what we save and load from browserFileStorage)
Simple Examples
browserFileStorage.save('settings.txt', 'This is the settings file!').then(((file) => {
console.log('Saved file successfully!', file)
}).catch((error) => {
console.error('Could not save the file!', file)
})
browserFileStorage.save('background.png', myImage).then(((file) => {
console.log('Saved image successfully!', file)
}).catch((error) => {
console.error('Could not save the image!', file)
})
In Depth Example and Error Handling
const data = { preferences: { notifications: "off" } }
const json = JSON.stringify(data)
browserFileStorage.save('settings.json', json ).then((file) => {
console.log('File Saved successfully!', file)
}).catch((error) => {
if(!error.init) {
}
if(error.invalidFilename) {
}
if(error.invalidContents) {
}
if(error.dbError) {
console.error(error.error)
}
})
Auto-Assigned MimeType
If save received a raw string, or blob with no mimetype, it will try to match the extension from the filename to a known mimetype.
Since there are LOTS of mimetypes, we only detect some of the most common types, other wise files default to text/plain or application/octet-stream
If you want to make sure your file is saved with the proper mimetype, so that when it is loaded again it can be read by the browser as you want it to, simply pass a third parameter to the save function with the mimetype; This will force the internal blob to that type Even if the blob was already typed
const data = { preferences: { notifications: "off" } }
const json = JSON.stringify(data)
browserFileStorage.save('settings.json', json, 'application/json' ).then((file) => {
}).catch((error) => {
})
File metadata
In case you want to store any sort of metadata for the file, you can use an optional 4th parameter after mimetype when using the save function (You can leave mimetype as null if you are using auto-assigned mimetype)
Metadata is expected to be a simple JS object.
const data = { preferences: { notifications: "off" } }
const json = JSON.stringify(data)
browserFileStorage.save('settings.json', json, null, { md5: 'some hash' }).then((file) => {
}).catch((error) => {
})
More Specific Examples
Some specific save examples of different scenarios
Browser File methods and properties
When saving or loading a file, it is stored in a BrowserFile Object.
let file =
console.log(file.extension)
console.log(file.filename)
console.log(file.lastModified)
console.log(file.size)
console.log(file.blob)
console.log(file.type)
console.log(file.metadata)
let url = file.createURL()
file.destroyURL()
file.toString().then((string) => {
console.log(string)
}).catch((error) => {
if(!error.supported) {
}
if(error.fileError) {
}
if(error.readError) {
console.error(error.e)
}
})
file.toBinaryString().then((binaryString) => { })
file.toDataURL().then((dataURL) => { })
file.toArrayBuffer().then((arrayBuffer) => { })
Loading a file
Loading simply takes a filename as an argument and returns a promise that when resolved, contains the BrowserFile object.
Basic Examples
browserFileStorage.load('background.png').then((file) => {
let image = document.getElementById('someImage')
let url = file.createURL()
image.src = url
}).catch((error) => {
console.error(error)
})
browserFileStorage.load('settings.json').then((file) => {
file.toString().then((stringContents) => {
let object = {}
try {
object = JSON.parse(stringContents)
} catch (e) {
}
console.log(object)
})
}).catch((error) => {
console.error(error)
})
In Depth Example and Error Handling
browserFileStorage.load('favicon.ico').then((file) => {
console.log('File Loaded Successfully!', file)
}).catch((error) => {
if(!error.init) {
}
if(error.invalidFilename) {
}
if(error.notFound) {
}
if(error.dbError) {
console.error(error.error)
}
})
Loading All Files
If you want to load all files, they will be returned as an array of BrowserFile objects.
Basic Example
browserFileStorage.loadAll().then((files) => {
for(let f in files) {
console.log('Loaded a file: ', files[f])
}
}).catch((error) => {
console.error(error)
})
In Depth Example and Error Handling
browserFileStorage.loadAll().then((files) => {
for(let f in files) {
let file = files[f]
if(file.type === 'image/png') {
let image = document.createElement('img')
image.src = file.createURL()
document.body.appendChild(image)
}
}
}).catch((error) => {
if(!error.init) {
}
if(error.dbError) {
console.error(error.error)
}
})
Listing all filenames / keys
If you want to get a list of all filenames or keys (Ex: in order to delete some of them)
Basic Example
browserFileStorage.list().then((filenames) => {
for(let f in filenames) {
console.log('Use this key to delete or load a file: ', filenames[f])
}
}).catch((error) => {
if(!error.init) {
}
if(error.dbError) {
console.error(error.error)
}
})
Deleting a file
Delete takes a filename and resolves once the file is no longer there. It will resolve regardless of if the file existed in the first place or not.
Basic Example
browserFileStorage.delete('favicon.ico').then(() => {
console.log('Favicon.ico no longer exists locally!')
}).catch((error) => {
console.error(error)
})
In Depth Example and Error Handling
browserFileStorage.delete('settings.json').then(() => {
console.log('settings.json no longer exists locally!')
}).catch((error) => {
if(!error.init) {
}
if(error.invalidFilename) {
}
if(error.dbError) {
console.error(error.error)
}
})
Deleting All Files
Basic Example
browserFileStorage.deleteAll().then(() => {
console.log('Deleted All Files!')
}).catch((error) => {
console.error(error)
})
In Depth Example and Error Handling
browserFileStorage.deleteAll().then(() => {
console.log('Deleted All Files!')
}).catch((error) => {
if(!error.init) {
}
if(error.dbError) {
console.error(error.error)
}
})
Capacity
Storage allowed for the inner IndexedDB Api is surprisingly high, specially when persisted, but it is very browser dependent.
https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria
...
Save specific examples.
Storing a file fetched using XHR
var xhr = new XMLHttpRequest();
xhr.open('GET', 'static/icon-source.png', true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status == 200) {
var blob = this.response;
browserFileStorage.save('icon-source.png', blob).then((file) => {
console.log('Saved file!', file)
})
.catch((error) => {
console.error(error)
})
}
};
xhr.onerror = function(e) {
alert("Error " + e.target.status + " occurred while receiving the document.");
};
xhr.send();
Storing a file fetched using XHR (axios)
axios({
url: 'static/icon-source.png',
method: 'GET',
responseType: 'blob'
}).then((response) => {
browserFileStorage.save('icon-source.png',response.data).then((file) => {
console.log('Saved file!', file)
})
.catch((error) => {
console.error(error)
})
}).catch((error) => [
console.error(error)
])
Storing a file from upload file input
let fileInput = document.getElementById('file-upload')
fileInput.addEventListener('change', (e) => {
let files = e.target.files
let file = files ? files[0] : null
if(file) {
browserFileStorage.save(file.name, file).then((savedFile) => {
console.log('saved file! - ', savedFile)
}).catch(error => {
console.error(error)
})
} else {
console.log('no file...')
}
})
Getting file hashes to know if file needs to be updated
While I decided not to implement something like this in the base library, you can use libraries such as CryptoJS to get hashes from a browserFileStorage file.
let loadedFile =
loadedFile.toArrayBuffer().then((arrayBuffer) => {
let wordArray = window.CryptoJS.lib.WordArray.create(arrayBuffer)
let hash = window.CryptoJS.MD5(wordArray).toString())
console.log(hash)
})