惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
IT之家
IT之家
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
N
News | PayPal Newsroom
Cloudbric
Cloudbric
Webroot Blog
Webroot Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Simon Willison's Weblog
Simon Willison's Weblog
Spread Privacy
Spread Privacy
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Vulnerabilities – Threatpost
Attack and Defense Labs
Attack and Defense Labs
C
Cyber Attacks, Cyber Crime and Cyber Security
Cisco Talos Blog
Cisco Talos Blog
C
Cybersecurity and Infrastructure Security Agency CISA
H
Hackread – Cybersecurity News, Data Breaches, AI and More
P
Proofpoint News Feed
阮一峰的网络日志
阮一峰的网络日志
S
Secure Thoughts
T
Tor Project blog
Latest news
Latest news
aimingoo的专栏
aimingoo的专栏
V2EX - 技术
V2EX - 技术
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Securelist
T
Tenable Blog
N
Netflix TechBlog - Medium
Google Online Security Blog
Google Online Security Blog
博客园 - Franky
T
Troy Hunt's Blog
量子位
大猫的无限游戏
大猫的无限游戏
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
MongoDB | Blog
MongoDB | Blog
H
Heimdal Security Blog
D
Docker
W
WeLiveSecurity
Stack Overflow Blog
Stack Overflow Blog
G
Google Developers Blog
博客园 - 叶小钗
腾讯CDC
The Hacker News
The Hacker News
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
Project Zero
Project Zero
Martin Fowler
Martin Fowler

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
FARA CRM. Как мы работаем с файлами
telkatrax (f · 2026-04-30 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение9 мин

Охват и читатели1.2K

Обзор

CRM с файлами в облаке: как мы перестали хранить вложения у себя — и что из этого получилось

Статья о том, как мы прикрутили Google Drive и Яндекс.Диск к CRM-системе FARA так, чтобы пользователь не замечал, в каком хранилище лежит файл, а вся команда могла работать с документами и через CRM, и напрямую через облако.

Канбан файлов, облачные файлы доступны для редактирования из CRM

Канбан файлов, облачные файлы доступны для редактирования из CRM

Зачем вообще выносить файлы из CRM

Если коротко — из-за того, что CRM перестала быть единственным местом работы с документом. Раньше «прикрепить файл к сделке» означало «загрузить копию на сервер CRM, а если кому-то нужно отредактировать — скачать, поправить, загрузить заново». Сейчас от документа ждут другого:

  • его открывают в облаке и редактируют вдвоём с менеджером прямо во время звонка с клиентом;

  • бухгалтер вообще не заходит в CRM — у неё открыт Drive, и она видит все договоры по сделкам в одной папке;

  • скан акта прилетает на корпоративный диск через мобильное приложение — и должен сам появиться в карточке сделки, без ручного «прикрепить».

«Не занимать место на диске нашего сервера» — это самая скучная причина из всех, она просто прилагается. Главное — то, что облако становится частью рабочей среды команды, а CRM — одним из окон в эту среду. Не единственным.

Дальше расскажу, как это устроено в FARA CRM на уровне кода и интерфейса. Модули attachments_google и attachments_yandex — открытые и бесплатные, и архитектурно сделаны так, что добавить, скажем, OneDrive или Dropbox — это написать ещё одну стратегию по тому же контракту.

Четыре идеи, которые двигают всё остальное

Прежде чем нырять в код, обозначу, что именно мы хотели получить. Это четыре сценария, каждый из которых раньше упирался в «ну, у нас файлы внутри CRM, поэтому никак».

1. Файл редактируется и просматривается онлайн средствами облака. Договор лежит в Google Drive — значит, его открывает Google Docs, не наш просмотрщик. Презентация лежит в Яндекс.Диске — её открывает онлайн-редактор Яндекса. CRM не пытается быть Office. Она пишет правильную ссылку и доверяет облаку всё остальное.

2. Часть команды работает напрямую с диском. У бухгалтера, юриста, аудитора — может вообще не быть учётки в CRM. Им открыли доступ к папке Sales/ на Google Drive — и всё, они видят актуальную структуру по сделкам. Никто им не пересылает документы.

3. Обратная синхронизация — файл, попавший в облако, появляется в CRM. Менеджер кинул скан в папку сделки прямо с телефона. Через пару минут он увидит этот файл в карточке в CRM, без ручных действий. По такому же принципу синхронизируются переименования и удаления (если включить это поведение).

4. Динамическая раскладка по стадиям. У сделки есть стадия (новая → согласование → оплачена → завершена). Файлы должны жить в подпапке, соответствующей стадии. Перетащил карточку в kanban на «Оплачена» — и все её файлы автоматически переехали в папку Оплаченные/SO-0000123/. Это то, что снаружи выглядит как магия, а на самом деле — продуманные паттерны имён папок и пара хуков.

Вокруг этих четырёх идей и крутится вся остальная статья.

Стадии сделок на kanban-доске. Перетаскивание карточки запускает перенос файлов в облаке.

Стадии сделок на kanban-доске. Перетаскивание карточки запускает перенос файлов в облаке.

Архитектурно: Strategy + Route

Чтобы CRM не превратилась в спагетти из «если Google — то так, если локально — то иначе, если Яндекс — то ещё как-то», у нас два уровня абстракции.

Strategy — это как именно класть файл. То есть конкретные операции CRUD над файлом в облаке: создать, прочитать, обновить, удалить, плюс создать папку, плюс получить URL для просмотра. У каждого хранилища своя стратегия:

class StorageStrategyBase(ABC):
    strategy_type: str = ""

    @abstractmethod
    async def create_file(self, storage, attachment, content, filename, ...): ...
    @abstractmethod
    async def read_file(self, storage, attachment) -> bytes | None: ...
    @abstractmethod
    async def update_file(self, storage, attachment, content=None, ...): ...
    @abstractmethod
    async def delete_file(self, storage, attachment) -> bool: ...
    @abstractmethod
    async def create_folder(self, storage, folder_name, parent_id=None, ...): ...
    @abstractmethod
    async def validate_connection(self, storage) -> bool: ...
    @abstractmethod
    async def get_file_url(self, storage, attachment) -> str | None: ...

Стратегий уже три: FileStoreStrategy (локальная папка на сервере, дефолт), GoogleDriveStrategy и YandexDiskStrategy. Регистрация — одна строчка в app.py модуля:

register_strategy(GoogleDriveStrategy)

Route — это куда именно класть файл и под каким именем. Маршрут не знает, как делается create_file, ему всё равно. Он отвечает за то, чтобы для сделки Sale#123 файлы попали именно в Sales Orders/SO-0000123-Иванов/, а не в плоскую кучу. Про маршруты — отдельный раздел чуть ниже.

Эти две сущности живут отдельно. Стратегию пишут один раз под облако и забывают. Маршруты настраивает админ кликами в интерфейсе.

Связка Strategy + Route: что куда складывать и как именно класть.

Связка Strategy + Route: что куда складывать и как именно класть.


Модули хранилищ: что внутри Google и Яндекса

Снаружи это просто пункт в выпадающем «Тип хранилища»: FileStore, Google Drive, Yandex Disk. Под капотом — отдельные модули attachments_google и attachments_yandex.

Каждый модуль состоит из трёх частей:

  • Mixin — расширяет модель AttachmentStorage, добавляя свои поля (google_credentials, google_team_id, yandex_access_token, yandex_refresh_token и т. д.). Делается через декоратор @extend, без правок ядра.

  • Strategy — реализация интерфейса выше для конкретного API.

  • OAuth-роутер — два эндпоинта: /{provider}/auth/{storage_id} (стартует авторизацию, возвращает authorization_url) и /{provider}/callback (обменивает код на токены, сохраняет в storage).

В коде это лаконично и зеркально между провайдерами. Например, обновление access-токена в Яндексе:

async def _refresh_access_token(self, storage):
    async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
        response = await client.post(OAUTH_TOKEN_URL, data={
            "grant_type": "refresh_token",
            "refresh_token": storage.yandex_refresh_token,
            "client_id": storage.yandex_client_id,
            "client_secret": storage.yandex_client_secret,
        })
    # ...сохраняем новые токены в storage

В Google логика обновления делегирована официальной либе google-auth, но сценарий тот же: при истечении токен молча обновляется, в БД пишутся свежие credentials, пользователь ничего не замечает.

 Форма настройки хранилища Google Drive в FARA CRM.

Форма настройки хранилища Google Drive в FARA CRM.

Форма настройки хранилища Яндекс.Диск.

Форма настройки хранилища Яндекс.Диск.

Что важно: пользователь FARA, открывая карточку сделки, визуально не видит, где лежит файл. Превью открывается, файл скачивается, ссылка работает — а внутри CRM, по локальному пути или из облака, для него не имеет значения. Этот принцип нам был принципиален: облако — это инфраструктура, а не отдельный «второй интерфейс для облачных вложений».


Маршруты — главная фича, ради которой стоит читать дальше

Маршрут (AttachmentRoute) — это правило, которое отвечает на два вопроса для каждой сущности CRM: как назвать корневую папку этой сущности в облаке и как назвать папку конкретной записи внутри неё.

Модель упрощённо:

class AttachmentRoute(DotModel):
    name: str           # человекочитаемое имя
    model: str | None   # 'sale', 'lead', None для fallback-маршрута
    priority: int       # выше число — выше приоритет
    pattern_root: str   # шаблон корневой папки модели
    pattern_record: str # шаблон папки записи
    flat: bool          # без подпапок на запись?
    filter: list | None # фильтр записей (DSL CRM)
    storage_id: M2O     # к какому хранилищу относится
    active: bool

В pattern_root и pattern_record можно использовать переменные: имя модели, ID, любые поля записи, плюс хелпер zfill (дополнение нулями до семи знаков). Простейший пример:

pattern_root   = "Sales Orders"
pattern_record = "SO-{zfill(id)}-{name}"

Получаем структуру:

Drive/
└── Sales Orders/
    ├── SO-0000123-Иванов/
    │   ├── contract.pdf
    │   └── invoice.pdf
    └── SO-0000124-Петров/
        └── proposal.docx

Это уже папка, в которую можно дать прямой доступ бухгалтеру — и она будет осмысленно выглядеть для человека, не знающего CRM. Не attachments_42_storage_3_file_uuid.bin, а SO-0000123-Иванов/contract.pdf.

Настройка маршрута: какие сущности и как раскладывать в облаке.

Настройка маршрута: какие сущности и как раскладывать в облаке.

Структура папок в Google Drive, собранная по маршруту.

Структура папок в Google Drive, собранная по маршруту.

Приоритеты, фильтры и fallback

Маршрутов на одну модель может быть несколько. Например:

  • маршрут «VIP-клиенты» с фильтром [("client_tier", "=", "vip")], приоритет 100, паттерн VIP/{name};

  • обычный маршрут на модель sale, приоритет 10;

  • fallback-маршрут (model = None) — для всего, что не попало ни в один специфичный.

Логика выбора маршрута:

@classmethod
async def get_route_for_attachment(cls, res_model, res_id):
    if res_model and res_id:
        # 1) специфичные по модели — по приоритету
        specific = await cls.search(filter=[
            ("active", "=", True),
            ("model", "=", res_model),
        ], sort="priority", order="DESC")
        for route in specific:
            if await route._check_record_in_filter(res_id):
                return route
    # 2) fallback (model=None) — по приоритету
    fallback = await cls.search(filter=[
        ("active", "=", True),
        ("model", "=", None),
    ], sort="priority", order="DESC")
    return fallback[0] if fallback else None

То есть «специфичный маршрут» всегда побеждает fallback — даже если у fallback приоритет выше. Это важно: один админ настраивает общий fallback на всю CRM, другой — кастомные маршруты под конкретные кейсы, и они не дерутся.


Три типа синхронизации

В настройках хранилища есть три флага, которые описывают, как именно CRM и облако обмениваются файлами:

enable_realtime      = Boolean(default=False, ...)  # сразу при создании
enable_one_way_cron  = Boolean(default=False, ...)  # CRM → облако, по расписанию
enable_two_way_cron  = Boolean(default=False, ...)  # CRM ↔ облако, по расписанию

И отдельно — как реагировать на расхождения:

file_missing_cloud   # что делать, если файл есть в CRM, но нет в облаке
file_missing_local   # что делать, если файл есть в облаке, но нет в CRM

Realtime (онлайн). Файл загружен в карточку сделки → стратегия немедленно отправляет его в облако. Это обычное поведение, когда облако — основное хранилище, а CRM — клиент.

One-way cron. По расписанию (например, раз в час) выполняется задача-синхронизатор: всё, что было загружено локально и ещё не оказалось в облаке, доезжает до него батчем. Полезно, если синхронные загрузки в облако нежелательны — например, чтобы UI не зависал на больших файлах.

Two-way cron — обратная синхронизация. Тот самый сценарий «менеджер кинул скан в папку сделки прямо с телефона». Cron обходит папки облака, по custom_properties (route_id, res_model, res_id) понимает, к какой записи относится файл, и создаёт Attachment в CRM с пустым content, но заполненным storage_file_id. Дальше это нормальное вложение — открывается, скачивается, удаляется как любое другое.


Google Shared Drive (Team)

Отдельная неочевидная фича — поддержка Shared Drive в Google. Это командные диски, где владельцем выступает не отдельный сотрудник, а организация. Если менеджер увольняется — его личный Drive уходит вместе с ним, и все файлы по сделкам приходится переподключать. Если файлы лежат на Shared Drive, ничего такого не случится.

В модуле attachments_google это два дополнительных поля:

google_team_enabled: bool = Boolean(default=False, string="Use Shared Drive")
google_team_id:      str  = Char(string="Shared Drive ID")

И в стратегии — корректные supportsAllDrives=True / supportsTeamDrives=True в каждом вызове Drive API. Снаружи — просто чекбокс в форме хранилища.

Для команд это критично: подключаться к корпоративным дискам, а не к личным. Многие интеграции этого не умеют — а у нас это две строчки в форме.


Безопасность OAuth: что мы храним и как

Тема, которую обычно не любят показывать в статьях, потому что «и так понятно». Покажу, как именно у нас.

Логины и пароли пользователя облака мы не храним. Только токены OAuth2, выданные нам с явного согласия пользователя на конкретные scope-ы.

Для Google:

  • google_json_credentials — содержимое credentials.json (client_id / client_secret приложения), загружается админом один раз. Это credentials самого приложения, а не пользователя.

  • google_credentials — сериализованные OAuth2 credentials с access/refresh токенами. Обновляются автоматически при истечении.

  • google_refresh_token — чтобы можно было обновлять access-токен молча, без переавторизации.

  • Scope-ы: drive и drive.file. Ничего лишнего.

Для Яндекса:

  • yandex_client_id / yandex_client_secret — параметры зарегистрированного OAuth-приложения.

  • yandex_access_token / yandex_refresh_token / yandex_token_expires_at.

  • Scope-ы: cloud_api:disk.read и cloud_api:disk.write.

OAuth-флоу — стандартный authorization code: пользователь жмёт «Авторизовать» в форме хранилища, ему открывается окно провайдера, после согласия провайдер редиректит на наш endpoint /{provider}/callback?code=...&state=.... Параметр state мы заранее сохраняем в storage (*_verify_code) и проверяем при возврате — это защита от CSRF на колбэке. Использованный state обнуляется сразу после обмена.

Один важный нюанс реализации, которым легко пренебречь: при скачивании файла Яндекс отвечает 302 на CDN, и httpx по умолчанию редиректы не следует. Поэтому в стратегии явно ставим follow_redirects=True — но только для анонимных запросов на upload/download URL, не для API-запросов с Authorization: OAuth …. Иначе при каком-нибудь экзотическом редиректе токен может уехать на чужой хост.

# OK — анонимный download URL, можно идти за редиректом
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT, follow_redirects=True) as c:
    file_resp = await c.get(href)

