slice large file into chunks and upload using ajax and html5 FileReader - javascript

What I want to implement is:
In the front end, I use the html5 file api to read the file, and then upload the file's content to the php backend using ajax, and it's ok if the filesize is small. However,if the file is big enough, it causes chrome to crash. So I split the large file into chunks using file.slice, when all chunks are uploaded to the php, merge the chunks into a single complete one.
the code is as follows:
the front end:
<style>
#container {
min-width:300px;
min-height:200px;
border:3px dashed #000;
}
</style>
<div id='container'>
</div>
<script>
function addDNDListener(obj){
obj.addEventListener('dragover',function(e){
e.preventDefault();
e.stopPropagation();
},false);
obj.addEventListener('dragenter',function(e){
e.preventDefault();
e.stopPropagation();
},false);
obj.addEventListener('drop',function(e){
e.preventDefault();
e.stopPropagation();
var ul = document.createElement("ul");
var filelist = e.dataTransfer.files;
for(var i=0;i<filelist.length;i++){
var file = filelist[i];
var li = document.createElement('li');
li.innerHTML = '<label id="'+file.name+'">'+file.name+':</label> <progress value="0" max="100"></progress>';
ul.appendChild(li);
}
document.getElementById('container').appendChild(ul);
for(var i=0;i<filelist.length;i++){
var file = filelist[i];
uploadFile(file);
}
},false);
}
function uploadFile(file){
var loaded = 0;
var step = 1024*1024;
var total = file.size;
var start = 0;
var progress = document.getElementById(file.name).nextSibling;
var reader = new FileReader();
reader.onprogress = function(e){
loaded += e.loaded;
progress.value = (loaded/total) * 100;
};
reader.onload = function(e){
var xhr = new XMLHttpRequest();
var upload = xhr.upload;
upload.addEventListener('load',function(){
if(loaded <= total){
blob = file.slice(loaded,loaded+step+1);
reader.readAsBinaryString(blob);
}else{
loaded = total;
}
},false);
xhr.open("POST", "upload.php?fileName="+file.name+"&nocache="+new Date().getTime());
xhr.overrideMimeType("application/octet-stream");
xhr.sendAsBinary(e.target.result);
};
var blob = file.slice(start,start+step+1);
reader.readAsBinaryString(blob);
}
window.onload = function(){
addDNDListener(document.getElementById('container'));
if(!XMLHttpRequest.prototype.sendAsBinary){
XMLHttpRequest.prototype.sendAsBinary = function(datastr) {
function byteValue(x) {
return x.charCodeAt(0) & 0xff;
}
var ords = Array.prototype.map.call(datastr, byteValue);
var ui8a = new Uint8Array(ords);
try{
this.send(ui8a);
}catch(e){
this.send(ui8a.buffer);
}
};
}
};
</script>
the php code:
<?php
$filename = "upload/".$_GET['fileName'];
//$filename = "upload/".$_GET['fileName']."_".$_GET['nocache'];
$xmlstr = $GLOBALS['HTTP_RAW_POST_DATA'];
if(empty($xmlstr)){
$xmlstr = file_get_contents('php://input');
}
$is_ok = false;
while(!$is_ok){
$file = fopen($filename,"ab");
if(flock($file,LOCK_EX)){
fwrite($file,$xmlstr);
flock($file,LOCK_UN);
fclose($file);
$is_ok = true;
}else{
fclose($file);
sleep(3);
}
}
The problem is, after the chunks of the file all being uploaded to the server and merged into a new one, the total file size is smaller than the original, and the merged one is broken. Where is the problem and how to fix it?

Using the readAsBinaryString fn is just bad practice
SendAsBinary is also depricated
Reading the chunks content is just pure dum. Slicing them is enough. xhr.send(blob.slice(0,10))
Slicing is also unnecessary unless the server don't accept such large files (such as dropbox limited REST API)
So if anyone trying to be smart about using worker threads, base64 or FileReader for uploading large files - don't do that, it's all unnecessary.
Only time it's okey to read/slice the file is if you are deciding to encrypt/decrypt/zip the files before sending it to the server.
But only for a limited time until all browser start supporting streams.
Then you should take a look at fetch and ReadableStream
fetch(url, {method: 'post', body: new ReadableStream({...})})
if you just need to forward the blob to the server, just do:
xhr.send(blob_or_file) and the browser will take care of reading it (correctly) and not consume any memory. And the file can be however large the file/blob is

