记住并重新填充文件输入

55

注意:

下面的答案反映了2009年遗留浏览器的状态。现在你可以在2017年通过JavaScript设置文件输入元素的值。

查看这个问题中的答案,了解详细信息和演示:
如何在程序化拖放文件时设置文件输入值?

我有一个网站,允许用户多次上传文件进行处理。目前我只有一个文件输入框,但希望能够记住用户的选择并在屏幕上显示它。

我想知道的是,在用户选择文件后,如何在页面重新加载时记住他们的选择并将文件预先选定在文件输入框中。我只需要知道如何记住和重新填充文件输入框即可。

如果可能,我也可以接受不使用文件输入框的方法。

我正在使用 JQuery。


1
那么,一个用户的路径可以被“记住”,比如注册表、您的税务程序的默认位置等等。这是个好主意,谢天谢地现在不再可能了。 - GitaarLAB
1
请记住,这是他们在文件输入中已经选择的文件。我不是在谈论访问文件系统中的任何文件。 - user1150103
2
是的,你是可以做到的,你只是没有意识到而已。你明确要求:“在页面重新加载时使用预选文件重新显示文件输入”。如果您可以这样做,那么即使使用简单的CSS,任何人都可以欺骗用户上传他们不知道的东西!此外,完整路径是私有的(想象一下有人可以读取您的用户名或路径到您的用户文件夹),不应该在现代浏览器中对JavaScript可访问。您唯一能做的事情就是仅记住文件名...因此相当无用。 - GitaarLAB
1
实际上,即使是你的问题标题也非常清晰:“记住并重新填充文件输入”……这里没有任何误解。没有路径的文件名是无用的(你无法上传/访问文件),而且没有人有权知道我的文件夹结构,所以你没有路径。如果你没有任何关于我的文件路径的权利……那么再次变得不可能甚至重新填充文件输入(即使可以这样做,它也不会知道应该从哪个路径获取文件)等等。 - GitaarLAB
4个回答

229
好的,您想要“记住并重新填充文件输入”,“记住他们的选择并在页面重新加载时使用预选的文件重新显示文件输入”。。
而且在我之前的答案评论中,您表示并不真正接受其他选择:“抱歉,没有Flash和Applets,只有javascript和/或文件输入,可能还有拖放。”
我注意到在浏览(相当多的)重复问题123等)时,几乎所有其他答案都是这样说的:“不,你不能这样做,那将是一个安全问题”,随后可以简单地概述或代码示例来说明安全风险。
然而,像顽固的骡子一样的人(在某种程度上不一定是坏事)可能会把这些答案视为:“不行,因为我这么说”,这确实与“不行,并且这里有不允许它的规格”是不同的。
所以,这是我第三次也是最后一次回答你的问题(我引导你到饮水处,我带你到河边,现在我在推动你去源头,但我不能强迫你喝水)。 编辑3: 实际上,你想做的事情曾经在RFC1867第3.4节中描述/建议过:

VALUE属性可以与<INPUT TYPE=file>标签一起使用来设置默认文件名。这种用法可能依赖于平台。然而,在多个交易序列中使用它可能很有用,例如,避免用户一遍又一遍地提示相同的文件名。

实际上,HTML 4.01规范第17.4.1节指定:

用户代理可以使用value属性的值作为初始文件名。

('用户代理'指的是'浏览器')。

事实上,由于javascript既可以修改又可以提交表单(包括文件输入),而且可以使用css隐藏表单/表单元素(如文件输入),仅凭上述陈述就可能在用户不知情的情况下从用户的计算机上传文件。
显然,这是绝对不能发生的,因此,RFC1867在第8节安全注意事项中声明:

重要的是,用户代理不发送用户没有明确要求发送的任何文件。因此,期望HTML解释代理使用 <INPUT TYPE=file VALUE="yyyy"> 确认可能建议的任何默认文件名。

然而,唯一实现此功能的浏览器(据我所知)是(某些旧版本的)Opera:它接受由 JavaScript 设置的 <input type="file" value="C:\foo\bar.txt> 或值(elm_input_file.value='c:\\foo\\bar.txt';)。
当此文件框在表单提交时未更改,Opera 将弹出一个安全窗口,告知用户将要上传哪些文件到什么位置(url/webserver)。

