mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-08 01:15:37 +08:00
initial framework commit
This commit is contained in:
126
src/SPC/store/Config.php
Normal file
126
src/SPC/store/Config.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\store;
|
||||
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* 一个读取 config 配置的操作类
|
||||
*/
|
||||
class Config
|
||||
{
|
||||
public static ?array $source = null;
|
||||
|
||||
public static ?array $lib = null;
|
||||
|
||||
public static ?array $ext = null;
|
||||
|
||||
/**
|
||||
* 从配置文件读取一个资源(source)的元信息
|
||||
*
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function getSource(string $name): ?array
|
||||
{
|
||||
if (self::$source === null) {
|
||||
self::$source = FileSystem::loadConfigArray('source');
|
||||
}
|
||||
return self::$source[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据不同的操作系统分别选择不同的 lib 库依赖项
|
||||
* 如果 key 为 null,那么直接返回整个 meta。
|
||||
* 如果 key 不为 null,则可以使用的 key 有 static-libs、headers、lib-depends、lib-suggests。
|
||||
* 对于 macOS 平台,支持 frameworks。
|
||||
*
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function getLib(string $name, ?string $key = null, mixed $default = null)
|
||||
{
|
||||
if (self::$lib === null) {
|
||||
self::$lib = FileSystem::loadConfigArray('lib');
|
||||
}
|
||||
if (!isset(self::$lib[$name])) {
|
||||
throw new RuntimeException('lib [' . $name . '] is not supported yet for get');
|
||||
}
|
||||
$supported_sys_based = ['static-libs', 'headers', 'lib-depends', 'lib-suggests', 'frameworks'];
|
||||
if ($key !== null && in_array($key, $supported_sys_based)) {
|
||||
$m_key = match (PHP_OS_FAMILY) {
|
||||
'Windows' => ['-windows', '-win', ''],
|
||||
'Darwin' => ['-macos', '-unix', ''],
|
||||
'Linux' => ['-linux', '-unix', ''],
|
||||
default => throw new RuntimeException('OS ' . PHP_OS_FAMILY . ' is not supported'),
|
||||
};
|
||||
foreach ($m_key as $v) {
|
||||
if (isset(self::$lib[$name][$key . $v])) {
|
||||
return self::$lib[$name][$key . $v];
|
||||
}
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
if ($key !== null) {
|
||||
return self::$lib[$name][$key] ?? $default;
|
||||
}
|
||||
return self::$lib[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function getLibs(): array
|
||||
{
|
||||
if (self::$lib === null) {
|
||||
self::$lib = FileSystem::loadConfigArray('lib');
|
||||
}
|
||||
return self::$lib;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function getExt(string $name, ?string $key = null, mixed $default = null)
|
||||
{
|
||||
if (self::$ext === null) {
|
||||
self::$ext = FileSystem::loadConfigArray('ext');
|
||||
}
|
||||
if (!isset(self::$ext[$name])) {
|
||||
throw new RuntimeException('ext [' . $name . '] is not supported yet for get');
|
||||
}
|
||||
$supported_sys_based = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests', 'arg-type'];
|
||||
if ($key !== null && in_array($key, $supported_sys_based)) {
|
||||
$m_key = match (PHP_OS_FAMILY) {
|
||||
'Windows' => ['-windows', '-win', ''],
|
||||
'Darwin' => ['-macos', '-unix', ''],
|
||||
'Linux' => ['-linux', '-unix', ''],
|
||||
default => throw new RuntimeException('OS ' . PHP_OS_FAMILY . ' is not supported'),
|
||||
};
|
||||
foreach ($m_key as $v) {
|
||||
if (isset(self::$ext[$name][$key . $v])) {
|
||||
return self::$ext[$name][$key . $v];
|
||||
}
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
if ($key !== null) {
|
||||
return self::$ext[$name][$key] ?? $default;
|
||||
}
|
||||
return self::$ext[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function getExts(): array
|
||||
{
|
||||
if (self::$ext === null) {
|
||||
self::$ext = FileSystem::loadConfigArray('ext');
|
||||
}
|
||||
return self::$ext;
|
||||
}
|
||||
}
|
||||
328
src/SPC/store/Downloader.php
Normal file
328
src/SPC/store/Downloader.php
Normal file
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\store;
|
||||
|
||||
use JetBrains\PhpStorm\ArrayShape;
|
||||
use SPC\exception\DownloaderException;
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* 资源下载器
|
||||
*/
|
||||
class Downloader
|
||||
{
|
||||
/**
|
||||
* 获取 BitBucket 仓库的最新 Tag
|
||||
*
|
||||
* @param string $name 资源名称
|
||||
* @param array $source 资源的元信息,包含字段 repo
|
||||
* @return array<int, string> 返回下载 url 链接和文件名
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function getLatestBitbucketTag(string $name, array $source): array
|
||||
{
|
||||
logger()->debug("finding {$name} source from bitbucket tag");
|
||||
$data = json_decode(self::curlExec(
|
||||
url: "https://api.bitbucket.org/2.0/repositories/{$source['repo']}/refs/tags"
|
||||
), true);
|
||||
$ver = $data['values'][0]['name'];
|
||||
if (!$ver) {
|
||||
throw new DownloaderException("failed to find {$name} bitbucket source");
|
||||
}
|
||||
$url = "https://bitbucket.org/{$source['repo']}/get/{$ver}.tar.gz";
|
||||
$headers = self::curlExec(
|
||||
url: $url,
|
||||
method: 'HEAD'
|
||||
);
|
||||
preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?<filename>.+\.tar\.gz)\1/im', $headers, $matches);
|
||||
if ($matches) {
|
||||
$filename = $matches['filename'];
|
||||
} else {
|
||||
$filename = "{$name}-{$data['tag_name']}.tar.gz";
|
||||
}
|
||||
|
||||
return [$url, $filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 GitHub 最新的打包地址和文件名
|
||||
*
|
||||
* @param string $name 包名称
|
||||
* @param array $source 源信息
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function getLatestGithubTarball(string $name, array $source, string $type = 'releases'): array
|
||||
{
|
||||
logger()->debug("finding {$name} source from github {$type} tarball");
|
||||
$data = json_decode(self::curlExec(
|
||||
url: "https://api.github.com/repos/{$source['repo']}/{$type}",
|
||||
hooks: [[CurlHook::class, 'setupGithubToken']]
|
||||
), true);
|
||||
$url = $data[0]['tarball_url'];
|
||||
if (!$url) {
|
||||
throw new DownloaderException("failed to find {$name} source");
|
||||
}
|
||||
$headers = self::curlExec(
|
||||
url: $url,
|
||||
method: 'HEAD',
|
||||
hooks: [[CurlHook::class, 'setupGithubToken']],
|
||||
);
|
||||
preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?<filename>.+\.tar\.gz)\1/im', $headers, $matches);
|
||||
if ($matches) {
|
||||
$filename = $matches['filename'];
|
||||
} else {
|
||||
$filename = "{$name}-" . ($type === 'releases' ? $data['tag_name'] : $data['name']) . '.tar.gz';
|
||||
}
|
||||
|
||||
return [$url, $filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 GitHub 最新的 Release 下载信息
|
||||
*
|
||||
* @param string $name 资源名
|
||||
* @param array $source 资源的元信息,包含字段 repo、match
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function getLatestGithubRelease(string $name, array $source): array
|
||||
{
|
||||
logger()->debug("finding {$name} source from github releases assests");
|
||||
$data = json_decode(self::curlExec(
|
||||
url: "https://api.github.com/repos/{$source['repo']}/releases",
|
||||
hooks: [[CurlHook::class, 'setupGithubToken']],
|
||||
), true);
|
||||
$url = null;
|
||||
foreach ($data[0]['assets'] as $asset) {
|
||||
if (preg_match('|' . $source['match'] . '|', $asset['name'])) {
|
||||
$url = $asset['browser_download_url'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$url) {
|
||||
throw new DownloaderException("failed to find {$name} source");
|
||||
}
|
||||
$filename = basename($url);
|
||||
|
||||
return [$url, $filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件列表的资源链接和名称
|
||||
*
|
||||
* @param string $name 资源名称
|
||||
* @param array $source 资源元信息,包含 url、regex
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function getFromFileList(string $name, array $source): array
|
||||
{
|
||||
logger()->debug("finding {$name} source from file list");
|
||||
$page = self::curlExec($source['url']);
|
||||
preg_match_all($source['regex'], $page, $matches);
|
||||
if (!$matches) {
|
||||
throw new DownloaderException("Failed to get {$name} version");
|
||||
}
|
||||
$versions = [];
|
||||
foreach ($matches['version'] as $i => $version) {
|
||||
$lowerVersion = strtolower($version);
|
||||
foreach ([
|
||||
'alpha',
|
||||
'beta',
|
||||
'rc',
|
||||
'pre',
|
||||
'nightly',
|
||||
'snapshot',
|
||||
'dev',
|
||||
] as $betaVersion) {
|
||||
if (str_contains($lowerVersion, $betaVersion)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
$versions[$version] = $matches['file'][$i];
|
||||
}
|
||||
uksort($versions, 'version_compare');
|
||||
|
||||
return [$source['url'] . end($versions), end($versions)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过链接下载资源到本地并解压
|
||||
*
|
||||
* @param string $name 资源名称
|
||||
* @param string $url 下载链接
|
||||
* @param string $filename 下载到下载目录的目标文件名称,例如 xz.tar.gz
|
||||
* @param null|string $path 如果指定了此参数,则会移动该资源目录到目标目录
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function downloadUrl(string $name, string $url, string $filename, ?string $path = null): void
|
||||
{
|
||||
if (!file_exists(DOWNLOAD_PATH . "/{$filename}")) {
|
||||
logger()->debug("downloading {$url}");
|
||||
self::curlDown(url: $url, path: DOWNLOAD_PATH . "/{$filename}");
|
||||
} else {
|
||||
logger()->notice("{$filename} already exists");
|
||||
}
|
||||
FileSystem::extractSource($name, DOWNLOAD_PATH . "/{$filename}");
|
||||
if ($path) {
|
||||
$path = FileSystem::convertPath(SOURCE_PATH . "/{$path}");
|
||||
$src_path = FileSystem::convertPath(SOURCE_PATH . "/{$name}");
|
||||
switch (PHP_OS_FAMILY) {
|
||||
case 'Windows':
|
||||
f_passthru('move "' . $src_path . '" "' . $path . '"');
|
||||
break;
|
||||
case 'Linux':
|
||||
case 'Darwin':
|
||||
f_passthru('mv "' . $src_path . '" "' . $path . '"');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取资源
|
||||
*
|
||||
* @param string $name 资源名称
|
||||
* @param array $source 资源参数,包含 type、path、rev、url、filename、regex、license
|
||||
* @throws DownloaderException
|
||||
* @throws RuntimeException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function fetchSource(string $name, array $source): void
|
||||
{
|
||||
// 避免重复 fetch
|
||||
if (is_dir(FileSystem::convertPath(SOURCE_PATH . "/{$name}")) || isset($source['path']) && is_dir(FileSystem::convertPath(SOURCE_PATH . "/{$source['path']}"))) {
|
||||
logger()->notice("{$name} source already extracted");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
switch ($source['type']) {
|
||||
case 'bitbuckettag': // 从 BitBucket 的 Tag 拉取
|
||||
[$url, $filename] = self::getLatestBitbucketTag($name, $source);
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'ghtar': // 从 GitHub 的 TarBall 拉取
|
||||
[$url, $filename] = self::getLatestGithubTarball($name, $source);
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'ghtagtar': // 根据 GitHub 的 Tag 拉取相应版本的 Tar
|
||||
[$url, $filename] = self::getLatestGithubTarball($name, $source, 'tags');
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'ghrel': // 通过 GitHub Release 来拉取
|
||||
[$url, $filename] = self::getLatestGithubRelease($name, $source);
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'filelist': // 通过网站提供的 filelist 使用正则提取后拉取
|
||||
[$url, $filename] = self::getFromFileList($name, $source);
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'url': // 通过直链拉取
|
||||
$url = $source['url'];
|
||||
$filename = $source['filename'] ?? basename($source['url']);
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'git': // 通过拉回 Git 仓库的形式拉取
|
||||
if ($source['path'] ?? null) {
|
||||
$path = SOURCE_PATH . "/{$source['path']}";
|
||||
} else {
|
||||
$path = SOURCE_PATH . "/{$name}";
|
||||
}
|
||||
if (file_exists($path)) {
|
||||
logger()->notice("{$name} source already exists");
|
||||
break;
|
||||
}
|
||||
logger()->debug("cloning {$name} source");
|
||||
$check = !defined('DEBUG_MODE') ? ' -q' : '';
|
||||
f_passthru(
|
||||
'git clone' . $check .
|
||||
' --config core.autocrlf=false ' .
|
||||
"--branch \"{$source['rev']}\" " . (defined('GIT_SHALLOW_CLONE') ? '--depth 1 --single-branch' : '') . " --recursive \"{$source['url']}\" \"{$path}\""
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new DownloaderException('unknown source type: ' . $source['type']);
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
// 因为某些时候通过命令行下载的文件在失败后不会删除,这里检测到文件存在需要手动删一下
|
||||
if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) {
|
||||
logger()->warning('Deleting download file: ' . $filename);
|
||||
unlink(DOWNLOAD_PATH . '/' . $filename);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 PHP x.y 的具体版本号,例如通过 8.1 来获取 8.1.10
|
||||
*
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
#[ArrayShape(['type' => 'string', 'path' => 'string', 'rev' => 'string', 'url' => 'string'])]
|
||||
public static function getLatestPHPInfo(string $major_version): array
|
||||
{
|
||||
// 查找最新的小版本号
|
||||
$info = json_decode(self::curlExec(url: "https://www.php.net/releases/index.php?json&version={$major_version}"), true);
|
||||
$version = $info['version'];
|
||||
|
||||
// 从官网直接下载
|
||||
return [
|
||||
'type' => 'url',
|
||||
'url' => "https://www.php.net/distributions/php-{$version}.tar.gz",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 curl 命令拉取元信息
|
||||
*
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function curlExec(string $url, string $method = 'GET', array $headers = [], array $hooks = []): string
|
||||
{
|
||||
foreach ($hooks as $hook) {
|
||||
$hook($method, $url, $headers);
|
||||
}
|
||||
|
||||
FileSystem::findCommandPath('curl');
|
||||
|
||||
$methodArg = match ($method) {
|
||||
'GET' => '',
|
||||
'HEAD' => '-I',
|
||||
default => "-X \"{$method}\"",
|
||||
};
|
||||
$headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
|
||||
|
||||
$cmd = "curl -sfSL {$methodArg} {$headerArg} \"{$url}\"";
|
||||
f_exec($cmd, $output, $ret);
|
||||
if ($ret !== 0) {
|
||||
throw new DownloaderException('failed http fetch');
|
||||
}
|
||||
return implode("\n", $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 curl 命令下载文件
|
||||
*
|
||||
* @throws DownloaderException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function curlDown(string $url, string $path, string $method = 'GET', array $headers = [], array $hooks = []): void
|
||||
{
|
||||
foreach ($hooks as $hook) {
|
||||
$hook($method, $url, $headers);
|
||||
}
|
||||
|
||||
$methodArg = match ($method) {
|
||||
'GET' => '',
|
||||
'HEAD' => '-I',
|
||||
default => "-X \"{$method}\"",
|
||||
};
|
||||
$headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
|
||||
$check = !defined('DEBUG_MODE') ? 's' : '#';
|
||||
$cmd = "curl -{$check}fSL -o \"{$path}\" {$methodArg} {$headerArg} \"{$url}\"";
|
||||
f_passthru($cmd);
|
||||
}
|
||||
}
|
||||
379
src/SPC/store/FileSystem.php
Normal file
379
src/SPC/store/FileSystem.php
Normal file
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\store;
|
||||
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
use Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator;
|
||||
|
||||
class FileSystem
|
||||
{
|
||||
private static $source_cache;
|
||||
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function loadConfigArray(string $config): array
|
||||
{
|
||||
$whitelist = ['ext', 'lib', 'source'];
|
||||
if (!in_array($config, $whitelist)) {
|
||||
throw new FileSystemException('Reading ' . $config . '.json is not allowed');
|
||||
}
|
||||
$tries = [
|
||||
WORKING_DIR . '/config/' . $config . '.json',
|
||||
ROOT_DIR . '/config/' . $config . '.json',
|
||||
];
|
||||
foreach ($tries as $try) {
|
||||
if (file_exists($try)) {
|
||||
$json = json_decode(self::readFile($try), true);
|
||||
if (!is_array($json)) {
|
||||
throw new FileSystemException('Reading ' . $try . ' failed');
|
||||
}
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
throw new FileSystemException('Reading ' . $config . '.json failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件,读不出来直接抛出异常
|
||||
*
|
||||
* @param string $filename 文件路径
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function readFile(string $filename): string
|
||||
{
|
||||
// logger()->debug('Reading file: ' . $filename);
|
||||
$r = file_get_contents(self::convertPath($filename));
|
||||
if ($r === false) {
|
||||
throw new FileSystemException('Reading file ' . $filename . ' failed');
|
||||
}
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function replaceFile(string $filename, int $replace_type = REPLACE_FILE_STR, mixed $callback_or_search = null, mixed $to_replace = null): bool|int
|
||||
{
|
||||
logger()->debug('Replacing file with type[' . $replace_type . ']: ' . $filename);
|
||||
$file = self::readFile($filename);
|
||||
switch ($replace_type) {
|
||||
case REPLACE_FILE_STR:
|
||||
default:
|
||||
$file = str_replace($callback_or_search, $to_replace, $file);
|
||||
break;
|
||||
case REPLACE_FILE_PREG:
|
||||
$file = preg_replace($callback_or_search, $to_replace, $file);
|
||||
break;
|
||||
case REPLACE_FILE_USER:
|
||||
$file = $callback_or_search($file);
|
||||
break;
|
||||
}
|
||||
return file_put_contents($filename, $file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件后缀
|
||||
*
|
||||
* @param string $fn 文件名
|
||||
*/
|
||||
public static function extname(string $fn): string
|
||||
{
|
||||
$parts = explode('.', basename($fn));
|
||||
if (count($parts) < 2) {
|
||||
return '';
|
||||
}
|
||||
return array_pop($parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻找命令的真实路径,效果类似 which
|
||||
*
|
||||
* @param string $name 命令名称
|
||||
* @param array $paths 路径列表,如果为空则默认从 PATH 系统变量搜索
|
||||
*/
|
||||
public static function findCommandPath(string $name, array $paths = []): ?string
|
||||
{
|
||||
if (!$paths) {
|
||||
$paths = explode(PATH_SEPARATOR, getenv('PATH'));
|
||||
}
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
foreach ($paths as $path) {
|
||||
foreach (['.exe', '.bat', '.cmd'] as $suffix) {
|
||||
if (file_exists($path . DIRECTORY_SEPARATOR . $name . $suffix)) {
|
||||
return $path . DIRECTORY_SEPARATOR . $name . $suffix;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
foreach ($paths as $path) {
|
||||
if (file_exists($path . DIRECTORY_SEPARATOR . $name)) {
|
||||
return $path . DIRECTORY_SEPARATOR . $name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function copyDir(string $from, string $to): void
|
||||
{
|
||||
$iterator = new \RecursiveIteratorIterator(new RecursiveDirectoryIterator($from, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_FILEINFO), \RecursiveIteratorIterator::SELF_FIRST);
|
||||
foreach ($iterator as $item) {
|
||||
/**
|
||||
* @var \SplFileInfo $item
|
||||
*/
|
||||
$target = $to . substr($item->getPathname(), strlen($from));
|
||||
if ($item->isDir()) {
|
||||
logger()->info("mkdir {$target}");
|
||||
f_mkdir($target, recursive: true);
|
||||
} else {
|
||||
logger()->info("copying {$item} to {$target}");
|
||||
@f_mkdir(dirname($target), recursive: true);
|
||||
copy($item->getPathname(), $target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压缩下载的资源包到 source 目录
|
||||
*
|
||||
* @param string $name 资源名
|
||||
* @param string $filename 文件名
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function extractSource(string $name, string $filename): void
|
||||
{
|
||||
logger()->info("extracting {$name} source");
|
||||
try {
|
||||
if (PHP_OS_FAMILY !== 'Windows') {
|
||||
if (f_mkdir(directory: SOURCE_PATH . "/{$name}", recursive: true) !== true) {
|
||||
throw new FileSystemException('create ' . $name . 'source dir failed');
|
||||
}
|
||||
switch (self::extname($filename)) {
|
||||
case 'xz':
|
||||
case 'txz':
|
||||
f_passthru("tar -xf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
// f_passthru("cat {$filename} | xz -d | tar -x -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
case 'gz':
|
||||
case 'tgz':
|
||||
f_passthru("tar -xzf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
case 'bz2':
|
||||
f_passthru("tar -xjf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
case 'zip':
|
||||
f_passthru("unzip {$filename} -d " . SOURCE_PATH . "/{$name}");
|
||||
break;
|
||||
// case 'zstd':
|
||||
// case 'zst':
|
||||
// passthru('cat ' . $filename . ' | zstd -d | tar -x -C ".SOURCE_PATH . "/' . $name . ' --strip-components 1', $ret);
|
||||
// break;
|
||||
case 'tar':
|
||||
f_passthru("tar -xf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
default:
|
||||
throw new FileSystemException('unknown archive format: ' . $filename);
|
||||
}
|
||||
} else {
|
||||
// find 7z
|
||||
$_7zExe = self::findCommandPath('7z', [
|
||||
'C:\Program Files\7-Zip-Zstandard',
|
||||
'C:\Program Files (x86)\7-Zip-Zstandard',
|
||||
'C:\Program Files\7-Zip',
|
||||
'C:\Program Files (x86)\7-Zip',
|
||||
]);
|
||||
if (!$_7zExe) {
|
||||
throw new FileSystemException('windows needs 7z to unpack');
|
||||
}
|
||||
f_mkdir(SOURCE_PATH . "/{$name}", recursive: true);
|
||||
switch (self::extname($filename)) {
|
||||
case 'zstd':
|
||||
case 'zst':
|
||||
if (!str_contains($_7zExe, 'Zstandard')) {
|
||||
throw new FileSystemException("zstd is not supported: {$filename}");
|
||||
}
|
||||
// no break
|
||||
case 'xz':
|
||||
case 'txz':
|
||||
case 'gz':
|
||||
case 'tgz':
|
||||
case 'bz2':
|
||||
f_passthru("\"{$_7zExe}\" x -so {$filename} | tar -f - -x -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
case 'tar':
|
||||
f_passthru("tar -xf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
case 'zip':
|
||||
f_passthru("\"{$_7zExe}\" x {$filename} -o" . SOURCE_PATH . "/{$name}");
|
||||
break;
|
||||
default:
|
||||
throw new FileSystemException("unknown archive format: {$filename}");
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
f_passthru('rmdir /s /q ' . SOURCE_PATH . "/{$name}");
|
||||
} else {
|
||||
f_passthru('rm -r ' . SOURCE_PATH . "/{$name}");
|
||||
}
|
||||
throw new FileSystemException('Cannot extract source ' . $name, $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据系统环境的不同,自动转换路径的分隔符
|
||||
*
|
||||
* @param string $path 路径
|
||||
*/
|
||||
public static function convertPath(string $path): string
|
||||
{
|
||||
if (str_starts_with($path, 'phar://')) {
|
||||
return $path;
|
||||
}
|
||||
return str_replace('/', DIRECTORY_SEPARATOR, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归或非递归扫描目录,可返回相对目录的文件列表或绝对目录的文件列表
|
||||
*
|
||||
* @param string $dir 目录
|
||||
* @param bool $recursive 是否递归扫描子目录
|
||||
* @param bool|string $relative 是否返回相对目录,如果为true则返回相对目录,如果为false则返回绝对目录
|
||||
* @param bool $include_dir 非递归模式下,是否包含目录
|
||||
* @return array|false
|
||||
* @since 2.5
|
||||
*/
|
||||
public static function scanDirFiles(string $dir, bool $recursive = true, bool|string $relative = false, bool $include_dir = false): bool|array
|
||||
{
|
||||
$dir = self::convertPath($dir);
|
||||
// 不是目录不扫,直接 false 处理
|
||||
if (!is_dir($dir)) {
|
||||
logger()->warning('Scan dir failed, no such directory.');
|
||||
return false;
|
||||
}
|
||||
logger()->debug('scanning directory ' . $dir);
|
||||
// 套上 zm_dir
|
||||
$scan_list = scandir($dir);
|
||||
if ($scan_list === false) {
|
||||
logger()->warning('Scan dir failed, cannot scan directory: ' . $dir);
|
||||
return false;
|
||||
}
|
||||
$list = [];
|
||||
// 将 relative 置为相对目录的前缀
|
||||
if ($relative === true) {
|
||||
$relative = $dir;
|
||||
}
|
||||
// 遍历目录
|
||||
foreach ($scan_list as $v) {
|
||||
// Unix 系统排除这俩目录
|
||||
if ($v == '.' || $v == '..') {
|
||||
continue;
|
||||
}
|
||||
$sub_file = self::convertPath($dir . '/' . $v);
|
||||
if (is_dir($sub_file) && $recursive) {
|
||||
# 如果是 目录 且 递推 , 则递推添加下级文件
|
||||
$list = array_merge($list, self::scanDirFiles($sub_file, $recursive, $relative));
|
||||
} elseif (is_file($sub_file) || is_dir($sub_file) && !$recursive && $include_dir) {
|
||||
# 如果是 文件 或 (是 目录 且 不递推 且 包含目录)
|
||||
if (is_string($relative) && mb_strpos($sub_file, $relative) === 0) {
|
||||
$list[] = ltrim(mb_substr($sub_file, mb_strlen($relative)), '/\\');
|
||||
} elseif ($relative === false) {
|
||||
$list[] = $sub_file;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|mixed $rule
|
||||
* @param mixed $return_path_value
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function getClassesPsr4(string $dir, string $base_namespace, $rule = null, $return_path_value = false): array
|
||||
{
|
||||
$classes = [];
|
||||
// 扫描目录,使用递归模式,相对路径模式,因为下面此路径要用作转换成namespace
|
||||
$files = FileSystem::scanDirFiles($dir, true, true);
|
||||
if ($files === false) {
|
||||
throw new FileSystemException('Cannot scan dir files during get classes psr-4 from dir: ' . $dir);
|
||||
}
|
||||
foreach ($files as $v) {
|
||||
$pathinfo = pathinfo($v);
|
||||
if (($pathinfo['extension'] ?? '') == 'php') {
|
||||
$path = rtrim($dir, '/') . '/' . rtrim($pathinfo['dirname'], './') . '/' . $pathinfo['basename'];
|
||||
|
||||
// 过滤不包含类的文件
|
||||
$tokens = token_get_all(self::readFile($path));
|
||||
$found = false;
|
||||
foreach ($tokens as $token) {
|
||||
if (!is_array($token)) {
|
||||
continue;
|
||||
}
|
||||
if ($token[0] === T_CLASS) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($rule === null) { // 规则未设置回调时候,使用默认的识别过滤规则
|
||||
/*if (substr(file_get_contents($dir . '/' . $v), 6, 6) == '#plain') {
|
||||
continue;
|
||||
}*/
|
||||
if (file_exists($dir . '/' . $pathinfo['basename'] . '.ignore')) {
|
||||
continue;
|
||||
}
|
||||
if (mb_substr($pathinfo['basename'], 0, 7) == 'global_' || mb_substr($pathinfo['basename'], 0, 7) == 'script_') {
|
||||
continue;
|
||||
}
|
||||
} elseif (is_callable($rule) && !$rule($dir, $pathinfo)) {
|
||||
continue;
|
||||
}
|
||||
$dirname = $pathinfo['dirname'] == '.' ? '' : (str_replace('/', '\\', $pathinfo['dirname']) . '\\');
|
||||
$class_name = $base_namespace . '\\' . $dirname . $pathinfo['filename'];
|
||||
if (is_string($return_path_value)) {
|
||||
$classes[$class_name] = $return_path_value . '/' . $v;
|
||||
} else {
|
||||
$classes[] = $class_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除目录及目录下的所有文件(危险操作)
|
||||
*
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function removeDir(string $dir, bool $throw_on_fail = false): bool
|
||||
{
|
||||
$dir = FileSystem::convertPath($dir);
|
||||
logger()->warning('Removing path recursively: "' . $dir . '"');
|
||||
switch (PHP_OS_FAMILY) {
|
||||
case 'Windows':
|
||||
case 'WINNT':
|
||||
case 'Cygwin':
|
||||
f_exec('rmdir /s /g "' . $dir . '"', $out, $ret);
|
||||
break;
|
||||
case 'Darwin':
|
||||
case 'Linux':
|
||||
f_exec('rm -rf ' . escapeshellarg($dir), $out, $ret);
|
||||
break;
|
||||
default:
|
||||
throw new FileSystemException('Unsupported OS type: ' . PHP_OS_FAMILY);
|
||||
}
|
||||
if ($ret !== 0 && $throw_on_fail) {
|
||||
throw new FileSystemException('Cannot remove dir "' . $dir . '"');
|
||||
}
|
||||
return $ret === 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user