Passed
Push — main ( 2521b1...eda7c4 )
by Gabor
07:39
created

Adapter::getSearchOptions()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 4
nop 3
dl 0
loc 23
ccs 13
cts 13
cp 1
crap 4
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Worst Practice Aws S3 Adapter
5
 *
6
 * PHP version 8.0
7
 *
8
 * @copyright 2021 Worst Practice
9
 * @license   https://opensource.org/licenses/MIT The MIT License (MIT)
10
 *
11
 * @link http://www.worstpractice.dev
12
 */
13
14
declare(strict_types=1);
15
16
namespace WorstPractice\Component\Aws\S3;
17
18
use Aws\S3\S3Client;
19
20
/**
21
 * AWS S3 adapter.
22
 */
23
class Adapter implements AdapterInterface
24
{
25
    /** @var string */
26
    private string $bucket;
27
    /** @var string[] */
28
    private array $validSortByKeys = [
29
        self::OBJECT_SORT_BY_NAME,
30
        self::OBJECT_SORT_BY_NAME_DESC,
31
        self::OBJECT_SORT_BY_DATE,
32
        self::OBJECT_SORT_BY_DATE_DESC,
33
    ];
34
35
    /**
36
     * Adapter constructor.
37
     *
38
     * @param S3Client $s3Client  The AWS SDK S3 Client instance
39
     *
40
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
41
     */
42 16
    public function __construct(private S3Client $s3Client)
43
    {
44 16
    }
45
46
    /**
47
     * Set active bucket.
48
     *
49
     * @param string $bucket
50
     */
51 16
    public function setBucket(string $bucket): void
52
    {
53 16
        $this->bucket = $bucket;
54 16
    }
55
56
    /**
57
     * Gets the last uploaded object's key in the given S3 bucket by key prefix.
58
     *
59
     * @param string $keyPrefix
60
     * @return string|null
61
     */
62 2
    public function getLastUploadedKeyByPrefix(string $keyPrefix): ?string
63
    {
64 2
        $object = $this->getObjectListByPrefix($keyPrefix, self::OBJECT_SORT_BY_DATE_DESC, 1);
65
66 2
        return $object[0]['Key'] ?? null;
67
    }
68
69
    /**
70
     * Gets the object list for the given S3 bucket by key prefix.
71
     *
72
     * @param string $keyPrefix The path on the S3 bucket. Mandatory.
73
     * @param string|null $sortBy Sort the results by the given key.
74
     *                            Use the constants because this parameter requires special values.
75
     * @param int $limit Limit the results. 0 means return all.
76
     * @return array
77
     */
78 16
    public function getObjectListByPrefix(string $keyPrefix, string $sortBy = null, int $limit = 0): array
79
    {
80 16
        $options = $this->getSearchOptions($keyPrefix, $sortBy, $limit);
81
82 16
        $results = $this->fetchFullFileList($options);
83
        // Avoid sort if not needed.
84 16
        $sortBy !== self::OBJECT_SORT_BY_NAME && $this->sortFileList($results, $sortBy);
85
        // Avoid limit if not needed.
86 16
        $limit && $this->limitFileList($results, $limit);
87
88 16
        return $results;
89
    }
90
91
    /**
92
     * Create the search options, and modify the sort and limit attributes if necessary.
93
     *
94
     * @param string $keyPrefix
95
     * @param string|null $sortBy
96
     * @param int $limit
97
     * @return array
98
     */
99 16
    private function getSearchOptions(string $keyPrefix, ?string &$sortBy, int &$limit): array
100
    {
101 16
        $options = [
102 16
            'Bucket' => $this->bucket,
103 16
            'EncodingType' => 'url',
104 16
            'Prefix' => $keyPrefix,
105 16
            'RequestPayer' => 'requester'
106
        ];
107
108 16
        if (empty($sortBy)) {
109 5
            $sortBy = self::OBJECT_SORT_BY_NAME;
110
        }
111
112 16
        $limit = (int) abs($limit);
113
114
        // We can add a query limit here only when we don't want any special sorting.
115 16
        if ($sortBy === self::OBJECT_SORT_BY_NAME && $limit < self::AWS_DEFAULT_LIST_LIMIT) {
116 7
            $options['MaxKeys'] = $limit;
117
            // Set the parameter to 0 to avoid the unnecessary array_chunk later.
118 7
            $limit = 0;
119
        }
120
121 16
        return $options;
122
    }
123
124
    /**
125
     * Fetches full file list.
126
     *
127
     * @param array $options
128
     * @return array
129
     */
130 16
    private function fetchFullFileList(array $options): array
131
    {
132 16
        $results = [];
133 16
        $continuationToken = '';
134
135
        do {
136 16
            $options['ContinuationToken'] = $continuationToken;
137
138 16
            $response = $this->s3Client->listObjectsV2($options);
139
140 16
            if (empty($response['Contents'])) {
141 3
                break;
142
            }
143
144 13
            $results[] = $response['Contents'];
145 13
            $continuationToken = $response['NextContinuationToken'];
146 13
            $isTruncated = $response['IsTruncated'];
147 13
            usleep(50000); // 50 ms pause to avoid CPU spikes
148 13
        } while ($isTruncated);
149
150 16
        return array_merge([], ...$results);
151
    }
152
153
    /**
154
     * Sorts file list by name or date, ascending or descending.
155
     *
156
     * @param array $fileList
157
     * @param string|null $sortBy
158
     * @return bool
159
     */
160 9
    private function sortFileList(array &$fileList, ?string $sortBy): bool
161
    {
162 9
        if (empty($fileList) || empty($sortBy) || !in_array($sortBy, $this->validSortByKeys, true)) {
163 2
            return false;
164
        }
165
166 7
        $direction = $sortBy[0] === '^' ? 'asc' : 'desc';
167 7
        $sortByKey = substr($sortBy, 1);
168
169 7
        usort($fileList, static function ($a, $b) use ($direction, $sortByKey) {
170 6
            $cmp = strcmp($a[$sortByKey], $b[$sortByKey]);
171 6
            return $direction === 'asc' ? $cmp : -$cmp;
172 7
        });
173
174 7
        return true;
175
    }
176
177
    /**
178
     * Limit the file list at most the given number of items.
179
     *
180
     * @param array $fileList
181
     * @param int $limit
182
     * @return bool
183
     */
184 3
    private function limitFileList(array &$fileList, int $limit): bool
185
    {
186 3
        if (empty($fileList) || $limit <= 0) {
187 1
            return false;
188
        }
189
190 2
        $fileList = array_chunk($fileList, $limit)[0];
191
192 2
        return true;
193
    }
194
}
195