Для AI-агентов: markdown этой страницы — /docs-content/recipes/disk-files.md индекс документации — /llms.txt

Работа с файлами Диска

Сложность: начальный | Скоупы: disk | Стек: cURL / JavaScript

Раскладываем документы по Диску Битрикс24: находим нужное хранилище, строим в нём дерево папок, загружаем файл и скачиваем его обратно. Тот же порядок действий подходит для выгрузки отчётов по расписанию и для приёма вложений из внешней системы.

Что понадобится

  • API-ключ Вайбкод со скоупом disk
  • Node.js 18 или новее

Во всех примерах $VIBE_URL — базовый адрес https://vibecode.bitrix24.tech, $VIBE_API_KEY — ваш API-ключ.

Как устроено решение

  1. Находим хранилище и его корневую папку — от неё начинается всё дерево Диска.
  2. Смотрим содержимое папки, разделяя подпапки и файлы.
  3. Создаём папку под задачу, при необходимости вложенную.
  4. Загружаем файл в эту папку.
  5. Скачиваем файл обратно.
  6. Переименовываем, удаляем ненужное и собираем обзор нескольких папок одним пакетным запросом.

Примеры шагов показывают отдельные вызовы. Готовый к запуску скрипт — в разделе «Полный код».

Шаг 1. Хранилище и его корневая папка

Список хранилищ возвращает GET /v1/storages. Вид хранилища задаёт поле entityType: user — личный диск сотрудника, group — диск рабочей группы, common — общий диск портала. Фильтр по этому полю сразу сужает выдачу до нужного типа.

Корневую папку хранилища даёт поле rootFolderId. Это единственная точка входа в дерево: все дальнейшие запросы папок и файлов идут от идентификатора папки, а не от идентификатора хранилища.

cURL

Terminal
curl -s -H "X-Api-Key: $VIBE_API_KEY" \
  "$VIBE_URL/v1/storages?filter[entityType]=common"

JavaScript

javascript
const res = await fetch(`${VIBE_URL}/v1/storages?filter[entityType]=common`, {
  headers: { 'X-Api-Key': VIBE_API_KEY },
})
const { data } = await res.json()
const rootFolderId = data[0].rootFolderId
JSON
{
  "success": true,
  "data": [
    {
      "id": 11,
      "name": "Общий диск",
      "code": null,
      "module": "disk",
      "entityType": "common",
      "entityId": "shared_files_s1",
      "rootFolderId": 19
    }
  ],
  "meta": { "total": 1, "hasMore": false }
}

Поле entityId у общего диска — нечисловая строка, приводить его к числу не нужно.

Шаг 2. Содержимое папки

GET /v1/folders возвращает содержимое папки, указанной в parentId. Параметр обязателен: без него запрос отвечает 400 MISSING_REQUIRED_PARAMS.

Выдача смешанная — в ней и подпапки, и файлы. Различает их поле type со значением folder или file.

cURL

Terminal
curl -s -H "X-Api-Key: $VIBE_API_KEY" \
  "$VIBE_URL/v1/folders?parentId=19"

JavaScript

javascript
const res = await fetch(`${VIBE_URL}/v1/folders?parentId=${rootFolderId}`, {
  headers: { 'X-Api-Key': VIBE_API_KEY },
})
const { data } = await res.json()

const folders = data.filter(item => item.type === 'folder')
const files = data.filter(item => item.type === 'file')
JSON
{
  "success": true,
  "data": [
    {
      "id": 611,
      "name": "Договоры",
      "code": null,
      "storageId": 11,
      "type": "folder",
      "realObjectId": 611,
      "parentId": 19,
      "deletedType": 0,
      "createdAt": "2026-06-08T14:36:45.000Z",
      "updatedAt": "2026-07-10T15:03:12.000Z",
      "deletedAt": null,
      "createdBy": 1,
      "updatedBy": 1,
      "deletedBy": null,
      "detailUrl": "https://your-portal.bitrix24.ru/docs/path/Договоры"
    }
  ],
  "meta": { "total": 13, "hasMore": false }
}

Тот же состав папки отдаёт GET /v1/files?folderId=19. Родительская папка называется в нём folderId, и этот параметр тоже обязателен. У записей-файлов там приходят размер size, идентификатор fileId и ссылка downloadUrl.

Шаг 3. Папка под задачу

POST /v1/folders создаёт папку. В теле нужны два поля: parentId и name. Ответ возвращает карточку созданной папки, её id становится parentId для следующего уровня — так набирается любое дерево.

cURL

Terminal
curl -s -X POST -H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
  "$VIBE_URL/v1/folders" \
  -d '{ "parentId": 19, "name": "Документы клиентов" }'

JavaScript

