2023-03-15 20:40:49 +08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace SPC\store;
|
|
|
|
|
|
|
|
|
|
use SPC\exception\DownloaderException;
|
2025-08-06 20:43:23 +08:00
|
|
|
use SPC\exception\InterruptException;
|
|
|
|
|
use SPC\exception\SPCException;
|
2025-06-18 20:55:24 +08:00
|
|
|
use SPC\store\pkg\CustomPackage;
|
2023-04-09 12:11:14 +08:00
|
|
|
use SPC\store\source\CustomSourceBase;
|
2025-06-28 16:36:05 +08:00
|
|
|
use SPC\util\SPCTarget;
|
2023-03-15 20:40:49 +08:00
|
|
|
|
|
|
|
|
/**
|
2023-08-20 19:51:45 +08:00
|
|
|
* Source Downloader.
|
2023-03-15 20:40:49 +08:00
|
|
|
*/
|
|
|
|
|
class Downloader
|
|
|
|
|
{
|
|
|
|
|
/**
|
2025-07-29 11:08:53 +08:00
|
|
|
* Get latest version from BitBucket tag
|
2023-03-15 20:40:49 +08:00
|
|
|
*
|
2025-08-06 20:17:26 +08:00
|
|
|
* @param string $name Source name
|
|
|
|
|
* @param array $source Source meta info: [repo]
|
|
|
|
|
* @return array<int, string> [url, filename]
|
2023-03-15 20:40:49 +08:00
|
|
|
*/
|
|
|
|
|
public static function getLatestBitbucketTag(string $name, array $source): array
|
|
|
|
|
{
|
|
|
|
|
logger()->debug("finding {$name} source from bitbucket tag");
|
|
|
|
|
$data = json_decode(self::curlExec(
|
2024-03-10 16:23:30 +08:00
|
|
|
url: "https://api.bitbucket.org/2.0/repositories/{$source['repo']}/refs/tags",
|
2025-04-22 12:23:47 +07:00
|
|
|
retries: self::getRetryAttempts()
|
2023-03-15 20:40:49 +08:00
|
|
|
), 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,
|
2024-03-10 16:23:30 +08:00
|
|
|
method: 'HEAD',
|
2025-04-22 12:23:47 +07:00
|
|
|
retries: self::getRetryAttempts()
|
2023-03-15 20:40:49 +08:00
|
|
|
);
|
|
|
|
|
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];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-07-29 11:08:53 +08:00
|
|
|
* Get latest version from GitHub tarball
|
2023-03-15 20:40:49 +08:00
|
|
|
*
|
2025-08-06 20:17:26 +08:00
|
|
|
* @param string $name Source name
|
|
|
|
|
* @param array $source Source meta info: [repo]
|
|
|
|
|
* @param string $type Type of tarball, default is 'releases'
|
|
|
|
|
* @return array<int, string> [url, filename]
|
2023-03-15 20:40:49 +08:00
|
|
|
*/
|
|
|
|
|
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}",
|
2024-03-10 16:23:30 +08:00
|
|
|
hooks: [[CurlHook::class, 'setupGithubToken']],
|
2025-04-22 12:23:47 +07:00
|
|
|
retries: self::getRetryAttempts()
|
2023-03-15 20:40:49 +08:00
|
|
|
), true);
|
2024-06-30 22:36:22 +08:00
|
|
|
|
2025-03-20 04:36:46 +01:00
|
|
|
$url = null;
|
|
|
|
|
for ($i = 0; $i < count($data); ++$i) {
|
|
|
|
|
if (($data[$i]['prerelease'] ?? false) === true && ($source['prefer-stable'] ?? false)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!($source['match'] ?? null)) {
|
|
|
|
|
$url = $data[$i]['tarball_url'] ?? null;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (preg_match('|' . $source['match'] . '|', $data[$i]['tarball_url'])) {
|
|
|
|
|
$url = $data[$i]['tarball_url'];
|
|
|
|
|
break;
|
2024-06-30 22:36:22 +08:00
|
|
|
}
|
|
|
|
|
}
|
2023-03-15 20:40:49 +08:00
|
|
|
if (!$url) {
|
|
|
|
|
throw new DownloaderException("failed to find {$name} source");
|
|
|
|
|
}
|
|
|
|
|
$headers = self::curlExec(
|
|
|
|
|
url: $url,
|
|
|
|
|
method: 'HEAD',
|
|
|
|
|
hooks: [[CurlHook::class, 'setupGithubToken']],
|
2025-04-22 12:23:47 +07:00
|
|
|
retries: self::getRetryAttempts()
|
2023-03-15 20:40:49 +08:00
|
|
|
);
|
|
|
|
|
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];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2023-08-20 19:51:45 +08:00
|
|
|
* Get latest version from GitHub release (uploaded archive)
|
2023-03-15 20:40:49 +08:00
|
|
|
*
|
2025-08-06 20:17:26 +08:00
|
|
|
* @param string $name Source name
|
|
|
|
|
* @param array $source Source meta info: [repo, match]
|
|
|
|
|
* @param bool $match_result Whether to return matched result by `match` param (default: true)
|
|
|
|
|
* @return array<int, string> When $match_result = true, and we matched, [url, filename]. Otherwise, [{asset object}. ...]
|
2023-03-15 20:40:49 +08:00
|
|
|
*/
|
2024-07-07 20:45:18 +08:00
|
|
|
public static function getLatestGithubRelease(string $name, array $source, bool $match_result = true): array
|
2023-03-15 20:40:49 +08:00
|
|
|
{
|
2024-11-09 10:50:08 +08:00
|
|
|
logger()->debug("finding {$name} from github releases assets");
|
2023-03-15 20:40:49 +08:00
|
|
|
$data = json_decode(self::curlExec(
|
|
|
|
|
url: "https://api.github.com/repos/{$source['repo']}/releases",
|
|
|
|
|
hooks: [[CurlHook::class, 'setupGithubToken']],
|
2025-04-22 12:23:47 +07:00
|
|
|
retries: self::getRetryAttempts()
|
2023-03-15 20:40:49 +08:00
|
|
|
), true);
|
|
|
|
|
$url = null;
|
2025-04-24 14:21:22 +08:00
|
|
|
$filename = null;
|
2023-10-18 10:55:57 +08:00
|
|
|
foreach ($data as $release) {
|
2024-06-30 22:36:22 +08:00
|
|
|
if (($source['prefer-stable'] ?? false) === true && $release['prerelease'] === true) {
|
2023-10-18 10:55:57 +08:00
|
|
|
continue;
|
|
|
|
|
}
|
2025-03-30 19:21:29 +08:00
|
|
|
logger()->debug("Found {$release['name']} releases assets");
|
2024-07-07 20:45:18 +08:00
|
|
|
if (!$match_result) {
|
|
|
|
|
return $release['assets'];
|
|
|
|
|
}
|
2023-10-18 10:55:57 +08:00
|
|
|
foreach ($release['assets'] as $asset) {
|
|
|
|
|
if (preg_match('|' . $source['match'] . '|', $asset['name'])) {
|
2025-04-24 14:21:22 +08:00
|
|
|
$url = "https://api.github.com/repos/{$source['repo']}/releases/assets/{$asset['id']}";
|
|
|
|
|
$filename = $asset['name'];
|
2023-10-18 10:55:57 +08:00
|
|
|
break 2;
|
|
|
|
|
}
|
2023-03-15 20:40:49 +08:00
|
|
|
}
|
|
|
|
|
}
|
2023-10-18 10:55:57 +08:00
|
|
|
|
2025-04-24 14:21:22 +08:00
|
|
|
if (!$url || !$filename) {
|
2024-07-07 20:45:18 +08:00
|
|
|
throw new DownloaderException("failed to find {$name} release metadata");
|
2023-03-15 20:40:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [$url, $filename];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2023-08-20 19:51:45 +08:00
|
|
|
* Get latest version from file list (regex based crawler)
|
2023-03-15 20:40:49 +08:00
|
|
|
*
|
2025-08-06 20:17:26 +08:00
|
|
|
* @param string $name Source name
|
|
|
|
|
* @param array $source Source meta info: [filelist]
|
|
|
|
|
* @return array<int, string> [url, filename]
|
2023-03-15 20:40:49 +08:00
|
|
|
*/
|
|
|
|
|
public static function getFromFileList(string $name, array $source): array
|
|
|
|
|
{
|
|
|
|
|
logger()->debug("finding {$name} source from file list");
|
2025-04-22 12:23:47 +07:00
|
|
|
$page = self::curlExec($source['url'], retries: self::getRetryAttempts());
|
2023-03-15 20:40:49 +08:00
|
|
|
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');
|
|
|
|
|
|
2023-04-09 12:11:14 +08:00
|
|
|
return [$source['url'] . end($versions), end($versions), key($versions)];
|
2023-03-15 20:40:49 +08:00
|
|
|
}
|
|
|
|
|
|
2023-04-30 12:42:19 +08:00
|
|
|
/**
|
2025-07-29 11:08:53 +08:00
|
|
|
* Download file from URL
|
2023-08-20 19:51:45 +08:00
|
|
|
*
|
2025-08-06 20:17:26 +08:00
|
|
|
* @param string $name Download name
|
|
|
|
|
* @param string $url Download URL
|
|
|
|
|
* @param string $filename Target filename
|
|
|
|
|
* @param null|string $move_path Optional move path after download
|
|
|
|
|
* @param int $download_as Download type constant
|
|
|
|
|
* @param array $headers Optional HTTP headers
|
|
|
|
|
* @param array $hooks Optional curl hooks
|
2023-04-30 12:42:19 +08:00
|
|
|
*/
|
2025-04-24 14:21:22 +08:00
|
|
|
public static function downloadFile(string $name, string $url, string $filename, ?string $move_path = null, int $download_as = SPC_DOWNLOAD_SOURCE, array $headers = [], array $hooks = []): void
|
2023-04-30 12:42:19 +08:00
|
|
|
{
|
|
|
|
|
logger()->debug("Downloading {$url}");
|
2024-01-10 21:08:25 +08:00
|
|
|
$cancel_func = function () use ($filename) {
|
|
|
|
|
if (file_exists(FileSystem::convertPath(DOWNLOAD_PATH . '/' . $filename))) {
|
2023-04-30 12:42:19 +08:00
|
|
|
logger()->warning('Deleting download file: ' . $filename);
|
2024-01-10 21:08:25 +08:00
|
|
|
unlink(FileSystem::convertPath(DOWNLOAD_PATH . '/' . $filename));
|
2023-04-30 12:42:19 +08:00
|
|
|
}
|
2024-01-10 21:08:25 +08:00
|
|
|
};
|
2025-08-06 20:43:23 +08:00
|
|
|
keyboard_interrupt_register($cancel_func);
|
2025-04-24 14:21:22 +08:00
|
|
|
self::curlDown(url: $url, path: FileSystem::convertPath(DOWNLOAD_PATH . "/{$filename}"), headers: $headers, hooks: $hooks, retries: self::getRetryAttempts());
|
2025-08-06 20:43:23 +08:00
|
|
|
keyboard_interrupt_unregister();
|
2023-04-30 12:42:19 +08:00
|
|
|
logger()->debug("Locking {$filename}");
|
2025-03-30 23:27:43 +08:00
|
|
|
if ($download_as === SPC_DOWNLOAD_PRE_BUILT) {
|
2025-03-30 20:16:41 +08:00
|
|
|
$name = self::getPreBuiltLockName($name);
|
2025-03-30 16:53:14 +08:00
|
|
|
}
|
2025-06-18 14:05:43 +08:00
|
|
|
LockFile::lockSource($name, ['source_type' => SPC_SOURCE_ARCHIVE, 'filename' => $filename, 'move_path' => $move_path, 'lock_as' => $download_as]);
|
2023-04-30 12:42:19 +08:00
|
|
|
}
|
|
|
|
|
|
2023-03-15 20:40:49 +08:00
|
|
|
/**
|
2025-07-29 11:08:53 +08:00
|
|
|
* Download Git repository
|
2023-03-15 20:40:49 +08:00
|
|
|
*
|
2025-08-06 20:17:26 +08:00
|
|
|
* @param string $name Repository name
|
|
|
|
|
* @param string $url Git repository URL
|
|
|
|
|
* @param string $branch Branch to checkout
|
|
|
|
|
* @param null|array $submodules Optional submodules to initialize
|
|
|
|
|
* @param null|string $move_path Optional move path after download
|
|
|
|
|
* @param int $retries Number of retry attempts
|
|
|
|
|
* @param int $lock_as Lock type constant
|
2023-03-15 20:40:49 +08:00
|
|
|
*/
|
2025-07-21 23:21:00 +08:00
|
|
|
public static function downloadGit(string $name, string $url, string $branch, ?array $submodules = null, ?string $move_path = null, int $retries = 0, int $lock_as = SPC_DOWNLOAD_SOURCE): void
|
2023-03-18 18:26:18 +08:00
|
|
|
{
|
2024-01-10 21:08:25 +08:00
|
|
|
$download_path = FileSystem::convertPath(DOWNLOAD_PATH . "/{$name}");
|
2023-03-18 18:26:18 +08:00
|
|
|
if (file_exists($download_path)) {
|
2023-04-30 12:42:19 +08:00
|
|
|
FileSystem::removeDir($download_path);
|
2023-03-18 18:26:18 +08:00
|
|
|
}
|
2023-04-30 12:42:19 +08:00
|
|
|
logger()->debug("cloning {$name} source");
|
2025-07-21 23:21:00 +08:00
|
|
|
|
|
|
|
|
$quiet = !defined('DEBUG_MODE') ? '-q --quiet' : '';
|
|
|
|
|
$git = SPC_GIT_EXEC;
|
|
|
|
|
$shallow = defined('GIT_SHALLOW_CLONE') ? '--depth 1 --single-branch' : '';
|
2025-07-21 23:29:38 +08:00
|
|
|
$recursive = ($submodules === null) ? '--recursive' : '';
|
2025-07-21 23:21:00 +08:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
self::registerCancelEvent(function () use ($download_path) {
|
|
|
|
|
if (is_dir($download_path)) {
|
|
|
|
|
logger()->warning('Removing path ' . $download_path);
|
|
|
|
|
FileSystem::removeDir($download_path);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
f_passthru("{$git} clone {$quiet} --config core.autocrlf=false --branch \"{$branch}\" {$shallow} {$recursive} \"{$url}\" \"{$download_path}\"");
|
|
|
|
|
if ($submodules !== null) {
|
|
|
|
|
foreach ($submodules as $submodule) {
|
2025-07-21 23:33:20 +08:00
|
|
|
f_passthru("cd \"{$download_path}\" && {$git} submodule update --init " . escapeshellarg($submodule));
|
2025-07-21 23:21:00 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-06 20:43:23 +08:00
|
|
|
} catch (SPCException $e) {
|
2023-04-30 12:42:19 +08:00
|
|
|
if (is_dir($download_path)) {
|
|
|
|
|
FileSystem::removeDir($download_path);
|
|
|
|
|
}
|
2024-03-10 16:35:19 +08:00
|
|
|
if ($e->getCode() === 2 || $e->getCode() === -1073741510) {
|
2025-08-06 20:43:23 +08:00
|
|
|
throw new InterruptException('Keyboard interrupted, download failed !');
|
2024-03-10 16:23:30 +08:00
|
|
|
}
|
2025-04-22 14:34:43 +07:00
|
|
|
if ($retries > 0) {
|
2025-07-21 23:21:00 +08:00
|
|
|
self::downloadGit($name, $url, $branch, $submodules, $move_path, $retries - 1, $lock_as);
|
2024-03-10 16:23:30 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
throw $e;
|
|
|
|
|
} finally {
|
|
|
|
|
self::unregisterCancelEvent();
|
|
|
|
|
}
|
2023-04-30 12:42:19 +08:00
|
|
|
// Lock
|
|
|
|
|
logger()->debug("Locking git source {$name}");
|
2025-06-18 14:05:43 +08:00
|
|
|
LockFile::lockSource($name, ['source_type' => SPC_SOURCE_GIT, 'dirname' => $name, 'move_path' => $move_path, 'lock_as' => $lock_as]);
|
2023-04-30 12:42:19 +08:00
|
|
|
|
|
|
|
|
/*
|
2023-03-18 18:26:18 +08:00
|
|
|
// 复制目录过去
|
|
|
|
|
if ($path !== $download_path) {
|
|
|
|
|
$dst_path = FileSystem::convertPath($path);
|
|
|
|
|
$src_path = FileSystem::convertPath($download_path);
|
|
|
|
|
switch (PHP_OS_FAMILY) {
|
|
|
|
|
case 'Windows':
|
|
|
|
|
f_passthru('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i');
|
|
|
|
|
break;
|
|
|
|
|
case 'Linux':
|
|
|
|
|
case 'Darwin':
|
|
|
|
|
f_passthru('cp -r "' . $src_path . '" "' . $dst_path . '"');
|
|
|
|
|
break;
|
|
|
|
|
}
|
2023-04-30 12:42:19 +08:00
|
|
|
}*/
|
2023-03-18 18:26:18 +08:00
|
|
|
}
|
|
|
|
|
|
2024-03-10 10:58:58 +08:00
|
|
|
/**
|
2025-03-30 15:17:09 +08:00
|
|
|
* @param string $name Package name
|
|
|
|
|
* @param null|array{
|
|
|
|
|
* type: string,
|
|
|
|
|
* repo: ?string,
|
|
|
|
|
* url: ?string,
|
|
|
|
|
* rev: ?string,
|
|
|
|
|
* path: ?string,
|
|
|
|
|
* filename: ?string,
|
|
|
|
|
* match: ?string,
|
|
|
|
|
* prefer-stable: ?bool,
|
|
|
|
|
* extract-files: ?array<string, string>
|
|
|
|
|
* } $pkg Package config
|
2025-08-06 20:17:26 +08:00
|
|
|
* @param bool $force Download all the time even if it exists
|
2024-03-10 10:58:58 +08:00
|
|
|
*/
|
2024-02-18 13:54:06 +08:00
|
|
|
public static function downloadPackage(string $name, ?array $pkg = null, bool $force = false): void
|
|
|
|
|
{
|
|
|
|
|
if ($pkg === null) {
|
|
|
|
|
$pkg = Config::getPkg($name);
|
|
|
|
|
}
|
|
|
|
|
if ($pkg === null) {
|
|
|
|
|
logger()->warning('Package {name} unknown. Skipping.', ['name' => $name]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!is_dir(DOWNLOAD_PATH)) {
|
|
|
|
|
FileSystem::createDir(DOWNLOAD_PATH);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-30 23:27:43 +08:00
|
|
|
if (self::isAlreadyDownloaded($name, $force, SPC_DOWNLOAD_PACKAGE)) {
|
2025-03-30 16:53:14 +08:00
|
|
|
return;
|
2024-02-18 13:54:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
switch ($pkg['type']) {
|
|
|
|
|
case 'bitbuckettag': // BitBucket Tag
|
|
|
|
|
[$url, $filename] = self::getLatestBitbucketTag($name, $pkg);
|
2025-03-30 23:27:43 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE);
|
2024-02-18 13:54:06 +08:00
|
|
|
break;
|
|
|
|
|
case 'ghtar': // GitHub Release (tar)
|
|
|
|
|
[$url, $filename] = self::getLatestGithubTarball($name, $pkg);
|
2025-04-24 14:21:22 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, hooks: [[CurlHook::class, 'setupGithubToken']]);
|
2024-02-18 13:54:06 +08:00
|
|
|
break;
|
|
|
|
|
case 'ghtagtar': // GitHub Tag (tar)
|
|
|
|
|
[$url, $filename] = self::getLatestGithubTarball($name, $pkg, 'tags');
|
2025-04-24 14:21:22 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, hooks: [[CurlHook::class, 'setupGithubToken']]);
|
2024-02-18 13:54:06 +08:00
|
|
|
break;
|
|
|
|
|
case 'ghrel': // GitHub Release (uploaded)
|
|
|
|
|
[$url, $filename] = self::getLatestGithubRelease($name, $pkg);
|
2025-04-24 14:21:22 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]);
|
2024-02-18 13:54:06 +08:00
|
|
|
break;
|
|
|
|
|
case 'filelist': // Basic File List (regex based crawler)
|
|
|
|
|
[$url, $filename] = self::getFromFileList($name, $pkg);
|
2025-03-30 23:27:43 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE);
|
2024-02-18 13:54:06 +08:00
|
|
|
break;
|
|
|
|
|
case 'url': // Direct download URL
|
|
|
|
|
$url = $pkg['url'];
|
|
|
|
|
$filename = $pkg['filename'] ?? basename($pkg['url']);
|
2025-03-30 23:27:43 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE);
|
2024-02-18 13:54:06 +08:00
|
|
|
break;
|
|
|
|
|
case 'git': // Git repo
|
2024-03-10 16:23:30 +08:00
|
|
|
self::downloadGit(
|
|
|
|
|
$name,
|
|
|
|
|
$pkg['url'],
|
|
|
|
|
$pkg['rev'],
|
2025-07-21 23:21:00 +08:00
|
|
|
$pkg['submodules'] ?? null,
|
2024-03-10 16:23:30 +08:00
|
|
|
$pkg['extract'] ?? null,
|
2025-04-22 12:23:47 +07:00
|
|
|
self::getRetryAttempts(),
|
2025-03-30 23:27:43 +08:00
|
|
|
SPC_DOWNLOAD_PRE_BUILT
|
2024-03-10 16:23:30 +08:00
|
|
|
);
|
2024-02-18 13:54:06 +08:00
|
|
|
break;
|
2025-06-14 16:09:48 +08:00
|
|
|
case 'local':
|
|
|
|
|
// Local directory, do nothing, just lock it
|
|
|
|
|
logger()->debug("Locking local source {$name}");
|
2025-06-18 14:05:43 +08:00
|
|
|
LockFile::lockSource($name, [
|
2025-06-14 16:09:48 +08:00
|
|
|
'source_type' => SPC_SOURCE_LOCAL,
|
|
|
|
|
'dirname' => $pkg['dirname'],
|
|
|
|
|
'move_path' => $pkg['extract'] ?? null,
|
|
|
|
|
'lock_as' => SPC_DOWNLOAD_PACKAGE,
|
|
|
|
|
]);
|
|
|
|
|
break;
|
2024-02-18 13:54:06 +08:00
|
|
|
case 'custom': // Custom download method, like API-based download or other
|
2025-06-18 20:55:24 +08:00
|
|
|
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg');
|
2025-07-25 11:29:53 +08:00
|
|
|
if (isset($pkg['func']) && is_callable($pkg['func'])) {
|
|
|
|
|
$pkg['name'] = $name;
|
|
|
|
|
$pkg['func']($force, $pkg, SPC_DOWNLOAD_PACKAGE);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-02-18 13:54:06 +08:00
|
|
|
foreach ($classes as $class) {
|
2025-06-18 20:55:24 +08:00
|
|
|
if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) {
|
|
|
|
|
$cls = new $class();
|
|
|
|
|
if (in_array($name, $cls->getSupportName())) {
|
|
|
|
|
(new $class())->fetch($name, $force, $pkg);
|
2025-06-26 12:29:58 +07:00
|
|
|
break;
|
2025-06-18 20:55:24 +08:00
|
|
|
}
|
2024-02-18 13:54:06 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new DownloaderException('unknown source type: ' . $pkg['type']);
|
|
|
|
|
}
|
2025-08-06 20:43:23 +08:00
|
|
|
} catch (\Throwable $e) {
|
2024-02-18 13:54:06 +08:00
|
|
|
// Because sometimes files downloaded through the command line are not automatically deleted after a failure.
|
|
|
|
|
// Here we need to manually delete the file if it is detected to exist.
|
|
|
|
|
if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) {
|
|
|
|
|
logger()->warning('Deleting download file: ' . $filename);
|
|
|
|
|
unlink(DOWNLOAD_PATH . '/' . $filename);
|
|
|
|
|
}
|
|
|
|
|
throw new DownloaderException('Download failed! ' . $e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-15 20:40:49 +08:00
|
|
|
/**
|
2025-07-29 11:08:53 +08:00
|
|
|
* Download source
|
2023-03-15 20:40:49 +08:00
|
|
|
*
|
2025-03-30 15:17:09 +08:00
|
|
|
* @param string $name source name
|
|
|
|
|
* @param null|array{
|
|
|
|
|
* type: string,
|
|
|
|
|
* repo: ?string,
|
|
|
|
|
* url: ?string,
|
|
|
|
|
* rev: ?string,
|
|
|
|
|
* path: ?string,
|
|
|
|
|
* filename: ?string,
|
|
|
|
|
* match: ?string,
|
|
|
|
|
* prefer-stable: ?bool,
|
|
|
|
|
* provide-pre-built: ?bool,
|
|
|
|
|
* license: array{
|
|
|
|
|
* type: string,
|
|
|
|
|
* path: ?string,
|
|
|
|
|
* text: ?string
|
|
|
|
|
* }
|
|
|
|
|
* } $source source meta info: [type, path, rev, url, filename, regex, license]
|
2025-08-06 20:17:26 +08:00
|
|
|
* @param bool $force Whether to force download (default: false)
|
|
|
|
|
* @param int $download_as Lock source type (default: SPC_LOCK_SOURCE)
|
2023-03-15 20:40:49 +08:00
|
|
|
*/
|
2025-03-30 23:27:43 +08:00
|
|
|
public static function downloadSource(string $name, ?array $source = null, bool $force = false, int $download_as = SPC_DOWNLOAD_SOURCE): void
|
2023-03-15 20:40:49 +08:00
|
|
|
{
|
2023-04-30 12:42:19 +08:00
|
|
|
if ($source === null) {
|
|
|
|
|
$source = Config::getSource($name);
|
|
|
|
|
}
|
2023-08-21 22:16:42 +02:00
|
|
|
if ($source === null) {
|
|
|
|
|
logger()->warning('Source {name} unknown. Skipping.', ['name' => $name]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-22 21:33:09 +08:00
|
|
|
if (!is_dir(DOWNLOAD_PATH)) {
|
|
|
|
|
FileSystem::createDir(DOWNLOAD_PATH);
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-30 12:42:19 +08:00
|
|
|
// load lock file
|
2025-03-30 16:53:14 +08:00
|
|
|
if (self::isAlreadyDownloaded($name, $force, $download_as)) {
|
|
|
|
|
return;
|
2023-03-15 20:40:49 +08:00
|
|
|
}
|
2023-04-30 12:42:19 +08:00
|
|
|
|
2023-03-15 20:40:49 +08:00
|
|
|
try {
|
|
|
|
|
switch ($source['type']) {
|
2023-08-20 19:51:45 +08:00
|
|
|
case 'bitbuckettag': // BitBucket Tag
|
2023-03-15 20:40:49 +08:00
|
|
|
[$url, $filename] = self::getLatestBitbucketTag($name, $source);
|
2025-03-30 16:53:14 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as);
|
2023-03-15 20:40:49 +08:00
|
|
|
break;
|
2023-08-20 19:51:45 +08:00
|
|
|
case 'ghtar': // GitHub Release (tar)
|
2023-03-15 20:40:49 +08:00
|
|
|
[$url, $filename] = self::getLatestGithubTarball($name, $source);
|
2025-04-24 14:21:22 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]);
|
2023-03-15 20:40:49 +08:00
|
|
|
break;
|
2023-08-20 19:51:45 +08:00
|
|
|
case 'ghtagtar': // GitHub Tag (tar)
|
2023-03-15 20:40:49 +08:00
|
|
|
[$url, $filename] = self::getLatestGithubTarball($name, $source, 'tags');
|
2025-04-24 14:21:22 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]);
|
2023-03-15 20:40:49 +08:00
|
|
|
break;
|
2023-08-20 19:51:45 +08:00
|
|
|
case 'ghrel': // GitHub Release (uploaded)
|
2023-03-15 20:40:49 +08:00
|
|
|
[$url, $filename] = self::getLatestGithubRelease($name, $source);
|
2025-04-24 14:21:22 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]);
|
2023-03-15 20:40:49 +08:00
|
|
|
break;
|
2023-08-20 19:51:45 +08:00
|
|
|
case 'filelist': // Basic File List (regex based crawler)
|
2023-03-15 20:40:49 +08:00
|
|
|
[$url, $filename] = self::getFromFileList($name, $source);
|
2025-03-30 16:53:14 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as);
|
2023-03-15 20:40:49 +08:00
|
|
|
break;
|
2023-08-20 19:51:45 +08:00
|
|
|
case 'url': // Direct download URL
|
2023-03-15 20:40:49 +08:00
|
|
|
$url = $source['url'];
|
|
|
|
|
$filename = $source['filename'] ?? basename($source['url']);
|
2025-03-30 16:53:14 +08:00
|
|
|
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as);
|
2023-03-15 20:40:49 +08:00
|
|
|
break;
|
2023-08-20 19:51:45 +08:00
|
|
|
case 'git': // Git repo
|
2024-03-10 16:23:30 +08:00
|
|
|
self::downloadGit(
|
|
|
|
|
$name,
|
|
|
|
|
$source['url'],
|
|
|
|
|
$source['rev'],
|
2025-07-21 23:21:00 +08:00
|
|
|
$source['submodules'] ?? null,
|
2024-03-10 16:23:30 +08:00
|
|
|
$source['path'] ?? null,
|
2025-04-22 12:23:47 +07:00
|
|
|
self::getRetryAttempts(),
|
2025-03-30 16:53:14 +08:00
|
|
|
$download_as
|
2024-03-10 16:23:30 +08:00
|
|
|
);
|
2023-03-15 20:40:49 +08:00
|
|
|
break;
|
2025-06-14 16:09:48 +08:00
|
|
|
case 'local':
|
|
|
|
|
// Local directory, do nothing, just lock it
|
|
|
|
|
logger()->debug("Locking local source {$name}");
|
2025-06-18 14:05:43 +08:00
|
|
|
LockFile::lockSource($name, [
|
2025-06-14 16:09:48 +08:00
|
|
|
'source_type' => SPC_SOURCE_LOCAL,
|
|
|
|
|
'dirname' => $source['dirname'],
|
|
|
|
|
'move_path' => $source['extract'] ?? null,
|
|
|
|
|
'lock_as' => $download_as,
|
|
|
|
|
]);
|
|
|
|
|
break;
|
2023-08-20 19:51:45 +08:00
|
|
|
case 'custom': // Custom download method, like API-based download or other
|
2025-04-22 20:25:44 +08:00
|
|
|
if (isset($source['func']) && is_callable($source['func'])) {
|
|
|
|
|
$source['name'] = $name;
|
|
|
|
|
$source['func']($force, $source, $download_as);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-06-20 14:46:08 +08:00
|
|
|
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source');
|
2023-04-09 12:11:14 +08:00
|
|
|
foreach ($classes as $class) {
|
|
|
|
|
if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) {
|
2025-03-30 16:53:14 +08:00
|
|
|
(new $class())->fetch($force, $source, $download_as);
|
2023-04-30 12:42:19 +08:00
|
|
|
break;
|
2023-04-09 12:11:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
2023-03-15 20:40:49 +08:00
|
|
|
default:
|
|
|
|
|
throw new DownloaderException('unknown source type: ' . $source['type']);
|
|
|
|
|
}
|
2025-08-06 20:43:23 +08:00
|
|
|
} catch (\Throwable $e) {
|
2023-08-20 19:51:45 +08:00
|
|
|
// Because sometimes files downloaded through the command line are not automatically deleted after a failure.
|
|
|
|
|
// Here we need to manually delete the file if it is detected to exist.
|
2023-03-15 20:40:49 +08:00
|
|
|
if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) {
|
|
|
|
|
logger()->warning('Deleting download file: ' . $filename);
|
|
|
|
|
unlink(DOWNLOAD_PATH . '/' . $filename);
|
|
|
|
|
}
|
2023-10-23 20:12:47 +08:00
|
|
|
throw new DownloaderException('Download failed! ' . $e->getMessage());
|
2023-03-15 20:40:49 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2023-08-20 19:51:45 +08:00
|
|
|
* Use curl command to get http response
|
2023-03-15 20:40:49 +08:00
|
|
|
*
|
2025-08-06 20:17:26 +08:00
|
|
|
* @param string $url Target URL
|
|
|
|
|
* @param string $method HTTP method (GET, POST, etc.)
|
|
|
|
|
* @param array $headers HTTP headers
|
|
|
|
|
* @param array $hooks Curl hooks
|
|
|
|
|
* @param int $retries Number of retry attempts
|
|
|
|
|
* @return string Response body
|
2023-03-15 20:40:49 +08:00
|
|
|
*/
|
2025-04-22 12:23:47 +07:00
|
|
|
public static function curlExec(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0): string
|
2023-03-15 20:40:49 +08:00
|
|
|
{
|
|
|
|
|
foreach ($hooks as $hook) {
|
|
|
|
|
$hook($method, $url, $headers);
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-22 14:34:43 +07:00
|
|
|
FileSystem::findCommandPath('curl');
|
|
|
|
|
|
|
|
|
|
$methodArg = match ($method) {
|
|
|
|
|
'GET' => '',
|
|
|
|
|
'HEAD' => '-I',
|
|
|
|
|
default => "-X \"{$method}\"",
|
|
|
|
|
};
|
|
|
|
|
$headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
|
|
|
|
|
$retry = $retries > 0 ? "--retry {$retries}" : '';
|
|
|
|
|
$cmd = SPC_CURL_EXEC . " -sfSL {$retry} {$methodArg} {$headerArg} \"{$url}\"";
|
|
|
|
|
if (getenv('CACHE_API_EXEC') === 'yes') {
|
|
|
|
|
if (!file_exists(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache'))) {
|
|
|
|
|
$cache = [];
|
|
|
|
|
} else {
|
|
|
|
|
$cache = json_decode(file_get_contents(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache')), true);
|
|
|
|
|
}
|
|
|
|
|
if (isset($cache[$cmd]) && $cache[$cmd]['expire'] >= time()) {
|
2023-03-19 01:16:54 +08:00
|
|
|
return $cache[$cmd]['cache'];
|
|
|
|
|
}
|
|
|
|
|
f_exec($cmd, $output, $ret);
|
2024-03-10 16:35:19 +08:00
|
|
|
if ($ret === 2 || $ret === -1073741510) {
|
2025-08-06 20:43:23 +08:00
|
|
|
throw new InterruptException(sprintf('Canceled fetching "%s"', $url));
|
2024-03-10 16:23:30 +08:00
|
|
|
}
|
2023-03-19 01:16:54 +08:00
|
|
|
if ($ret !== 0) {
|
2025-06-11 14:20:24 +02:00
|
|
|
throw new DownloaderException(sprintf('Failed to fetch "%s"', $url));
|
2023-03-19 01:16:54 +08:00
|
|
|
}
|
2025-04-22 14:34:43 +07:00
|
|
|
$cache[$cmd]['cache'] = implode("\n", $output);
|
|
|
|
|
$cache[$cmd]['expire'] = time() + 3600;
|
|
|
|
|
file_put_contents(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache'), json_encode($cache));
|
|
|
|
|
return $cache[$cmd]['cache'];
|
|
|
|
|
}
|
|
|
|
|
f_exec($cmd, $output, $ret);
|
|
|
|
|
if ($ret === 2 || $ret === -1073741510) {
|
2025-08-06 20:43:23 +08:00
|
|
|
throw new InterruptException(sprintf('Canceled fetching "%s"', $url));
|
2025-04-22 14:34:43 +07:00
|
|
|
}
|
|
|
|
|
if ($ret !== 0) {
|
2025-06-11 14:20:24 +02:00
|
|
|
throw new DownloaderException(sprintf('Failed to fetch "%s"', $url));
|
2023-03-15 20:40:49 +08:00
|
|
|
}
|
2025-04-22 14:34:43 +07:00
|
|
|
return implode("\n", $output);
|
2023-03-15 20:40:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2023-08-20 19:51:45 +08:00
|
|
|
* Use curl to download sources from url
|
2023-03-15 20:40:49 +08:00
|
|
|
*
|
2025-08-06 20:17:26 +08:00
|
|
|
* @param string $url Download URL
|
|
|
|
|
* @param string $path Target file path
|
|
|
|
|
* @param string $method HTTP method
|
|
|
|
|
* @param array $headers HTTP headers
|
|
|
|
|
* @param array $hooks Curl hooks
|
|
|
|
|
* @param int $retries Number of retry attempts
|
2023-03-15 20:40:49 +08:00
|
|
|
*/
|
2025-04-22 14:34:43 +07:00
|
|
|
public static function curlDown(string $url, string $path, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0): void
|
2023-03-15 20:40:49 +08:00
|
|
|
{
|
2024-03-10 16:23:30 +08:00
|
|
|
$used_headers = $headers;
|
2023-03-15 20:40:49 +08:00
|
|
|
foreach ($hooks as $hook) {
|
2024-03-10 16:23:30 +08:00
|
|
|
$hook($method, $url, $used_headers);
|
2023-03-15 20:40:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$methodArg = match ($method) {
|
|
|
|
|
'GET' => '',
|
|
|
|
|
'HEAD' => '-I',
|
|
|
|
|
default => "-X \"{$method}\"",
|
|
|
|
|
};
|
2024-03-10 16:23:30 +08:00
|
|
|
$headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $used_headers));
|
2023-03-15 20:40:49 +08:00
|
|
|
$check = !defined('DEBUG_MODE') ? 's' : '#';
|
2025-04-22 14:34:43 +07:00
|
|
|
$retry = $retries > 0 ? "--retry {$retries}" : '';
|
|
|
|
|
$cmd = SPC_CURL_EXEC . " -{$check}fSL {$retry} -o \"{$path}\" {$methodArg} {$headerArg} \"{$url}\"";
|
2025-04-22 14:44:57 +07:00
|
|
|
try {
|
|
|
|
|
f_passthru($cmd);
|
2025-08-06 20:43:23 +08:00
|
|
|
} catch (\Throwable $e) {
|
2025-04-22 14:44:57 +07:00
|
|
|
if ($e->getCode() === 2 || $e->getCode() === -1073741510) {
|
2025-08-06 20:43:23 +08:00
|
|
|
throw new InterruptException('Keyboard interrupted, download failed !');
|
2025-04-22 14:44:57 +07:00
|
|
|
}
|
|
|
|
|
throw $e;
|
|
|
|
|
}
|
2023-03-15 20:40:49 +08:00
|
|
|
}
|
2024-01-10 21:08:25 +08:00
|
|
|
|
2025-07-29 11:08:53 +08:00
|
|
|
/**
|
|
|
|
|
* Get pre-built lock name from source
|
|
|
|
|
*
|
|
|
|
|
* @param string $source Source name
|
|
|
|
|
* @return string Lock name
|
|
|
|
|
*/
|
2025-03-30 20:16:41 +08:00
|
|
|
public static function getPreBuiltLockName(string $source): string
|
2025-03-30 16:53:14 +08:00
|
|
|
{
|
2025-06-28 16:36:05 +08:00
|
|
|
$os_family = PHP_OS_FAMILY;
|
|
|
|
|
$gnu_arch = getenv('GNU_ARCH') ?: 'unknown';
|
2025-06-29 16:00:17 +08:00
|
|
|
$libc = SPCTarget::getLibc();
|
|
|
|
|
$libc_version = SPCTarget::getLibcVersion() ?? 'default';
|
2025-06-28 16:36:05 +08:00
|
|
|
|
|
|
|
|
return "{$source}-{$os_family}-{$gnu_arch}-{$libc}-{$libc_version}";
|
2025-03-30 16:53:14 +08:00
|
|
|
}
|
|
|
|
|
|
2025-07-29 11:08:53 +08:00
|
|
|
/**
|
|
|
|
|
* Get default alternative source
|
|
|
|
|
*
|
|
|
|
|
* @param string $source_name Source name
|
|
|
|
|
* @return array Alternative source configuration
|
|
|
|
|
*/
|
2025-07-24 00:42:30 +08:00
|
|
|
public static function getDefaultAlternativeSource(string $source_name): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'type' => 'custom',
|
|
|
|
|
'func' => function (bool $force, array $source, int $download_as) use ($source_name) {
|
|
|
|
|
logger()->debug("Fetching alternative source for {$source_name}");
|
|
|
|
|
// get from dl.static-php.dev
|
|
|
|
|
$url = "https://dl.static-php.dev/static-php-cli/deps/spc-download-mirror/{$source_name}/?format=json";
|
|
|
|
|
$json = json_decode(Downloader::curlExec(url: $url, retries: intval(getenv('SPC_DOWNLOAD_RETRIES') ?: 0)), true);
|
|
|
|
|
if (!is_array($json)) {
|
2025-08-06 20:43:23 +08:00
|
|
|
throw new DownloaderException('failed http fetch');
|
2025-07-24 00:42:30 +08:00
|
|
|
}
|
|
|
|
|
$item = $json[0] ?? null;
|
|
|
|
|
if ($item === null) {
|
2025-08-06 20:43:23 +08:00
|
|
|
throw new DownloaderException('failed to parse json');
|
2025-07-24 00:42:30 +08:00
|
|
|
}
|
|
|
|
|
$full_url = 'https://dl.static-php.dev' . $item['full_path'];
|
|
|
|
|
$filename = basename($item['full_path']);
|
|
|
|
|
Downloader::downloadFile($source_name, $full_url, $filename, $source['path'] ?? null, $download_as);
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-10 21:08:25 +08:00
|
|
|
/**
|
|
|
|
|
* Register CTRL+C event for different OS.
|
|
|
|
|
*
|
|
|
|
|
* @param callable $callback callback function
|
|
|
|
|
*/
|
|
|
|
|
private static function registerCancelEvent(callable $callback): void
|
|
|
|
|
{
|
|
|
|
|
if (PHP_OS_FAMILY === 'Windows') {
|
|
|
|
|
sapi_windows_set_ctrl_handler($callback);
|
|
|
|
|
} elseif (extension_loaded('pcntl')) {
|
2024-03-10 16:35:19 +08:00
|
|
|
pcntl_signal(2, $callback);
|
2024-01-10 21:08:25 +08:00
|
|
|
} else {
|
|
|
|
|
logger()->debug('You have not enabled `pcntl` extension, cannot prevent download file corruption when Ctrl+C');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Unegister CTRL+C event for different OS.
|
|
|
|
|
*/
|
|
|
|
|
private static function unregisterCancelEvent(): void
|
|
|
|
|
{
|
|
|
|
|
if (PHP_OS_FAMILY === 'Windows') {
|
|
|
|
|
sapi_windows_set_ctrl_handler(null);
|
|
|
|
|
} elseif (extension_loaded('pcntl')) {
|
2024-03-10 16:35:19 +08:00
|
|
|
pcntl_signal(2, SIG_IGN);
|
2024-01-10 21:08:25 +08:00
|
|
|
}
|
|
|
|
|
}
|
2024-11-09 10:50:08 +08:00
|
|
|
|
2025-04-22 12:23:47 +07:00
|
|
|
private static function getRetryAttempts(): int
|
2024-11-09 10:50:08 +08:00
|
|
|
{
|
2025-04-22 12:23:47 +07:00
|
|
|
return intval(getenv('SPC_DOWNLOAD_RETRIES') ?: 0);
|
2024-11-09 10:50:08 +08:00
|
|
|
}
|
2025-03-30 16:53:14 +08:00
|
|
|
|
2025-03-30 23:27:43 +08:00
|
|
|
private static function isAlreadyDownloaded(string $name, bool $force, int $download_as = SPC_DOWNLOAD_SOURCE): bool
|
2025-03-30 16:53:14 +08:00
|
|
|
{
|
2025-06-18 14:05:43 +08:00
|
|
|
// If the lock file exists, skip downloading for source mode
|
|
|
|
|
$lock_item = LockFile::get($name);
|
|
|
|
|
if (!$force && $download_as === SPC_DOWNLOAD_SOURCE && $lock_item !== null) {
|
|
|
|
|
if (file_exists($path = LockFile::getLockFullPath($lock_item))) {
|
|
|
|
|
logger()->notice("Source [{$name}] already downloaded: {$path}");
|
2025-03-30 16:53:14 +08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-18 14:05:43 +08:00
|
|
|
$lock_name = self::getPreBuiltLockName($name);
|
|
|
|
|
$lock_item = LockFile::get($lock_name);
|
|
|
|
|
if (!$force && $download_as === SPC_DOWNLOAD_PRE_BUILT && $lock_item !== null) {
|
2025-03-30 16:53:14 +08:00
|
|
|
// lock name with env
|
2025-06-18 14:05:43 +08:00
|
|
|
if (file_exists($path = LockFile::getLockFullPath($lock_item))) {
|
|
|
|
|
logger()->notice("Pre-built content [{$name}] already downloaded: {$path}");
|
2025-03-30 16:53:14 +08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-19 11:00:07 +07:00
|
|
|
if (!$force && $download_as === SPC_DOWNLOAD_PACKAGE && $lock_item !== null) {
|
|
|
|
|
if (file_exists($path = LockFile::getLockFullPath($lock_item))) {
|
|
|
|
|
logger()->notice("Source [{$name}] already downloaded: {$path}");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-30 16:53:14 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
2023-03-15 20:40:49 +08:00
|
|
|
}
|