-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
FsUtils.php
309 lines (279 loc) · 10 KB
/
FsUtils.php
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
<?php
declare(strict_types=1);
namespace Drush\Utils;
use Drush\Drush;
use Drush\Sql\SqlBase;
use finfo;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;
final class FsUtils
{
// @var null|string[] List of directories to delete
private static $deletionList;
/**
* Return path to the backup directory.
*
* @param string $subdir
* The name of the desired subdirectory(s) under drush-backups.
* Usually a database name.
* @throws \Exception
*/
public static function getBackupDir(?string $subdir = null): string
{
$parent = self::getBackupDirParent();
// Try to use db name as subdir if none was provided.
if (empty($subdir)) {
if ($sql = SqlBase::create()) {
$db_spec = $sql->getDbSpec();
$subdir = $db_spec['database'];
}
}
// Add in the subdirectory if it was provided or inferred.
if (!empty($subdir)) {
$parent = Path::join($parent, $subdir);
}
// Save the date to be used in the backup directory's path name.
$date = gmdate('YmdHis', $_SERVER['REQUEST_TIME']);
return Path::join($parent, $date);
}
/**
* Get the base dir where our backup directories will be stored. Also stores CLI history file.
*/
public static function getBackupDirParent()
{
// Try in order:
// 1. The user-specified backup directory from drush.yml config file
// 2. The 'drush-backups' directory in $HOME
// 3. The 'drush-backups' directory in tmp
$candidates = [
Drush::config()->get('drush.paths.backup-dir'),
Path::join(
Drush::config()->home(),
'drush-backups'
),
Path::join(
Drush::config()->tmp(),
'drush-backups'
),
];
// Return the first usable candidate
foreach ($candidates as $dir) {
if (self::isUsableDirectory($dir)) {
return $dir;
}
}
throw new \Exception('No viable backup directory found.');
}
/**
* Determine if the specified location is writable, or if a writable
* directory could be created at that path.
*
* @param $dir
* Path to directory that we are considering using
*
* @return bool|string
*/
public static function isUsableDirectory(?string $dir)
{
// This directory is not usable if it is empty or if it is the root.
if (empty($dir) || (dirname($dir) === $dir)) {
return false;
}
// If the directory already exists and is writable, then it is usable.
if (is_writable($dir)) {
return $dir;
}
// If the directory exists (and is not writable), then it is not usable.
if (file_exists($dir)) {
return false;
}
// Otherwise, this directory is usable (could be created) if its
// parent directory is usable.
return self::isUsableDirectory(dirname($dir));
}
/**
* Prepare a temporary directory that will be deleted on exit.
*
* @param string $subdir
* A string naming the subdirectory of the backup directory.
* @return string
* Path to the specified backup directory.
* @throws \Exception
*/
public static function tmpDir($subdir = null): string
{
$parent = self::getBackupDirParent();
$fs = new Filesystem();
$dir = $fs->tempnam($parent, $subdir ?? 'drush');
unlink($dir);
$fs->mkdir($dir);
static::registerForDeletion($dir);
return $dir;
}
/**
* Add the given directory to a list to be deleted on exit.
*
* @param string $dir
* Path to directory to be deleted later.
*/
public static function registerForDeletion(string $dir)
{
if (!isset(static::$deletionList)) {
static::$deletionList = [];
register_shutdown_function([static::class, 'cleanup']);
}
static::$deletionList[] = $dir;
}
/**
* Delete all of the files registered for deletion.
*/
public static function cleanup()
{
if (!isset(static::$deletionList)) {
return;
}
$fs = new Filesystem();
foreach (static::$deletionList as $dir) {
try {
$fs->remove($dir);
} catch (\Exception $e) {
// No action taken if someone already deleted the directory
}
}
}
/**
* Prepare a backup directory.
*
* @param string $subdir
* A string naming the subdirectory of the backup directory.
* @return string
* Path to the specified backup directory.
* @throws \Exception
*/
public static function prepareBackupDir($subdir = null): string
{
$fs = new Filesystem();
$backup_dir = self::getBackupDir($subdir);
$fs->mkdir($backup_dir);
return $backup_dir;
}
/**
* Returns canonicalized absolute pathname.
*
* The difference between this and PHP's realpath() is that this will
* return the original path even if it doesn't exist.
*
* @param string $path
* The path being checked.
* @return string
* The canonicalized absolute pathname.
*/
public static function realpath(string $path): string
{
$realpath = realpath($path);
return $realpath ?: $path;
}
/**
* Check whether a file is a supported tarball.
*
*
* @return string|bool
* The file content type if it's a tarball. FALSE otherwise.
*/
public static function isTarball(string $path)
{
$content_type = self::getMimeContentType($path);
$supported = [
'application/x-bzip2',
'application/x-gzip',
'application/gzip',
'application/x-tar',
'application/x-zip',
'application/zip',
];
if (in_array($content_type, $supported)) {
return $content_type;
}
return false;
}
/**
* Determines the MIME content type of the specified file.
*
* The power of this function depends on whether the PHP installation
* has either mime_content_type() or finfo installed -- if not, only tar,
* gz, zip and bzip2 types can be detected.
*
*
* @return string|bool|null
* The MIME content type of the file.
*/
public static function getMimeContentType(string $path)
{
$content_type = false;
if (class_exists('finfo')) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$content_type = $finfo->file($path);
if ($content_type == 'application/octet-stream') {
Drush::logger()->debug(dt('Mime type for !file is application/octet-stream.', ['!file' => $path]));
$content_type = false;
}
}
// If apache is configured in such a way that all files are considered
// octet-stream (e.g with mod_mime_magic and an http conf that's serving all
// archives as octet-stream for other reasons) we'll detect mime types on our
// own by examining the file's magic header bytes.
if (!$content_type) {
Drush::logger()->debug(dt('Examining !file headers.', ['!file' => $path]));
if ($file = fopen($path, 'rb')) {
$first = fread($file, 2);
fclose($file);
if ($first !== false) {
// Interpret the two bytes as a little endian 16-bit unsigned int.
$data = unpack('v', $first);
switch ($data[1]) {
case 0x8b1f:
// First two bytes of gzip files are 0x1f, 0x8b (little-endian).
// See http://www.gzip.org/zlib/rfc-gzip.html#header-trailer
$content_type = 'application/x-gzip';
break;
case 0x4b50:
// First two bytes of zip files are 0x50, 0x4b ('PK') (little-endian).
// See http://en.wikipedia.org/wiki/Zip_(file_format)#File_headers
$content_type = 'application/zip';
break;
case 0x5a42:
// First two bytes of bzip2 files are 0x5a, 0x42 ('BZ') (big-endian).
// See http://en.wikipedia.org/wiki/Bzip2#File_format
$content_type = 'application/x-bzip2';
break;
default:
Drush::logger()->debug(dt('Unable to determine mime type from header bytes 0x!hex of !file.', ['!hex' => dechex($data[1]), '!file' => $path,]));
}
} else {
Drush::logger()->warning(dt('Unable to read !file.', ['!file' => $path]));
}
} else {
Drush::logger()->warning(dt('Unable to open !file.', ['!file' => $path]));
}
}
// 3. Lastly if above methods didn't work, try to guess the mime type from
// the file extension. This is useful if the file has no identifiable magic
// header bytes (for example tarballs).
if (!$content_type) {
Drush::logger()->debug(dt('Examining !file extension.', ['!file' => $path]));
// Remove querystring from the filename, if present.
$path = basename(current(explode('?', $path, 2)));
$extension_mimetype = [
'.tar' => 'application/x-tar',
'.sql' => 'application/octet-stream',
];
foreach ($extension_mimetype as $extension => $ct) {
if (str_ends_with($path, $extension)) {
$content_type = $ct;
break;
}
}
}
return $content_type;
}
}