backlog-exporter 1.0.0 → 1.1.1
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 +143 -636
- package/dist/commands/all/index.d.ts +1 -0
- package/dist/commands/all/index.js +8 -2
- package/dist/commands/document/index.d.ts +1 -0
- package/dist/commands/document/index.js +9 -2
- package/dist/commands/issue/index.d.ts +1 -0
- package/dist/commands/issue/index.js +12 -2
- package/dist/commands/update/index.d.ts +1 -0
- package/dist/commands/update/index.js +5 -0
- package/dist/commands/wiki/index.d.ts +1 -0
- package/dist/commands/wiki/index.js +9 -2
- package/dist/composition/backlog-repositories.d.ts +1 -1
- package/dist/composition/backlog-repositories.js +1 -1
- package/dist/modules/all/use-case/export-all.d.ts +1 -0
- package/dist/modules/all/use-case/export-all.js +7 -1
- package/dist/modules/document/domain/document-markdown.d.ts +1 -1
- package/dist/modules/document/domain/document-markdown.js +6 -3
- package/dist/modules/document/domain/document-path.d.ts +11 -1
- package/dist/modules/document/domain/document-path.js +17 -0
- package/dist/modules/document/domain/document-repository.d.ts +1 -0
- package/dist/modules/document/domain/document-save-plan.d.ts +4 -3
- package/dist/modules/document/domain/document-save-plan.js +11 -5
- package/dist/modules/document/domain/document-tree-gap.d.ts +2 -0
- package/dist/modules/document/domain/document-tree-gap.js +21 -0
- package/dist/modules/document/domain/document.d.ts +4 -0
- package/dist/modules/document/repository/backlog-document-repository.js +4 -0
- package/dist/modules/document/use-case/export-documents.d.ts +1 -0
- package/dist/modules/document/use-case/export-documents.js +72 -9
- package/dist/modules/issue/domain/issue-markdown.d.ts +4 -3
- package/dist/modules/issue/domain/issue-markdown.js +23 -8
- package/dist/modules/issue/domain/issue-path.d.ts +12 -1
- package/dist/modules/issue/domain/issue-path.js +12 -0
- package/dist/modules/issue/domain/issue-repository.d.ts +1 -0
- package/dist/modules/issue/domain/issue.d.ts +6 -0
- package/dist/modules/issue/repository/backlog-issue-repository.js +3 -0
- package/dist/modules/issue/use-case/export-issues.d.ts +1 -0
- package/dist/modules/issue/use-case/export-issues.js +29 -3
- package/dist/modules/prune/repository/prune-walker.js +5 -0
- package/dist/modules/prune/use-case/prune-directories.d.ts +1 -1
- package/dist/modules/prune/use-case/prune-directories.js +1 -1
- package/dist/modules/prune/use-case/prune-exports.js +15 -12
- package/dist/modules/settings/domain/settings.d.ts +1 -0
- package/dist/modules/update/domain/update-plan.d.ts +2 -0
- package/dist/modules/update/domain/update-plan.js +1 -0
- package/dist/modules/update/use-case/update-exports.d.ts +1 -1
- package/dist/modules/update/use-case/update-exports.js +4 -1
- package/dist/modules/wiki/domain/wiki-markdown.d.ts +6 -1
- package/dist/modules/wiki/domain/wiki-markdown.js +17 -2
- package/dist/modules/wiki/domain/wiki-path.d.ts +3 -0
- package/dist/modules/wiki/domain/wiki-path.js +12 -0
- package/dist/modules/wiki/domain/wiki-repository.d.ts +1 -0
- package/dist/modules/wiki/domain/wiki.d.ts +6 -0
- package/dist/modules/wiki/repository/backlog-wiki-repository.js +3 -0
- package/dist/modules/wiki/use-case/export-wikis.d.ts +1 -0
- package/dist/modules/wiki/use-case/export-wikis.js +31 -3
- package/dist/shared/attachment.d.ts +8 -0
- package/dist/shared/attachment.js +37 -0
- package/dist/shared/backlog/http-client.d.ts +5 -2
- package/dist/shared/backlog/http-client.js +41 -10
- package/dist/shared/backlog/sleep.d.ts +1 -0
- package/dist/shared/backlog/sleep.js +3 -0
- package/dist/shared/console/progress.js +8 -1
- package/dist/shared/file-name.d.ts +1 -0
- package/dist/shared/file-name.js +11 -0
- package/dist/shared/storage/markdown-store.d.ts +2 -0
- package/dist/shared/storage/markdown-store.js +11 -0
- package/oclif.manifest.json +43 -2
- package/package.json +1 -1
- package/dist/shared/backlog/rate-limiter.d.ts +0 -9
- package/dist/shared/backlog/rate-limiter.js +0 -25
|
@@ -5,6 +5,7 @@ export default class All extends Command {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
apiKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
domain: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
downloadAttachments: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
exclude: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
issueKeyFileName: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
issueKeyFolder: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -44,6 +44,11 @@ export default class All extends Command {
|
|
|
44
44
|
description: 'Backlog domain (e.g. example.backlog.jp)',
|
|
45
45
|
required: true,
|
|
46
46
|
}),
|
|
47
|
+
downloadAttachments: Flags.boolean({
|
|
48
|
+
char: 'd',
|
|
49
|
+
description: '課題の添付ファイルもダウンロードする',
|
|
50
|
+
required: false,
|
|
51
|
+
}),
|
|
47
52
|
exclude: Flags.string({
|
|
48
53
|
description: "Exclude the specified types, separated by commas (e.g., 'documents,wiki')",
|
|
49
54
|
required: false,
|
|
@@ -79,7 +84,7 @@ export default class All extends Command {
|
|
|
79
84
|
async run() {
|
|
80
85
|
const { flags } = await this.parse(All);
|
|
81
86
|
try {
|
|
82
|
-
const { domain, exclude, issueKeyFileName, issueKeyFolder, maxCount, only, projectIdOrKey } = flags;
|
|
87
|
+
const { domain, downloadAttachments, exclude, issueKeyFileName, issueKeyFolder, maxCount, only, projectIdOrKey } = flags;
|
|
83
88
|
const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
|
|
84
89
|
this.error(API_KEY_NOT_FOUND_MESSAGE);
|
|
85
90
|
const outputRoot = flags.output || './backlog-data';
|
|
@@ -93,11 +98,12 @@ export default class All extends Command {
|
|
|
93
98
|
const repositories = createBacklogRepositories({
|
|
94
99
|
apiKey,
|
|
95
100
|
domain,
|
|
96
|
-
|
|
101
|
+
onRateLimitExceeded: (waitSeconds) => this.log(`レート制限の上限に達しました。${waitSeconds}秒待機します...`),
|
|
97
102
|
});
|
|
98
103
|
await exportAll({ ...repositories, logger }, {
|
|
99
104
|
apiKey,
|
|
100
105
|
domain,
|
|
106
|
+
downloadAttachments,
|
|
101
107
|
issueKeyFileName,
|
|
102
108
|
issueKeyFolder,
|
|
103
109
|
maxCount,
|
|
@@ -5,6 +5,7 @@ export default class Document extends Command {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
apiKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
domain: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
downloadAttachments: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
keyword: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -29,6 +29,11 @@ export default class Document extends Command {
|
|
|
29
29
|
description: 'Backlog domain (e.g. example.backlog.jp)',
|
|
30
30
|
required: true,
|
|
31
31
|
}),
|
|
32
|
+
downloadAttachments: Flags.boolean({
|
|
33
|
+
char: 'd',
|
|
34
|
+
description: '添付ファイルもダウンロードする',
|
|
35
|
+
required: false,
|
|
36
|
+
}),
|
|
32
37
|
keyword: Flags.string({
|
|
33
38
|
description: '検索キーワード',
|
|
34
39
|
required: false,
|
|
@@ -46,7 +51,7 @@ export default class Document extends Command {
|
|
|
46
51
|
async run() {
|
|
47
52
|
const { flags } = await this.parse(Document);
|
|
48
53
|
try {
|
|
49
|
-
const { domain, keyword, projectIdOrKey } = flags;
|
|
54
|
+
const { domain, downloadAttachments, keyword, projectIdOrKey } = flags;
|
|
50
55
|
const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
|
|
51
56
|
this.error(API_KEY_NOT_FOUND_MESSAGE);
|
|
52
57
|
const outputDir = flags.output || './backlog-documents';
|
|
@@ -54,7 +59,7 @@ export default class Document extends Command {
|
|
|
54
59
|
const { documentRepository, projectRepository } = createBacklogRepositories({
|
|
55
60
|
apiKey,
|
|
56
61
|
domain,
|
|
57
|
-
|
|
62
|
+
onRateLimitExceeded: (waitSeconds) => this.log(`レート制限の上限に達しました。${waitSeconds}秒待機します...`),
|
|
58
63
|
});
|
|
59
64
|
// 出力ディレクトリの作成
|
|
60
65
|
await ensureDirectory(outputDir);
|
|
@@ -65,6 +70,7 @@ export default class Document extends Command {
|
|
|
65
70
|
await updateSettings(outputDir, {
|
|
66
71
|
apiKey,
|
|
67
72
|
domain,
|
|
73
|
+
downloadAttachments,
|
|
68
74
|
folderType: FolderType.DOCUMENT,
|
|
69
75
|
outputDir,
|
|
70
76
|
projectIdOrKey,
|
|
@@ -72,6 +78,7 @@ export default class Document extends Command {
|
|
|
72
78
|
// ドキュメントの取得と保存
|
|
73
79
|
await exportDocuments({ documentRepository, logger }, {
|
|
74
80
|
domain,
|
|
81
|
+
downloadAttachments,
|
|
75
82
|
keyword,
|
|
76
83
|
outputDir,
|
|
77
84
|
projectId,
|
|
@@ -5,6 +5,7 @@ export default class Issue extends Command {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
apiKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
domain: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
downloadAttachments: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
issueKeyFileName: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
issueKeyFolder: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
maxCount: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -30,6 +30,9 @@ export default class Issue extends Command {
|
|
|
30
30
|
`,
|
|
31
31
|
`<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFileName --issueKeyFolder
|
|
32
32
|
課題キーでフォルダを作成し、ファイル名も課題キーにする
|
|
33
|
+
`,
|
|
34
|
+
`<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --downloadAttachments
|
|
35
|
+
課題の添付ファイルもダウンロードする
|
|
33
36
|
`,
|
|
34
37
|
];
|
|
35
38
|
static flags = {
|
|
@@ -41,6 +44,11 @@ export default class Issue extends Command {
|
|
|
41
44
|
description: 'Backlog domain (e.g. example.backlog.jp)',
|
|
42
45
|
required: true,
|
|
43
46
|
}),
|
|
47
|
+
downloadAttachments: Flags.boolean({
|
|
48
|
+
char: 'd',
|
|
49
|
+
description: '課題の添付ファイルもダウンロードする',
|
|
50
|
+
required: false,
|
|
51
|
+
}),
|
|
44
52
|
issueKeyFileName: Flags.boolean({
|
|
45
53
|
description: 'ファイル名を課題キーにする',
|
|
46
54
|
required: false,
|
|
@@ -72,7 +80,7 @@ export default class Issue extends Command {
|
|
|
72
80
|
async run() {
|
|
73
81
|
const { flags } = await this.parse(Issue);
|
|
74
82
|
try {
|
|
75
|
-
const { domain, issueKeyFileName, issueKeyFolder, maxCount, projectIdOrKey, statusId } = flags;
|
|
83
|
+
const { domain, downloadAttachments, issueKeyFileName, issueKeyFolder, maxCount, projectIdOrKey, statusId } = flags;
|
|
76
84
|
const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
|
|
77
85
|
this.error(API_KEY_NOT_FOUND_MESSAGE);
|
|
78
86
|
const outputDir = flags.output || './backlog-issues';
|
|
@@ -80,7 +88,7 @@ export default class Issue extends Command {
|
|
|
80
88
|
const { issueRepository, projectRepository } = createBacklogRepositories({
|
|
81
89
|
apiKey,
|
|
82
90
|
domain,
|
|
83
|
-
|
|
91
|
+
onRateLimitExceeded: (waitSeconds) => this.log(`レート制限の上限に達しました。${waitSeconds}秒待機します...`),
|
|
84
92
|
});
|
|
85
93
|
// 出力ディレクトリの作成
|
|
86
94
|
await ensureDirectory(outputDir);
|
|
@@ -91,6 +99,7 @@ export default class Issue extends Command {
|
|
|
91
99
|
await updateSettings(outputDir, {
|
|
92
100
|
apiKey,
|
|
93
101
|
domain,
|
|
102
|
+
downloadAttachments,
|
|
94
103
|
folderType: FolderType.ISSUE,
|
|
95
104
|
issueKeyFileName,
|
|
96
105
|
issueKeyFolder,
|
|
@@ -101,6 +110,7 @@ export default class Issue extends Command {
|
|
|
101
110
|
await exportIssues({ issueRepository, logger }, {
|
|
102
111
|
count: maxCount,
|
|
103
112
|
domain,
|
|
113
|
+
downloadAttachments,
|
|
104
114
|
issueKeyFileName,
|
|
105
115
|
issueKeyFolder,
|
|
106
116
|
outputDir,
|
|
@@ -10,6 +10,7 @@ export default class Update extends Command {
|
|
|
10
10
|
documentId: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
documentsOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
12
|
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
downloadAttachments: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
15
|
issueIdOrKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
16
|
issueKeyFileName: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -62,6 +62,11 @@ export default class Update extends Command {
|
|
|
62
62
|
description: 'Backlog domain (e.g. example.backlog.jp)',
|
|
63
63
|
required: false,
|
|
64
64
|
}),
|
|
65
|
+
downloadAttachments: Flags.boolean({
|
|
66
|
+
char: 'd',
|
|
67
|
+
description: '課題の添付ファイルもダウンロードする(未指定時は設定ファイルの値を使用)',
|
|
68
|
+
required: false,
|
|
69
|
+
}),
|
|
65
70
|
force: Flags.boolean({
|
|
66
71
|
char: 'f',
|
|
67
72
|
description: '確認プロンプトをスキップする',
|
|
@@ -5,6 +5,7 @@ export default class Wiki extends Command {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
apiKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
domain: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
downloadAttachments: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
};
|
|
@@ -26,6 +26,11 @@ Wikiをダウンロードする
|
|
|
26
26
|
description: 'Backlog domain (e.g. example.backlog.jp)',
|
|
27
27
|
required: true,
|
|
28
28
|
}),
|
|
29
|
+
downloadAttachments: Flags.boolean({
|
|
30
|
+
char: 'd',
|
|
31
|
+
description: '添付ファイルもダウンロードする',
|
|
32
|
+
required: false,
|
|
33
|
+
}),
|
|
29
34
|
output: Flags.string({
|
|
30
35
|
char: 'o',
|
|
31
36
|
description: '出力ディレクトリパス',
|
|
@@ -39,7 +44,7 @@ Wikiをダウンロードする
|
|
|
39
44
|
async run() {
|
|
40
45
|
const { flags } = await this.parse(Wiki);
|
|
41
46
|
try {
|
|
42
|
-
const { domain, projectIdOrKey } = flags;
|
|
47
|
+
const { domain, downloadAttachments, projectIdOrKey } = flags;
|
|
43
48
|
const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
|
|
44
49
|
this.error(API_KEY_NOT_FOUND_MESSAGE);
|
|
45
50
|
const outputDir = flags.output || './wiki';
|
|
@@ -47,7 +52,7 @@ Wikiをダウンロードする
|
|
|
47
52
|
const { wikiRepository } = createBacklogRepositories({
|
|
48
53
|
apiKey,
|
|
49
54
|
domain,
|
|
50
|
-
|
|
55
|
+
onRateLimitExceeded: (waitSeconds) => this.log(`レート制限の上限に達しました。${waitSeconds}秒待機します...`),
|
|
51
56
|
});
|
|
52
57
|
// 出力ディレクトリの作成
|
|
53
58
|
await ensureDirectory(outputDir);
|
|
@@ -55,6 +60,7 @@ Wikiをダウンロードする
|
|
|
55
60
|
await updateSettings(outputDir, {
|
|
56
61
|
apiKey,
|
|
57
62
|
domain,
|
|
63
|
+
downloadAttachments,
|
|
58
64
|
folderType: FolderType.WIKI,
|
|
59
65
|
outputDir,
|
|
60
66
|
projectIdOrKey,
|
|
@@ -62,6 +68,7 @@ Wikiをダウンロードする
|
|
|
62
68
|
// Wikiの取得と保存
|
|
63
69
|
await exportWikis({ logger, wikiRepository }, {
|
|
64
70
|
domain,
|
|
71
|
+
downloadAttachments,
|
|
65
72
|
outputDir,
|
|
66
73
|
projectIdOrKey,
|
|
67
74
|
});
|
|
@@ -5,7 +5,7 @@ import { WikiRepository } from '../modules/wiki/domain/wiki-repository.js';
|
|
|
5
5
|
export interface BacklogConnection {
|
|
6
6
|
apiKey: string;
|
|
7
7
|
domain: string;
|
|
8
|
-
|
|
8
|
+
onRateLimitExceeded?: (waitSeconds: number) => void;
|
|
9
9
|
}
|
|
10
10
|
export interface BacklogRepositories {
|
|
11
11
|
documentRepository: DocumentRepository;
|
|
@@ -3,7 +3,7 @@ import { newBacklogIssueRepository } from '../modules/issue/repository/backlog-i
|
|
|
3
3
|
import { newBacklogProjectRepository } from '../modules/project/repository/backlog-project-repository.js';
|
|
4
4
|
import { newBacklogWikiRepository } from '../modules/wiki/repository/backlog-wiki-repository.js';
|
|
5
5
|
import { BacklogHttpClient } from '../shared/backlog/http-client.js';
|
|
6
|
-
// 1つのHTTP
|
|
6
|
+
// 1つのHTTPクライアントを全repositoryで共有する
|
|
7
7
|
export function createBacklogRepositories(connection) {
|
|
8
8
|
const client = new BacklogHttpClient(connection);
|
|
9
9
|
return {
|
|
@@ -7,7 +7,7 @@ import { updateSettings } from '../../settings/repository/settings-store.js';
|
|
|
7
7
|
import { exportWikis } from '../../wiki/use-case/export-wikis.js';
|
|
8
8
|
export async function exportAll(deps, options) {
|
|
9
9
|
const { documentRepository, issueRepository, logger, projectRepository, wikiRepository } = deps;
|
|
10
|
-
const { apiKey, domain, issueKeyFileName, issueKeyFolder, maxCount, outputRoot, projectIdOrKey, targets } = options;
|
|
10
|
+
const { apiKey, domain, downloadAttachments, issueKeyFileName, issueKeyFolder, maxCount, outputRoot, projectIdOrKey, targets } = options;
|
|
11
11
|
await ensureDirectory(outputRoot);
|
|
12
12
|
const projectId = await projectRepository.resolveProjectId(projectIdOrKey);
|
|
13
13
|
logger.log(`プロジェクトID: ${projectId} を使用します`);
|
|
@@ -17,6 +17,7 @@ export async function exportAll(deps, options) {
|
|
|
17
17
|
await updateSettings(issueOutput, {
|
|
18
18
|
apiKey,
|
|
19
19
|
domain,
|
|
20
|
+
downloadAttachments,
|
|
20
21
|
folderType: FolderType.ISSUE,
|
|
21
22
|
issueKeyFileName,
|
|
22
23
|
issueKeyFolder,
|
|
@@ -27,6 +28,7 @@ export async function exportAll(deps, options) {
|
|
|
27
28
|
await exportIssues({ issueRepository, logger }, {
|
|
28
29
|
count: maxCount,
|
|
29
30
|
domain,
|
|
31
|
+
downloadAttachments,
|
|
30
32
|
issueKeyFileName,
|
|
31
33
|
issueKeyFolder,
|
|
32
34
|
outputDir: issueOutput,
|
|
@@ -41,6 +43,7 @@ export async function exportAll(deps, options) {
|
|
|
41
43
|
await updateSettings(wikiOutput, {
|
|
42
44
|
apiKey,
|
|
43
45
|
domain,
|
|
46
|
+
downloadAttachments,
|
|
44
47
|
folderType: FolderType.WIKI,
|
|
45
48
|
outputDir: wikiOutput,
|
|
46
49
|
projectIdOrKey,
|
|
@@ -48,6 +51,7 @@ export async function exportAll(deps, options) {
|
|
|
48
51
|
logger.log('Wikiの取得を開始します...');
|
|
49
52
|
await exportWikis({ logger, wikiRepository }, {
|
|
50
53
|
domain,
|
|
54
|
+
downloadAttachments,
|
|
51
55
|
outputDir: wikiOutput,
|
|
52
56
|
projectIdOrKey,
|
|
53
57
|
});
|
|
@@ -60,6 +64,7 @@ export async function exportAll(deps, options) {
|
|
|
60
64
|
await updateSettings(documentOutput, {
|
|
61
65
|
apiKey,
|
|
62
66
|
domain,
|
|
67
|
+
downloadAttachments,
|
|
63
68
|
folderType: FolderType.DOCUMENT,
|
|
64
69
|
outputDir: documentOutput,
|
|
65
70
|
projectIdOrKey,
|
|
@@ -67,6 +72,7 @@ export async function exportAll(deps, options) {
|
|
|
67
72
|
logger.log('ドキュメントの取得を開始します...');
|
|
68
73
|
await exportDocuments({ documentRepository, logger }, {
|
|
69
74
|
domain,
|
|
75
|
+
downloadAttachments,
|
|
70
76
|
outputDir: documentOutput,
|
|
71
77
|
projectId,
|
|
72
78
|
projectIdOrKey,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { DocumentDetail } from './document.js';
|
|
2
|
-
export declare function buildDocumentMarkdown(documentDetail: DocumentDetail, backlogDocumentUrl: string): string;
|
|
2
|
+
export declare function buildDocumentMarkdown(documentDetail: DocumentDetail, backlogDocumentUrl: string, attachmentLinks?: Map<number, string>): string;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { escapeLinkText } from '../../../shared/attachment.js';
|
|
1
2
|
import { wrapBody } from '../../../shared/markdown/body-marker.js';
|
|
2
|
-
export function buildDocumentMarkdown(documentDetail, backlogDocumentUrl) {
|
|
3
|
-
//
|
|
3
|
+
export function buildDocumentMarkdown(documentDetail, backlogDocumentUrl, attachmentLinks) {
|
|
4
|
+
// 添付ファイルリストの作成(ダウンロード済みはローカルへの相対リンク付き)
|
|
4
5
|
let attachmentsSection = '';
|
|
5
6
|
if (documentDetail.attachments && documentDetail.attachments.length > 0) {
|
|
6
7
|
attachmentsSection = '\n\n## 添付ファイル\n';
|
|
7
8
|
for (const attachment of documentDetail.attachments) {
|
|
8
9
|
const attachmentDate = new Date(attachment.created).toLocaleString('ja-JP');
|
|
9
10
|
const fileSize = (attachment.size / 1024).toFixed(1);
|
|
10
|
-
|
|
11
|
+
const link = attachmentLinks?.get(attachment.id);
|
|
12
|
+
const label = link ? `[${escapeLinkText(attachment.name)}](${link})` : `**${attachment.name}**`;
|
|
13
|
+
attachmentsSection += `- ${label} (${fileSize} KB) - 作成者: ${attachment.createdUser.name}, 作成日時: ${attachmentDate}\n`;
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
16
|
// タグリストの作成
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { ExpectedPaths } from '../../prune/domain/expected-paths.js';
|
|
2
|
-
import { DocumentNode } from './document.js';
|
|
2
|
+
import { DocumentNode, DocumentSummary } from './document.js';
|
|
3
3
|
export declare const PARENT_DOCUMENT_INDEX_FILENAME = "00_index.md";
|
|
4
|
+
export declare const DOCUMENT_FALLBACK_PARENT_PATH = "";
|
|
4
5
|
export declare function documentFolderPath(currentPath: string, folderName: string): string;
|
|
5
6
|
export declare function documentFileName(title: string, asParentIndex: boolean): string;
|
|
6
7
|
export declare function documentUrl(domain: string, projectIdOrKey: string, documentId: string): string;
|
|
8
|
+
export declare function documentAttachmentRelativePath(currentPath: string, documentTitle: string, attachment: {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
}): string;
|
|
12
|
+
export declare function documentAttachmentMarkdownLink(documentTitle: string, attachment: {
|
|
13
|
+
id: number;
|
|
14
|
+
name: string;
|
|
15
|
+
}): string;
|
|
7
16
|
export interface DocumentTreePaths extends ExpectedPaths {
|
|
8
17
|
leafNodes: Array<{
|
|
9
18
|
currentPath: string;
|
|
@@ -13,3 +22,4 @@ export interface DocumentTreePaths extends ExpectedPaths {
|
|
|
13
22
|
}
|
|
14
23
|
export declare function collectDocumentTreePaths(rootNodes: DocumentNode[]): DocumentTreePaths;
|
|
15
24
|
export declare function resolveDocumentLeafPaths(paths: DocumentTreePaths, titlesById: Map<string, string>): void;
|
|
25
|
+
export declare function addFallbackDocumentPaths(paths: ExpectedPaths, documents: DocumentSummary[]): void;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { attachmentFileName, encodeLinkDestination } from '../../../shared/attachment.js';
|
|
2
3
|
import { backlogOrigin } from '../../../shared/backlog-url.js';
|
|
3
4
|
import { sanitizeFileName } from '../../../shared/file-name.js';
|
|
4
5
|
// 子を持つ親ドキュメント自身の本文の保存先。ダウンロード側とprune側でレイアウト定義を共有する
|
|
5
6
|
export const PARENT_DOCUMENT_INDEX_FILENAME = '00_index.md';
|
|
7
|
+
// ツリーに現れないドキュメントの保存先。ツリー上の位置が分からないため出力ルート直下に置く
|
|
8
|
+
export const DOCUMENT_FALLBACK_PARENT_PATH = '';
|
|
6
9
|
export function documentFolderPath(currentPath, folderName) {
|
|
7
10
|
return path.join(currentPath, sanitizeFileName(folderName));
|
|
8
11
|
}
|
|
@@ -12,6 +15,14 @@ export function documentFileName(title, asParentIndex) {
|
|
|
12
15
|
export function documentUrl(domain, projectIdOrKey, documentId) {
|
|
13
16
|
return `${backlogOrigin(domain)}/document/${projectIdOrKey}/${documentId}`;
|
|
14
17
|
}
|
|
18
|
+
// 添付の保存先はMarkdownと同じディレクトリの attachments/{ドキュメント名}/ 配下。
|
|
19
|
+
// タイトル改名時は再ダウンロードになるが、Markdown本体({タイトル}.md)と同じ挙動で閲覧性を優先する
|
|
20
|
+
export function documentAttachmentRelativePath(currentPath, documentTitle, attachment) {
|
|
21
|
+
return path.join(currentPath, 'attachments', sanitizeFileName(documentTitle), attachmentFileName(attachment));
|
|
22
|
+
}
|
|
23
|
+
export function documentAttachmentMarkdownLink(documentTitle, attachment) {
|
|
24
|
+
return encodeLinkDestination(['.', 'attachments', sanitizeFileName(documentTitle), attachmentFileName(attachment)].join('/'));
|
|
25
|
+
}
|
|
15
26
|
export function collectDocumentTreePaths(rootNodes) {
|
|
16
27
|
const expectedFiles = new Set();
|
|
17
28
|
const expectedDirs = new Set();
|
|
@@ -46,3 +57,9 @@ export function resolveDocumentLeafPaths(paths, titlesById) {
|
|
|
46
57
|
paths.expectedFiles.add(path.join(leaf.currentPath, `${sanitizeFileName(title)}.md`).normalize('NFC'));
|
|
47
58
|
}
|
|
48
59
|
}
|
|
60
|
+
// ツリーに現れないドキュメントは出力ルート直下に保存されるため、同じ配置で期待パスに加える
|
|
61
|
+
export function addFallbackDocumentPaths(paths, documents) {
|
|
62
|
+
for (const document of documents) {
|
|
63
|
+
paths.expectedFiles.add(path.join(DOCUMENT_FALLBACK_PARENT_PATH, documentFileName(document.title, false)).normalize('NFC'));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DocumentDetail, DocumentTree } from './document.js';
|
|
2
2
|
export interface DocumentRepository {
|
|
3
|
+
downloadAttachment(documentId: string, attachmentId: number): Promise<ArrayBuffer>;
|
|
3
4
|
fetchAllTitles(projectId: number): Promise<Map<string, string>>;
|
|
4
5
|
fetchDetail(documentId: string): Promise<DocumentDetail>;
|
|
5
6
|
fetchTree(projectId: number): Promise<DocumentTree>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export type DocumentSaveAction = 'delete-stale-parent-index' | 'save' | 'skip-empty-parent' | 'skip-parent-index-collision' | 'skip-unchanged';
|
|
1
|
+
export type DocumentSaveAction = 'delete-stale-parent-index' | 'save' | 'skip-empty-parent' | 'skip-fallback-collision' | 'skip-parent-index-collision' | 'skip-unchanged';
|
|
2
2
|
export declare function planDocumentSave(input: {
|
|
3
|
+
alreadyWrittenThisRun: boolean;
|
|
3
4
|
asParentIndex: boolean;
|
|
4
5
|
body: null | string | undefined;
|
|
6
|
+
fileExists: boolean;
|
|
5
7
|
lastUpdated?: string;
|
|
6
|
-
|
|
7
|
-
parentIndexExists: boolean;
|
|
8
|
+
missingFromTree: boolean;
|
|
8
9
|
updated: string;
|
|
9
10
|
}): DocumentSaveAction;
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
// 保存/スキップ/親index削除の判断。本文が空の親はファイルを作らず、空に変更された場合は古い親indexを削除する
|
|
2
2
|
export function planDocumentSave(input) {
|
|
3
|
-
if (input.asParentIndex && input.
|
|
3
|
+
if (input.asParentIndex && input.alreadyWrittenThisRun) {
|
|
4
4
|
return 'skip-parent-index-collision';
|
|
5
5
|
}
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
if (input.
|
|
6
|
+
// ツリーに現れないドキュメントは出力ルート直下に固定で置かれるため、同名の保存先が既に使われていたら譲る。
|
|
7
|
+
// ツリー上の位置が確かなドキュメントを、位置の分からないドキュメントで黙って上書きしないための保険
|
|
8
|
+
if (input.missingFromTree && input.alreadyWrittenThisRun) {
|
|
9
|
+
return 'skip-fallback-collision';
|
|
10
|
+
}
|
|
11
|
+
// 前回の更新日時チェック。親index・ツリーに現れないドキュメントは、そもそも過去のエクスポートで
|
|
12
|
+
// 取得できていない可能性があるため、ファイルが無い場合は未更新でもバックフィルとして保存する
|
|
13
|
+
const backfill = (input.asParentIndex || input.missingFromTree) && !input.fileExists;
|
|
14
|
+
if (input.lastUpdated && !backfill && new Date(input.updated) <= new Date(input.lastUpdated)) {
|
|
9
15
|
return 'skip-unchanged';
|
|
10
16
|
}
|
|
11
17
|
if (input.asParentIndex && !input.body?.trim()) {
|
|
12
|
-
return input.
|
|
18
|
+
return input.fileExists ? 'delete-stale-parent-index' : 'skip-empty-parent';
|
|
13
19
|
}
|
|
14
20
|
return 'save';
|
|
15
21
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Backlogのドキュメントツリーは、作成後に一度も再保存されていないドキュメントを返さないことがある。
|
|
2
|
+
// ツリーだけを辿るとそれらが構造的に取得できないため、全件が載る一覧APIとの差分で拾う。
|
|
3
|
+
// ゴミ箱のドキュメントを復活させないよう、trashTreeに載っているものは差分から除く。
|
|
4
|
+
export function findDocumentsMissingFromTree(tree, titlesById) {
|
|
5
|
+
const knownIds = new Set();
|
|
6
|
+
const collect = (nodes) => {
|
|
7
|
+
for (const node of nodes ?? []) {
|
|
8
|
+
knownIds.add(node.id);
|
|
9
|
+
collect(node.children);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
collect(tree.activeTree.children);
|
|
13
|
+
collect(tree.trashTree?.children);
|
|
14
|
+
const missing = [];
|
|
15
|
+
for (const [id, title] of titlesById) {
|
|
16
|
+
if (!knownIds.has(id)) {
|
|
17
|
+
missing.push({ id, title });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return missing;
|
|
21
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export function newBacklogDocumentRepository(client) {
|
|
2
2
|
return {
|
|
3
|
+
// 公式ドキュメント未記載だが公式SDK(nulab/backlog-js)に存在するエンドポイント
|
|
4
|
+
async downloadAttachment(documentId, attachmentId) {
|
|
5
|
+
return client.getBinary(`/documents/${documentId}/attachments/${attachmentId}`);
|
|
6
|
+
},
|
|
3
7
|
async fetchAllTitles(projectId) {
|
|
4
8
|
const titlesById = new Map();
|
|
5
9
|
const pageSize = 100;
|