Для 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. Хранилище и его корневая папка
Список хранилищ возвращает GET /v1/storages. Вид хранилища задаёт поле entityType: user — личный диск сотрудника, group — диск рабочей группы, common — общий диск портала. Фильтр по этому полю сразу сужает выдачу до нужного типа.
Корневую папку хранилища даёт поле rootFolderId. Это единственная точка входа в дерево: все дальнейшие запросы папок и файлов идут от идентификатора папки, а не от идентификатора хранилища.
cURL
curl -s -H "X-Api-Key: $VIBE_API_KEY" \
"$VIBE_URL/v1/storages?filter[entityType]=common"
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
{
"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
curl -s -H "X-Api-Key: $VIBE_API_KEY" \
"$VIBE_URL/v1/folders?parentId=19"
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')
{
"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
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
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')
{
"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
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
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()
{
"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.
{
"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
curl -s -H "X-Api-Key: $VIBE_API_KEY" \
-o report.txt \
"$VIBE_URL/v1/files/9423/download"
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
curl -s -X DELETE -H "X-Api-Key: $VIBE_API_KEY" \
"$VIBE_URL/v1/files/9423"
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
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
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. В примере ниже массивы сокращены до одной записи.
{
"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 дополнительно применяет собственное ограничение на размер файла Диска, его ответ приходит без изменений.
Полный код
// 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: перед загрузкой она удаляет из папки прежние файлы с тем же именем, поэтому ежедневный запуск оставляет один актуальный отчёт, а не тридцать одноимённых. Если история нужна — уберите этот вызов и добавьте дату в имя файла.