JavaScript:上传文件...无需文件

64

我想模拟文件上传,而不是使用用户的文件输入。文件内容将从字符串动态生成。

这种方法可行吗? 有人以前做过吗?是否有示例或理论支持?

需要澄清的是,我知道如何使用 AJAX 技术通过隐藏 iframe 等方式上传文件 - 问题是上传不在表单中的文件。

我正在使用 ExtJS,但 jQuery 也可以,因为 ExtJS 可以与其集成 (ext-jquery-base)。


如果您可以控制服务器端,那么这似乎不是解决问题的正确方法。如果文件内容将从字符串生成,为什么不只是POST该字符串并在服务器上创建文件(使用PHP或其他语言)?如果您要上传文件到第三方目标,则忽略此评论。 - Jonathan Julian
@JonathanJulian,不管怎样,这个用例都有一种真正的黑客价值的气味 -), 很棒的技巧! - JWL
7个回答

47
如果您不需要支持旧版浏览器,可以使用FormData对象,该对象是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);

所有当前浏览器(IE10+)都支持文件API


2
我避免编写自己的XMLHttpRequests。这绝对是我的首选答案! - Chris
1
这应该是被接受的答案 - 我花了8个小时梳理各种帖子,这就是有效的解决方案,并且代码非常简洁。 - domoarigato

36

为什么不直接使用带有POST的XMLHttpRequest()

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));
}

服务器上的处理程序只是将文件数据写入文件。

编辑
文件上传仍然是一个HTTP POST请求,但内容类型不同。您可以使用此内容类型并使用分界符将内容分开:

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);
}
如果您想发送附加数据,则只需使用边界分隔每个部分,并为每个部分描述content-disposition和content-type标头。每个标头由换行符分隔,正文则由额外的换行符与标头分隔。自然地,以这种方式上传二进制数据会更加困难 :-)
进一步编辑:忘记提到,请确保任何边界字符串不在您要发送的文本“文件”中,否则它将被视为边界。

因为服务器不会将其识别为已上传的“文件”。 - LiraNuna
@LiraNuna:如果您是从字符串生成内容,那为什么这很重要呢?它不能只将其识别为字符串并写入吗? - Andy E
1
这将需要我更改服务器端代码,而在我的情况下是不可能的(远程服务)。 - LiraNuna
1
@Andy:没关系,我也不得不读几遍RFC才让它正常工作! - LiraNuna
1
只有两个小修正(我发现一个Web应用程序不接受上述格式):Contenty-Type 应该有一个大写的“T”,而“body”的最后一行应该在开头再加两个破折号:+ '--' + boundary + '--'; - sowdust
显示剩余2条评论

13

分享最终结果,它能够正常工作-并且具有添加/删除参数的干净方式,而无需硬编码任何内容。

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
    }
});

该方案已在所有现代浏览器上进行了测试,包括但不限于:

  • IE6+
  • FF 1.5+
  • Opera 9+
  • Chrome 1.0+
  • Safari 3.0+

+1 不错的解决方案。但是我认为你的算法有些问题。为什么要在参数对象中使用 for in?它似乎准备处理多个文件,但第二个文件在对象中如何命名?actionoverwritedestination 在哪里使用?它们如何不破坏 for in 中的代码? - Mariano Desanze
@Protron:我使用 for( in ) 的原因是为了从描述对象中获取键。代码将检测是否在嵌套对象上设置了 filename(描述要上传的文件)。其他参数(overwriteactiondestination)只是作为表单传递的额外参数。 - LiraNuna
2
@LiraNuna,我看到你们所有人都对-----------------------------感到神奇,但 MIME 规范(参见 RFC 1341,第7.2.1节)唯一的要求是边界以--开头,后跟有效令牌(参见 RFC 1341 第4节)。希望这能帮助其他人了解他们的自由 :-) - JWL
2
嗨,这段代码不太正确。 Content-Length 的计算有误 - 它没有在数组连接中包括 '\r\n'。 此外,从技术上讲,这并没有正确地处理边界。它应该最初是 '--boundary',然后在部分之间是 'boundary',最后是 'boundary--'。 通过这些修复,对我来说在 Tomcat/JBoss 上似乎工作正常。 干得好 :-) - Swannie

7
文件上传只是一个带有正确编码的文件内容和特殊的multipart/formdata头部的POST请求。您需要使用,因为浏览器安全性禁止直接访问用户磁盘。
由于您不需要读取用户磁盘,所以可以使用JavaScript来模拟。这将只是一个XMLHttpRequest。要伪造“真实”的上传请求,您可以安装Fiddler并检查您的传出请求。
您需要正确地编码该文件,因此此链接可能非常有用:RFC 2388:从表单返回值:multipart/form-data。

那个请求应该放什么内容呢?协议是如何定义的?如何伪造它? - LiraNuna
那不是一个协议,它只是一个普通的HTTP请求;我更新了我的答案。 - Rubens Farias
我没有使用Fiddler(我是Linux用户),但Firebug确实展示了它应该是什么样子。这让我更接近目标了。我会点赞因为这很有帮助,但还没有选择答案。 - LiraNuna

6

使用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
});

4

我刚用Firefox的TamperData插件捕获了这个POST_DATA字符串。我提交了一个带有一个名为“myfile”的type="file"字段和一个名称为“btn-submit”的提交按钮,值为“上传”的表单。上传文件的内容如下:

Line One
Line Two
Line Three

所以这里是POST_DATA字符串:

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

我不确定这个数字的含义(192642264827446),但是找出来应该不难。


我重新格式化了POST_DATA以使其更易于阅读,192642264827446看起来像一个边界标记。 - John La Rooy
谢谢,gnibbler。是的,我想这可能是一个边界标记,可能只是一些随机数。 - Tom Bartel
2
是的,这是一个边界标记。如果您检查 multipart/form-data 标头,边界将跟随它。结尾处的随机数是为了避免与正在发送的数据发生冲突。 - Andy E

3

https://dev59.com/FHI95IYBdhLWcg3wsQFm#2198524对我很有帮助,只需要在有效载荷中的最后一个boundary前添加额外的'--'即可:

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 + '--';

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接