Test Failed
Push — main ( fb0dfd...39d50b )
by Gabor
24:01
created

Adapter::limitFileList()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 3
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 7
rs 10
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
    public function __construct(private S3Client $s3Client)
43
    {
44
    }
45
46
    /**
47
     * Set active bucket.
48
     *
49
     * @param string $bucket
50
     */
51
    public function setBucket(string $bucket): void
52
    {
53
        $this->bucket = $bucket;
54
    }
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
    public function getLastUploadedKeyByPrefix(string $keyPrefix): ?string
63
    {
64
        $object = $this->getObjectListByPrefix($keyPrefix, self::OBJECT_SORT_BY_DATE_DESC, 1);
65
66
        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
    public function getObjectListByPrefix(string $keyPrefix, string $sortBy = null, int $limit = 0): array
79
    {
80
        $options = [
81
            'Bucket' => $this->bucket,
82
            'EncodingType' => 'url',
83
            'Prefix' => $keyPrefix,
84
            'RequestPayer' => 'requester'
85
        ];
86
87
        // We can add a query limit here only when we don't want any special sorting.
88
        // The default chunk limit is 1000. Probably the guys at the AWS know why not recommended to go over this limit
89
        // so I won't do either.
90
        if (empty($sortBy) && $limit > 0 && $limit < self::AWS_DEFAULT_LIST_LIMIT) {
91
            $options['MaxKeys'] = $limit;
92
            // Set the parameter to 0 to avoid the unnecessary array_chunk later.
93
            $limit = 0;
94
        }
95
96
        $results = $this->fetchFullFileList($options);
97
        $this->sortFileList($results, $sortBy);
98
        $this->limitFileList($results, $limit);
99
100
        return $results;
101
    }
102
103
    /**
104
     * Fetches full file list.
105
     *
106
     * @param array $options
107
     * @return array
108
     */
109
    protected function fetchFullFileList(array $options): array
110
    {
111
        $results = [];
112
        $continuationToken = '';
113
114
        do {
115
            $options['ContinuationToken'] = $continuationToken;
116
117
            $response = $this->s3Client->listObjectsV2($options);
118
119
            if (empty($response['Contents'])) {
120
                break;
121
            }
122
123
            $results[] = $response['Contents'];
124
            $continuationToken = $response['NextContinuationToken'];
125
            $isTruncated = $response['IsTruncated'];
126
            usleep(50000); // 50 ms pause to avoid CPU spikes
127
        } while ($isTruncated);
128
129
        return array_merge([], ...$results);
130
    }
131
132
    /**
133
     * Sorts file list by name or date, ascending or descending.
134
     *
135
     * @param array $fileList
136
     * @param string|null $sortBy
137
     */
138
    protected function sortFileList(array &$fileList, ?string $sortBy): void
139
    {
140
        if (empty($sortBy) || !in_array($sortBy, $this->validSortByKeys, true)) {
141
            return;
142
        }
143
144
        $direction = $sortBy[0] === '^' ? 'asc' : 'desc';
145
        $sortByKey = substr($sortBy, 1);
146
147
        usort($fileList, static function ($a, $b) use ($direction, $sortByKey) {
148
            $cmp = strcmp($a[$sortByKey], $b[$sortByKey]);
149
            return $direction === 'asc' ? $cmp : -$cmp;
150
        });
151
    }
152
153
    /**
154
     * Limit the file list at most the given number of items.
155
     *
156
     * @param array $fileList
157
     * @param int $limit
158
     */
159
    protected function limitFileList(array &$fileList, int $limit): void
160
    {
161
        if (empty($fileList) || $limit <= 0) {
162
            return;
163
        }
164
165
        $fileList = array_chunk($fileList, $limit)[0];
166
    }
167
}
168