Completed
Pull Request — develop (#7)
by Ben
03:06
created

Api::isMetadataForDirectory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 12
rs 9.4285
cc 2
eloc 6
nc 2
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 getTreeMetadata($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
        $path = rtrim($path, '/') . '/';
249
250
        $treeMetadata = $this->extractMetaDataFromTreeInfo($info[self::KEY_TREE], $path, $recursive);
251
252
        $normalizeTreeMetadata = $this->normalizeTreeMetadata($treeMetadata);
253
254
        $directoryTimestamp = 0000000000;
255
256
        array_walk($normalizeTreeMetadata, function (&$entry) use (&$directoryTimestamp) {
257
            if ($this->hasKey($entry, self::KEY_TIMESTAMP) === false
258
                || $entry[self::KEY_TIMESTAMP] === false
259
            ) {
260
                $timestamp = $this->getCreatedTimestamp($entry[self::KEY_PATH])['timestamp'];
261
262
                $entry[self::KEY_TIMESTAMP] = $timestamp;
263
264
                if ($timestamp > $directoryTimestamp) {
265
                    $directoryTimestamp = $timestamp;
266
                }
267
            }
268
        });
269
270
        /* @FIXME: It might be wise to use a filter to find the right entry instead of always using the first entry in the array. */
271
272
        $normalizeTreeMetadata[0]['timestamp'] = $directoryTimestamp;
273
274
        return $normalizeTreeMetadata;
275
    }
276
277
    /**
278
     * @param string $path
279
     *
280
     * @return null|string
281
     *
282
     * @throws \Github\Exception\ErrorException
283
     * @throws \Github\Exception\InvalidArgumentException
284
     * @throws \Github\Exception\RuntimeException
285
     */
286
    final public function guessMimeType($path)
287
    {
288
        //@NOTE: The github API does not return a MIME type, so we have to guess :-(
289
        $meta = $this->getMetaData($path);
290
291
        if ($this->hasKey($meta, self::KEY_TYPE) && $meta[self::KEY_TYPE] === self::KEY_DIRECTORY) {
292
            $mimeType = 'directory'; //application/x-directory
293
        } else {
294
            $content = $this->getFileContents($path);
295
            $mimeType = MimeType::detectByContent($content);
296
        }
297
298
        return $mimeType;
299
    }
300
301
    ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
302
    /**
303
     *
304
     * @throws \Github\Exception\InvalidArgumentException If no authentication method was given
305
     */
306
    private function authenticate()
307
    {
308
        if ($this->isAuthenticationAttempted === false) {
309
            $credentials = $this->settings->getCredentials();
310
311
            if (empty($credentials) === false) {
312
                $credentials = array_replace(
313
                    [null, null, null],
314
                    $credentials
315
                );
316
317
                $this->client->authenticate(
318
                    $credentials[1],
319
                    $credentials[2],
320
                    $credentials[0]
321
                );
322
            }
323
            $this->isAuthenticationAttempted = true;
324
        }
325
    }
326
327
    /**
328
     * @param array $tree
329
     * @param string $path
330
     * @param bool $recursive
331
     *
332
     * @return array
333
     */
334
    private function extractMetaDataFromTreeInfo(array $tree, $path, $recursive)
335
    {
336
        $matchPath = substr($path, 0, -1);
337
        $length = strlen($matchPath) - 1;
338
339
        $metadata = array_filter($tree, function ($entry) use ($matchPath, $recursive, $length) {
340
            $match = false;
341
342
            $entryPath = $entry[self::KEY_PATH];
343
344
            if ($matchPath === '' || strpos($entryPath, $matchPath) === 0) {
345
                if ($recursive === true) {
346
                    $match = true;
347
                } else {
348
                    $match = ($matchPath !== '' || strpos($entryPath, '/', $length) === false);
349
                }
350
            }
351
352
            return $match;
353
        });
354
355
        return array_values($metadata);
356
    }
357
358
    /**
359
     * @param $permissions
360
     * @return string
361
     */
362
    private function guessVisibility($permissions)
363
    {
364
        $visibility = AdapterInterface::VISIBILITY_PUBLIC;
365
366
        if (! substr($permissions, -4) & 0044) {
367
            $visibility = AdapterInterface::VISIBILITY_PRIVATE;
368
        }
369
370
        return $visibility;
371
    }
372
373
    /**
374
     * @param array $metadata
375
     *
376
     * @return array
377
     */
378
    private function normalizeTreeMetadata($metadata)
379
    {
380
        $result = [];
381
382
        if (is_array(current($metadata)) === false) {
383
            $metadata = [$metadata];
384
        }
385
386
        foreach ($metadata as $entry) {
387
            $this->setEntryName($entry);
388
            $this->setEntryType($entry);
389
            $this->setEntryVisibility($entry);
390
391
            $this->setDefaultValue($entry, self::KEY_CONTENTS);
392
            $this->setDefaultValue($entry, self::KEY_STREAM);
393
            $this->setDefaultValue($entry, self::KEY_TIMESTAMP);
394
395
396
            $result[] = $entry;
397
        }
398
399
        return $result;
400
    }
401
402
    /**
403
     * @param $path
404
     *
405
     * @return array
406
     *
407
     * @throws \Github\Exception\InvalidArgumentException
408
     */
409
    private function commitsForFile($path)
410
    {
411
        return $this->getRepositoryApi()->commits()->all(
412
            $this->settings->getVendor(),
413
            $this->settings->getPackage(),
414
            array(
415
                'sha' => $this->settings->getBranch(),
416
                'path' => $path
417
            )
418
        );
419
    }
420
421
    /**
422
     * @param array $entry
423
     * @param string $key
424
     * @param bool $default
425
     *
426
     * @return mixed
427
     */
428
    private function setDefaultValue(array &$entry, $key, $default = false)
429
    {
430
        if ($this->hasKey($entry, $key) === false) {
431
            $entry[$key] = $default;
432
        }
433
    }
434
435
    /**
436
     * @param $entry
437
     */
438
    private function setEntryType(&$entry)
439
    {
440
        if ($this->hasKey($entry, self::KEY_TYPE) === true) {
441
            switch ($entry[self::KEY_TYPE]) {
442
                case self::KEY_BLOB:
443
                    $entry[self::KEY_TYPE] = self::KEY_FILE;
444
                    break;
445
446
                case self::KEY_TREE:
447
                    $entry[self::KEY_TYPE] = self::KEY_DIRECTORY;
448
                    break;
449
            }
450
        }
451
    }
452
453
    /**
454
     * @param $entry
455
     */
456
    private function setEntryVisibility(&$entry)
457
    {
458
        if ($this->hasKey($entry, self::KEY_MODE)) {
459
            $entry[self::KEY_VISIBILITY] = $this->guessVisibility($entry[self::KEY_MODE]);
460
        } else {
461
            /* Assume public by default */
462
            $entry[self::KEY_VISIBILITY] = GithubAdapter::VISIBILITY_PUBLIC;
463
        }
464
    }
465
466
    /**
467
     * @param $entry
468
     */
469
    private function setEntryName(&$entry)
470
    {
471
        if ($this->hasKey($entry, self::KEY_NAME) === false) {
472
            if ($this->hasKey($entry, self::KEY_FILENAME) === true) {
473
                $entry[self::KEY_NAME] = $entry[self::KEY_FILENAME];
474
            } elseif ($this->hasKey($entry, self::KEY_PATH) === true) {
475
                $entry[self::KEY_NAME] = $entry[self::KEY_PATH];
476
            } else {
477
                $entry[self::KEY_NAME] = null;
478
            }
479
        }
480
    }
481
482
    /**
483
     * @param $metadata
484
     * @return bool
485
     */
486
    private function isMetadataForDirectory($metadata)
487
    {
488
        $isDirectory = false;
489
490
        $keys = array_keys($metadata);
491
492
        if ($keys[0] === 0) {
493
            $isDirectory = true;
494
        }
495
496
        return $isDirectory;
497
    }
498
499
    /**
500
     * @param $subject
501
     * @param $key
502
     * @return mixed
503
     */
504
    private function hasKey(&$subject, $key)
505
    {
506
        $keyExists = false;
507
508
        if (is_array($subject)) {
509
        /** @noinspection ReferenceMismatchInspection */
510
            $keyExists = array_key_exists($key, $subject);
511
        }
512
513
        return $keyExists;
514
    }
515
}
516