REST M2M — Загрузка файлов через API

Вам нужно интегрировать загрузку товаров с фотографиями из 1С или другой внешней системы? Обычно это становится узким местом: файловая система 1С может быть недоступна извне, firewall блокирует прямые ссылки, network problems замораживают uploads. Каждая система требует своего подхода.

RestV1M2M решает это единым эндпоинтом: `POST /rest/m2m/files` — один метод принимает файл двумя способами: base64 (1С отправляет бинарные данные в тексте JSON) и url (внешняя система предоставляет ссылку, сервер скачивает сам).

21.07.2026

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):

  1. Декодировать/скачать → Invalid base64 data / Invalid url / Unable to download file from url
  2. Определить MIME → Unable to detect mime type
  3. Разрешить расширение по MIME → Unsupported mime type: {mime}
  4. Сохранить во временный файл → 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

Удаление работает в два этапа:

  1. Before-колбэк собирает физические пути (FileUrlConnect::$projectDev['root'] . $fileUrl) для удаляемых ID
  2. 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-строкой упирается в него первым 64M128M (base64 увеличивает объём на ~33%)
upload_max_filesize Максимальный размер одного загружаемого файла (для multipart; на JSON-base64 напрямую не влияет, но не должен быть меньше post_max_size) 64M128M
memory_limit Память для base64_decode(): строка base64 + декодированные бинарные данные живут в памяти 256M512M (особенно для batch)
max_execution_time Скачивание файла по url (file_get_contents) — сетевые операции медленные 120300 сек или 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С-интеграций — это даёт надёжность и не требует доступа внешней системы наружу.

Как фреймворк: Гибкость разработки

Полный контроль над кодом, архитектурой и расширениями для сложных проектов

wapps framework

Как CMS: Простота управления

Интуитивная админ-панель для редакторов контента без программирования

wapps cms

Как платформа: Готовые решения

Быстрый старт проектов с возможностью глубокой кастомизации под любые задачи

wapps platform
R
REST M2M - Архитектура машинного API в Wepps

Вам нужно создать REST API для работы с данными таблиц? Может быть, это интеграция с 1С, синхронизация с внешней CRM, или обмен данными между системами. Обычно это приводит к дублированию кода: одинаковые методы, одинаковые ошибки, одинаковые траты времени.

RestV1M2M решает эту проблему простым способом: два файла, явные методы на каждую таблицу и один универсальный utils для работы с любой таблицей.

17.07.2026
R
REST V1 APP - API для мобильных приложений

RestV1APP — это REST API специально для мобильных приложений и фронтенда. Возвращает агрегированные данные, готовые к отображению на экране.

V1 APP — бизнес-ориентированные endpoints (getHome, postCart, placeOrder).

14.07.2026
R
REST API - Общая архитектура Wepps Platform

REST API Wepps Platform — это система предоставления данных через HTTP endpoints.

Состоит из версионированных API с разными подходами: M2M для машинных интеграций (1С, CRM), APP для мобильных приложений и фронтенда, wepps для админки, v0 для тестовых endpoints и cli для командной строки.

08.07.2026