
## Загрузить файл

`POST /v1/infra/servers/:id/upload`

Записывает файл на BLACKHOLE-сервер через агент туннеля. Два варианта источника: встроенное base64-содержимое в теле запроса — до 96 МБ тела, то есть около 72 МБ самого файла, — или URL, с которого агент скачает файл сам, до 500 МБ. На серверах, включённых в rollout надёжной URL-загрузки, агент выполняет до четырёх попыток с backoff в общем бюджете пяти минут. До включения сохраняется прежнее поведение URL-загрузки. Поддерживается автоматическая распаковка архивов `tar.gz` / `tar.bz2` / `zip` — формат определяется по сигнатуре файла (magic bytes) или по расширению URL.

## Параметры

| Параметр | В | Тип | Обяз. | Описание |
|----------|---|-----|:-----:|----------|
| `id` | path | string (UUID) | да | ID BLACKHOLE-сервера, `status: running`, `blackholeStatus: CONNECTED` |

## Поля запроса (body)

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|----------|
| `path` | string | **да** | Путь на сервере, 1–500 символов. Для архивов — куда положить загруженный архив до распаковки |
| `content` | string | да (или `url`) | Base64-содержимое файла. Максимум 96 МБ на тело запроса, то есть около 72 МБ самого файла: base64 тяжелее исходных байт примерно на треть. Тело сверх потолка отклоняется кодом `413 INLINE_SOURCE_TOO_LARGE`. Файл крупнее передавайте полем `url` |
| `url` | string | да (или `content`) | HTTPS-URL, откуда агент скачает файл. До 500 МБ |
| `mode` | string | нет | Права файла в восьмеричном формате: `0644`, `0755`. Применяется к файлу по пути `path` |
| `extract` | boolean | нет | Распаковать архив после загрузки. По умолчанию `false`. Требует `content` или `url` с архивом |
| `extractTo` | string | нет | Директория для распаковки (при `extract: true`). По умолчанию — директория из `path`. Проверяется до начала загрузки: абсолютный путь внутри одного из корней `/opt`, `/tmp`, `/root`, `/var/log`, `/var/lib`, `/etc/systemd`, `/etc/nginx`, не указывающий на каталоги ключей и расписаний. Иначе — `400 INVALID_EXTRACT_TO` |

Нужно передать ровно одно из: `content` или `url`.

## Примеры

### curl — личный ключ

```bash
# Загрузка файла по URL с автоматической распаковкой
curl -X POST https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/upload \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://github.com/user/repo/archive/main.tar.gz",
    "path": "/opt/app/source.tar.gz",
    "extract": true,
    "extractTo": "/opt/app"
  }'

# Встроенное base64-содержимое — маленький файл (package.json)
CONTENT=$(echo '{"name":"my-app","version":"1.0.0"}' | base64)
curl -X POST https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/upload \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"content\":\"$CONTENT\",\"path\":\"/opt/app/package.json\",\"mode\":\"0644\"}"
```

### curl — OAuth-приложение

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/upload \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/config.json","path":"/etc/myapp/config.json"}'
```

### JavaScript — личный ключ

```javascript
// Загрузка архива и распаковка
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/upload`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url: 'https://github.com/user/app/archive/main.tar.gz',
      path: '/opt/app/source.tar.gz',
      extract: true,
      extractTo: '/opt/app',
    }),
  }
)
const { data } = await res.json()
console.log(`Записано ${data.size} байт по пути ${data.path}${data.extracted ? ', распаковано' : ''}`)
```

### JavaScript — OAuth-приложение

```javascript
// Встроенное base64-содержимое — маленький конфиг
const content = Buffer.from('NODE_ENV=production\n').toString('base64')
await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/upload`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      content,
      path: '/opt/app/.env',
      mode: '0600',
    }),
  }
)
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | `true` при успешной записи |
| `data.path` | string | Итоговый путь файла на сервере |
| `data.size` | number | Размер в байтах |
| `data.extracted` | boolean | `true` если архив был распакован после загрузки |

