@d-zero/backlog-projects 0.5.6 → 0.6.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 +40 -0
- package/dist/cli.js +23 -0
- package/dist/delete-attachments.d.ts +18 -0
- package/dist/delete-attachments.js +138 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
- `-v, --version`: バージョンを表示
|
|
8
8
|
- `-a, --assign`: プロジェクトアサインモード
|
|
9
|
+
- `-d, --delete`: 添付ファイル削除モード
|
|
10
|
+
- `-V, --verbose`: 詳細出力
|
|
9
11
|
|
|
10
12
|
### プロジェクトアサイン
|
|
11
13
|
|
|
@@ -28,6 +30,29 @@ npx @d-zero/backlog-projects --assign
|
|
|
28
30
|
|
|
29
31
|
逐次、課題が登録されます。
|
|
30
32
|
|
|
33
|
+
### 添付ファイル削除
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npx @d-zero/backlog-projects --delete
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
指定した基準日以前に最終更新された課題の添付ファイルをダウンロードしてから削除します。
|
|
40
|
+
|
|
41
|
+
プロンプトに従って入力してください。
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
? 基準日(例: 2024-01-01) ›
|
|
45
|
+
? 保存先ディレクトリ ›
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
添付ファイルは保存先ディレクトリに `プロジェクトキー/課題キー/` の階層で保存されます。各ファイルには削除結果のメタデータ(`.json`)も併せて保存されます。
|
|
49
|
+
|
|
50
|
+
`-V` オプションで詳細出力を有効にできます。
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npx @d-zero/backlog-projects --delete -V
|
|
54
|
+
```
|
|
55
|
+
|
|
31
56
|
```sh
|
|
32
57
|
︙
|
|
33
58
|
API_TEST-1190 メールフォーム 情報設計・項目定義 @DZ平尾
|
|
@@ -67,6 +92,21 @@ type Params = {
|
|
|
67
92
|
};
|
|
68
93
|
```
|
|
69
94
|
|
|
95
|
+
### `deleteAttachments`
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
function deleteAttachments(
|
|
99
|
+
backlog: Backlog,
|
|
100
|
+
params: DeleteAttachmentsParams,
|
|
101
|
+
): Promise<void>;
|
|
102
|
+
|
|
103
|
+
type DeleteAttachmentsParams = {
|
|
104
|
+
updatedUntil: string;
|
|
105
|
+
outDir: string;
|
|
106
|
+
verbose?: boolean;
|
|
107
|
+
};
|
|
108
|
+
```
|
|
109
|
+
|
|
70
110
|
### `createBacklogClient`
|
|
71
111
|
|
|
72
112
|
```ts
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import minimist from 'minimist';
|
|
|
6
6
|
import { assign } from './assign.js';
|
|
7
7
|
import { createBacklogClient } from './create-backlog-client.js';
|
|
8
8
|
import { roles } from './define.js';
|
|
9
|
+
import { deleteAttachments } from './delete-attachments.js';
|
|
9
10
|
import { getBacklogProjectIdFromUrl } from './get-backlog-project-id-from-url.js';
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
const pkg = require('../package.json');
|
|
@@ -13,7 +14,9 @@ dotenv.config();
|
|
|
13
14
|
const cli = minimist(process.argv.slice(2), {
|
|
14
15
|
alias: {
|
|
15
16
|
a: 'assign',
|
|
17
|
+
d: 'delete',
|
|
16
18
|
v: 'version',
|
|
19
|
+
V: 'verbose',
|
|
17
20
|
},
|
|
18
21
|
});
|
|
19
22
|
// Handle -v / --version option
|
|
@@ -72,3 +75,23 @@ if (cli.assign) {
|
|
|
72
75
|
},
|
|
73
76
|
});
|
|
74
77
|
}
|
|
78
|
+
else if (cli.delete) {
|
|
79
|
+
process.stdout.write('この日以前(この日を含む)に最終更新された課題の添付ファイルが削除対象になります。\n');
|
|
80
|
+
const { updatedUntil } = await Enquirer.prompt({
|
|
81
|
+
name: 'updatedUntil',
|
|
82
|
+
message: '基準日(例: 2024-01-01)',
|
|
83
|
+
type: 'input',
|
|
84
|
+
required: true,
|
|
85
|
+
});
|
|
86
|
+
const { outDir } = await Enquirer.prompt({
|
|
87
|
+
name: 'outDir',
|
|
88
|
+
message: '保存先ディレクトリ',
|
|
89
|
+
type: 'input',
|
|
90
|
+
required: true,
|
|
91
|
+
});
|
|
92
|
+
await deleteAttachments(backlog, {
|
|
93
|
+
updatedUntil,
|
|
94
|
+
outDir,
|
|
95
|
+
verbose: !!cli.verbose,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Backlog } from 'backlog-js';
|
|
2
|
+
interface DeleteAttachmentsParams {
|
|
3
|
+
/**
|
|
4
|
+
* この日付以前(この日を含む)に最終更新された課題が対象。
|
|
5
|
+
* Backlog API の updatedUntil パラメータに渡される。
|
|
6
|
+
* 例: '2024-01-01' → 2024年1月1日以前に更新された課題
|
|
7
|
+
*/
|
|
8
|
+
updatedUntil: string;
|
|
9
|
+
outDir: string;
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param backlog
|
|
15
|
+
* @param params
|
|
16
|
+
*/
|
|
17
|
+
export declare function deleteAttachments(backlog: Backlog, params: DeleteAttachmentsParams): Promise<void>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { deal } from '@d-zero/dealer';
|
|
6
|
+
import { delay } from '@d-zero/shared/delay';
|
|
7
|
+
import { kbSize } from '@d-zero/shared/filesize';
|
|
8
|
+
import { retryCall } from '@d-zero/shared/retry';
|
|
9
|
+
import c from 'ansi-colors';
|
|
10
|
+
import dayjs from 'dayjs';
|
|
11
|
+
const BATCH_SIZE = 20;
|
|
12
|
+
const ISSUES_PER_REQUEST = 100;
|
|
13
|
+
const API_DELAY_MS = 1000;
|
|
14
|
+
const RETRY_OPTIONS = {
|
|
15
|
+
retries: 5,
|
|
16
|
+
interval: { random: { min: 5000, max: 15_000 } },
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
*
|
|
20
|
+
* @param backlog
|
|
21
|
+
* @param params
|
|
22
|
+
*/
|
|
23
|
+
export async function deleteAttachments(backlog, params) {
|
|
24
|
+
const { updatedUntil, outDir, verbose } = params;
|
|
25
|
+
const projectKeyCache = new Map();
|
|
26
|
+
let processedCount = 0;
|
|
27
|
+
let totalAttachments = 0;
|
|
28
|
+
let totalSize = 0;
|
|
29
|
+
let issueCount = 0;
|
|
30
|
+
let collectionDone = false;
|
|
31
|
+
const resolveProjectKey = async (projectId) => {
|
|
32
|
+
const cached = projectKeyCache.get(projectId);
|
|
33
|
+
if (cached) {
|
|
34
|
+
return cached;
|
|
35
|
+
}
|
|
36
|
+
await delay(API_DELAY_MS);
|
|
37
|
+
const project = await retryCall(() => backlog.getProject(projectId), RETRY_OPTIONS);
|
|
38
|
+
projectKeyCache.set(projectId, project.projectKey);
|
|
39
|
+
return project.projectKey;
|
|
40
|
+
};
|
|
41
|
+
await deal([{ kind: 'collector' }], (task, update, _index, setLineHeader, push) => {
|
|
42
|
+
if (task.kind === 'collector') {
|
|
43
|
+
return async () => {
|
|
44
|
+
update('%braille% プロジェクト一覧を取得中%dots%');
|
|
45
|
+
const [archivedProjects, activeProjects] = await Promise.all([
|
|
46
|
+
retryCall(() => backlog.getProjects({ archived: true }), RETRY_OPTIONS),
|
|
47
|
+
retryCall(() => backlog.getProjects({ archived: false }), RETRY_OPTIONS),
|
|
48
|
+
]);
|
|
49
|
+
const allProjects = [...archivedProjects, ...activeProjects];
|
|
50
|
+
const totalBatches = Math.ceil(allProjects.length / BATCH_SIZE);
|
|
51
|
+
for (const p of allProjects) {
|
|
52
|
+
projectKeyCache.set(p.id, p.projectKey);
|
|
53
|
+
}
|
|
54
|
+
for (let i = 0; i < allProjects.length; i += BATCH_SIZE) {
|
|
55
|
+
const batch = allProjects.slice(i, i + BATCH_SIZE);
|
|
56
|
+
const projectIds = batch.map((p) => p.id);
|
|
57
|
+
const batchIndex = Math.floor(i / BATCH_SIZE) + 1;
|
|
58
|
+
update(`%braille% ${c.gray(batch.map((p) => p.projectKey).join(', '))}: バッチ ${batchIndex}/${totalBatches} 課題を検索中%dots%`);
|
|
59
|
+
let hasMore = true;
|
|
60
|
+
let offset = 0;
|
|
61
|
+
while (hasMore) {
|
|
62
|
+
await delay(API_DELAY_MS);
|
|
63
|
+
const issues = await retryCall(() => backlog.getIssues({
|
|
64
|
+
projectId: projectIds,
|
|
65
|
+
attachment: true,
|
|
66
|
+
count: ISSUES_PER_REQUEST,
|
|
67
|
+
offset,
|
|
68
|
+
updatedUntil,
|
|
69
|
+
}), RETRY_OPTIONS);
|
|
70
|
+
if (issues.length === 0) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
for (const issue of issues) {
|
|
74
|
+
if (issue.attachments.length === 0) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
issueCount++;
|
|
78
|
+
const projectKey = await resolveProjectKey(issue.projectId);
|
|
79
|
+
for (const attachment of issue.attachments) {
|
|
80
|
+
totalAttachments++;
|
|
81
|
+
await push({
|
|
82
|
+
kind: 'attachment',
|
|
83
|
+
issueKey: issue.issueKey,
|
|
84
|
+
projectKey,
|
|
85
|
+
attachment,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
offset += issues.length;
|
|
90
|
+
if (issues.length < ISSUES_PER_REQUEST) {
|
|
91
|
+
hasMore = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
collectionDone = true;
|
|
96
|
+
update(`${c.green('✓')} 収集完了 | 課題: ${issueCount} | ファイル: ${totalAttachments}`);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return async () => {
|
|
100
|
+
setLineHeader(`${c.bgWhite(c.black(` ${task.issueKey} `))} `);
|
|
101
|
+
update(`${task.attachment.name}: ダウンロード中%dots%`);
|
|
102
|
+
const issueDir = path.join(outDir, task.projectKey, task.issueKey);
|
|
103
|
+
await mkdir(issueDir, { recursive: true });
|
|
104
|
+
const filePath = path.join(issueDir, `${dayjs(task.attachment.created).format('YYYYMMDD')}_${task.attachment.id}_${task.attachment.name}`);
|
|
105
|
+
const fileData = await retryCall(() => backlog.getIssueAttachment(task.issueKey, task.attachment.id), RETRY_OPTIONS);
|
|
106
|
+
await pipeline(fileData.body, createWriteStream(filePath));
|
|
107
|
+
totalSize += task.attachment.size;
|
|
108
|
+
update(`${task.attachment.name} (${kbSize(task.attachment.size)}): 削除中%dots%`);
|
|
109
|
+
await delay(API_DELAY_MS);
|
|
110
|
+
const deleteResult = await retryCall(() => backlog.deleteIssueAttachment(task.issueKey, String(task.attachment.id)), RETRY_OPTIONS);
|
|
111
|
+
const metaPath = `${filePath}.json`;
|
|
112
|
+
await writeFile(metaPath, JSON.stringify(deleteResult, null, 2));
|
|
113
|
+
processedCount++;
|
|
114
|
+
update(`${c.green('✓')} ${task.attachment.name} (${kbSize(task.attachment.size)})`);
|
|
115
|
+
};
|
|
116
|
+
}, {
|
|
117
|
+
limit: 6,
|
|
118
|
+
verbose,
|
|
119
|
+
interval: API_DELAY_MS,
|
|
120
|
+
header: () => {
|
|
121
|
+
if (collectionDone &&
|
|
122
|
+
processedCount === totalAttachments &&
|
|
123
|
+
totalAttachments > 0) {
|
|
124
|
+
return c.bold.green(`✓ 完了 | 課題: ${issueCount} | ファイル: ${processedCount}/${totalAttachments} | ${kbSize(totalSize)}`);
|
|
125
|
+
}
|
|
126
|
+
const pct = totalAttachments > 0
|
|
127
|
+
? Math.round((processedCount / totalAttachments) * 100)
|
|
128
|
+
: 0;
|
|
129
|
+
if (collectionDone) {
|
|
130
|
+
return `${c.bold.cyan('処理中')} %earth% ${processedCount}/${totalAttachments} (${pct}%) | 課題: ${issueCount} | ${kbSize(totalSize)}`;
|
|
131
|
+
}
|
|
132
|
+
return `${c.bold.cyan('収集&処理中')} %earth% ${processedCount}/${totalAttachments} (${pct}%) | 課題: ${issueCount} | ${kbSize(totalSize)}`;
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
if (totalAttachments === 0) {
|
|
136
|
+
process.stderr.write(c.bold.yellow('対象の添付ファイルはありません') + '\n');
|
|
137
|
+
}
|
|
138
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d-zero/backlog-projects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "A manipulating Backlog projects library",
|
|
5
5
|
"author": "D-ZERO",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,13 +26,15 @@
|
|
|
26
26
|
"clean": "tsc --build --clean"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@d-zero/
|
|
29
|
+
"@d-zero/dealer": "1.7.0",
|
|
30
|
+
"@d-zero/notion": "2.0.14",
|
|
30
31
|
"@d-zero/shared": "0.20.1",
|
|
32
|
+
"ansi-colors": "4.1.3",
|
|
31
33
|
"backlog-js": "0.16.0",
|
|
32
34
|
"dayjs": "1.11.19",
|
|
33
35
|
"dotenv": "17.3.1",
|
|
34
36
|
"enquirer": "2.4.1",
|
|
35
37
|
"minimist": "1.2.8"
|
|
36
38
|
},
|
|
37
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "866364294ddacc3095f65a9f8b488d40c4ccb904"
|
|
38
40
|
}
|