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
21 changes: 21 additions & 0 deletions CHANGELOG.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ 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

- Confluence deployment-type auto-detection from `--base-url`: `*.atlassian.net`
hosts are treated as Cloud, everything else as on-prem; an explicit
`--auth-type` takes precedence. An invalid `--auth-type` value now fails with
a clear error instead of being silently ignored, and `config show` displays
the effective mode (e.g. `cloud (auto-detected)`). The `cloud` mode is
preparatory for now: the existing (v1) API client is used and a warning is
printed — full Cloud API support arrives in upcoming releases.

### Changed

- HTTP request retries honour the server's `Retry-After` header (429/503): the
wait is taken from the header, capped at 60 seconds — matters for Confluence
Cloud rate limits.
- POST requests are now retried on 429: the rate limiter rejects the request
before processing, so duplicated side effects are impossible. POST is still
never retried on network errors or 5xx.

## [2.16.0] — 2026-06-24

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

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

## [Unreleased]

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

- Автоопределение типа развёртывания Confluence по `--base-url`: хосты
`*.atlassian.net` считаются Cloud, остальные — on-prem; явный `--auth-type`
приоритетнее. Недопустимое значение `--auth-type` теперь приводит к понятной
ошибке вместо тихого игнорирования, а `config show` показывает эффективный
режим (например `cloud (auto-detected)`). Режим `cloud` пока подготовительный:
используется прежний API-клиент (v1) и выводится предупреждение — полноценная
поддержка Cloud API появится в следующих релизах.

### Изменено

- Повторы HTTP-запросов учитывают заголовок `Retry-After` из ответа сервера
(429/503): ожидание берётся из заголовка с потолком 60 секунд — важно для
рейт-лимитов Confluence Cloud.
- POST-запросы теперь повторяются при 429: рейт-лимитер отклоняет запрос до
обработки, поэтому дублирование побочных эффектов исключено. При сетевых
ошибках и 5xx POST по-прежнему не повторяется.

## [2.16.0] — 2026-06-24

