文档

§处理文件上传

§使用 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>

}

现在定义 upload 操作

import java.nio.file.Paths;
import play.libs.Files.TemporaryFile;
import play.mvc.Controller;
import play.mvc.Http;
import play.mvc.Result;

public class HomeController extends Controller {

  public Result upload(Http.Request request) {
    Http.MultipartFormData<TemporaryFile> body = request.body().asMultipartFormData();
    Http.MultipartFormData.FilePart<TemporaryFile> picture = body.getFile("picture");
    if (picture != null) {
      String fileName = picture.getFilename();
      long fileSize = picture.getFileSize();
      String contentType = picture.getContentType();
      TemporaryFile file = picture.getRef();
      file.copyTo(Paths.get("/tmp/picture/destination.jpg"), true);
      return ok("File uploaded");
    } else {
      return badRequest().flashing("error", "Missing file");
    }
  }
}

getRef() 方法为您提供了一个对 TemporaryFile 的引用。这是 Play 处理文件上传的默认方式。

最后,添加一个 POST 路由

POST  /          controllers.HomeController.upload(request: Request)

注意:空文件将被视为没有上传任何文件。如果 multipart/form-data 文件上传部分的 filename 标头为空,即使文件本身不为空,也会出现这种情况。

§测试文件上传

您还可以为 upload 操作编写一个自动化的 JUnit 测试

@Test
public void testFileUpload() throws IOException {
  File file = getFile();
  Http.MultipartFormData.Part<Source<ByteString, ?>> part =
      new Http.MultipartFormData.FilePart<>(
          "picture",
          "file.pdf",
          "application/pdf",
          FileIO.fromPath(file.toPath()),
          Files.size(file.toPath()));

  Http.RequestBuilder request =
      Helpers.fakeRequest()
            .uri(routes.MyController.upload().url())
          .method("POST")
          .bodyRaw(
              Collections.singletonList(part),
              play.libs.Files.singletonTemporaryFileCreator(),
              app.asScala().materializer());

  Result result = Helpers.route(app, request);
  String content = Helpers.contentAsString(result);
    assertThat(content, CoreMatchers.equalTo("File uploaded"));
}

基本上,我们创建了一个 Http.MultipartFormData.FilePart,它是 RequestBuilder 方法 bodyMultipart 所需的。除此之外,其他一切与 单元测试控制器 相同。

§直接文件上传

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

public Result upload(Http.Request request) {
  File file = request.body().asRaw().asFile();
  return ok("File uploaded");
}

§编写自定义的多部分文件部件主体解析器

MultipartFormData 指定的多部分上传将来自请求的上传数据放入 TemporaryFile 对象中。可以使用 DelegatingMultipartFormDataBodyParser 类覆盖此行为,以便将 Multipart.FileInfo 信息流式传输到另一个类。

public static class MultipartFormDataWithFileBodyParser
    extends BodyParser.DelegatingMultipartFormDataBodyParser<File> {

  @Inject
  public MultipartFormDataWithFileBodyParser(
      Materializer materializer,
      play.api.http.HttpConfiguration config,
      HttpErrorHandler errorHandler) {
    super(
        materializer,
        config.parser().maxMemoryBuffer(), // Small buffer used for parsing the body
        config.parser().maxDiskBuffer(), // Maximum allowed length of the request body
        config.parser().allowEmptyFiles(),
        errorHandler);
  }

  /** Creates a file part handler that uses a custom accumulator. */
  @Override
  public Function<Multipart.FileInfo, Accumulator<ByteString, FilePart<File>>>
      createFilePartHandler() {
    return (Multipart.FileInfo fileInfo) -> {
      final String filename = fileInfo.fileName();
      final String partname = fileInfo.partName();
      final String contentType = fileInfo.contentType().getOrElse(null);
      final File file = generateTempFile();
      final String dispositionType = fileInfo.dispositionType();

      final Sink<ByteString, CompletionStage<IOResult>> sink = FileIO.toPath(file.toPath());
      return Accumulator.fromSink(
          sink.mapMaterializedValue(
              completionStage ->
                  completionStage.thenApplyAsync(
                      results ->
                          new Http.MultipartFormData.FilePart<>(
                              partname,
                              filename,
                              contentType,
                              file,
                              results.getCount(),
                              dispositionType))));
    };
  }

  /** Generates a temp file directly without going through TemporaryFile. */
  private File generateTempFile() {
    try {
      final Path path = Files.createTempFile("multipartBody", "tempFile");
      return path.toFile();
    } catch (IOException e) {
      throw new IllegalStateException(e);
    }
  }
}

在此,pekko.stream.javadsl.FileIO 类用于创建接收器,该接收器将来自 Accumulator 的 ByteString 发送到 java.io.File 对象,而不是 TemporaryFile 对象。

使用自定义文件部件处理程序还意味着可以注入行为,因此可以将上传字节的运行计数发送到系统中的其他位置。

§清理临时文件

上传文件使用 TemporaryFile API,该 API 依赖于将文件存储在临时文件系统中,可以通过 getRef() 方法访问。所有 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 数据库


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