2014 年 2 月 25 日
教程:文件上传详解
将文件上传到 Web 服务器可能是一个棘手的过程,因为很多取决于 Web 服务器的功能、能力和限制。人们可能认为存在一种“标准”上传方法,但不幸的是,情况并非如此。运行 PHP 的 Apache 服务器处理事情的方式可能与运行 Visual Basic 或 C# 的 .NET 服务器截然不同。即使在共享通用语言的服务器家族中,规则也会因服务器和上传脚本而异。
您还必须非常小心允许哪些文件上传到您的 Web 服务器。由于您正在创建一个任何人都可以通过 URL 运行的脚本 — 并有效地向您的服务器发送数据 — 除非您谨慎小心,否则您可能会使您的服务器受到黑客攻击。使用服务器端脚本很容易允许使用恶意代码覆盖关键文件,因此如果您不了解服务器端的操作,您可能需要考虑其他选项。此外,您的服务器可能会对您的上传能力施加超出您控制范围的限制。例如,许多服务器将上传限制为小文件(小于 2 MB),或者它们可能会限制可以上传的文件数量。在开始实施文件上传过程之前,必须考虑所有这些限制。
开始之前
在继续本教程之前,您应该阅读本手册,以更清楚地了解整个过程。本教程介绍了一种“快速而粗略”的文件上传方法,但在您将其用于生产之前,您必须进行一些调整并根据需要保护脚本。您应该预料到需要付出一些努力才能做好这件事!
使用“network.upload()”
Corona 的 network.*
库包含一个名为 network.upload() 的 API,这是一种将文件上传到服务器的简单方法,前提是您的服务器使用 HTTP PUT
处理简单的文件上传方法。大多数现有的接受文件上传的脚本可能不适用于 network.upload(),因为它们正在寻找基于 HTTP POST
表单的 MIME 多部分上传格式。Corona 的 network.upload() API 不与这些类型的脚本通信,但我们稍后会进一步讨论。首先,让我们看看 Corona 方面...
Corona 脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
-- 处理生成的上传事件的回调函数。 -- 将会有几个事件:一个指示过程的开始和结束, -- 以及几个指示进度的事件(取决于文件大小)。 -- 始终测试您的错误情况! local function uploadListener( event ) if ( event.isError ) then print( "网络错误。" ) -- 这很可能是超时或服务器关闭。换句话说, -- 它无法与 Web 服务器通信。现在,如果 -- 与 Web 服务器的连接正常,但请求错误,则 -- 这将为 false,您需要查看 event.status 和 event.response -- 以了解 Web 服务器为什么未能执行您想要的操作。 else if ( event.phase == "began" ) then print( "上传开始" ) elseif ( event.phase == "progress" ) then print( "正在上传... 已传输的字节数 ", event.bytesTransferred ) elseif ( event.phase == "ended" ) then print( "上传结束..." ) print( "状态:", event.status ) print( "响应:", event.response ) end end end -- 指定要上传到的 PHP 脚本的 URL。在您自己的服务器上执行此操作。 -- 还要将方法定义为“PUT”。 local url = "http://yourwebserver.com/somepath/upload.php" local method = "PUT" -- 为上传过程设置一些合理的参数 local params = { timeout = 60, progress = true, bodyType = "binary" } -- 指定要上传的文件以及上传的位置。 -- 此外,设置文件的 MIME 类型,以便服务器知道预期内容。 local filename = "image.jpg" local baseDirectory = system.ResourceDirectory local contentType = "image/jpeg" -- 另一个选项是 "text/plain" -- 没有使用 HTTP PUT 告诉远程主机该文件名称的标准方法。 -- 我们将在这里创建自己的标头,以便我们的 PHP 脚本 -- 期望寻找该标头并提供文件的名称。您的 PHP 脚本 -- 需要“强化”,因为这是一种安全风险。例如,有人 -- 可能会传入一个路径名,该路径名可能会尝试将任意文件写入您的 -- 服务器,并使用恶意代码覆盖关键系统文件。 -- 不要假设“这不会发生在我身上!”因为它很可能发生。 local headers = {} headers.filename = filename params.headers = headers network.upload( url , method, uploadListener, params, filename, baseDirectory, contentType ) |
脚本解密…
与所有 network.*
API 调用一样,此操作是异步的,这意味着它会立即返回到您的程序并在后台处理上传。但是,您需要知道上传的状态,特别是当它完成时,这由事件监听器函数处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
local function uploadListener( event ) if ( event.isError ) then print( "网络错误。" ) -- 这很可能是超时或服务器宕机导致的。 -- 换句话说,它无法与 Web 服务器通信。 -- 如果与 Web 服务器的连接正常,但请求错误,则此 -- 将为“false”,您需要查看“event.status”和“event.response” -- 以了解为什么 Web 服务器未能处理您的请求。 else if ( event.phase == "began" ) then print( "上传开始" ) elseif ( event.phase == "progress" ) then print( "正在上传... 已传输的字节数 ", event.bytesTransfered ) elseif ( event.phase == "ended" ) then print( "上传结束..." ) print( "状态:", event.status ) print( "响应:", event.response ) end end end |
让我们更详细地检查此函数。您必须检查的第一件事是是否发生网络错误。这在 event.isError
属性中返回。在网络编程中,重要的是要理解此错误可能表示服务器已关闭、无法访问或超时。实际上,这意味着您从未与服务器成功通信。
您可以成功连接到 Web 服务器并与之交互,要求它执行它无法执行的操作,但它会在 isError
属性方面报告“成功”。换句话说,无论服务器向您发送“正确”还是“错误”的响应,从技术上讲,您都与它进行了成功的交易。因此,您可以在 else
条件块中检查和处理上传的各个阶段。例如,在 "began"
阶段,您可以选择显示 widget.newProgressView()。然后在 "progress"
阶段,根据传输的字节数增加该小部件的状态。最后,"ended"
阶段会通知您 Web 服务器已完成上传过程。
全部完成,对吗?没那么快!该文件可能仍然由于各种原因而上传失败,例如,文件太大,文件名无效,或者服务器上的某些权限问题阻止了文件上传。因此,您应该检查 event.status
属性,该属性将保存 HTTP “结果”代码,例如 201(成功)或 403(权限被拒绝)。根据服务器的不同,event.response
属性中可能存在其他信息,可以指示问题所在。
现在,让我们重新检查 network.upload() API 调用所需的各种变量和表。这些包括
- 要执行的服务器脚本的 URL。
- HTTP 方法(在本例中,我们将使用“PUT”)。
- 一些用于配置 API 调用的参数,包括超时、主体类型以及是否需要进度更新。
- 要上传的文件名。
- 可以找到该文件的源目录。
- 一个 MIME 类型字符串,指示服务器使用的文件类型。
- PHP 脚本的自定义标头(我们将在后面进一步讨论)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
-- 指定要上传到的 PHP 脚本的 URL。在您自己的服务器上执行此操作。 -- 还要将方法定义为“PUT”。 local url = "http://yourwebserver.com/somepath/upload.php" local method = "PUT" -- 为上传过程设置一些合理的参数 local params = { timeout = 60, progress = true, bodyType = "binary" } -- 指定要上传的文件以及上传的位置。 -- 此外,设置文件的 MIME 类型,以便服务器知道预期内容。 local filename = "image.jpg" local baseDirectory = system.ResourceDirectory local contentType = "image/jpeg" -- 另一个选项是 "text/plain" local headers = {} headers.filename = filename params.headers = headers |
由于此方法无法告知服务器要保存的文件名,因此我们必须创建一个名为 filename
的特殊标头,其中将包含该名称(我们要在远程服务器上使用的文件)。完成此步骤后,将 headers
表添加到 params
表,然后调用 network.upload()。
1 2 |
network.upload( url , method, uploadListener, params, filename, baseDirectory, contentType ) |
PHP 脚本
为了在本教程中使用,我们提供了一个 PHP 脚本示例供下载。此脚本非常简单明了,但除非您了解 PHP,否则它看起来会令人望而生畏。在尝试为自己的项目修改它之前,有几件重要的事情需要理解。
1. 安全性
当您在线编写脚本时,您有 100% 的责任确保它不易被黑客入侵。将文件写入服务器的脚本最容易受到攻击。您可能认为只有您的应用会使用它,但一旦它在网站上,任何人都可以执行它,找出参数和方法,并找到漏洞加以利用。我们已经采取了一些基本步骤来防止这种情况,但您需要完成剩下的步骤。此脚本允许调用者设置文件名,这本身就很危险,因为人们可以输入技巧来创建虚假的路径,这可能会让他们将文件写入任意位置。您的脚本绝不应以提升的权限运行,并且您的文件系统在任何存在可执行脚本文件或系统二进制可执行文件的地方都应为只读。最后,您应该尽最大努力在将此代码投入生产之前清理任何提供的文件名。最安全的方法是阻止调用者指定文件名,但这可能会在保持文件井井有条方面产生可用性问题。
2. 了解服务器的限制
许多 PHP 服务器,如果允许上传,都会对可以上传的文件大小设置非常严格的限制。上面的代码有一个大小限制检查,但它只有在服务器允许更大的文件并且您想要限制大小时才起作用。在此示例中,我们允许最大 5 MB 的文件,但服务器本身可能只允许 1 MB。如果文件太大,服务器甚至可能无法访问您的脚本,因此您有责任控制限制。许多网站与其他网站共享环境,您应该成为一个好的网络公民!
3. 处理同名文件
上面的代码使用一种简单(但有些缺陷)的方法,即将递增的序列号添加到字符串的末尾。它会在 100 时停止递增数字。检查重复文件名的 while
循环不能永远运行,并且在您点击 100 次同名上传后,它将开始覆盖第 100 个文件。同样,这不是可用于生产的脚本,您必须根据自己的需求进行调整。另一种潜在的方法是获取文件列表,找到具有最大数字的文件,从数字中解析名称,然后对其进行递增。
4. 上传目录
此脚本假设您将创建一个名为 upload 的文件夹,作为此脚本所在位置的子文件夹,但这可能不是您网站的最佳选择。此上传文件夹必须具有您的 Web 服务器运行的 ID 的写入权限。通常,Web 服务器不会使用您的帐户有权访问的 ID 或组内运行。这意味着该文件夹需要对 World 用户具有可写权限。作为安全预防措施,您可能不希望该文件夹对 World 用户具有 READ 或 EXECUTE/LIST 权限。最后,您的服务器可能需要将文件转储到服务器树中的完全不同的位置,因此您必须找出该路径并相应地调整 PHP 脚本。
总结
如您所见,上传文件的行为可能是一项复杂的任务。希望本教程阐明了这一过程。与往常一样,请在下面留下您的问题和评论。如果您希望将其用作您自己实现的基础,您也可以下载基于本教程的 PHP 脚本。
develephant
发布于 2 月 25 日 12:41这真是超越了职责范围!感谢您抽出时间分享这些内容,罗伯。
是否有任何特殊原因导致 network.upload() 不支持基于 POST 表单的 MIME 多部分上传格式?
最好。
内森
发布于 2 月 26 日 14:02是否有适用于 asp/aspx 的等效项?
谢谢,
内森。
罗伯·米拉克
发布于 2 月 26 日 15:30我不是 ASP 程序员,所以我不知道从哪里开始。
内森
发布于 2 月 26 日 15:53好的,酷。
乔纳森
发布于 2 月 27 日 15:44你好,内森,
如果您对 MVC 类型架构有更好的理解,我建议您查看 Microsoft 上的 Web API 文档,因为它利用了接受 Web 请求的 MVC 结构。如果不是,我建议您研究 WCF Web 服务,因为它们非常通用,您可以创建接受几乎任何类型的数据(例如自定义类)的 Web 服务,并允许您对其执行任何您想做的事情。
这是 Web API 的一个很好的起点——在我看来,它非常易于使用,也很棒! http://www.asp.net/web-api/overview/getting-started-with-aspnet-web-api/tutorial-your-first-web-api
至于从 Corona 传递到 Web 服务的数据,无论它是来自 Corona 还是来自网页的 jQuery,都无关紧要。Web API 或 WCF 只关心它接收到的数据,并且在 HTTP 请求/响应(GET、POST、PUT、DELETE)方面始终是标准的。您可以在 WCF Web 服务中轻松创建自定义类型,其中包含您从 JSON 请求中作为该“类型”数组传递的属性。有很多途径可以探索!
希望这有帮助!
伊恩
发布于 2 月 28 日 00:22你好,内森,
几周前我发布了一个关于使用ASP.Net上传文件的帖子,当时我使用了network.request和HTTP “POST”。我一直在以此为基础进行我最近的项目,现在,我使用json.encode进行编码,然后在.Net端进行解码,可以一次上传多个文件,效果很好。
http://forums.coronalabs.com/topic/44199-example-file-upload-using-aspnet-and-a-progress-bar/
有人知道这种方法是否不再合适吗?我们应该使用PUT和network.upload代替吗…?
谢谢,
伊恩
Kerem
发布于 16:20, 2 月 28 日Rob,非常感谢您这个非常有用的教程!非常及时。
Pat
发布于 16:20, 4 月 8 日感谢您的教程。但是我在iOS 7.1设备(iphone 5S)上使用 event.phase 检查时遇到了问题。应用程序似乎忽略了使用 event.phase 的 if-then-elseif 块。其他事件触发器(event.completed)在设备和模拟器上都能触发,但是 event.phase 代码仅在 Corona 模拟器上有效,在 Xcode 模拟器或 iOS 设备上无法执行。 任何帮助将不胜感激!
罗伯·米拉克
发布于 15:45, 4 月 9 日你的参数里有 progress = true 这个标志吗?
Pat
发布于 17:47, 4 月 9 日是的,参数里有 progress = true。该代码块在 Corona 模拟器上按预期工作,并更新 event.phase。我知道脚本执行完毕了,因为我上传的文件确实出现在服务器上,即使是从设备端上传的,只是在设备上 event.phase 条件中实例化的任何显示对象都被忽略了。我尝试在 iOS 模拟器上调试,但是 event.phase 代码块中的所有打印语句也被忽略了。
罗伯·米拉克
发布于 16:31, 4 月 10 日你上传的文件有多大?也许应该在论坛上进一步诊断。
Rob
Vonn
发布于 00:25, 1 月 26 日大家好
我已成功地从Web端上传了一个html文件到web php
但是当我从app端上传到web php时,它却不起作用。我使用的是web html和app端的相同php。
罗伯·米拉克
发布于 15:35, 1 月 26 日本教程解释说,并非所有的web脚本都是一样的。大多数基于PHP的上传脚本都期望使用多部分MIME编码的数据。我们的network.upload()不进行这种类型的上传。所有内容都在“使用network.upload()”标题下的上一段中解释了。
unknown
发布于 05:07, 7 月 30 日嗨,我使用相同的代码来保存摄像头功能拍摄的图像到数据库,但是我的模拟器崩溃了
Josep
发布于 03:27, 9 月 27 日嗨,
有没有办法使用流进行保存? 我想将麦克风的音频直接保存到云端,这是一个非常大的音频文件(假设是10小时的连续音频)。
使用描述的技术,我必须首先在移动设备上有一个文件,然后再上传。由于麦克风生成的是未压缩的音频格式,在许多情况下它可能会非常大…
为了加载和播放大型音频,我们可以使用audio.loadStream,但是我需要类似audio.saveStream的东西。
谢谢!
罗伯·米拉克
发布于 15:51, 9 月 27 日您或许可以使用Corona Enterprise来实现,但是Corona SDK没有类似的功能。
Rob
Alex Poon
发布于 01:37, 10 月 30 日我最近将脚本移动到了另一个服务器,发现此脚本无法正常工作。我已经更改了路径,但是我收到的错误消息与路径无关。我不知道下一步该怎么做。希望有人可以帮忙。
当我调用同一脚本时,我收到一个html错误400,显示以下内容
“由于安全规则,您尝试访问的页面受到限制。
如果您认为安全规则正在影响您网站的正常运行,请联系您的主机支持团队,并提供有关如何重现此错误的详细说明。
他们将能够帮助您解决问题并在需要时调整安全配置。”
我应该更改哪些服务器设置才能使此上传脚本再次工作?
Alex Poon
发布于 00:54, 11 月 25 日我最终通过更新.htaccess修复了它。如果有人遇到同样的问题,请参阅下面的链接
https://wordpress.org/support/topic/security-error-1
备注
1. 我的上传目录位于wordpress站点下
2. 我正在使用的新服务器是SiteGround
Sem
发布于 01:42, 2 月 26 日如何发送变量?
Sem
发布于 02:12, 2 月 26 日我明白了。
如果我添加这样的头信息:
param.headers[myVar] = “ABC”
…那么在php中,它会出现在:
$_SERVER[“HTTP_MYVAR”]
让我困惑的是,HTTP_会被添加到前面,不知道为什么,也不太在意。 🙂
Mike
发布于 09:43, 7 月 29 日@Rob
有没有什么原因会导致它在Windows Server上无法正常工作?当我在本地MAMP服务器上运行时,它可以正常工作,但是在我的Windows主机服务上却无法正常工作。我收到以下错误
[-1001: 请求超时。] 网络错误。
罗伯·米拉克
发布于 12:26, 7 月 29 日我不知道它为什么在Windows服务器上无法工作,只要它支持HTTP PUT请求。