Javascript: Uploading a file... without a file
Asked Answered
B

7

65

I am trying to fake a file upload without actually using a file input from the user. The file's content will be dynamically generated from a string.

Is this possible? Have anyone ever done this before? Are there examples/theory available?

To clarify, I know how to upload a file using AJAX techniques using a hidden iframe and friends - the problem is uploading a file that is not in the form.

I am using ExtJS, but jQuery is feasible as well since ExtJS can plug into it (ext-jquery-base).

Beardsley answered 4/2, 2010 at 9:18 Comment(2)
This seems like the wrong solution to your problem (if you have control of the server-side). If the file's content will be generated from a string, why not just POST that string and create the file on the server (using PHP or whatever)? If you are uploading a file to a 3rd party destination, then ignore this comment.Toweling
@JonathanJulian, no matter what, this usecase smells of real hack-value -), awesome trick!Vaginal
W
36

Why not just use XMLHttpRequest() with POST?

function beginQuoteFileUnquoteUpload(data)
{
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "http://www.mysite.com/myuploadhandler.php", true);
    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xhr.onreadystatechange = function ()
    {
        if (xhr.readyState == 4 && xhr.status == 200)
            alert("File uploaded!");
    }
    xhr.send("filedata="+encodeURIComponent(data));
}

The handler script at the server just writes the file data to a file.

EDIT
File upload is still a http post with a different content type. You can use this content type and separate your content with boundaries:

function beginQuoteFileUnquoteUpload(data)
{
    // Define a boundary, I stole this from IE but you can use any string AFAIK
    var boundary = "---------------------------7da24f2e50046";
    var xhr = new XMLHttpRequest();
    var body = '--' + boundary + '\r\n'
             // Parameter name is "file" and local filename is "temp.txt"
             + 'Content-Disposition: form-data; name="file";'
             + 'filename="temp.txt"\r\n'
             // Add the file's mime-type
             + 'Content-type: plain/text\r\n\r\n'
             + data + '\r\n'
             + boundary + '--';

    xhr.open("POST", "http://www.mysite.com/myuploadhandler.php", true);
    xhr.setRequestHeader(
        "Content-type", "multipart/form-data; boundary="+boundary

    );
    xhr.onreadystatechange = function ()
    {
        if (xhr.readyState == 4 && xhr.status == 200)
            alert("File uploaded!");
    }
    xhr.send(body);
}

If you want to send additional data, you just separate each section with a boundary and describe the content-disposition and content-type headers for each section. Each header is separated by a newline and the body is separated from the headers by an additional newline. Naturally, uploading binary data in this fashion would be slightly more difficult :-)

Further edit: forgot to mention, make sure whatever boundary string isn't in the text "file" that you're sending, otherwise it will be treated as a boundary.

Wellinformed answered 4/2, 2010 at 9:29 Comment(7)
Because the server will not recognize it as an uploaded 'file'.Beardsley
I think he wants to know how to generate data.Rheotropism
@LiraNuna: Why does that matter if you're generating the content from a string? Can't it just recognize it as a string and write it?Wellinformed
That would require me to change server-side code, which is in my case, impossible (remote service).Beardsley
Maybe I'm doing something bad, but I just can't get the server to recognize the request as valid POST.Beardsley
@Andy: It's okay, I had to read the RFC several times to get it working!Beardsley
Just two minor corrections (I found a web application not accepting the above format): Contenty-Type should have a capital "T" and the last line of "body" should have two more dashes at the beginning: + '--' + boundary + '--';Merla
A
49

If you don't need support for older browsers, you can use the FormData Object, which is part of the File API:

const formData = new FormData();
const blob = new Blob(['Lorem ipsum'], { type: 'plain/text' });
formData.append('file', blob, 'readme.txt');

const request = new XMLHttpRequest();
request.open('POST', 'http://example.org/upload');
request.send(formData);

File API is supported by all current browsers (IE10+)

Ayah answered 4/4, 2014 at 9:32 Comment(2)
I avoid writing my own XMLHttpRequests. This is definitely my preferred answer!Outsert
This should be the accepted answer - I spent 8 hours combing through various posts, and this is what worked, and in very few lines of code.Bahia
W
36

