Modify items of $_FILES global var in JS before uploading files - javascript

Suppose simple file uploading with JS request and PHP processing
For example:
<input type="file" name="items[]">
<button>UPLOAD</button>
<script>
document.querySelector('button').addEventListener('click', (e) => {
let data = new FormData();
data.append('items[]', document.querySelector('input').files[0]);
fetch('/upload.php', {
method: 'POST',
body: data
})
.then(response => response.text())
.then(text => {
console.log(text);
});
});
</script>
Is it possible in JS to somehow modify items[] array that is $_FILES['items'] for example its ['name'] and ['type'] subarrays before sending to PHP?
Something like (doesn't work)
data.append('items[]["name"][0]', 'fake_name.ext');

You cannot change the type property of a File as it is read-only but you can set a custom name as the 3rd parameter to FormData.prototype.append()
data.append(
"items[]",
document.querySelector("input[type=file]")?.files[0],
"fake_name.ext"
);
If you really want to change the type, you could copy the original file into a new File and alter the type property in options
const file = document.querySelector("input[type=file]")?.files[0];
const copy = new File([file], "fake_name.ext", {
type: "some/mime-type",
});
data.append("items[]", copy);

Related

Send FileList in FormData in javascrirpt

I am trying to send a bunch of images using FormData but of course, I cannot send FileList and I was unsuccessful in figuring out a way to send these files. I have already tried to find answers on my own but most of them suggest appending each file.
for (let i = 0 ; i < images.length ; i++) {
formData.append("images[]", images[i]);
}
But it ends up with only the last file inside formData, for some reason previous files are being replaced by the next one in line.
I have also tried to convert FileList to an array, which works but I don't know how I could separate these files, right now every file is inside one key as a string.
0: "[{\"lastModified\":1606255989000,\"lastModifiedDate\":\"undefined\",\"name\":\"logo.png\",\"size\":54438,\"type\":\"image/png\"},{\"lastModified\":1606255979000,\"lastModifiedDate\":\"undefined\",\"name\":\"logo1.png\",\"size\":58023,\"type\":\"image/png\"},{\"lastModified\":1606252752000,\"lastModifiedDate\":\"undefined\",\"name\":\"logo2.png\",\"size\":28147,\"type\":\"image/png\"},{\"lastModified\":1606255121000,\"lastModifiedDate\":\"undefined\",\"name\":\"logo3.png\",\"size\":18260,\"type\":\"image/png\"}]"
I could just convert it to string and cut it to their own keys using } as and end of each entry. I don't want to do this, even with my little experience I know it's not a good way to go about it.
As of this moment, my javascript code looks like this.
File.prototype.toObject = function () {
return Object({
lastModified: parseInt(this.lastModified),
lastModifiedDate: String(this.lastModifiedDate),
name: String(this.name),
size: parseInt(this.size),
type: String(this.type)
})
}
FileList.prototype.toArray = function () {
return Array.from(this).map(function (file) {
return file.toObject()
})
}
let files = document.getElementById('files').files
let filesArray = files.toArray();
let formData = new FormData();
formData.append('images[]', JSON.stringify(filesArray));
I then send this data like this
fetch('<?=env('app.baseURL')?>/admin/gallery', {
method: 'POST',
processData: false,
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData,
data: formData,
}).then(function (response) {...rest of the code...})
After sending this data, which is received without any problems I want to save these files, but I cannot without first separating them. I am using PHP for the back-end.
I don't have much experience with javascript so I might be missing something obvious, any help is appreciated.
In your for loop you could use dynamic name for your formData appends. For example:
formData.append(`image-${i}`, images[i])
Or in case you want to push your images to a single key value pair in your formData, like in your example, you should use the getAll() method to retrieve your images.
formData.getAll('images[]')
Here is the PHP code to go through the array containing the images :
<?php
if(isset($_FILES) && isset($_FILES['images'])){
$countfiles = count($_FILES['images']['name']);
for($i=0;$i<$countfiles;$i++){
$filename = $_FILES['images']['name'][$i];
$sql = "INSERT INTO myimages (filename) VALUES ('".$filename."')";
$db->query($sql);
move_uploaded_file($_FILES['images']['tmp_name'][$i],'upload/'.$filename);
}
}
?>

Is it possible to send a list of files and a whole json object in a FormData object to a ASP.NET CORE MVC Controller

