REST M2M - Загрузка файлов через API
Вам нужно интегрировать загрузку товаров с фотографиями из 1С или другой внешней системы? Обычно это становится узким местом: файловая система 1С может быть недоступна извне, firewall блокирует прямые ссылки, network problems замораживают uploads. Каждая система требует своего подхода.
RestV1M2M решает это единым эндпоинтом: POST /rest/m2m/files — один метод принимает файл двумя способами: base64 (1С отправляет бинарные данные в тексте JSON) и url (внешняя система предоставляет ссылку, сервер скачивает сам).
Три проблемы интеграции с 1С
Проблема 1: 1С не может отправить multipart форму
Системные ограничения 1С делают сложным прямую отправку бинарных файлов через multipart/form-data.
Решение: Поддержка base64 — 1С кодирует файл в base64 и отправляет в JSON. API декодирует на лету.
Проблема 2: Внешние файлы недоступны со стороны сервера
Если файл находится в сети 1С или за NAT/firewall, сервер Wepps не сможет до него достучаться.
Решение: Поддержка url. Если внешняя система может отправить ссылку — сервер скачает файл сам.
Проблема 3: Нужна гибкость для разных интеграций
Одна 1С отправляет base64, другая может предоставить URL, третья хочет отправить ссылку на файл.
Решение: Один эндпоинт files принимает любой источник: base64 или url. Приоритет: base64 → url (если передан base64 — он используется, url игнорируется).
Архитектура: Один endpoint, два способа
Общая схема загрузки
┌─────────────────────────────────────────────────────┐
│ Клиент (1С, веб, мобильное приложение) │
└────────────────┬────────────────────────────────────┘
│
┌─────────┴─────────┐
│ │
▼ ▼
1С Base64 Приложение/1С URL
(1) base64 (2) url
│ │
└─────────┼─────────┘
│
▼
┌───────────────────────────────────────┐
│ POST /rest/m2m/files │
│ (RestV1M2M::postFiles) │
└─────────────┬───────────────────────┘
│
┌──────────┼─────────┐
│ │ │
▼ ▼ ▼
prepareUploadFromBase64() / prepareUploadFromUrl()
(скачать/декодировать → временный файл
в Template/Forms/uploads)
│
▼
Lists::getUploadFileName()
(финализировать имя, путь в /pic/lists/)
│
▼
┌───────────────────────────────────────┐
│ Запись в s_Files БД │
│ - TableName: список (list) │
│ - TableNameId: ID записи (listId) │
│ - Priority: MAX(Priority) в группе +1│
│ - FileUrl: /pic/lists/... │
└───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ Response 201 Created │
│ { "data": { "id": file_id, "url": ... } }
└───────────────────────────────────────┘
Компоненты реализации
1. RestV1M2M::postFiles() — Диспетчер загрузки
Назначение: Принимает записи, для каждой выбирает источник (base64 или url), загружает во временную папку, определяет приоритет, финализирует имя через Lists::getUploadFileName(), затем создаёт записи в s_Files.
public function postFiles(): array
{
$records = $this->normalizeInput();
$utils = $this->getUtils('s_Files');
$uploadContext = [];
$utils->setBefore(function (array $records, string $tableName, RestV1M2MUtils $utils) use (&$uploadContext) {
// 1. Собрать группы (list|listField|listId) для расчёта приоритета
// 2. Для каждой группы: SELECT MAX(Priority) ...
// 3. Для каждой записи:
// - base64 → prepareUploadFromBase64()
// - url → prepareUploadFromUrl()
// - ошибка → setValidationErrors([...])
// 4. Priority = MAX(Priority) в группе + 1
// 5. Lists::getUploadFileName() → финальное имя и url
// 6. Запомнить uploadContext (временный файл, destination, fileUrl)
return $records;
})->setAfter(function (array $results, string $tableName, RestV1M2MUtils $utils) use (&$uploadContext) {
// 1. Удалить временный файл (upload_temp)
// 2. Если запись создана (201) → добавить url в ответ
// 3. Если ошибка → удалить загруженный файл (destination)
return $results;
});
return $this->create('s_Files', $records);
}
Ключевые моменты:
- Before-колбэк выполняется внутри транзакции — при ошибке всё откатывается
- Приоритет внутри группы (
list|listField|listId):MAX(Priority) + 1 - После успешного создания в ответ добавляется
urlготового файла - After-колбэк подчищает: временные файлы всегда, загруженные — при ошибке
2. Подготовка файла — prepareUploadFromBase64() / prepareUploadFromUrl()
Назначение: Получить бинарные данные из base64 или URL и сохранить во временную папку.
// base64
public function prepareUploadFromBase64(string $base64, string $fileName): array
{
$binary = base64_decode($base64, true);
if ($binary === false) {
return ['error' => 'Invalid base64 data'];
}
$mime = $this->detectMimeType($binary);
if ($mime === null) {
return ['error' => 'Unable to detect mime type'];
}
$ext = $this->resolveExtensionByMime($mime, $fileName);
if ($ext === null) {
return ['error' => 'Unsupported mime type: ' . $mime];
}
$tmpPath = $this->saveTempFile($binary, $ext);
if ($tmpPath === null) {
return ['error' => 'Failed to save temporary file'];
}
return ['path' => $tmpPath, 'name' => $fileName, 'type' => $mime, 'size' => strlen($binary)];
}
// url
public function prepareUploadFromUrl(string $url, string $fileName): array
{
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return ['error' => 'Invalid url'];
}
$binary = @file_get_contents($url);
if ($binary === false || $binary === '') {
return ['error' => 'Unable to download file from url'];
}
// ... аналогично: detectMimeType → resolveExtensionByMime → saveTempFile
}
Цепочка проверок (guard clause):
- Декодировать/скачать →
Invalid base64 data/Invalid url/Unable to download file from url - Определить MIME →
Unable to detect mime type - Разрешить расширение по MIME →
Unsupported mime type: {mime} - Сохранить во временный файл →
Failed to save temporary file
Временные файлы сохраняются в packages/WeppsExtensions/Template/Forms/uploads/wepps_upload_*.{ext}.
3. Финализация имени — Lists::getUploadFileName()
Назначение: Перемещает файл в постоянное хранилище и возвращает финальный URL.
$upload = Lists::getUploadFileName($upload, $list, $listField, $tableNameId);
// $upload = ['type' => mime, 'size' => bytes, 'url' => '/pic/lists/...', 'inner' => ..., 'ext' => 'jpg']
Результат:
url— публичный путь для фронтенда (например/pic/lists/Products/123/a1b2c3d4.jpg)inner— внутреннее имя файлаtype/size— сохраняются в записиs_Files
Использование
Создание файла через base64 (основной способ для 1С)
POST /rest/m2m/files HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"data": {
"guid": "550e8400-e29b-41d4-a716-446655440000",
"name": "photo.jpg",
"list": "Products",
"listField": "Id",
"listId": 123,
"description": "Главное фото товара",
"base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
}
}
Response: 201 Created
{
"status": 201,
"message": "Created",
"data": {
"id": 456,
"url": "/pic/lists/Products/123/a1b2c3d4e5f6.jpg"
}
}
Создание файла по ссылке (url)
POST /rest/m2m/files HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"data": {
"guid": "550e8400-e29b-41d4-a716-446655440001",
"name": "photo.jpg",
"list": "Products",
"listField": "Id",
"listId": 124,
"url": "https://crm-system.example.com/files/product124.jpg"
}
}
Приоритет источников: если переданы оба поля — используется
base64,urlигнорируется.
Batch-загрузка (массив записей, макс. 100)
POST /rest/m2m/files HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"data": [
{
"guid": "550e8400-e29b-41d4-a716-446655440002",
"name": "photo1.jpg",
"list": "Products",
"listField": "Id",
"listId": 123,
"base64": "iVBORw0KGgo..."
},
{
"guid": "550e8400-e29b-41d4-a716-446655440003",
"name": "photo2.jpg",
"list": "Products",
"listField": "Id",
"listId": 123,
"base64": "iVBORw0KGgo..."
}
]
}
Response: 207 Multi-Status
{
"status": 207,
"message": "Multi-Status",
"data": [
{"status": 201, "message": "Created", "data": {"id": 457, "url": "/pic/lists/Products/123/....jpg"}},
{"status": 201, "message": "Created", "data": {"id": 458, "url": "/pic/lists/Products/123/....jpg"}}
]
}
Приоритеты в batch назначаются последовательно внутри группы: первый файл группы получает MAX(Priority) + 1, второй — +2 и т.д.
Получить файлы (GET)
# Все файлы с пагинацией
GET /rest/m2m/files?page=1&limit=20
# Файлы конкретного товара
GET /rest/m2m/files?list=Products&listField=Id&listId=123
# Файлы с фильтром
GET /rest/m2m/files?list=Products&listId=123&filter=main
Параметры GET: page, limit, list (TableName), listField (TableNameField), listId (TableNameId), filter (ApiFilter), description.
Сортировка: TableNameField, TableNameId, Priority.
Ответ:
{
"status": 200,
"message": "OK",
"data": [
{
"id": 456,
"guid": "550e8400-e29b-41d4-a716-446655440000",
"name": "photo.jpg",
"list": "Products",
"listField": "Id",
"listId": 123,
"priority": 1,
"description": "Главное фото товара",
"filter": "main",
"type": "image/jpeg",
"size": 204800,
"url": "/pic/lists/Products/123/a1b2c3d4e5f6.jpg"
}
],
"pagination": { "page": 1, "limit": 20, "count": 5 }
}
Обновить файл (PUT)
PUT /rest/m2m/files HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"data": {
"id": 456,
"name": "photo-renamed.jpg",
"description": "Новое описание",
"priority": 2
}
}
PUT — частичное обновление: id обязателен, остальные поля (guid, name, list, listField, listId, description, filter, priority) — необязательные.
⚠️ PUT меняет только метаданные (
name,description,priority, ...) — сам файл заменить нельзя: поляbase64/urlв PUT игнорируются.putFiles()— это обычныйupdate('s_Files', ...), без логики загрузки. Чтобы заменить содержимое файла — удалите старый (DELETE /rest/m2m/filesс{"data": [id]}) и создайте новый (POST /rest/m2m/filesс новымbase64/url).
Удалить файлы (DELETE)
DELETE /rest/m2m/files HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"data": [456, 457]
}
Response: 207 Multi-Status
Удаление работает в два этапа:
- Before-колбэк собирает физические пути (
FileUrl→Connect::$projectDev['root'] . $fileUrl) для удаляемых ID - After-колбэк (после успешного удаления записей из БД) выполняет
@unlink()по каждому пути
Структура файлов в системе
На диске
/pic/lists/
├── Products/
│ ├── 123/
│ │ ├── a1b2c3d4e5f6.jpg
│ │ ├── x9y8z7w6v5u4t3s2.png
│ │ └── ...
│ ├── 124/
│ │ └── ...
│ └── ...
└── ...
В БД (таблица s_Files)
| Поле | Значение | Объяснение |
|---|---|---|
| TableName | Products | Список (list), к которому привязан файл |
| TableNameField | Id | Поле привязки (listField) |
| TableNameId | 123 | ID записи (listId) |
| Name | "photo.jpg" | Имя файла |
| FileDescription | "Главное фото" | Описание файла |
| ApiFilter | "main" | Фильтр/категория файла |
| FileType | image/jpeg | MIME-тип |
| FieSize | 204800 | Размер в байтах |
| Priority | 1 | Приоритет внутри группы (list/listField/listId) |
| FileUrl | /pic/lists/Products/123/a1b2c3d4.jpg | URL доступа |
Обработка ошибок
API возвращает ясные коды ошибок (per-item в batch):
// 400 - неверные base64 данные
{ "status": 400, "message": "Invalid base64 data" }
// 400 - не удалось определить MIME
{ "status": 400, "message": "Unable to detect mime type" }
// 400 - неподдерживаемый тип файла
{ "status": 400, "message": "Unsupported mime type: application/x-msdownload" }
// 400 - не удалось сохранить временный файл
{ "status": 400, "message": "Failed to save temporary file" }
// 400 - невалидный URL
{ "status": 400, "message": "Invalid url" }
// 400 - не удалось скачать по URL
{ "status": 400, "message": "Unable to download file from url" }
// 400 - не передан ни один источник
{ "status": 400, "message": "Either valid base64 or valid url required" }
Endpoints
| Метод | Endpoint | Описание |
|---|---|---|
GET |
/rest/m2m/files |
Список файлов (с фильтрами и пагинацией) |
POST |
/rest/m2m/files |
Создание файлов (base64 или url, single → 201, batch → 207) |
PUT |
/rest/m2m/files |
Обновление файлов по id (частичное) |
DELETE |
/rest/m2m/files |
Удаление файлов по id (тело {"data": [id, ...]}) |
Параметры POST:
guid(guid, обязательно) — уникальный идентификаторname(string, обязательно) — имя файла (используется для расширения)list(string, обязательно) — имя таблицы/списка (Products,ProductsVariations, ...)listField(string, обязательно) — поле привязки (обычноId)listId(int, обязательно) — ID записи, к которой привязывается файлdescription(string, опционально) — описание файлаfilter(string, опционально) — фильтр/категория (для разделения фото и документов)base64(string, опционально) — содержимое файла в base64 (приоритет)url(string, опционально) — ссылка на файл (используется если нет base64)
Пример для 1С
Чтение файла и отправка base64
// Получить двоичные данные файла
ДанныеФайла = Новый ДвоичныеДанные("D:\Images\product123.jpg");
// Кодировать в Base64
Base64Строка = Base64Строка(ДанныеФайла);
// Подготовить JSON для API
СтруктураДанных = Новый Структура;
СтруктураДанных.Вставить("guid", Новый УникальныйИдентификатор);
СтруктураДанных.Вставить("name", "product123.jpg");
СтруктураДанных.Вставить("list", "Products");
СтруктураДанных.Вставить("listField", "Id");
СтруктураДанных.Вставить("listId", 123);
СтруктураДанных.Вставить("base64", Base64Строка);
Тело = Новый Структура;
Тело.Вставить("data", СтруктураДанных);
// Сериализовать в JSON
JSONСтрока = ПреобразоватьВJSON(Тело);
// Отправить на API
Результат = ОтправитьHTTPЗапрос(
"POST",
"https://api.example.com/rest/m2m/files",
JSONСтрока,
АвторизацияТокен
);
Обработка ответа
РезультатПарсинга = ПроанализироватьJSON(Результат);
Если РезультатПарсинга.status = 201 Тогда
ИД_Файла = РезультатПарсинга.data.id;
Сообщить("Файл загружен успешно! ID: " + ИД_Файла);
Иначе
Сообщить("Ошибка: " + РезультатПарсинга.message);
КонецЕсли;
Bruno примеры
В проекте Bruno находятся готовые примеры:
.tools/bruno/WeppsPlatformV1/clientM2M/files/
├── files.post.bru # Загрузка по URL
├── files.base64.post.bru # Загрузка Base64 (основной для 1С)
├── files.get.bru # Список файлов
├── files.put.bru # Обновление файла
└── files.delete.bru # Удаление файла
Производительность и безопасность
Безопасность
- Проверка MIME-типа через
finfo(FILEINFO_MIME_TYPE) — определяет реальный тип по содержимому, а не по расширению - Разрешённые расширения —
resolveExtensionByMime()возвращает расширение только для поддерживаемых MIME (иначеUnsupported mime type) - Финализация имени через
Lists::getUploadFileName()— безопасное имя файла, защита от path traversal - Транзакции — before/after колбэки внутри транзакции: ошибка откатывает запись и подчищает файлы
Очистка
- After-колбэк всегда удаляет временный файл (
wepps_upload_*) - При ошибке создания записи удаляется и загруженный файл (
destination) - DELETE собирает пути и физически удаляет файлы после успешного удаления записей
Лимиты PHP (размеры загрузок)
В коде платформы нет собственных лимитов на размер файла — ограничения определяются настройками PHP (php.ini). Для M2M-загрузки (JSON с base64/url) важны:
Параметр php.ini |
На что влияет | Рекомендация |
|---|---|---|
post_max_size |
Максимальный размер всего тела запроса — самый важный для base64: JSON с base64-строкой упирается в него первым | 64M–128M (base64 увеличивает объём на ~33%) |
upload_max_filesize |
Максимальный размер одного загружаемого файла (для multipart; на JSON-base64 напрямую не влияет, но не должен быть меньше post_max_size) |
64M–128M |
memory_limit |
Память для base64_decode(): строка base64 + декодированные бинарные данные живут в памяти |
256M–512M (особенно для batch) |
max_execution_time |
Скачивание файла по url (file_get_contents) — сетевые операции медленные |
120–300 сек или 0 для CLI |
Пример для php.ini:
post_max_size = 128M
upload_max_filesize = 128M
memory_limit = 512M
max_execution_time = 300
💡 Batch учитывает суммарный размер:
{"data": [100 файлов]}— это один POST-запрос, значит весь объём base64 (все файлы вместе) должен влезать вpost_max_size. Для больших объёмов загружайте файлы по одному или порциями.⚠️ Замена
php.iniтребует перезапуска PHP-FPM:systemctl restart php8.x-fpm(см.platform.wepps.dev/php-fpm.md).
Итоги
| Способ | Удобство | Надёжность | Когда использовать |
|---|---|---|---|
| base64 | ⭐⭐⭐ (для 1С) | ⭐⭐⭐ | 1С отправляет файлы, нужна полная копия |
| url | ⭐⭐ | ⭐⭐ | Файл уже доступен в web, сервер скачает сам |
Рекомендация: используйте base64 для 1С-интеграций — это даёт надёжность и не требует доступа внешней системы наружу.