openspec: contacts-caldav-server — двусторонний синк (правки с телефона → в базу)
This commit is contained in:
@@ -47,29 +47,36 @@ curl -u estorozhenko:PASS -X PROPFIND -H 'Depth: 0' \
|
||||
```
|
||||
→ должен содержать `<C:addressbook>`.
|
||||
|
||||
### 2. Синк в contacts_extractor.py
|
||||
### 2. Двусторонний синк в contacts_extractor.py
|
||||
|
||||
В конец существующего пайплайна (после save_progress) добавляется шаг
|
||||
**sync_contacts_to_caldav()**, который:
|
||||
После извлечения/дедупликации запускается **sync_carddav()**, который
|
||||
выполняет двустороннюю сверку между `contacts.json` и адресной книгой
|
||||
Radicale.
|
||||
|
||||
1. Читает `contacts.json` → список контактов (email, full_name, phone,
|
||||
phone_secondary, position, company, address, contact_id).
|
||||
2. Генерирует vCard 4.0 на каждый контакт (FN, EMAIL, TEL, ORG, TITLE, ADR)
|
||||
— использует уже существующую generate_vcard() логику (перенести/
|
||||
отрефакторить в общий модуль или дублировать минимально).
|
||||
3. Определяет URL карточки: `<base>/estorozhenko/Контакты/<uid>.vcf`,
|
||||
uid = стабильный хэш от email (contact_id из contacts.json).
|
||||
4. Делает **PROPFIND Depth:1** по адресной книге, строит карту
|
||||
`uid → (ETag, href)` — чтобы знать, какие карточки уже есть (idempotency).
|
||||
5. Для каждого контакта:
|
||||
- если карточки ещё нет → **PUT** (без If-Match);
|
||||
- если есть и ETag тот же → пропуск (ничего не менять);
|
||||
- если есть и ETag отличается и локальные данные изменились → **PUT с
|
||||
If-Match: <etag>**; при 412 → лог конфликта, пропуск.
|
||||
6. `--prune-caldav`: сравнить uid на сервере с uid в базе, отсутствующие в
|
||||
базе → DELETE. По умолчанию — выключено.
|
||||
7. Записывает лог синка: `contacts/caldav-sync.log` (дата, контакт, действие
|
||||
created/updated/skipped/conflict/deleted).
|
||||
**Состояние:**
|
||||
- У каждого контакта в `contacts.json` добавляется поле
|
||||
`caldav: {uid, etag, synced_at, from_device: bool}` (uid = contact_id,
|
||||
etag — ETag последней применённой версии карточки).
|
||||
- Локальная база остаётся источником истины для дедупликации по `email`.
|
||||
|
||||
**Алгоритм (запуск 1):**
|
||||
1. `PROPFIND Depth:1` по `/estorozhenko/Контакты/` → карта `href → (ETag, content-ty`pe, vCard)` (vCard тянем GET'ом по href для сравнения содержимого, если нужно).
|
||||
2. **Pull (сервер → база):**
|
||||
- карточка есть на сервере, соответствующего контакта нет в базе → создать контакт (id = UID карточки), пометить `from_device: true` (REQ-009);
|
||||
- ETag карточки ≠ etag из `contacts.json[caldav.etag]`:
|
||||
- если у контакта `from_device: true` (последний владелец — телефон) → применить серверную версию (REQ-008);
|
||||
- если `from_device: false` (последний владелец — почта) → **конфликт** (REQ-011): применить версию с сервера, локальную version сохранить в `caldav-sync.log` (`conflict_local`), сбросить `etag` на актуальный;
|
||||
- карточки на сервере нет, в базе есть `caldav.uid` → контакт `deleted: true` (REQ-010).
|
||||
3. **Push (база → сервер):**
|
||||
- у контакта есть `caldav.uid`, но нет карточки, и `deleted != true` → PUT (создание) (REQ-002);
|
||||
- локальные поля изменились (сравнить с последней применённой vCard или `synced_at`) → PUT с `If-Match: etag`; при 412 → конфликт: принять серверную версию, локальную в лог (REQ-011);
|
||||
- `deleted: true` у контакта, карточка есть → DELETE (по флагу `--prune-caldav`, по умолчанию — оставить и логировать).
|
||||
4. `--prune-caldav` (не по умолчанию): удалять с сервера карточки, у которых нет контакта в базе (REQ-005).
|
||||
|
||||
**Ключевое правило конфликтов:** приоритет — сервер (телефон) как актуальная
|
||||
версия; локальная версия никогда не теряется (лог `conflict_local`). Это
|
||||
сознательное решение: правки руками на телефоне считаются более «живыми»,
|
||||
чем автопарсинг подписей писем.
|
||||
|
||||
**Клиент:** стандартный `urllib.request` + `base64` Basic Auth (без новых
|
||||
зависимостей) или `curl` через subprocess. Предпочтительно urllib — синк
|
||||
@@ -117,8 +124,15 @@ CALDAV_PASS — из `radicale/.env` (источник пароля один).
|
||||
- Создание книги: PROPFIND → 207 + addressbook RS.
|
||||
- PUT vCard → 201; повторный PUT/Bad Request при невалидной vCard → 400.
|
||||
- Повторный синк → 204/пропуск, дублей нет.
|
||||
- Изменение контакта → PUT 204 + обновлённая vCard.
|
||||
- Конфликт: на сервере вручную поменять vCard → синк даёт 412 + лог.
|
||||
- Изменение контакта в базе → PUT 204 + обновлённая vCard.
|
||||
- **Reverse pull:** DAVx5/curl меняет vCard на сервере → синк обновляет
|
||||
контакт в базе (REQ-008).
|
||||
- **Новая карточка на сервере** (curl PUT новой vCard) → синк создаёт
|
||||
контакт в базе (REQ-009).
|
||||
- **Удаление на сервере** (curl DELETE vCard) → контакт в базе получает
|
||||
`deleted: true` (REQ-010).
|
||||
- **Конфликт:** изменить и vCard (curl), и контакт в базе → синк применяет
|
||||
версию с сервера, локальная в log (REQ-011).
|
||||
- `--prune-caldav` удаляет отсутствующие карточки.
|
||||
- После синка: DAVx5 на телефоне видит контакты (ручная проверка).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user