-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathall-spaces.js
More file actions
executable file
·187 lines (161 loc) · 5.32 KB
/
Copy pathall-spaces.js
File metadata and controls
executable file
·187 lines (161 loc) · 5.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#!/usr/bin/env node
/* eslint-disable no-console */
/* eslint-disable no-undef */
import fs from "fs";
import path from "path";
import {
convertToMarkdown,
fetchWithBackoff,
OUTPUT_DIR,
sanitizeName,
ensureDirectoryExists,
getUniqueDirectoryName,
formatApiUrl,
} from "../utils/index.js";
/**
* @typedef {Object} ConfluenceSpace
* @property {string} key - The space key (e.g., "ENGINEERING")
* @property {string} name - The display name of the space
* @property {Object} homePage - Information about the space's home page
* @property {string} homePage.id - The ID of the home page
*/
/**
* @typedef {Object} ConfluencePage
* @property {string} id - The page ID
* @property {string} title - The page title
* @property {Object} body - The page content
* @property {Object} body.storage - The storage format content
* @property {string} body.storage.value - The HTML content
* @property {Array<Object>} ancestors - Parent pages in the hierarchy
* @property {Object} space - The containing space
* @property {Object} space.homePage - The space's home page
*/
// Add after imports
console.log(`
Confluence Space Scraper
-----------------------
This script will:
1. Fetch all available Confluence spaces
2. Download all pages from each space
3. Convert them to Markdown
4. Save them in a directory structure matching Confluence
Output will be in: ./confluence_markdown/
Note: Configure BASE_URL and ACCESS_TOKEN in utils/index.js first
`);
// Create output directory if it doesn't exist
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
/**
* Fetches all available Confluence spaces
* @returns {Promise<ConfluenceSpace[]>} Array of space objects
*/
async function getAllSpaces(limit = Infinity) {
let spaces = [];
let url = "/space?limit=1000";
let requestCount = 0;
while (url) {
if (requestCount >= limit) {
console.log(`Request limit (${limit}) reached. Stopping...`);
break;
}
const data = await fetchWithBackoff(url);
requestCount++;
spaces.push(...data.results);
url = data._links?.next || null;
if (url) {
url = formatApiUrl(url);
}
}
return spaces;
}
/**
* Fetches all content from a specific space
* @param {string} spaceKey - The space key to fetch content from
* @returns {Promise<ConfluencePage[]>} Array of page objects
*/
async function getSpaceContent(spaceKey) {
let pages = [];
let url = formatApiUrl(`/space/${spaceKey}/content`, {
expand: "body.storage,ancestors,space,history",
limit: "100",
});
while (url) {
const data = await fetchWithBackoff(url);
pages.push(...data.results);
url = data._links?.next || null;
if (url) {
url = formatApiUrl(url, {
expand: "body.storage,ancestors,space,history",
});
}
}
return pages;
}
/**
* Saves a page's content as a Markdown file
* @param {string} spaceKey - The space key
* @param {ConfluencePage} page - The page object containing content and metadata
* @param {string} content - The markdown content to save
*/
async function saveToMarkdown(spaceKey, page, content) {
const spacePath = path.join(OUTPUT_DIR, spaceKey);
ensureDirectoryExists(spacePath);
// Create home directory for root/orphaned pages
const homePath = path.join(spacePath, "home");
ensureDirectoryExists(homePath);
const title = sanitizeName(page.title);
let targetDir;
let fileName;
if (!page.ancestors || page.ancestors.length === 0) {
// Root level page - goes in home directory
targetDir = homePath;
fileName =
page.id === page.space.homePage.id
? `0_${spaceKey}.md` // Space homepage
: `${title}.md`; // Other root pages
} else {
// Nested page - create parent directory structure
const parentPage = page.ancestors[page.ancestors.length - 1];
targetDir = getUniqueDirectoryName(
spacePath,
sanitizeName(parentPage.title),
);
ensureDirectoryExists(targetDir);
fileName = page.id === parentPage.id ? "index.md" : `${title}.md`;
}
const filePath = path.join(targetDir, fileName);
fs.writeFileSync(filePath, content, "utf8");
console.log(`✅ Saved: ${filePath}`);
}
/**
* Main function to scrape all Confluence spaces
* Fetches all spaces, their pages, and saves them as Markdown files
* @returns {Promise<void>}
* @throws {Error} If API calls fail or file operations fail
*/
export async function scrapeConfluence(limit = Infinity) {
console.log("Fetching all spaces...");
const spaces = await getAllSpaces(limit);
console.log(`Found ${spaces.length} spaces.`);
for (const space of spaces) {
const spaceKey = space.key;
console.log(`Fetching content for space: ${spaceKey}`);
const pages = await getSpaceContent(spaceKey);
for (const page of pages) {
const bodyStorage = page.body?.storage?.value || "";
if (bodyStorage) {
const markdownContent = convertToMarkdown(bodyStorage);
await saveToMarkdown(spaceKey, page, markdownContent);
}
}
}
console.log("✅ All content has been scraped and saved!");
}
// Parse command line arguments
const args = process.argv.slice(2);
const limitIndex = args.findIndex((arg) => arg === "-l" || arg === "--limit");
const limit = limitIndex !== -1 ? parseInt(args[limitIndex + 1], 10) : Infinity;
if (import.meta.url === process.argv[1]) {
scrapeConfluence(limit).catch((err) => console.error("Error:", err));
}