mirror of
https://github.com/getgrav/grav.git
synced 2025-12-05 15:29:57 +01:00
1
.gitignore
vendored
1
.gitignore
vendored
@@ -51,3 +51,4 @@ system/templates/testing/*
|
||||
/system/recovery.window
|
||||
tmp/*
|
||||
#needs_fixing.txt
|
||||
/AGENTS.md
|
||||
|
||||
209
bin/build-test-update.php
Executable file
209
bin/build-test-update.php
Executable file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
if (!\defined('GRAV_ROOT')) {
|
||||
\define('GRAV_ROOT', realpath(__DIR__ . '/..') ?: getcwd());
|
||||
}
|
||||
|
||||
if (!\extension_loaded('zip')) {
|
||||
fwrite(STDERR, "The PHP zip extension is required.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$options = getopt('', [
|
||||
'version:',
|
||||
'output::',
|
||||
'port::',
|
||||
'base-url::',
|
||||
'serve',
|
||||
]);
|
||||
|
||||
if (!isset($options['version'])) {
|
||||
fwrite(
|
||||
STDERR,
|
||||
"Usage: php bin/build-test-update.php --version=1.7.999 [--output=tmp/test-gpm] [--port=8043] [--base-url=http://127.0.0.1:8043] [--serve]\n"
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$version = trim((string) $options['version']);
|
||||
if ($version === '') {
|
||||
fwrite(STDERR, "A non-empty --version value is required.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$root = GRAV_ROOT;
|
||||
|
||||
$output = $options['output'] ?? $root . '/tmp/test-gpm';
|
||||
if (!str_starts_with($output, DIRECTORY_SEPARATOR)) {
|
||||
$output = $root . '/' . ltrim($output, '/');
|
||||
}
|
||||
$output = rtrim($output, DIRECTORY_SEPARATOR);
|
||||
|
||||
$defaultPort = isset($options['port']) ? (int) $options['port'] : 8043;
|
||||
$baseUrl = $options['base-url'] ?? sprintf('http://127.0.0.1:%d', $defaultPort);
|
||||
$serve = array_key_exists('serve', $options);
|
||||
|
||||
Folder::create($output);
|
||||
|
||||
$downloadName = sprintf('grav-update-%s.zip', $version);
|
||||
$zipPath = $output . '/' . $downloadName;
|
||||
$jsonPath = $output . '/grav.json';
|
||||
$zipPrefix = 'grav-update/';
|
||||
|
||||
$excludeDirs = [
|
||||
'.build',
|
||||
'.crush',
|
||||
'.ddev',
|
||||
'.git',
|
||||
'.github',
|
||||
'.gitlab',
|
||||
'.circleci',
|
||||
'.idea',
|
||||
'.vscode',
|
||||
'.pytest_cache',
|
||||
'backup',
|
||||
'cache',
|
||||
'images',
|
||||
'logs',
|
||||
'node_modules',
|
||||
'tests',
|
||||
'tmp',
|
||||
'user',
|
||||
];
|
||||
|
||||
$excludeFiles = [
|
||||
'.htaccess',
|
||||
'.DS_Store',
|
||||
'robots.txt',
|
||||
];
|
||||
|
||||
$directory = new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS);
|
||||
$filtered = new RecursiveCallbackFilterIterator(
|
||||
$directory,
|
||||
function (SplFileInfo $current) use ($root, $excludeDirs, $excludeFiles): bool {
|
||||
$relative = ltrim(str_replace($root, '', $current->getPathname()), DIRECTORY_SEPARATOR);
|
||||
$relative = str_replace('\\', '/', $relative);
|
||||
|
||||
if ($relative === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_contains($relative, '..')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($excludeDirs as $prefix) {
|
||||
$prefix = trim($prefix, '/');
|
||||
if ($prefix === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($relative === $prefix || str_starts_with($relative, $prefix . '/')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($current->getFilename(), $excludeFiles, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
throw new RuntimeException(sprintf('Unable to open archive at %s', $zipPath));
|
||||
}
|
||||
|
||||
$zip->addEmptyDir($zipPrefix);
|
||||
|
||||
$iterator = new RecursiveIteratorIterator($filtered, RecursiveIteratorIterator::SELF_FIRST);
|
||||
/** @var SplFileInfo $fileInfo */
|
||||
foreach ($iterator as $fileInfo) {
|
||||
$fullPath = $fileInfo->getPathname();
|
||||
$relative = ltrim(str_replace($root, '', $fullPath), DIRECTORY_SEPARATOR);
|
||||
$relative = str_replace('\\', '/', $relative);
|
||||
$targetPath = $zipPrefix . $relative;
|
||||
|
||||
if ($fileInfo->isDir()) {
|
||||
$zip->addEmptyDir(rtrim($targetPath, '/') . '/');
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($fileInfo->isLink()) {
|
||||
$target = readlink($fullPath);
|
||||
$zip->addFromString($targetPath, $target === false ? '' : $target);
|
||||
$zip->setExternalAttributesName($targetPath, ZipArchive::OPSYS_UNIX, 0120000 << 16);
|
||||
continue;
|
||||
}
|
||||
|
||||
$zip->addFile($fullPath, $targetPath);
|
||||
|
||||
$perms = @fileperms($fullPath);
|
||||
if ($perms !== false) {
|
||||
$zip->setExternalAttributesName($targetPath, ZipArchive::OPSYS_UNIX, ($perms & 0xFFFF) << 16);
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
$size = filesize($zipPath);
|
||||
$sha256 = hash_file('sha256', $zipPath);
|
||||
$timestamp = date('c');
|
||||
$downloadUrl = rtrim($baseUrl, '/') . '/' . rawurlencode($downloadName);
|
||||
|
||||
$manifest = [
|
||||
'version' => $version,
|
||||
'date' => $timestamp,
|
||||
'min_php' => '8.3.0',
|
||||
'assets' => [
|
||||
'grav-update' => [
|
||||
'name' => $downloadName,
|
||||
'slug' => 'grav-update',
|
||||
'version' => $version,
|
||||
'date' => $timestamp,
|
||||
'testing' => false,
|
||||
'description' => 'Local test update package generated for safe-upgrade validation.',
|
||||
'download' => $downloadUrl,
|
||||
'size' => $size,
|
||||
'checksum' => 'sha256:' . $sha256,
|
||||
'sha256' => $sha256,
|
||||
'host' => parse_url($downloadUrl, PHP_URL_HOST),
|
||||
],
|
||||
],
|
||||
'changelog' => [
|
||||
$version => [
|
||||
'date' => $timestamp,
|
||||
'content' => "- Local test update package generated by build-test-update.\n",
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
file_put_contents($jsonPath, json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
|
||||
$manifestUrl = rtrim($baseUrl, '/') . '/grav.json';
|
||||
|
||||
echo "Update package created at: {$zipPath}\n";
|
||||
echo "Manifest written to: {$jsonPath}\n";
|
||||
echo "Manifest URL: {$manifestUrl}\n";
|
||||
echo "Download URL: {$downloadUrl}\n";
|
||||
echo "Archive size: {$size} bytes\n";
|
||||
echo "SHA256: {$sha256}\n";
|
||||
|
||||
if ($serve) {
|
||||
$host = parse_url($baseUrl, PHP_URL_HOST) ?: '127.0.0.1';
|
||||
$port = parse_url($baseUrl, PHP_URL_PORT) ?: $defaultPort;
|
||||
$command = sprintf('php -S %s:%d -t %s', $host, $port, escapeshellarg($output));
|
||||
echo "\nServing files using PHP built-in server. Press Ctrl+C to stop.\n";
|
||||
echo $command . "\n\n";
|
||||
passthru($command);
|
||||
}
|
||||
@@ -23,30 +23,39 @@ use RecursiveIteratorIterator;
|
||||
use FilesystemIterator;
|
||||
use function array_key_exists;
|
||||
use function basename;
|
||||
use function chgrp;
|
||||
use function chmod;
|
||||
use function chown;
|
||||
use function copy;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function filegroup;
|
||||
use function fileowner;
|
||||
use function glob;
|
||||
use function in_array;
|
||||
use function is_dir;
|
||||
use function is_file;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
use function lchgrp;
|
||||
use function lchown;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use function property_exists;
|
||||
use function rename;
|
||||
use function rsort;
|
||||
use function sort;
|
||||
use function time;
|
||||
use function rtrim;
|
||||
use function uniqid;
|
||||
use function trim;
|
||||
use function strpos;
|
||||
use function time;
|
||||
use function trim;
|
||||
use function uniqid;
|
||||
use function unlink;
|
||||
use function ltrim;
|
||||
use function preg_replace;
|
||||
use function posix_getgrgid;
|
||||
use function posix_getpwuid;
|
||||
use const GRAV_ROOT;
|
||||
use const GLOB_ONLYDIR;
|
||||
use const JSON_PRETTY_PRINT;
|
||||
@@ -179,21 +188,26 @@ class SafeUpgradeService
|
||||
$stagingMode = $this->stageExtractedPackage($extractedPath, $packagePath);
|
||||
$this->reportProgress('installing', 'Preparing staged package...', null, ['mode' => $stagingMode]);
|
||||
|
||||
$packageEntries = $this->collectPackageEntries($packagePath);
|
||||
|
||||
$this->carryOverRootDotfiles($packagePath);
|
||||
|
||||
// Ensure ignored directories are replaced with live copies.
|
||||
$this->hydrateIgnoredDirectories($packagePath, $ignores);
|
||||
$this->carryOverRootFiles($packagePath, $ignores);
|
||||
|
||||
$entries = $this->collectPackageEntries($packagePath);
|
||||
if (!$entries) {
|
||||
$snapshotEntries = $this->collectPackageEntries($packagePath);
|
||||
if (!$packageEntries) {
|
||||
throw new RuntimeException('Staged package does not contain any files to promote.');
|
||||
}
|
||||
if (!$snapshotEntries) {
|
||||
$snapshotEntries = $packageEntries;
|
||||
}
|
||||
|
||||
$this->reportProgress('snapshot', 'Creating backup snapshot...', null);
|
||||
$this->createBackupSnapshot($entries, $backupPath);
|
||||
$this->createBackupSnapshot($snapshotEntries, $backupPath);
|
||||
|
||||
$manifest = $this->buildManifest($stageId, $targetVersion, $packagePath, $backupPath, $entries);
|
||||
$manifest = $this->buildManifest($stageId, $targetVersion, $packagePath, $backupPath, $snapshotEntries);
|
||||
$manifestPath = $stagePath . DIRECTORY_SEPARATOR . 'manifest.json';
|
||||
Folder::create(dirname($manifestPath));
|
||||
file_put_contents($manifestPath, json_encode($manifest, JSON_PRETTY_PRINT));
|
||||
@@ -201,9 +215,9 @@ class SafeUpgradeService
|
||||
$this->reportProgress('installing', 'Copying update files...', null);
|
||||
|
||||
try {
|
||||
$this->copyEntries($entries, $packagePath, $this->rootPath, 'installing', 'Deploying');
|
||||
$this->copyEntries($packageEntries, $packagePath, $this->rootPath, 'installing', 'Deploying');
|
||||
} catch (Throwable $e) {
|
||||
$this->copyEntries($entries, $backupPath, $this->rootPath, 'installing', 'Restoring');
|
||||
$this->copyEntries($snapshotEntries, $backupPath, $this->rootPath, 'installing', 'Restoring');
|
||||
throw new RuntimeException('Failed to promote staged Grav release.', 0, $e);
|
||||
}
|
||||
|
||||
@@ -334,6 +348,7 @@ class SafeUpgradeService
|
||||
}
|
||||
|
||||
$destination = $targetBase . DIRECTORY_SEPARATOR . $entry;
|
||||
$metadata = $this->captureEntryMeta($destination);
|
||||
$this->removeEntry($destination);
|
||||
|
||||
if (is_link($source)) {
|
||||
@@ -341,9 +356,13 @@ class SafeUpgradeService
|
||||
if (!@symlink(readlink($source), $destination)) {
|
||||
throw new RuntimeException(sprintf('Failed to replicate symlink "%s".', $source));
|
||||
}
|
||||
$this->applyEntryMeta($destination, $metadata);
|
||||
continue;
|
||||
} elseif (is_dir($source)) {
|
||||
Folder::create(dirname($destination));
|
||||
Folder::rcopy($source, $destination, true);
|
||||
$this->applyEntryMeta($destination, $metadata);
|
||||
continue;
|
||||
} else {
|
||||
Folder::create(dirname($destination));
|
||||
if (!@copy($source, $destination)) {
|
||||
@@ -358,6 +377,8 @@ class SafeUpgradeService
|
||||
@touch($destination, $mtime);
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyEntryMeta($destination, $metadata);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,6 +391,122 @@ class SafeUpgradeService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture ownership and permission data for an existing filesystem entry.
|
||||
*
|
||||
* @param string $path
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function captureEntryMeta(string $path): array
|
||||
{
|
||||
if (!file_exists($path) && !is_link($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$meta = [
|
||||
'link' => is_link($path),
|
||||
];
|
||||
|
||||
$perms = @fileperms($path);
|
||||
if ($perms !== false) {
|
||||
$meta['perms'] = $perms & 0777;
|
||||
}
|
||||
|
||||
$owner = @fileowner($path);
|
||||
if ($owner !== false) {
|
||||
$meta['owner'] = $owner;
|
||||
}
|
||||
|
||||
$group = @filegroup($path);
|
||||
if ($group !== false) {
|
||||
$meta['group'] = $group;
|
||||
}
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reapply ownership and permission data to a copied entry when possible.
|
||||
*
|
||||
* @param string $path
|
||||
* @param array<string, mixed> $meta
|
||||
* @return void
|
||||
*/
|
||||
private function applyEntryMeta(string $path, array $meta): void
|
||||
{
|
||||
if (!$meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($meta['perms'])) {
|
||||
@chmod($path, (int) $meta['perms']);
|
||||
}
|
||||
|
||||
$isLink = !empty($meta['link']);
|
||||
|
||||
if (isset($meta['owner'])) {
|
||||
$owner = $this->resolveOwner($meta['owner']);
|
||||
if ($isLink && function_exists('lchown')) {
|
||||
@lchown($path, $owner);
|
||||
} elseif (!$isLink && function_exists('chown')) {
|
||||
@chown($path, $owner);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($meta['group'])) {
|
||||
$group = $this->resolveGroup($meta['group']);
|
||||
if ($isLink && function_exists('lchgrp')) {
|
||||
@lchgrp($path, $group);
|
||||
} elseif (!$isLink && function_exists('chgrp')) {
|
||||
@chgrp($path, $group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve stored owner identifier to a format accepted by chown/lchown.
|
||||
*
|
||||
* @param int|string $owner
|
||||
* @return int|string
|
||||
*/
|
||||
private function resolveOwner($owner)
|
||||
{
|
||||
if (is_string($owner)) {
|
||||
return $owner;
|
||||
}
|
||||
|
||||
if (function_exists('posix_getpwuid')) {
|
||||
$info = @posix_getpwuid((int) $owner);
|
||||
if (is_array($info) && isset($info['name'])) {
|
||||
return $info['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return (int) $owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve stored group identifier to a format accepted by chgrp/lchgrp.
|
||||
*
|
||||
* @param int|string $group
|
||||
* @return int|string
|
||||
*/
|
||||
private function resolveGroup($group)
|
||||
{
|
||||
if (is_string($group)) {
|
||||
return $group;
|
||||
}
|
||||
|
||||
if (function_exists('posix_getgrgid')) {
|
||||
$info = @posix_getgrgid((int) $group);
|
||||
if (is_array($info) && isset($info['name'])) {
|
||||
return $info['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return (int) $group;
|
||||
}
|
||||
|
||||
public function setProgressCallback(?callable $callback): self
|
||||
{
|
||||
$this->progressCallback = $callback;
|
||||
|
||||
Reference in New Issue
Block a user