## Пример ответа

```json
{
  "success": true,
  "data": {
    "path": "/opt/app/source.tar.gz",
    "size": 1048576,
    "extracted": true
  }
}
```

## Пример ответа при ошибке

403 — путь запрещён для загрузки:

```json
{
  "success": false,
  "error": {
    "code": "UPLOAD_PATH_DENIED",
    "message": "Upload to system path is not allowed"
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `VALIDATION_ERROR` | Нарушена схема: одновременно `content` и `url`, либо ни одного, неверный `path` |
| 413 | `INLINE_SOURCE_TOO_LARGE` | Тело со встроенным `content` больше 96 МБ, то есть около 72 МБ самого файла. Решение принимается по заголовку `Content-Length` до чтения тела, поэтому отказ детерминированный — то же тело повторной отправкой не пройдёт. Трафик это не экономит: платформа принимает тело целиком, и отказ приходит после того, как загрузка закончилась. В `error.hint` приходят четыре строки — `reason`, `recovery`, `recoveryAction` и `note`: этот маршрут кладёт на машину произвольный файл, поэтому рецепт здесь — выложить файл туда, откуда машина заберёт его сама, и прислать `url` вместо байтов |
| 400 | `NOT_BLACKHOLE` | Сервер в режиме OPEN — Deploy API недоступен |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |
| 402 | `ACCOUNT_FROZEN` | Баланс Вайбкод заморожен. Запрос отбивается до операции, спящий сервер при этом не будится — пополните баланс и повторите |
| 403 | `SERVER_WAKE_BLOCKED` | Сервер спит, а пробуждение запрещено платформой — завершённый пробный период или административная блокировка. Загрузка такой сервер не поднимает, повтор не поможет, пока запрет не снят. Разбор запрета — [Разбудить сервер](/docs/infra/lifecycle/wake) |
| 403 | `WRONG_KEY` | Сервер существует, но вызывающему не принадлежит. Операции над содержимым приложения — выкладка, команда, загрузка файла и чтение логов — открыты по любому из трёх оснований: управляющий ключ сервера, ключ, приложение которого привязано к этому серверу (`Application.serverId`), либо членство в команде разработки этого сервера (обе роли, «Разработчик» и «Администратор»). Токены доступа и загрузка значка требуют именно управляющего ключа. Управление самой машиной открыто ещё и роли «Администратор» команды. В ответе — `hint` с восстановлением из двух шагов. Подробнее — [Восстановление доступа к серверу](/docs/infra/server-access-recovery) |
| 400 | `INVALID_EXTRACT_TO` | Путь `extractTo` не подходит — см. описание поля в параметрах. Текст ответа называет нарушенное правило. Проверка идёт до начала загрузки |
| 400 | `GALAXY_HOST_NOT_A_DEPLOY_TARGET` | Загрузка пришла на id машины-галактики (`kind: "GALAXY"`). Машина несёт контейнеры приложений и целью загрузки не является: файлы приложения кладите в архив [деплоя](./deploy.md) либо создавайте их командой через [`/exec`](./exec.md) по id приложения. См. [Galaxy-приложение](/docs/infra/galaxy) |
| 400 | `GALAXY_APP_USE_GALAXY_ROUTE` | Загрузка пришла на id приложения галактики (`kind: "GALAXY_APP"`). Запись шла бы на файловую систему общего сервера, а не в контейнер приложения. Файлы в такое приложение доставляются пересборкой образа из исходников — поле `source` в теле [деплоя](./deploy.md) |
| 403 | `UPLOAD_PATH_DENIED` | Загрузка в системные директории (`/root/.ssh`, `/etc/passwd`, `/boot`, …) запрещена |
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | Вызов сделан ключом Cowork/Code — такой ключ работает только с данными, изменяющие операции ему закрыты. Что делать — [Проектный ключ для деплоя](/docs/cowork/deploy-key) |
| 404 | `NOT_FOUND` | Сервера с таким `id` нет или он удалён. Сервер, который существует, но вам не принадлежит, отвечает `403 WRONG_KEY` — код различает «нет такого сервера» и «нет прав на него» |
| 409 | `SERVER_NOT_READY` | Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле `hint` с причиной и следующим шагом. Спящий сервер этот код не описывает — его платформа будит сама. Запустите остановленный сервер вызовом [`/start`](/docs/infra/lifecycle/start) либо восстановите туннель вызовом [`/repair`](/docs/infra/lifecycle/repair) и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как `502 TUNNEL_NOT_FOUND` (см. ниже) |
| 409 | `WAKE_IN_PROGRESS` | Спящий сервер уже будит другой запрос. Дождитесь его завершения и повторите |
| 409 | `EXEC_BUSY` | Канал команд сервера занят другой операцией — в том числе префлайтом `unzip` перед распаковкой zip. Это не отказ установить `unzip` и не повод менять формат архива. Ответ несёт `retryable: true`, `hint` и заголовок `Retry-After`; повторите запрос |
| 422 | `VM_MISSING` | У записи сервера нет виртуальной машины у облачного провайдера, поэтому будить нечего. Удалите сервер и создайте новый |
| 502 | `WAKE_FAILED` | Во время пробуждения спящего сервера он перешёл в неожиданное состояние. Повторите запрос |
| 502 | `PROVIDER_ERROR` | Облачный провайдер вернул ошибку при запуске виртуальной машины спящего сервера. Туннель здесь ни при чём — вызов [`/repair`](/docs/infra/lifecycle/repair) не поможет |
| 429 | `RATE_LIMITED` | Превышен лимит 10 операций в минуту на сервер |
| 429 | `DEPLOY_BACKEND_BUSY` | Слишком много одновременных загрузок с телом запроса. Платформа ограничивает число одновременных запросов со встроенным содержимым, чтобы не исчерпать память: счётчик общий у `/upload`, `/deploy` и создания сервера с полем `source.content`. Ответ несёт заголовок `Retry-After: 30` — повторите через 30 секунд. Либо передайте архив ссылкой (`url`): URL-загрузка не занимает inline-memory слот |
| 502 | `UNZIP_PREFLIGHT_FAILED` | Не удалось установить `unzip` на сервере перед распаковкой zip-архива (настоящий отказ apt, не занятый канал). Чтобы не зависеть от этого шага, используйте архив `.tar.gz`. Занятый канал на этом шаге приходит как `409 EXEC_BUSY` |
| 500 | `UPLOAD_URL_DENIED` / `UPLOAD_DOWNLOAD_FAILED` / `UPLOAD_TOO_LARGE` / `UPLOAD_INTEGRITY_MISMATCH` / `UPLOAD_EXTRACT_FAILED` либо другой код агента | URL запрещён сетевой политикой, источник остался недоступен после исчерпания повторов, архив слишком велик, не прошёл проверку целостности или не распаковался, либо произошла другая ошибка записи. На серверах в rollout известные URL-download коды получают стабильный `error.message` без исходной ссылки или дословной ошибки сетевой библиотеки, прочие коды сохраняют собственное сообщение |
| 502 | `TUNNEL_NOT_FOUND` / `GATEWAY_UNREACHABLE: …` | Нет живого туннеля до сервера либо Gateway недоступен — вызовите [`/repair`](/docs/infra/lifecycle/repair) и повторите. `GATEWAY_UNREACHABLE` несёт детали после двоеточия — сопоставляйте `error.code` по префиксу |
| 503 | `GATEWAY_TIMEOUT: …` | Gateway не ответил вовремя. Код несёт детали после двоеточия — сравнивайте по префиксу. В отказоустойчивом режиме URL стабильный текст сообщает, что исход загрузки подтвердить не удалось, повтор безопасен |
| 503 | `WAKE_TIMEOUT` | Спящий сервер не поднялся за отведённые 6.5 минуты — машина не запустилась или туннель не подключился. Сервер возвращается в `sleeping`, поэтому повторный запрос безопасен. См. «Известные особенности» |

Полный список общих ошибок API — [Ошибки](/docs/errors).

При включённом отказоустойчивом URL-режиме проверки запроса и пробуждение VM завершаются до
открытия ответа и сохраняют HTTP-коды из таблицы. Перед долгой загрузкой платформа открывает
HTTP 200 и поддерживает JSON-ответ keepalive-пробелами. Её конечный исход определяется по
`success` и `error.code` в теле. Поэтому ошибки агента/Gateway из уже запущенной URL-загрузки
приходят под HTTP 200, а не под 500/502/503. Для `content`, URL до включения режима и отказов
до начала долгой фазы контракт HTTP-статусов не меняется.

## Известные особенности

- **Спящую отдельную виртуальную машину загрузка будит сама.** Если сервер (`kind: "STANDALONE"`) в статусе `sleeping`, платформа запускает пробуждение и ждёт его в том же запросе — до 6.5 минут, и только потом записывает файл. Отдельный вызов [`/wake`](/docs/infra/lifecycle/wake) не нужен, но клиентский таймаут запроса это время обязан пережить. Если сервер не поднялся, приходит `503 WAKE_TIMEOUT` и сервер возвращается в `sleeping` — файл не записан, повторный запрос безопасен.
- **Автоопределение формата архива при `extract: true`.** Агент определяет формат по сигнатуре файла (magic bytes, функция `detectArchiveExt()`): поддерживает `.tar.gz`, `.zip`, `.tar.bz2`. Формат можно не указывать явно. Для URL — агент также проверяет расширение.
- **macOS-архивы чистятся автоматически.** Если архив содержит AppleDouble-сайдкары (`._*`) или `.DS_Store`, агент удаляет их после распаковки — это предотвращает ложные срабатывания сканеров вроде Tailwind v4 oxide. При создании архива на macOS советуем использовать `COPYFILE_DISABLE=1 tar -czf ...` чтобы сайдкары вообще не попадали в архив.
- **Windows PowerShell ZIP — работает на агенте ≥ 1.2.3.** `Compress-Archive` в Windows пишет ZIP с литеральными `\` в именах файлов. Агент с версии 1.2.3 автоматически нормализует эти пути через шаг `normalize_windows_paths`. Для старых версий агента используйте `tar.gz` (через WSL / Git Bash) либо выкладывайте готовый tar-архив и загружайте через `url`.
- **Список запрещённых путей.** Агент блокирует загрузку в: `/root/.ssh/` (защита SSH-ключей), `/boot`, `/etc/shadow`, `/etc/passwd`, системные сервис-файлы. Для приложения используйте `/opt/<app>/` или `/var/lib/<app>/`.
- **`mode` применяется к архиву, не к распакованным файлам.** При `extract: true` права распакованных файлов определяются содержимым архива, а `mode` задаётся только сохранённому архиву. Если нужно изменить права внутри — сделайте через [`/exec`](./exec.md) c `chmod`.
- **`url` скачивает сама клиентская VM, надёжный режим включается поэтапно.** Этот маршрут намеренно не использует платформенный relay полного [`/deploy`](./deploy.md). На серверах в rollout агент выполняет до четырёх попыток с backoff в общем бюджете пяти минут: DNS/TCP/TLS/header/body-обрывы и временные HTTP-статусы повторяются, а постоянные URL-policy, HTTP, size, integrity и extraction-ошибки завершаются сразу. До включения сервер сохраняет прежний URL-контракт и таймаут. Путь в любом режиме зависит от egress/NAT этой VM. Для доставки исходников приложения, независимой от сети VM, используйте `source.url` полного деплоя. В надёжном режиме клиентский timeout должен выдерживать пробуждение сервера плюс пятиминутный бюджет.

## Смотрите также

- [Полный деплой](./deploy.md)
- [Выполнить команду](./exec.md)
- [Логи сервиса](./logs.md)