Just like the title says, how do I send a formdata object to a mvc controller with both a json object (including nested objects) and list of files.
I have already tried to stringify the object to a json object but the controller can not read the property, it reads the file list without problems.
Here is the controller method:
[HttpPost]
public IActionResult CreateTask(Task task, IEnumerable<IFormFile> files)
{
//some code
}
here is my javascript:
function createTask() {
var formData = new FormData();
var files = //some file objects
var obj = {
//some parameters
};
var task = JSON.stringify(task);
formData.append("task", task);
formData.append("files", files);
console.log(task);
$.ajax({
type: "POST",
url: "/Task/CreateTask",
processData: false,
contentType: false,
data: formData,
success: function (data) {
},
error: function (data) {
}
})
}
I need the controller method to read both the task and the file list at the same time if this is possible.
The only way to do this would be to bind the JSON sent as task to a string server-side. Then, you'd have to manually deserialize it into an object. In case it's not obvious, that also means you won't get any validation on any of the members of that JSON object. It will just be a string as far as ASP.NET Core and the modelbinder is concerned.
That said, I think the issue here is that you're needing to upload files and think that that necessitates posting as multipart/form-data. You can actually post as JSON and yet still include file uploads. That requires two changes, though:
You must bind the file "uploads" to byte[]s, instead of IFormFiles, server-side.
Client-side, you must add them to the JSON object you're posting as either Base64-encoded strings or uint8 arrays.
The first part is relatively straight-forward. The JSON deserializer invoked by the modelbinder will automatically convert Base64-encoded strings to byte array, and of course a JS unint8 array is essentially just a byte array, anyways.
The second part probably bears a bit more discussion. You'll need to need to use the File API to read the upload file data, and then convert that into either a Base64-encoded string or uint8 array:
Base64
var reader = new FileReader();
reader.onload = function(e) {
let base64 = btoa(reader.result);
myJsonObject.files.push(base64);
}
reader.readAsBinaryString(file);
Byte Array
var reader = new FileReader();
reader.onload = function(e) {
let bytes = Array.from(new Uint8Array(reader.result));
myJsonObject.files.push(bytes);
}
reader.readAsArrayBuffer(file);
You could try to convert object to form-data like below:
View
<script type="text/javascript">
$(document).ready(function () {
$("input").change(function () {
var formData = new FormData();
var files = $("#files")[0].files;
var obj = {
id: 1,
name: "jack"
};
for (var key in obj) {
formData.append(key, obj[key]);
}
for (var key in files) {
formData.append("files", files[key]);
}
$.ajax({
type: "POST",
url: "/api/values/CreateTask",
processData: false,
contentType: false,
data: formData,
success: function (data) {
},
error: function (data) {
}
})
});
});
</script>
Controller
[HttpPost]
public IActionResult CreateTask([FromForm]Task task, [FromForm]IEnumerable<IFormFile> files)
{
return Ok("Success");
}

Convert and object to file and send via Ajax

