1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Potherca\Flysystem\Github; |
4
|
|
|
|
5
|
|
|
use Github\Api\GitData; |
6
|
|
|
use Github\Api\Repo; |
7
|
|
|
use Github\Api\Repository\Contents; |
8
|
|
|
use Github\Client; |
9
|
|
|
use Github\Exception\RuntimeException; |
10
|
|
|
use League\Flysystem\AdapterInterface; |
11
|
|
|
use League\Flysystem\Util\MimeType; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Facade class for the Github Api Library |
15
|
|
|
*/ |
16
|
|
|
class Api implements ApiInterface |
17
|
|
|
{ |
18
|
|
|
////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\ |
19
|
|
|
const ERROR_NO_NAME = 'Could not set name for entry'; |
20
|
|
|
const ERROR_NOT_FOUND = 'Not Found'; |
21
|
|
|
|
22
|
|
|
const API_GIT_DATA = 'git'; |
23
|
|
|
const API_REPO = 'repo'; |
24
|
|
|
|
25
|
|
|
const KEY_BLOB = 'blob'; |
26
|
|
|
const KEY_DIRECTORY = 'dir'; |
27
|
|
|
const KEY_FILE = 'file'; |
28
|
|
|
const KEY_FILENAME = 'basename'; |
29
|
|
|
const KEY_MODE = 'mode'; |
30
|
|
|
const KEY_NAME = 'name'; |
31
|
|
|
const KEY_PATH = 'path'; |
32
|
|
|
const KEY_SHA = 'sha'; |
33
|
|
|
const KEY_SIZE = 'size'; |
34
|
|
|
const KEY_STREAM = 'stream'; |
35
|
|
|
const KEY_TIMESTAMP = 'timestamp'; |
36
|
|
|
const KEY_TREE = 'tree'; |
37
|
|
|
const KEY_TYPE = 'type'; |
38
|
|
|
const KEY_VISIBILITY = 'visibility'; |
39
|
|
|
|
40
|
|
|
const GITHUB_API_URL = 'https://api.github.com'; |
41
|
|
|
const GITHUB_URL = 'https://github.com'; |
42
|
|
|
|
43
|
|
|
const MIME_TYPE_DIRECTORY = 'directory'; |
44
|
|
|
|
45
|
|
|
/** @var Client */ |
46
|
|
|
private $client; |
47
|
|
|
/** @var Contents */ |
48
|
|
|
private $contents; |
49
|
|
|
/** @var SettingsInterface */ |
50
|
|
|
private $settings; |
51
|
|
|
/** @var bool */ |
52
|
|
|
private $isAuthenticationAttempted = false; |
53
|
|
|
|
54
|
|
|
//////////////////////////// SETTERS AND GETTERS \\\\\\\\\\\\\\\\\\\\\\\\\\\ |
55
|
|
|
/** |
56
|
|
|
* @param string $name |
57
|
|
|
* |
58
|
|
|
* @return \Github\Api\ApiInterface |
59
|
|
|
* |
60
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
61
|
|
|
*/ |
62
|
|
|
private function getApi($name) |
63
|
|
|
{ |
64
|
|
|
$this->authenticate(); |
65
|
|
|
return $this->client->api($name); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* @return GitData |
70
|
|
|
* |
71
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
72
|
|
|
*/ |
73
|
|
|
private function getGitDataApi() |
74
|
|
|
{ |
75
|
|
|
return $this->getApi(self::API_GIT_DATA); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @return Repo |
80
|
|
|
* |
81
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
82
|
|
|
*/ |
83
|
|
|
private function getRepositoryApi() |
84
|
|
|
{ |
85
|
|
|
return $this->getApi(self::API_REPO); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* @return \Github\Api\Repository\Contents |
90
|
|
|
* |
91
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
92
|
|
|
*/ |
93
|
|
|
private function getRepositoryContent() |
94
|
|
|
{ |
95
|
|
|
if ($this->contents === null) { |
96
|
|
|
$this->contents = $this->getRepositoryApi()->contents(); |
97
|
|
|
} |
98
|
|
|
return $this->contents; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
//////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ |
102
|
|
|
final public function __construct(Client $client, SettingsInterface $settings) |
103
|
|
|
{ |
104
|
|
|
/* @NOTE: If $settings contains `credentials` but not an `author` we are |
105
|
|
|
* still in `read-only` mode. |
106
|
|
|
*/ |
107
|
|
|
|
108
|
|
|
$this->client = $client; |
109
|
|
|
$this->settings = $settings; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* @param string $path |
114
|
|
|
* |
115
|
|
|
* @return bool |
116
|
|
|
* |
117
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
118
|
|
|
*/ |
119
|
|
|
final public function exists($path) |
120
|
|
|
{ |
121
|
|
|
return $this->getRepositoryContent()->exists( |
122
|
|
|
$this->settings->getVendor(), |
123
|
|
|
$this->settings->getPackage(), |
124
|
|
|
$path, |
125
|
|
|
$this->settings->getReference() |
126
|
|
|
); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* @param $path |
131
|
|
|
* |
132
|
|
|
* @return null|string |
133
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
134
|
|
|
* |
135
|
|
|
* @throws \Github\Exception\ErrorException |
136
|
|
|
*/ |
137
|
|
|
final public function getFileContents($path) |
138
|
|
|
{ |
139
|
|
|
return $this->getRepositoryContent()->download( |
140
|
|
|
$this->settings->getVendor(), |
141
|
|
|
$this->settings->getPackage(), |
142
|
|
|
$path, |
143
|
|
|
$this->settings->getReference() |
144
|
|
|
); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* @param string $path |
149
|
|
|
* |
150
|
|
|
* @return array |
151
|
|
|
* |
152
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
153
|
|
|
*/ |
154
|
|
View Code Duplication |
final public function getLastUpdatedTimestamp($path) |
|
|
|
|
155
|
|
|
{ |
156
|
|
|
$commits = $this->commitsForFile($path); |
157
|
|
|
|
158
|
|
|
$updated = array_shift($commits); |
159
|
|
|
|
160
|
|
|
$time = new \DateTime($updated['commit']['committer']['date']); |
161
|
|
|
|
162
|
|
|
return ['timestamp' => $time->getTimestamp()]; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* @param string $path |
167
|
|
|
* |
168
|
|
|
* @return array |
169
|
|
|
* |
170
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
171
|
|
|
*/ |
172
|
|
View Code Duplication |
final public function getCreatedTimestamp($path) |
|
|
|
|
173
|
|
|
{ |
174
|
|
|
$commits = $this->commitsForFile($path); |
175
|
|
|
|
176
|
|
|
$created = array_pop($commits); |
177
|
|
|
|
178
|
|
|
$time = new \DateTime($created['commit']['committer']['date']); |
179
|
|
|
|
180
|
|
|
return ['timestamp' => $time->getTimestamp()]; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* @param string $path |
185
|
|
|
* |
186
|
|
|
* @return array|bool |
187
|
|
|
* |
188
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
189
|
|
|
* @throws \Github\Exception\RuntimeException |
190
|
|
|
*/ |
191
|
|
|
final public function getMetaData($path) |
192
|
|
|
{ |
193
|
|
|
try { |
194
|
|
|
$metadata = $this->getRepositoryContent()->show( |
195
|
|
|
$this->settings->getVendor(), |
196
|
|
|
$this->settings->getPackage(), |
197
|
|
|
$path, |
198
|
|
|
$this->settings->getReference() |
199
|
|
|
); |
200
|
|
|
} catch (RuntimeException $exception) { |
201
|
|
|
if ($exception->getMessage() === self::ERROR_NOT_FOUND) { |
202
|
|
|
$metadata = false; |
203
|
|
|
} else { |
204
|
|
|
throw $exception; |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
if (is_array($metadata) === true && $this->isMetadataForDirectory($metadata) === true) { |
209
|
|
|
/** @var $metadata array */ |
210
|
|
|
$project = sprintf('%s/%s', $this->settings->getVendor(), $this->settings->getPackage()); |
211
|
|
|
$reference = $this->settings->getReference(); |
212
|
|
|
|
213
|
|
|
$url = sprintf( |
214
|
|
|
'%s/repos/%s/contents/%s?ref=%s', |
215
|
|
|
self::GITHUB_API_URL, |
216
|
|
|
$project, |
217
|
|
|
trim($path, '/'), |
218
|
|
|
$reference |
219
|
|
|
); |
220
|
|
|
$htmlUrl = sprintf( |
221
|
|
|
'%s/%s/blob/%s/%s', |
222
|
|
|
self::GITHUB_URL, |
223
|
|
|
$project, |
224
|
|
|
$reference, |
225
|
|
|
trim($path, '/') |
226
|
|
|
); |
227
|
|
|
|
228
|
|
|
$metadata = [ |
229
|
|
|
self::KEY_TYPE => self::KEY_DIRECTORY, |
230
|
|
|
'url' => $url, |
231
|
|
|
'html_url' => $htmlUrl, |
232
|
|
|
'_links' => [ |
233
|
|
|
'self' => $url, |
234
|
|
|
'html' => $htmlUrl |
235
|
|
|
] |
236
|
|
|
]; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
return $metadata; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* @param string $path |
244
|
|
|
* @param bool $recursive |
245
|
|
|
* |
246
|
|
|
* @return array |
247
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
248
|
|
|
*/ |
249
|
|
|
final public function getTreeMetadata($path, $recursive) |
250
|
|
|
{ |
251
|
|
|
// If $info['truncated'] is `true`, the number of items in the tree array |
252
|
|
|
// exceeded the github maximum limit. If we need to fetch more items, |
253
|
|
|
// multiple calls will be needed |
254
|
|
|
|
255
|
|
|
$info = $this->getGitDataApi()->trees()->show( |
256
|
|
|
$this->settings->getVendor(), |
257
|
|
|
$this->settings->getPackage(), |
258
|
|
|
$this->settings->getReference(), |
259
|
|
|
true //@NOTE: To retrieve all needed date the 'recursive' flag should always be 'true' |
260
|
|
|
); |
261
|
|
|
|
262
|
|
|
$path = rtrim($path, '/') . '/'; |
263
|
|
|
|
264
|
|
|
$treeMetadata = $this->extractMetaDataFromTreeInfo($info[self::KEY_TREE], $path, $recursive); |
265
|
|
|
|
266
|
|
|
$normalizeTreeMetadata = $this->normalizeTreeMetadata($treeMetadata); |
267
|
|
|
|
268
|
|
|
$directoryTimestamp = 0000000000; |
269
|
|
|
|
270
|
|
|
array_walk($normalizeTreeMetadata, function (&$entry) use (&$directoryTimestamp) { |
271
|
|
|
if ($this->hasKey($entry, self::KEY_TIMESTAMP) === false |
272
|
|
|
|| $entry[self::KEY_TIMESTAMP] === false |
273
|
|
|
) { |
274
|
|
|
$timestamp = $this->getCreatedTimestamp($entry[self::KEY_PATH])['timestamp']; |
275
|
|
|
|
276
|
|
|
$entry[self::KEY_TIMESTAMP] = $timestamp; |
277
|
|
|
|
278
|
|
|
if ($timestamp > $directoryTimestamp) { |
279
|
|
|
$directoryTimestamp = $timestamp; |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
}); |
283
|
|
|
|
284
|
|
|
/* @FIXME: It might be wise to use a filter to find the right entry instead of always using the first entry in the array. */ |
285
|
|
|
|
286
|
|
|
$normalizeTreeMetadata[0]['timestamp'] = $directoryTimestamp; |
287
|
|
|
|
288
|
|
|
return $normalizeTreeMetadata; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* @param string $path |
293
|
|
|
* |
294
|
|
|
* @return null|string |
295
|
|
|
* |
296
|
|
|
* @throws \Github\Exception\ErrorException |
297
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
298
|
|
|
* @throws \Github\Exception\RuntimeException |
299
|
|
|
*/ |
300
|
|
|
final public function guessMimeType($path) |
301
|
|
|
{ |
302
|
|
|
//@NOTE: The github API does not return a MIME type, so we have to guess :-( |
303
|
|
|
$meta = $this->getMetaData($path); |
304
|
|
|
|
305
|
|
|
if ($this->hasKey($meta, self::KEY_TYPE) && $meta[self::KEY_TYPE] === self::KEY_DIRECTORY) { |
306
|
|
|
$mimeType = self::MIME_TYPE_DIRECTORY; // or application/x-directory |
307
|
|
|
} else { |
308
|
|
|
$content = $this->getFileContents($path); |
309
|
|
|
$mimeType = MimeType::detectByContent($content); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
return $mimeType; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ |
316
|
|
|
/** |
317
|
|
|
* |
318
|
|
|
* @throws \Github\Exception\InvalidArgumentException If no authentication method was given |
319
|
|
|
*/ |
320
|
|
|
private function authenticate() |
321
|
|
|
{ |
322
|
|
|
if ($this->isAuthenticationAttempted === false) { |
323
|
|
|
$credentials = $this->settings->getCredentials(); |
324
|
|
|
|
325
|
|
|
if (empty($credentials) === false) { |
326
|
|
|
$credentials = array_replace( |
327
|
|
|
[null, null, null], |
328
|
|
|
$credentials |
329
|
|
|
); |
330
|
|
|
|
331
|
|
|
$this->client->authenticate( |
332
|
|
|
$credentials[1], |
333
|
|
|
$credentials[2], |
334
|
|
|
$credentials[0] |
335
|
|
|
); |
336
|
|
|
} |
337
|
|
|
$this->isAuthenticationAttempted = true; |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* @param array $tree |
343
|
|
|
* @param string $path |
344
|
|
|
* @param bool $recursive |
345
|
|
|
* |
346
|
|
|
* @return array |
347
|
|
|
*/ |
348
|
|
|
private function extractMetaDataFromTreeInfo(array $tree, $path, $recursive) |
349
|
|
|
{ |
350
|
|
|
$matchPath = substr($path, 0, -1); |
351
|
|
|
$length = abs(strlen($matchPath) - 1); |
352
|
|
|
|
353
|
|
|
$metadata = array_filter($tree, function ($entry) use ($matchPath, $recursive, $length) { |
354
|
|
|
$match = false; |
355
|
|
|
|
356
|
|
|
$entryPath = $entry[self::KEY_PATH]; |
357
|
|
|
|
358
|
|
|
if ($matchPath === '' || strpos($entryPath, $matchPath) === 0) { |
359
|
|
|
if ($recursive === true) { |
360
|
|
|
$match = true; |
361
|
|
|
} else { |
362
|
|
|
$match = ($matchPath !== '' || strpos($entryPath, '/', $length) === false); |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
return $match; |
367
|
|
|
}); |
368
|
|
|
|
369
|
|
|
return array_values($metadata); |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
/** |
373
|
|
|
* @param $permissions |
374
|
|
|
* @return string |
375
|
|
|
*/ |
376
|
|
|
private function guessVisibility($permissions) |
377
|
|
|
{ |
378
|
|
|
$visibility = AdapterInterface::VISIBILITY_PUBLIC; |
379
|
|
|
|
380
|
|
|
if (! substr($permissions, -4) & 0044) { |
381
|
|
|
$visibility = AdapterInterface::VISIBILITY_PRIVATE; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
return $visibility; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* @param array $metadata |
389
|
|
|
* |
390
|
|
|
* @return array |
391
|
|
|
*/ |
392
|
|
|
private function normalizeTreeMetadata($metadata) |
393
|
|
|
{ |
394
|
|
|
$result = []; |
395
|
|
|
|
396
|
|
|
if (is_array(current($metadata)) === false) { |
397
|
|
|
$metadata = [$metadata]; |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
foreach ($metadata as $entry) { |
401
|
|
|
$this->setEntryName($entry); |
402
|
|
|
$this->setEntryType($entry); |
403
|
|
|
$this->setEntryVisibility($entry); |
404
|
|
|
|
405
|
|
|
$this->setDefaultValue($entry, self::KEY_CONTENTS); |
406
|
|
|
$this->setDefaultValue($entry, self::KEY_STREAM); |
407
|
|
|
$this->setDefaultValue($entry, self::KEY_TIMESTAMP); |
408
|
|
|
|
409
|
|
|
|
410
|
|
|
$result[] = $entry; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
return $result; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* @param $path |
418
|
|
|
* |
419
|
|
|
* @return array |
420
|
|
|
* |
421
|
|
|
* @throws \Github\Exception\InvalidArgumentException |
422
|
|
|
*/ |
423
|
|
|
private function commitsForFile($path) |
424
|
|
|
{ |
425
|
|
|
return $this->getRepositoryApi()->commits()->all( |
426
|
|
|
$this->settings->getVendor(), |
427
|
|
|
$this->settings->getPackage(), |
428
|
|
|
array( |
429
|
|
|
'sha' => $this->settings->getBranch(), |
430
|
|
|
'path' => $path |
431
|
|
|
) |
432
|
|
|
); |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
/** |
436
|
|
|
* @param array $entry |
437
|
|
|
* @param string $key |
438
|
|
|
* @param bool $default |
439
|
|
|
* |
440
|
|
|
* @return mixed |
441
|
|
|
*/ |
442
|
|
|
private function setDefaultValue(array &$entry, $key, $default = false) |
443
|
|
|
{ |
444
|
|
|
if ($this->hasKey($entry, $key) === false) { |
445
|
|
|
$entry[$key] = $default; |
446
|
|
|
} |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* @param $entry |
451
|
|
|
*/ |
452
|
|
|
private function setEntryType(&$entry) |
453
|
|
|
{ |
454
|
|
|
if ($this->hasKey($entry, self::KEY_TYPE) === true) { |
455
|
|
|
switch ($entry[self::KEY_TYPE]) { |
456
|
|
|
case self::KEY_BLOB: |
457
|
|
|
$entry[self::KEY_TYPE] = self::KEY_FILE; |
458
|
|
|
break; |
459
|
|
|
|
460
|
|
|
case self::KEY_TREE: |
461
|
|
|
$entry[self::KEY_TYPE] = self::KEY_DIRECTORY; |
462
|
|
|
break; |
463
|
|
|
} |
464
|
|
|
} |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
/** |
468
|
|
|
* @param $entry |
469
|
|
|
*/ |
470
|
|
|
private function setEntryVisibility(&$entry) |
471
|
|
|
{ |
472
|
|
|
if ($this->hasKey($entry, self::KEY_MODE)) { |
473
|
|
|
$entry[self::KEY_VISIBILITY] = $this->guessVisibility($entry[self::KEY_MODE]); |
474
|
|
|
} else { |
475
|
|
|
/* Assume public by default */ |
476
|
|
|
$entry[self::KEY_VISIBILITY] = GithubAdapter::VISIBILITY_PUBLIC; |
477
|
|
|
} |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
/** |
481
|
|
|
* @param $entry |
482
|
|
|
*/ |
483
|
|
|
private function setEntryName(&$entry) |
484
|
|
|
{ |
485
|
|
|
if ($this->hasKey($entry, self::KEY_NAME) === false) { |
486
|
|
|
if ($this->hasKey($entry, self::KEY_FILENAME) === true) { |
487
|
|
|
$entry[self::KEY_NAME] = $entry[self::KEY_FILENAME]; |
488
|
|
|
} elseif ($this->hasKey($entry, self::KEY_PATH) === true) { |
489
|
|
|
$entry[self::KEY_NAME] = $entry[self::KEY_PATH]; |
490
|
|
|
} else { |
491
|
|
|
$entry[self::KEY_NAME] = null; |
492
|
|
|
} |
493
|
|
|
} |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
/** |
497
|
|
|
* @param $metadata |
498
|
|
|
* @return bool |
499
|
|
|
*/ |
500
|
|
|
private function isMetadataForDirectory($metadata) |
501
|
|
|
{ |
502
|
|
|
$isDirectory = false; |
503
|
|
|
|
504
|
|
|
$keys = array_keys($metadata); |
505
|
|
|
|
506
|
|
|
if ($keys[0] === 0) { |
507
|
|
|
$isDirectory = true; |
508
|
|
|
} |
509
|
|
|
|
510
|
|
|
return $isDirectory; |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
/** |
514
|
|
|
* @param $subject |
515
|
|
|
* @param $key |
516
|
|
|
* @return mixed |
517
|
|
|
*/ |
518
|
|
|
private function hasKey(&$subject, $key) |
519
|
|
|
{ |
520
|
|
|
$keyExists = false; |
521
|
|
|
|
522
|
|
|
if (is_array($subject)) { |
523
|
|
|
/** @noinspection ReferenceMismatchInspection */ |
524
|
|
|
$keyExists = array_key_exists($key, $subject); |
525
|
|
|
} |
526
|
|
|
|
527
|
|
|
return $keyExists; |
528
|
|
|
} |
529
|
|
|
} |
530
|
|
|
|
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.