| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| package com.cloud.bridge.io; | |
| import java.io.File; | |
| import java.io.FileOutputStream; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| import java.net.MalformedURLException; | |
| import java.security.MessageDigest; | |
| import java.security.NoSuchAlgorithmException; | |
| import javax.activation.DataHandler; | |
| import javax.activation.DataSource; | |
| import org.apache.log4j.Logger; | |
| import com.cloud.bridge.service.core.s3.S3BucketAdapter; | |
| import com.cloud.bridge.service.core.s3.S3MultipartPart; | |
| import com.cloud.bridge.service.exception.FileNotExistException; | |
| import com.cloud.bridge.service.exception.InternalErrorException; | |
| import com.cloud.bridge.service.exception.OutOfStorageException; | |
| import com.cloud.bridge.util.OrderedPair; | |
| import com.cloud.bridge.util.StringHelper; | |
| public class S3FileSystemBucketAdapter implements S3BucketAdapter { | |
| protected final static Logger logger = Logger.getLogger(S3FileSystemBucketAdapter.class); | |
| public S3FileSystemBucketAdapter() { | |
| } | |
| @Override | |
| public void createContainer(String mountedRoot, String bucket) { | |
| String dir = getBucketFolderDir(mountedRoot, bucket); | |
| File container = new File(dir); | |
| if (!container.exists()) { | |
| if (!container.mkdirs()) | |
| throw new OutOfStorageException("Unable to create " + dir + " for bucket " + bucket); | |
| } | |
| } | |
| @Override | |
| public void deleteContainer(String mountedRoot, String bucket) { | |
| String dir = getBucketFolderDir(mountedRoot, bucket); | |
| File path = new File(dir); | |
| if (!deleteDirectory(path)) | |
| throw new OutOfStorageException("Unable to delete " + dir + " for bucket " + bucket); | |
| } | |
| @Override | |
| public String getBucketFolderDir(String mountedRoot, String bucket) { | |
| String bucketFolder = getBucketFolderName(bucket); | |
| String dir; | |
| String separator = "" + File.separatorChar; | |
| if (!mountedRoot.endsWith(separator)) | |
| dir = mountedRoot + separator + bucketFolder; | |
| else | |
| dir = mountedRoot + bucketFolder; | |
| return dir; | |
| } | |
| @Override | |
| public String saveObject(InputStream is, String mountedRoot, String bucket, String fileName) { | |
| FileOutputStream fos = null; | |
| MessageDigest md5 = null; | |
| try { | |
| md5 = MessageDigest.getInstance("MD5"); | |
| } catch (NoSuchAlgorithmException e) { | |
| logger.error("Unexpected exception " + e.getMessage(), e); | |
| throw new InternalErrorException("Unable to get MD5 MessageDigest", e); | |
| } | |
| File file = new File(getBucketFolderDir(mountedRoot, bucket) + File.separatorChar + fileName); | |
| try { | |
| // -> when versioning is off we need to rewrite the file contents | |
| file.delete(); | |
| file.createNewFile(); | |
| fos = new FileOutputStream(file); | |
| byte[] buffer = new byte[4096]; | |
| int len = 0; | |
| while ((len = is.read(buffer)) > 0) { | |
| fos.write(buffer, 0, len); | |
| md5.update(buffer, 0, len); | |
| } | |
| //Convert MD4 digest to (lowercase) hex String | |
| return StringHelper.toHexString(md5.digest()); | |
| } catch (IOException e) { | |
| logger.error("Unexpected exception " + e.getMessage(), e); | |
| throw new OutOfStorageException(e); | |
| } finally { | |
| try { | |
| if (null != fos) | |
| fos.close(); | |
| } catch (Exception e) { | |
| logger.error("Can't close FileOutputStream " + e.getMessage(), e); | |
| } | |
| } | |
| } | |
| /** | |
| * From a list of files (each being one part of the multipart upload), concatentate all files into a single | |
| * object that can be accessed by normal S3 calls. This function could take a long time since a multipart is | |
| * allowed to have upto 10,000 parts (each 5 gib long). Amazon defines that while this operation is in progress | |
| * whitespace is sent back to the client inorder to keep the HTTP connection alive. | |
| * | |
| * @param mountedRoot - where both the source and dest buckets are located | |
| * @param destBucket - resulting location of the concatenated objects | |
| * @param fileName - resulting file name of the concatenated objects | |
| * @param sourceBucket - special bucket used to save uploaded file parts | |
| * @param parts - an array of file names in the sourceBucket | |
| * @param client - if not null, then keep the servlet connection alive while this potentially long concatentation takes place | |
| * @return OrderedPair with the first value the MD5 of the final object, and the second value the length of the final object | |
| */ | |
| @Override | |
| public OrderedPair<String, Long> concatentateObjects(String mountedRoot, String destBucket, String fileName, String sourceBucket, S3MultipartPart[] parts, | |
| OutputStream client) { | |
| MessageDigest md5; | |
| long totalLength = 0; | |
| try { | |
| md5 = MessageDigest.getInstance("MD5"); | |
| } catch (NoSuchAlgorithmException e) { | |
| logger.error("Unexpected exception " + e.getMessage(), e); | |
| throw new InternalErrorException("Unable to get MD5 MessageDigest", e); | |
| } | |
| File file = new File(getBucketFolderDir(mountedRoot, destBucket) + File.separatorChar + fileName); | |
| try { | |
| // -> when versioning is off we need to rewrite the file contents | |
| file.delete(); | |
| file.createNewFile(); | |
| final FileOutputStream fos = new FileOutputStream(file); | |
| byte[] buffer = new byte[4096]; | |
| // -> get the input stream for the next file part | |
| for (int i = 0; i < parts.length; i++) { | |
| DataHandler nextPart = loadObject(mountedRoot, sourceBucket, parts[i].getPath()); | |
| InputStream is = nextPart.getInputStream(); | |
| int len = 0; | |
| while ((len = is.read(buffer)) > 0) { | |
| fos.write(buffer, 0, len); | |
| md5.update(buffer, 0, len); | |
| totalLength += len; | |
| } | |
| is.close(); | |
| // -> after each file write tell the client we are still here to keep connection alive | |
| if (null != client) { | |
| client.write(new String(" ").getBytes()); | |
| client.flush(); | |
| } | |
| } | |
| fos.close(); | |
| return new OrderedPair<String, Long>(StringHelper.toHexString(md5.digest()), new Long(totalLength)); | |
| //Create an ordered pair whose first element is the MD4 digest as a (lowercase) hex String | |
| } catch (IOException e) { | |
| logger.error("concatentateObjects unexpected exception " + e.getMessage(), e); | |
| throw new OutOfStorageException(e); | |
| } | |
| } | |
| @Override | |
| public DataHandler loadObject(String mountedRoot, String bucket, String fileName) { | |
| File file = new File(getBucketFolderDir(mountedRoot, bucket) + File.separatorChar + fileName); | |
| try { | |
| return new DataHandler(file.toURL()); | |
| } catch (MalformedURLException e) { | |
| throw new FileNotExistException("Unable to open underlying object file"); | |
| } | |
| } | |
| @Override | |
| public void deleteObject(String mountedRoot, String bucket, String fileName) { | |
| String filePath = new String(getBucketFolderDir(mountedRoot, bucket) + File.separatorChar + fileName); | |
| File file = new File(filePath); | |
| if (!file.delete()) { | |
| logger.error("file: " + filePath + ", f=" + file.isFile() + ", h=" + file.isHidden() + ", e=" + file.exists() + ", w=" + file.canWrite()); | |
| throw new OutOfStorageException("Unable to delete " + filePath + " for object deletion"); | |
| } | |
| } | |
| @Override | |
| public DataHandler loadObjectRange(String mountedRoot, String bucket, String fileName, long startPos, long endPos) { | |
| File file = new File(getBucketFolderDir(mountedRoot, bucket) + File.separatorChar + fileName); | |
| try { | |
| DataSource ds = new FileRangeDataSource(file, startPos, endPos); | |
| return new DataHandler(ds); | |
| } catch (MalformedURLException e) { | |
| throw new FileNotExistException("Unable to open underlying object file"); | |
| } catch (IOException e) { | |
| throw new FileNotExistException("Unable to open underlying object file"); | |
| } | |
| } | |
| public static boolean deleteDirectory(File path) { | |
| if (path.exists()) { | |
| File[] files = path.listFiles(); | |
| for (int i = 0; i < files.length; i++) { | |
| if (files[i].isDirectory()) { | |
| deleteDirectory(files[i]); | |
| } else { | |
| files[i].delete(); | |
| } | |
| } | |
| } | |
| return path.delete(); | |
| } | |
| private String getBucketFolderName(String bucket) { | |
| // temporary | |
| String name = bucket.replace(' ', '_'); | |
| name = bucket.replace('\\', '-'); | |
| name = bucket.replace('/', '-'); | |
| return name; | |
| } | |
| } |