Skip to content

Commit e367ce2

Browse files
authored
feat: #42 import confluence html zip
1 parent 9917bcf commit e367ce2

File tree

5 files changed

+116
-16
lines changed

5 files changed

+116
-16
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,27 @@ chmod +x coding.phar
3131
sudo mv coding.phar /usr/local/bin/coding
3232
coding list
3333
```
34+
35+
## Confluence 导入 CODING Wiki
36+
37+
1. 浏览器访问 Confluence 空间,导出 HTML,获得一个 zip 压缩包。
38+
39+
![image](https://user-images.githubusercontent.com/4971414/127876158-8ab62714-e43f-4e20-8865-f8817f9264e1.png)
40+
41+
2. 浏览器访问 CODING,创建个人令牌
42+
43+
![image](https://user-images.githubusercontent.com/4971414/127877027-68a3f58e-c253-4ba9-b4f9-68b6673582a3.png)
44+
45+
3. 打开命令行,进入 zip 文件所在的目录,执行命令导入:
46+
47+
```shell
48+
cd ~/Downloads/
49+
docker run -it -v $(pwd):/root --env CODING_IMPORT_PROVIDER=Confluence \
50+
--env CODING_IMPORT_DATA_TYPE=HTML \
51+
--env CODING_IMPORT_DATA_PATH=./Confluence-space-export-231543-81.html.zip \
52+
--env CODING_TOKEN=foo \
53+
ecoding/coding-cli wiki:import
54+
```
55+
56+
![image](https://user-images.githubusercontent.com/4971414/127878108-f778bfd6-fe7f-49f3-9590-9efd68404df5.png)
57+

app/Commands/WikiImportCommand.php

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace App\Commands;
44

5-
use App\Coding;
65
use App\Coding\Disk;
76
use App\Coding\Wiki;
87
use Confluence\Content;
@@ -11,6 +10,7 @@
1110
use Illuminate\Support\Str;
1211
use LaravelFans\Confluence\Facades\Confluence;
1312
use LaravelZero\Framework\Commands\Command;
13+
use ZipArchive;
1414

1515
class WikiImportCommand extends Command
1616
{
@@ -26,7 +26,7 @@ class WikiImportCommand extends Command
2626
protected $signature = 'wiki:import
2727
{--coding_import_provider= : 数据来源,如 Confluence、MediaWiki}
2828
{--coding_import_data_type= : 数据类型,如 HTML、API}
29-
{--coding_import_data_path= : 空间导出的 HTML 目录,如 ./confluence/space1/}
29+
{--coding_import_data_path= : 空间导出的 HTML zip 文件路径,如 ./Confluence-space-export-231543-81.html.zip}
3030
{--confluence_base_uri= : Confluence API URL,如 http://localhost:8090/confluence/rest/api/}
3131
{--confluence_username=}
3232
{--confluence_password=}
@@ -135,15 +135,8 @@ private function handleConfluenceApi(): int
135135

136136
private function handleConfluenceHtml(): int
137137
{
138-
$dataPath = $this->option('coding_import_data_path');
139-
if (is_null($dataPath)) {
140-
$dataPath = config('coding.import.data_path') ?? trim($this->ask(
141-
'空间导出的 HTML 目录',
142-
'./confluence/space1/'
143-
));
144-
}
145-
$dataPath = str_ends_with($dataPath, '/index.html') ? substr($dataPath, 0, -10) : Str::finish($dataPath, '/');
146-
$filePath = $dataPath . 'index.html';
138+
$htmlDir = $this->unzipConfluenceHtml();
139+
$filePath = $htmlDir . 'index.html';
147140
if (!file_exists($filePath)) {
148141
$this->error("文件不存在:$filePath");
149142
return 1;
@@ -171,7 +164,7 @@ private function handleConfluenceHtml(): int
171164
}
172165
$this->info('发现 ' . count($pages['tree']) . ' 个一级页面');
173166
$this->info("开始导入 CODING:");
174-
$this->uploadConfluencePages($dataPath, $pages['tree'], $pages['titles']);
167+
$this->uploadConfluencePages($htmlDir, $pages['tree'], $pages['titles']);
175168
} catch (\ErrorException $e) {
176169
$this->error($e->getMessage());
177170
return 1;
@@ -239,4 +232,31 @@ private function uploadConfluencePages(string $dataPath, array $tree, array $tit
239232
}
240233
}
241234
}
235+
236+
private function unzipConfluenceHtml(): string
237+
{
238+
$dataPath = $this->option('coding_import_data_path');
239+
if (is_null($dataPath)) {
240+
$dataPath = config('coding.import.data_path') ?? trim($this->ask(
241+
'空间导出的 HTML zip 文件路径',
242+
'./confluence/space1.zip'
243+
));
244+
}
245+
246+
if (str_ends_with($dataPath, '.zip')) {
247+
$zip = new ZipArchive();
248+
$zip->open($dataPath);
249+
$tmpDir = sys_get_temp_dir() . '/confluence-' . Str::uuid();
250+
mkdir($tmpDir);
251+
for ($i = 0; $i < $zip->numFiles; $i++) {
252+
// HACK crash when zip include root path /
253+
if ($zip->getNameIndex($i) != '/' && $zip->getNameIndex($i) != '__MACOSX/_') {
254+
$zip->extractTo($tmpDir, [$zip->getNameIndex($i)]);
255+
}
256+
}
257+
$zip->close();
258+
return $tmpDir . '/' . scandir($tmpDir, 1)[0] . '/';
259+
}
260+
return str_ends_with($dataPath, '/index.html') ? substr($dataPath, 0, -10) : Str::finish($dataPath, '/');
261+
}
242262
}

composer.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/Feature/WikiImportCommandTest.php

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,14 @@ public function testHandleConfluenceHtmlFileNotExist()
133133
$this->artisan('wiki:import')
134134
->expectsQuestion('数据来源?', 'Confluence')
135135
->expectsQuestion('数据类型?', 'HTML')
136-
->expectsQuestion('空间导出的 HTML 目录', '~/Downloads/')
136+
->expectsQuestion('空间导出的 HTML zip 文件路径', '~/Downloads/')
137137
->expectsOutput('文件不存在:~/Downloads/index.html')
138138
->assertExitCode(1);
139139

140140
$this->artisan('wiki:import')
141141
->expectsQuestion('数据来源?', 'Confluence')
142142
->expectsQuestion('数据类型?', 'HTML')
143-
->expectsQuestion('空间导出的 HTML 目录', '~/Downloads/index.html')
143+
->expectsQuestion('空间导出的 HTML zip 文件路径', '~/Downloads/index.html')
144144
->expectsOutput('文件不存在:~/Downloads/index.html')
145145
->assertExitCode(1);
146146
}
@@ -177,7 +177,7 @@ public function testHandleConfluenceHtmlSuccess()
177177
$this->artisan('wiki:import')
178178
->expectsQuestion('数据来源?', 'Confluence')
179179
->expectsQuestion('数据类型?', 'HTML')
180-
->expectsQuestion('空间导出的 HTML 目录', $this->dataDir . 'confluence/space1/')
180+
->expectsQuestion('空间导出的 HTML zip 文件路径', $this->dataDir . 'confluence/space1/')
181181
->expectsOutput('空间名称:空间 1')
182182
->expectsOutput('空间标识:space1')
183183
->expectsOutput('发现 2 个一级页面')
@@ -210,4 +210,59 @@ public function testAskNothing()
210210
->expectsOutput('文件不存在:/dev/null/index.html')
211211
->assertExitCode(1);
212212
}
213+
214+
public function testHandleConfluenceHtmlZipSuccess()
215+
{
216+
$codingToken = $this->faker->md5;
217+
config(['coding.token' => $codingToken]);
218+
$codingTeamDomain = $this->faker->domainWord;
219+
config(['coding.team_domain' => $codingTeamDomain]);
220+
$codingProjectUri = $this->faker->slug;
221+
config(['coding.project_uri' => $codingProjectUri]);
222+
223+
// 注意:不能使用 partialMock
224+
// https://laracasts.com/discuss/channels/testing/this-partialmock-doesnt-call-the-constructor
225+
$mock = \Mockery::mock(Wiki::class, [])->makePartial();
226+
$this->instance(Wiki::class, $mock);
227+
228+
$mock->shouldReceive('createWikiByUploadZip')->times(5)->andReturn(json_decode(
229+
file_get_contents($this->dataDir . 'coding/' . 'CreateWikiByZipResponse.json'),
230+
true
231+
)['Response']);
232+
$mock->shouldReceive('getImportJobStatus')->times(5)->andReturn(json_decode(
233+
file_get_contents($this->dataDir . 'coding/' . 'DescribeImportJobStatusResponse.json'),
234+
true
235+
)['Response']['Data']);
236+
$mock->shouldReceive('updateWikiTitle')->times(5)->andReturn(true);
237+
238+
239+
$mockDisk = \Mockery::mock(Disk::class, [])->makePartial();
240+
$this->instance(Disk::class, $mockDisk);
241+
$mockDisk->shouldReceive('uploadAttachments')->times(5)->andReturn([]);
242+
243+
$this->artisan('wiki:import')
244+
->expectsQuestion('数据来源?', 'Confluence')
245+
->expectsQuestion('数据类型?', 'HTML')
246+
->expectsQuestion(
247+
'空间导出的 HTML zip 文件路径',
248+
$this->dataDir . 'confluence/Confluence-space-export-231543-81.html.zip'
249+
)
250+
->expectsOutput('空间名称:空间 1')
251+
->expectsOutput('空间标识:space1')
252+
->expectsOutput('发现 1 个一级页面')
253+
->expectsOutput("开始导入 CODING:")
254+
->expectsOutput('标题:空间 1 Home')
255+
->expectsOutput('上传成功,正在处理,任务 ID:a12353fa-f45b-4af2-83db-666bf9f66615')
256+
->expectsOutput('发现 2 个子页面')
257+
->expectsOutput('标题:hello world')
258+
->expectsOutput('上传成功,正在处理,任务 ID:a12353fa-f45b-4af2-83db-666bf9f66615')
259+
->expectsOutput('发现 2 个子页面')
260+
->expectsOutput('标题:hello')
261+
->expectsOutput('上传成功,正在处理,任务 ID:a12353fa-f45b-4af2-83db-666bf9f66615')
262+
->expectsOutput('标题:world')
263+
->expectsOutput('上传成功,正在处理,任务 ID:a12353fa-f45b-4af2-83db-666bf9f66615')
264+
->expectsOutput('标题:你好世界')
265+
->expectsOutput('上传成功,正在处理,任务 ID:a12353fa-f45b-4af2-83db-666bf9f66615')
266+
->assertExitCode(0);
267+
}
213268
}
Binary file not shown.

0 commit comments

Comments
 (0)