| Дата | Порт HTTP | Порт PostgreSQL на хосте | Имя сервиса |
|---|---|---|---|
11.09.2026 |
8080 |
5433 |
astral-docs |
REST API на Go для хранения, получения и выборочной передачи доступа к документам. Проект выполнен в рамках задания ГК Астрал.
Приложение:
- регистрирует пользователей по admin token и создаёт пользовательские сессии;
- принимает JSON-документы и файлы через multipart form;
- хранит JSON и метаданные в PostgreSQL, файлы в локальном каталоге;
- проверяет права владельца, публичный доступ и список
grant; - кеширует успешные ответы GET/HEAD в памяти и инвалидирует их после изменений;
- поддерживает миграции, автоматические проверки API и тесты кеша.
Оглавление
- Go
1.26.1или новее для локального запуска, сборки и тестов. - Стандартный
net/httpиhttp.ServeMux. - PostgreSQL 17,
pgx/v5, генерация запросов черезsqlc. bcryptдля паролей,zerologдля логирования.- Docker Engine / Docker Desktop и Docker Compose v2 для PostgreSQL, миграций и контейнерного запуска приложения.
- Bash,
curlиjqдляcheck.shи примеров ниже.
Для запуска приложения целиком через Docker устанавливать Go на компьютер не требуется.
Команды рассчитаны на Linux, macOS или WSL. Docker должен быть запущен. Для go test -race требуется поддерживаемая платформа и доступный C-компилятор.
Сгенерированный код SQL-запросов находится в репозитории. Для обычного запуска устанавливать sqlc не требуется.
| Путь | Назначение |
|---|---|
cmd/app/main.go |
Точка входа |
internal/server/ |
Запуск HTTP-сервера, маршруты и graceful shutdown |
internal/handlers/ |
HTTP-обработчики, модели запросов и ответов |
internal/service/ |
Авторизация, проверки прав и операции с документами |
internal/database/queries/ |
SQL-запросы для sqlc |
internal/database/sqlc/ |
Сгенерированные методы и модели БД |
internal/database/schema.sql |
Схема для генерации sqlc |
internal/storage/ |
Сохранение, чтение и удаление файлов |
internal/cache/ |
Кеш в памяти и его тесты |
internal/config/ |
Загрузка и проверка конфигурации |
internal/logs/ |
Настройка логирования |
migrations/ |
Миграции создания и отката таблиц |
sqlc.yaml |
Конфигурация генератора запросов |
check.sh |
Сборка, запуск сервера и проверка API |
data/files/ |
Загруженные файлы; каталог создаётся при запуске |
Dockerfile |
Сборка и запуск приложения в контейнере |
.dockerignore |
Исключение локальных данных из контекста сборки |
docker-compose.yaml |
PostgreSQL, миграции и приложение |
internal/middleware/ |
Логирование HTTP-запросов |
Taskfile.yml |
Быстрые команды запуска, сборки, тестирования и Docker |
bruno/ |
Различные сценарии для проверки работы через Bruno |
Все команды выполняются из корня проекта.
Создать конфиг из примера при первом запуске:
cp document-cache-api_config.cfg.example document-cache-api_config.cfgПоднять PostgreSQL и применить миграции:
docker compose up -d postgres
docker compose run --rm migrate upЗапустить приложение:
go run ./cmd/appAPI доступен по адресу http://localhost:8080/api. Проверить работу можно через curl или коллекцию запросов в папке bruno. Для Bruno создайте окружение с переменными url (http://localhost) и port (8080); после авторизации заполните token, а для операций с конкретным документом document_id.
Запрос /api/docs без токена вернёт 401: для чтения документов нужна сессия. Для остановки сервера нажмите Ctrl+C.
При локальном запуске PostgreSQL работает в Docker, а Go-сервер запускается командой go run ./cmd/app. Для запуска всего приложения в контейнерах используется профиль app, описанный ниже.
mkdir -p bin
go build -o bin/document-cache-api ./cmd/app
./bin/document-cache-apiПосле создания конфига, при остановленном HTTP-сервере:
chmod +x check.sh
./check.shСкрипт сам поднимет PostgreSQL, применит миграции, соберёт и запустит приложение, выполнит проверки и остановит свой процесс сервера. Подробности в разделе Тестирование.
Для быстрых команд используется Task.
task --listОсновные команды:
task dev— PostgreSQL, миграции и локальный запуск приложения;task run— запуск только локального сервера;task build— сборка бинарника;task fmt— форматирование кода;task verify— go vet, тесты с детектором гонок и проверка API;task generate— генерация sqlc;task docker-up— сборка и запуск проекта в Docker;task docker-stop— остановка контейнера приложения;task docker-down— остановка и удаление контейнеров с сохранением данных;task docker-logs— просмотр логов приложения.
Для task dev нужен локальный конфиг или переменные окружения.
Перед task verify остановите HTTP-сервер: скрипт проверки запускает собственный процесс.
Для контейнерного запуска нужны Docker и Docker Compose v2. Устанавливать Go локально и создавать cfg-файл не требуется.
Перед запуском остановите локальный HTTP-сервер, чтобы освободить порт 8080.
docker compose --profile app up -d --buildCompose дожидается готовности PostgreSQL, применяет миграции и запускает приложение после их успешного завершения.
Проверить состояние контейнеров и посмотреть логи:
docker compose --profile app ps -a
docker compose logs -f appКонтейнер migrate со статусом Exited (0) завершился успешно. API доступен по адресу http://localhost:8080.
curl -i http://localhost:8080/api/docsОтвет 401 ожидаем: для получения документов нужна пользовательская сессия. Примеры curl из этого README подходят для контейнерного запуска.
Настройки передаются через environment сервиса app в docker-compose.yaml. Локальный cfg-файл в образ не копируется и на контейнер не влияет.
Приложение подключается к БД по адресу postgres:5432. Для подключения к этой же БД с компьютера используется localhost:5433.
По умолчанию admin token равен local-dev-admin-token. Чтобы изменить его:
DOC_CACHE_AUTH_ADMIN_TOKEN='my-local-admin-token' \
docker compose --profile app up -d --buildИспользуются два именованных тома:
postgres_dataхранит PostgreSQL;document_filesхранит загруженные файлы приложения.
Тома сохраняются при пересоздании контейнеров. Кеш находится в памяти процесса и очищается при перезапуске приложения.
Локальный сервер и контейнер используют одну БД Compose, но разные файловые хранилища. Локальные файлы из data/files автоматически в том document_files не переносятся. При переключении режима старые записи файлов остаются в БД, но для чтения их содержимого нужен перенос соответствующих файлов. JSON-документы доступны в обоих режимах.
Остановить контейнеры с сохранением данных:
docker compose --profile app downПосле изменения Go-кода пересобрать приложение:
docker compose --profile app up -d --buildПри добавлении новых миграций к уже работающей установке:
docker compose stop app
docker compose run --rm migrate up
docker compose --profile app up -d --buildМиграции запускаются отдельной утилитой; Go-сервер самостоятельно их не выполняет.
Команда docker compose --profile app down -v удаляет тома вместе с данными. Для обычной остановки параметр -v не нужен.
Скрипт проверяет локально собранное приложение, поэтому перед его запуском нужно остановить контейнер сервера:
docker compose stop app
./check.shДля скрипта по-прежнему нужны локальный Go, cfg-файл или переменные окружения. PostgreSQL может продолжать работать в Docker.
Сначала читается document-cache-api_config.cfg в рабочем каталоге. Если его нет, используются переменные окружения процесса. При наличии cfg-файла переменные окружения не переопределяют его значения.
После загрузки задаются значения по умолчанию и проверяются обязательные параметры. database.dsn и auth.admin_token должны быть заполнены. Демонстрационные значения есть в примерах, но не подставляются кодом автоматически.
Формат файла — TOML:
[server]
host = "0.0.0.0"
port = 8080
read_timeout = "30s"
write_timeout = "30s"
idle_timeout = "120s"
shutdown_timeout = "10s"
[log]
level = "info"
dir = "log"
pretty = false
to_file = false
caller = false
[database]
dsn = "postgres://document_cache:document_cache@localhost:5433/document_cache?sslmode=disable"
[auth]
admin_token = "local-dev-admin-token"
session_ttl = "24h"
[storage]
dir = "data/files"
max_upload_size = 10485760
[cache]
max_entries = 1000
max_bytes = 67108864| Параметр | Назначение |
|---|---|
server.*_timeout |
Таймауты чтения, записи, keep-alive и остановки сервера |
log.pretty |
Человекочитаемые логи вместо JSON |
log.to_file |
Дополнительная запись логов в файл с ротацией |
log.caller |
Добавление имени файла и строки вызова |
auth.session_ttl |
Срок действия сессии, по умолчанию 24 часа |
storage.max_upload_size |
Лимит всего multipart-запроса, включая поля и файл; по умолчанию 10 МиБ |
cache.max_entries |
Максимальное количество записей; по умолчанию 1000 |
cache.max_bytes |
Максимальный суммарный размер тел ответов; по умолчанию 64 МиБ |
Относительные пути считаются от рабочего каталога процесса. Таймауты, срок сессии и лимиты должны быть положительными; нулевые значения перечисленных числовых настроек заменяются значениями по умолчанию.
Альтернатива cfg-файлу. Убедитесь, что document-cache-api_config.cfg отсутствует, затем загрузите пример в окружение Bash:
set -a
source ./document-cache-api_config.env.example
set +a
go run ./cmd/appФайлы .env и *_config.env самостоятельно не загружаются приложением. Полный список переменных с префиксом DOC_CACHE_ находится в document-cache-api_config.env.example.
Compose создаёт пользователя и базу document_cache с демонстрационным паролем document_cache при первой инициализации пустого тома.
| Откуда выполняется подключение | Адрес PostgreSQL |
|---|---|
| Локальный Go-сервер, DBGate, psql | localhost:5433 |
| Контейнер миграций в сети Compose | postgres:5432 |
Данные PostgreSQL сохраняются в именованном томе postgres_data. Изменение POSTGRES_USER, POSTGRES_PASSWORD или POSTGRES_DB не перенастраивает уже инициализированный том.
| Таблица | Содержимое |
|---|---|
users |
UUID пользователя, уникальный логин, хеш пароля, дата создания |
sessions |
Хеш токена, пользователь и срок действия сессии |
documents |
Владелец, имя, MIME, признаки file/public, JSON, ключ файла и размер |
document_grants |
Пользователи, которым выдан доступ к документу |
schema_migrations |
Служебная таблица утилиты миграций |
Применить изменения:
docker compose run --rm migrate upПроверить версию:
docker compose run --rm migrate versionПосле начальной миграции ожидается версия 1. Сообщение no change при повторном up означает, что новых миграций нет. Команда version сама миграции не применяет.
Откатить начальную миграцию:
docker compose run --rm migrate down 1Откат удалит таблицы приложения и их данные. Файлы документов на диске при этом не удаляются.
Go-сервер самостоятельно миграции не запускает. При локальном запуске они применяются отдельной командой, а при контейнерном запуске последовательность запуска обеспечивает Compose.
Остановить все сервисы, включая приложение, с сохранением томов:
docker compose --profile app downПосле изменения запросов:
sqlc generateПри изменении структуры БД обновите миграции и internal/database/schema.sql. Сгенерированные файлы в internal/database/sqlc/ вручную не редактируются.
| Метод | Путь | Назначение | Где передаётся токен |
|---|---|---|---|
| POST | /api/register |
Регистрация пользователя | Поле формы token, admin token |
| POST | /api/auth |
Создание сессии | Не требуется; поля login и pswd |
| POST | /api/docs |
Загрузка документа | token внутри multipart-поля meta |
| GET, HEAD | /api/docs |
Список документов | Query-параметр token |
| GET, HEAD | /api/docs/{id} |
Получение документа | Query-параметр token |
| DELETE | /api/docs/{id} |
Удаление документа | Query-параметр token |
| DELETE | /api/auth/{token} |
Завершение сессии | Параметр пути |
Успешные операции возвращают 200. JSON-ответы используют общую модель: response для подтверждения действия, data для содержимого, error для ошибки. Неиспользуемые поля отсутствуют. Файловый GET возвращает исходные байты с указанным MIME.
Пример ошибки:
{ "error": { "code": 403, "text": "document access denied" } }| Статус | Значение |
|---|---|
200 |
Операция выполнена |
400 |
Некорректные параметры, занятый логин или превышен лимит загрузки |
401 |
Неверные учётные данные, admin token или недействующая сессия |
403 |
Нет права чтения или удаления документа |
404 |
Документ не найден |
405 |
Метод не поддерживается для известного маршрута; заголовок Allow содержит разрешённые методы |
500 |
Внутренняя ошибка |
Для отсутствующего документа используется 404 как дополнительный статус к перечисленным в задании. Неизвестные маршруты обслуживаются стандартным ServeMux и могут возвращать текстовый 404.
| Параметр | Поведение |
|---|---|
login |
Коллекция выбранного владельца; без параметра показываются свои документы |
key, value |
Передаются вместе; точное совпадение по выбранному полю |
limit |
От 1 до 1000, по умолчанию 100 |
Поддерживаемые key: id, name, mime, file, public. Для boolean-фильтров используйте true или false. Другие поля отклоняются с 400. Сортировка по имени и дате создания по возрастанию, затем по ID. Дата в ответе отображается в UTC в формате 2006-01-02 15:04:05.
Команды выполняются последовательно в одном Bash-терминале при запущенном сервере. Для извлечения токенов и ID используется jq. Логины примера нужно регистрировать один раз; при повторном запуске используйте существующие учётные записи или другие имена.
BASE_URL='http://localhost:8080'
ADMIN_TOKEN='local-dev-admin-token'curl -i "$BASE_URL/api/register" \
--data-urlencode "token=$ADMIN_TOKEN" \
--data-urlencode 'login=exampleuser1' \
--data-urlencode 'pswd=Examplepass1!'
curl -i "$BASE_URL/api/register" \
--data-urlencode "token=$ADMIN_TOKEN" \
--data-urlencode 'login=exampleuser2' \
--data-urlencode 'pswd=Examplepass2!'Ответ первой регистрации:
{ "response": { "login": "exampleuser1" } }SESSION_TOKEN="$(curl -fsS "$BASE_URL/api/auth" \
--data-urlencode 'login=exampleuser1' \
--data-urlencode 'pswd=Examplepass1!' | jq -er '.response.token')"
SECOND_SESSION_TOKEN="$(curl -fsS "$BASE_URL/api/auth" \
--data-urlencode 'login=exampleuser2' \
--data-urlencode 'pswd=Examplepass2!' | jq -er '.response.token')"META="$(jq -cn --arg token "$SESSION_TOKEN" '{
name: "note.json",
file: false,
public: false,
token: $token,
mime: "application/json",
grant: ["exampleuser2"]
}')"
curl -i "$BASE_URL/api/docs" \
--form-string "meta=$META" \
--form-string 'json={"title":"Заметка","text":"Документ для второго пользователя"}'Ответ содержит переданный JSON в data.json. Чтобы создать публичный документ, задайте public: true. Для приватного документа без общего доступа используйте public: false и grant: [].
printf 'Document Cache API example\n' > /tmp/document-cache-example.txt
META="$(jq -cn --arg token "$SESSION_TOKEN" '{
name: "sample.txt",
file: true,
public: false,
token: $token,
mime: "text/plain",
grant: []
}')"
curl -i "$BASE_URL/api/docs" \
--form-string "meta=$META" \
--form 'file=@/tmp/document-cache-example.txt'Ожидаемый ответ:
{ "data": { "file": "sample.txt" } }curl -i -G "$BASE_URL/api/docs" \
--data-urlencode "token=$SESSION_TOKEN"
curl -i -G "$BASE_URL/api/docs" \
--data-urlencode "token=$SESSION_TOKEN" \
--data-urlencode 'key=name' \
--data-urlencode 'value=note.json' \
--data-urlencode 'limit=10'Загрузка не возвращает ID, поэтому получаем его через список. Имена документов не уникальны; здесь выбирается первая найденная запись:
DOCUMENT_ID="$(curl -fsS -G "$BASE_URL/api/docs" \
--data-urlencode "token=$SESSION_TOKEN" \
--data-urlencode 'key=name' \
--data-urlencode 'value=note.json' | jq -er '.data.docs[0].id')"
FILE_ID="$(curl -fsS -G "$BASE_URL/api/docs" \
--data-urlencode "token=$SESSION_TOKEN" \
--data-urlencode 'key=name' \
--data-urlencode 'value=sample.txt' | jq -er '.data.docs[0].id')"curl -i -G "$BASE_URL/api/docs/$DOCUMENT_ID" \
--data-urlencode "token=$SESSION_TOKEN"
curl -I "$BASE_URL/api/docs/$DOCUMENT_ID?token=$SESSION_TOKEN"GET возвращает {"data":{...}}. HEAD возвращает статус и заголовки, включая размер тела GET в Content-Length, но без самого тела.
curl -fsS -G "$BASE_URL/api/docs/$FILE_ID" \
--data-urlencode "token=$SESSION_TOKEN" \
-o /tmp/document-cache-downloaded.txt
cmp /tmp/document-cache-example.txt /tmp/document-cache-downloaded.txtcurl -i -G "$BASE_URL/api/docs" \
--data-urlencode "token=$SECOND_SESSION_TOKEN" \
--data-urlencode 'login=exampleuser1'
curl -i -G "$BASE_URL/api/docs/$DOCUMENT_ID" \
--data-urlencode "token=$SECOND_SESSION_TOKEN"Заметка доступна через grant. Приватный sample.txt второму пользователю недоступен: его получение вернёт 403.
curl -i -X DELETE -G "$BASE_URL/api/docs/$DOCUMENT_ID" \
--data-urlencode "token=$SESSION_TOKEN"Ответ: {"response":{"<id>":true}}. Повторное удаление и получение этого документа возвращают 404.
Удалить созданный файл:
curl -i -X DELETE -G "$BASE_URL/api/docs/$FILE_ID" \
--data-urlencode "token=$SESSION_TOKEN"curl -i -X DELETE "$BASE_URL/api/auth/$SESSION_TOKEN"
curl -i -X DELETE "$BASE_URL/api/auth/$SECOND_SESSION_TOKEN"После logout прежний токен возвращает 401 на защищённых маршрутах, даже если ответ есть в кеше. Повторный logout с корректно сформированным токеном возвращает 200.
Логин содержит не менее 8 символов: латинские буквы и цифры. Пароль содержит не менее 8 символов, буквы в верхнем и нижнем регистрах, цифру и специальный символ. Ограничение bcrypt: не более 72 байт пароля.
Пароли сохраняются как bcrypt-хеши. Session token состоит из 32 случайных байт в hex-представлении; в БД хранится только SHA-256 от строки токена. Каждый login создаёт отдельную сессию, предыдущие остаются действующими до истечения срока или logout. Истёкшие сессии отклоняются при проверке, автоматическая очистка их строк не реализована.
| Пользователь | Чтение | Удаление |
|---|---|---|
| Владелец | Да | Да |
Авторизованный пользователь при public=true |
Да | Нет |
Пользователь из grant |
Да | Нет |
| Другой пользователь приватного документа | Нет | Нет |
| Без действующей сессии | Нет | Нет |
grant содержит логины существующих пользователей. Дубли логинов исключаются. Неизвестный логин в grant приводит к 400 без создания документа. Права задаются при загрузке; отдельного метода их изменения нет.
Без login список содержит свои документы. Чтобы получить доступные документы другого владельца, нужно передать его login.
Кеш хранит подготовленные успешные ответы в map под защитой sync.RWMutex. Для списка ключ включает ID пользователя и параметры запроса, для документа — ID пользователя и документа. Токен в ключ не входит.
Перед каждым обращением к кешу сессия проверяется в PostgreSQL. При попадании запросы за документами и чтение файлов не выполняются. GET и HEAD используют одну запись; если первым пришёл HEAD, ответ подготавливается полностью для кеша, но тело клиенту не отправляется.
| Заголовок | Значение |
|---|---|
X-Cache: MISS |
Ответ подготовлен заново |
X-Cache: HIT |
Ответ получен из памяти |
Cache-Control: no-store |
Запрет внешнего кеширования клиентом и прокси; внутреннему кешу приложения не мешает |
TTL не используется. Запись хранится до инвалидации, вытеснения или перезапуска сервера. При нехватке места вытесняются произвольные записи. Ответ больше общего лимита памяти не сохраняется. Лимит байтов учитывает тела ответов, а не всю память процесса.
После вызова сервиса загрузки сбрасываются все списки. После вызова сервиса удаления сбрасываются списки и копии документа для всех пользователей. Сброс выполняется и при ошибке сервиса, в том числе на случай неопределённого результата операции с БД.
При инвалидации увеличивается generation. Запрос, начавший чтение до сброса, не сможет сохранить старый результат после него. Уже выполняющийся запрос может завершиться со старым ответом, но не вернёт его обратно в кеш.
Logout не очищает ответы: проверка сессии блокирует доступ с отозванным токеном. Новый login того же пользователя может использовать сохранившийся кеш.
Не требуют PostgreSQL или запущенного сервера:
go test -race -count=1 -v ./internal/cacheПроверяются сохранение ответа, разделение ключей, инвалидация списков и документов, запрет записи старой версии, ограничения размера и количества записей, учёт памяти при замене, отключённый кеш и параллельные операции.
./check.shСкрипт выполняет 21 группу проверок. Поднимает PostgreSQL, применяет миграции, собирает бинарник и запускает собственный процесс сервера. Проверяет регистрацию, login, загрузку JSON и файла, права, фильтры, GET/HEAD, кеш, удаление, logout и JSON-ответы 405.
Если по адресу уже работает HTTP-сервер, скрипт завершается с ошибкой. Перед запуском нужен настроенный cfg-файл или окружение. Unit-тесты скрипт не запускает.
Настройки обращения скрипта:
BASE_URL=http://localhost:8080 \
ADMIN_TOKEN=local-dev-admin-token \
APP_PACKAGE=./cmd/app \
./check.shAPP_PACKAGE можно не указывать: скрипт найдёт единственный пакет main. BASE_URL и ADMIN_TOKEN должны соответствовать конфигу сервера; сами по себе эти переменные его конфиг не меняют.
При успехе выводится PASS. При ошибке возвращается ненулевой код и сохраняется каталог диагностики. Скрипт останавливает свой сервер, удаляет тестовые документы и завершает сессии; при ошибке выполняет очистку по возможности. PostgreSQL остаётся работать. Три созданных пользователя сохраняются в БД, а уникальные логины позволяют повторять запуск.
Проверка использует текущую локальную БД. Скрипт проверяет недоступность удалённого файла через API, но не проверяет его физическое отсутствие на диске. HEAD проверяется через curl по статусу и заголовкам, без отдельного анализа сырых HTTP-байтов. Истечение сессии по времени и отказы БД автоматически не воспроизводятся.
go test -race -count=1 ./...
go vet ./...
go build ./cmd/app- JSON хранится в
jsonb, поэтому порядок ключей и форматирование могут отличаться от исходного текста. Файлы возвращаются без изменения байтов. - JSON-документ имеет
storage_key = NULL. Для файла в БД хранится серверный ключ, а содержимое находится вdata/files/. - Документ и grants создаются в одной транзакции. При ошибке до начала commit сохранённый файл удаляется. При неопределённом результате commit файл остаётся, чтобы не потерять содержимое потенциально сохранённого документа.
- При удалении сначала удаляется запись БД, затем файл. Если очистка файла не удалась, ошибка записывается в лог, а API возвращает успех: документ уже недоступен. Автоматическая очистка оставшихся файлов не реализована.
- Кеш рассчитан на один экземпляр приложения и изменения через API. Ручные изменения БД или файлов не инвалидируют его; после таких изменений нужно перезапустить сервер.
- Завершение по SIGINT/SIGTERM даёт активным запросам время закончиться в пределах
shutdown_timeout. - HTTP-middleware логирует метод, шаблон маршрута, статус, длительность обработки и HIT/MISS при наличии. Токены, параметры URL и тела запросов в эти записи не включаются.
- Подключение к PostgreSQL не устанавливается. Проверьте
docker compose ps, наличие запущенного PostgreSQL и DSN. Для локального сервера нужен порт5433, внутри Compose —5432. database.dsn is required/auth.admin_token is required. Создайте cfg-файл из примера или передайте обязательные переменные окружения.- Изменения env не влияют на приложение. При наличии cfg-файла настройки читаются из него. Env-файл нужно явно загрузить в окружение процесса.
relation ... does not exist. Примените миграции черезdocker compose run --rm migrate up.no migrationпри проверке версии. В выбранной БД ещё не применена начальная миграция. Выполнитеup, затемversion.login is already taken. Пользователь уже зарегистрирован. Выполните login или выберите новый логин.invalid or expired session. Проверьте переменную токена. После logout или истечения срока выполните login заново.- В чужой коллекции пустой список. У пользователя нет доступа к документам выбранного владельца. Нужен
public=trueили его логин вgrant. invalid multipart form or upload size limit exceeded. Проверьте формат multipart и лимит всего запроса, включая служебные поля.check.shсообщает, что сервер уже отвечает. Остановите вручную запущенное приложение: скрипт запускает собственный бинарник.