Загрузка файлов
Картинка к посту, аватар, документ — всё это пользователь отправляет на сервер загрузкой файла. Для этого форма использует особый формат 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/uploadsHTTP/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.pngHTTP/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.