Why not just use XMLHttpRequest() with POST?

function beginQuoteFileUnquoteUpload(data)
{
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "http://www.mysite.com/myuploadhandler.php", true);
    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xhr.onreadystatechange = function ()
    {
        if (xhr.readyState == 4 && xhr.status == 200)
            alert("File uploaded!");
    }
    xhr.send("filedata="+encodeURIComponent(data));
}

The handler script at the server just writes the file data to a file.

EDIT
File upload is still a http post with a different content type. You can use this content type and separate your content with boundaries:

function beginQuoteFileUnquoteUpload(data)
{
    // Define a boundary, I stole this from IE but you can use any string AFAIK
    var boundary = "---------------------------7da24f2e50046";
    var xhr = new XMLHttpRequest();
    var body = '--' + boundary + '\r\n'
             // Parameter name is "file" and local filename is "temp.txt"
             + 'Content-Disposition: form-data; name="file";'
             + 'filename="temp.txt"\r\n'
             // Add the file's mime-type
             + 'Content-type: plain/text\r\n\r\n'
             + data + '\r\n'
             + boundary + '--';

    xhr.open("POST", "http://www.mysite.com/myuploadhandler.php", true);
    xhr.setRequestHeader(
        "Content-type", "multipart/form-data; boundary="+boundary

    );
    xhr.onreadystatechange = function ()
    {
        if (xhr.readyState == 4 && xhr.status == 200)
            alert("File uploaded!");
    }
    xhr.send(body);
}

If you want to send additional data, you just separate each section with a boundary and describe the content-disposition and content-type headers for each section. Each header is separated by a newline and the body is separated from the headers by an additional newline. Naturally, uploading binary data in this fashion would be slightly more difficult :-)

Further edit: forgot to mention, make sure whatever boundary string isn't in the text "file" that you're sending, otherwise it will be treated as a boundary.

Wellinformed answered 4/2, 2010 at 9:29 Comment(7)
Because the server will not recognize it as an uploaded 'file'.Beardsley
I think he wants to know how to generate data.Rheotropism
@LiraNuna: Why does that matter if you're generating the content from a string? Can't it just recognize it as a string and write it?Wellinformed
That would require me to change server-side code, which is in my case, impossible (remote service).Beardsley
Maybe I'm doing something bad, but I just can't get the server to recognize the request as valid POST.Beardsley
@Andy: It's okay, I had to read the RFC several times to get it working!Beardsley
Just two minor corrections (I found a web application not accepting the above format): Contenty-Type should have a capital "T" and the last line of "body" should have two more dashes at the beginning: + '--' + boundary + '--';Merla
B
13

Just sharing the final result, which works - and has clean way of adding/removing parameters without hardcoding anything.

var boundary = '-----------------------------' +
            Math.floor(Math.random() * Math.pow(10, 8));

    /* Parameters go here */
var params = {
    file: {
        type: 'text/plain',
        filename: Path.utils.basename(currentTab.id),
        content: GET_CONTENT() /* File content goes here */
    },
    action: 'upload',
    overwrite: 'true',
    destination: '/'
};

var content = [];
for(var i in params) {
    content.push('--' + boundary);

    var mimeHeader = 'Content-Disposition: form-data; name="'+i+'"; ';
    if(params[i].filename)
        mimeHeader += 'filename="'+ params[i].filename +'";';
    content.push(mimeHeader);

    if(params[i].type)
        content.push('Content-Type: ' + params[i].type);

    content.push('');
    content.push(params[i].content || params[i]);
};

    /* Use your favorite toolkit here */
    /* it should still work if you can control headers and POST raw data */
Ext.Ajax.request({
    method: 'POST',
    url: 'www.example.com/upload.php',
    jsonData: content.join('\r\n'),
    headers: {
        'Content-Type': 'multipart/form-data; boundary=' + boundary,
        'Content-Length': content.length
    }
});

This was tested to work on all modern browsers, including but not limited to:

  • IE6+
  • FF 1.5+
  • Opera 9+
  • Chrome 1.0+
  • Safari 3.0+
