140 lines
4.3 KiB
PHP
140 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Channel;
|
|
use RuntimeException;
|
|
use ZipArchive;
|
|
|
|
class ChannelEmbedZipService
|
|
{
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public function listFiles(Channel $channel): array
|
|
{
|
|
$dir = $channel->embedAssetDir();
|
|
if ($dir === null) {
|
|
return [];
|
|
}
|
|
|
|
return $this->collectFiles($dir);
|
|
}
|
|
|
|
public function build(Channel $channel): string
|
|
{
|
|
$dir = $channel->embedAssetDir();
|
|
if ($dir === null) {
|
|
throw new RuntimeException('渠道静态资源不存在,请先构建');
|
|
}
|
|
$files = $this->collectFiles($dir);
|
|
if ($files === []) {
|
|
throw new RuntimeException('渠道目录里没有可打包的浏览器资源');
|
|
}
|
|
if (! class_exists(ZipArchive::class)) {
|
|
throw new RuntimeException('PHP ZipArchive 不可用');
|
|
}
|
|
|
|
$tmp = tempnam(sys_get_temp_dir(), 'coruna-embed-');
|
|
if ($tmp === false) {
|
|
throw new RuntimeException('无法创建临时文件');
|
|
}
|
|
@unlink($tmp);
|
|
$zipPath = $tmp.'.zip';
|
|
|
|
$zip = new ZipArchive();
|
|
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
|
throw new RuntimeException('无法创建 zip');
|
|
}
|
|
|
|
$statOrigin = $this->statOrigin();
|
|
foreach ($files as $rel) {
|
|
$abs = $dir.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $rel);
|
|
$contents = file_get_contents($abs);
|
|
if ($contents === false) {
|
|
continue;
|
|
}
|
|
if ($rel === 'index.js' && $statOrigin !== '') {
|
|
$contents = $this->bakeStatOrigin($contents, $statOrigin);
|
|
}
|
|
$zip->addFromString($rel, $contents);
|
|
}
|
|
$zip->addFromString('README.txt', $this->readme($channel));
|
|
$zip->close();
|
|
|
|
return $zipPath;
|
|
}
|
|
|
|
private function statOrigin(): string
|
|
{
|
|
$domains = Channel::normalizeDomainList(config('coruna.channel_domains', []));
|
|
$host = trim((string) ($domains[0] ?? ''));
|
|
if ($host === '') {
|
|
return '';
|
|
}
|
|
if (preg_match('#^https?://#i', $host)) {
|
|
return rtrim($host, '/');
|
|
}
|
|
$scheme = trim((string) config('coruna.static_site.scheme', 'https')) ?: 'https';
|
|
|
|
return $scheme.'://'.rtrim($host, '/');
|
|
}
|
|
|
|
private function bakeStatOrigin(string $boot, string $origin): string
|
|
{
|
|
$quoted = json_encode($origin, JSON_UNESCAPED_SLASHES);
|
|
$updated = preg_replace(
|
|
'/var STAT_ORIGIN = ([\'"][^\'"]*[\'"]|__STAT_ORIGIN__)/',
|
|
'var STAT_ORIGIN = '.$quoted,
|
|
$boot,
|
|
1,
|
|
);
|
|
|
|
return is_string($updated) ? $updated : $boot;
|
|
}
|
|
|
|
private function readme(Channel $channel): string
|
|
{
|
|
$id = (string) $channel->channel_id;
|
|
|
|
return "把本 zip 解压到站点根目录(与首页同级),页面中加入:\n"
|
|
."<script src=\"./index.js\"></script>\n\n"
|
|
."渠道 {$id} 已写入 index.js。iframe 投放仍可用原落地页链接。\n";
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function collectFiles(string $dir): array
|
|
{
|
|
$skipNames = ['.DS_Store', 'manifest.json', 'README.md', 'README.txt'];
|
|
$skipDirs = ['templates', '_bak', '__pycache__'];
|
|
$files = [];
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS)
|
|
);
|
|
foreach ($iterator as $file) {
|
|
if (! $file->isFile()) {
|
|
continue;
|
|
}
|
|
$abs = $file->getPathname();
|
|
$rel = ltrim(str_replace('\\', '/', substr($abs, strlen($dir))), '/');
|
|
$parts = explode('/', $rel);
|
|
if (array_intersect($parts, $skipDirs) !== []) {
|
|
continue;
|
|
}
|
|
if (in_array(end($parts), $skipNames, true)) {
|
|
continue;
|
|
}
|
|
$ext = strtolower((string) $file->getExtension());
|
|
if (! in_array($ext, ['js', 'html', 'htm', 'css'], true)) {
|
|
continue;
|
|
}
|
|
$files[] = $rel;
|
|
}
|
|
sort($files);
|
|
|
|
return $files;
|
|
}
|
|
}
|