There is a small error in your js script
I noticed that the reader.onprogress event is triggered more times than the xhr load event. In this case some chunks are skipped. Try to increment the loaded variable inside the load function.
function uploadFile(file){
var loaded = 0;
var step = 1024*1024;
var total = file.size;
var start = 0;
var progress = document.getElementById(file.name).nextSibling.nextSibling;
var reader = new FileReader();
reader.onload = function(e){
var xhr = new XMLHttpRequest();
var upload = xhr.upload;
upload.addEventListener('load',function(){
loaded += step;
progress.value = (loaded/total) * 100;
if(loaded <= total){
blob = file.slice(loaded,loaded+step);
reader.readAsBinaryString(blob);
}else{
loaded = total;
}
},false);
xhr.open("POST", "upload.php?fileName="+file.name+"&nocache="+new Date().getTime());
xhr.overrideMimeType("application/octet-stream");
xhr.sendAsBinary(e.target.result);
};
var blob = file.slice(start,step);
reader.readAsBinaryString(blob); }

This Problem is caused mostly by global restrictions of shared Hosts. They often control the amount of data and drop the Connection if Limit is overridden.
I tried this several times and several ways. Always stucking at the same Position. target file was smaller and corrupted. Even taking a smaller file for upload than this size, so that the merged data fits in the limit, the result was OK.
YOu have only one Chance: Get the max size increased, for files to be opened.
Every time you open the target file and write the chunk Content into it, and the size overrides the Limit, the hoster will break the Connection. Example hoster: Strato
Here the Limit is globally set to 16MB. I get max merge-result size double this size. No Chance to override it.

Related

Image duplication after multiple file input