现在有人可能会认为所有其他浏览器都违反了规范,但这是错误的:因为规范声明:“may”(它没有说“must”)“..使用 value 属性作为初始文件名”。
而且,如果浏览器不接受设置文件输入值(也就是该值只是“只读”的),那么浏览器也不需要弹出这样一个“可怕”和“困难”的安全弹出窗口(如果用户不理解它(和/或被“训练”始终单击“确定”),那么它甚至可能无法起到作用)。

让我们快进到HTML5吧..
这里清除了所有的歧义(但仍需要一些思考):
4.10.7.1.18文件上传状态下,我们可以在账目细节中阅读到以下内容:
  • 值IDL属性处于文件名模式。
    ...
  • 元素的值属性必须省略。
因此,文件输入的值属性必须省略,但它还在某种被称为“文件名”的“模式”下运行,这在4.10.7.4通用输入元素API中有描述:

值IDL属性允许脚本操作输入元素的值。该属性处于以下模式之一,定义其行为:

跳转到 '模式文件名':

在获取时,如果有选定的文件,则返回包括第一个选定文件的文件名和字符串“C:\fakepath\”,否则返回空字符串。在设置时,如果新值为空字符串,则应清空已选文件列表;否则,必须抛出InvalidStateError异常。

让我再重申一遍:如果尝试将文件输入值设置为非空字符串,则必须抛出InvalidStateError异常!!!(但可以通过将其值设置为空字符串来清除输入字段。)

因此,在当前和可预见的 HTML5 未来中(以及过去,除了 Opera),只有用户可以通过浏览器或操作系统提供的“文件选择器”来填充文件输入框。(不能使用 JavaScript 或设置默认值重新填充文件输入框。)

获取文件名/文件路径

假设重新填充文件输入框不是不可能的,那么您需要完整的路径:目录+文件名(+扩展名)。

过去,一些浏览器(最著名的是IE6到IE8)确实会将完整的路径+文件名作为值显示出来:只需在javascript中使用简单的alert( elm_input_file.value );等语句即可。并且浏览器还会在表单提交时将此完整的路径+文件名(+扩展名)发送到接收服务器。
注意:有些浏览器也有'file或fileName'属性(通常发送到服务器),但这显然不包括路径..

这是一个现实的安全/隐私风险:恶意网站(所有者/利用者)可以获得用户主目录的路径(其中个人资料、帐户、cookie、注册表用户部分、历史记录、收藏夹、桌面等位于已知常量位置),当典型的非技术 Windows 用户从 C:\Documents and Settings\[用户名]\My Documents\My Pictures\kinky_stuff\image.ext 上传文件时。
我甚至没有谈到在传输数据时的风险(即使通过https“加密”)或“安全”存储此数据!

因此,越来越多的替代浏览器开始遵循最古老的经过验证的安全措施之一:根据需要共享信息。
绝大多数网站不需要知道文件路径,因此它们只显示文件名(+扩展名)。

到IE8发布时,微软决定跟随竞争对手,并添加了一个名为“上传文件时包括本地目录路径”的URLAction选项,该选项默认情况下在一般Internet区域中设置为“禁用”(在可信区域中设置为“启用”)。

这个变化在一些“优化为IE”的环境中(主要包括自定义代码和专有的“控件”)造成了小混乱:它们无法获取已上传文件的文件名,因为它们被硬编码为期望一个包含完整路径的字符串,并提取最后一个反斜杠(或者如果你很幸运的话,是正斜杠)之后的部分。1, 2 随着HTML5的出现,正如您在上面所读到的,“mode filename”指定了:
在获取时,如果有任何文件被选中,它必须返回以“C:\ fakepath \”开头的第一个文件的文件名的字符串,否则返回空字符串。
并且他们指出:
“fakepath”要求是历史上的悲伤偶然事件。

For historical reasons, the value IDL attribute prefixes the filename with the string "C:\fakepath\". Some legacy user agents actually included the full path (which was a security vulnerability). As a result of this, obtaining the filename from the value IDL attribute in a backwards-compatible way is non-trivial. The following function extracts the filename in a suitably compatible manner:

function extractFilename(path) {
  if (path.substr(0, 12) == "C:\\fakepath\\")
    return path.substr(12); // modern browser
  var x;
  x = path.lastIndexOf('/');
  if (x >= 0) // Unix-based path
    return path.substr(x+1);
  x = path.lastIndexOf('\\');
  if (x >= 0) // Windows-based path
    return path.substr(x+1);
  return path; // just the filename
}
注意:我认为这个函数很愚蠢:整个重点是始终有一个虚假的Windows路径来解析。因此,第一个“if”不仅无用,甚至会引发错误:想象一下使用旧浏览器上传文件的用户,从c:\fakepath\Some folder\file.ext上传文件(因为它将返回:Some folder\file.ext)...
我只会简单地使用:
function extractFilename(s){ 
  // returns string containing everything from the end of the string 
  //   that is not a back/forward slash or an empty string on error
  //   so one can check if return_value===''
  return (typeof s==='string' && (s=s.match(/[^\\\/]+$/)) && s[0]) || '';
} 

(正如HTML5规范明确指出的那样)。

让我们回顾一下(获取路径/文件名):

  • 旧版浏览器(和新版浏览器,如果像IE>=8这样的选项启用了此选项)将显示完整的Windows / Unix路径
  • 较旧的浏览器不会显示任何路径,只显示文件名(+扩展名)
  • 当前/未来/符合HTML5标准的浏览器在获取文件输入的值时始终会在文件名前添加字符串:c:\fakepath\
    除此之外,如果文件输入接受多个文件并且用户选择了多个文件,则它们将仅返回“已选择文件列表”中的第一个文件名。

因此,在最近的过去、现在和可预见的HTML5未来,通常只会得到文件名。

这就带我们来到我们需要检查的最后一件事情:这个“已选择文件列表”/多个文件,这导致我们进入谜题的第三部分:

(HTML5)文件API

首先:'文件API'不应与'文件系统API'混淆,以下是文件系统API的摘要:
该规范定义了一种API来浏览文件系统层次结构,并定义了一种方式,通过该方式用户代理可以向Web应用程序公开用户本地文件系统的沙盒部分。它建立在[FILE-WRITER-ED]之上,后者又建立在[FILE-API-ED]之上,每个都添加了不同类型的功能。
“用户本地文件系统的沙盒部分”已经清楚地表明,无法使用此功能来获取沙盒外的用户文件(因此与问题无关,尽管可以将用户选择的文件复制到持久性本地存储器并重新上传该“副本”使用AJAX等。在上传失败时有用作为“重试”。但它不会是指向可能已在此期间更改的原始文件的指针)。
更重要的是只有Webkit(考虑较旧版本的Chrome)实现了此功能,并且该规范很可能不会继续存在,因为它{{link1:不再积极维护,规范目前被放弃,因为它没有取得任何显著进展}}

让我们继续学习“文件API”,
它的摘要告诉我们:

这份规范提供了一个API,用于在Web应用程序中表示文件对象,以及以编程方式选择它们和访问它们的数据。这包括:
- FileList接口,它表示来自底层系统的单独选择的文件数组。选择的用户界面可以通过``调用,即当输入元素处于文件上传状态[HTML]时。 - Blob接口,它表示不可变的原始二进制数据,并允许对Blob对象内的字节范围进行访问。 - File接口,它包括有关文件的只读信息属性,例如其名称和文件的最后修改时间(在磁盘上)。 - FileReader接口,提供读取File或Blob的方法,并提供事件模型以获取这些读取的结果。 - 用于与二进制数据(例如文件)一起使用的URL方案,以便可以在Web应用程序中引用它们。
因此,FileList可以通过文件模式的输入字段进行填充:``。这意味着关于value属性的所有内容仍然适用!
当输入字段处于文件模式时,它会获得一个只读属性files,这是一个类似数组的FileList对象,引用了输入元素用户选择的文件,并且可以通过FileList接口访问。
我提到过类型为FileListfiles属性是(文件API第5.2节)只读的吗?:

HTMLInputElement接口[HTML]有一个只读属性,类型为FileList...

那么拖放呢?

来自mdn文档 - 使用拖放选择文件

The real magic happens in the drop() function:

function drop(e) {
  e.stopPropagation();
  e.preventDefault();

  var dt = e.dataTransfer;
  var files = dt.files;

  handleFiles(files);
}

Here, we retrieve the dataTransfer field from the event, then pull the file list out of it, passing that to handleFiles(). From this point on, handling the files is the same whether the user used the input element or drag and drop.

因此,(就像输入字段类型为“file”一样),事件的dataTransfer属性具有类似数组的属性files,它是一个类似数组的FileList对象,我们刚刚学习到(上面)FileList是只读的。

FileList包含对用户选择的文件(或在拖放目标上放置的文件)的引用和一些属性。从文件API第7.2节文件属性中,我们可以阅读:

name

文件名;在获取时,必须返回文件名的字符串形式。不同系统有许多文件名变体;这仅是文件名,不包含路径信息。如果用户代理无法提供此信息,则必须返回空字符串。

lastModifiedDate

文件的最后修改日期。在获取时,如果用户代理可以提供此信息,则必须返回一个新的Date[HTML]对象,该对象初始化为文件的最后修改日期。如果未知最后修改日期和时间,则该属性必须返回当前日期和时间作为Date对象。

还有一个size属性:

F.size与fileBits Blob参数的大小相同,该参数必须是F的不可变原始数据。

同样,没有路径,只有只读文件名。

因此:

  • (elm_input||event.dataTransfer).files会得到文件列表对象。
  • (elm_input||event.dataTransfer).files.length会得到选择的文件数量。
  • (elm_input||event.dataTransfer).files[0]是第一个被选择的文件。
  • (elm_input||event.dataTransfer).files[0].name会得到第一个被选择文件的文件名。(同时也是在中返回的value值)

那么对于这个“用于二进制数据的URL方案,例如文件,以便它们可以在Web应用程序中引用”,肯定可以包含对用户所选文件的私有引用,对吗?

File API - A URL for Blob and File reference我们可以学习到:

该规范定义了一种带有以下URL的方案:
blob:550e8400-e29b-41d4-a716-446655440000#aboutABBA。

这些内容存储在一个URL存储器中(浏览器甚至应该有自己的小型HTTP服务器,以便可以在css、img src和甚至XMLHttpRequest中使用这些url)。可以使用以下方法创建这些Blob URL:
  • var myBlobURL=window.URL.createFor(object); 返回一个Blob URL,在第一次使用后会自动撤销。
  • var myBlobURL=window.URL.createObjectURL(object, flag_oneTimeOnly); 返回可重复使用的Blob URL(除非flag_oneTImeOnly评估为true),并且可以使用window.URL.revokeObjectURL(myBlobURL)进行撤销。
然而,URL存储器只在会话期间维护(因此它将在页面刷新时保留,因为它仍然是同一会话),并且在文档卸载时丢失。
来自MDN-使用对象URL
对象URL是标识文件对象的字符串。每次调用window.URL.createObjectURL()时,都会创建一个唯一的对象URL,即使您已经为该文件创建了对象URL。每个URL必须被释放。虽然在文档卸载时它们会自动释放,但如果您的页面动态使用它们,则应通过调用window.URL.revokeObjectURL()来显式释放它们。
这意味着,即使您将Blob URL字符串存储在cookie或持久本地存储中,在新会话中该字符串也将无用!
这应该带我们回到一个完整的循环和最终结论:
不可能(重新)填充输入字段或用户选择的文件(不在浏览器沙盒“本地存储”区域中)。
(除非您强制用户使用过时版本的Opera,或者强制用户使用IE和一些ActiveX编码/模块(实现自定义文件选择器)等)

进一步阅读:
http://www.cs.tut.fi/~jkorpela/forms/file.html
https://developer.mozilla.org/en-US/docs/Using_files_from_web_applications
http://www.html5rocks.com/en/tutorials/file/filesystem/
http://www.html5rocks.com/en/tutorials/file/dndfiles/
http://caniuse.com/filereader
JavaScript:权威指南 - David Flanagan,第22章:“文件系统 API”
如何保存window.URL.createObjectURL()的结果以备将来使用?
Blob会持续多久?
如何解决C:\fakepath的问题?