javascript
async function createFolder(parentId, name) {
  const res = await fetch(`${VIBE_URL}/v1/folders`, {
    method: 'POST',
    headers: { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ parentId, name }),
  })
  const { data } = await res.json()
  return data
}

const clients = await createFolder(rootFolderId, 'Документы клиентов')
const month = await createFolder(clients.id, '2026-07')
JSON
{
  "success": true,
  "data": {
    "id": 9415,
    "name": "Документы клиентов",
    "code": null,
    "storageId": 11,
    "type": "folder",
    "realObjectId": 9415,
    "parentId": 19,
    "deletedType": 0,
    "createdAt": "2026-07-21T08:55:35.000Z",
    "updatedAt": "2026-07-21T08:55:35.000Z",
    "deletedAt": null,
    "createdBy": 1,
    "updatedBy": 1,
    "deletedBy": null,
    "detailUrl": "https://your-portal.bitrix24.ru/docs/path/Документы клиентов"
  }
}

Переименовать папку позволяет PATCH /v1/folders/:id с одним полем name. Перенос в другую родительскую папку — отдельная операция POST /v1/folders/:id/moveto.

Шаг 4. Загрузка файла

POST /v1/files/upload кладёт файл в папку. Содержимое передаётся строкой в кодировке Base64 в поле content, имя с расширением — в filename, папка-назначение — в folderId. Вместо folderId можно указать storageId, тогда файл попадёт в корень хранилища.

cURL

Terminal
curl -s -X POST -H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
  "$VIBE_URL/v1/files/upload" \
  -d '{
    "folderId": 9415,
    "filename": "report.txt",
    "content": "0J7RgtGH0ZHRgiDQt9CwINC40Y7Qu9GMCg=="
  }'

JavaScript

javascript
import { readFile } from 'node:fs/promises'

const content = (await readFile('report.txt')).toString('base64')

const res = await fetch(`${VIBE_URL}/v1/files/upload`, {
  method: 'POST',
  headers: { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ folderId: clients.id, filename: 'report.txt', content }),
})
const { data: file } = await res.json()
JSON
{
  "success": true,
  "data": {
    "id": 9423,
    "name": "report.txt",
    "code": null,
    "storageId": 11,
    "type": "file",
    "folderId": 9415,
    "deletedType": 0,
    "globalContentVersion": 1,
    "fileId": 36245,
    "size": 25,
    "createdAt": "2026-07-21T08:58:53.000Z",
    "updatedAt": "2026-07-21T08:58:53.000Z",
    "deletedAt": null,
    "createdBy": 1,
    "updatedBy": 1,
    "deletedBy": null,
    "downloadUrl": "https://your-portal.bitrix24.ru/rest/1/WEBHOOK/download/?token=...",
    "detailUrl": "https://your-portal.bitrix24.ru/docs/file/Документы клиентов/report.txt"
  }
}

Успешная загрузка отвечает кодом 201. Поле size в ответе — размер исходного файла в байтах, а не длина строки Base64. Сверка size с размером отправленного файла подтверждает, что содержимое дошло целиком. Если в теле нет filename или content, а также если не указаны ни folderId, ни storageId, запрос отвечает 400 MISSING_PARAMS.

JSON
{
  "success": false,
  "error": {
    "code": "MISSING_PARAMS",
    "message": "filename and content (base64) are required."
  }
}

Шаг 5. Скачивание файла

GET /v1/files/:id/download отдаёт содержимое файла напрямую: тело ответа — сам файл, имя приходит в заголовке Content-Disposition. Ключ передаётся тем же заголовком X-Api-Key, что и в остальных вызовах, поэтому отдельная авторизация для скачивания не нужна.

cURL

Terminal
curl -s -H "X-Api-Key: $VIBE_API_KEY" \
  -o report.txt \
  "$VIBE_URL/v1/files/9423/download"

JavaScript

javascript
import { writeFile } from 'node:fs/promises'

const res = await fetch(`${VIBE_URL}/v1/files/${file.id}/download`, {
  headers: { 'X-Api-Key': VIBE_API_KEY },
})
await writeFile('report.txt', Buffer.from(await res.arrayBuffer()))

Шаг 6. Переименование и удаление

Переименовать файл позволяет PATCH /v1/files/:id с полем name. Удаление файла и удаление папки — DELETE /v1/files/:id и DELETE /v1/folders/:id, обе операции отвечают 204 с пустым телом.

cURL

Terminal
curl -s -X DELETE -H "X-Api-Key: $VIBE_API_KEY" \
  "$VIBE_URL/v1/files/9423"

JavaScript

javascript
await fetch(`${VIBE_URL}/v1/files/${file.id}`, {
  method: 'DELETE',
  headers: { 'X-Api-Key': VIBE_API_KEY },
})

