Passed
Push — master ( 76d448...3ef509 )
by Robin
17:04 queued 13s
created

S3ObjectTrait::readObject()   B

Complexity

Conditions 6
Paths 2

Size

Total Lines 39
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 6
eloc 27
c 1
b 0
f 1
nc 2
nop 1
dl 0
loc 39
rs 8.8657
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Robin Appelman <[email protected]>
4
 *
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Florent <[email protected]>
7
 * @author Morris Jobke <[email protected]>
8
 * @author Robin Appelman <[email protected]>
9
 * @author Roeland Jago Douma <[email protected]>
10
 *
11
 * @license GNU AGPL version 3 or any later version
12
 *
13
 * This program is free software: you can redistribute it and/or modify
14
 * it under the terms of the GNU Affero General Public License as
15
 * published by the Free Software Foundation, either version 3 of the
16
 * License, or (at your option) any later version.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License
24
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25
 *
26
 */
27
namespace OC\Files\ObjectStore;
28
29
use Aws\S3\Exception\S3MultipartUploadException;
30
use Aws\S3\MultipartUploader;
31
use Aws\S3\S3Client;
32
use GuzzleHttp\Psr7;
33
use GuzzleHttp\Psr7\Utils;
34
use OC\Files\Stream\SeekableHttpStream;
35
use Psr\Http\Message\StreamInterface;
36
37
trait S3ObjectTrait {
38
	/**
39
	 * Returns the connection
40
	 *
41
	 * @return S3Client connected client
42
	 * @throws \Exception if connection could not be made
43
	 */
44
	abstract protected function getConnection();
45
46
	abstract protected function getCertificateBundlePath(): ?string;
47
	abstract protected function getSSECParameters(bool $copy = false): array;
48
49
	/**
50
	 * @param string $urn the unified resource name used to identify the object
51
	 *
52
	 * @return resource stream with the read data
53
	 * @throws \Exception when something goes wrong, message will be logged
54
	 * @since 7.0.0
55
	 */
56
	public function readObject($urn) {
57
		$fh = SeekableHttpStream::open(function ($range) use ($urn) {
58
			$command = $this->getConnection()->getCommand('GetObject', [
59
				'Bucket' => $this->bucket,
60
				'Key' => $urn,
61
				'Range' => 'bytes=' . $range,
62
			] + $this->getSSECParameters());
63
			$request = \Aws\serialize($command);
64
			$headers = [];
65
			foreach ($request->getHeaders() as $key => $values) {
66
				foreach ($values as $value) {
67
					$headers[] = "$key: $value";
68
				}
69
			}
70
			$opts = [
71
				'http' => [
72
					'protocol_version' => $request->getProtocolVersion(),
73
					'header' => $headers,
74
				]
75
			];
76
			$bundle = $this->getCertificateBundlePath();
77
			if ($bundle) {
78
				$opts['ssl'] = [
79
					'cafile' => $bundle
80
				];
81
			}
82
83
			if ($this->getProxy()) {
0 ignored issues
show
Bug introduced by
It seems like getProxy() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

83
			if ($this->/** @scrutinizer ignore-call */ getProxy()) {
Loading history...
84
				$opts['http']['proxy'] = $this->getProxy();
85
				$opts['http']['request_fulluri'] = true;
86
			}
87
88
			$context = stream_context_create($opts);
89
			return fopen($request->getUri(), 'r', false, $context);
90
		});
91
		if (!$fh) {
0 ignored issues
show
introduced by
$fh is of type resource, thus it always evaluated to false.
Loading history...
92
			throw new \Exception("Failed to read object $urn");
93
		}
94
		return $fh;
95
	}
96
97
98
	/**
99
	 * Single object put helper
100
	 *
101
	 * @param string $urn the unified resource name used to identify the object
102
	 * @param StreamInterface $stream stream with the data to write
103
	 * @param string|null $mimetype the mimetype to set for the remove object @since 22.0.0
104
	 * @throws \Exception when something goes wrong, message will be logged
105
	 */
106
	protected function writeSingle(string $urn, StreamInterface $stream, string $mimetype = null): void {
107
		$this->getConnection()->putObject([
108
			'Bucket' => $this->bucket,
109
			'Key' => $urn,
110
			'Body' => $stream,
111
			'ACL' => 'private',
112
			'ContentType' => $mimetype,
113
			'StorageClass' => $this->storageClass,
114
		] + $this->getSSECParameters());
115
	}
116
117
118
	/**
119
	 * Multipart upload helper that tries to avoid orphaned fragments in S3
120
	 *
121
	 * @param string $urn the unified resource name used to identify the object
122
	 * @param StreamInterface $stream stream with the data to write
123
	 * @param string|null $mimetype the mimetype to set for the remove object
124
	 * @throws \Exception when something goes wrong, message will be logged
125
	 */
126
	protected function writeMultiPart(string $urn, StreamInterface $stream, string $mimetype = null): void {
127
		$uploader = new MultipartUploader($this->getConnection(), $stream, [
128
			'bucket' => $this->bucket,
129
			'key' => $urn,
130
			'part_size' => $this->uploadPartSize,
131
			'params' => [
132
				'ContentType' => $mimetype,
133
				'StorageClass' => $this->storageClass,
134
			] + $this->getSSECParameters(),
135
		]);
136
137
		try {
138
			$uploader->upload();
139
		} catch (S3MultipartUploadException $e) {
140
			// if anything goes wrong with multipart, make sure that you don´t poison and
141
			// slow down s3 bucket with orphaned fragments
142
			$uploadInfo = $e->getState()->getId();
143
			if ($e->getState()->isInitiated() && (array_key_exists('UploadId', $uploadInfo))) {
144
				$this->getConnection()->abortMultipartUpload($uploadInfo);
145
			}
146
			throw new \OCA\DAV\Connector\Sabre\Exception\BadGateway("Error while uploading to S3 bucket", 0, $e);
147
		}
148
	}
149
150
151
	/**
152
	 * @param string $urn the unified resource name used to identify the object
153
	 * @param resource $stream stream with the data to write
154
	 * @param string|null $mimetype the mimetype to set for the remove object @since 22.0.0
155
	 * @throws \Exception when something goes wrong, message will be logged
156
	 * @since 7.0.0
157
	 */
158
	public function writeObject($urn, $stream, string $mimetype = null) {
159
		$psrStream = Utils::streamFor($stream);
160
161
		// ($psrStream->isSeekable() && $psrStream->getSize() !== null) evaluates to true for a On-Seekable stream
162
		// so the optimisation does not apply
163
		$buffer = new Psr7\Stream(fopen("php://memory", 'rwb+'));
164
		Utils::copyToStream($psrStream, $buffer, $this->putSizeLimit);
165
		$buffer->seek(0);
166
		if ($buffer->getSize() < $this->putSizeLimit) {
167
			// buffer is fully seekable, so use it directly for the small upload
168
			$this->writeSingle($urn, $buffer, $mimetype);
169
		} else {
170
			$loadStream = new Psr7\AppendStream([$buffer, $psrStream]);
171
			$this->writeMultiPart($urn, $loadStream, $mimetype);
172
		}
173
	}
174
175
	/**
176
	 * @param string $urn the unified resource name used to identify the object
177
	 * @return void
178
	 * @throws \Exception when something goes wrong, message will be logged
179
	 * @since 7.0.0
180
	 */
181
	public function deleteObject($urn) {
182
		$this->getConnection()->deleteObject([
183
			'Bucket' => $this->bucket,
184
			'Key' => $urn,
185
		]);
186
	}
187
188
	public function objectExists($urn) {
189
		return $this->getConnection()->doesObjectExist($this->bucket, $urn, $this->getSSECParameters());
190
	}
191
192
	public function copyObject($from, $to) {
193
		$this->getConnection()->copy($this->getBucket(), $from, $this->getBucket(), $to, 'private', [
0 ignored issues
show
Bug introduced by
It seems like getBucket() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

193
		$this->getConnection()->copy($this->/** @scrutinizer ignore-call */ getBucket(), $from, $this->getBucket(), $to, 'private', [
Loading history...
194
			'params' => $this->getSSECParameters() + $this->getSSECParameters(true)
195
		]);
196
	}
197
}
198