Completed
Push — develop ( 9b083d...637460 )
by Ben
10s
created

Api::getTreeMetadata()   B

Complexity

Conditions 4
Paths 1

Size

Total Lines 41
Code Lines 19

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 41
rs 8.5806
cc 4
eloc 19
nc 1
nop 2
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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