Как проверить, что объект действительно удалён, — в разделе «Ограничения».

Шаг 7. Обзор нескольких папок одним запросом

POST /v1/batch объединяет до 50 чтений в один запрос. Каждый вызов получает свой id, по нему же ответ раскладывается в data.results. Отдельная карта data.totals даёт число объектов в каждой папке, data.summary — сколько вызовов прошло и сколько отказало.

cURL

Terminal
curl -s -X POST -H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
  "$VIBE_URL/v1/batch" \
  -d '{
    "calls": [
      { "id": "storage", "entity": "storages", "action": "get", "entityId": 11 },
      { "id": "contracts", "entity": "folders", "action": "list", "params": { "parentId": 611 } },
      { "id": "clients", "entity": "folders", "action": "list", "params": { "parentId": 9415 } }
    ]
  }'

JavaScript

javascript
const res = await fetch(`${VIBE_URL}/v1/batch`, {
  method: 'POST',
  headers: { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    calls: [
      { id: 'storage', entity: 'storages', action: 'get', entityId: 11 },
      { id: 'contracts', entity: 'folders', action: 'list', params: { parentId: 611 } },
      { id: 'clients', entity: 'folders', action: 'list', params: { parentId: clients.id } },
    ],
  }),
})
const { data } = await res.json()
console.log(data.totals) // { contracts: 10, clients: 1 }

Каждый вызов кладёт свой результат в data.results под собственным id. В примере ниже массивы сокращены до одной записи.

JSON
{
  "success": true,
  "data": {
    "results": {
      "storage": {
        "id": 11,
        "name": "Общий диск",
        "code": null,
        "module": "disk",
        "entityType": "common",
        "entityId": "shared_files_s1",
        "rootFolderId": 19
      },
      "contracts": [
        {
          "id": 613,
          "name": "2026-06",
          "code": null,
          "storageId": 11,
          "type": "folder",
          "realObjectId": 613,
          "parentId": 611,
          "deletedType": 0,
          "createdAt": "2026-06-08T14:36:47.000Z",
          "updatedAt": "2026-06-08T14:36:47.000Z",
          "deletedAt": null,
          "createdBy": 1,
          "updatedBy": 1,
          "deletedBy": null,
          "detailUrl": "https://your-portal.bitrix24.ru/docs/path/Договоры/2026-06"
        }
      ],
      "clients": [
        {
          "id": 9417,
          "name": "2026-07",
          "code": null,
          "storageId": 11,
          "type": "folder",
          "realObjectId": 9417,
          "parentId": 9415,
          "deletedType": 0,
          "createdAt": "2026-07-21T08:55:50.000Z",
          "updatedAt": "2026-07-21T08:55:50.000Z",
          "deletedAt": null,
          "createdBy": 1,
          "updatedBy": 1,
          "deletedBy": null,
          "detailUrl": "https://your-portal.bitrix24.ru/docs/path/Документы клиентов/2026-07"
        }
      ]
    },
    "totals": { "contracts": 10, "clients": 1 },
    "errors": {},
    "summary": { "total": 3, "succeeded": 3, "failed": 0 },
    "meta": {
      "contracts": { "total": 10, "returned": 10, "hasMore": false, "truncated": false },
      "clients": { "total": 1, "returned": 1, "hasMore": false, "truncated": false }
    }
  }
}

Чтению одной записи нужен entityId на верхнем уровне вызова, а не внутри params. Вызов get с идентификатором в params отвечает MISSING_ENTITY_ID, и весь пакет возвращает INVALID_REQUEST, если ни один вызов не прошёл проверку.

Ограничения

Удаление переносит объект в корзину, а не стирает его. DELETE отвечает 204, но GET по тому же идентификатору продолжает отвечать 200. У удалённого объекта заполняются deletedAt и deletedBy, а deletedType становится 3 для того, что удалили напрямую, и 4 для вложенного, которое ушло вместе с родительской папкой. Проверка «удалилось ли» через код 404 не сработает никогда — сверяйте deletedType или отсутствие записи в списке содержимого папки. Повторный DELETE по тому же идентификатору тоже отвечает 204. Операции восстановления из корзины в API нет — вернуть объект можно из интерфейса Битрикс24.

Списки папок и файлов смешанные. И GET /v1/folders?parentId=…, и GET /v1/files?folderId=… возвращают содержимое папки целиком — подпапки и файлы вместе. Разбирать выдачу нужно по полю type.

Родительская папка обязательна. GET /v1/folders без parentId и GET /v1/files без folderId отвечают 400 MISSING_REQUIRED_PARAMS. Обход дерева всегда начинается с rootFolderId хранилища. Запрос по несуществующему идентификатору отвечает 404 ENTITY_NOT_FOUND.

