Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Загрузка файлов

Картинка к посту, аватар, документ — всё это пользователь отправляет на сервер загрузкой файла. Для этого форма использует особый формат multipart/form-data: тело запроса делится на части, и каждая часть — отдельное поле или файл. Javalin разбирает такой запрос сам, а файл отдаёт объектом UploadedFile. Сделаем загрузку картинок для блога.

Форма для файла

В HTML у формы должен быть атрибут enctype, а у поля — тип file:

<!-- src/main/resources/public/upload.html -->
<form method="post" action="/uploads" enctype="multipart/form-data">
    <input type="file" name="image" accept="image/*">
    <button type="submit">Загрузить</button>
</form>

Страница откроется по адресу http://localhost:8080/upload.html: файлы из public отдаются как статические.

Контроллер загрузок

// src/main/java/com/example/blog/UploadController.java
package com.example.blog;
 
import io.javalin.http.BadRequestResponse;
import io.javalin.http.Context;
import io.javalin.http.HttpStatus;
import io.javalin.http.UploadedFile;
 
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
 
public class UploadController {
    private static final Set<String> ALLOWED = Set.of(".png", ".jpg", ".jpeg", ".gif", ".webp");
 
    private final Path dir;
 
    public UploadController(Path dir) {
        this.dir = dir;
    }
 
    public void upload(Context ctx) throws IOException {
        UploadedFile file = ctx.uploadedFile("image");
        if (file == null) {
            throw new BadRequestResponse("Нет файла в поле image");
        }
        String name = save(file);
        ctx.status(HttpStatus.CREATED).json(Map.of("url", "/uploads/" + name, "size", file.size()));
    }
 
    private String save(UploadedFile file) throws IOException {
        String ext = file.extension().toLowerCase();
        if (!ALLOWED.contains(ext)) {
            throw new BadRequestResponse("Можно загружать только картинки: " + ALLOWED);
        }
        String name = UUID.randomUUID() + ext;
        try (InputStream in = file.content()) {
            Files.copy(in, dir.resolve(name));
        }
        return name;
    }
}

ctx.uploadedFile("image") возвращает файл из поля image или null. У UploadedFile есть filename(), extension() (вместе с точкой), size(), contentType() и content() — поток с содержимым.

Почему не своё имя файла

Имя файла присылает клиент, и доверять ему нельзя: в нём может быть ../../Main.java или имя чужого файла. Поэтому сервер проверяет расширение по списку разрешённых и придумывает своё имя через UUID — случайный уникальный идентификатор. Так файл не перезапишет другой и не выйдет за пределы папки.

Подключение, лимит и раздача

// src/main/java/com/example/blog/Main.java
Path uploadDir = Path.of("uploads");
Files.createDirectories(uploadDir);   // main теперь объявляет throws IOException
var uploads = new UploadController(uploadDir);
 
// внутри Javalin.create(config -> { ... })
config.jetty.multipartConfig.maxFileSize(2, SizeUnit.MB);   // io.javalin.config.SizeUnit
config.staticFiles.add(files -> {
    files.hostedPath = "/uploads";
    files.directory = "uploads";
    files.location = Location.EXTERNAL;
});
 
// в блоке apiBuilder
post("/uploads", uploads::upload);

По умолчанию размер файла не ограничен. maxFileSize запрещает файлы больше 2 МБ, а maxTotalRequestSize ограничивает весь запрос. Папку uploads рядом с проектом Javalin раздаёт как статические файлы с диска (Location.EXTERNAL) по адресу /uploads/....

Проверка

Ключ -F отправляет запрос multipart/form-data, а @ перед именем означает «взять файл с диска»:

curl -i -F "image=@cat.png" http://localhost:8080/uploads
HTTP/1.1 201 Created
Content-Type: application/json

{"url":"/uploads/ac9f97fd-b61c-45c2-a2f9-be28abc9ce3a.png","size":70}

Файл доступен по ссылке из ответа:

curl -I http://localhost:8080/uploads/ac9f97fd-b61c-45c2-a2f9-be28abc9ce3a.png
HTTP/1.1 200 OK
Content-Type: image/png
Cache-Control: max-age=0

Текстовый файл получит 400 и текст Можно загружать только картинки: [.gif, .jpg, .jpeg, .webp, .png]. Несколько файлов из одного поля читает ctx.uploadedFiles("images") — он возвращает список, который обходят циклом.

Частая ошибка

Если файл больше лимита, Javalin не может разобрать запрос. Клиент получает ошибку или обрыв соединения (curl: (56) Recv failure: Connection was reset), а в логе сервера:

WARN io.javalin.Javalin - Uncaught exception
jakarta.servlet.ServletException: org.eclipse.jetty.http.BadMessageException: 400: bad multipart
...
Caused by: java.lang.IllegalStateException: max file size exceeded: 2097152

Число — лимит в байтах: 2 МБ. Если ошибки нет, а uploadedFile всё равно возвращает null, проверьте enctype="multipart/form-data" в форме: без него браузер отправит только имя файла, и ответ будет Нет файла в поле image.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.