Completed
Pull Request — develop (#7)
by Ben
02:10
created

Api::setEntryName()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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