Beardsley answered 6/2, 2010 at 21:29 Comment(4)
+1 Nice solution. But I think is something wrong with your algorithm. Why you use a for in for the params object? It seams like it's prepared for more than one file but the second file how will be named in the object? Where are action, overwrite, and destination used? and how they not break the code inside the for in?Eldridgeeldritch
@Protron: The reason I use for( in ) is to get the keys from the description object. The code will detect if filename is set on a nested object (that describes a file to upload). The other parameters (overwrite, action, destination) are just extra parameters passed as if you used a form.Beardsley
@LiraNuna, I see all you guys getting all magical about the -----------------------------, the only requirement by the MIME spec (see RFC 1341, sec 7.2.1) is that the the boundary commence with --followed by a valid token (see RFC 1341 sec.4). Hope this helps others know their freedom too :-)Vaginal
Hi, this code isn't quite correct. Content-Length is incorrectly calculated - it doesn't include the '\r\n' in the array join. Also technically this doesn't do the boundary correctly. It should be '--boundary' initially, then 'boundary' between parts and 'boundary--' afterwards. With these fixes it appears to work OK for me against Tomcat/JBoss. Great work :-)Scaly
O
7

A file upload it's just a POST request with that file content properly encoded and with an special multipart/formdata header. You need to use that <input type=file /> because your browser security forbid you to access user disk directly.

As you don't need to read user disk, YES, you can fake it using Javascript. It will be just a XMLHttpRequest. To forge an "authentic" upload request, you can install Fiddler and inspect your outgoing request.

You'll need to encode that file correctly, so this link can be very useful: RFC 2388: Returning Values from Forms: multipart/form-data

Oodles answered 4/2, 2010 at 9:35 Comment(3)
What should go in that request then? how is that protocol defined? how to fake it?Beardsley
that isn't a protocol, it's just a regular HTTP request; I updated my answerOodles
I didn't use Fiddler (Linux user here), but Firebug does show how it should look. This brings me one step closer. I am upvoting as it is helpful, but not yet selecting the answer.Beardsley
E
6

Easy way to imitate "fake" file upload with jQuery:

var fd = new FormData();
var file = new Blob(['file contents'], {type: 'plain/text'});

fd.append('formFieldName', file, 'fileName.txt');

$.ajax({
  url: 'http://example.com/yourAddress',
  method: 'post',
  data: fd,
  processData: false,        //this...
  contentType: false         //and this is for formData type
});
Ekg answered 15/11, 2016 at 7:50 Comment(0)
A
4

I just caught this POST_DATA string with the Firefox TamperData addon. I submitted a form with one type="file" field named "myfile" and a submit button named "btn-submit" with value "Upload". The contents of the uploaded file are

Line One
Line Two
Line Three

So here is the POST_DATA string:

-----------------------------192642264827446\r\n
Content-Disposition: form-data;    \n
name="myfile"; filename="local-file-name.txt"\r\n
Content-Type: text/plain\r\n
\r\n
Line \n
One\r\n
Line Two\r\n
Line Three\r\n
\r\n
-----------------------------192642264827446\n
\r\n
Content-Disposition: form-data; name="btn-submit"\r\n
\r\n
Upload\n
\r\n
-----------------------------192642264827446--\r\n

I'm not sure what the number means (192642264827446), but that should not be too hard to find out.

Alurta answered 4/2, 2010 at 9:53 Comment(3)
I reformatted the POST_DATA to make it easier to read, the 192642264827446 looks like a boundary markerLigation
Thanks, gnibbler. Yeah, I thought it might be something like a boundary marker, probably just some random number.Alurta
Yeah, it's a boundary marker. If you check the multipart/form-data header, the boundary will follow it. The random number at the end is to avoid any conflictions with the data being sent.Wellinformed
S
3

https://mcmap.net/q/297026/-javascript-uploading-a-file-without-a-file worked for me, after I added an extra '--' before the final boundary in the payload:

var body = '--' + boundary + '\r\n'
         // Parameter name is "file" and local filename is "temp.txt"
         + 'Content-Disposition: form-data; name="file";'
         + 'filename="temp.txt"\r\n'
         // Add the file's mime-type
         + 'Content-type: plain/text\r\n\r\n'
         + data + '\r\n'
         + '--' + boundary + '--';
Sectionalism answered 25/1, 2014 at 10:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.