@onozaty/growi-uploader 1.0.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/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/index.mjs +397 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 onozaty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# growi-uploader
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@onozaty/growi-uploader)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A CLI tool to batch upload local Markdown files and attachments to [GROWI](https://growi.org/) Wiki.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 📁 **Preserves directory structure** - Local folder hierarchy becomes GROWI page hierarchy
|
|
11
|
+
- 📝 **Markdown file upload** - Creates or updates GROWI pages from `.md` files
|
|
12
|
+
- 📎 **Automatic attachment detection** - Files matching `<page>_attachment_<file>` pattern are uploaded as attachments
|
|
13
|
+
- 🔗 **Link replacement** - Automatically converts local attachment links to GROWI format (`/attachment/{id}`)
|
|
14
|
+
- 🖼️ **Image embedding** - Supports image links (``) with automatic conversion
|
|
15
|
+
- ⚙️ **Flexible configuration** - Control base path, update behavior, and more
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
1. Create a configuration file `growi-uploader.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"url": "https://your-growi-instance.com",
|
|
24
|
+
"token": "your-api-token",
|
|
25
|
+
"basePath": "/",
|
|
26
|
+
"update": false
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
2. Run with npx (no installation required):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @onozaty/growi-uploader ./docs
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That's it! Your local `./docs` directory will be uploaded to GROWI.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
### Using npx (Recommended)
|
|
41
|
+
|
|
42
|
+
No installation required. Just run:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx @onozaty/growi-uploader <source-dir>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Global Installation
|
|
49
|
+
|
|
50
|
+
For frequent use, you can install globally:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install -g @onozaty/growi-uploader
|
|
54
|
+
growi-uploader <source-dir>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
### Basic Command
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
growi-uploader <source-dir> [options]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Arguments:**
|
|
66
|
+
- `<source-dir>`: Path to the directory containing Markdown files
|
|
67
|
+
|
|
68
|
+
**Options:**
|
|
69
|
+
- `-c, --config <path>`: Path to config file (default: `growi-uploader.json`)
|
|
70
|
+
- `-V, --version`: Output the version number
|
|
71
|
+
- `-h, --help`: Display help information
|
|
72
|
+
|
|
73
|
+
### Examples
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Upload with default config file
|
|
77
|
+
npx @onozaty/growi-uploader ./docs
|
|
78
|
+
|
|
79
|
+
# Upload with custom config file
|
|
80
|
+
npx @onozaty/growi-uploader ./docs -c my-config.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Directory Structure Example
|
|
84
|
+
|
|
85
|
+
### Local Directory
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
docs/
|
|
89
|
+
guide.md
|
|
90
|
+
guide_attachment_diagram.svg
|
|
91
|
+
guide_attachment_sample.txt
|
|
92
|
+
api/
|
|
93
|
+
overview.md
|
|
94
|
+
overview_attachment_example.json
|
|
95
|
+
authentication.md
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Resulting GROWI Pages
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
/docs/guide (from guide.md)
|
|
102
|
+
└─ diagram.svg (attachment)
|
|
103
|
+
└─ sample.txt (attachment)
|
|
104
|
+
/docs/api/overview (from api/overview.md)
|
|
105
|
+
└─ example.json (attachment)
|
|
106
|
+
/docs/api/authentication (from api/authentication.md)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Configuration File
|
|
110
|
+
|
|
111
|
+
Create a `growi-uploader.json` file in your project root:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"url": "https://your-growi-instance.com",
|
|
116
|
+
"token": "your-api-token",
|
|
117
|
+
"basePath": "/imported",
|
|
118
|
+
"update": true
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Configuration Options
|
|
123
|
+
|
|
124
|
+
| Option | Type | Required | Default | Description |
|
|
125
|
+
|--------|------|----------|---------|-------------|
|
|
126
|
+
| `url` | string | ✅ | - | GROWI instance URL |
|
|
127
|
+
| `token` | string | ✅ | - | GROWI API access token |
|
|
128
|
+
| `basePath` | string | ❌ | `/` | Base path for imported pages |
|
|
129
|
+
| `update` | boolean | ❌ | `false` | Update existing pages if true, skip if false |
|
|
130
|
+
|
|
131
|
+
### Getting an API Token
|
|
132
|
+
|
|
133
|
+
1. Log in to your GROWI instance
|
|
134
|
+
2. Go to **User Settings** → **API Settings**
|
|
135
|
+
3. Click **Issue new token**
|
|
136
|
+
4. Copy the generated token to your config file
|
|
137
|
+
|
|
138
|
+
## Attachment Files
|
|
139
|
+
|
|
140
|
+
### Naming Convention
|
|
141
|
+
|
|
142
|
+
Attachment files must follow this naming pattern:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
<page-name>_attachment_<filename>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Example:**
|
|
149
|
+
```
|
|
150
|
+
guide.md → GROWI page: /guide
|
|
151
|
+
guide_attachment_image.png → Attached to /guide
|
|
152
|
+
guide_attachment_document.pdf → Attached to /guide
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Automatic Link Replacement
|
|
156
|
+
|
|
157
|
+
Markdown links to attachments are automatically converted to GROWI format.
|
|
158
|
+
|
|
159
|
+
**Local Markdown (before upload):**
|
|
160
|
+
```markdown
|
|
161
|
+
# User Guide
|
|
162
|
+
|
|
163
|
+

|
|
164
|
+
|
|
165
|
+
Download the [documentation](guide_attachment_document.pdf).
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**GROWI Page (after upload):**
|
|
169
|
+
```markdown
|
|
170
|
+
# User Guide
|
|
171
|
+
|
|
172
|
+

|
|
173
|
+
|
|
174
|
+
Download the [documentation](/attachment/68f3a3fa794f665ad2c0d2b3).
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Supported Link Formats
|
|
178
|
+
|
|
179
|
+
Both formats are automatically detected and converted:
|
|
180
|
+
|
|
181
|
+
1. **Filename only**: ``
|
|
182
|
+
2. **Relative path**: ``
|
|
183
|
+
|
|
184
|
+
## Advanced Usage
|
|
185
|
+
|
|
186
|
+
### Update Existing Pages
|
|
187
|
+
|
|
188
|
+
Set `update: true` in your config file to update existing pages:
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"url": "https://your-growi-instance.com",
|
|
193
|
+
"token": "your-api-token",
|
|
194
|
+
"update": true
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Import to Specific Path
|
|
199
|
+
|
|
200
|
+
Use `basePath` to import all pages under a specific path:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"url": "https://your-growi-instance.com",
|
|
205
|
+
"token": "your-api-token",
|
|
206
|
+
"basePath": "/imported"
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Result:**
|
|
211
|
+
```
|
|
212
|
+
docs/guide.md → /imported/docs/guide
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Output Example
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
Found 5 Markdown file(s) and 3 attachment(s)
|
|
219
|
+
|
|
220
|
+
[SUCCESS] docs/guide.md → /docs/guide (created)
|
|
221
|
+
[SUCCESS] docs/guide_attachment_diagram.svg → /docs/guide (attachment)
|
|
222
|
+
[SUCCESS] docs/guide.md → /docs/guide (attachment links replaced)
|
|
223
|
+
[SUCCESS] docs/api/overview.md → /docs/api/overview (created)
|
|
224
|
+
[SKIP] docs/api/auth.md → /docs/api/auth (page already exists)
|
|
225
|
+
|
|
226
|
+
Completed:
|
|
227
|
+
- Pages created: 2
|
|
228
|
+
- Pages updated: 0
|
|
229
|
+
- Pages skipped: 1
|
|
230
|
+
- Page errors: 0
|
|
231
|
+
- Attachments uploaded: 2
|
|
232
|
+
- Attachments skipped: 0
|
|
233
|
+
- Attachment errors: 0
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Requirements
|
|
237
|
+
|
|
238
|
+
- Node.js 18 or later
|
|
239
|
+
- GROWI instance with REST API v3 support
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT
|
|
244
|
+
|
|
245
|
+
## Author
|
|
246
|
+
|
|
247
|
+
[onozaty](https://github.com/onozaty)
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { glob } from "glob";
|
|
6
|
+
import * as axios$1 from "axios";
|
|
7
|
+
import axios from "axios";
|
|
8
|
+
import { lookup } from "mime-types";
|
|
9
|
+
|
|
10
|
+
//#region src/config.ts
|
|
11
|
+
const loadConfig = (configPath) => {
|
|
12
|
+
const fullPath = resolve(configPath);
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
15
|
+
const config = JSON.parse(content);
|
|
16
|
+
if (!config.url) throw new Error("Missing required field: url");
|
|
17
|
+
if (!config.token) throw new Error("Missing required field: token");
|
|
18
|
+
if (config.basePath === void 0) config.basePath = "/";
|
|
19
|
+
if (config.update === void 0) config.update = false;
|
|
20
|
+
return config;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error.code === "ENOENT") throw new Error(`Config file not found: ${fullPath}`);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/scanner.ts
|
|
29
|
+
const scanMarkdownFiles = async (sourceDir, basePath = "/") => {
|
|
30
|
+
const mdFiles = await glob("**/*.md", {
|
|
31
|
+
cwd: sourceDir,
|
|
32
|
+
absolute: false
|
|
33
|
+
});
|
|
34
|
+
mdFiles.sort();
|
|
35
|
+
return await Promise.all(mdFiles.map(async (file) => {
|
|
36
|
+
const content = readFileSync(join(sourceDir, file), "utf-8");
|
|
37
|
+
const growiPath = join(basePath, file.replace(/\.md$/, "")).replace(/\\/g, "/");
|
|
38
|
+
const dir = dirname(file);
|
|
39
|
+
const pageName = basename(file, ".md");
|
|
40
|
+
const attachments = (await glob(dir === "." ? `${pageName}_attachment_*` : `${dir}/${pageName}_attachment_*`, {
|
|
41
|
+
cwd: sourceDir,
|
|
42
|
+
absolute: false
|
|
43
|
+
})).map((attachFile) => ({
|
|
44
|
+
localPath: attachFile,
|
|
45
|
+
fileName: basename(attachFile).replace(`${pageName}_attachment_`, "")
|
|
46
|
+
}));
|
|
47
|
+
return {
|
|
48
|
+
localPath: file,
|
|
49
|
+
growiPath: growiPath.startsWith("/") ? growiPath : `/${growiPath}`,
|
|
50
|
+
content,
|
|
51
|
+
attachments
|
|
52
|
+
};
|
|
53
|
+
}));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/growi.ts
|
|
58
|
+
/**
|
|
59
|
+
* Add attachment to the page
|
|
60
|
+
* @summary /attachment
|
|
61
|
+
*/
|
|
62
|
+
const postAttachment = (postAttachmentBody, options) => {
|
|
63
|
+
return axios$1.default.post(`/attachment`, postAttachmentBody, options);
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* get page by pagePath or pageId
|
|
67
|
+
* @summary Get page
|
|
68
|
+
*/
|
|
69
|
+
const getPage = (params, options) => {
|
|
70
|
+
return axios$1.default.get(`/page`, {
|
|
71
|
+
...options,
|
|
72
|
+
params: {
|
|
73
|
+
...params,
|
|
74
|
+
...options?.params
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Create page
|
|
80
|
+
* @summary Create page
|
|
81
|
+
*/
|
|
82
|
+
const postPage = (postPageBody, options) => {
|
|
83
|
+
return axios$1.default.post(`/page`, postPageBody, options);
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Update page
|
|
87
|
+
*/
|
|
88
|
+
const putPage = (putPageBody, options) => {
|
|
89
|
+
return axios$1.default.put(`/page`, putPageBody, options);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/uploader.ts
|
|
94
|
+
const configureAxios = (growiUrl, token) => {
|
|
95
|
+
axios.defaults.baseURL = `${growiUrl}/_api/v3`;
|
|
96
|
+
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
97
|
+
};
|
|
98
|
+
const createOrUpdatePage = async (file, config) => {
|
|
99
|
+
try {
|
|
100
|
+
const actualResponse = (await getPage({ path: file.growiPath })).data;
|
|
101
|
+
if (actualResponse.page) {
|
|
102
|
+
const pageId = actualResponse.page._id;
|
|
103
|
+
const revisionId = actualResponse.page.revision?._id;
|
|
104
|
+
if (!config.update) {
|
|
105
|
+
console.log(`[SKIP] ${file.localPath} → ${file.growiPath} (page already exists)`);
|
|
106
|
+
return {
|
|
107
|
+
pageId,
|
|
108
|
+
revisionId,
|
|
109
|
+
action: "skipped"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const newRevisionId = (await putPage({
|
|
113
|
+
body: file.content,
|
|
114
|
+
pageId,
|
|
115
|
+
revisionId
|
|
116
|
+
})).data.page?.revision;
|
|
117
|
+
console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (updated)`);
|
|
118
|
+
return {
|
|
119
|
+
pageId,
|
|
120
|
+
revisionId: newRevisionId || revisionId,
|
|
121
|
+
action: "updated"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (!axios.isAxiosError(error) || error.response?.status !== 404) {
|
|
126
|
+
if (axios.isAxiosError(error)) {
|
|
127
|
+
const status = error.response?.status;
|
|
128
|
+
const message = error.response?.data?.message || error.message;
|
|
129
|
+
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${status} ${message})`);
|
|
130
|
+
} else console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${error})`);
|
|
131
|
+
return {
|
|
132
|
+
pageId: void 0,
|
|
133
|
+
revisionId: void 0,
|
|
134
|
+
action: "error"
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const response = await postPage({
|
|
140
|
+
path: file.growiPath,
|
|
141
|
+
body: file.content
|
|
142
|
+
});
|
|
143
|
+
const pageId = response.data.page?._id;
|
|
144
|
+
const revisionId = response.data.page?.revision;
|
|
145
|
+
console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (created)`);
|
|
146
|
+
return {
|
|
147
|
+
pageId,
|
|
148
|
+
revisionId,
|
|
149
|
+
action: "created"
|
|
150
|
+
};
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (axios.isAxiosError(error)) {
|
|
153
|
+
const status = error.response?.status;
|
|
154
|
+
const message = error.response?.data?.message || error.message;
|
|
155
|
+
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${status} ${message})`);
|
|
156
|
+
} else console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${error})`);
|
|
157
|
+
return {
|
|
158
|
+
pageId: void 0,
|
|
159
|
+
revisionId: void 0,
|
|
160
|
+
action: "error"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Replace attachment links in Markdown content with GROWI format
|
|
166
|
+
*
|
|
167
|
+
* Supports:
|
|
168
|
+
* - Filename only: guide_attachment_file.png
|
|
169
|
+
* - Relative path: ./guide_attachment_file.png
|
|
170
|
+
*
|
|
171
|
+
* @param markdown Original Markdown content
|
|
172
|
+
* @param attachments List of attachments with their IDs
|
|
173
|
+
* @param pageName Page name (without .md extension)
|
|
174
|
+
* @returns Object with replaced content and whether any replacement occurred
|
|
175
|
+
*/
|
|
176
|
+
const replaceAttachmentLinks = (markdown, attachments, pageName) => {
|
|
177
|
+
let result = markdown;
|
|
178
|
+
let replaced = false;
|
|
179
|
+
for (const attachment of attachments) {
|
|
180
|
+
if (!attachment.attachmentId) continue;
|
|
181
|
+
const localFileName = `${pageName}_attachment_${attachment.fileName}`;
|
|
182
|
+
const growiPath = `/attachment/${attachment.attachmentId}`;
|
|
183
|
+
const escapedFileName = localFileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
184
|
+
const patterns = [escapedFileName, `\\./${escapedFileName}`];
|
|
185
|
+
for (const pattern of patterns) {
|
|
186
|
+
const imgRegex = new RegExp(`!\\[([^\\]]*)\\]\\(${pattern}\\)`, "g");
|
|
187
|
+
if (imgRegex.test(result)) {
|
|
188
|
+
replaced = true;
|
|
189
|
+
result = result.replace(imgRegex, ``);
|
|
190
|
+
}
|
|
191
|
+
const linkRegex = new RegExp(`(?<!!)\\[([^\\]]*)\\]\\(${pattern}\\)`, "g");
|
|
192
|
+
if (linkRegex.test(result)) {
|
|
193
|
+
replaced = true;
|
|
194
|
+
result = result.replace(linkRegex, `[$1](${growiPath})`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
content: result,
|
|
200
|
+
replaced
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Update page content only (for re-updating after attachment link replacement)
|
|
205
|
+
*
|
|
206
|
+
* @param pageId Page ID
|
|
207
|
+
* @param revisionId Current revision ID
|
|
208
|
+
* @param content New Markdown content
|
|
209
|
+
* @param growiPath GROWI page path (for logging)
|
|
210
|
+
* @returns True if update succeeded
|
|
211
|
+
*/
|
|
212
|
+
const updatePageContent = async (pageId, revisionId, content, growiPath) => {
|
|
213
|
+
try {
|
|
214
|
+
await putPage({
|
|
215
|
+
body: content,
|
|
216
|
+
pageId,
|
|
217
|
+
revisionId
|
|
218
|
+
});
|
|
219
|
+
return true;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (axios.isAxiosError(error)) {
|
|
222
|
+
const status = error.response?.status;
|
|
223
|
+
const message = error.response?.data?.message || error.message;
|
|
224
|
+
console.error(`[WARN] Failed to update attachment links for ${growiPath} (${status} ${message})`);
|
|
225
|
+
} else console.error(`[WARN] Failed to update attachment links for ${growiPath}`);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const uploadAttachment = async (attachment, pageId, growiPath, sourceDir) => {
|
|
230
|
+
try {
|
|
231
|
+
const fileBuffer = readFileSync(join(sourceDir, attachment.localPath));
|
|
232
|
+
const mimeType = lookup(attachment.fileName) || "application/octet-stream";
|
|
233
|
+
const formData = new FormData();
|
|
234
|
+
formData.append("page_id", pageId);
|
|
235
|
+
formData.append("file", new Blob([fileBuffer], { type: mimeType }), attachment.fileName);
|
|
236
|
+
const response = await postAttachment(formData);
|
|
237
|
+
const attachmentId = response.data.attachment?._id;
|
|
238
|
+
const revisionId = typeof response.data.revision === "string" ? response.data.revision : void 0;
|
|
239
|
+
console.log(`[SUCCESS] ${attachment.localPath} → ${growiPath} (attachment)`);
|
|
240
|
+
if (attachmentId && revisionId) return {
|
|
241
|
+
success: true,
|
|
242
|
+
attachmentId,
|
|
243
|
+
revisionId
|
|
244
|
+
};
|
|
245
|
+
else if (attachmentId) return {
|
|
246
|
+
success: true,
|
|
247
|
+
attachmentId
|
|
248
|
+
};
|
|
249
|
+
else if (revisionId) return {
|
|
250
|
+
success: true,
|
|
251
|
+
revisionId
|
|
252
|
+
};
|
|
253
|
+
else return { success: true };
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (axios.isAxiosError(error)) {
|
|
256
|
+
const status = error.response?.status;
|
|
257
|
+
const message = error.response?.data?.message || error.message;
|
|
258
|
+
console.error(`[ERROR] ${attachment.localPath} → ${growiPath} (${status} ${message})`);
|
|
259
|
+
} else console.error(`[ERROR] ${attachment.localPath} → ${growiPath} (${error})`);
|
|
260
|
+
return { success: false };
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region package.json
|
|
266
|
+
var name = "@onozaty/growi-uploader";
|
|
267
|
+
var version = "1.0.0";
|
|
268
|
+
var description = "A content uploader for GROWI";
|
|
269
|
+
var type = "module";
|
|
270
|
+
var bin = { "growi-uploader": "./dist/index.mjs" };
|
|
271
|
+
var files = ["dist/**/*"];
|
|
272
|
+
var scripts = {
|
|
273
|
+
"build": "pnpm gen && tsdown",
|
|
274
|
+
"prepublishOnly": "pnpm run build",
|
|
275
|
+
"dev": "tsx src/index.ts",
|
|
276
|
+
"gen": "orval",
|
|
277
|
+
"lint": "eslint .",
|
|
278
|
+
"typecheck": "tsc --noEmit",
|
|
279
|
+
"check": "pnpm run lint && pnpm run typecheck",
|
|
280
|
+
"format": "prettier --write src/"
|
|
281
|
+
};
|
|
282
|
+
var repository = {
|
|
283
|
+
"type": "git",
|
|
284
|
+
"url": "git+https://github.com/onozaty/growi-uploader.git"
|
|
285
|
+
};
|
|
286
|
+
var keywords = ["growi", "uploader"];
|
|
287
|
+
var author = "onozaty";
|
|
288
|
+
var license = "MIT";
|
|
289
|
+
var bugs = { "url": "https://github.com/onozaty/growi-uploader/issues" };
|
|
290
|
+
var homepage = "https://github.com/onozaty/growi-uploader#readme";
|
|
291
|
+
var packageManager = "pnpm@10.12.1";
|
|
292
|
+
var devDependencies = {
|
|
293
|
+
"@eslint/js": "^9.37.0",
|
|
294
|
+
"@types/mime-types": "^3.0.1",
|
|
295
|
+
"@types/node": "^24.7.2",
|
|
296
|
+
"eslint": "^9.37.0",
|
|
297
|
+
"globals": "^16.4.0",
|
|
298
|
+
"jiti": "^2.6.1",
|
|
299
|
+
"orval": "^7.13.2",
|
|
300
|
+
"prettier": "^3.6.2",
|
|
301
|
+
"tsdown": "^0.15.7",
|
|
302
|
+
"tsx": "^4.20.6",
|
|
303
|
+
"typescript": "^5.9.3",
|
|
304
|
+
"typescript-eslint": "^8.46.1"
|
|
305
|
+
};
|
|
306
|
+
var dependencies = {
|
|
307
|
+
"axios": "^1.12.2",
|
|
308
|
+
"commander": "^14.0.1",
|
|
309
|
+
"glob": "^11.0.3",
|
|
310
|
+
"mime-types": "^3.0.1"
|
|
311
|
+
};
|
|
312
|
+
var package_default = {
|
|
313
|
+
name,
|
|
314
|
+
version,
|
|
315
|
+
description,
|
|
316
|
+
type,
|
|
317
|
+
bin,
|
|
318
|
+
files,
|
|
319
|
+
scripts,
|
|
320
|
+
repository,
|
|
321
|
+
keywords,
|
|
322
|
+
author,
|
|
323
|
+
license,
|
|
324
|
+
bugs,
|
|
325
|
+
homepage,
|
|
326
|
+
packageManager,
|
|
327
|
+
devDependencies,
|
|
328
|
+
dependencies
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/index.ts
|
|
333
|
+
const program = new Command();
|
|
334
|
+
program.name("growi-uploader").description("A content uploader for GROWI").version(package_default.version).argument("<source-dir>", "Source directory containing Markdown files").option("-c, --config <path>", "Path to config file", "growi-uploader.json").action(async (sourceDir, options) => {
|
|
335
|
+
try {
|
|
336
|
+
const config = loadConfig(options.config);
|
|
337
|
+
const sourceDirPath = resolve(sourceDir);
|
|
338
|
+
configureAxios(config.url, config.token);
|
|
339
|
+
const files$1 = await scanMarkdownFiles(sourceDirPath, config.basePath);
|
|
340
|
+
const totalAttachments = files$1.reduce((sum, file) => sum + file.attachments.length, 0);
|
|
341
|
+
console.log(`Found ${files$1.length} Markdown file(s) and ${totalAttachments} attachment(s)\n`);
|
|
342
|
+
let pagesCreated = 0;
|
|
343
|
+
let pagesUpdated = 0;
|
|
344
|
+
let pagesSkipped = 0;
|
|
345
|
+
let pageErrors = 0;
|
|
346
|
+
let attachmentsUploaded = 0;
|
|
347
|
+
let attachmentsSkipped = 0;
|
|
348
|
+
let attachmentErrors = 0;
|
|
349
|
+
for (const file of files$1) {
|
|
350
|
+
const result = await createOrUpdatePage(file, config);
|
|
351
|
+
if (result.action === "created") pagesCreated++;
|
|
352
|
+
else if (result.action === "updated") pagesUpdated++;
|
|
353
|
+
else if (result.action === "skipped") pagesSkipped++;
|
|
354
|
+
else if (result.action === "error") pageErrors++;
|
|
355
|
+
if (result.pageId && (result.action === "created" || result.action === "updated") && file.attachments.length > 0) {
|
|
356
|
+
let hasAttachments = false;
|
|
357
|
+
let latestRevisionId = result.revisionId;
|
|
358
|
+
for (const attachment of file.attachments) {
|
|
359
|
+
const attachmentResult = await uploadAttachment(attachment, result.pageId, file.growiPath, sourceDirPath);
|
|
360
|
+
if (attachmentResult.success) {
|
|
361
|
+
attachmentsUploaded++;
|
|
362
|
+
if (attachmentResult.attachmentId) {
|
|
363
|
+
attachment.attachmentId = attachmentResult.attachmentId;
|
|
364
|
+
hasAttachments = true;
|
|
365
|
+
}
|
|
366
|
+
if (attachmentResult.revisionId) latestRevisionId = attachmentResult.revisionId;
|
|
367
|
+
} else attachmentErrors++;
|
|
368
|
+
}
|
|
369
|
+
if (hasAttachments && latestRevisionId) {
|
|
370
|
+
const pageName = basename(file.localPath, ".md");
|
|
371
|
+
const { content: replacedContent, replaced } = replaceAttachmentLinks(file.content, file.attachments, pageName);
|
|
372
|
+
if (replaced) {
|
|
373
|
+
if (await updatePageContent(result.pageId, latestRevisionId, replacedContent, file.growiPath)) console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (attachment links replaced)`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} else if (file.attachments.length > 0) for (const attachment of file.attachments) {
|
|
377
|
+
console.log(`[SKIP] ${attachment.localPath} → ${file.growiPath} (attachment skipped)`);
|
|
378
|
+
attachmentsSkipped++;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
console.log("\nCompleted:");
|
|
382
|
+
console.log(`- Pages created: ${pagesCreated}`);
|
|
383
|
+
console.log(`- Pages updated: ${pagesUpdated}`);
|
|
384
|
+
console.log(`- Pages skipped: ${pagesSkipped}`);
|
|
385
|
+
console.log(`- Page errors: ${pageErrors}`);
|
|
386
|
+
console.log(`- Attachments uploaded: ${attachmentsUploaded}`);
|
|
387
|
+
console.log(`- Attachments skipped: ${attachmentsSkipped}`);
|
|
388
|
+
console.log(`- Attachment errors: ${attachmentErrors}`);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
program.parse();
|
|
395
|
+
|
|
396
|
+
//#endregion
|
|
397
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onozaty/growi-uploader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A content uploader for GROWI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"growi-uploader": "./dist/index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/**/*"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "pnpm gen && tsdown",
|
|
14
|
+
"prepublishOnly": "pnpm run build",
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"gen": "orval",
|
|
17
|
+
"lint": "eslint .",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"check": "pnpm run lint && pnpm run typecheck",
|
|
20
|
+
"format": "prettier --write src/"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/onozaty/growi-uploader.git"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"growi",
|
|
28
|
+
"uploader"
|
|
29
|
+
],
|
|
30
|
+
"author": "onozaty",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/onozaty/growi-uploader/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/onozaty/growi-uploader#readme",
|
|
36
|
+
"packageManager": "pnpm@10.12.1",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@eslint/js": "^9.37.0",
|
|
39
|
+
"@types/mime-types": "^3.0.1",
|
|
40
|
+
"@types/node": "^24.7.2",
|
|
41
|
+
"eslint": "^9.37.0",
|
|
42
|
+
"globals": "^16.4.0",
|
|
43
|
+
"jiti": "^2.6.1",
|
|
44
|
+
"orval": "^7.13.2",
|
|
45
|
+
"prettier": "^3.6.2",
|
|
46
|
+
"tsdown": "^0.15.7",
|
|
47
|
+
"tsx": "^4.20.6",
|
|
48
|
+
"typescript": "^5.9.3",
|
|
49
|
+
"typescript-eslint": "^8.46.1"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"axios": "^1.12.2",
|
|
53
|
+
"commander": "^14.0.1",
|
|
54
|
+
"glob": "^11.0.3",
|
|
55
|
+
"mime-types": "^3.0.1"
|
|
56
|
+
}
|
|
57
|
+
}
|