### Добавлено
Expand Down
14 changes: 7 additions & 7 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The tool supports:
- an `index.html` file with the page content in storage representation
- 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`
- Authentication modes: `--auth-type onprem` and `--auth-type cloud`; by default the type is auto-detected from `--base-url` (`*.atlassian.net` hosts → `cloud`)
- 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 Expand Up @@ -294,7 +294,7 @@ Force-downloads a Confluence page (or subtree) to the local disk. Differing page
- `--page-id` or `--page-title` (exactly one must be specified)
- `--output-dir` (required)
- `--recursive` (optional)
- `--auth-type onprem|cloud` (optional, default `onprem`)
- `--auth-type onprem|cloud` (optional, default: auto-detected from `--base-url`)
- `--dry-run` (optional)
- `--report` (optional)

Expand Down Expand Up @@ -324,7 +324,7 @@ Downloads pages from the server, overwriting only those that are newer on the se
- `--page-id` or `--page-title` (exactly one must be specified)
- `--output-dir` (required)
- `--recursive` (optional)
- `--auth-type onprem|cloud` (optional, default `onprem`)
- `--auth-type onprem|cloud` (optional, default: auto-detected from `--base-url`)
- `--dry-run` (optional)
- `--report` (optional)

Expand Down Expand Up @@ -355,7 +355,7 @@ Force-uploads local pages to the server. Differing pages are overwritten with th
- `--page-id` or `--page-title` (optional, explicit root page)
- `--recursive` (optional)
- `--multi-tree` (optional; `--source-dir` points to a directory with several trees — each is processed with its own space; incompatible with `--page-id`/`--page-title`)
- `--auth-type onprem|cloud` (optional, default `onprem`)
- `--auth-type onprem|cloud` (optional, default: auto-detected from `--base-url`)
- `--dry-run` (optional)
- `--report` (optional)

Expand Down Expand Up @@ -408,7 +408,7 @@ If the same page's content was also changed on the server, the structural move i
- `--page-id` or `--page-title` (optional, explicit root page)
- `--recursive` (optional)
- `--multi-tree` (optional; `--source-dir` points to a directory with several trees — each is processed with its own space; incompatible with `--page-id`/`--page-title`)
- `--auth-type onprem|cloud` (optional, default `onprem`)
- `--auth-type onprem|cloud` (optional, default: auto-detected from `--base-url`)
- `--dry-run` (optional)
- `--report` (optional)

Expand Down Expand Up @@ -437,7 +437,7 @@ Creates new Confluence pages from local content.
- `--source-dir` (required)
- `--parent-id` or `--parent-title` (optional)
- `--recursive` (optional)
- `--auth-type onprem|cloud` (optional, default `onprem`)
- `--auth-type onprem|cloud` (optional, default: auto-detected from `--base-url`)
- `--dry-run` (optional)

### upload create example
Expand Down Expand Up @@ -470,7 +470,7 @@ With `--detect-source`, the Confluence version history is additionally analysed
- `--recursive` (optional)
- `--match-by-title` (optional)
- `--detect-source` (optional) — analyse version history to determine the source of renames and moves (extra API calls)
- `--auth-type onprem|cloud` (optional, default `onprem`)
- `--auth-type onprem|cloud` (optional, default: auto-detected from `--base-url`)

### Matching strategy

Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
- файл `index.html` с контентом страницы в storage representation
- вложения как отдельные файлы
- файл-маркер `.id<pageId>_<version>` (в теле — JSON с оригинальным заголовком и ключом пространства) для стабильной идентификации страницы и отслеживания версии
- Режимы аутентификации: `--auth-type onprem` и `--auth-type cloud`
- Режимы аутентификации: `--auth-type onprem` и `--auth-type cloud`; по умолчанию тип определяется автоматически по `--base-url` (хосты `*.atlassian.net` → `cloud`)
- Многоуровневая конфигурация с приоритетом: CLI > переменные окружения > файл > значение по умолчанию
- Глобальный параметр `--verbose` для подробного (debug-level) вывода
- Поддержка dry-run там, где применимо
Expand Down Expand Up @@ -294,7 +294,7 @@ ConfluencePageExporter upload merge --source-dir ./export/MyPage --recursive --r
- `--page-id` или `--page-title` (обязательно указать ровно один)
- `--output-dir` (обязательный)
- `--recursive` (опционально)
- `--auth-type onprem|cloud` (опционально, по умолчанию `onprem`)
- `--auth-type onprem|cloud` (опционально, по умолчанию — автоопределение по `--base-url`)
- `--dry-run` (опционально)
- `--report` (опционально)

Expand Down Expand Up @@ -324,7 +324,7 @@ ConfluencePageExporter download update \
- `--page-id` или `--page-title` (обязательно указать ровно один)
- `--output-dir` (обязательный)
- `--recursive` (опционально)
- `--auth-type onprem|cloud` (опционально, по умолчанию `onprem`)
- `--auth-type onprem|cloud` (опционально, по умолчанию — автоопределение по `--base-url`)
- `--dry-run` (опционально)
- `--report` (опционально)

Expand Down Expand Up @@ -355,7 +355,7 @@ ConfluencePageExporter --report download merge \
- `--page-id` или `--page-title` (опционально, явное указание корневой страницы)
- `--recursive` (опционально)
- `--multi-tree` (опционально; `--source-dir` указывает на каталог с несколькими деревьями — каждое обрабатывается со своим пространством; несовместимо с `--page-id`/`--page-title`)
- `--auth-type onprem|cloud` (опционально, по умолчанию `onprem`)
- `--auth-type onprem|cloud` (опционально, по умолчанию — автоопределение по `--base-url`)
- `--dry-run` (опционально)
- `--report` (опционально)

Expand Down Expand Up @@ -408,7 +408,7 @@ ConfluencePageExporter upload update \
- `--page-id` или `--page-title` (опционально, явное указание корневой страницы)
- `--recursive` (опционально)
- `--multi-tree` (опционально; `--source-dir` указывает на каталог с несколькими деревьями — каждое обрабатывается со своим пространством; несовместимо с `--page-id`/`--page-title`)
- `--auth-type onprem|cloud` (опционально, по умолчанию `onprem`)
- `--auth-type onprem|cloud` (опционально, по умолчанию — автоопределение по `--base-url`)
- `--dry-run` (опционально)
- `--report` (опционально)

Expand Down Expand Up @@ -437,7 +437,7 @@ ConfluencePageExporter --report upload merge \
- `--source-dir` (обязательный)
- `--parent-id` или `--parent-title` (опционально)
- `--recursive` (опционально)
- `--auth-type onprem|cloud` (опционально, по умолчанию `onprem`)
- `--auth-type onprem|cloud` (опционально, по умолчанию — автоопределение по `--base-url`)
- `--dry-run` (опционально)

### Пример upload create
Expand Down Expand Up @@ -470,7 +470,7 @@ ConfluencePageExporter upload create \
- `--recursive` (опционально)
- `--match-by-title` (опционально)
- `--detect-source` (опционально) — анализировать историю версий для определения источника переименований и перемещений (дополнительные API-вызовы)
- `--auth-type onprem|cloud` (опционально, по умолчанию `onprem`)
- `--auth-type onprem|cloud` (опционально, по умолчанию — автоопределение по `--base-url`)

### Стратегия сопоставления

Expand Down
20 changes: 19 additions & 1 deletion src/ConfluencePageExporter/Commands/ConfigShowCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private void PrintGlobal()
PrintValue(" Username", g.Username, "Global:Username");
PrintValue(" Token", Mask(g.Token), "Global:Token");
PrintValue(" SpaceKey", g.SpaceKey, "Global:SpaceKey");
PrintValue(" AuthType", g.AuthType ?? "onprem", "Global:AuthType");
PrintValue(" AuthType", DescribeAuthType(g), "Global:AuthType");
PrintValue(" Verbose", (g.Verbose ?? false).ToString(), "Global:Verbose");
PrintValue(" DryRun", (g.DryRun ?? false).ToString(), "Global:DryRun");
PrintValue(" Recursive", (g.Recursive ?? false).ToString(), "Global:Recursive");
Expand Down Expand Up @@ -184,6 +184,24 @@ private static bool HasEnvVar(string configKey)
return Environment.GetEnvironmentVariable(envKey) != null;
}

/// <summary>
/// An explicit AuthType is echoed as-is (with a hint when unrecognised —
/// the API client registration would reject it with the same expectation);
/// an absent one shows what deployment auto-detection resolves to, so the
/// operator sees the effective mode, not just "(not set)".
/// </summary>
private static string DescribeAuthType(GlobalOptions g)
{
if (!string.IsNullOrWhiteSpace(g.AuthType))
return DeploymentTypeResolver.TryParse(g.AuthType, out _)
? g.AuthType.Trim().ToLowerInvariant()
: $"{g.AuthType} (invalid: expected onprem|cloud)";

return DeploymentTypeResolver.Autodetect(g.BaseUrl) == DeploymentType.Cloud
? "cloud (auto-detected)"
: "onprem";
}

private static string? Mask(string? value)
{
if (string.IsNullOrEmpty(value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static RootCommand Build()
root.Options.Add(Opt<string?>("--username", "Username or email for authentication", recursive: true));
root.Options.Add(Opt<string?>("--token", "API token or password for authentication", recursive: true));
root.Options.Add(Opt<string?>("--space-key", "Confluence space key", recursive: true));
root.Options.Add(Opt<string?>("--auth-type", "Authentication type: 'onprem' or 'cloud'", recursive: true));
root.Options.Add(Opt<string?>("--auth-type", "Deployment type: 'onprem' or 'cloud' (default: auto-detected from --base-url, *.atlassian.net → cloud)", recursive: true));
root.Options.Add(Flag("--dry-run", "Perform a dry run without writing changes", recursive: true));
root.Options.Add(Flag("--recursive", "Recursively process child pages", recursive: true));
root.Options.Add(Flag("--multi-tree", "Upload update/merge: process every page tree under --source-dir independently (each may live in a different space)", recursive: true));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
namespace ConfluencePageExporter.Infrastructure;

/// <summary>
/// Confluence deployment flavour the tool talks to. Determines which REST API
/// family the client must use: Server/Data Center (v1, <c>/rest/api</c>) or
/// Cloud (v2, <c>/wiki/api/v2</c>, with a few operations remaining on v1).
/// </summary>
public enum DeploymentType
{
/// <summary>Server / Data Center.</summary>
OnPrem,

/// <summary>Atlassian Cloud (<c>*.atlassian.net</c> or a custom domain).</summary>
Cloud,
}

/// <summary>
/// Resolves the effective <see cref="DeploymentType"/> from configuration.
/// An explicit <c>--auth-type</c> / <c>Global:AuthType</c> value wins;
/// otherwise the type is auto-detected from the base URL host
/// (<c>*.atlassian.net</c> ⇒ Cloud). Cloud sites served from a custom domain
/// are not detectable by host, so they need the explicit setting.
/// </summary>
public static class DeploymentTypeResolver
{
public const string OnPremValue = "onprem";
public const string CloudValue = "cloud";

private const string CloudHostSuffix = ".atlassian.net";

/// <summary>
/// Resolves the deployment type. Throws <see cref="ArgumentException"/>
/// for an explicit value that is neither <see cref="OnPremValue"/> nor
/// <see cref="CloudValue"/> — a typo here would otherwise silently select
/// the wrong API family.
/// </summary>
public static DeploymentType Resolve(string? authType, string? baseUrl)
{
if (string.IsNullOrWhiteSpace(authType))
return Autodetect(baseUrl);

if (TryParse(authType, out var type))
return type;

throw new ArgumentException(
$"Invalid --auth-type (or Global:AuthType in config) value '{authType}'. " +
$"Expected '{OnPremValue}' or '{CloudValue}'.");
}

/// <summary>Case-insensitive, whitespace-tolerant parse of an explicit value.</summary>
public static bool TryParse(string? authType, out DeploymentType type)
{
switch (authType?.Trim().ToLowerInvariant())
{
case OnPremValue:
type = DeploymentType.OnPrem;
return true;
case CloudValue:
type = DeploymentType.Cloud;
return true;
default:
type = default;
return false;
}
}

/// <summary>
/// Detects Cloud by the well-known Atlassian Cloud host suffix. A missing
/// or unparseable base URL maps to <see cref="DeploymentType.OnPrem"/>
/// (the historical default) — reporting a missing base URL is the client
/// registration's job, not this resolver's.
/// </summary>
public static DeploymentType Autodetect(string? baseUrl)
{
if (string.IsNullOrWhiteSpace(baseUrl))
return DeploymentType.OnPrem;

if (!Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out var uri))
return DeploymentType.OnPrem;

return uri.Host.EndsWith(CloudHostSuffix, StringComparison.OrdinalIgnoreCase)
? DeploymentType.Cloud
: DeploymentType.OnPrem;
}
}
Loading