Have this issue here: I want to upload several images at once and show them directly afterwards in the browser. Everything is fine. However, doing this on mobile, sometimes I don't get all the images, but duplicates of another one from the file array. I have a suspicion, that this has to do with hardware limitations, since on PC everything works fine, but not on mobile.
Here is the fiddle. You can check it via mobile to see if duplicates appear.
So in result I get this on mobile. It wasn't two images in the selection, only one:
Here I have the Javascript snippet. Is there any room for improvement? Espacially onload processes are really confusing me from time to time. Is there any chance to write it better?
Javascript:
jQuery('.upload-mobile').change(function(){
jQuery('body').toggleClass( "loading" );
var imagecounter = jQuery(this)[0].files.length;
var loadcounter = 0;
for(var i = 0; i<imagecounter;i++){
for(var p = 1;p<=12;p++){
if(jQuery('#preview'+p).length<=0){
jQuery('.image-collection').prepend(jQuery('<div></div>',{id:'preview'+p,css:{'display':'inline-block'}}));
break;
};
}
var file = this.files[i];
var reader = new FileReader();
reader.onload = function(e){
for(var w = 1;w<=12;w++){
if(jQuery('#preview'+w).children().length>0) {
}
else{
var img = document.createElement("img");
img.src = e.target.result;
img.id = 'img'+w;
img.className += " img-responsive";
img.className += " border-blue";
img.className += " image-small";
img.className += " image-cover";
img.style.display='none';
document.getElementById("preview"+w).appendChild(img);
break;
}
}
loadcounter++;
if(loadcounter==imagecounter){
var count = jQuery('.image-collection').children().length;
jQuery('.image-small').delay('1500').fadeIn('3000', function(){
jQuery('#image-counter').text(count+'/12');
jQuery('body').removeClass( "loading" );
if( jQuery('#image-counter').text()=='12/12'){
jQuery('#upload-photos').fadeOut('200', function(){
jQuery('#three-cube-compact').fadeIn('200');
})
}
});
}
};
var image = reader.readAsDataURL(file);
}
});
EDIT:
I tried to use the forEach function. I didn't use the for each loops earlier, so I can not say much about the difference between foreach and for. I can't say if it works better or not, but I have seen less duplicates (maybe I didn't check to often to asume good functionality of it). It almost works, but maybe there is another way to make it work flawlessly?
Javascript:
jQuery('.upload-mobile').change(function(){
jQuery('body').toggleClass( "loading" );
var imagecounter = jQuery(this)[0].files.length;
var loadcounter = 0;
Array.from(this.files).forEach(function (image, index){
for(var p = 1;p<=12;p++){
if(jQuery('#preview'+p).length<=0){
jQuery('.image-collection').prepend(jQuery('<div></div>',{id:'preview'+p,css:{'display':'inline-block'}}));
break;
};
};
var reader = new FileReader();
reader.onload = function(e){
for(var w = 1;w<=12;w++){
if(jQuery('#preview'+w).children().length>0) {
}
else{
var img = document.createElement("img");
img.src = e.target.result;
img.id = 'img'+w;
img.className += " img-responsive";
img.className += " border-blue";
img.className += " image-small";
img.className += " image-cover";
img.style.display='none';
document.getElementById("preview"+w).appendChild(img);
break;
}
}
loadcounter++;
if(loadcounter==imagecounter){
var count = jQuery('.image-collection').children().length;
jQuery('.image-small').delay('1500').fadeIn('3000', function(){
jQuery('#image-counter').text(count+'/12');
jQuery('body').removeClass( "loading" );
if( jQuery('#image-counter').text()=='12/12'){
jQuery('#upload-photos').fadeOut('200', function(){
jQuery('#three-cube-compact').fadeIn('200');
})
}
});
}
};
var pic = reader.readAsDataURL(image);
});
});
EDIT2:
The fiddle was updated
EDIT3:
Added an image of the mobile screen. I also figured out it might has something to do with the file explorer on the mobile device. When I select using the first file explorer, I don't get the duplication bug:
However, by clicking on Browse ("Durchsuchen" on the image), I get to another file explorer. And using that one creates this "duplication bug":
Is there maybe a way to solve this or maybe to disable the Browse button?

JSZip temporary file location

I have a electron application using jszip to create a zip file that the user is then able save. Everything works fine but my problem is my application uses the users download folder. Im guessing its making a temporary file. I've submitted my application to the mac store and they want me to use another location instead of the users downloads folder for the temporary file. Is there anyway I can specify the temporary location or maybe something else other then jszip that will do this?
Here is the code I use
savePNGButton.addEventListener('click', function(e) {
var zip = new JSZip();
if (WatermarkText == ""){
var img = zip.folder("images");
} else {
var img = zip.folder(WatermarkText);
}
$(".WatermarkPhoto").each(function(index) {
imgsrc = this.src;
var DataURL = imgsrc.replace('data:image/png;base64,','');
img.file(WatermarkText+index+".png", DataURL, {base64: true});
});
zip.generateAsync({type:"blob"})
.then(function(content) {
saveAs(content, WatermarkText+".zip");
});
});
[edit]
Looking more into this it looks like my problem is not with JSZip but with chrome or FileSaver.js using the downloads folder as a temp folder for the file before the users chooses where to place the file. Is there anyway I can change the temp location for my electron app?
If anyone comes across this, I never figured a way around the HTML5 filesystem way of moving the temp file before the users selects the download location. Instead I am using nodejs file system with electrons showSaveDialog. I also had to change JSZip to use .generateNodeStream instead of .generateAsync. Below is my function that I got working for me.
savePNGButton.addEventListener('click', function(e) {
var zip = new JSZip();
if (WatermarkText == ""){
var img = zip.folder("images");
} else {
var img = zip.folder(WatermarkText);
}
$(".WatermarkPhoto").each(function(index) {
imgsrc = this.src;
var DataURL = imgsrc.replace('data:image/png;base64,','');
img.file(WatermarkText+index+".png", DataURL, {base64: true});
});
// zip.file("file", content);
// ... and other manipulations
dialog.showSaveDialog({title: 'Test',defaultPath: '~/'+WatermarkText+'.zip',extensions: ['zip']},(fileName) => {
if (fileName === undefined){
console.log("You didn't save the file");
return;
}
zip
.generateNodeStream({type:'nodebuffer',streamFiles:true})
.pipe(fs.createWriteStream(fileName))
.on('finish', function () {
// JSZip generates a readable stream with a "end" event,
// but is piped here in a writable stream which emits a "finish" event.
console.log("zip written.");
});
});
});

how to get photo compleet url form form in array

I am trying to use jquery to take a picture from my comp via a form.
- So I want the entire URL out of the form in an array
It works + / - in Dreamweaver, but not in the explorer browsers not even chrome
The end goal is a calendar with picture / app for people with disabilities, but as long as I get to go through the phone gap
var foto= new Array();
var i=-1;
//foto=["toets.png"];
$('#fotouit').append("FOTO UIT");
$('#knop01').click(function(){
$('input:file[name=foto]').each(function(){
//alert($(this).val());
foto.push($(this).val());
foto.forEach( function(){
i++;
$('#fotouit').append(foto[i]);
$('#fotouit').append('<img src=" '+ foto[i] + ' " width="100" height="100" />');
});
});
})
I don't think it is possible to get the URL of the picture in you computer's local filesystem, but you can use Javascript's FileReader API to read the contents of the uploaded file (in your case, the picture). The read contents can be used in the src of the img element as you did in your example code.
This is an in depth explanation of what you're trying to accomplish: https://developer.mozilla.org/en-US/docs/Using_files_from_web_applications
Example:
function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
var file = files[i];
var imageType = /image.*/;
if (!file.type.match(imageType)) {
continue;
}
var img = document.createElement("img");
img.classList.add("obj");
img.file = file;
preview.appendChild(img); // Assuming that "preview" is a the div output where the content will be displayed.
var reader = new FileReader();
reader.onload = (function(aImg) { return function(e) { aImg.src = e.target.result; }; })(img);
reader.readAsDataURL(file);
}
}
Note:
You can use the multiple attribute on a file input to allow selecting many files with one input
You can use the file inputs change event to immediately capture the files rather than providing a second button to click

