Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,27 @@ chmod +x coding.phar
sudo mv coding.phar /usr/local/bin/coding
coding list
```

## Confluence 导入 CODING Wiki

1. 浏览器访问 Confluence 空间,导出 HTML,获得一个 zip 压缩包。

![image](https://user-images.githubusercontent.com/4971414/127876158-8ab62714-e43f-4e20-8865-f8817f9264e1.png)

2. 浏览器访问 CODING,创建个人令牌

![image](https://user-images.githubusercontent.com/4971414/127877027-68a3f58e-c253-4ba9-b4f9-68b6673582a3.png)

3. 打开命令行,进入 zip 文件所在的目录,执行命令导入:

```shell
cd ~/Downloads/
docker run -it -v $(pwd):/root --env CODING_IMPORT_PROVIDER=Confluence \
--env CODING_IMPORT_DATA_TYPE=HTML \
--env CODING_IMPORT_DATA_PATH=./Confluence-space-export-231543-81.html.zip \
--env CODING_TOKEN=foo \
ecoding/coding-cli wiki:import
```

![image](https://user-images.githubusercontent.com/4971414/127878108-f778bfd6-fe7f-49f3-9590-9efd68404df5.png)

44 changes: 32 additions & 12 deletions app/Commands/WikiImportCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace App\Commands;

use App\Coding;
use App\Coding\Disk;
use App\Coding\Wiki;
use Confluence\Content;
Expand All @@ -11,6 +10,7 @@
use Illuminate\Support\Str;
use LaravelFans\Confluence\Facades\Confluence;
use LaravelZero\Framework\Commands\Command;
use ZipArchive;

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

private function handleConfluenceHtml(): int
{
$dataPath = $this->option('coding_import_data_path');
if (is_null($dataPath)) {
$dataPath = config('coding.import.data_path') ?? trim($this->ask(
'空间导出的 HTML 目录',
'./confluence/space1/'
));
}
$dataPath = str_ends_with($dataPath, '/index.html') ? substr($dataPath, 0, -10) : Str::finish($dataPath, '/');
$filePath = $dataPath . 'index.html';
$htmlDir = $this->unzipConfluenceHtml();
$filePath = $htmlDir . 'index.html';
if (!file_exists($filePath)) {
$this->error("文件不存在:$filePath");
return 1;
Expand Down Expand Up @@ -171,7 +164,7 @@ private function handleConfluenceHtml(): int
}
$this->info('发现 ' . count($pages['tree']) . ' 个一级页面');
$this->info("开始导入 CODING:");
$this->uploadConfluencePages($dataPath, $pages['tree'], $pages['titles']);
$this->uploadConfluencePages($htmlDir, $pages['tree'], $pages['titles']);
} catch (\ErrorException $e) {
$this->error($e->getMessage());
return 1;
Expand Down Expand Up @@ -239,4 +232,31 @@ private function uploadConfluencePages(string $dataPath, array $tree, array $tit
}
}
}

private function unzipConfluenceHtml(): string
{
$dataPath = $this->option('coding_import_data_path');
if (is_null($dataPath)) {
$dataPath = config('coding.import.data_path') ?? trim($this->ask(
'空间导出的 HTML zip 文件路径',
'./confluence/space1.zip'
));
}

if (str_ends_with($dataPath, '.zip')) {
$zip = new ZipArchive();
$zip->open($dataPath);
$tmpDir = sys_get_temp_dir() . '/confluence-' . Str::uuid();
mkdir($tmpDir);
for ($i = 0; $i < $zip->numFiles; $i++) {
// HACK crash when zip include root path /
if ($zip->getNameIndex($i) != '/' && $zip->getNameIndex($i) != '__MACOSX/_') {
$zip->extractTo($tmpDir, [$zip->getNameIndex($i)]);
}
}
$zip->close();
return $tmpDir . '/' . scandir($tmpDir, 1)[0] . '/';
}
return str_ends_with($dataPath, '/index.html') ? substr($dataPath, 0, -10) : Str::finish($dataPath, '/');
}
}
3 changes: 2 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 58 additions & 3 deletions tests/Feature/WikiImportCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,14 @@ public function testHandleConfluenceHtmlFileNotExist()
$this->artisan('wiki:import')
->expectsQuestion('数据来源?', 'Confluence')
->expectsQuestion('数据类型?', 'HTML')
->expectsQuestion('空间导出的 HTML 目录', '~/Downloads/')
->expectsQuestion('空间导出的 HTML zip 文件路径', '~/Downloads/')
->expectsOutput('文件不存在:~/Downloads/index.html')
->assertExitCode(1);

$this->artisan('wiki:import')
->expectsQuestion('数据来源?', 'Confluence')
->expectsQuestion('数据类型?', 'HTML')
->expectsQuestion('空间导出的 HTML 目录', '~/Downloads/index.html')
->expectsQuestion('空间导出的 HTML zip 文件路径', '~/Downloads/index.html')
->expectsOutput('文件不存在:~/Downloads/index.html')
->assertExitCode(1);
}
Expand Down Expand Up @@ -177,7 +177,7 @@ public function testHandleConfluenceHtmlSuccess()
$this->artisan('wiki:import')
->expectsQuestion('数据来源?', 'Confluence')
->expectsQuestion('数据类型?', 'HTML')
->expectsQuestion('空间导出的 HTML 目录', $this->dataDir . 'confluence/space1/')
->expectsQuestion('空间导出的 HTML zip 文件路径', $this->dataDir . 'confluence/space1/')
->expectsOutput('空间名称:空间 1')
->expectsOutput('空间标识:space1')
->expectsOutput('发现 2 个一级页面')
Expand Down Expand Up @@ -210,4 +210,59 @@ public function testAskNothing()
->expectsOutput('文件不存在:/dev/null/index.html')
->assertExitCode(1);
}

public function testHandleConfluenceHtmlZipSuccess()
{
$codingToken = $this->faker->md5;
config(['coding.token' => $codingToken]);
$codingTeamDomain = $this->faker->domainWord;
config(['coding.team_domain' => $codingTeamDomain]);
$codingProjectUri = $this->faker->slug;
config(['coding.project_uri' => $codingProjectUri]);

// 注意:不能使用 partialMock
// https://laracasts.com/discuss/channels/testing/this-partialmock-doesnt-call-the-constructor
$mock = \Mockery::mock(Wiki::class, [])->makePartial();
$this->instance(Wiki::class, $mock);

$mock->shouldReceive('createWikiByUploadZip')->times(5)->andReturn(json_decode(
file_get_contents($this->dataDir . 'coding/' . 'CreateWikiByZipResponse.json'),
true
)['Response']);
$mock->shouldReceive('getImportJobStatus')->times(5)->andReturn(json_decode(
file_get_contents($this->dataDir . 'coding/' . 'DescribeImportJobStatusResponse.json'),
true
)['Response']['Data']);
$mock->shouldReceive('updateWikiTitle')->times(5)->andReturn(true);


$mockDisk = \Mockery::mock(Disk::class, [])->makePartial();
$this->instance(Disk::class, $mockDisk);
$mockDisk->shouldReceive('uploadAttachments')->times(5)->andReturn([]);

$this->artisan('wiki:import')
->expectsQuestion('数据来源?', 'Confluence')
->expectsQuestion('数据类型?', 'HTML')
->expectsQuestion(
'空间导出的 HTML zip 文件路径',
$this->dataDir . 'confluence/Confluence-space-export-231543-81.html.zip'
)
->expectsOutput('空间名称:空间 1')
->expectsOutput('空间标识:space1')
->expectsOutput('发现 1 个一级页面')
->expectsOutput("开始导入 CODING:")
->expectsOutput('标题:空间 1 Home')
->expectsOutput('上传成功,正在处理,任务 ID:a12353fa-f45b-4af2-83db-666bf9f66615')
->expectsOutput('发现 2 个子页面')
->expectsOutput('标题:hello world')
->expectsOutput('上传成功,正在处理,任务 ID:a12353fa-f45b-4af2-83db-666bf9f66615')
->expectsOutput('发现 2 个子页面')
->expectsOutput('标题:hello')
->expectsOutput('上传成功,正在处理,任务 ID:a12353fa-f45b-4af2-83db-666bf9f66615')
->expectsOutput('标题:world')
->expectsOutput('上传成功,正在处理,任务 ID:a12353fa-f45b-4af2-83db-666bf9f66615')
->expectsOutput('标题:你好世界')
->expectsOutput('上传成功,正在处理,任务 ID:a12353fa-f45b-4af2-83db-666bf9f66615')
->assertExitCode(0);
}
}
Binary file not shown.