1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* (c) 2018 Dennis Birkholz <[email protected]> |
5
|
|
|
* |
6
|
|
|
* $Id$ |
7
|
|
|
* Author: $Format:%an <%ae>, %ai$ |
8
|
|
|
* Committer: $Format:%cn <%ce>, %cI$ |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace morgue\archive; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* This class represents a single entry in an archive (file, directory) |
15
|
|
|
* |
16
|
|
|
* @author Dennis Birkholz <[email protected]> |
17
|
|
|
*/ |
18
|
|
|
final class ArchiveEntry |
19
|
|
|
{ |
20
|
|
|
const UNIX_ATTRIBUTES_DEFAULT = UNIX_ATTRIBUTE_USER_READ|UNIX_ATTRIBUTE_USER_WRITE|UNIX_ATTRIBUTE_GROUP_READ|UNIX_ATTRIBUTE_OTHER_READ; |
21
|
|
|
const UNIX_ATTRIBUTES_DEFAULT_FILE = self::UNIX_ATTRIBUTES_DEFAULT|UNIX_ATTRIBUTE_TYPE_FILE; |
22
|
|
|
const UNIX_ATTRIBUTES_DEFAULT_DIRECTORY = self::UNIX_ATTRIBUTES_DEFAULT|UNIX_ATTRIBUTE_TYPE_DIRECTORY|UNIX_ATTRIBUTE_USER_EXECUTE|UNIX_ATTRIBUTE_GROUP_EXECUTE|UNIX_ATTRIBUTE_OTHER_EXECUTE; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* The name of this entry, relative to the archive root or absolute/fully qualified |
26
|
|
|
* @var string |
27
|
|
|
*/ |
28
|
|
|
private $name; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var int |
32
|
|
|
*/ |
33
|
|
|
private $uncompressedSize; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var int |
37
|
|
|
*/ |
38
|
|
|
private $sourceSize; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var int |
42
|
|
|
*/ |
43
|
|
|
private $targetSize; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var \DateTimeInterface |
47
|
|
|
*/ |
48
|
|
|
private $creationTime; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var \DateTimeInterface |
52
|
|
|
*/ |
53
|
|
|
private $modificationTime; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* CRC32 checksum of the uncompressed file |
57
|
|
|
* @var int |
58
|
|
|
*/ |
59
|
|
|
private $checksumCrc32; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var string |
63
|
|
|
*/ |
64
|
|
|
private $comment; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* One of the COMPRESSION_METHOD_* constants |
68
|
|
|
* @var string |
69
|
|
|
*/ |
70
|
|
|
private $sourceCompressionMethod; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* One of the COMPRESSION_METHOD_* constants |
74
|
|
|
* @var string |
75
|
|
|
*/ |
76
|
|
|
private $targetCompressionMethod; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Combination of DOS_ATTRIBUTE_* flags |
80
|
|
|
* May be unsupported by target file format |
81
|
|
|
* @var int |
82
|
|
|
*/ |
83
|
|
|
private $dosAttributes; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Combination of UNIX_ATTRIBUTE_* flags |
87
|
|
|
* May be unsupported by target file format |
88
|
|
|
* @var int |
89
|
|
|
*/ |
90
|
|
|
private $unixAttributes; |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* A PHP stream to the underlying file |
94
|
|
|
* The stream should not perform any translation (like decompression) |
95
|
|
|
* so reading from this stream should yield data in the format |
96
|
|
|
* specified by $sourceCompressionMethod |
97
|
|
|
* @var resource |
98
|
|
|
*/ |
99
|
|
|
private $sourceStream; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* Fully qualified path to the underlying uncompressed file |
103
|
|
|
* @var string |
104
|
|
|
*/ |
105
|
|
|
private $sourcePath; |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Binary string containing the content of this entry |
109
|
|
|
* @var string |
110
|
|
|
*/ |
111
|
|
|
private $sourceString; |
112
|
|
|
|
113
|
2 |
|
public function __construct(string $name) |
114
|
|
|
{ |
115
|
2 |
|
$this->name = $name; |
116
|
2 |
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Import data from the supplied $stat array. Format is the one returned by stat()/fstat() |
120
|
|
|
* @param array $stat |
121
|
|
|
*/ |
122
|
3 |
|
private function importStat(array $stat) |
123
|
|
|
{ |
124
|
3 |
|
if (!empty($stat['mode']) && $this->unixAttributes === null) { |
125
|
3 |
|
$this->unixAttributes = $stat['mode']; |
126
|
|
|
} |
127
|
|
|
|
128
|
3 |
|
$timezone = new \DateTimeZone(\date_default_timezone_get()); |
129
|
|
|
|
130
|
3 |
View Code Duplication |
if (!empty($stat['ctime']) && $this->creationTime === null) { |
|
|
|
|
131
|
3 |
|
$this->creationTime = (new \DateTimeImmutable('@' . $stat['ctime']))->setTimezone($timezone); |
|
|
|
|
132
|
|
|
} |
133
|
|
|
|
134
|
3 |
View Code Duplication |
if (!empty($stat['mtime']) && $this->modificationTime === null) { |
|
|
|
|
135
|
3 |
|
$this->modificationTime = (new \DateTimeImmutable('@' . $stat['mtime']))->setTimezone($timezone); |
|
|
|
|
136
|
|
|
} |
137
|
|
|
|
138
|
3 |
|
if ($this->unixAttributes & UNIX_ATTRIBUTE_TYPE_DIRECTORY) { |
139
|
1 |
|
$this->sourceSize = 0; |
140
|
1 |
|
$this->targetSize = 0; |
141
|
1 |
|
$this->uncompressedSize = 0; |
142
|
1 |
|
$this->sourceCompressionMethod = COMPRESSION_METHOD_STORE; |
143
|
1 |
|
$this->targetCompressionMethod = COMPRESSION_METHOD_STORE; |
144
|
|
|
} |
145
|
|
|
|
146
|
2 |
|
elseif ($this->unixAttributes & UNIX_ATTRIBUTE_TYPE_FILE) { |
147
|
2 |
|
$this->sourceSize = $stat['size']; |
148
|
2 |
|
if ($this->sourceCompressionMethod === COMPRESSION_METHOD_STORE) { |
149
|
2 |
|
$this->uncompressedSize = $this->sourceSize; |
150
|
|
|
} |
151
|
|
|
} |
152
|
3 |
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* @return resource |
156
|
|
|
*/ |
157
|
3 |
|
public function getSourceAsStream() |
158
|
|
|
{ |
159
|
3 |
|
if ($this->sourceStream !== null) { |
160
|
1 |
|
return $this->sourceStream; |
161
|
|
|
} |
162
|
|
|
|
163
|
2 |
|
elseif ($this->sourcePath !== null) { |
164
|
1 |
|
return \fopen($this->sourcePath, 'r'); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
else { |
168
|
1 |
|
$fp = \fopen('php://memory', 'r+'); |
169
|
1 |
|
\fwrite($fp, $this->sourceString); |
170
|
1 |
|
\fseek($fp, 0); |
171
|
1 |
|
return $fp; |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* @return string|null |
177
|
|
|
*/ |
178
|
2 |
|
public function getSourcePath() |
179
|
|
|
{ |
180
|
2 |
|
return $this->sourcePath; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Set the path of the file that will serve as source for this entry. |
185
|
|
|
* The file must exist and be readable. |
186
|
|
|
* You may provide a URL if a matching stream wrapper is registered with stat support. |
187
|
|
|
* Otherwise, use withSourceStream() to add a stream directly. |
188
|
|
|
* |
189
|
|
|
* @param string $path |
190
|
|
|
* @param string|null $compressionMethod |
191
|
|
|
* @return ArchiveEntry |
192
|
|
|
*/ |
193
|
6 |
|
public function withSourcePath(string $path, string $compressionMethod = null) : self |
194
|
|
|
{ |
195
|
6 |
|
if (!\file_exists($path) || !\is_readable($path)) { |
196
|
1 |
|
throw new \InvalidArgumentException('Can not use non-existing or unreadable file as source.'); |
197
|
|
|
} |
198
|
|
|
|
199
|
5 |
View Code Duplication |
if ($this->sourcePath !== null || $this->sourceStream !== null || $this->sourceString !== null) { |
|
|
|
|
200
|
3 |
|
throw new \InvalidArgumentException('Can not replace source, create a new entry instead.'); |
201
|
|
|
} |
202
|
|
|
|
203
|
3 |
|
$obj = clone $this; |
204
|
3 |
|
$obj->sourcePath = $path; |
205
|
|
|
|
206
|
3 |
|
if ($compressionMethod !== null) { |
207
|
1 |
|
$obj->sourceCompressionMethod = $compressionMethod; |
208
|
|
|
} |
209
|
|
|
|
210
|
3 |
|
if ($compressionMethod === COMPRESSION_METHOD_STORE) { |
211
|
1 |
|
$obj->uncompressedSize = $obj->sourceSize; |
212
|
|
|
} |
213
|
|
|
|
214
|
3 |
|
if (($stat = @\stat($path)) !== false) { |
215
|
3 |
|
$obj->importStat($stat); |
216
|
|
|
} |
217
|
|
|
|
218
|
3 |
|
return $obj; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* @return resource|null |
223
|
|
|
*/ |
224
|
1 |
|
public function getSourceStream() |
225
|
|
|
{ |
226
|
1 |
|
return $this->sourceStream; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* @param resource $stream |
231
|
|
|
* @param string $compressionMethod |
232
|
|
|
* @return ArchiveEntry |
233
|
|
|
*/ |
234
|
4 |
|
public function withSourceStream($stream, string $compressionMethod = null) : self |
235
|
|
|
{ |
236
|
4 |
View Code Duplication |
if ($this->sourcePath !== null || $this->sourceStream !== null || $this->sourceString !== null) { |
|
|
|
|
237
|
3 |
|
throw new \InvalidArgumentException('Can not replace source, create a new entry instead.'); |
238
|
|
|
} |
239
|
|
|
|
240
|
2 |
|
$obj = clone $this; |
241
|
2 |
|
$obj->sourcePath = null; |
242
|
2 |
|
$obj->sourceStream = $stream; |
243
|
2 |
|
$obj->sourceString = null; |
244
|
|
|
|
245
|
2 |
|
if ($compressionMethod !== null) { |
246
|
1 |
|
$obj->sourceCompressionMethod = $compressionMethod; |
247
|
|
|
} |
248
|
|
|
|
249
|
2 |
|
if (($stat = @\fstat($stream)) !== false) { |
250
|
2 |
|
$obj->importStat($stat); |
251
|
|
|
} |
252
|
|
|
|
253
|
2 |
|
return $obj; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* @return string|null |
258
|
|
|
*/ |
259
|
1 |
|
public function getSourceString() |
260
|
|
|
{ |
261
|
1 |
|
return $this->sourceString; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* @param string $content |
266
|
|
|
* @param string $compressionMethod |
267
|
|
|
* @return ArchiveEntry |
268
|
|
|
*/ |
269
|
4 |
|
public function withSourceString(string $content, string $compressionMethod = null) : self |
270
|
|
|
{ |
271
|
4 |
View Code Duplication |
if ($this->sourcePath !== null || $this->sourceStream !== null || $this->sourceString !== null) { |
|
|
|
|
272
|
3 |
|
throw new \InvalidArgumentException('Can not replace source, create a new entry instead.'); |
273
|
|
|
} |
274
|
|
|
|
275
|
2 |
|
$obj = clone $this; |
276
|
2 |
|
$obj->sourcePath = null; |
277
|
2 |
|
$obj->sourceStream = null; |
278
|
2 |
|
$obj->sourceString = $content; |
279
|
2 |
|
$obj->sourceSize = \strlen($content); |
280
|
|
|
|
281
|
2 |
|
if ($compressionMethod !== null) { |
282
|
1 |
|
$obj->sourceCompressionMethod = $compressionMethod; |
283
|
|
|
} |
284
|
|
|
|
285
|
2 |
|
if ($compressionMethod === COMPRESSION_METHOD_STORE) { |
286
|
1 |
|
$obj->uncompressedSize = $obj->sourceSize; |
287
|
|
|
} |
288
|
|
|
|
289
|
2 |
|
$now = new \DateTimeImmutable('now', new \DateTimeZone(\date_default_timezone_get())); |
290
|
2 |
|
if ($obj->creationTime === null) { |
291
|
2 |
|
$obj->creationTime = $now; |
292
|
|
|
} |
293
|
|
|
|
294
|
2 |
|
if ($obj->modificationTime === null) { |
295
|
2 |
|
$obj->modificationTime = $now; |
296
|
|
|
} |
297
|
|
|
|
298
|
2 |
|
return $obj; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* @return string|null |
303
|
|
|
*/ |
304
|
2 |
|
public function getName() |
305
|
|
|
{ |
306
|
2 |
|
return $this->name; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* @param string $name |
311
|
|
|
* @return ArchiveEntry |
312
|
|
|
*/ |
313
|
1 |
|
public function withName(string $name) : self |
314
|
|
|
{ |
315
|
1 |
|
$obj = clone $this; |
316
|
1 |
|
$obj->name = $name; |
317
|
1 |
|
return $obj; |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* @return int|null |
322
|
|
|
*/ |
323
|
1 |
|
public function getUncompressedSize() |
324
|
|
|
{ |
325
|
1 |
|
return $this->uncompressedSize; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* @param int $uncompressedSize |
330
|
|
|
* @return ArchiveEntry |
331
|
|
|
*/ |
332
|
1 |
|
public function withUncompressedSize(int $uncompressedSize) : self |
333
|
|
|
{ |
334
|
1 |
|
$obj = clone $this; |
335
|
1 |
|
$obj->uncompressedSize = $uncompressedSize; |
336
|
1 |
|
return $obj; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* @return int|null |
341
|
|
|
*/ |
342
|
1 |
|
public function getSourceSize() |
343
|
|
|
{ |
344
|
1 |
|
return $this->sourceSize; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* @param int $sourceSize |
349
|
|
|
* @return ArchiveEntry |
350
|
|
|
*/ |
351
|
1 |
|
public function withSourceSize(int $sourceSize) : self |
352
|
|
|
{ |
353
|
1 |
|
$obj = clone $this; |
354
|
1 |
|
$obj->sourceSize = $sourceSize; |
355
|
1 |
|
return $obj; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* @return int|null |
360
|
|
|
*/ |
361
|
1 |
|
public function getTargetSize() |
362
|
|
|
{ |
363
|
1 |
|
return $this->targetSize; |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
/** |
367
|
|
|
* @param int $targetSize |
368
|
|
|
* @return ArchiveEntry |
369
|
|
|
*/ |
370
|
1 |
|
public function withTargetSize(int $targetSize) : self |
371
|
|
|
{ |
372
|
1 |
|
$obj = clone $this; |
373
|
1 |
|
$obj->targetSize = $targetSize; |
374
|
1 |
|
return $obj; |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* @return \DateTimeInterface|null |
379
|
|
|
*/ |
380
|
1 |
|
public function getCreationTime() |
381
|
|
|
{ |
382
|
1 |
|
return $this->creationTime; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* @param \DateTimeInterface $creationTime |
387
|
|
|
* @return ArchiveEntry |
388
|
|
|
*/ |
389
|
1 |
|
public function withCreationTime(\DateTimeInterface $creationTime) : self |
390
|
|
|
{ |
391
|
1 |
|
$obj = clone $this; |
392
|
1 |
|
$obj->creationTime = $creationTime; |
393
|
1 |
|
return $obj; |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
/** |
397
|
|
|
* @return \DateTimeInterface|null |
398
|
|
|
*/ |
399
|
1 |
|
public function getModificationTime() |
400
|
|
|
{ |
401
|
1 |
|
return $this->modificationTime; |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
/** |
405
|
|
|
* @param \DateTimeInterface $modificationTime |
406
|
|
|
* @return ArchiveEntry |
407
|
|
|
*/ |
408
|
1 |
|
public function withModificationTime(\DateTimeInterface $modificationTime) : self |
409
|
|
|
{ |
410
|
1 |
|
$obj = clone $this; |
411
|
1 |
|
$obj->modificationTime = $modificationTime; |
412
|
1 |
|
return $obj; |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
/** |
416
|
|
|
* @return int|null |
417
|
|
|
*/ |
418
|
1 |
|
public function getChecksumCrc32() |
419
|
|
|
{ |
420
|
1 |
|
return $this->checksumCrc32; |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* @param int $checksumCrc32 |
425
|
|
|
* @return ArchiveEntry |
426
|
|
|
*/ |
427
|
1 |
|
public function withChecksumCrc32(int $checksumCrc32) : self |
428
|
|
|
{ |
429
|
1 |
|
$obj = clone $this; |
430
|
1 |
|
$obj->checksumCrc32 = $checksumCrc32; |
431
|
1 |
|
return $obj; |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
/** |
435
|
|
|
* @return string|null |
436
|
|
|
*/ |
437
|
1 |
|
public function getComment() |
438
|
|
|
{ |
439
|
1 |
|
return $this->comment; |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
/** |
443
|
|
|
* @param string|null $comment |
444
|
|
|
* @return ArchiveEntry |
445
|
|
|
*/ |
446
|
2 |
|
public function withComment(string $comment = null) : self |
447
|
|
|
{ |
448
|
2 |
|
$obj = clone $this; |
449
|
2 |
|
$obj->comment = $comment; |
450
|
2 |
|
return $obj; |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
/** |
454
|
|
|
* @return string|null |
455
|
|
|
*/ |
456
|
1 |
|
public function getSourceCompressionMethod() |
457
|
|
|
{ |
458
|
1 |
|
return $this->sourceCompressionMethod; |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
/** |
462
|
|
|
* Any of the COMPRESSION_METHOD_* constants |
463
|
|
|
* |
464
|
|
|
* @param string $sourceCompressionMethod |
465
|
|
|
* @return ArchiveEntry |
466
|
|
|
*/ |
467
|
1 |
|
public function withSourceCompressionMethod($sourceCompressionMethod) : self |
468
|
|
|
{ |
469
|
1 |
|
$obj = clone $this; |
470
|
1 |
|
$obj->sourceCompressionMethod = $sourceCompressionMethod; |
471
|
1 |
|
return $obj; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* @return string|null |
476
|
|
|
*/ |
477
|
1 |
|
public function getTargetCompressionMethod() |
478
|
|
|
{ |
479
|
1 |
|
return $this->targetCompressionMethod; |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
/** |
483
|
|
|
* @param string $targetCompressionMethod |
484
|
|
|
* @return ArchiveEntry |
485
|
|
|
*/ |
486
|
1 |
|
public function withTargetCompressionMethod($targetCompressionMethod) : self |
487
|
|
|
{ |
488
|
1 |
|
$obj = clone $this; |
489
|
1 |
|
$obj->targetCompressionMethod = $targetCompressionMethod; |
490
|
1 |
|
return $obj; |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
/** |
494
|
|
|
* @return int|null |
495
|
|
|
*/ |
496
|
1 |
|
public function getDosAttributes() |
497
|
|
|
{ |
498
|
1 |
|
return $this->dosAttributes; |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
/** |
502
|
|
|
* @param int $dosAttributes |
503
|
|
|
* @return ArchiveEntry |
504
|
|
|
*/ |
505
|
1 |
|
public function withDosAttributes(int $dosAttributes) : self |
506
|
|
|
{ |
507
|
1 |
|
$obj = clone $this; |
508
|
1 |
|
$obj->dosAttributes = $dosAttributes; |
509
|
1 |
|
return $obj; |
510
|
|
|
} |
511
|
|
|
|
512
|
|
|
/** |
513
|
|
|
* @return int|null |
514
|
|
|
*/ |
515
|
1 |
|
public function getUnixAttributes() |
516
|
|
|
{ |
517
|
1 |
|
return $this->unixAttributes; |
518
|
|
|
} |
519
|
|
|
|
520
|
|
|
/** |
521
|
|
|
* @param int $unixAttributes |
522
|
|
|
* @return ArchiveEntry |
523
|
|
|
*/ |
524
|
1 |
|
public function withUnixAttributes(int $unixAttributes) : self |
525
|
|
|
{ |
526
|
1 |
|
$obj = clone $this; |
527
|
1 |
|
$obj->unixAttributes = $unixAttributes; |
528
|
1 |
|
return $obj; |
529
|
|
|
} |
530
|
|
|
} |
531
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.