Через PATCH меняется только имя. И у папки, и у файла PATCH принимает поле name. Перенос в другую папку — отдельные операции POST /v1/folders/:id/moveto и POST /v1/files/:id/moveto.

Хранилища доступны только для чтения. Личный диск заводится вместе с сотрудником, диск рабочей группы — вместе с группой, общий диск на портале один. Операций создания и изменения хранилищ нет.

Размер загружаемого файла ограничен телом запроса. Кодировка Base64 увеличивает объём примерно на треть, и тело запроса на загрузку ограничено 70 МБ — это файл размером примерно до 50 МБ. Крупный файл загружается дольше, поэтому увеличьте для такого запроса время ожидания ответа и загружайте большие файлы по одному, а не пакетом. Битрикс24 дополнительно применяет собственное ограничение на размер файла Диска, его ответ приходит без изменений.

Полный код

javascript
// drive-upload.js — папка под задачу и загрузка в неё отчёта
import { readFile } from 'node:fs/promises'

const VIBE_URL = process.env.VIBE_URL ?? 'https://vibecode.bitrix24.tech'
const VIBE_API_KEY = process.env.VIBE_API_KEY
if (!VIBE_API_KEY) throw new Error('Переменная окружения VIBE_API_KEY не задана')

const headers = { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' }

async function api(path, init = {}) {
  const res = await fetch(`${VIBE_URL}${path}`, { headers, ...init })
  const body = await res.json()
  if (!body.success) throw new Error(body.error?.message ?? `ошибка ${res.status}`)
  return body.data
}

async function findRootFolderId() {
  const storages = await api('/v1/storages?filter[entityType]=common')
  if (!storages.length) throw new Error('общий диск не найден')
  return storages[0].rootFolderId
}

async function ensureFolder(parentId, name) {
  const children = await api(`/v1/folders?parentId=${parentId}`)
  // deletedType обязателен: удалённая папка остаётся доступной по GET и лежит
  // в корзине. Без этой проверки скрипт нашёл бы её по имени и молча складывал
  // отчёты в корзину, откуда их никто не заберёт.
  const existing = children.find(
    item => item.type === 'folder' && item.name === name && !item.deletedType,
  )
  if (existing) return existing

  return api('/v1/folders', {
    method: 'POST',
    body: JSON.stringify({ parentId, name }),
  })
}

async function upload(folderId, filename, filePath) {
  const content = (await readFile(filePath)).toString('base64')
  return api('/v1/files/upload', {
    method: 'POST',
    body: JSON.stringify({ folderId, filename, content }),
  })
}

async function removeExisting(folderId, name) {
  const children = await api(`/v1/files?folderId=${folderId}`)
  const stale = children.filter(item => item.type === 'file' && item.name === name && !item.deletedType)
  for (const item of stale) {
    // DELETE отвечает 204 с пустым телом — разбирать его как JSON нельзя,
    // поэтому проверяется только статус, а не конверт { success, data }.
    const res = await fetch(`${VIBE_URL}/v1/files/${item.id}`, { method: 'DELETE', headers })
    if (!res.ok) throw new Error(`не удалось удалить ${item.name}: ${res.status}`)
    console.log(`удалён прежний ${item.name} (id=${item.id})`)
  }
}

async function main() {
  const rootFolderId = await findRootFolderId()

  const month = new Date().toISOString().slice(0, 7)
  const clients = await ensureFolder(rootFolderId, 'Документы клиентов')
  const target = await ensureFolder(clients.id, month)

  // Диск допускает несколько файлов с одним именем в папке, поэтому прежний
  // одноимённый файл удаляется до загрузки — иначе ежедневный запуск за месяц
  // оставит тридцать одноимённых отчётов.
  await removeExisting(target.id, 'report.txt')
  const file = await upload(target.id, 'report.txt', './report.txt')

  console.log(`Папка: ${clients.name}/${target.name}`)
  console.log(`Файл: ${file.name}, ${file.size} байт, id=${file.id}`)
}

main().catch(error => {
  console.error(error.message)
  process.exitCode = 1
})

Функция ensureFolder сначала ищет папку с таким именем в содержимом родительской и создаёт новую, только если её там нет. Так повторный запуск скрипта складывает файлы в уже созданное дерево, а не строит его заново.

Для самих файлов идемпотентность обеспечивает removeExisting: перед загрузкой она удаляет из папки прежние файлы с тем же именем, поэтому ежедневный запуск оставляет один актуальный отчёт, а не тридцать одноимённых. Если история нужна — уберите этот вызов и добавьте дату в имя файла.

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