文档

§处理文件上传

§使用 multipart/form-data 在表单中上传文件

在 Web 应用程序中上传文件的标准方法是使用带有特殊 multipart/form-data 编码的表单,这使您可以将标准表单数据与文件附件数据混合在一起。

注意:用于提交表单的 HTTP 方法必须为 POST(而不是 GET)。

从编写 HTML 表单开始

@helper.form(action = routes.HomeController.upload(), Symbol("enctype") -> "multipart/form-data") {
    
    <input type="file" name="picture">
    
    <p>
        <input type="submit">
    </p>
    
}

除非您已禁用 CSRF 过滤器,否则请向表单添加 CSRF 令牌。CSRF 过滤器按字段列出的顺序检查多部分表单,因此请将 CSRF 令牌放在文件输入字段之前。这提高了效率,并避免了如果文件大小超过 play.filters.csrf.body.bufferSize 则出现令牌未找到错误。

现在使用 multipartFormData 主体解析器定义 upload 操作

def upload: Action[MultipartFormData[Files.TemporaryFile]] = Action(parse.multipartFormData) { request =>
  request.body
    .file("picture")
    .map { picture =>
      // only get the last part of the filename
      // otherwise someone can send a path like ../../home/foo/bar.txt to write to other files on the system
      val filename    = Paths.get(picture.filename).getFileName
      val fileSize    = picture.fileSize
      val contentType = picture.contentType

      picture.ref.copyTo(Paths.get(s"/tmp/picture/$filename"), replace = true)
      Ok("File uploaded")
    }
    .getOrElse {
      Redirect(routes.HomeController.index()).flashing("error" -> "Missing file")
    }
}

ref 属性为您提供对 TemporaryFile 的引用。这是 multipartFormData 解析器处理文件上传的默认方式。

注意:与往常一样,您也可以使用 anyContent 主体解析器,并将其检索为 request.body.asMultipartFormData

最后,添加一个 POST 路由器

POST  /          democontrollers.HomeController.upload()

注意:空文件将与根本没有上传文件一样对待。如果 multipart/form-data 文件上传部分的 filename 标头为空,即使文件本身不为空,也是如此。

§直接文件上传

将文件发送到服务器的另一种方法是使用 Ajax 从表单异步上传文件。在这种情况下,请求主体不会被编码为 multipart/form-data,而只会包含纯文件内容。

在这种情况下,我们可以只使用主体解析器将请求主体内容存储在文件中。对于此示例,让我们使用 temporaryFile 主体解析器

def upload: Action[Files.TemporaryFile] = Action(parse.temporaryFile) { request =>
  request.body.moveTo(Paths.get("/tmp/picture/uploaded"), replace = true)
  Ok("File uploaded")
}

§编写您自己的主体解析器

如果您想直接处理文件上传,而不在临时文件中缓冲它,您可以编写自己的 `BodyParser`。在这种情况下,您将收到数据块,您可以自由地将它们推送到任何您想要的地方。

如果您想使用 `multipart/form-data` 编码,您仍然可以使用默认的 `multipartFormData` 解析器,方法是提供一个 `FilePartHandler[A]` 并使用不同的 Sink 来累积数据。例如,您可以使用 `FilePartHandler[File]` 而不是 `TemporaryFile`,方法是指定一个 `Accumulator(fileSink)`

type FilePartHandler[A] = FileInfo => Accumulator[ByteString, FilePart[A]]

def handleFilePartAsFile: FilePartHandler[File] = {
  case FileInfo(partName, filename, contentType, dispositionType) =>
    val perms       = java.util.EnumSet.of(OWNER_READ, OWNER_WRITE)
    val attr        = PosixFilePermissions.asFileAttribute(perms)
    val path        = JFiles.createTempFile("multipartBody", "tempFile", attr)
    val file        = path.toFile
    val fileSink    = FileIO.toPath(path)
    val accumulator = Accumulator(fileSink)
    accumulator.map {
      case IOResult(count, status) =>
        FilePart(partName, filename, contentType, file, count, dispositionType)
    }(ec)
}

def uploadCustom: Action[MultipartFormData[File]] = Action(parse.multipartFormData(handleFilePartAsFile)) {
  request =>
    val fileOption = request.body.file("name").map {
      case FilePart(key, filename, contentType, file, fileSize, dispositionType, _) =>
        file.toPath
    }

    Ok(s"File uploaded: $fileOption")
}

§清理临时文件

上传文件使用 TemporaryFile API,该 API 依赖于将文件存储在临时文件系统中,可以通过 ref 属性访问。所有 TemporaryFile 引用都来自 TemporaryFileCreator 特性,并且可以根据需要交换实现,现在有一个 atomicMoveWithFallback 方法,如果可用,它将使用 `StandardCopyOption.ATOMIC_MOVE`。

上传文件本质上是一个危险的操作,因为无限制的文件上传会导致文件系统填满 - 因此,TemporaryFile 背后的理念是,它只在完成时处于作用域内,并且应该尽快从临时文件系统中移出。任何未移动的临时文件都会被删除。

但是,在 某些情况下,垃圾回收不会及时发生。因此,还有一个 play.api.libs.Files.TemporaryFileReaper,它可以通过使用 Pekko 调度程序在计划的基础上删除临时文件,这与垃圾回收方法不同。

默认情况下,清理程序处于禁用状态,可以通过配置 `application.conf` 来启用。

play.temporaryFile {
  reaper {
    enabled = true
    initialDelay = "5 minutes"
    interval = "30 seconds"
    olderThan = "30 minutes"
  }
}

上面的配置将使用“olderThan”属性删除超过 30 分钟的文件。它将在应用程序启动后五分钟启动清理程序,并此后每 30 秒检查一次文件系统。清理程序不知道任何现有的文件上传,因此如果系统配置不当,长时间的文件上传可能会遇到清理程序。

下一步: 访问 SQL 数据库


发现此文档中的错误?此页面的源代码可以在 此处 找到。在阅读 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?转到 我们的社区论坛 开始与社区对话。