2
嗨,GitaarLAB,感谢您的回答。我明白您的意思,但您假设目录信息将以纯文本形式存储。如果我在Java中实现文件输入,我会加密目录。然后,我可以序列化和反序列化对象,从而记住和重新填充信息。我也无法更改该数据(如上面的示例)。我理解安全性的需要,但我认为这不会是纯文本。抱歉,没有Flash和Applets,只有JavaScript和/或文件输入,可能还有拖放功能。谢谢。 - user1150103
7
我从未谈论过您如何处理数据的安全性,因此您没有理解我的先前回答和评论,即您不能这样做。另外,为了确保,我想指出javascript与java无关!!! 因此,我稍微修改了我的答案(我恭请您不要删除您的问题,从而删除我放在这个原始答案中的所有工作。同样,对于管理员也有同样谦卑的请求)。 - GitaarLAB
3
谢谢提到HTML5及其新可能性(例如File API)。我之前不知道这些。 - Oliver
4
“所以,你告诉我还有机会!” - MadTurki
不错的文章。我有一些评论/想法。如果我想从拖放事件文件列表中填充input的文件列表,该怎么办?为了在不支持将文件拖放到“input”元素的浏览器中保持用户体验的一致性?我认为这不会带来任何安全风险(我不知道文件位置,这是用户发起的操作等等),但我没有看到实现它的方法。 - n0rd
显示剩余5条评论

1
在你的表单上创建一个输入框。当用户选择文件时,将结果复制到该字段中,类似于:
jQuery('#inFile').change(
 function(){ jQuery('#inCopy').val( jQuery('#inFile').val() ); }
);

实际上,结果并没有完全复制,而是复制了"C:/fakepath/SELECTED_FILE_NAME"。虽然您无法设置文件输入的值,但可以将文本输入字段的值设置为不带"C:/fakepath/"的值,因为服务器准备表格。
现在,当服务器收到表单时,请检查文本输入字段。如果以"C:/fakepath/"开头,则用户必须选择了新文件,因此请上传他们的新选择。如果不是,则用户已选择先前的选择,这应该不是问题,因为根据原始问题,先前的选择已在上传之前完成,并且应该(至少具有适当的编程能力)仍在服务器上。

这仍然无法让我能够保存文件信息并在以后检索它。 - user1150103
我认为没有问题。例如,mySQL不应受到任何影响。 - DaveWalley

0

冒着可能会触碰到GitaarLAB提供的大量信息的风险,我想建议DaveWalley非常接近提供实际解决方案。你们两个都帮了我很多,谢谢。

我的主要收获是:

  1. 文件输入提供了一个单向通道。它坐在那里等待处理您的下一个上传。这就是它所做的几乎全部内容。它只是“接收”。
  2. 在标签旁边放置一个div或只读文本表单输入可以进行“服务”。也就是说,它可以用于告诉用户:“这是当前上传的内容:” - 需要显示的文件名需要从您的服务器端逻辑中填充。

因此,基本用例如下:

  • 用户通过网页上的表单输入上传文件
  • 服务器端逻辑存储文件
  • 在Ajax周期或网页重新加载中,服务器端代码识别出要写入div的fileName字符串,“这是当前上传的内容”
  • 文件输入也可以被重新呈现,以便欢迎用户上传另一个文件/替换文件。
你可能需要做更多的工作来处理“*必填”验证,但这是另一回事。

-1

如果您只需要将文件数据暂时保存,例如页面出现错误或者您想在提交数据到数据库之前先确认一下,那么通常可以将文件放在临时位置,并将数据保存在隐藏字段中。

这不会重新填充文件输入框。但是您也可以将输入文件的名称放在文件输入框旁边。

就像这样:

<input type=hidden name="filename" value="<?php echo $filename; ?>" />
<input type="file" name="uploadfile" size="50" />

<?php if (!empty($filename)) echo $filename; ?>

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