Display Image Using Upload JS

I want to display the image using the upload form and submit button.
But the problem though, I can't make the image appear.
Here's what I did.
function myFunction() {
var x = document.getElementById("myFile").value;
document.getElementById('myImg').src = document.getElementById("myFile").name;
<form>Select a file to upload:
<input type="file" id="myFile" name=filename>
</form>
<button type="button" onclick="myFunction()">Upload This</button>
<img id="myImg" src="">
I just don't know what seems to be the problem with this.
Thank you for helping me out.
The browser does NOT allow javascript full access to the .value property of an input tag with type="file". This is for security reasons so no local path information is made available to javascript.
Thus, you can't set a .src value on an image tag based on a file input value that the end-user specified.
If you don't want to upload the image to server side, you have a possibility to do it only in the client side.
add a div to your html (dom):
<div id='bottom'>
<div id='drag-image' class='holder-file-uploader bottom-asset'> Drag here an image</div>
</div>
add this javascript:
<script>
var holder = document.getElementById('drag-image');
holder.ondragover = function () { return false; };
holder.ondragend = function () { return false; };
holder.ondrop = function (event) {
event.preventDefault && event.preventDefault();
//do something with:
var files = event.dataTransfer.files;
console.log(files);
bottomFileAdd(files, 0);
return false;
};
//Recursive function to add files in the bottom div
//this code was adapted from another code, i didn't test it
var bottomFileAdd = function (files, i) {
if(!i) i=0;
if (!files || files.length>=i) return;
var file = files.item(i);
var img = document.createElement('img');
var bottom = document.getElementById('bottom'); //this string should not be static
bottom.appendChild(img);
var reader = new FileReader();
reader.onload = function (event) {
console.log(event.target);
img.src = event.target.result;
bottomFileAdd(files, i+1);
};
reader.readAsDataURL(file);
}
</script>
note: It may not work in older browsers.
I hope it helps.
You tried to set the src attribute of the <img> tag to a local file path. However, the web browser doesn't expose the local file URL (file://...) in the value property of the <input> tag. The browser implementation may vary; Chrome, for example, gives you a fake path.
You can load the image by the FileReader API into a data URI and show it by setting the src attribute of the <img> tag:
function myFunction() {
var myFile = document.getElementById("myFile");
if (myFile.files && myFile.files.length) {
if (typeof FileReader !== "undefined") {
var fileReader = new FileReader();
fileReader.onload = function(event) {
document.getElementById("myImg").src = event.target.result;
};
fileReader.readAsDataURL(myFile.files[0]);
} else {
alert("Your browser doesn't support the FileReader API.")
}
} else {
alert("No file was selected.")
}
}
You need a fairly modern web browser for this to work:
Browser Firefox Chrome IE Opera Safari
Version 3.6 7 10 12.02 6.0.2
You can have a look at a working sample page. The final implementation of such image preview should set the <img> element to a fixed size, but your markup with my function is enough as a simple demonstration.

On Click preload images using javascript

Directly calling preload function works without any issues. But when preload() is called onClick,even after loading of images ,not ending its processing and that can be viewed as "loading..." in a browser
function preload(images) {
if (document.images) {
var i = 0;
var imageArray = new Array();
imageArray = images.split(',');
var imageObj = new Image();
for(i=0; i<=imageArray.length-1; i++) {
document.write('<img src="' + imageArray[i] + '" width="335px" height="180px" alt="[Alternative text]" />');
imageObj.src=imageArray[i];
}
}
}
Gallery
You can't call document.write after the page is loaded. If you want to add something to the page, you must call DOM manipulation functions like document.createElement (see example).
But what you do in your function doesn't look like preloading but rather like direct insertion of images in the page.
If you want to preload images, that is to ask the browser to cache them so that they are instantly available later, then you'd better use XmlHttpRequest instead of creating Image elements. Issuing XmlHttpRequest requests doesn't make the browser display a hourglass and the user doesn't feel like something is happening.
I made a small "library" last week-end just for this : easily preload resources.
var preload = (function(){
var queue = [], nbActives = 0;
function bip(){
if (queue.length==0 || nbActives>=4) return;
nbActives++;
var req = new XMLHttpRequest(), task=queue.shift();
req.open("GET", task.src, true);
req.onload = function () {
nbActives--;
bip();
if (task.callback) task.callback(task.src);
};
req.send();
}
return function(src, priority, callback) {
queue[priority?'unshift':'push']({src:src, callback:callback});
bip();
}
})();
Usage :
preload('path/to/file.png'); // preload the file
preload('path/to/file.png', true); // preload the file with high priority
preload('path/to/file.png', false, callback); // preload the file and be notified when it's finished
Github repository : https://github.com/Canop/preload

Categories

Resources