Google Drive¶
GoogleDriveStrategy — провайдер для Google Drive. OAuth2 авторизация через сервисный аккаунт компании, поддержка Shared Drives, real-time синхронизация и cron-режимы.
Когда выбирать¶
- Команда уже использует Google Workspace.
- Нужно делиться файлами с внешними партнёрами через ссылки Google.
- Хочется bulk-просмотр в нативном UI Drive.
- Нет ограничений по объёму (Workspace платный).
Минусы по сравнению с локальным:
- OAuth-токен может протухать — нужно следить.
- Latency на каждую операцию (300-800ms).
- Ограничения на количество запросов в Google API.
Авторизация — OAuth2¶
Стратегия сохраняет в AttachmentStorage четыре поля:
google_client_id Char
OAuth2 client ID из Google Cloud Console.
google_client_secret Char
OAuth2 client secret. Хранится зашифрованным.
google_access_token Text
Текущий access token. Действует ~1 час.
google_refresh_token Text
Refresh token. Не протухает (до отзыва пользователем). Используется для автообновления access token.
Scopes¶
Стратегия запрашивает два scope:
SCOPES = [
"https://www.googleapis.com/auth/drive", # полный доступ
"https://www.googleapis.com/auth/drive.file", # доступ к файлам приложения
]
Первый шире — нужен для работы с Shared Drives и поиска папок по имени.
Поток OAuth¶
sequenceDiagram
participant U as User
participant API as FARA API
participant G as Google
participant DB as PostgreSQL
U->>API: GET /attachments_google/oauth/start
API-->>U: redirect → accounts.google.com
U->>G: authorize
G-->>U: redirect → /oauth/callback?code=...
U->>API: GET /oauth/callback?code=...
API->>G: POST token endpoint<br/>code + client_id/secret
G-->>API: access_token, refresh_token
API->>DB: UPDATE storage SET tokens=...
API-->>U: 200 OK, авторизация прошла
Автообновление токена¶
Перед каждой операцией стратегия вызывает get_credentials(). Если access_token истекает в ближайшие 5 минут — автоматически обновляется через refresh_token. Новый сохраняется в AttachmentStorage.
async def get_credentials(self, storage):
creds = Credentials(
token=storage.google_access_token,
refresh_token=storage.google_refresh_token,
...
)
needs_refresh = (
creds.expiry and creds.expiry - datetime.utcnow() < timedelta(minutes=5)
)
if needs_refresh and creds.refresh_token:
creds.refresh(Request())
await storage.update(AttachmentStorage(
google_access_token=creds.token,
...
))
return creds
Shared Drives¶
Если файлы должны лежать в общем диске компании (а не на личном My Drive), включаются Shared Drives:
google_team_enabled bool
Использовать ли Shared Drive вместо личного Drive.
google_team_id Char
ID Shared Drive (из URL: drive.google.com/drive/folders/{ID}).
При включённом флаге к каждому API-запросу добавляются параметры:
{
"supportsAllDrives": True,
"driveId": storage.google_team_id,
"corpora": "drive",
"includeItemsFromAllDrives": True,
}
Это критично — без них API будет искать файлы только в My Drive сервисного аккаунта.
Структура папок¶
При первой загрузке файла к записи стратегия:
- Смотрит подходящий
AttachmentRouteдляres_model. - Применяет
pattern_root→ создаёт корневую папку модели (Sales Orders/). - Применяет
pattern_record→ создаёт папку записи (SO-0000042-ClientA/). - Загружает файл туда.
- Сохраняет
parent_idвAttachmentCache, чтобы следующие файлы той же записи легли в эту же папку без повторного поиска.
graph TB
R[Route<br/>model=sale<br/>pattern_root=Sales Orders<br/>pattern_record=SO-0000{id}-{name}] --> RootCheck{Папка<br/>Sales Orders/<br/>есть?}
RootCheck -->|нет| CreateRoot[Создать]
RootCheck -->|да| RootId
CreateRoot --> RootId[parent_id=A]
RootId --> RecCheck{Папка<br/>SO-0000042-ClientA/<br/>есть?}
RecCheck -->|нет| CreateRec[Создать]
RecCheck -->|да| RecId
CreateRec --> RecId[parent_id=B]
RecId --> Cache[Сохранить в Cache:<br/>storage,sale,42 → B]
Cache --> Upload[Upload file → parent_id=B]
Real-time vs cron¶
- Real-time (
enable_realtime=true): каждая загрузка через FARA сразу льётся в Google. Латентность операции = 0.5-2 сек на типичный файл, видна пользователю. - One-way cron: файл сначала пишется локально через FileStore, в облако улетает в фоне. Пользователь видит мгновенный успех, ошибки облака не блокируют сохранение.
- Two-way cron: дополнительно тянет вниз файлы, которые менеджер положил в Drive вручную (минуя FARA).
- Routes cron: при переименовании записей (например, поле
nameлида) переименовывает соответствующие папки в Drive.
Размещение файлов в обход Route¶
Если res_model без специального Route — используется fallback Route (model=NULL, priority=0). Имя корневой папки по умолчанию {model} (то есть просто имя таблицы).
Что использует библиотека¶
Внутри стратегии — googleapiclient.discovery.build("drive", "v3", credentials=...). Все вызовы оборачиваются в asyncio.to_thread(...), потому что googleapiclient синхронный.
async def _drive_call(self, drive, fn, *args, **kwargs):
return await asyncio.to_thread(fn, *args, **kwargs)
Ограничения Google API¶
- Лимит запросов: 1000 запросов/100 сек на пользователя. На массовых импортах легко упереться. Стратегия делает paced uploads.
- Размер файла через resumable upload: 5 ТБ. Через простой upload — 5 МБ. Стратегия использует
MediaFileUpload(resumable=True)всегда. - Расширение в имени файла: Google Drive отделяет MIME-тип от имени. FARA пишет MIME явно, имя берётся из
Attachment.name.
Известные нюансы¶
Один аккаунт — один storage
Если хочется иметь два разных Google аккаунта (например, разделить личные и корпоративные файлы) — заводится два AttachmentStorage с разными OAuth, и активным может быть только один. Переключение через set_active().
Файлы, удалённые в Drive вручную
Если включён file_missing_cloud=cloud — при следующей попытке прочитать файл (которого уже нет в Drive) запись Attachment тоже удалится. Если nothing — запись останется, скачивание вернёт 404.