-
Notifications
You must be signed in to change notification settings - Fork 28
/
Copy pathArchive.php
481 lines (418 loc) · 11.7 KB
/
Archive.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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
<?php
namespace Barracuda\ArchiveStream;
use GMP;
use Barracuda\ArchiveStream\TarArchive as Tar;
use Barracuda\ArchiveStream\ZipArchive as Zip;
/**
* A streaming archive object.
*/
class Archive
{
/**
* Whether to use the specified base path or not for files in the archive.
* @var bool
*/
protected $use_container_dir = false;
/**
* Base path for files added to the archive.
* @var string
*/
protected $container_dir_name = '';
/**
* List of errors encountered while generating the archive.
* @var string
*/
private $errors = array();
/**
* Filename for the error log which will be placed inside the archive.
* @var string
*/
private $error_log_filename = 'archive_errors.log';
/**
* Message to place at the top of the error log.
* @var string
*/
private $error_header_text = 'The following errors were encountered while generating this archive:';
/**
* Block size to process files in. Defaults to 1M
* @var int
*/
protected $block_size = 1048576;
/**
* Create a new ArchiveStream object.
*
* @param string $name The name of the resulting archive (optional).
* @param array $opt Hash of archive options (see archive options in readme).
* @param string $base_path An optional base path for files to be named under.
* @param resource $output_stream Output stream for archive contents.
*/
public function __construct(
$name = null,
array $opt = array(),
$base_path = null,
$output_stream = STDOUT
)
{
$this->output_stream = $output_stream;
// save options
$this->opt = $opt;
// if a $base_path was passed set the protected property with that value, otherwise leave it empty
$this->container_dir_name = isset($base_path) ? $base_path . '/' : '';
// set large file defaults: size = 20 megabytes, method = store
if (!isset($this->opt['large_file_size']))
{
$this->opt['large_file_size'] = 20 * 1024 * 1024;
}
if (!isset($this->opt['large_files_only']))
{
$this->opt['large_files_only'] = false;
}
$this->output_name = $name;
if ($name || isset($opt['send_http_headers']))
{
$this->need_headers = true;
}
// turn off output buffering
while (ob_get_level() > 0)
{
// throw away any output left in the buffer
ob_end_clean();
}
}
/**
* Create instance based on useragent string
*
* @param string $base_filename A name for the resulting archive (without an extension).
* @param array $opt Map of archive options (see above for list).
* @param resource $output_stream Output stream for archive contents.
* @return ArchiveStream for either zip or tar
*/
public static function instance_by_useragent(
$base_filename = null,
array $opt = array(),
$output_stream = STDOUT
)
{
$user_agent = (isset($_SERVER['HTTP_USER_AGENT']) ? strtolower($_SERVER['HTTP_USER_AGENT']) : '');
// detect windows and use zip
if (strpos($user_agent, 'windows') !== false)
{
$filename = (($base_filename === null) ? null : $base_filename . '.zip');
return new Zip($filename, $opt, $base_filename, $output_stream);
}
// fallback to tar
else
{
$filename = (($base_filename === null) ? null : $base_filename . '.tar');
return new Tar($filename, $opt, $base_filename, $output_stream);
}
}
/**
* Add file to the archive
*
* Parameters:
*
* @param string $name Path of file in the archive (including directory).
* @param string $data Contents of the file.
* @param array $opt Map of file options (see above for list).
* @return void
*/
public function add_file($name, $data, array $opt = array())
{
// calculate header attributes
$this->meth_str = 'deflate';
$meth = 0x08;
// send file header
$this->init_file_stream_transfer($name, strlen($data), $opt, $meth);
// send data
$this->stream_file_part($data, $single_part = true);
// complete the file stream
$this->complete_file_stream();
}
/**
* Add file by path
*
* @param string $name Name of file in archive (including directory path).
* @param string $path Path to file on disk (note: paths should be encoded using
* UNIX-style forward slashes -- e.g '/path/to/some/file').
* @param array $opt Map of file options (see above for list).
* @return void
*/
public function add_file_from_path($name, $path, array $opt = array())
{
if ($this->opt['large_files_only'] || $this->is_large_file($path))
{
// file is too large to be read into memory; add progressively
$this->add_large_file($name, $path, $opt);
}
else
{
// file is small enough to read into memory; read file contents and
// handle with add_file()
$data = file_get_contents($path);
$this->add_file($name, $data, $opt);
}
}
/**
* Log an error to be added to the error log in the archive.
*
* @param string $message Error text to add to the log file.
* @return void
*/
public function push_error($message)
{
$this->errors[] = (string) $message;
}
/**
* Set whether or not all elements in the archive will be placed within one container directory.
*
* @param bool $bool True to use contaner directory, false to prevent using one. Defaults to false.
* @return void
*/
public function set_use_container_dir($bool = false)
{
$this->use_container_dir = (bool) $bool;
}
/**
* Set the name filename for the error log file when it's added to the archive
*
* @param string $name Filename for the error log.
* @return void
*/
public function set_error_log_filename($name)
{
if (isset($name))
{
$this->error_log_filename = (string) $name;
}
}
/**
* Set the first line of text in the error log file
*
* @param string $msg Message to display on the first line of the error log file.
* @return void
*/
public function set_error_header_text($msg)
{
if (isset($msg))
{
$this->error_header_text = (string) $msg;
}
}
/***************************
* PRIVATE UTILITY METHODS *
***************************/
/**
* Add a large file from the given path
*
* @param string $name Name of file in archive (including directory path).
* @param string $path Path to file on disk (note: paths should be encoded using
* UNIX-style forward slashes -- e.g '/path/to/some/file').
* @param array $opt Map of file options (see above for list).
* @return void
*/
protected function add_large_file($name, $path, array $opt = array())
{
// send file header
$this->init_file_stream_transfer($name, filesize($path), $opt);
// open input file
$fh = fopen($path, 'rb');
// send file blocks
while ($data = fread($fh, $this->block_size))
{
// send data
$this->stream_file_part($data);
}
// close input file
fclose($fh);
// complete the file stream
$this->complete_file_stream();
}
/**
* Is this file larger than large_file_size?
*
* @param string $path Path to file on disk.
* @return bool True if large, false if small.
*/
protected function is_large_file($path)
{
$st = stat($path);
return ($this->opt['large_file_size'] > 0) && ($st['size'] > $this->opt['large_file_size']);
}
/**
* Send HTTP headers for this stream.
*
* @return void
*/
private function send_http_headers()
{
// grab options
$opt = $this->opt;
// grab content type from options
if (isset($opt['content_type']))
{
$content_type = $opt['content_type'];
}
else
{
$content_type = 'application/x-zip';
}
// grab content type encoding from options and append to the content type option
if (isset($opt['content_type_encoding']))
{
$content_type .= '; charset=' . $opt['content_type_encoding'];
}
// grab content disposition
$disposition = 'attachment';
if (isset($opt['content_disposition']))
{
$disposition = $opt['content_disposition'];
}
if ($this->output_name)
{
$disposition .= "; filename=\"{$this->output_name}\"";
}
$headers = array(
'Content-Type' => $content_type,
'Content-Disposition' => $disposition,
'Pragma' => 'public',
'Cache-Control' => 'public, must-revalidate',
'Content-Transfer-Encoding' => 'binary',
);
foreach ($headers as $key => $val)
{
header("$key: $val");
}
}
/**
* Send string, sending HTTP headers if necessary.
*
* @param string $data Data to send.
* @return void
*/
protected function send($data)
{
if ($this->need_headers)
{
$this->send_http_headers();
}
$this->need_headers = false;
do
{
$result = fwrite($this->output_stream, $data);
$data = substr($data, $result);
fflush($this->output_stream);
} while ($data && $result !== false);
}
/**
* If errors were encountered, add an error log file to the end of the archive
* @return void
*/
public function add_error_log()
{
if (!empty($this->errors))
{
$msg = $this->error_header_text;
foreach ($this->errors as $err)
{
$msg .= "\r\n\r\n" . $err;
}
// stash current value so it can be reset later
$temp = $this->use_container_dir;
// set to false to put the error log file in the root instead of the container directory, if we're using one
$this->use_container_dir = false;
$this->add_file($this->error_log_filename, $msg);
// reset to original value and dump the temp variable
$this->use_container_dir = $temp;
unset($temp);
}
}
/**
* Convert a UNIX timestamp to a DOS timestamp.
*
* @param int $when Unix timestamp.
* @return string DOS timestamp
*/
protected function dostime($when = 0)
{
// get date array for timestamp
$d = getdate($when);
// set lower-bound on dates
if ($d['year'] < 1980)
{
$d = array(
'year' => 1980, 'mon' => 1, 'mday' => 1,
'hours' => 0, 'minutes' => 0, 'seconds' => 0
);
}
// remove extra years from 1980
$d['year'] -= 1980;
// return date string
return ($d['year'] << 25) | ($d['mon'] << 21) | ($d['mday'] << 16) |
($d['hours'] << 11) | ($d['minutes'] << 5) | ($d['seconds'] >> 1);
}
/**
* Split a 64-bit integer to two 32-bit integers.
*
* @param mixed $value Integer or GMP resource.
* @return array Containing high and low 32-bit integers.
*/
protected function int64_split($value)
{
// gmp
if (is_resource($value) || $value instanceof GMP)
{
$hex = str_pad(gmp_strval($value, 16), 16, '0', STR_PAD_LEFT);
$high = $this->gmp_convert(substr($hex, 0, 8), 16, 10);
$low = $this->gmp_convert(substr($hex, 8, 8), 16, 10);
}
// int
else
{
$left = 0xffffffff00000000;
$right = 0x00000000ffffffff;
$high = ($value & $left) >>32;
$low = $value & $right;
}
return array($low, $high);
}
/**
* Create a format string and argument list for pack(), then call pack() and return the result.
*
* @param array $fields Key is format string and the value is the data to pack.
* @return string Binary packed data returned from pack().
*/
protected function pack_fields(array $fields)
{
$fmt = '';
$args = array();
// populate format string and argument list
foreach ($fields as $field)
{
$fmt .= $field[0];
$args[] = $field[1];
}
// prepend format string to argument list
array_unshift($args, $fmt);
// build output string from header and compressed data
return call_user_func_array('pack', $args);
}
/**
* Convert a number between bases via GMP.
*
* @param int $num Number to convert.
* @param int $base_a Base to convert from.
* @param int $base_b Base to convert to.
* @return string Number in string format.
*/
private function gmp_convert($num, $base_a, $base_b)
{
$gmp_num = gmp_init($num, $base_a);
if (!(is_resource($gmp_num) || $gmp_num instanceof GMP))
{
// FIXME: Really? We just die here? Can we detect GMP in __constructor() instead maybe?
die("gmp_convert could not convert [$num] from base [$base_a] to base [$base_b]");
}
return gmp_strval($gmp_num, $base_b);
}
}