1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 package com.s3auth.hosts;
31
32 import com.amazonaws.services.s3.AmazonS3;
33 import com.amazonaws.services.s3.model.GetObjectRequest;
34 import com.amazonaws.services.s3.model.ObjectMetadata;
35 import com.amazonaws.services.s3.model.S3Object;
36 import com.jcabi.aspects.Loggable;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.OutputStream;
40 import java.net.HttpURLConnection;
41 import java.util.Collection;
42 import java.util.Date;
43 import java.util.LinkedList;
44 import javax.validation.constraints.NotNull;
45 import javax.ws.rs.core.HttpHeaders;
46 import lombok.EqualsAndHashCode;
47 import org.apache.commons.io.IOUtils;
48 import org.apache.commons.lang3.StringUtils;
49
50
51
52
53
54
55
56
57
58
59 @EqualsAndHashCode(of = { "bucket", "key", "range" })
60 @Loggable(Loggable.DEBUG)
61 @SuppressWarnings("PMD.TooManyMethods")
62 final class DefaultResource implements Resource {
63
64
65
66 private final transient AmazonS3 client;
67
68
69
70
71 private final transient String bucket;
72
73
74
75
76 private final transient String key;
77
78
79
80
81 private final transient Range range;
82
83
84
85
86 private final transient Version version;
87
88
89
90
91 private final transient S3Object object;
92
93
94
95
96 private final transient DomainStatsData stats;
97
98
99
100
101
102
103
104
105
106
107
108 DefaultResource(@NotNull final AmazonS3 clnt,
109 @NotNull final String bckt, @NotNull final String name,
110 @NotNull final Range rng, @NotNull final Version ver,
111 @NotNull final DomainStatsData dstats) {
112 this.client = clnt;
113 this.bucket = bckt;
114 this.key = name;
115 this.range = rng;
116 this.version = ver;
117 this.object = this.client.getObject(
118 this.request(this.range, this.version)
119 );
120 this.stats = dstats;
121 }
122
123 @Override
124 public String toString() {
125 return String.format("%s:%s", this.bucket, this.key);
126 }
127
128 @Override
129 public int status() {
130 final int status;
131 if (this.range.equals(Range.ENTIRE)) {
132 status = HttpURLConnection.HTTP_OK;
133 } else {
134 status = HttpURLConnection.HTTP_PARTIAL;
135 }
136 return status;
137 }
138
139 @Override
140 @Loggable(
141 value = Loggable.DEBUG, limit = Integer.MAX_VALUE,
142 ignore = DefaultResource.StreamingException.class
143 )
144 public long writeTo(@NotNull final OutputStream output) throws IOException {
145 final InputStream input = this.object.getObjectContent();
146 assert input != null;
147 int total = 0;
148
149 final byte[] buffer = new byte[16 * 1024];
150 try {
151 while (true) {
152 final int count;
153 try {
154 count = input.read(buffer);
155 } catch (final IOException ex) {
156 throw new DefaultResource.StreamingException(
157 String.format(
158 "failed to read %s/%s, range=%s, total=%d",
159 this.bucket,
160 this.key,
161 this.range,
162 total
163 ),
164 ex
165 );
166 }
167 if (count == -1) {
168 break;
169 }
170 try {
171 output.write(buffer, 0, count);
172 } catch (final IOException ex) {
173 throw new DefaultResource.StreamingException(
174 String.format(
175
176 "failed to write %s/%s, range=%s, total=%d, count=%d",
177 this.bucket,
178 this.key,
179 this.range,
180 total,
181 count
182 ),
183 ex
184 );
185 }
186 total += count;
187 }
188 this.stats.put(this.bucket, new Stats.Simple(total));
189 } finally {
190 input.close();
191 }
192 return total;
193 }
194
195 @Override
196 @NotNull
197 public Collection<String> headers() {
198 final ObjectMetadata meta = this.object.getObjectMetadata();
199 final Collection<String> headers = new LinkedList<String>();
200 headers.add(
201 DefaultResource.header(
202 HttpHeaders.CONTENT_LENGTH,
203 Long.toString(meta.getContentLength())
204 )
205 );
206 if (meta.getContentType() != null) {
207 headers.add(
208 DefaultResource.header(
209 HttpHeaders.CONTENT_TYPE,
210 meta.getContentType()
211 )
212 );
213 }
214 if (meta.getContentEncoding() != null) {
215 headers.add(
216 DefaultResource.header(
217 HttpHeaders.CONTENT_ENCODING,
218 meta.getContentEncoding()
219 )
220 );
221 }
222 if (meta.getETag() != null) {
223 headers.add(
224 DefaultResource.header(
225 HttpHeaders.ETAG,
226 meta.getETag()
227 )
228 );
229 }
230 headers.add(
231 DefaultResource.header(
232 HttpHeaders.CACHE_CONTROL,
233 StringUtils.defaultString(
234 meta.getCacheControl(),
235 "must-revalidate"
236 )
237 )
238 );
239 headers.add(DefaultResource.header("Accept-Ranges", "bytes"));
240 if (!this.range.equals(Range.ENTIRE)) {
241 headers.add(
242 DefaultResource.header(
243 "Content-Range",
244 String.format(
245 "bytes %d-%d/%d",
246 this.range.first(),
247 this.range.last(),
248 this.size()
249 )
250 )
251 );
252 }
253 return headers;
254 }
255
256 @Override
257 @NotNull
258 public String etag() {
259 return this.object.getObjectMetadata().getETag();
260 }
261
262 @Override
263 public Date lastModified() {
264 return new Date(
265 this.object.getObjectMetadata().getLastModified().getTime()
266 );
267 }
268
269 @Override
270 public String contentType() {
271 return this.object.getObjectMetadata().getContentType();
272 }
273
274 @Override
275 public void close() throws IOException {
276 this.object.close();
277 }
278
279
280
281
282
283
284
285 @NotNull
286 private static String header(@NotNull final String name,
287 @NotNull final String value) {
288 return String.format("%s: %s", name, value);
289 }
290
291
292
293
294
295
296
297 private GetObjectRequest request(final Range rng, final Version ver) {
298 final GetObjectRequest request =
299 new GetObjectRequest(this.bucket, this.key);
300 if (!rng.equals(Range.ENTIRE)) {
301 request.withRange(rng.first(), rng.last());
302 }
303 if (!ver.latest()) {
304 request.withVersionId(ver.version());
305 }
306 return request;
307 }
308
309
310
311
312
313 private long size() {
314 final long size;
315 if (this.range.equals(Range.ENTIRE)) {
316 size = this.object.getObjectMetadata().getContentLength();
317 } else {
318 S3Object obj = null;
319 try {
320 obj = this.client.getObject(
321 this.request(Range.ENTIRE, this.version)
322 );
323 size = obj.getObjectMetadata().getContentLength();
324 } finally {
325 IOUtils.closeQuietly(obj);
326 }
327 }
328 return size;
329 }
330
331
332
333
334 private static final class StreamingException extends IOException {
335
336
337
338 private static final long serialVersionUID = 0x7529FA781E111179L;
339
340
341
342
343
344 StreamingException(final String cause, final Throwable thr) {
345 super(
346 String.format("%s: '%s'", cause, thr.getMessage()),
347 thr
348 );
349 }
350 }
351
352 }