001/* 002 * (C) Copyright 2011-2014 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Mathieu Guillaume 016 * Florent Guillaume 017 */ 018package org.nuxeo.ecm.core.storage.sql; 019 020import static org.apache.commons.lang.StringUtils.isBlank; 021import static org.apache.commons.lang.StringUtils.isNotBlank; 022 023import java.io.File; 024import java.io.FileInputStream; 025import java.io.IOException; 026import java.net.URI; 027import java.net.URISyntaxException; 028import java.net.URL; 029import java.security.GeneralSecurityException; 030import java.security.KeyPair; 031import java.security.KeyStore; 032import java.security.PrivateKey; 033import java.security.PublicKey; 034import java.security.cert.Certificate; 035import java.util.Date; 036import java.util.HashSet; 037import java.util.Map; 038import java.util.Set; 039import java.util.regex.Pattern; 040 041import javax.servlet.http.HttpServletRequest; 042 043import org.apache.commons.lang.StringUtils; 044import org.apache.commons.logging.Log; 045import org.apache.commons.logging.LogFactory; 046import org.nuxeo.common.Environment; 047import org.nuxeo.common.utils.RFC2231; 048import org.nuxeo.ecm.core.api.Blob; 049import org.nuxeo.ecm.core.blob.BlobManager.BlobInfo; 050import org.nuxeo.ecm.core.blob.BlobManager.UsageHint; 051import org.nuxeo.ecm.core.blob.BlobProvider; 052import org.nuxeo.ecm.core.blob.ManagedBlob; 053import org.nuxeo.ecm.core.blob.binary.BinaryBlobProvider; 054import org.nuxeo.ecm.core.blob.binary.BinaryGarbageCollector; 055import org.nuxeo.ecm.core.blob.binary.BinaryManagerStatus; 056import org.nuxeo.ecm.core.blob.binary.CachingBinaryManager; 057import org.nuxeo.ecm.core.blob.binary.FileStorage; 058import org.nuxeo.ecm.core.io.download.DownloadHelper; 059import org.nuxeo.ecm.core.model.Document; 060import org.nuxeo.runtime.api.Framework; 061 062import com.amazonaws.AmazonClientException; 063import com.amazonaws.AmazonServiceException; 064import com.amazonaws.ClientConfiguration; 065import com.amazonaws.HttpMethod; 066import com.amazonaws.auth.AWSCredentialsProvider; 067import com.amazonaws.auth.InstanceProfileCredentialsProvider; 068import com.amazonaws.services.s3.AmazonS3; 069import com.amazonaws.services.s3.AmazonS3Client; 070import com.amazonaws.services.s3.AmazonS3EncryptionClient; 071import com.amazonaws.services.s3.internal.ServiceUtils; 072import com.amazonaws.services.s3.model.CannedAccessControlList; 073import com.amazonaws.services.s3.model.CryptoConfiguration; 074import com.amazonaws.services.s3.model.EncryptedPutObjectRequest; 075import com.amazonaws.services.s3.model.EncryptionMaterials; 076import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; 077import com.amazonaws.services.s3.model.GetObjectRequest; 078import com.amazonaws.services.s3.model.ObjectListing; 079import com.amazonaws.services.s3.model.ObjectMetadata; 080import com.amazonaws.services.s3.model.PutObjectRequest; 081import com.amazonaws.services.s3.model.S3ObjectSummary; 082import com.amazonaws.services.s3.model.StaticEncryptionMaterialsProvider; 083import com.amazonaws.services.s3.transfer.TransferManager; 084import com.amazonaws.services.s3.transfer.Upload; 085import com.amazonaws.services.s3.transfer.model.UploadResult; 086import com.google.common.base.Objects; 087 088/** 089 * A Binary Manager that stores binaries as S3 BLOBs 090 * <p> 091 * The BLOBs are cached locally on first access for efficiency. 092 * <p> 093 * Because the BLOB length can be accessed independently of the binary stream, it is also cached in a simple text file 094 * if accessed before the stream. 095 */ 096public class S3BinaryManager extends CachingBinaryManager implements BlobProvider { 097 098 private static final String MD5 = "MD5"; // must be MD5 for Etag 099 100 @Override 101 protected String getDefaultDigestAlgorithm() { 102 return MD5; 103 } 104 105 private static final Log log = LogFactory.getLog(S3BinaryManager.class); 106 107 public static final String BUCKET_NAME_KEY = "nuxeo.s3storage.bucket"; 108 109 public static final String BUCKET_PREFIX_KEY = "nuxeo.s3storage.bucket.prefix"; 110 111 public static final String BUCKET_REGION_KEY = "nuxeo.s3storage.region"; 112 113 public static final String DEFAULT_BUCKET_REGION = null; // US East 114 115 public static final String AWS_ID_KEY = "nuxeo.s3storage.awsid"; 116 117 public static final String AWS_ID_ENV_KEY = "AWS_ACCESS_KEY_ID"; 118 119 public static final String AWS_SECRET_KEY = "nuxeo.s3storage.awssecret"; 120 121 public static final String AWS_SECRET_ENV_KEY = "AWS_SECRET_ACCESS_KEY"; 122 123 public static final String CACHE_SIZE_KEY = "nuxeo.s3storage.cachesize"; 124 125 public static final String DEFAULT_CACHE_SIZE = "100 MB"; 126 127 /** AWS ClientConfiguration default 50 */ 128 public static final String CONNECTION_MAX_KEY = "nuxeo.s3storage.connection.max"; 129 130 /** AWS ClientConfiguration default 3 (with exponential backoff) */ 131 public static final String CONNECTION_RETRY_KEY = "nuxeo.s3storage.connection.retry"; 132 133 /** AWS ClientConfiguration default 50*1000 = 50s */ 134 public static final String CONNECTION_TIMEOUT_KEY = "nuxeo.s3storage.connection.timeout"; 135 136 /** AWS ClientConfiguration default 50*1000 = 50s */ 137 public static final String SOCKET_TIMEOUT_KEY = "nuxeo.s3storage.socket.timeout"; 138 139 public static final String KEYSTORE_FILE_KEY = "nuxeo.s3storage.crypt.keystore.file"; 140 141 public static final String KEYSTORE_PASS_KEY = "nuxeo.s3storage.crypt.keystore.password"; 142 143 public static final String PRIVKEY_ALIAS_KEY = "nuxeo.s3storage.crypt.key.alias"; 144 145 public static final String PRIVKEY_PASS_KEY = "nuxeo.s3storage.crypt.key.password"; 146 147 public static final String ENDPOINT_KEY = "nuxeo.s3storage.endpoint"; 148 149 public static final String DIRECTDOWNLOAD_KEY = "nuxeo.s3storage.downloadfroms3"; 150 151 public static final String DEFAULT_DIRECTDOWNLOAD = "false"; 152 153 public static final String DIRECTDOWNLOAD_EXPIRE_KEY = "nuxeo.s3storage.downloadfroms3.expire"; 154 155 public static final int DEFAULT_DIRECTDOWNLOAD_EXPIRE = 60 * 60; // 1h 156 157 private static final Pattern MD5_RE = Pattern.compile("(.*/)?[0-9a-f]{32}"); 158 159 protected String bucketName; 160 161 protected String bucketNamePrefix; 162 163 protected AWSCredentialsProvider awsCredentialsProvider; 164 165 protected ClientConfiguration clientConfiguration; 166 167 protected EncryptionMaterials encryptionMaterials; 168 169 protected boolean isEncrypted; 170 171 protected CryptoConfiguration cryptoConfiguration; 172 173 protected AmazonS3 amazonS3; 174 175 protected TransferManager transferManager; 176 177 protected boolean directDownload; 178 179 protected int directDownloadExpire; 180 181 @Override 182 public void initialize(String blobProviderId, Map<String, String> properties) throws IOException { 183 super.initialize(blobProviderId, properties); 184 185 // Get settings from the configuration 186 // TODO parse properties too 187 bucketName = Framework.getProperty(BUCKET_NAME_KEY); 188 bucketNamePrefix = Objects.firstNonNull(Framework.getProperty(BUCKET_PREFIX_KEY), StringUtils.EMPTY); 189 String bucketRegion = Framework.getProperty(BUCKET_REGION_KEY); 190 if (isBlank(bucketRegion)) { 191 bucketRegion = DEFAULT_BUCKET_REGION; 192 } 193 String awsID = Framework.getProperty(AWS_ID_KEY); 194 String awsSecret = Framework.getProperty(AWS_SECRET_KEY); 195 196 String proxyHost = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_HOST); 197 String proxyPort = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_PORT); 198 String proxyLogin = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_LOGIN); 199 String proxyPassword = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_PASSWORD); 200 201 String cacheSizeStr = Framework.getProperty(CACHE_SIZE_KEY); 202 if (isBlank(cacheSizeStr)) { 203 cacheSizeStr = DEFAULT_CACHE_SIZE; 204 } 205 206 int maxConnections = getIntProperty(CONNECTION_MAX_KEY); 207 int maxErrorRetry = getIntProperty(CONNECTION_RETRY_KEY); 208 int connectionTimeout = getIntProperty(CONNECTION_TIMEOUT_KEY); 209 int socketTimeout = getIntProperty(SOCKET_TIMEOUT_KEY); 210 211 String keystoreFile = Framework.getProperty(KEYSTORE_FILE_KEY); 212 String keystorePass = Framework.getProperty(KEYSTORE_PASS_KEY); 213 String privkeyAlias = Framework.getProperty(PRIVKEY_ALIAS_KEY); 214 String privkeyPass = Framework.getProperty(PRIVKEY_PASS_KEY); 215 String endpoint = Framework.getProperty(ENDPOINT_KEY); 216 217 // Fallback on default env keys for ID and secret 218 if (isBlank(awsID)) { 219 awsID = System.getenv(AWS_ID_ENV_KEY); 220 } 221 if (isBlank(awsSecret)) { 222 awsSecret = System.getenv(AWS_SECRET_ENV_KEY); 223 } 224 225 if (isBlank(bucketName)) { 226 throw new RuntimeException("Missing conf: " + BUCKET_NAME_KEY); 227 } 228 229 if (!isBlank(bucketNamePrefix) && !bucketNamePrefix.endsWith("/")) { 230 log.warn(String.format("%s %s S3 bucket prefix should end by '/' " + ": added automatically.", 231 BUCKET_PREFIX_KEY, bucketNamePrefix)); 232 bucketNamePrefix += "/"; 233 } 234 // set up credentials 235 if (isBlank(awsID) || isBlank(awsSecret)) { 236 awsCredentialsProvider = new InstanceProfileCredentialsProvider(); 237 try { 238 awsCredentialsProvider.getCredentials(); 239 } catch (AmazonClientException e) { 240 throw new RuntimeException("Missing AWS credentials and no instance role found"); 241 } 242 } else { 243 awsCredentialsProvider = new BasicAWSCredentialsProvider(awsID, awsSecret); 244 } 245 246 // set up client configuration 247 clientConfiguration = new ClientConfiguration(); 248 if (isNotBlank(proxyHost)) { 249 clientConfiguration.setProxyHost(proxyHost); 250 } 251 if (isNotBlank(proxyPort)) { 252 clientConfiguration.setProxyPort(Integer.parseInt(proxyPort)); 253 } 254 if (isNotBlank(proxyLogin)) { 255 clientConfiguration.setProxyUsername(proxyLogin); 256 } 257 if (proxyPassword != null) { // could be blank 258 clientConfiguration.setProxyPassword(proxyPassword); 259 } 260 if (maxConnections > 0) { 261 clientConfiguration.setMaxConnections(maxConnections); 262 } 263 if (maxErrorRetry >= 0) { // 0 is allowed 264 clientConfiguration.setMaxErrorRetry(maxErrorRetry); 265 } 266 if (connectionTimeout >= 0) { // 0 is allowed 267 clientConfiguration.setConnectionTimeout(connectionTimeout); 268 } 269 if (socketTimeout >= 0) { // 0 is allowed 270 clientConfiguration.setSocketTimeout(socketTimeout); 271 } 272 273 // set up encryption 274 encryptionMaterials = null; 275 if (isNotBlank(keystoreFile)) { 276 boolean confok = true; 277 if (keystorePass == null) { // could be blank 278 log.error("Keystore password missing"); 279 confok = false; 280 } 281 if (isBlank(privkeyAlias)) { 282 log.error("Key alias missing"); 283 confok = false; 284 } 285 if (privkeyPass == null) { // could be blank 286 log.error("Key password missing"); 287 confok = false; 288 } 289 if (!confok) { 290 throw new RuntimeException("S3 Crypto configuration incomplete"); 291 } 292 try { 293 // Open keystore 294 File ksFile = new File(keystoreFile); 295 FileInputStream ksStream = new FileInputStream(ksFile); 296 KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); 297 keystore.load(ksStream, keystorePass.toCharArray()); 298 ksStream.close(); 299 // Get keypair for alias 300 if (!keystore.isKeyEntry(privkeyAlias)) { 301 throw new RuntimeException("Alias " + privkeyAlias + " is missing or not a key alias"); 302 } 303 PrivateKey privKey = (PrivateKey) keystore.getKey(privkeyAlias, privkeyPass.toCharArray()); 304 Certificate cert = keystore.getCertificate(privkeyAlias); 305 PublicKey pubKey = cert.getPublicKey(); 306 KeyPair keypair = new KeyPair(pubKey, privKey); 307 // Get encryptionMaterials from keypair 308 encryptionMaterials = new EncryptionMaterials(keypair); 309 cryptoConfiguration = new CryptoConfiguration(); 310 } catch (IOException | GeneralSecurityException e) { 311 throw new RuntimeException("Could not read keystore: " + keystoreFile + ", alias: " + privkeyAlias, e); 312 } 313 } 314 isEncrypted = encryptionMaterials != null; 315 316 // Try to create bucket if it doesn't exist 317 if (!isEncrypted) { 318 amazonS3 = new AmazonS3Client(awsCredentialsProvider, clientConfiguration); 319 } else { 320 amazonS3 = new AmazonS3EncryptionClient(awsCredentialsProvider, new StaticEncryptionMaterialsProvider( 321 encryptionMaterials), clientConfiguration, cryptoConfiguration); 322 } 323 if (isNotBlank(endpoint)) { 324 amazonS3.setEndpoint(endpoint); 325 } 326 327 try { 328 if (!amazonS3.doesBucketExist(bucketName)) { 329 amazonS3.createBucket(bucketName, bucketRegion); 330 amazonS3.setBucketAcl(bucketName, CannedAccessControlList.Private); 331 } 332 } catch (AmazonClientException e) { 333 throw new IOException(e); 334 } 335 336 directDownload = Boolean.parseBoolean(Framework.getProperty(DIRECTDOWNLOAD_KEY, DEFAULT_DIRECTDOWNLOAD)); 337 directDownloadExpire = getIntProperty(DIRECTDOWNLOAD_EXPIRE_KEY); 338 if (directDownloadExpire < 0) { 339 directDownloadExpire = DEFAULT_DIRECTDOWNLOAD_EXPIRE; 340 } 341 342 // Create file cache 343 initializeCache(cacheSizeStr, newFileStorage()); 344 createGarbageCollector(); 345 346 transferManager = new TransferManager(amazonS3); 347 abortOldUploads(); 348 } 349 350 @Override 351 public void close() { 352 // this also shuts down the AmazonS3Client 353 transferManager.shutdownNow(); 354 super.close(); 355 } 356 357 /** 358 * Aborts uploads that crashed and are older than 1 day. 359 * 360 * @since 7.2 361 */ 362 protected void abortOldUploads() throws IOException { 363 int oneDay = 1000 * 60 * 60 * 24; 364 try { 365 transferManager.abortMultipartUploads(bucketName, new Date(System.currentTimeMillis() - oneDay)); 366 } catch (AmazonClientException e) { 367 throw new IOException("Failed to abort old uploads", e); 368 } 369 } 370 371 /** 372 * Gets an integer framework property, or -1 if undefined. 373 */ 374 protected static int getIntProperty(String key) { 375 String s = Framework.getProperty(key); 376 int value = -1; 377 if (!isBlank(s)) { 378 try { 379 value = Integer.parseInt(s.trim()); 380 } catch (NumberFormatException e) { 381 log.error("Cannot parse " + key + ": " + s); 382 } 383 } 384 return value; 385 } 386 387 protected void createGarbageCollector() { 388 garbageCollector = new S3BinaryGarbageCollector(this); 389 } 390 391 protected void removeBinary(String digest) { 392 amazonS3.deleteObject(bucketName, digest); 393 } 394 395 protected static boolean isMissingKey(AmazonClientException e) { 396 if (e instanceof AmazonServiceException) { 397 AmazonServiceException ase = (AmazonServiceException) e; 398 return (ase.getStatusCode() == 404) || "NoSuchKey".equals(ase.getErrorCode()) 399 || "Not Found".equals(e.getMessage()); 400 } 401 return false; 402 } 403 404 public static boolean isMD5(String digest) { 405 return MD5_RE.matcher(digest).matches(); 406 } 407 408 protected FileStorage newFileStorage() { 409 return new S3FileStorage(); 410 } 411 412 public class S3FileStorage implements FileStorage { 413 414 @Override 415 public void storeFile(String digest, File file) throws IOException { 416 long t0 = 0; 417 if (log.isDebugEnabled()) { 418 t0 = System.currentTimeMillis(); 419 log.debug("storing blob " + digest + " to S3"); 420 } 421 String etag; 422 String key = bucketNamePrefix + digest; 423 try { 424 ObjectMetadata metadata = amazonS3.getObjectMetadata(bucketName, key); 425 etag = metadata.getETag(); 426 if (log.isDebugEnabled()) { 427 log.debug("blob " + digest + " is already in S3"); 428 } 429 } catch (AmazonClientException e) { 430 if (!isMissingKey(e)) { 431 throw new IOException(e); 432 } 433 // not already present -> store the blob 434 PutObjectRequest request; 435 if (!isEncrypted) { 436 request = new PutObjectRequest(bucketName, key, file); 437 } else { 438 request = new EncryptedPutObjectRequest(bucketName, key, file); 439 } 440 Upload upload = transferManager.upload(request); 441 try { 442 UploadResult result = upload.waitForUploadResult(); 443 etag = result.getETag(); 444 } catch (AmazonClientException ee) { 445 throw new IOException(ee); 446 } catch (InterruptedException ee) { 447 // reset interrupted status 448 Thread.currentThread().interrupt(); 449 // continue interrupt 450 throw new RuntimeException(ee); 451 } finally { 452 if (log.isDebugEnabled()) { 453 long dtms = System.currentTimeMillis() - t0; 454 log.debug("stored blob " + digest + " to S3 in " + dtms + "ms"); 455 } 456 } 457 } 458 // check transfer went ok 459 if (!isEncrypted && !etag.equals(digest) && !ServiceUtils.isMultipartUploadETag(etag)) { 460 // When the blob is not encrypted by S3, the MD5 remotely 461 // computed by S3 and passed as a Etag should match the locally 462 // computed MD5 digest. 463 // This check cannot be done when encryption is enabled unless 464 // we could replicate that encryption locally just for that 465 // purpose which would add further load and complexity on the 466 // client. 467 throw new IOException("Invalid ETag in S3, ETag=" + etag + " digest=" + digest); 468 } 469 } 470 471 @Override 472 public boolean fetchFile(String digest, File file) throws IOException { 473 long t0 = 0; 474 if (log.isDebugEnabled()) { 475 t0 = System.currentTimeMillis(); 476 log.debug("fetching blob " + digest + " from S3"); 477 } 478 try { 479 480 ObjectMetadata metadata = amazonS3.getObject( 481 new GetObjectRequest(bucketName, bucketNamePrefix + digest), file); 482 // check ETag 483 String etag = metadata.getETag(); 484 if (!isEncrypted && !etag.equals(digest) && !ServiceUtils.isMultipartUploadETag(etag)) { 485 log.error("Invalid ETag in S3, ETag=" + etag + " digest=" + digest); 486 return false; 487 } 488 return true; 489 } catch (AmazonClientException e) { 490 if (!isMissingKey(e)) { 491 throw new IOException(e); 492 } 493 return false; 494 } finally { 495 if (log.isDebugEnabled()) { 496 long dtms = System.currentTimeMillis() - t0; 497 log.debug("fetched blob " + digest + " from S3 in " + dtms + "ms"); 498 } 499 } 500 501 } 502 503 @Override 504 public Long fetchLength(String digest) throws IOException { 505 long t0 = 0; 506 if (log.isDebugEnabled()) { 507 t0 = System.currentTimeMillis(); 508 log.debug("fetching blob length " + digest + " from S3"); 509 } 510 try { 511 ObjectMetadata metadata = amazonS3.getObjectMetadata(bucketName, bucketNamePrefix + digest); 512 // check ETag 513 String etag = metadata.getETag(); 514 if (!isEncrypted && !etag.equals(digest) && !ServiceUtils.isMultipartUploadETag(etag)) { 515 log.error("Invalid ETag in S3, ETag=" + etag + " digest=" + digest); 516 return null; 517 } 518 return Long.valueOf(metadata.getContentLength()); 519 } catch (AmazonClientException e) { 520 if (!isMissingKey(e)) { 521 throw new IOException(e); 522 } 523 return null; 524 } finally { 525 if (log.isDebugEnabled()) { 526 long dtms = System.currentTimeMillis() - t0; 527 log.debug("fetched blob length " + digest + " from S3 in " + dtms + "ms"); 528 } 529 } 530 } 531 } 532 533 /** 534 * Garbage collector for S3 binaries that stores the marked (in use) binaries in memory. 535 */ 536 public static class S3BinaryGarbageCollector implements BinaryGarbageCollector { 537 538 protected final S3BinaryManager binaryManager; 539 540 protected volatile long startTime; 541 542 protected BinaryManagerStatus status; 543 544 protected Set<String> marked; 545 546 public S3BinaryGarbageCollector(S3BinaryManager binaryManager) { 547 this.binaryManager = binaryManager; 548 } 549 550 @Override 551 public String getId() { 552 return "s3:" + binaryManager.bucketName; 553 } 554 555 @Override 556 public BinaryManagerStatus getStatus() { 557 return status; 558 } 559 560 @Override 561 public boolean isInProgress() { 562 // volatile as this is designed to be called from another thread 563 return startTime != 0; 564 } 565 566 @Override 567 public void start() { 568 if (startTime != 0) { 569 throw new RuntimeException("Already started"); 570 } 571 startTime = System.currentTimeMillis(); 572 status = new BinaryManagerStatus(); 573 marked = new HashSet<>(); 574 575 // XXX : we should be able to do better 576 // and only remove the cache entry that will be removed from S3 577 binaryManager.fileCache.clear(); 578 } 579 580 @Override 581 public void mark(String digest) { 582 marked.add(digest); 583 // TODO : should clear the specific cache entry 584 } 585 586 @Override 587 public void stop(boolean delete) { 588 if (startTime == 0) { 589 throw new RuntimeException("Not started"); 590 } 591 592 try { 593 // list S3 objects in the bucket 594 // record those not marked 595 Set<String> unmarked = new HashSet<>(); 596 ObjectListing list = null; 597 do { 598 if (list == null) { 599 list = binaryManager.amazonS3.listObjects(binaryManager.bucketName, 600 binaryManager.bucketNamePrefix); 601 } else { 602 list = binaryManager.amazonS3.listNextBatchOfObjects(list); 603 } 604 for (S3ObjectSummary summary : list.getObjectSummaries()) { 605 String digest = summary.getKey(); 606 if (!isMD5(digest)) { 607 // ignore files that cannot be MD5 digests for 608 // safety 609 continue; 610 } 611 long length = summary.getSize(); 612 if (marked.contains(digest)) { 613 status.numBinaries++; 614 status.sizeBinaries += length; 615 } else { 616 status.numBinariesGC++; 617 status.sizeBinariesGC += length; 618 // record file to delete 619 unmarked.add(digest); 620 marked.remove(digest); // optimize memory 621 } 622 } 623 } while (list.isTruncated()); 624 marked = null; // help GC 625 626 // delete unmarked objects 627 if (delete) { 628 for (String digest : unmarked) { 629 binaryManager.removeBinary(digest); 630 } 631 } 632 633 } catch (AmazonClientException e) { 634 throw new RuntimeException(e); 635 } 636 637 status.gcDuration = System.currentTimeMillis() - startTime; 638 startTime = 0; 639 } 640 } 641 642 // ******************** BlobProvider ******************** 643 644 @Override 645 public Blob readBlob(BlobInfo blobInfo) throws IOException { 646 // just delegate to avoid copy/pasting code 647 return new BinaryBlobProvider(this).readBlob(blobInfo); 648 } 649 650 @Override 651 public String writeBlob(Blob blob, Document doc) throws IOException { 652 // just delegate to avoid copy/pasting code 653 return new BinaryBlobProvider(this).writeBlob(blob, doc); 654 } 655 656 @Override 657 public boolean supportsWrite() { 658 return true; 659 } 660 661 @Override 662 public URI getURI(ManagedBlob blob, UsageHint hint, HttpServletRequest servletRequest) throws IOException { 663 if (hint != UsageHint.DOWNLOAD || !directDownload) { 664 return null; 665 } 666 String digest = blob.getKey(); 667 // strip prefix 668 int colon = digest.indexOf(':'); 669 if (colon >= 0) { 670 digest = digest.substring(colon + 1); 671 } 672 return getS3URI(digest, blob, servletRequest); 673 } 674 675 protected URI getS3URI(String digest, Blob blob, HttpServletRequest servletRequest) throws IOException { 676 String key = bucketNamePrefix + digest; 677 Date expiration = new Date(); 678 expiration.setTime(expiration.getTime() + directDownloadExpire * 1000); 679 GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, key, HttpMethod.GET); 680 request.addRequestParameter("response-content-type", getContentTypeHeader(blob)); 681 request.addRequestParameter("response-content-disposition", getContentDispositionHeader(blob, servletRequest)); 682 request.setExpiration(expiration); 683 URL url = amazonS3.generatePresignedUrl(request); 684 try { 685 return url.toURI(); 686 } catch (URISyntaxException e) { 687 throw new IOException(e); 688 } 689 } 690 691 protected String getContentTypeHeader(Blob blob) { 692 String contentType = blob.getMimeType(); 693 String encoding = blob.getEncoding(); 694 if (contentType != null && !StringUtils.isBlank(encoding)) { 695 int i = contentType.indexOf(';'); 696 if (i >= 0) { 697 contentType = contentType.substring(0, i); 698 } 699 contentType += "; charset=" + encoding; 700 } 701 return contentType; 702 } 703 704 protected String getContentDispositionHeader(Blob blob, HttpServletRequest servletRequest) { 705 if (servletRequest == null) { 706 return RFC2231.encodeContentDisposition(blob.getFilename(), false, null); 707 } else { 708 return DownloadHelper.getRFC2231ContentDisposition(servletRequest, blob.getFilename()); 709 } 710 } 711 712}