From 38613874ecd9623eafdd117f51285bceb5cf5d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AE=E3=81=B6?= Date: Tue, 3 Mar 2026 14:50:30 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20UnifiedArchive=E3=82=92=E9=99=A4?= =?UTF-8?q?=E5=8E=BB=E3=81=97Archive=5FTar+ZipArchive=E3=81=AB=E7=BD=AE?= =?UTF-8?q?=E6=8F=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UnifiedArchiveは内部でArchive_TarやPharDataをラップしていただけなので 直接Archive_Tar+ZipArchiveを使用するように変更。 - DataMigrationService: extractArchive()を追加 - tar/tar.gz: Archive_Tar(EC-CUBE 2系の壊れたtarに対応) - zip: ZipArchive - composer.json: wapmorgan/unified-archive依存を除去 - Tests/Service/ArchiveExtractionTest: 全フィクスチャの解凍テスト追加 Co-Authored-By: Claude Opus 4.6 --- Service/DataMigrationService.php | 67 +++++++++++++-- Tests/Service/ArchiveExtractionTest.php | 108 ++++++++++++++++++++++++ composer.json | 1 - 3 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 Tests/Service/ArchiveExtractionTest.php diff --git a/Service/DataMigrationService.php b/Service/DataMigrationService.php index 8493e03..8aea76f 100644 --- a/Service/DataMigrationService.php +++ b/Service/DataMigrationService.php @@ -5,7 +5,6 @@ use Eccube\Common\EccubeConfig; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Logging\Middleware; -use wapmorgan\UnifiedArchive\UnifiedArchive; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; class DataMigrationService @@ -72,10 +71,8 @@ public function disableLogging(Connection $em) public function setMigrationVersion($em, $tmpDir, $tmpFile) { - $archive = UnifiedArchive::open($tmpDir . '/' . $tmpFile); - $fileNames = $archive->getFileNames(); - // 解凍 - $archive->extractFiles($tmpDir, $fileNames); + $archivePath = $tmpDir . '/' . $tmpFile; + $fileNames = $this->extractArchive($archivePath, $tmpDir); // 圧縮方式の間違いに対応する $path = pathinfo($fileNames[0]); @@ -118,6 +115,66 @@ public function isVersion($version) return $this->migrationVersion === $version; } + /** + * アーカイブを解凍してファイル名一覧を返す + * + * tar/tar.gz は Archive_Tar、zip は ZipArchive を使用。 + * EC-CUBE 2系バックアップの壊れた tar に対応するため PharData は使わない。 + * + * @param string $archivePath アーカイブファイルのパス + * @param string $outputDir 解凍先ディレクトリ + * @return string[] 解凍されたファイル名一覧 + */ + public function extractArchive(string $archivePath, string $outputDir): array + { + $ext = strtolower(pathinfo($archivePath, PATHINFO_EXTENSION)); + + if ($ext === 'zip') { + return $this->extractZip($archivePath, $outputDir); + } + + // tar / tar.gz / tgz + return $this->extractTar($archivePath, $outputDir); + } + + private function extractTar(string $archivePath, string $outputDir): array + { + $tar = new \Archive_Tar($archivePath); + $result = $tar->extract($outputDir); + if ($result === false) { + throw new \RuntimeException('アーカイブの解凍に失敗しました: ' . basename($archivePath)); + } + + $fileNames = []; + foreach ($tar->listContent() as $entry) { + $name = $entry['filename']; + // ディレクトリエントリ、macリソースフォーク、空エントリを除外 + if ($name === '' || $name === './' || substr($name, -1) === '/' || strpos(basename($name), '._') === 0) { + continue; + } + $fileNames[] = $name; + } + + return $fileNames; + } + + private function extractZip(string $archivePath, string $outputDir): array + { + $zip = new \ZipArchive(); + if ($zip->open($archivePath) !== true) { + throw new \RuntimeException('ZIPファイルの読み込みに失敗しました: ' . basename($archivePath)); + } + + $zip->extractTo($outputDir); + $fileNames = []; + for ($i = 0; $i < $zip->numFiles; $i++) { + $fileNames[] = $zip->getNameIndex($i); + } + $zip->close(); + + return $fileNames; + } + public function updateEnv($newMagicValue) { $projectDir = $this->params->get('kernel.project_dir'); diff --git a/Tests/Service/ArchiveExtractionTest.php b/Tests/Service/ArchiveExtractionTest.php new file mode 100644 index 0000000..6f200e2 --- /dev/null +++ b/Tests/Service/ArchiveExtractionTest.php @@ -0,0 +1,108 @@ +service = self::getContainer()->get(DataMigrationService::class); + $this->fixturesDir = __DIR__ . '/../Fixtures/'; + $this->tmpDir = sys_get_temp_dir() . '/datamigration_test_' . uniqid(); + mkdir($this->tmpDir, 0777, true); + } + + public function tearDown(): void + { + $fs = new Filesystem(); + if (is_dir($this->tmpDir)) { + $fs->remove($this->tmpDir); + } + parent::tearDown(); + } + + public function tarGzProvider() + { + return [ + '2.11系' => ['2_11_5.tar.gz', ['bkup_data.csv', 'autoinc_data.csv']], + '2.12系' => ['2_12_6.tar.gz', ['dtb_customer.csv', 'dtb_order.csv', 'dtb_member.csv']], + '2.13系' => ['2_13_5.tar.gz', ['dtb_customer.csv', 'dtb_order.csv', 'dtb_member.csv']], + '3.0.9' => ['3_0_9.tar.gz', ['dtb_product.csv', 'dtb_customer.csv']], + '3.0.18' => ['3_0_18.tar.gz', ['dtb_product.csv', 'dtb_customer.csv']], + '4.0系' => ['4_0_6.tar.gz', ['dtb_order_item.csv', 'dtb_customer.csv']], + '4.1系' => ['4_1_2.tar.gz', ['dtb_order_item.csv', 'dtb_customer.csv']], + 'member_test' => ['member_test.tar.gz', ['dtb_member.csv', 'mtb_authority.csv']], + ]; + } + + /** + * @dataProvider tarGzProvider + */ + public function testTarGz解凍(string $filename, array $expectedFiles) + { + $archivePath = $this->fixturesDir . $filename; + $fileNames = $this->service->extractArchive($archivePath, $this->tmpDir); + + self::assertNotEmpty($fileNames, $filename . ' のファイル一覧が空'); + + // 期待するファイルが解凍されているか確認 + foreach ($expectedFiles as $expected) { + $found = false; + // サブディレクトリ内も検索 + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->tmpDir, \FilesystemIterator::SKIP_DOTS) + ); + foreach ($iterator as $file) { + if ($file->getFilename() === $expected) { + $found = true; + break; + } + } + self::assertTrue($found, $filename . ' から ' . $expected . ' が解凍されていること'); + } + } + + public function testZip解凍() + { + // テスト用ZIPを作成 + $zipPath = $this->tmpDir . '/test.zip'; + $zip = new \ZipArchive(); + $zip->open($zipPath, \ZipArchive::CREATE); + $zip->addFromString('dtb_customer.csv', "id,name\n1,test\n"); + $zip->addFromString('dtb_product.csv', "id,name\n1,product\n"); + $zip->close(); + + $outputDir = $this->tmpDir . '/output'; + mkdir($outputDir, 0777, true); + + $fileNames = $this->service->extractArchive($zipPath, $outputDir); + + self::assertContains('dtb_customer.csv', $fileNames); + self::assertContains('dtb_product.csv', $fileNames); + self::assertFileExists($outputDir . '/dtb_customer.csv'); + self::assertFileExists($outputDir . '/dtb_product.csv'); + } + + public function test不正なファイルで例外() + { + $badFile = $this->tmpDir . '/bad.tar.gz'; + file_put_contents($badFile, 'not an archive'); + + $this->expectException(\RuntimeException::class); + $this->service->extractArchive($badFile, $this->tmpDir); + } +} diff --git a/composer.json b/composer.json index 2f4572f..1983e61 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,6 @@ "ec-cube/plugin-installer": "^2.0", "pear/pear-core-minimal" : "^1.10.10", "pear/archive_tar" : "^1.4.13", - "wapmorgan/unified-archive": "^1.1.1", "nobuhiko/bulk-insert-query": "^1.1" }, "extra": {