@onozaty/growi-uploader 1.4.0 → 1.6.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.ja.md +341 -0
- package/README.md +44 -1
- package/dist/index.mjs +86 -12
- package/package.json +1 -1
package/README.ja.md
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# growi-uploader
|
|
2
|
+
|
|
3
|
+
日本語 | [English](README.md)
|
|
4
|
+
|
|
5
|
+
[](https://github.com/onozaty/growi-uploader/actions/workflows/test.yaml)
|
|
6
|
+
[](https://codecov.io/gh/onozaty/growi-uploader)
|
|
7
|
+
[](https://www.npmjs.com/package/@onozaty/growi-uploader)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
ローカルのMarkdownファイルと添付ファイルを[GROWI](https://growi.org/) Wikiに一括アップロードするCLIツールです。
|
|
11
|
+
|
|
12
|
+
## 機能
|
|
13
|
+
|
|
14
|
+
- 📁 **ディレクトリ構造の維持** - ローカルのフォルダ階層がGROWIのページ階層になります
|
|
15
|
+
- 📝 **Markdownファイルのアップロード** - `.md`ファイルからGROWIページを作成・更新
|
|
16
|
+
- 📎 **添付ファイルの自動検出** - `<ページ名>_attachment_<ファイル名>`パターンのファイルを自動的に添付ファイルとしてアップロード
|
|
17
|
+
- 🔗 **リンクの自動置換** - ローカルの添付ファイルへのリンクをGROWI形式(`/attachment/{id}`)に自動変換
|
|
18
|
+
- 🖼️ **画像の埋め込み** - 画像リンク(``)を自動変換
|
|
19
|
+
- ⚙️ **柔軟な設定** - ベースパス、更新動作などを制御可能
|
|
20
|
+
|
|
21
|
+
## クイックスタート
|
|
22
|
+
|
|
23
|
+
1. 設定ファイル`growi-uploader.json`を作成:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"url": "https://your-growi-instance.com",
|
|
28
|
+
"token": "your-api-token",
|
|
29
|
+
"basePath": "/",
|
|
30
|
+
"update": false
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
2. npxで実行(インストール不要):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx @onozaty/growi-uploader ./docs
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
これだけです!ローカルの`./docs`ディレクトリがGROWIにアップロードされます。
|
|
41
|
+
|
|
42
|
+
## インストール
|
|
43
|
+
|
|
44
|
+
### npxを使用(推奨)
|
|
45
|
+
|
|
46
|
+
インストール不要で実行できます:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx @onozaty/growi-uploader <source-dir>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### グローバルインストール
|
|
53
|
+
|
|
54
|
+
頻繁に使用する場合は、グローバルインストールできます:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install -g @onozaty/growi-uploader
|
|
58
|
+
growi-uploader <source-dir>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 使い方
|
|
62
|
+
|
|
63
|
+
### 基本コマンド
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
growi-uploader <source-dir> [options]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**引数:**
|
|
70
|
+
- `<source-dir>`: Markdownファイルを含むディレクトリのパス
|
|
71
|
+
|
|
72
|
+
**オプション:**
|
|
73
|
+
- `-c, --config <path>`: 設定ファイルのパス(デフォルト: `growi-uploader.json`)
|
|
74
|
+
- `-v, --verbose`: 詳細なエラー出力を有効化
|
|
75
|
+
- `-V, --version`: バージョン番号を表示
|
|
76
|
+
- `-h, --help`: ヘルプ情報を表示
|
|
77
|
+
|
|
78
|
+
### 実行例
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Upload with default config file
|
|
82
|
+
npx @onozaty/growi-uploader ./docs
|
|
83
|
+
|
|
84
|
+
# Upload with custom config file
|
|
85
|
+
npx @onozaty/growi-uploader ./docs -c my-config.json
|
|
86
|
+
|
|
87
|
+
# Upload with verbose error output
|
|
88
|
+
npx @onozaty/growi-uploader ./docs --verbose
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## ディレクトリ構造の例
|
|
92
|
+
|
|
93
|
+
### ローカルディレクトリ
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
docs/
|
|
97
|
+
guide.md
|
|
98
|
+
guide_attachment_diagram.svg
|
|
99
|
+
guide_attachment_sample.txt
|
|
100
|
+
api/
|
|
101
|
+
overview.md
|
|
102
|
+
overview_attachment_example.json
|
|
103
|
+
authentication.md
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### アップロード後のGROWIページ
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
/docs/guide (from guide.md)
|
|
110
|
+
└─ diagram.svg (attachment)
|
|
111
|
+
└─ sample.txt (attachment)
|
|
112
|
+
/docs/api/overview (from api/overview.md)
|
|
113
|
+
└─ example.json (attachment)
|
|
114
|
+
/docs/api/authentication (from api/authentication.md)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## ページ名の正規化
|
|
118
|
+
|
|
119
|
+
APIエラーを防ぐため、ページ名は以下のルールで自動的に正規化されます:
|
|
120
|
+
|
|
121
|
+
### 正規化ルール
|
|
122
|
+
|
|
123
|
+
1. **スラッシュ前後のスペース** → アンダースコアに置換
|
|
124
|
+
- `a / b.md` → `/a_/_b`
|
|
125
|
+
|
|
126
|
+
2. **特殊文字** → 安全な文字列に置換:
|
|
127
|
+
- `+` → `-plus-`
|
|
128
|
+
- `?` → `-question-`
|
|
129
|
+
- `*` → `-asterisk-`
|
|
130
|
+
- `$` → `-dollar-`
|
|
131
|
+
- `^` → `-caret-`
|
|
132
|
+
- `%` → `-percent-`
|
|
133
|
+
|
|
134
|
+
3. **予約済みページ名** → アンダースコアを末尾に追加:
|
|
135
|
+
- `edit` → `edit_` (パスの最後のセグメントの場合のみ)
|
|
136
|
+
|
|
137
|
+
### 例
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
Local file GROWI page path
|
|
141
|
+
──────────────────────────────────────────────────
|
|
142
|
+
C++.md → /C-plus--plus-
|
|
143
|
+
What?.md → /What-question-
|
|
144
|
+
C++ / Python?.md → /C-plus--plus-_/_Python-question-
|
|
145
|
+
edit.md → /edit_
|
|
146
|
+
docs/edit.md → /docs/edit_
|
|
147
|
+
docs/normal-page.md → /docs/normal-page (no change)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
この正規化により、GROWIのページ名要件との互換性を確保しつつ、ファイル名の可読性を維持します。
|
|
151
|
+
|
|
152
|
+
## 設定ファイル
|
|
153
|
+
|
|
154
|
+
プロジェクトルートに`growi-uploader.json`ファイルを作成:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"url": "https://your-growi-instance.com",
|
|
159
|
+
"token": "your-api-token",
|
|
160
|
+
"basePath": "/imported",
|
|
161
|
+
"update": true,
|
|
162
|
+
"verbose": false
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 設定オプション
|
|
167
|
+
|
|
168
|
+
| オプション | 型 | 必須 | デフォルト | 説明 |
|
|
169
|
+
|--------|------|----------|---------|-------------|
|
|
170
|
+
| `url` | string | ✅ | - | GROWIインスタンスのURL |
|
|
171
|
+
| `token` | string | ✅ | - | GROWI APIアクセストークン |
|
|
172
|
+
| `basePath` | string | ❌ | `/` | インポートされるページのベースパス |
|
|
173
|
+
| `update` | boolean | ❌ | `false` | trueの場合は既存ページを更新、falseの場合はスキップ |
|
|
174
|
+
| `verbose` | boolean | ❌ | `false` | 詳細なエラー出力を有効化 |
|
|
175
|
+
|
|
176
|
+
### APIトークンの取得方法
|
|
177
|
+
|
|
178
|
+
1. GROWIインスタンスにログイン
|
|
179
|
+
2. **ユーザー設定** → **API設定** に移動
|
|
180
|
+
3. **新しいトークンを発行** をクリック
|
|
181
|
+
4. 生成されたトークンを設定ファイルにコピー
|
|
182
|
+
|
|
183
|
+
## 添付ファイル
|
|
184
|
+
|
|
185
|
+
添付ファイルは2つの方法で自動検出されます:
|
|
186
|
+
|
|
187
|
+
### 方法1: 命名規則
|
|
188
|
+
|
|
189
|
+
以下の命名パターンに従うファイルが添付ファイルとして検出されます:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
<ページ名>_attachment_<ファイル名>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**例:**
|
|
196
|
+
```
|
|
197
|
+
guide.md → GROWI page: /guide
|
|
198
|
+
guide_attachment_image.png → Attached to /guide
|
|
199
|
+
guide_attachment_document.pdf → Attached to /guide
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 方法2: リンクベースの検出
|
|
203
|
+
|
|
204
|
+
Markdownリンクで参照されているファイルが自動的に添付ファイルとして検出されます:
|
|
205
|
+
|
|
206
|
+
**ローカルディレクトリ:**
|
|
207
|
+
```
|
|
208
|
+
guide.md
|
|
209
|
+
assets/
|
|
210
|
+
banner.png
|
|
211
|
+
images/
|
|
212
|
+
logo.png
|
|
213
|
+
screenshot.png
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**guide.mdの内容:**
|
|
217
|
+
```markdown
|
|
218
|
+

|
|
219
|
+

|
|
220
|
+

|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
参照されているすべてのファイル(`logo.png`、`screenshot.png`、`banner.png`)は、`_attachment_`命名規則に従っていなくても、`/guide`ページに添付ファイルとしてアップロードされます。
|
|
224
|
+
|
|
225
|
+
**パスの解決:**
|
|
226
|
+
- Markdownエスケープシーケンスは解除されます(`\(` → `(`)
|
|
227
|
+
- URLエンコーディング(パーセントエンコーディング)はデコードされます(`%20` → スペース、`%E7%94%BB%E5%83%8F` → `画像`)
|
|
228
|
+
- 相対パス(`./`、`../`、またはプレフィックスなし): Markdownファイルのディレクトリからの相対パス
|
|
229
|
+
- 絶対パス(`/`で始まる): ソースディレクトリのルートからの絶対パス
|
|
230
|
+
- 例: `/assets/banner.png` → `<source-dir>/assets/banner.png`
|
|
231
|
+
|
|
232
|
+
**サポートされるリンク形式:**
|
|
233
|
+
```markdown
|
|
234
|
+
 # Standard relative path
|
|
235
|
+
 # Relative path without ./
|
|
236
|
+
 # URL-encoded Japanese filename
|
|
237
|
+
[File](./docs/my%20file.pdf) # URL-encoded space
|
|
238
|
+
[File](<./path/file (1).png>) # Special chars with angle brackets
|
|
239
|
+
.png) # Special chars with escaping
|
|
240
|
+
<img src="./images/logo.png" alt="Logo"> # HTML img tag (double quotes)
|
|
241
|
+
<img src='./images/logo.png' alt='Logo'> # HTML img tag (single quotes)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**検出から除外されるもの:**
|
|
245
|
+
- `.md`ファイル(ページリンクとして扱われます)
|
|
246
|
+
- 外部URL(`http://`、`https://`)
|
|
247
|
+
- 存在しないファイル
|
|
248
|
+
|
|
249
|
+
### リンクの自動置換
|
|
250
|
+
|
|
251
|
+
添付ファイルへのMarkdownリンクは自動的にGROWI形式(`/attachment/{id}`)に変換されます。
|
|
252
|
+
|
|
253
|
+
**例(命名規則):**
|
|
254
|
+
|
|
255
|
+
```markdown
|
|
256
|
+
# Before upload
|
|
257
|
+

|
|
258
|
+
Download the [documentation](guide_attachment_document.pdf).
|
|
259
|
+
|
|
260
|
+
# After upload (on GROWI)
|
|
261
|
+

|
|
262
|
+
Download the [documentation](/attachment/68f3a3fa794f665ad2c0d2b3).
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**例(リンクベース):**
|
|
266
|
+
|
|
267
|
+
```markdown
|
|
268
|
+
# Before upload
|
|
269
|
+

|
|
270
|
+
|
|
271
|
+
# After upload (on GROWI)
|
|
272
|
+

|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
両方の検出方法で、複数のリンク形式(`./`あり・なし)がサポートされています。
|
|
276
|
+
|
|
277
|
+
## 高度な使い方
|
|
278
|
+
|
|
279
|
+
### 既存ページの更新
|
|
280
|
+
|
|
281
|
+
設定ファイルで`update: true`を設定すると、既存ページを更新できます:
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"url": "https://your-growi-instance.com",
|
|
286
|
+
"token": "your-api-token",
|
|
287
|
+
"update": true
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 特定のパスへのインポート
|
|
292
|
+
|
|
293
|
+
`basePath`を使用して、すべてのページを特定のパス配下にインポートできます:
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{
|
|
297
|
+
"url": "https://your-growi-instance.com",
|
|
298
|
+
"token": "your-api-token",
|
|
299
|
+
"basePath": "/imported"
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**結果:**
|
|
304
|
+
```
|
|
305
|
+
docs/guide.md → /imported/docs/guide
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### 出力例
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
Found 5 Markdown file(s) and 3 attachment(s)
|
|
312
|
+
|
|
313
|
+
[SUCCESS] docs/guide.md → /docs/guide (created)
|
|
314
|
+
[SUCCESS] docs/guide_attachment_diagram.svg → /docs/guide (attachment)
|
|
315
|
+
[SUCCESS] docs/guide.md → /docs/guide (attachment links replaced)
|
|
316
|
+
[SUCCESS] docs/api/overview.md → /docs/api/overview (created)
|
|
317
|
+
[SKIP] docs/api/auth.md → /docs/api/auth (page already exists)
|
|
318
|
+
|
|
319
|
+
Completed:
|
|
320
|
+
- Pages created: 2
|
|
321
|
+
- Pages updated: 0
|
|
322
|
+
- Pages skipped: 1
|
|
323
|
+
- Page errors: 0
|
|
324
|
+
- Attachments uploaded: 2
|
|
325
|
+
- Attachments skipped: 0
|
|
326
|
+
- Attachment errors: 0
|
|
327
|
+
- Link replacement errors: 0
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## 必要環境
|
|
331
|
+
|
|
332
|
+
- Node.js 18以降
|
|
333
|
+
- REST API v3をサポートするGROWIインスタンス
|
|
334
|
+
|
|
335
|
+
## ライセンス
|
|
336
|
+
|
|
337
|
+
MIT
|
|
338
|
+
|
|
339
|
+
## 作者
|
|
340
|
+
|
|
341
|
+
[onozaty](https://github.com/onozaty)
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# growi-uploader
|
|
2
2
|
|
|
3
|
+
English | [日本語](README.ja.md)
|
|
4
|
+
|
|
3
5
|
[](https://github.com/onozaty/growi-uploader/actions/workflows/test.yaml)
|
|
4
6
|
[](https://codecov.io/gh/onozaty/growi-uploader)
|
|
5
7
|
[](https://www.npmjs.com/package/@onozaty/growi-uploader)
|
|
@@ -69,6 +71,7 @@ growi-uploader <source-dir> [options]
|
|
|
69
71
|
|
|
70
72
|
**Options:**
|
|
71
73
|
- `-c, --config <path>`: Path to config file (default: `growi-uploader.json`)
|
|
74
|
+
- `-v, --verbose`: Enable verbose error output with detailed information
|
|
72
75
|
- `-V, --version`: Output the version number
|
|
73
76
|
- `-h, --help`: Display help information
|
|
74
77
|
|
|
@@ -80,6 +83,9 @@ npx @onozaty/growi-uploader ./docs
|
|
|
80
83
|
|
|
81
84
|
# Upload with custom config file
|
|
82
85
|
npx @onozaty/growi-uploader ./docs -c my-config.json
|
|
86
|
+
|
|
87
|
+
# Upload with verbose error output
|
|
88
|
+
npx @onozaty/growi-uploader ./docs --verbose
|
|
83
89
|
```
|
|
84
90
|
|
|
85
91
|
## Directory Structure Example
|
|
@@ -108,6 +114,41 @@ docs/
|
|
|
108
114
|
/docs/api/authentication (from api/authentication.md)
|
|
109
115
|
```
|
|
110
116
|
|
|
117
|
+
## Page Name Normalization
|
|
118
|
+
|
|
119
|
+
To prevent API errors, page names are automatically normalized using the following rules:
|
|
120
|
+
|
|
121
|
+
### Normalization Rules
|
|
122
|
+
|
|
123
|
+
1. **Spaces around slashes** → Replaced with underscores
|
|
124
|
+
- `a / b.md` → `/a_/_b`
|
|
125
|
+
|
|
126
|
+
2. **Special characters** → Replaced with safe alternatives:
|
|
127
|
+
- `+` → `-plus-`
|
|
128
|
+
- `?` → `-question-`
|
|
129
|
+
- `*` → `-asterisk-`
|
|
130
|
+
- `$` → `-dollar-`
|
|
131
|
+
- `^` → `-caret-`
|
|
132
|
+
- `%` → `-percent-`
|
|
133
|
+
|
|
134
|
+
3. **Reserved page names** → Suffixed with underscore:
|
|
135
|
+
- `edit` → `edit_` (only when it's the last path segment)
|
|
136
|
+
|
|
137
|
+
### Examples
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
Local file GROWI page path
|
|
141
|
+
──────────────────────────────────────────────────
|
|
142
|
+
C++.md → /C-plus--plus-
|
|
143
|
+
What?.md → /What-question-
|
|
144
|
+
C++ / Python?.md → /C-plus--plus-_/_Python-question-
|
|
145
|
+
edit.md → /edit_
|
|
146
|
+
docs/edit.md → /docs/edit_
|
|
147
|
+
docs/normal-page.md → /docs/normal-page (no change)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
This normalization ensures compatibility with GROWI's page naming requirements while preserving the readability of your file names.
|
|
151
|
+
|
|
111
152
|
## Configuration File
|
|
112
153
|
|
|
113
154
|
Create a `growi-uploader.json` file in your project root:
|
|
@@ -117,7 +158,8 @@ Create a `growi-uploader.json` file in your project root:
|
|
|
117
158
|
"url": "https://your-growi-instance.com",
|
|
118
159
|
"token": "your-api-token",
|
|
119
160
|
"basePath": "/imported",
|
|
120
|
-
"update": true
|
|
161
|
+
"update": true,
|
|
162
|
+
"verbose": false
|
|
121
163
|
}
|
|
122
164
|
```
|
|
123
165
|
|
|
@@ -129,6 +171,7 @@ Create a `growi-uploader.json` file in your project root:
|
|
|
129
171
|
| `token` | string | ✅ | - | GROWI API access token |
|
|
130
172
|
| `basePath` | string | ❌ | `/` | Base path for imported pages |
|
|
131
173
|
| `update` | boolean | ❌ | `false` | Update existing pages if true, skip if false |
|
|
174
|
+
| `verbose` | boolean | ❌ | `false` | Enable verbose error output with detailed information |
|
|
132
175
|
|
|
133
176
|
### Getting an API Token
|
|
134
177
|
|
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.6.0";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/config.ts
|
|
@@ -19,6 +19,7 @@ var version = "1.4.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
|
};
|
|
@@ -308,6 +363,19 @@ const mergeAttachments = (namingAttachments, linkAttachments) => {
|
|
|
308
363
|
}
|
|
309
364
|
return Array.from(map.values());
|
|
310
365
|
};
|
|
366
|
+
/**
|
|
367
|
+
* Normalize GROWI page path to avoid API errors
|
|
368
|
+
*
|
|
369
|
+
* @param path Raw GROWI page path
|
|
370
|
+
* @returns Normalized page path
|
|
371
|
+
*/
|
|
372
|
+
const normalizeGrowiPath = (path) => {
|
|
373
|
+
let normalized = path;
|
|
374
|
+
normalized = normalized.replace(/\s+\/\s+/g, "_/_");
|
|
375
|
+
normalized = normalized.replace(/\+/g, "-plus-").replace(/\?/g, "-question-").replace(/\*/g, "-asterisk-").replace(/\$/g, "-dollar-").replace(/\^/g, "-caret-").replace(/%/g, "-percent-");
|
|
376
|
+
normalized = normalized.replace(/\/edit$/g, "/edit_");
|
|
377
|
+
return normalized;
|
|
378
|
+
};
|
|
311
379
|
const scanMarkdownFiles = async (sourceDir, basePath = "/") => {
|
|
312
380
|
const pageFiles = (await glob("**/*.md", {
|
|
313
381
|
cwd: sourceDir,
|
|
@@ -316,7 +384,8 @@ const scanMarkdownFiles = async (sourceDir, basePath = "/") => {
|
|
|
316
384
|
pageFiles.sort();
|
|
317
385
|
return await Promise.all(pageFiles.map(async (file) => {
|
|
318
386
|
const content = readFileSync(join(sourceDir, file), "utf-8");
|
|
319
|
-
|
|
387
|
+
let growiPath = join(basePath, file.replace(/\.md$/, "")).replace(/\\/g, "/");
|
|
388
|
+
growiPath = normalizeGrowiPath(growiPath);
|
|
320
389
|
const dir = dirname(file);
|
|
321
390
|
const pageName = basename(file, ".md");
|
|
322
391
|
const attachments = mergeAttachments((await glob(dir === "." ? `${pageName}_attachment_*` : `${dir}/${pageName}_attachment_*`, {
|
|
@@ -469,6 +538,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
469
538
|
} else if (result.action === "error") {
|
|
470
539
|
stats.pageErrors++;
|
|
471
540
|
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${result.errorMessage || "unknown error"})`);
|
|
541
|
+
if (config.verbose && result.error) console.error(formatDetailedError(result.error));
|
|
472
542
|
}
|
|
473
543
|
if (result.pageId && (result.action === "created" || result.action === "updated")) {
|
|
474
544
|
let currentContent = content;
|
|
@@ -489,6 +559,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
489
559
|
} else {
|
|
490
560
|
stats.attachmentErrors++;
|
|
491
561
|
console.error(`[ERROR] ${attachment.localPath} → ${file.growiPath} (${attachmentResult.errorMessage || "failed to upload attachment"})`);
|
|
562
|
+
if (config.verbose && attachmentResult.error) console.error(formatDetailedError(attachmentResult.error));
|
|
492
563
|
}
|
|
493
564
|
}
|
|
494
565
|
if (hasAttachments && latestRevisionId) {
|
|
@@ -503,6 +574,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
503
574
|
currentRevisionId = updateResult.revisionId;
|
|
504
575
|
} else {
|
|
505
576
|
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (failed to update attachment links: ${updateResult.errorMessage || "unknown error"})`);
|
|
577
|
+
if (config.verbose && updateResult.error) console.error(formatDetailedError(updateResult.error));
|
|
506
578
|
stats.linkReplacementErrors++;
|
|
507
579
|
}
|
|
508
580
|
}
|
|
@@ -517,6 +589,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
517
589
|
currentRevisionId = updateResult.revisionId;
|
|
518
590
|
} else {
|
|
519
591
|
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (failed to update page links: ${updateResult.errorMessage || "unknown error"})`);
|
|
592
|
+
if (config.verbose && updateResult.error) console.error(formatDetailedError(updateResult.error));
|
|
520
593
|
stats.linkReplacementErrors++;
|
|
521
594
|
}
|
|
522
595
|
}
|
|
@@ -532,8 +605,9 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
532
605
|
//#endregion
|
|
533
606
|
//#region src/index.ts
|
|
534
607
|
const program = new Command();
|
|
535
|
-
const main = async (sourceDir, configPath) => {
|
|
608
|
+
const main = async (sourceDir, configPath, verboseOverride) => {
|
|
536
609
|
const config = loadConfig(configPath);
|
|
610
|
+
if (verboseOverride !== void 0) config.verbose = verboseOverride;
|
|
537
611
|
const sourceDirPath = resolve(sourceDir);
|
|
538
612
|
configureAxios(config.url, config.token);
|
|
539
613
|
const files = await scanMarkdownFiles(sourceDirPath, config.basePath);
|
|
@@ -550,9 +624,9 @@ const main = async (sourceDir, configPath) => {
|
|
|
550
624
|
console.log(`- Attachment errors: ${stats.attachmentErrors}`);
|
|
551
625
|
console.log(`- Link replacement errors: ${stats.linkReplacementErrors}`);
|
|
552
626
|
};
|
|
553
|
-
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) => {
|
|
627
|
+
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) => {
|
|
554
628
|
try {
|
|
555
|
-
await main(sourceDir, options.config);
|
|
629
|
+
await main(sourceDir, options.config, options.verbose);
|
|
556
630
|
} catch (error) {
|
|
557
631
|
console.error("Error:", error instanceof Error ? error.message : error);
|
|
558
632
|
process.exit(1);
|