v7.0.0-beta.2 release
This commit is contained in:
1 parent
6595fd0eb5
commit
fa8eba7a0d
1165 files changed
+107118
-762127
No files matched your search
@@ -2,6 +2,11 @@
|
||||
|
||||
namespace App\Extension\Helpers;
|
||||
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
@@ -9,6 +14,9 @@ use Illuminate\Support\Facades\Log;
|
||||
*
|
||||
* 코어 업데이트(CoreUpdateService)와 확장 수동 설치(ModuleService, PluginService, TemplateService)
|
||||
* 모두에서 사용할 수 있는 GitHub API 관련 공통 메서드를 제공합니다.
|
||||
*
|
||||
* 모든 원격 HTTP 호출은 Laravel Http 파사드를 사용합니다.
|
||||
* `allow_url_fopen=Off` 환경(공유 호스팅)에서도 정상 동작합니다.
|
||||
*/
|
||||
class GithubHelper
|
||||
{
|
||||
@@ -43,26 +51,20 @@ class GithubHelper
|
||||
*/
|
||||
public static function checkRepoExists(string $owner, string $repo, string $token = ''): bool
|
||||
{
|
||||
$apiUrl = "https://api.github.com/repos/{$owner}/{$repo}";
|
||||
try {
|
||||
$response = static::request($token, 5)
|
||||
->get("https://api.github.com/repos/{$owner}/{$repo}");
|
||||
} catch (ConnectionException $e) {
|
||||
Log::warning('GitHub 저장소 확인 연결 실패', [
|
||||
'owner' => $owner,
|
||||
'repo' => $repo,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => static::buildHeaders($token),
|
||||
'timeout' => 5,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = @file_get_contents($apiUrl, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$statusCode = static::extractStatusCode($http_response_header ?? []);
|
||||
|
||||
return $statusCode === 200;
|
||||
return $response->successful();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,30 +79,21 @@ class GithubHelper
|
||||
{
|
||||
$apiUrl = "https://api.github.com/repos/{$owner}/{$repo}/releases/latest";
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => static::buildHeaders($token),
|
||||
'timeout' => 10,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
]);
|
||||
try {
|
||||
$response = static::request($token, 10)->get($apiUrl);
|
||||
} catch (ConnectionException $e) {
|
||||
Log::warning('GitHub API 연결 실패', ['url' => $apiUrl, 'error' => $e->getMessage()]);
|
||||
|
||||
$response = @file_get_contents($apiUrl, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
return ['version' => null, 'zipball_url' => null, 'error' => __('common.errors.github_api_failed')];
|
||||
}
|
||||
|
||||
$statusCode = static::extractStatusCode($http_response_header ?? []);
|
||||
|
||||
if ($statusCode !== 200) {
|
||||
if (! $response->successful()) {
|
||||
return ['version' => null, 'zipball_url' => null, 'error' => null];
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
$data = $response->json();
|
||||
|
||||
if (! isset($data['tag_name'])) {
|
||||
if (! is_array($data) || ! isset($data['tag_name'])) {
|
||||
return ['version' => null, 'zipball_url' => null, 'error' => null];
|
||||
}
|
||||
|
||||
@@ -166,7 +159,9 @@ class GithubHelper
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 URL에서 아카이브를 다운로드합니다.
|
||||
* 특정 URL에서 아카이브를 다운로드하여 파일로 저장합니다.
|
||||
*
|
||||
* Http 파사드의 `sink()`를 사용하여 메모리 사용을 최소화합니다.
|
||||
*
|
||||
* @param string $url 다운로드 URL
|
||||
* @param string $savePath 저장할 파일 경로
|
||||
@@ -176,22 +171,100 @@ class GithubHelper
|
||||
*/
|
||||
public static function downloadArchive(string $url, string $savePath, string $token = ''): void
|
||||
{
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => static::buildHeaders($token),
|
||||
'follow_location' => true,
|
||||
'timeout' => 120,
|
||||
],
|
||||
]);
|
||||
try {
|
||||
$response = static::request($token, 120)
|
||||
->sink($savePath)
|
||||
->get($url);
|
||||
} catch (ConnectionException $e) {
|
||||
// 연결 실패 시에도 sink가 생성했을 수 있는 빈/부분 파일 정리
|
||||
if (File::exists($savePath)) {
|
||||
File::delete($savePath);
|
||||
}
|
||||
|
||||
$content = @file_get_contents($url, false, $context);
|
||||
Log::warning('GitHub 아카이브 다운로드 연결 실패', ['url' => $url, 'error' => $e->getMessage()]);
|
||||
|
||||
if ($content === false) {
|
||||
throw new \RuntimeException(__('common.errors.github_archive_download_failed', ['url' => $url]));
|
||||
}
|
||||
|
||||
\Illuminate\Support\Facades\File::put($savePath, $content);
|
||||
if (! $response->successful()) {
|
||||
// sink로 저장된 미완성 파일 정리
|
||||
if (File::exists($savePath)) {
|
||||
File::delete($savePath);
|
||||
}
|
||||
|
||||
throw new \RuntimeException(__('common.errors.github_archive_download_failed', ['url' => $url]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 URL에서 아카이브를 다운로드하여 바이너리 내용을 반환합니다.
|
||||
*
|
||||
* `downloadArchive()`와 달리 파일로 저장하지 않고 메모리로 내용을 반환합니다.
|
||||
* 호출부가 바이너리 내용을 직접 처리해야 하는 경우에만 사용하세요.
|
||||
*
|
||||
* @param string $url 다운로드 URL
|
||||
* @param string $token GitHub Personal Access Token
|
||||
* @return string 다운로드된 바이너리 내용
|
||||
*
|
||||
* @throws \RuntimeException 다운로드 실패 시
|
||||
*/
|
||||
public static function downloadArchiveContent(string $url, string $token = ''): string
|
||||
{
|
||||
try {
|
||||
$response = static::request($token, 120)->get($url);
|
||||
} catch (ConnectionException $e) {
|
||||
Log::warning('GitHub 아카이브 다운로드 연결 실패', ['url' => $url, 'error' => $e->getMessage()]);
|
||||
|
||||
throw new \RuntimeException(__('settings.core_update.download_failed', ['version' => basename($url)]));
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
throw new \RuntimeException(__('settings.core_update.download_failed', ['version' => basename($url)]));
|
||||
}
|
||||
|
||||
return $response->body();
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub 아카이브 URL을 해석합니다 (v 접두사 유/무 모두 시도).
|
||||
*
|
||||
* HEAD 요청으로 200/302 응답이 오는 첫 번째 URL을 반환합니다.
|
||||
*
|
||||
* @param string $owner 저장소 소유자
|
||||
* @param string $repo 저장소 이름
|
||||
* @param string $version 버전 태그 (v 접두사 유/무 무관)
|
||||
* @param string $archiveType 아카이브 타입 (zipball/tarball)
|
||||
* @param string $token GitHub Personal Access Token
|
||||
* @return string|null 유효한 아카이브 URL 또는 null
|
||||
*/
|
||||
public static function resolveArchiveUrl(string $owner, string $repo, string $version, string $archiveType = 'zipball', string $token = ''): ?string
|
||||
{
|
||||
$tagVariants = ["v{$version}", $version];
|
||||
|
||||
foreach ($tagVariants as $tag) {
|
||||
// GitHub API 의 `zipball/{tag}` 엔드포인트는 태그가 존재하지 않아도 무조건 302 로
|
||||
// codeload 에 리다이렉트하며, 실제 다운로드 시점에 codeload 에서 404 가 반환되어
|
||||
// 다운로드 파일이 "404: Not Found" 14 byte 텍스트로 저장되는 불가시 실패가 발생한다.
|
||||
// 따라서 아카이브 URL 반환 전에 `git/refs/tags/{tag}` 로 태그 자체의 존재 여부를
|
||||
// 먼저 검증한다 (GitHub API: 존재=200, 부재=404).
|
||||
$tagCheckUrl = "https://api.github.com/repos/{$owner}/{$repo}/git/refs/tags/{$tag}";
|
||||
|
||||
try {
|
||||
$tagResponse = static::request($token, 10)->get($tagCheckUrl);
|
||||
} catch (ConnectionException $e) {
|
||||
Log::info('GitHub 태그 존재 검증 실패', ['url' => $tagCheckUrl, 'error' => $e->getMessage()]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($tagResponse->status() !== 200) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return "https://api.github.com/repos/{$owner}/{$repo}/{$archiveType}/{$tag}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,25 +288,14 @@ class GithubHelper
|
||||
foreach ($refs as $tryRef) {
|
||||
$url = "https://raw.githubusercontent.com/{$owner}/{$repo}/{$tryRef}/{$filePath}";
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => static::buildHeaders($token),
|
||||
'timeout' => 10,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$content = @file_get_contents($url, false, $context);
|
||||
|
||||
if ($content === false) {
|
||||
try {
|
||||
$response = static::request($token, 10)->get($url);
|
||||
} catch (ConnectionException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$statusCode = static::extractStatusCode($http_response_header ?? []);
|
||||
|
||||
if ($statusCode === 200) {
|
||||
return $content;
|
||||
if ($response->successful()) {
|
||||
return $response->body();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +303,10 @@ class GithubHelper
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub API 요청용 HTTP 헤더를 생성합니다.
|
||||
* GitHub API 요청용 HTTP 헤더를 생성합니다 (file_get_contents 스트림 컨텍스트 호환).
|
||||
*
|
||||
* 레거시 호출부 호환을 위해 배열 형태의 헤더를 반환합니다.
|
||||
* 신규 코드는 `request()`를 사용하세요.
|
||||
*
|
||||
* @param string $token GitHub Personal Access Token (빈 문자열이면 인증 없음)
|
||||
* @return array HTTP 헤더 배열
|
||||
@@ -261,22 +326,27 @@ class GithubHelper
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 응답 헤더에서 상태 코드를 추출합니다.
|
||||
* GitHub API 호출을 위한 표준화된 Http 파사드 PendingRequest를 생성합니다.
|
||||
*
|
||||
* @param array $responseHeaders $http_response_header 배열
|
||||
* @return int HTTP 상태 코드
|
||||
* 모든 원격 HTTP 호출의 단일 진입점입니다. User-Agent, Accept, 토큰 인증,
|
||||
* 타임아웃을 일괄 설정합니다.
|
||||
*
|
||||
* @param string $token GitHub Personal Access Token (빈 문자열이면 인증 없음)
|
||||
* @param int $timeout 읽기 타임아웃(초)
|
||||
*/
|
||||
public static function extractStatusCode(array $responseHeaders): int
|
||||
protected static function request(string $token = '', int $timeout = 10): PendingRequest
|
||||
{
|
||||
$statusCode = 0;
|
||||
$request = Http::withHeaders([
|
||||
'User-Agent' => 'G7',
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
])
|
||||
->timeout($timeout)
|
||||
->connectTimeout(5);
|
||||
|
||||
foreach ($responseHeaders as $header) {
|
||||
if (preg_match('#^HTTP/\S+\s+(\d{3})#', $header, $matches)) {
|
||||
// 리다이렉트 시 마지막 상태 코드를 사용하기 위해 계속 진행
|
||||
$statusCode = (int) $matches[1];
|
||||
}
|
||||
if ($token !== '') {
|
||||
$request = $request->withToken($token);
|
||||
}
|
||||
|
||||
return $statusCode;
|
||||
return $request;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user