Skip to content

Commit 989a287

Browse files
committed
Differentiate between FileConfig and Config
1 parent ad6b119 commit 989a287

File tree

12 files changed

+222
-205
lines changed

12 files changed

+222
-205
lines changed

README.md

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ Sync your markdown files to confluence.
44

55
## Features
66

7-
- upload new versions only when necessary
8-
- upload/delete local images as attachments
9-
- convert PlantUML code fence to Confluence PlantUML macro
10-
```
7+
- upload new versions only when necessary
8+
- upload/delete local images as attachments
9+
- convert PlantUML code fence to Confluence PlantUML macro
10+
````
1111
\```plantuml
1212
@startuml
1313
a --> b
1414
@enduml
1515
\```
16-
```
16+
````
1717
1818
## Usage
1919
@@ -39,7 +39,7 @@ yarn add --dev cosmere
3939
4040
### Configuration
4141
42-
To get started generate configuration using
42+
To get started generate configuration using
4343
4444
```bash
4545
cosmere generate-config [--config=<path>]
@@ -49,43 +49,43 @@ which produces:
4949
5050
```json
5151
{
52-
"baseUrl": "<your base URL>",
53-
"user": "<your username>",
54-
"pass": "<your password>",
55-
"authToken": "<your auth token (can be set instead of username/password)>",
56-
"cachePath": "build",
57-
"prefix": "This document is automatically generated. Please don't edit it directly!",
58-
"pages": [
59-
{
60-
"pageId": "1234567890",
61-
"file": "README.md",
62-
"title": "Optional title in the confluence page, remove to use # h1 from markdown file instead"
63-
}
64-
]
52+
"baseUrl": "<your base URL>",
53+
"user": "<your username>",
54+
"pass": "<your password>",
55+
"personalAccessToken": "<your personal access token (can be set instead of username/password)>",
56+
"cachePath": "build",
57+
"prefix": "This document is automatically generated. Please don't edit it directly!",
58+
"pages": [
59+
{
60+
"pageId": "1234567890",
61+
"file": "README.md",
62+
"title": "Optional title in the confluence page, remove to use # h1 from markdown file instead"
63+
}
64+
]
6565
}
6666
```
6767
6868
### Continuous Integration
6969
70-
In most scenarios it is not recommended to store your credentials in the configuration file, because you will probably add it to your VCS. Instead it is recommended to provide the following environment variables in your build pipeline (GitLab CI, GitHub Actions, Jenkins, ...):
70+
In most scenarios it is not recommended storing your credentials in the configuration file, because you will probably add it to your VCS. Instead, it is recommended to provide the following environment variables in your build pipeline (GitLab CI, GitHub Actions, Jenkins, ...):
7171
7272
```ini
73-
CONFLUENCE_USERNAME="<your username>"
74-
CONFLUENCE_PASSWORD="<your password>"
73+
CONFLUENCE_USERNAME=YOUR_USERNAME
74+
CONFLUENCE_PASSWORD=YOUR_PASSWORD
7575
```
7676
7777
or
7878
7979
```ini
80-
CONFLUENCE_AUTH_TOKEN="<your auth token>"
80+
CONFLUENCE_PERSONAL_ACCESS_TOKEN="<your auth token>"
8181
```
8282
8383
or add it in front of the command when executing locally (add a space in front of the command when using bash in order to not write the credentials to the bash history):
8484
8585
```bash
86-
CONFLUENCE_USER="<your username>" CONFLUENCE_PASSWORD="<your password>" cosmere
87-
# or
88-
CONFLUENCE_AUTH_TOKEN="<your auth token>" cosmere
86+
CONFLUENCE_USER=YOUR_USERNAME CONFLUENCE_PASSWORD=YOUR_PASSWORD cosmere
87+
# or
88+
CONFLUENCE_PERSONAL_ACCESS_TOKEN="<your personal access token>" cosmere
8989
```
9090
9191
### Run
@@ -108,9 +108,9 @@ or create an alias:
108108
109109
```json
110110
{
111-
"scripts": {
112-
"pushdoc": "cosmere"
113-
}
111+
"scripts": {
112+
"pushdoc": "cosmere"
113+
}
114114
}
115115
```
116116
@@ -137,11 +137,14 @@ Please, feel free to create any issues and pull request that you need.
137137
```
138138
139139
## History
140+
140141
### md2confluence
142+
141143
I had various scripts that stitched markdown files together and uploaded them. I forked [`md2confluence`](https://github.com/jormar/md2confluence) by [Jormar Arellano](https://github.com/jormar) and started playing around with that, but quickly noticed that many markdown files broke due to the conversion process (wiki -> storage instead of directly to storage).
142144
143145
### Cosmere
144-
The project diverged from its original intent and so I decided to rename it. [Cosmere](https://coppermind.net/wiki/Cosmere) is the wonderful universe of various books written by [Brandon Sanderson](https://www.brandonsanderson.com/). If you are into fantasy I strongly recommend checking him out.
146+
147+
The project diverged from its original intent and so I decided to rename it. [Cosmere](https://coppermind.net/wiki/Cosmere) is the wonderful universe of various books written by [Brandon Sanderson](https://www.brandonsanderson.com/). If you are into fantasy I strongly recommend checking him out.
145148
146149
## License
147150

src/ConfigLoader.ts

Lines changed: 81 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,115 @@
11
import * as path from "path";
22
import * as fs from "fs";
3-
import { Config, ConfigKey, isBasicAuth, isTokenAuth } from "./types/Config";
3+
import { Config } from "./types/Config";
44
import * as inquirer from "inquirer";
55
import signale from "signale";
6+
import { FileConfig } from "./types/FileConfig";
67

7-
8-
function overwriteConfigKeyWithEnvVarIfPresent<T>(config: Partial<Config>, configKey: ConfigKey, envKey: string): Partial<Config> {
9-
if (configKey === "pages") {
10-
throw Error("Cannot override pages using environment variable");
11-
}
12-
13-
if (process.env[envKey] !== undefined) {
14-
return { ...config, [configKey]: process.env[envKey] };
15-
} else {
16-
return config;
17-
}
18-
}
19-
8+
type AuthOptions = {
9+
user?: string;
10+
pass?: string;
11+
personalAccessToken?: string;
12+
};
2013

2114
export class ConfigLoader {
22-
static async load(configPath: string | null): Promise<Partial<Config>> {
23-
let config = ConfigLoader.readConfigFromFile(configPath);
24-
config = ConfigLoader.overwriteAuthFromConfigWithEnvIfPresent(config),
25-
config = await ConfigLoader.promptUserAndPassIfNoCredentialsSet(config);
26-
return config;
15+
static async load(configPath: string | null): Promise<Config> {
16+
const fileConfig = ConfigLoader.readConfigFromFile(configPath);
17+
const authOptions = await ConfigLoader.promptUserAndPassIfNotSet(
18+
ConfigLoader.useAuthOptionsFromEnvIfPresent(ConfigLoader.authOptionsFromFileConfig(fileConfig)),
19+
);
20+
return ConfigLoader.createConfig(fileConfig, ConfigLoader.createAuthorizationToken(authOptions));
2721
}
2822

29-
private static readConfigFromFile(configPath: string | null): Partial<Config> {
23+
private static readConfigFromFile(configPath: string | null, authorizationToken?: string): FileConfig {
3024
configPath = path.resolve(configPath || path.join("cosmere.json"));
3125
if (!fs.existsSync(configPath!)) {
3226
signale.fatal(`File "${configPath}" not found!`);
3327
process.exit(1);
3428
}
3529

36-
let config: Partial<Config> = JSON.parse(fs.readFileSync(configPath!, "utf8"));
37-
if (config.pages !== undefined) {
38-
for (const i in config.pages) {
39-
config.pages[i].file = path.isAbsolute(config.pages[i].file)
40-
? config.pages[i].file
41-
: path.resolve(path.dirname(configPath) + "/" + config.pages[i].file);
42-
}
30+
let config = JSON.parse(fs.readFileSync(configPath!, "utf8")) as Omit<FileConfig, "configPath">;
31+
for (const i in config.pages) {
32+
config.pages[i].file = path.isAbsolute(config.pages[i].file)
33+
? config.pages[i].file
34+
: path.resolve(path.dirname(configPath) + "/" + config.pages[i].file);
4335
}
44-
config.configPath = configPath;
45-
return config;
36+
37+
return {
38+
...config,
39+
configPath,
40+
};
4641
}
4742

48-
private static overwriteAuthFromConfigWithEnvIfPresent(config: Partial<Config>): Partial<Config> {
49-
config = overwriteConfigKeyWithEnvVarIfPresent(config, "user", "CONFLUENCE_USERNAME");
50-
config = overwriteConfigKeyWithEnvVarIfPresent(config, "pass", "CONFLUENCE_PASSWORD");
51-
config = overwriteConfigKeyWithEnvVarIfPresent(config, "authToken", "CONFLUENCE_AUTH_TOKEN");
52-
return config;
43+
private static createAuthorizationToken(authOptions: AuthOptions): string {
44+
if (authOptions.personalAccessToken) {
45+
return `Bearer ${authOptions.personalAccessToken}`;
46+
}
47+
48+
if (authOptions.user && authOptions.user.length > 0 && authOptions.pass && authOptions.pass.length > 0) {
49+
const encodedBasicToken = Buffer.from(`${authOptions.user}:${authOptions.pass}`).toString("base64");
50+
return `Basic ${encodedBasicToken}`;
51+
}
52+
53+
signale.fatal(
54+
"Missing configuration! You must either provide a combination of your Confluence username and password or a personal access token.",
55+
);
56+
process.exit(2);
57+
}
58+
59+
private static useAuthOptionsFromEnvIfPresent(authOptions: AuthOptions): AuthOptions {
60+
return {
61+
user: process.env.CONFLUENCE_USERNAME || authOptions.user,
62+
pass: process.env.CONFLUENCE_PASSWORD || authOptions.pass,
63+
personalAccessToken: process.env.CONFLUENCE_PERSONAL_ACCESS_TOKEN || authOptions.personalAccessToken,
64+
};
5365
}
5466

55-
private static async promptUserAndPassIfNoCredentialsSet(config: Partial<Config>): Promise<Partial<Config>> {
56-
if (isTokenAuth(config)) {
57-
return config;
67+
private static async promptUserAndPassIfNotSet(authOptions: AuthOptions): Promise<AuthOptions> {
68+
if (authOptions.personalAccessToken && authOptions.personalAccessToken.length > 0) {
69+
return authOptions;
5870
}
5971

60-
if (!("user" in config)) {
61-
const answers = await inquirer.prompt([{
72+
const prompts = [];
73+
if (!authOptions.user) {
74+
prompts.push({
6275
type: "input",
6376
name: "user",
6477
message: "Your Confluence username:",
65-
}]);
66-
(config as any).user = answers.user;
78+
});
6779
}
6880

69-
if (!("pass" in config)) {
70-
const answers = await inquirer.prompt([{
81+
if (!authOptions.pass) {
82+
prompts.push({
7183
type: "password",
7284
name: "pass",
7385
message: "Your Confluence password:",
74-
}]);
75-
(config as any).pass = answers.pass;
86+
});
7687
}
7788

78-
return config;
89+
const answers = await inquirer.prompt(prompts);
90+
return {
91+
user: authOptions.user || (answers.user as string),
92+
pass: authOptions.pass || (answers.pass as string),
93+
personalAccessToken: authOptions.personalAccessToken,
94+
};
95+
}
96+
97+
private static authOptionsFromFileConfig(fileConfig: FileConfig): AuthOptions {
98+
return {
99+
user: fileConfig.user,
100+
pass: fileConfig.pass,
101+
personalAccessToken: fileConfig.personalAccessToken,
102+
};
103+
}
104+
105+
private static createConfig(fileConfig: FileConfig, authorizationToken: string): Config {
106+
return {
107+
baseUrl: fileConfig.baseUrl,
108+
cachePath: fileConfig.cachePath,
109+
prefix: fileConfig.prefix,
110+
pages: fileConfig.pages,
111+
configPath: fileConfig.configPath,
112+
authorizationToken: authorizationToken,
113+
};
79114
}
80115
}

src/api/ConfluenceAPI.ts

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
1-
import { AuthConfig, isBasicAuth, isTokenAuth } from "../types/Config";
2-
import axios, { AxiosRequestConfig } from "axios";
1+
import axios from "axios";
32
import * as fs from "fs";
43
import signale from "signale";
54

6-
75
export class ConfluenceAPI {
8-
constructor(
9-
private readonly baseUrl: string,
10-
private readonly authConfig: AuthConfig
11-
) {}
6+
private readonly baseUrl: string;
7+
private readonly authHeader: {
8+
Authorization: string;
9+
};
10+
11+
constructor(baseUrl: string, authorizationToken: string) {
12+
this.baseUrl = baseUrl;
13+
this.authHeader = {
14+
Authorization: authorizationToken,
15+
};
16+
}
1217

1318
async updateConfluencePage(pageId: string, newPage: any) {
14-
const config = this.appendAuthHeaders({
19+
const config = {
1520
headers: {
21+
...this.authHeader,
1622
"Content-Type": "application/json",
1723
},
18-
});
19-
24+
};
2025
try {
2126
await axios.put(`${this.baseUrl}/content/${pageId}`, newPage, config);
2227
} catch (e) {
@@ -26,12 +31,15 @@ export class ConfluenceAPI {
2631
}
2732

2833
async deleteAttachments(pageId: string) {
29-
const config = this.appendAuthHeaders({});
30-
const attachments = await axios.get(`${this.baseUrl}/content/${pageId}/child/attachment`, config);
34+
const attachments = await axios.get(`${this.baseUrl}/content/${pageId}/child/attachment`, {
35+
headers: this.authHeader,
36+
});
3137
for (const attachment of attachments.data.results) {
3238
try {
3339
signale.await(`Deleting attachment "${attachment.title}" ...`);
34-
await axios.delete(`${this.baseUrl}/content/${attachment.id}`, config);
40+
await axios.delete(`${this.baseUrl}/content/${attachment.id}`, {
41+
headers: this.authHeader,
42+
});
3543
} catch (e) {
3644
signale.error(`Deleting attachment "${attachment.title}" failed ...`);
3745
}
@@ -40,49 +48,25 @@ export class ConfluenceAPI {
4048

4149
async uploadAttachment(filename: string, pageId: string) {
4250
try {
43-
const config = this.appendAuthHeaders({
51+
await require("axios-file")({
4452
url: `${this.baseUrl}/content/${pageId}/child/attachment`,
4553
method: "post",
4654
headers: {
4755
"X-Atlassian-Token": "nocheck",
56+
...this.authHeader,
4857
},
4958
data: {
5059
file: fs.createReadStream(filename),
5160
},
5261
});
53-
54-
await require("axios-file")(config);
5562
} catch (e) {
5663
signale.error(`Uploading attachment "${filename}" failed ...`);
5764
}
5865
}
5966

6067
async currentPage(pageId: string) {
61-
const config = this.appendAuthHeaders({});
62-
return axios.get(`${this.baseUrl}/content/${pageId}?expand=body.storage,version`, config);
63-
}
64-
65-
private appendAuthHeaders(config: AxiosRequestConfig): AxiosRequestConfig {
66-
if (isBasicAuth(this.authConfig)) {
67-
return {
68-
...config,
69-
auth: {
70-
username: this.authConfig.user,
71-
password: this.authConfig.pass,
72-
},
73-
};
74-
}
75-
else if (isTokenAuth(this.authConfig)) {
76-
return {
77-
...config,
78-
headers: {
79-
...(config.headers ?? {}),
80-
"Authorization": `Bearer ${this.authConfig.authToken}`,
81-
}
82-
}
83-
}
84-
else {
85-
throw Error("No credentials found in config");
86-
}
68+
return axios.get(`${this.baseUrl}/content/${pageId}?expand=body.storage,version`, {
69+
headers: this.authHeader,
70+
});
8771
}
8872
}

0 commit comments

Comments
 (0)