This and this answer is my motivation for trying to convert an object to file type and append to FormData as so:
var fileBag = new FormData();
fileArray.forEach(function(element) {
file_name = element.name;
file_type = element.type;
var file = new File([element], file_name, {type:file_type});
console.log("file type= ", typeof file); // o/p is object
fileBag.append(file_name, file);
});
When I tried the same, typeof newly created file is object (I expect it to be file) after being converted to file.
File object is same before and after conversion as so:
File
lastModified: 1543472205025
lastModifiedDate: Thu Nov 29 2018 11:46:45 GMT+0530 (India Standard Time) {}
name: "testFile.xlsx"
size: 1119910
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
webkitRelativePath: ""
__proto__: File
This is how I am sending via Ajax
$.ajax({
url: '/project/url/',
type: 'POST',
data: {'file': fileBag},
enctype: 'multipart/form-data',
processData: false,
contentType: false,
dataType: "json",
success: function (data) {
console.log("data= ",data);
},
error: (error) => {
console.log(JSON.stringify(error));
}
});
I have tried this in both Django and PHP. I am getting empty arrays. My question is:
1) Will the typeof before and after conversion will be the same?
2) How can I send file object arrays to backend? Is there any other solution?
Here is my codepen of the same.
Edit: Codepen code
var fileArray = [];
var fileBag = new FormData();
function handleFileUpload(event)
{
var fileUploadButtonId = event.currentTarget.id;
var files = document.getElementById(fileUploadButtonId).files;
var fileBag = new FormData();
var arrayOfAllUploadedFiles = Array.from(files);
for(var i=0; i<arrayOfAllUploadedFiles.length; i++)
{
fileArray.push(arrayOfAllUploadedFiles[i]);
}
fileArray.forEach(function(element) {
file_name = element.name;
file_type = element.type;
var file = new File([element], file_name, {type:file_type});
console.log("file type= ", typeof file); // o/p is object. // o/p is object. Shouldn't this be 'file'?
fileBag.append(file_name, file);
alert("filebag= ",fileBag); // this is empty
});
}
Will the typeof before and after conversion will be the same?
Because "file" isn't a JavaScript data type. Everything that isn't a primitive is an object.
console.log(typeof 1);
console.log(typeof "1");
console.log(typeof null);
console.log(typeof undefined);
console.log(typeof [1,2,3]);
console.log(typeof {hello: "world"});
console.log(typeof new Date());
console.log(typeof document.body);
console.log(typeof window);
2) How can I send file object arrays to backend? Is there any other solution?
Understand how data is formatted when you send it over HTTP
Understand the limitations of jQuery
You can't send an array. You can only send some text which server-side code can assemble into an array.
For both standard encodings supported by forms, PHP will populate $_POST with the data in them.
If the name of a field in that data ends in [], PHP will put an array in $_POST.
If you pass an array to jQuery ajax:
data: { someValue: [1, 2, 3] }
Then jQuery will encode it as:
someValue[]=1&someValue[]=2&someValue[]=3
… so PHP will generate an array.
However, the values have to be things that jQuery understands. It can't handle FormData objects.
If you want to send a FormData object (which you have to do in order to send files) then you need to send it instead of an object:
jQuery.ajax({
url: "/foo",
data: a_form_data_object,
processData: false,
contentType: false,
});
You need processData so it doesn't try to convert the FormData object (i.e. so it will let XMLHttpRequest do that). You need contentType to stop it overriding the one XMLHttpRequest will generate from the FormData object.
So then to send an array, we go back to the rules I described above. You said:
fileBag.append(file_name, file);
But to get an array you need a name ending in [].
fileBag.append("myname[]", file);
Finally, not visible in your question, but looking at your codepen. This will give you a set of files:
document.getElementById(fileUploadButtonId).files;
Since they are files, using code to convert them to files makes no sense.
You can simplify your code:
var fileBag = new FormData();
var files = document.getElementById(fileUploadButtonId).files;
var arrayOfAllUploadedFiles = Array.from(files);
fileArray.forEach(file => fileBag.append("myname[]", file);
jQuery.ajax({
url: "/foo",
data: fileBag,
processData: false,
contentType: false,
});

angular get image data and again append it to FormData

In angular 5 I am getting the images for hotelgallery from mongodb through my service. So basically the data what I am getting is like this
{
fieldname: "hotelgallery",
originalname: "e.jpg",
encoding: "7bit",
mimetype: "image/jpeg",
destination: "./public/",
encoding : "7bit",
filename : "1521139307413.jpg"
mimetype : "image/jpeg"
path : "public/1521139307413.jpg"
size : 66474
}
{
fieldname: "hotelgallery",
originalname: "e.jpg",
encoding: "7bit",
mimetype: "image/jpeg",
destination: "./public/",
encoding : "7bit",
filename : "1521139307413.jpg"
mimetype : "image/jpeg"
path : "public/1521139307413.jpg"
size : 66474
}
{
fieldname: "hotelgallery",
originalname: "j.jpg",
encoding: "7bit",
mimetype: "image/jpeg",
destination: "./public/",
encoding : "7bit",
filename : "1526753678390.jpg"
mimetype : "image/jpeg"
path : "public/1526753678390.jpg"
size : 66470
}
{
fieldname: "hotelgallery",
originalname: "k.jpg",
encoding: "7bit",
mimetype: "image/jpeg",
destination: "./public/",
encoding : "7bit",
filename : "7865456789413.jpg"
mimetype : "image/jpeg"
path : "public/7865456789413.jpg"
size : 66300
}
Now I want to again append those data to FormData but its not working.
The code what I have done so far
export class HotelEditComponent implements OnInit {
formData = new FormData();
ngOnInit() {
this.getOneHotel(this.route.snapshot.params['id']);
}
getOneHotel(id) {
this.http.get( this.apiUrl + '/api/hotel/' + id).subscribe(data => {
this.hotel = data;
this.appendImages(data['hotelimages']); //Here I am getting the data as mentioned here
});
}
public appendImages(imagedata) {
for (var i = 0; i < imagedata.length; i++) {
console.log(imagedata[i]);
this.formData.append('hotelgallery', imagedata[i], imagedata[i]['originalname']);
}
console.log(this.formData);
}
}
So can someone tell me how can I append the existing image data to FormData? Any help and suggestions will be really appreciable.
UseCase for this:
Actually I had used formData to upload images in angular. Now in the edit page the images are showing fine. But lets say a user edits some data and upload some images or remove some images. In that case I am getting the images from the database and again trying to upload them with formdata.
I have used this module and multer for nodejs to upload images with formData.
So can someone tell me how can I append the existing image data to FormData? Any help and suggestions will be really appreciable.
Actually, this approach need more add script solution. for example
1. Get Image Blob from server
Since you return detail object of images, not with the blob. You need have a endpoint to return as blob. (or if return as data buffer then it transform to blob you can use BlobUtil)
2. Put Blob to append form data
You need use blob to append in param 2 no a path, see documentation.
name
The name of the field whose data is contained in value.
value
The field's value. This can be a USVString or Blob (including subclasses
such as File).
filename Optional
The filename reported to the server
(a USVString), when a Blob or File is passed as the second parameter.
The default filename for Blob objects is "blob". The default filename
for File objects is the file's filename.
That what you need, but that is bad practice.
Let's say, you have 30 images to edit, then you need request blob endpoint to get those images blob to appends. But user only want to update 1 image, wasting time to request image blob, right?
For edit image usually we don't need append to file form (<input type="file" />).
Just show it as thumbnail to see what image uploaded and let file form empty.
What we do usually, thumbnail that image.
When user what to change, user put new image and replace old image with new want and update database.
if not, do nothing for image. (YAGNI)
FormData's append is silently failing here. You need to attach the 'image' as a blob. See the MDN docs.
formData.append('hotelgallery', new Blob([imagedata[i]], { type: "text/xml"}), imagedata[i]['originalname']);
Also, just printing formData won't show anything, instead try:
console.log(this.formData.getAll('hotelgallery'));
or
for (var value of this.formData.values()) {
console.log(value);
}
But lets say a user edits some data and upload some images or remove
some images. In that case I am getting the images from the database
and again trying to upload them with formdata.
So, you can pass object to universal method and on result get formData. Object data even can contain nested objects.
static createFormData(object: Object, form?: FormData, namespace?: string): FormData {
const formData = form || new FormData();
for (let property in object) {
if (!object.hasOwnProperty(property) || object[property] === undefined) {
continue;
}
const formKey = namespace ? `${namespace}[${property}]` : property;
if (object[property] instanceof Date) {
formData.append(formKey, object[property].toISOString());
} else if (typeof object[property] === 'object' && !(object[property] instanceof File)) {
this.createFormData(object[property], formData, formKey);
}
else if (typeof object[property] === 'number') {
let numberStr = object[property].toString().replace('.', ',');
formData.append(formKey, numberStr);
}
else {
formData.append(formKey, object[property]);
}
}
return formData;
}
}
In Component class:
export class HotelEditComponent implements OnInit {
formData = new FormData();
hotel: any;
...
ngOnInit() {
this.getOneHotel(this.route.snapshot.params['id']);
}
getOneHotel(id) {
this.http.get( this.apiUrl + '/api/hotel/' + id).subscribe(data => {
this.hotel = data;
});
}
postToServer() {
// or even can pass full `this.hotel` object
// this.helperService.createFormData(this.hotel)
this.formData = this.helperService.createFormData(this.hotel['hotelimages'])
this.service.post(this.formData);
}
...
}
It looks like you are trying to append a JSON array, since formData.append can only accept a string or blob, try the JSON.stringify() method to convert your array into a JSON string. (untested)
e.g. I think you can replace
this.appendImages(data['hotelimages']);
with
this.formData.append('hotelgallery', JSON.stringify(data['hotelimages']));
This is more of a design issue right now, rather then a tech problem. You are asking about posting FormData again and you want to fetch the images data again for that.
Now let's look at your current design.
User uploads 3 images of 4MB size each
On your edit page you downloads each of these images. Cost=12MB
On the edit page user deletes 2 images and adds 2 images. Cost=12MB
So final cost of updating 2 images of 8MB is 24MB. Which is a lot
Now before figuring out how to do FormData, figure out the right design for your app.
Consider the OLX site which allows you to post ads and later edit them. When you edit and remove a image, they call a API for removing the image
The ideal design in my opinion would be below
Submit all images in your create
For edit create a add and remove endpoint for the image
Submitting text data again on a edit form is ok, but submitting the same images data again on a edit form is never ok. Reconsider your design

