Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ All notable changes to the Confluence Page Exporter tool are documented in this

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Added

- Write support for Confluence Cloud: `upload update` / `upload create` /
`upload merge` and all attachment operations now work on Cloud — Cloud
support is complete. Pages are created and updated via REST API v2 (the
numeric space id is resolved from the key automatically; a version conflict
arrives as 409 and is handled as before), attachments are uploaded through
the v1 endpoints Atlassian kept on Cloud. Verified end-to-end against a
live Cloud site, including both-sides conflict detection.

### Fixed

- Removed the phantom "changed on server" in `compare` for pages whose macros
were authored without service attributes: Confluence adds
`ac:schema-version` to macros on save, and the Cloud editor stamps
`local-id`/`ac:local-id` onto elements — the normalizer now ignores these
attributes during comparison (uploaded content is not affected). The
canonicalization epoch is bumped to 3: hashes stored in markers are
recomputed on each page's next sync; until then local-edit detection falls
back to file modification times, once.

## [2.17.0] — 2026-07-02

### Added
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@

Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/).

## [Unreleased]

### Добавлено

- Поддержка записи на Confluence Cloud: `upload update` / `upload create` /
`upload merge` и все операции с вложениями теперь работают на Cloud —
поддержка Cloud стала полной. Страницы создаются и обновляются через REST
API v2 (числовой идентификатор пространства резолвится из ключа
автоматически, конфликт версий приходит как 409 и обрабатывается как
раньше), вложения загружаются через сохранившиеся на Cloud v1-эндпоинты.
Проверено сквозным тестом на живом Cloud-сайте, включая детект конфликтов
«изменено с обеих сторон».

### Исправлено

- Убран фантомный «изменено на сервере» в `compare` для страниц, чьи макросы
были созданы без служебных атрибутов: Confluence при сохранении добавляет
макросам `ac:schema-version`, а редактор Cloud штампует на элементы
`local-id`/`ac:local-id` — нормализатор теперь игнорирует эти атрибуты при
сравнении (на отправляемый контент это не влияет). Эпоха канонизации
поднята до 3: сохранённые в маркерах хэши пересчитаются при следующей
синхронизации каждой страницы, до этого детект локальных правок
одноразово опирается на время изменения файлов.

## [2.17.0] — 2026-07-02

### Добавлено
Expand Down
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ The tool supports:
- attachments as separate files
- a marker file `.id<pageId>_<version>` (its body is JSON with the original title and space key) for stable page identification and version tracking
- Authentication modes: `--auth-type onprem` and `--auth-type cloud`; by default the type is auto-detected from `--base-url` (`*.atlassian.net` hosts → `cloud`)
- Confluence Cloud support (REST API v2) — read-only for now: `download update`/`download merge`, `compare`, and the `confluence_ping` / `confluence_get_page_content` MCP tools. Write operations (upload, attachments) on Cloud fail with a clear error and arrive in upcoming releases
- Full Confluence Cloud support (REST API v2 + the v1 endpoints Atlassian kept): every command — download, upload, compare, attachments — works the same against Server/DC and Cloud; Cloud's numeric space ids are transparently resolved from keys
- Multi-layered configuration with priority: CLI > environment variables > file > default value
- Global `--verbose` flag for detailed (debug-level) output
- Dry-run support where applicable
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
- вложения как отдельные файлы
- файл-маркер `.id<pageId>_<version>` (в теле — JSON с оригинальным заголовком и ключом пространства) для стабильной идентификации страницы и отслеживания версии
- Режимы аутентификации: `--auth-type onprem` и `--auth-type cloud`; по умолчанию тип определяется автоматически по `--base-url` (хосты `*.atlassian.net` → `cloud`)
- Поддержка Confluence Cloud (REST API v2) — пока только чтение: `download update`/`download merge`, `compare`, MCP-инструменты `confluence_ping` и `confluence_get_page_content`. Операции записи (upload, вложения) на Cloud вернут понятную ошибку и появятся в следующих релизах
- Полная поддержка Confluence Cloud (REST API v2 + сохранившиеся v1-эндпоинты): все команды — download, upload, compare, вложения — работают одинаково на Server/DC и Cloud; числовые идентификаторы пространств Cloud прозрачно резолвятся из ключей
- Многоуровневая конфигурация с приоритетом: CLI > переменные окружения > файл > значение по умолчанию
- Глобальный параметр `--verbose` для подробного (debug-level) вывода
- Поддержка dry-run там, где применимо
Expand Down
4 changes: 2 additions & 2 deletions docs/mcp/agent-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ These same instructions are also published as `docs/mcp/agent-instructions.md` i

