@onozaty/growi-uploader 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/index.mjs +116 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ growi-uploader <source-dir> [options]
|
|
|
69
69
|
|
|
70
70
|
**Options:**
|
|
71
71
|
- `-c, --config <path>`: Path to config file (default: `growi-uploader.json`)
|
|
72
|
+
- `-v, --verbose`: Enable verbose error output with detailed information
|
|
72
73
|
- `-V, --version`: Output the version number
|
|
73
74
|
- `-h, --help`: Display help information
|
|
74
75
|
|
|
@@ -80,6 +81,9 @@ npx @onozaty/growi-uploader ./docs
|
|
|
80
81
|
|
|
81
82
|
# Upload with custom config file
|
|
82
83
|
npx @onozaty/growi-uploader ./docs -c my-config.json
|
|
84
|
+
|
|
85
|
+
# Upload with verbose error output
|
|
86
|
+
npx @onozaty/growi-uploader ./docs --verbose
|
|
83
87
|
```
|
|
84
88
|
|
|
85
89
|
## Directory Structure Example
|
|
@@ -117,7 +121,8 @@ Create a `growi-uploader.json` file in your project root:
|
|
|
117
121
|
"url": "https://your-growi-instance.com",
|
|
118
122
|
"token": "your-api-token",
|
|
119
123
|
"basePath": "/imported",
|
|
120
|
-
"update": true
|
|
124
|
+
"update": true,
|
|
125
|
+
"verbose": false
|
|
121
126
|
}
|
|
122
127
|
```
|
|
123
128
|
|
|
@@ -129,6 +134,7 @@ Create a `growi-uploader.json` file in your project root:
|
|
|
129
134
|
| `token` | string | ✅ | - | GROWI API access token |
|
|
130
135
|
| `basePath` | string | ❌ | `/` | Base path for imported pages |
|
|
131
136
|
| `update` | boolean | ❌ | `false` | Update existing pages if true, skip if false |
|
|
137
|
+
| `verbose` | boolean | ❌ | `false` | Enable verbose error output with detailed information |
|
|
132
138
|
|
|
133
139
|
### Getting an API Token
|
|
134
140
|
|
|
@@ -194,6 +200,8 @@ All referenced files (`logo.png`, `screenshot.png`, and `banner.png`) will be up
|
|
|
194
200
|
[File](./docs/my%20file.pdf) # URL-encoded space
|
|
195
201
|
[File](<./path/file (1).png>) # Special chars with angle brackets
|
|
196
202
|
.png) # Special chars with escaping
|
|
203
|
+
<img src="./images/logo.png" alt="Logo"> # HTML img tag (double quotes)
|
|
204
|
+
<img src='./images/logo.png' alt='Logo'> # HTML img tag (single quotes)
|
|
197
205
|
```
|
|
198
206
|
|
|
199
207
|
**Excluded from detection:**
|
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { lookup } from "mime-types";
|
|
|
8
8
|
import { glob } from "glob";
|
|
9
9
|
|
|
10
10
|
//#region package.json
|
|
11
|
-
var version = "1.
|
|
11
|
+
var version = "1.5.0";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/config.ts
|
|
@@ -19,6 +19,7 @@ var version = "1.3.0";
|
|
|
19
19
|
* @returns Configuration object with defaults applied:
|
|
20
20
|
* - basePath defaults to "/" if not specified or empty
|
|
21
21
|
* - update defaults to false if not specified
|
|
22
|
+
* - verbose defaults to false if not specified
|
|
22
23
|
* @throws Error if config file is not found or required fields (url, token) are missing
|
|
23
24
|
*/
|
|
24
25
|
const loadConfig = (configPath) => {
|
|
@@ -32,7 +33,8 @@ const loadConfig = (configPath) => {
|
|
|
32
33
|
url: input.url,
|
|
33
34
|
token: input.token,
|
|
34
35
|
basePath: input.basePath || "/",
|
|
35
|
-
update: input.update ?? false
|
|
36
|
+
update: input.update ?? false,
|
|
37
|
+
verbose: input.verbose ?? false
|
|
36
38
|
};
|
|
37
39
|
} catch (error) {
|
|
38
40
|
if (error.code === "ENOENT") throw new Error(`Config file not found: ${fullPath}`);
|
|
@@ -85,7 +87,8 @@ const putPage = (putPageBody, options) => {
|
|
|
85
87
|
* @param token API token for authentication
|
|
86
88
|
*/
|
|
87
89
|
const configureAxios = (growiUrl, token) => {
|
|
88
|
-
|
|
90
|
+
const baseUrl = growiUrl.replace(/\/+$/, "");
|
|
91
|
+
axios.defaults.baseURL = `${baseUrl}/_api/v3`;
|
|
89
92
|
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
90
93
|
};
|
|
91
94
|
/**
|
|
@@ -95,11 +98,59 @@ const configureAxios = (growiUrl, token) => {
|
|
|
95
98
|
* @returns Formatted error message string
|
|
96
99
|
*/
|
|
97
100
|
const formatErrorMessage = (error) => {
|
|
98
|
-
if (axios.isAxiosError(error))
|
|
101
|
+
if (axios.isAxiosError(error)) {
|
|
102
|
+
const status = error.response?.status;
|
|
103
|
+
const statusText = error.response?.statusText;
|
|
104
|
+
const data = error.response?.data;
|
|
105
|
+
const parts = [];
|
|
106
|
+
if (status) parts.push(`HTTP ${status}${statusText ? ` ${statusText}` : ""}`);
|
|
107
|
+
if (data && typeof data === "object") {
|
|
108
|
+
const message = data.message || data.error || data.errors;
|
|
109
|
+
if (message) parts.push(message);
|
|
110
|
+
else if (Object.keys(data).length > 0) parts.push(JSON.stringify(data));
|
|
111
|
+
}
|
|
112
|
+
if (error.code) parts.push(`(${error.code})`);
|
|
113
|
+
if (parts.length === 0) return error.message;
|
|
114
|
+
return parts.join(" ");
|
|
115
|
+
}
|
|
99
116
|
if (error instanceof Error) return error.message;
|
|
100
117
|
return String(error);
|
|
101
118
|
};
|
|
102
119
|
/**
|
|
120
|
+
* Format error into a detailed error message for verbose mode
|
|
121
|
+
*
|
|
122
|
+
* @param error Error object from catch block
|
|
123
|
+
* @returns Detailed multi-line error message with all available information
|
|
124
|
+
*/
|
|
125
|
+
const formatDetailedError = (error) => {
|
|
126
|
+
const lines = [];
|
|
127
|
+
if (axios.isAxiosError(error)) {
|
|
128
|
+
const status = error.response?.status;
|
|
129
|
+
const statusText = error.response?.statusText;
|
|
130
|
+
const data = error.response?.data;
|
|
131
|
+
if (status) lines.push(` HTTP Status: ${status}${statusText ? ` ${statusText}` : ""}`);
|
|
132
|
+
if (data) try {
|
|
133
|
+
const responseStr = typeof data === "object" ? JSON.stringify(data, null, 2).split("\n").map((line) => ` ${line}`).join("\n") : String(data);
|
|
134
|
+
lines.push(` Response Body:\n${responseStr}`);
|
|
135
|
+
} catch {
|
|
136
|
+
lines.push(` Response Body: ${String(data)}`);
|
|
137
|
+
}
|
|
138
|
+
if (error.code) lines.push(` Error Code: ${error.code}`);
|
|
139
|
+
if (error.config) lines.push(` Request: ${error.config.method?.toUpperCase()} ${error.config.url}`);
|
|
140
|
+
if (error.stack) {
|
|
141
|
+
const stackLines = error.stack.split("\n").slice(0, 5);
|
|
142
|
+
lines.push(` Stack Trace:\n${stackLines.map((line) => ` ${line}`).join("\n")}`);
|
|
143
|
+
}
|
|
144
|
+
} else if (error instanceof Error) {
|
|
145
|
+
lines.push(` Error: ${error.message}`);
|
|
146
|
+
if (error.stack) {
|
|
147
|
+
const stackLines = error.stack.split("\n").slice(0, 5);
|
|
148
|
+
lines.push(` Stack Trace:\n${stackLines.map((line) => ` ${line}`).join("\n")}`);
|
|
149
|
+
}
|
|
150
|
+
} else lines.push(` Error: ${String(error)}`);
|
|
151
|
+
return lines.length > 0 ? ` Details:\n${lines.join("\n")}` : "";
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
103
154
|
* Create a new page or update an existing page in GROWI
|
|
104
155
|
*
|
|
105
156
|
* @param file Markdown file to upload
|
|
@@ -133,7 +184,8 @@ const createOrUpdatePage = async (file, content, shouldUpdate) => {
|
|
|
133
184
|
pageId: void 0,
|
|
134
185
|
revisionId: void 0,
|
|
135
186
|
action: "error",
|
|
136
|
-
errorMessage: formatErrorMessage(error)
|
|
187
|
+
errorMessage: formatErrorMessage(error),
|
|
188
|
+
error
|
|
137
189
|
};
|
|
138
190
|
}
|
|
139
191
|
try {
|
|
@@ -151,7 +203,8 @@ const createOrUpdatePage = async (file, content, shouldUpdate) => {
|
|
|
151
203
|
pageId: void 0,
|
|
152
204
|
revisionId: void 0,
|
|
153
205
|
action: "error",
|
|
154
|
-
errorMessage: formatErrorMessage(error)
|
|
206
|
+
errorMessage: formatErrorMessage(error),
|
|
207
|
+
error
|
|
155
208
|
};
|
|
156
209
|
}
|
|
157
210
|
};
|
|
@@ -177,7 +230,8 @@ const updatePageContent = async (pageId, revisionId, content) => {
|
|
|
177
230
|
} catch (error) {
|
|
178
231
|
return {
|
|
179
232
|
success: false,
|
|
180
|
-
errorMessage: formatErrorMessage(error)
|
|
233
|
+
errorMessage: formatErrorMessage(error),
|
|
234
|
+
error
|
|
181
235
|
};
|
|
182
236
|
}
|
|
183
237
|
};
|
|
@@ -207,7 +261,8 @@ const uploadAttachment = async (attachment, pageId, sourceDir) => {
|
|
|
207
261
|
} catch (error) {
|
|
208
262
|
return {
|
|
209
263
|
success: false,
|
|
210
|
-
errorMessage: formatErrorMessage(error)
|
|
264
|
+
errorMessage: formatErrorMessage(error),
|
|
265
|
+
error
|
|
211
266
|
};
|
|
212
267
|
}
|
|
213
268
|
};
|
|
@@ -215,6 +270,37 @@ const uploadAttachment = async (attachment, pageId, sourceDir) => {
|
|
|
215
270
|
//#endregion
|
|
216
271
|
//#region src/scanner.ts
|
|
217
272
|
/**
|
|
273
|
+
* Process a link path and convert it to an AttachmentFile if it exists
|
|
274
|
+
*
|
|
275
|
+
* @param linkPath The link path from markdown
|
|
276
|
+
* @param originalLinkPath The original format of the link (for replacement)
|
|
277
|
+
* @param markdownFilePath Path to the markdown file (relative to sourceDir)
|
|
278
|
+
* @param sourceDir Source directory (absolute path)
|
|
279
|
+
* @returns AttachmentFile if the path resolves to an existing file, null otherwise
|
|
280
|
+
*/
|
|
281
|
+
const processLinkPath = (linkPath, originalLinkPath, markdownFilePath, sourceDir) => {
|
|
282
|
+
if (linkPath.startsWith("http://") || linkPath.startsWith("https://")) return null;
|
|
283
|
+
if (linkPath.endsWith(".md")) return null;
|
|
284
|
+
const unescapedPath = linkPath.replace(/\\([\\`*_{}[\]()#+\-.!])/g, "$1");
|
|
285
|
+
let decodedPath;
|
|
286
|
+
try {
|
|
287
|
+
decodedPath = decodeURIComponent(unescapedPath);
|
|
288
|
+
} catch {
|
|
289
|
+
decodedPath = unescapedPath;
|
|
290
|
+
}
|
|
291
|
+
let absolutePath;
|
|
292
|
+
if (linkPath.startsWith("/")) absolutePath = resolve(sourceDir, decodedPath.slice(1));
|
|
293
|
+
else absolutePath = resolve(dirname(join(sourceDir, markdownFilePath)), decodedPath);
|
|
294
|
+
if (!existsSync(absolutePath)) return null;
|
|
295
|
+
const normalizedPath = relative(sourceDir, absolutePath).replace(/\\/g, "/");
|
|
296
|
+
return {
|
|
297
|
+
localPath: normalizedPath,
|
|
298
|
+
fileName: basename(normalizedPath),
|
|
299
|
+
detectionPattern: "link",
|
|
300
|
+
originalLinkPaths: [originalLinkPath]
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
218
304
|
* Extract attachment files from markdown links
|
|
219
305
|
*
|
|
220
306
|
* Scans markdown content for image and link references, resolves their paths,
|
|
@@ -243,27 +329,15 @@ const extractLinkedAttachments = (content, markdownFilePath, sourceDir) => {
|
|
|
243
329
|
const { anglePath, regularPath } = match.groups;
|
|
244
330
|
const linkPath = anglePath || regularPath;
|
|
245
331
|
if (!linkPath) continue;
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
let absolutePath;
|
|
257
|
-
if (linkPath.startsWith("/")) absolutePath = resolve(sourceDir, decodedPath.slice(1));
|
|
258
|
-
else absolutePath = resolve(dirname(join(sourceDir, markdownFilePath)), decodedPath);
|
|
259
|
-
if (!existsSync(absolutePath)) continue;
|
|
260
|
-
const normalizedPath = relative(sourceDir, absolutePath).replace(/\\/g, "/");
|
|
261
|
-
attachments.push({
|
|
262
|
-
localPath: normalizedPath,
|
|
263
|
-
fileName: basename(normalizedPath),
|
|
264
|
-
detectionPattern: "link",
|
|
265
|
-
originalLinkPaths: [originalLinkPath]
|
|
266
|
-
});
|
|
332
|
+
const attachment = processLinkPath(linkPath, anglePath ? `<${anglePath}>` : regularPath, markdownFilePath, sourceDir);
|
|
333
|
+
if (attachment) attachments.push(attachment);
|
|
334
|
+
}
|
|
335
|
+
const imgTagRegex = /<img\s+[^>]*src=(?<quote>["'])(?<src>.*?)\k<quote>[^>]*>/gi;
|
|
336
|
+
while ((match = imgTagRegex.exec(content)) !== null) {
|
|
337
|
+
const { src } = match.groups;
|
|
338
|
+
if (!src) continue;
|
|
339
|
+
const attachment = processLinkPath(src, src, markdownFilePath, sourceDir);
|
|
340
|
+
if (attachment) attachments.push(attachment);
|
|
267
341
|
}
|
|
268
342
|
return attachments;
|
|
269
343
|
};
|
|
@@ -368,6 +442,11 @@ const replaceAttachmentLinks = (markdown, attachments, pageName) => {
|
|
|
368
442
|
replaced = true;
|
|
369
443
|
result = result.replace(linkRegex, `[$1](${growiPath})`);
|
|
370
444
|
}
|
|
445
|
+
const imgTagRegex = new RegExp(`(<img\\s+[^>]*src=)(["'])${pattern}\\2([^>]*>)`, "gi");
|
|
446
|
+
if (imgTagRegex.test(result)) {
|
|
447
|
+
replaced = true;
|
|
448
|
+
result = result.replace(imgTagRegex, `$1$2${growiPath}$2$3`);
|
|
449
|
+
}
|
|
371
450
|
}
|
|
372
451
|
}
|
|
373
452
|
return {
|
|
@@ -445,6 +524,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
445
524
|
} else if (result.action === "error") {
|
|
446
525
|
stats.pageErrors++;
|
|
447
526
|
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${result.errorMessage || "unknown error"})`);
|
|
527
|
+
if (config.verbose && result.error) console.error(formatDetailedError(result.error));
|
|
448
528
|
}
|
|
449
529
|
if (result.pageId && (result.action === "created" || result.action === "updated")) {
|
|
450
530
|
let currentContent = content;
|
|
@@ -465,6 +545,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
465
545
|
} else {
|
|
466
546
|
stats.attachmentErrors++;
|
|
467
547
|
console.error(`[ERROR] ${attachment.localPath} → ${file.growiPath} (${attachmentResult.errorMessage || "failed to upload attachment"})`);
|
|
548
|
+
if (config.verbose && attachmentResult.error) console.error(formatDetailedError(attachmentResult.error));
|
|
468
549
|
}
|
|
469
550
|
}
|
|
470
551
|
if (hasAttachments && latestRevisionId) {
|
|
@@ -479,6 +560,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
479
560
|
currentRevisionId = updateResult.revisionId;
|
|
480
561
|
} else {
|
|
481
562
|
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (failed to update attachment links: ${updateResult.errorMessage || "unknown error"})`);
|
|
563
|
+
if (config.verbose && updateResult.error) console.error(formatDetailedError(updateResult.error));
|
|
482
564
|
stats.linkReplacementErrors++;
|
|
483
565
|
}
|
|
484
566
|
}
|
|
@@ -493,6 +575,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
493
575
|
currentRevisionId = updateResult.revisionId;
|
|
494
576
|
} else {
|
|
495
577
|
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (failed to update page links: ${updateResult.errorMessage || "unknown error"})`);
|
|
578
|
+
if (config.verbose && updateResult.error) console.error(formatDetailedError(updateResult.error));
|
|
496
579
|
stats.linkReplacementErrors++;
|
|
497
580
|
}
|
|
498
581
|
}
|
|
@@ -508,8 +591,9 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
508
591
|
//#endregion
|
|
509
592
|
//#region src/index.ts
|
|
510
593
|
const program = new Command();
|
|
511
|
-
const main = async (sourceDir, configPath) => {
|
|
594
|
+
const main = async (sourceDir, configPath, verboseOverride) => {
|
|
512
595
|
const config = loadConfig(configPath);
|
|
596
|
+
if (verboseOverride !== void 0) config.verbose = verboseOverride;
|
|
513
597
|
const sourceDirPath = resolve(sourceDir);
|
|
514
598
|
configureAxios(config.url, config.token);
|
|
515
599
|
const files = await scanMarkdownFiles(sourceDirPath, config.basePath);
|
|
@@ -526,9 +610,9 @@ const main = async (sourceDir, configPath) => {
|
|
|
526
610
|
console.log(`- Attachment errors: ${stats.attachmentErrors}`);
|
|
527
611
|
console.log(`- Link replacement errors: ${stats.linkReplacementErrors}`);
|
|
528
612
|
};
|
|
529
|
-
program.name("growi-uploader").description("A content uploader for GROWI").version(version).argument("<source-dir>", "Source directory containing Markdown files").option("-c, --config <path>", "Path to config file", "growi-uploader.json").action(async (sourceDir, options) => {
|
|
613
|
+
program.name("growi-uploader").description("A content uploader for GROWI").version(version).argument("<source-dir>", "Source directory containing Markdown files").option("-c, --config <path>", "Path to config file", "growi-uploader.json").option("-v, --verbose", "Enable verbose error output with detailed information").action(async (sourceDir, options) => {
|
|
530
614
|
try {
|
|
531
|
-
await main(sourceDir, options.config);
|
|
615
|
+
await main(sourceDir, options.config, options.verbose);
|
|
532
616
|
} catch (error) {
|
|
533
617
|
console.error("Error:", error instanceof Error ? error.message : error);
|
|
534
618
|
process.exit(1);
|