# НЕ OK — авторизованные запросы с OAuth-заголовком, follow_redirects не нужен
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as c:
    response = await c.request(method, url, headers={"Authorization": f"OAuth {token}"}, ...)

Это та самая граница, на которой случаются громкие истории с утечкой токенов в логи или прокси.

 Окно согласия на стороне провайдера — единственный момент, когда администратор подтверждает доступ.

Окно согласия на стороне провайдера — единственный момент, когда администратор подтверждает доступ.


Что получили в итоге

Если собрать в одно предложение: CRM перестала быть единственным интерфейсом к файлам — и от этого выиграли все.

Менеджеру всё равно, где лежит файл: он открывает карточку сделки, нажимает на вложение, получает превью или редактор. Бухгалтеру не нужна учётка в CRM — у него есть папка Sales Orders на корпоративном Google Drive, и он там полноправный житель. Админ настраивает раскладку под бизнес-процесс кликами в интерфейсе, а не правкой кода. Разработчик добавляет новое облако одной стратегией.

Модули attachments_google и attachments_yandex в FARA CRM — бесплатные, открытые, и устроены симметрично, чтобы их легко было читать рядом и понимать, чем именно отличаются провайдеры (а отличаются они меньше, чем кажется).

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

P. S. Если интересно посмотреть, как именно устроен модуль интеграции — в ближайшее время выложим разбор attachments_yandex отдельной статьёй: что внутри стратегии, как сделан OAuth, как обходим квоты Яндекса. Подписывайтесь.