- **Sandbox root.** The server was started with `--root-dir <path>`. Every path you pass to a tool is resolved relative to that root; absolute paths are accepted only when they lie inside it. Outside paths fail with `OUT_OF_SANDBOX`. **You cannot widen this sandbox** — it is set at server startup and ignores env vars and config files.
- **Read-only mode.** If the server was started with `--read-only`, all `confluence_upload_*` tools refuse with `READ_ONLY_VIOLATION`. `confluence_download_*`, `confluence_compare`, `confluence_ping`, and `confluence_get_page_content` remain available.
- **Confluence Cloud (read-only phase).** Against a Cloud site (auto-detected from a `*.atlassian.net` base URL, or `AuthType=cloud`) the download/compare/ping/get-page-content tools work via the Cloud v2 API; all `confluence_upload_*` tools currently fail with `NOT_SUPPORTED` — Cloud write support has not shipped yet. Do not retry; report it to the user.
- **Confluence Cloud.** Cloud sites (auto-detected from a `*.atlassian.net` base URL, or `AuthType=cloud`) are fully supported — every tool works the same as against Server/Data Center. No Cloud-specific behaviour to account for.
- **Space keys (multi-space).** A default `spaceKey` may be configured server-side, and most tools accept an optional `spaceKey` that overrides it. For an **already-synced tree you normally don't need to pass `spaceKey` at all** — each page records its space locally (in the `.id` marker) and the server is the authority. Passing a `spaceKey` that contradicts a page's real space makes the upload tools **error** rather than silently mis-place pages. New child pages are always created in their parent's space (a Confluence tree lives in exactly one space). See "Working with multiple spaces" below.

## Local layout
Expand Down Expand Up @@ -64,7 +64,7 @@ Every tool returns one of two shapes:
| `INVALID_ARGS` | You passed contradictory or missing arguments. Read `error` and try again. |
| `OUT_OF_SANDBOX` | A path argument resolved outside `--root-dir`. Use a path inside the sandbox. |
| `READ_ONLY_VIOLATION` | You called an upload tool on a read-only server. Stop trying. |
| `NOT_SUPPORTED` | The operation is not available for this deployment — currently any write against Confluence Cloud (read-only Cloud phase). Stop trying and tell the user. |
| `NOT_SUPPORTED` | The operation is not available for this deployment. Stop trying and tell the user. |
| `AUTH_FAILED` | 401/403 from Confluence or a local FS auth error. Report to the user. |
| `PAGE_NOT_FOUND` | Page ID/title doesn't exist on the server. Re-check inputs. |
| `NETWORK_ERROR` | `HttpRequestException` with no status — DNS/TCP/SSL. The server already retried up to 3× with backoff, so this means all retries failed. Call `confluence_ping` to confirm the server can reach Confluence. |
Expand Down
65 changes: 65 additions & 0 deletions src/ConfluencePageExporter/Services/ConfluenceApiHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using System.Net.Http.Headers;

namespace ConfluencePageExporter.Services;

Expand Down Expand Up @@ -42,4 +43,68 @@ internal static async Task EnsureSuccessAsync(HttpResponseMessage response, stri
/// </summary>
internal static string EscapeCql(string value) =>
value.Replace("\\", "\\\\").Replace("\"", "\\\"");

private const string DefaultAttachmentMediaType = "application/octet-stream";

/// <summary>
/// Minimal extension → media-type map for the attachment upload path.
/// Confluence infers an attachment's media type from the multipart part's
/// Content-Type; when we send none it falls back to the filename
/// extension, which has no answer for extensionless files. This covers
/// the common attachment kinds so a new attachment keeps its proper type,
/// with <see cref="DefaultAttachmentMediaType"/> as the catch-all.
/// </summary>
private static readonly IReadOnlyDictionary<string, string> ExtensionMediaTypes =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
[".png"] = "image/png",
[".jpg"] = "image/jpeg",
[".jpeg"] = "image/jpeg",
[".gif"] = "image/gif",
[".bmp"] = "image/bmp",
[".webp"] = "image/webp",
[".svg"] = "image/svg+xml",
[".pdf"] = "application/pdf",
[".txt"] = "text/plain",
[".csv"] = "text/csv",
[".xml"] = "application/xml",
[".json"] = "application/json",
[".html"] = "text/html",
[".htm"] = "text/html",
[".zip"] = "application/zip",
[".drawio"] = "application/vnd.jgraph.mxfile",
};

/// <summary>
/// Resolves the media type to stamp on an uploaded attachment part.
/// Prefers the caller-supplied type (on update: the existing attachment's
/// stored server type, so the update never changes it), then the extension
/// map, and finally <see cref="DefaultAttachmentMediaType"/> for
/// extensionless/unknown files.
/// </summary>
private static string ResolveAttachmentContentType(string fileName, string? preferredMediaType)
{
if (!string.IsNullOrWhiteSpace(preferredMediaType))
return preferredMediaType;

var ext = Path.GetExtension(fileName);
if (!string.IsNullOrEmpty(ext) && ExtensionMediaTypes.TryGetValue(ext, out var mapped))
return mapped;

return DefaultAttachmentMediaType;
}

/// <summary>
/// Sets the multipart file part's Content-Type from
/// <see cref="ResolveAttachmentContentType"/>, falling back to
/// <see cref="DefaultAttachmentMediaType"/> if the resolved value is not a
/// valid media-type header (e.g. an odd server-reported value).
/// </summary>
internal static void ApplyAttachmentContentType(ByteArrayContent part, string fileName, string? preferredMediaType)
{
var resolved = ResolveAttachmentContentType(fileName, preferredMediaType);
if (!MediaTypeHeaderValue.TryParse(resolved, out var parsed))
MediaTypeHeaderValue.TryParse(DefaultAttachmentMediaType, out parsed);
part.Headers.ContentType = parsed;
}
}
Loading