passing php variables with ajax

I've been trying to get php to rename an image in an S3 bucket. There's three stages
upload image (you can see a previous question I asked to see my
solution to that) I've included the code I used below.
Use ajax to take the name I want for a file (the user ID) and
pass it to a PHP renaming script.
run the renaming script.
Step two is giving me problems. If i hard code the names into the PHP script then it will find the source file and rename it.
However I can't get it to rename the files using variables drawn from the page.
Here's the HTML button code
<input type="file" id="file-chooser" />
<button onclick= "pass()" id="upload-button">Upload to S3</button>
<div id="results"></div>
Here's the JS code
<script type="text/javascript">
var bucket = new AWS.S3({params: {Bucket: 'MY BUCKET'}});
var fileChooser = document.getElementById('file-chooser');
var button = document.getElementById('upload-button');
var results = document.getElementById('results');
button.addEventListener('click', function() {
var file = fileChooser.files[0];
if (file) {
results.innerHTML = '';
var filename = file.name;
var params = {Key: file.name, ContentType: file.type, Body: file};
bucket.upload(params, function (err, data) {
results.innerHTML = err ? 'ERROR!' : 'UPLOADED';
ajax_post();
});
} else {
results.innerHTML = 'Nothing to upload.';
}
}, false);
function pass() {
$.get("test.php");
return false;
}
function ajax_post() {
var ref = new
Firebase("https://MY FIREBASE .firebaseio.com/");
var authData = ref.getAuth();
// Create our XMLHttpRequest object
var fileChooser = document.getElementById('file-chooser');
var file = fileChooser.files[0];
var filename = file.name;
var userID = authData.uid;
var userID = USERS USER ID;
//alert($(this).attr('id'));
$.ajax({
type: "POST",
url: 'test.php',
data: { userID : userID },
data: { filename : filename },
success: function (data) {
alert("success!");
}
};
pass();
}
</script>
Here's the PHP.
<?php
require 'vendor/autoload.php';
use Aws\S3\S3Client;
$sourceBucket = "MY BUCKET";
$sourcename1 = $_POST['filename'];
$targetKeyname = $_POST['userID'];
$targetBucket = "MY BUCKET";
$s3 = S3Client::factory(array(
'key' => "MY KEY",
'secret' => "MY SECRET KEY"
));
$s3->copyObject(array(
'Bucket' => $targetBucket,
'Key' => $targetKeyname,
'CopySource' => "{$sourceBucket}/{$sourcename}",
));
?>
EDITING TO ADD
I've been running test after test. If I hard code the variables into the PHP file it works. If I hard code the variables into the JS script it fails. The ajax is running the php file it's just not passing the variables to it. I've tried with and without the ISSET on the PHP side it just fails to take in the variables each time.
Any ideas?
I suspect this is your issue var userID = USERS USER ID; I'm not sure where that information is coming from. But without seeing the html/js where that is derived from, its difficult to determine the problem.
If its text input with an id of userID, it should be something like:
<input type="text" name="userID" id="userID">
js:
var userID = $("#userID").val();
AJAX Update
merge BOTH data into one object:
data: { userID : userID },
data: { filename : filename },
will become
data: { userID : userID , filename : filename },
I found out what the issue was. It was Firebase. Firebase was returning the values I required however the PHP script was retrieving an error from AWS. I had assumed that the AJAX was failing to pass the value on but I was only half right. the AJAX failed because it didn't have the value at that point. I added in a 3 second delay before the upload occurs and it works fine. Firebase was just slower than the PHP.

Categories