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).

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. Commented Feb 4, 2010 at 18:00
  • 1
    @JonathanJulian, no matter what, this usecase smells of real hack-value -), awesome trick! Commented Jun 7, 2012 at 13:22

7 Answers 7

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+)

Sign up to request clarification or add additional context in comments.

2 Comments

I avoid writing my own XMLHttpRequests. This is definitely my preferred answer!
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.
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.

7 Comments

Because the server will not recognize it as an uploaded 'file'.
I think he wants to know how to generate data.
That would require me to change server-side code, which is in my case, impossible (remote service).
@Andy: It's okay, I had to read the RFC several times to get it working!
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 + '--';
|
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+

4 Comments

+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?
@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.
@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 :-)
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 :-)
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

3 Comments

What should go in that request then? how is that protocol defined? how to fake it?
that isn't a protocol, it's just a regular HTTP request; I updated my answer
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.
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
});

Comments

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.

3 Comments

I reformatted the POST_DATA to make it easier to read, the 192642264827446 looks like a boundary marker
Thanks, gnibbler. Yeah, I thought it might be something like a boundary marker, probably just some random number.
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.
3

https://stackoverflow.com/a/2198524/2914587 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 + '--';

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.