Repository navigation
Expand file tree
/
Copy pathCompiledDirectoryRemover.php
More file actions
148 lines (125 loc) · 4.87 KB
/
Copy pathCompiledDirectoryRemover.php
File metadata and controls
148 lines (125 loc) · 4.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<?php
declare(strict_types=1);
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Thelia\Setup;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
/**
* Removes the compiled container or the generated Propel models of the previous
* release before update.php boots on the new code.
*
* On a live shop these directories are written by the web server user, and the
* script often runs as another one. Deleting a file needs write access to the
* directory that holds it, which that user rarely has, while renaming the top
* directory only needs write access to its parent. So the directory is renamed
* first, which is enough to keep the kernel from loading it again, and deleted
* afterwards as far as the permissions allow.
*/
final readonly class CompiledDirectoryRemover
{
public function __construct(
private Filesystem $filesystem = new Filesystem(),
) {
}
/**
* @return string|null the directory where the files that could not be deleted
* were left, null when everything was deleted
*
* @throws \RuntimeException when the directory cannot be moved out of the way,
* in which case nothing was changed
*/
public function remove(string $directory): ?string
{
$movedDirectory = $directory.'.previous-'.bin2hex(random_bytes(4));
$renameError = null;
set_error_handler(static function (int $type, string $message) use (&$renameError): bool {
$renameError = $message;
return true;
});
try {
$moved = rename($directory, $movedDirectory);
} finally {
restore_error_handler();
}
if (!$moved) {
$owner = self::ownerOf($directory);
$message = \sprintf(
'%s holds the compiled files of the previous release and cannot be moved out of the way (%s). '
.'Remove it as %s, then run this script again: sudo -u %s rm -rf %s',
$directory,
$renameError ?? 'rename failed',
$owner,
$owner,
escapeshellarg($directory),
);
throw new \RuntimeException($message);
}
try {
// The content first, then the directory itself: remove() renames a
// directory before emptying it, and the files it cannot delete would end
// up in a sibling with a random name instead of the one reported here.
$this->filesystem->remove(new \FilesystemIterator(
$movedDirectory,
\FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS,
));
$this->filesystem->remove($movedDirectory);
} catch (IOException) {
return $movedDirectory;
}
return null;
}
/**
* The directory whose files could not be deleted, inside what remove() left
* behind. Deleting a file takes write access to the directory that holds it, so
* its owner is the user who can finish the job. The top of the leftover is
* usually not that directory: it can belong to the user running the script
* while the subdirectories the web server created belong to the web server.
*/
public static function directoryKeepingFiles(string $leftover): string
{
if (!is_writable($leftover)) {
return $leftover;
}
try {
$entries = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($leftover, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST,
\RecursiveIteratorIterator::CATCH_GET_CHILD,
);
foreach ($entries as $path => $entry) {
if ($entry->isDir() && !$entry->isLink() && !is_writable($path)) {
return $path;
}
}
} catch (\UnexpectedValueException) {
// An unreadable leftover: its own owner is the best answer there is.
}
return $leftover;
}
/**
* The name of the user that owns a file, for the command the message suggests.
*/
public static function ownerOf(string $path): string
{
$uid = file_exists($path) ? fileowner($path) : false;
if (false === $uid) {
return 'the web server user';
}
if (\function_exists('posix_getpwuid')) {
$user = posix_getpwuid($uid);
if (\is_array($user)) {
return $user['name'];
}
}
// sudo -u accepts a numeric user id prefixed with #.
return '#'.$uid;
}
}