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
|
/system/recovery.window
|
||||||
tmp/*
|
tmp/*
|
||||||
#needs_fixing.txt
|
#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 FilesystemIterator;
|
||||||
use function array_key_exists;
|
use function array_key_exists;
|
||||||
use function basename;
|
use function basename;
|
||||||
|
use function chgrp;
|
||||||
|
use function chmod;
|
||||||
|
use function chown;
|
||||||
use function copy;
|
use function copy;
|
||||||
use function count;
|
use function count;
|
||||||
use function dirname;
|
use function dirname;
|
||||||
use function file_get_contents;
|
use function file_get_contents;
|
||||||
use function file_put_contents;
|
use function file_put_contents;
|
||||||
|
use function filegroup;
|
||||||
|
use function fileowner;
|
||||||
use function glob;
|
use function glob;
|
||||||
use function in_array;
|
use function in_array;
|
||||||
use function is_dir;
|
use function is_dir;
|
||||||
use function is_file;
|
use function is_file;
|
||||||
use function json_decode;
|
use function json_decode;
|
||||||
use function json_encode;
|
use function json_encode;
|
||||||
|
use function lchgrp;
|
||||||
|
use function lchown;
|
||||||
use function preg_match;
|
use function preg_match;
|
||||||
|
use function preg_replace;
|
||||||
use function property_exists;
|
use function property_exists;
|
||||||
use function rename;
|
use function rename;
|
||||||
use function rsort;
|
use function rsort;
|
||||||
use function sort;
|
use function sort;
|
||||||
use function time;
|
|
||||||
use function rtrim;
|
use function rtrim;
|
||||||
use function uniqid;
|
|
||||||
use function trim;
|
|
||||||
use function strpos;
|
use function strpos;
|
||||||
|
use function time;
|
||||||
|
use function trim;
|
||||||
|
use function uniqid;
|
||||||
use function unlink;
|
use function unlink;
|
||||||
use function ltrim;
|
use function ltrim;
|
||||||
use function preg_replace;
|
use function posix_getgrgid;
|
||||||
|
use function posix_getpwuid;
|
||||||
use const GRAV_ROOT;
|
use const GRAV_ROOT;
|
||||||
use const GLOB_ONLYDIR;
|
use const GLOB_ONLYDIR;
|
||||||
use const JSON_PRETTY_PRINT;
|
use const JSON_PRETTY_PRINT;
|
||||||
@@ -179,21 +188,26 @@ class SafeUpgradeService
|
|||||||
$stagingMode = $this->stageExtractedPackage($extractedPath, $packagePath);
|
$stagingMode = $this->stageExtractedPackage($extractedPath, $packagePath);
|
||||||
$this->reportProgress('installing', 'Preparing staged package...', null, ['mode' => $stagingMode]);
|
$this->reportProgress('installing', 'Preparing staged package...', null, ['mode' => $stagingMode]);
|
||||||
|
|
||||||
|
$packageEntries = $this->collectPackageEntries($packagePath);
|
||||||
|
|
||||||
$this->carryOverRootDotfiles($packagePath);
|
$this->carryOverRootDotfiles($packagePath);
|
||||||
|
|
||||||
// Ensure ignored directories are replaced with live copies.
|
// Ensure ignored directories are replaced with live copies.
|
||||||
$this->hydrateIgnoredDirectories($packagePath, $ignores);
|
$this->hydrateIgnoredDirectories($packagePath, $ignores);
|
||||||
$this->carryOverRootFiles($packagePath, $ignores);
|
$this->carryOverRootFiles($packagePath, $ignores);
|
||||||
|
|
||||||
$entries = $this->collectPackageEntries($packagePath);
|
$snapshotEntries = $this->collectPackageEntries($packagePath);
|
||||||
if (!$entries) {
|
if (!$packageEntries) {
|
||||||
throw new RuntimeException('Staged package does not contain any files to promote.');
|
throw new RuntimeException('Staged package does not contain any files to promote.');
|
||||||
}
|
}
|
||||||
|
if (!$snapshotEntries) {
|
||||||
|
$snapshotEntries = $packageEntries;
|
||||||
|
}
|
||||||
|
|
||||||
$this->reportProgress('snapshot', 'Creating backup snapshot...', null);
|
$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';
|
$manifestPath = $stagePath . DIRECTORY_SEPARATOR . 'manifest.json';
|
||||||
Folder::create(dirname($manifestPath));
|
Folder::create(dirname($manifestPath));
|
||||||
file_put_contents($manifestPath, json_encode($manifest, JSON_PRETTY_PRINT));
|
file_put_contents($manifestPath, json_encode($manifest, JSON_PRETTY_PRINT));
|
||||||
@@ -201,9 +215,9 @@ class SafeUpgradeService
|
|||||||
$this->reportProgress('installing', 'Copying update files...', null);
|
$this->reportProgress('installing', 'Copying update files...', null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->copyEntries($entries, $packagePath, $this->rootPath, 'installing', 'Deploying');
|
$this->copyEntries($packageEntries, $packagePath, $this->rootPath, 'installing', 'Deploying');
|
||||||
} catch (Throwable $e) {
|
} 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);
|
throw new RuntimeException('Failed to promote staged Grav release.', 0, $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +348,7 @@ class SafeUpgradeService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$destination = $targetBase . DIRECTORY_SEPARATOR . $entry;
|
$destination = $targetBase . DIRECTORY_SEPARATOR . $entry;
|
||||||
|
$metadata = $this->captureEntryMeta($destination);
|
||||||
$this->removeEntry($destination);
|
$this->removeEntry($destination);
|
||||||
|
|
||||||
if (is_link($source)) {
|
if (is_link($source)) {
|
||||||
@@ -341,9 +356,13 @@ class SafeUpgradeService
|
|||||||
if (!@symlink(readlink($source), $destination)) {
|
if (!@symlink(readlink($source), $destination)) {
|
||||||
throw new RuntimeException(sprintf('Failed to replicate symlink "%s".', $source));
|
throw new RuntimeException(sprintf('Failed to replicate symlink "%s".', $source));
|
||||||
}
|
}
|
||||||
|
$this->applyEntryMeta($destination, $metadata);
|
||||||
|
continue;
|
||||||
} elseif (is_dir($source)) {
|
} elseif (is_dir($source)) {
|
||||||
Folder::create(dirname($destination));
|
Folder::create(dirname($destination));
|
||||||
Folder::rcopy($source, $destination, true);
|
Folder::rcopy($source, $destination, true);
|
||||||
|
$this->applyEntryMeta($destination, $metadata);
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
Folder::create(dirname($destination));
|
Folder::create(dirname($destination));
|
||||||
if (!@copy($source, $destination)) {
|
if (!@copy($source, $destination)) {
|
||||||
@@ -358,6 +377,8 @@ class SafeUpgradeService
|
|||||||
@touch($destination, $mtime);
|
@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
|
public function setProgressCallback(?callable $callback): self
|
||||||
{
|
{
|
||||||
$this->progressCallback = $callback;
|
$this->progressCallback = $callback;
|
||||||
|
|||||||
Reference in New Issue
Block a user