文件

§處理檔案上傳

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

§撰寫自訂 multipart 檔案部分主體剖析器

MultipartFormData 所指定的 multipart 上傳會從請求中取得上傳資料,並將其放入 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"
  }
}

上述設定會刪除超過 30 分鐘的檔案,使用「olderThan」屬性。它會在應用程式啟動後五分鐘啟動清除器,並在之後每 30 秒檢查一次檔案系統。清除器不知道任何現有的檔案上傳,因此如果系統未仔細設定,可能會在清除器執行時遇到延宕的檔案上傳。

下一步:存取 SQL 資料庫


在此文件發現錯誤?此頁面的原始碼可以在 這裡 找到。閱讀 文件指南 後,請隨時提交拉取請求。有問題或建議要分享?請前往 我們的社群論壇 與社群展開對話。