BJ-314: test branch

This commit is contained in:
a.karlov 2018-12-03 11:24:59 +03:00
parent 8bd10b427b
commit 376a703605
19 changed files with 1035 additions and 126 deletions

42
pom.xml
View File

@ -23,10 +23,34 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<springfox-swagger2.version>2.8.0</springfox-swagger2.version>
<woody.thrift.version>1.1.15</woody.thrift.version>
<file.storage.proto.version>1.5-81b8f4a</file.storage.proto.version>
<geck.version>0.6.8</geck.version>
</properties>
<dependencies>
<!-- rbkmoney -->
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>file-storage-proto</artifactId>
<version>${file.storage.proto.version}</version>
</dependency>
<dependency>
<groupId>com.rbkmoney.woody</groupId>
<artifactId>woody-thrift</artifactId>
<version>${woody.thrift.version}</version>
</dependency>
<dependency>
<groupId>com.rbkmoney.geck</groupId>
<artifactId>common</artifactId>
<version>${geck.version}</version>
</dependency>
<dependency>
<groupId>com.rbkmoney.geck</groupId>
<artifactId>serializer</artifactId>
<version>${geck.version}</version>
</dependency>
<!-- Spring libs -->
<dependency>
<groupId>org.springframework.boot</groupId>
@ -44,16 +68,6 @@
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.11.160</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox-swagger2.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox-swagger2.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@ -67,6 +81,12 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.8.3</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>

View File

@ -2,8 +2,10 @@ package com.rbkmoney.file.storage;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan
@SpringBootApplication(scanBasePackages = {"com.rbkmoney.file.storage"})
public class FileStorageApplication {
public static void main(String[] args) {

View File

@ -16,7 +16,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StorageConfig {
public class AmazonS3ClientConfiguration {
@Value("${storage.endpoint}")
private String endpoint;
@ -24,13 +24,13 @@ public class StorageConfig {
@Value("${storage.signingRegion}")
private String signingRegion;
@Value("${storage.accessKey:}")
@Value("${storage.accessKey}")
private String accessKey;
@Value("${storage.secretKey:}")
@Value("${storage.secretKey}")
private String secretKey;
@Value("${storage.client.protocol:HTTP}")
@Value("${storage.client.protocol}")
private Protocol protocol;
@Value("${storage.client.maxErrorRetry}")

View File

@ -0,0 +1,18 @@
package com.rbkmoney.file.storage.config;
import com.rbkmoney.file.storage.FileStorageSrv;
import com.rbkmoney.file.storage.handler.FileStorageHandler;
import com.rbkmoney.file.storage.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HandlerConfiguration {
@Bean
@Autowired
public FileStorageSrv.Iface fileStorageHandler(StorageService storageService) {
return new FileStorageHandler(storageService);
}
}

View File

@ -1,35 +0,0 @@
package com.rbkmoney.file.storage.config;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
@Bean
public Docket productApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(metaData())
.select()
.paths(Predicates.not(PathSelectors.regex("/error")))
.build();
}
private ApiInfo metaData() {
return new ApiInfoBuilder()
.title("REST endpoint")
.description("\"Endpoint для выгрузки документов на сервер\"")
.version("0.0.1-SNAPSHOT")
.contact(new Contact("RBK.money", "https://github.com/rbkmoney", "support@rbkmoney.com"))
.build();
}
}

View File

@ -1,34 +0,0 @@
package com.rbkmoney.file.storage.contorller;
import com.rbkmoney.file.storage.service.StorageService;
import io.swagger.annotations.*;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@Api(description = "Api для операций с файлами")
@RequiredArgsConstructor
public class FileController {
private final StorageService storageService;
@PostMapping("/upload")
@ApiOperation(value = "Выгрузить файл на сервер")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Файл выгружен на сервер")
})
public ResponseEntity handleFileUpload(@ApiParam(value = "выгружаемый файл", required = true)
@RequestParam(value = "file")
MultipartFile file,
@ApiParam(value = "id файла", required = true)
@RequestParam(value = "file_id")
String fileId) {
storageService.store(fileId, file);
return new ResponseEntity(HttpStatus.OK);
}
}

View File

@ -0,0 +1,42 @@
package com.rbkmoney.file.storage.contorller;
import com.rbkmoney.file.storage.service.StorageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.FileNotFoundException;
import static com.rbkmoney.file.storage.util.CheckerUtil.checkString;
@RestController
@RequiredArgsConstructor
@Slf4j
public class UploadFileController {
private final StorageService storageService;
@PostMapping("/file_storage/upload")
public ResponseEntity handleFileUpload(@RequestParam(value = "file_id") String fileId,
@RequestParam(value = "file") MultipartFile file) {
try {
log.info("Request handleFileUpload fileId: {}", fileId);
checkString(fileId, "Bad request parameter, fileId required and not empty arg");
storageService.uploadFile(fileId, file.getInputStream());
ResponseEntity<Object> responseEntity = ResponseEntity.ok().build();
log.info("Response: ResponseEntity: {}", responseEntity);
return responseEntity;
} catch (FileNotFoundException e) {
log.error("Error when handleFileUpload e: ", e);
return ResponseEntity.notFound().build();
} catch (Exception e) {
log.error("Error when handleFileUpload e: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}

View File

@ -0,0 +1,78 @@
package com.rbkmoney.file.storage.handler;
import com.rbkmoney.damsel.msgpack.Value;
import com.rbkmoney.file.storage.FileData;
import com.rbkmoney.file.storage.FileNotFound;
import com.rbkmoney.file.storage.FileStorageSrv;
import com.rbkmoney.file.storage.NewFileResult;
import com.rbkmoney.file.storage.service.StorageService;
import com.rbkmoney.geck.common.util.TypeUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import java.io.FileNotFoundException;
import java.net.URL;
import java.time.Instant;
import java.util.Map;
import static com.rbkmoney.file.storage.util.CheckerUtil.checkString;
@RequiredArgsConstructor
@Slf4j
public class FileStorageHandler implements FileStorageSrv.Iface {
private final StorageService storageService;
@Override
public FileData getFileData(String fileId) throws TException {
try {
log.info("Request getFileData fileId: {}", fileId);
checkString(fileId, "Bad request parameter, fileId required and not empty arg");
FileData fileData = storageService.getFileData(fileId);
log.info("Response: fileData: {}", fileData);
return fileData;
} catch (FileNotFoundException e) {
log.error("Error when getFileData e: ", e);
throw new FileNotFound();
} catch (Exception e) {
log.error("Error when getFileData e: ", e);
throw new TException(e);
}
}
@Override
public NewFileResult createNewFile(String fileName, Map<String, Value> metadata, String expiresAt) throws TException {
try {
log.info("Request createNewFile fileName: {}, metadata: {}, expiresAt: {}", fileName, metadata, expiresAt);
checkString(fileName, "Bad request parameter, fileName required and not empty arg");
checkString(expiresAt, "Bad request parameter, expiresAt required and not empty arg");
Instant instant = TypeUtil.stringToInstant(expiresAt);
NewFileResult newFile = storageService.createNewFile(fileName, metadata, instant);
log.info("Response: newFileResult: {}", newFile);
return newFile;
} catch (Exception e) {
log.error("Error when createNewFile e: ", e);
throw new TException(e);
}
}
@Override
public String generateDownloadUrl(String fileId, String expiresAt) throws TException {
try {
log.info("Request generateDownloadUrl fileId: {}, expiresAt: {}", fileId, expiresAt);
checkString(fileId, "Bad request parameter, fileId required and not empty arg");
checkString(expiresAt, "Bad request parameter, expiresAt required and not empty arg");
Instant instant = TypeUtil.stringToInstant(expiresAt);
URL url = storageService.generateDownloadUrl(fileId, instant);
log.info("Response: url: {}", url);
return url.toString();
} catch (FileNotFoundException e) {
log.error("Error when generateDownloadUrl e: ", e);
throw new FileNotFound();
} catch (Exception e) {
log.error("Error when generateDownloadUrl e: ", e);
throw new TException(e);
}
}
}

View File

@ -0,0 +1,30 @@
package com.rbkmoney.file.storage.resource;
import com.rbkmoney.file.storage.FileStorageSrv;
import com.rbkmoney.woody.thrift.impl.http.THServiceBuilder;
import lombok.RequiredArgsConstructor;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/file_storage")
@RequiredArgsConstructor
public class FileStorageServlet extends GenericServlet {
private Servlet thriftServlet;
private final FileStorageSrv.Iface requestHandler;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
thriftServlet = new THServiceBuilder()
.build(FileStorageSrv.Iface.class, requestHandler);
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
thriftServlet.service(req, res);
}
}

View File

@ -1,99 +1,408 @@
package com.rbkmoney.file.storage.service;
import com.amazonaws.AmazonClientException;
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.*;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.Upload;
import com.rbkmoney.file.storage.FileData;
import com.rbkmoney.file.storage.NewFileResult;
import com.rbkmoney.file.storage.contorller.UploadFileController;
import com.rbkmoney.file.storage.service.exception.StorageException;
import com.rbkmoney.file.storage.util.DamselUtil;
import com.rbkmoney.geck.common.util.TypeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.FileOutputStream;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
public class AmazonS3StorageService implements StorageService {
private static final String EXPIRATION_TIME = "x-rbkmoney-file-expiration-time";
private static final String FILE_UPLOADED = "x-rbkmoney-file-uploaded";
private static final String FILEDATA_FILE_ID = "x-rbkmoney-filedata-file-id";
private static final String FILEDATA_FILE_NAME = "x-rbkmoney-filedata-file-name";
private static final String FILEDATA_CREATED_AT = "x-rbkmoney-filedata-created-at";
private static final String FILEDATA_MD_5 = "x-rbkmoney-filedata-md5";
private static final String FILEDATA_METADATA = "x-rbkmoney-filedata-metadata-";
private final TransferManager transferManager;
private final AmazonS3 storageClient;
private final AmazonS3 s3Client;
private final String bucketName;
@Autowired
public AmazonS3StorageService(TransferManager transferManager, @Value("${storage.bucketName}") String bucketName) {
this.transferManager = transferManager;
this.storageClient = transferManager.getAmazonS3Client();
this.s3Client = transferManager.getAmazonS3Client();
this.bucketName = bucketName;
}
@PostConstruct
public void init() {
if (!storageClient.doesBucketExist(bucketName)) {
if (!s3Client.doesBucketExist(bucketName)) {
log.info("Create bucket in file storage, bucketId='{}'", bucketName);
storageClient.createBucket(bucketName);
s3Client.createBucket(bucketName);
}
}
@Override
public void store(String fileId, MultipartFile file) {
String filename = file.getOriginalFilename();
log.info("Trying to upload file to storage, filename='{}', bucketId='{}'", filename, bucketName);
public FileData getFileData(String fileId) throws StorageException, FileNotFoundException {
S3Object s3Object = getS3Object(fileId);
checkFileStatus(s3Object);
return extractFileData(s3Object.getObjectMetadata());
}
@Override
public NewFileResult createNewFile(String fileName, Map<String, com.rbkmoney.damsel.msgpack.Value> metadata, Instant expirationTime) throws StorageException {
log.info("Trying to create new file to storage, filename='{}', bucketId='{}'", fileName, bucketName);
try {
Path tempFile = createTempFile(file, filename);
// в хранилище сохраняется пустой файл
InputStream emptyContent = new ByteArrayInputStream(new byte[0]);
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentDisposition("attachment;filename=" + filename);
String fileId = getFileId();
String createdAt = Instant.now().toString();
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileId, tempFile.toFile());
putObjectRequest.setMetadata(objectMetadata);
Upload upload = transferManager.upload(putObjectRequest);
try {
upload.waitForUploadResult();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
FileData fileData = new FileData(
fileId,
fileName,
createdAt,
//todo
"",
metadata
);
Files.deleteIfExists(tempFile);
writeFileToStorage(fileData, emptyContent, expirationTime);
URL uploadUrl = createUploadUrl(fileId);
log.info(
"File have been successfully uploaded, fileId='{}', bucketId='{}', filename='{}', md5='{}'",
"File have been successfully created, fileId='{}', bucketId='{}', filename='{}', md5='{}'",
fileId,
bucketName,
filename,
DigestUtils.md5Hex(Files.newInputStream(tempFile))
fileName,
//todo
""
);
return new NewFileResult(uploadUrl.toString(), fileData);
} catch (AmazonClientException ex) {
throw new StorageException(
String.format(
"Failed to create new file to storage, filename='%s', bucketId='%s'",
fileName,
bucketName
),
ex
);
} catch (IOException | AmazonClientException ex) {
throw new StorageException(String.format("Failed to upload file to storage, filename='%s', bucketId='%s'", filename, bucketName), ex);
}
}
private Path createTempFile(MultipartFile file, String filename) throws IOException {
Path tempFile = Files.createTempFile(filename, "");
OutputStream outputStream = new FileOutputStream(tempFile.toFile());
@Override
public URL generateDownloadUrl(String fileId, Instant expirationTime) throws StorageException, FileNotFoundException {
checkFileStatus(getS3Object(fileId));
return generatePresignedUrl(fileId, expirationTime, HttpMethod.GET);
}
int read;
byte[] bytes = new byte[1024];
@Override
public void uploadFile(String fileId, InputStream inputStream) throws StorageException, IOException {
log.info("Trying to upload file to storage, filename='{}', bucketId='{}'", fileId, bucketName);
while ((read = file.getInputStream().read(bytes)) != -1) {
outputStream.write(bytes, 0, read);
try {
//todo
Path testFile = Files.createTempFile("", "test_file");
Files.write(testFile, "Test".getBytes());
S3Object object = getS3Object(fileId);
checkFileStatus(object);
object.getObjectMetadata().addUserMetadata(FILE_UPLOADED, "true");
/*
//todo DEBUG
try {
// InputStream inputStream = file.getInputStream();
Path testActualFile = Files.createTempFile("", "test_actual_file");
Files.copy(inputStream, testActualFile, StandardCopyOption.REPLACE_EXISTING);
System.out.println("!!!!!!!");
for (String readAllLine : Files.readAllLines(testActualFile)) {
System.out.println(readAllLine);
}
testActualFile = Files.createTempFile("", "test_actual_file");
Files.copy(inputStream, testActualFile, StandardCopyOption.REPLACE_EXISTING);
System.out.println("!!!!!!!");
for (String readAllLine : Files.readAllLines(testActualFile)) {
System.out.println(readAllLine);
}
} catch (Exception e) {
}
//todo
*/
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileId, testFile.toFile());
putObjectRequest.setMetadata(object.getObjectMetadata());
s3Client.putObject(putObjectRequest);
/*
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileId, new S3ObjectInputStream(
new FileInputStream(testFile.toFile()),null
),object.getObjectMetadata());
s3Client.putObject(putObjectRequest);
*/
/*
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileId, new FileInputStream(testFile.toFile()),object.getObjectMetadata());
s3Client.putObject(putObjectRequest);
*/
/* todo
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileId, inputStream, object.getObjectMetadata());
s3Client.putObject(putObjectRequest);
*/
log.info(
"File have been successfully uploaded, fileId='{}', bucketId='{}'",
fileId,
bucketName
);
//todo
/* try {
S3Object object1 = s3Client.getObject(bucketName, fileId);
S3ObjectInputStream objectContent = object1.getObjectContent();
Path testActualFile = Files.createTempFile("", "test_actual_file");
Files.copy(objectContent, testActualFile, StandardCopyOption.REPLACE_EXISTING);
System.out.println("!!!!!!!");
for (String readAllLine : Files.readAllLines(testActualFile)) {
System.out.println(readAllLine);
}
} catch (Exception e) {
}
*/
//todo
} catch (AmazonClientException ex) {
throw new StorageException(
String.format(
"Failed to to upload file to storage, filename='%s', bucketId='%s'",
fileId,
bucketName
),
ex
);
}
return tempFile;
}
@PreDestroy
public void terminate() {
transferManager.shutdownNow(true);
}
private S3Object getS3Object(String fileId) throws StorageException, FileNotFoundException {
try {
log.info(
"Trying to get file from storage, fileId='{}', bucketId='{}'",
fileId,
bucketName
);
GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, fileId);
S3Object object = s3Client.getObject(getObjectRequest);
checkNullable(object, fileId, "File");
log.info(
"File have been successfully got from storage, fileId='{}', bucketId='{}'",
fileId,
bucketName
);
return object;
} catch (AmazonClientException ex) {
throw new StorageException(
String.format(
"Failed to get file from storage, fileId='%s', bucketId='%s'",
fileId,
bucketName
),
ex
);
}
}
private void checkFileStatus(S3Object s3Object) throws StorageException {
log.info("Check file expiration and uploaded status: ETag='{}'", s3Object.getObjectMetadata().getETag());
ObjectMetadata objectMetadata = s3Object.getObjectMetadata();
Boolean isUploaded = getBooleanFromObjectMetadata(objectMetadata);
if (isUploaded) {
log.info("File was uploaded: ETag='{}'", s3Object.getObjectMetadata().getETag());
return;
}
Date expirationTime = getDateFromObjectMetadata(objectMetadata);
Date time = new Date();
if (time.getTime() < expirationTime.getTime()) {
log.info("File was uploaded: ETag='{}'", s3Object.getObjectMetadata().getETag());
return;
}
// если файл не соотвествует условиям, блокируем доступ к нему
throw new StorageException(String.format("File access error: fileId='%s', bucketId='%s', create a new file", s3Object.getKey(), bucketName));
}
private FileData extractFileData(ObjectMetadata objectMetadata) {
log.info("Trying to extract metadata from storage: ETag='{}'", objectMetadata.getETag());
String fileId = getUserMetadataParameter(objectMetadata, FILEDATA_FILE_ID);
String fileName = getUserMetadataParameter(objectMetadata, FILEDATA_FILE_NAME);
String createdAt = getUserMetadataParameter(objectMetadata, FILEDATA_CREATED_AT);
String md5 = getUserMetadataParameter(objectMetadata, FILEDATA_MD_5);
Map<String, com.rbkmoney.damsel.msgpack.Value> metadata = objectMetadata.getUserMetadata().entrySet().stream()
.filter(entry -> entry.getKey().startsWith(FILEDATA_METADATA) && entry.getValue() != null)
.collect(
Collectors.toMap(
o -> o.getKey().substring(FILEDATA_METADATA.length()),
o -> DamselUtil.fromJson(o.getValue(), com.rbkmoney.damsel.msgpack.Value.class)
)
);
log.info(
"Metadata have been successfully extracted from storage, fileId='{}', bucketId='{}'",
fileId,
bucketName
);
return new FileData(fileId, fileName, createdAt, md5, metadata);
}
private URL generatePresignedUrl(String fileId, Instant expirationTime, HttpMethod httpMethod) throws StorageException, FileNotFoundException {
try {
log.info(
"Trying to generate presigned url, fileId='{}', bucketId='{}', expirationTime='{}', httpMethod='{}'",
fileId,
bucketName,
expirationTime,
httpMethod
);
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, fileId)
.withMethod(httpMethod)
.withExpiration(Date.from(expirationTime));
URL url = s3Client.generatePresignedUrl(request);
checkNullable(url, fileId, "Presigned url");
log.info(
"Presigned url have been successfully generated, url='{}', fileId='{}', bucketId='{}', expirationTime='{}', httpMethod='{}'",
url,
fileId,
bucketName,
expirationTime,
httpMethod
);
return url;
} catch (AmazonClientException ex) {
throw new StorageException(
String.format(
"Failed to generate presigned url, fileId='%s', bucketId='%s', expirationTime='%s', httpMethod='%s'",
fileId,
bucketName,
expirationTime,
httpMethod
),
ex
);
}
}
private void writeFileToStorage(FileData fileData, InputStream inputStream, Instant expirationTime) throws AmazonClientException {
PutObjectRequest request = createS3Request(fileData, inputStream, expirationTime);
s3Client.putObject(request);
}
private PutObjectRequest createS3Request(FileData fileData, InputStream inputStream, Instant expirationTime) {
return new PutObjectRequest(
bucketName,
fileData.getFileId(),
inputStream,
createObjectMetadata(fileData, expirationTime)
);
}
private ObjectMetadata createObjectMetadata(FileData fileData, Instant expirationTime) {
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentDisposition("attachment;filename=" + fileData.getFileName());
// file parameters
objectMetadata.addUserMetadata(EXPIRATION_TIME, expirationTime.toString());
objectMetadata.addUserMetadata(FILE_UPLOADED, "false");
// filedata parameters
objectMetadata.addUserMetadata(FILEDATA_FILE_ID, fileData.getFileId());
objectMetadata.addUserMetadata(FILEDATA_FILE_NAME, fileData.getFileName());
objectMetadata.addUserMetadata(FILEDATA_CREATED_AT, fileData.getCreatedAt());
objectMetadata.addUserMetadata(FILEDATA_MD_5, fileData.getMd5());
fileData.getMetadata().forEach(
(key, value) -> objectMetadata.addUserMetadata(FILEDATA_METADATA + key, DamselUtil.toJsonString(value))
);
return objectMetadata;
}
private URL createUploadUrl(String fileId) {
try {
return MvcUriComponentsBuilder.fromMethodName(
UploadFileController.class,
"handleFileUpload",
fileId,
null
)
.buildAndExpand()
.encode()
.toUri()
.toURL();
} catch (MalformedURLException e) {
throw new StorageException(
String.format(
"Exception createUploadUrl: fileId='%s', bucketId='%s', create a new file",
fileId,
bucketName
),
e);
}
}
private Boolean getBooleanFromObjectMetadata(ObjectMetadata objectMetadata) {
String isUploadedString = getUserMetadataParameter(objectMetadata, FILE_UPLOADED);
return Boolean.valueOf(isUploadedString);
}
private Date getDateFromObjectMetadata(ObjectMetadata objectMetadata) throws StorageException {
String expirationTime = getUserMetadataParameter(objectMetadata, EXPIRATION_TIME);
return Date.from(TypeUtil.stringToInstant(expirationTime));
}
private String getUserMetadataParameter(ObjectMetadata objectMetadata, String key) throws StorageException {
return Optional.ofNullable(objectMetadata.getUserMetaDataOf(key))
.orElseThrow(() -> new StorageException("Failed to extract user metadata parameter, " + key + " is null"));
}
private String getFileId() {
String fileId;
do {
fileId = UUID.randomUUID().toString();
} while (s3Client.doesObjectExist(bucketName, fileId));
return fileId;
}
private void checkNullable(Object object, String fileId, String objectType) throws FileNotFoundException {
if (Objects.isNull(object)) {
throw new FileNotFoundException(String.format(objectType + " is null, fileId='%s', bucketId='%s'", fileId, bucketName));
}
}
}

View File

@ -1,8 +1,25 @@
package com.rbkmoney.file.storage.service;
import org.springframework.web.multipart.MultipartFile;
import com.rbkmoney.damsel.msgpack.Value;
import com.rbkmoney.file.storage.FileData;
import com.rbkmoney.file.storage.NewFileResult;
import com.rbkmoney.file.storage.service.exception.StorageException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.time.Instant;
import java.util.Map;
public interface StorageService {
void store(String fileId, MultipartFile file);
FileData getFileData(String fileId) throws StorageException, FileNotFoundException;
NewFileResult createNewFile(String fileName, Map<String, Value> metadata, Instant expirationTime) throws StorageException;
URL generateDownloadUrl(String fileId, Instant expirationTime) throws StorageException, FileNotFoundException;
void uploadFile(String fileId, InputStream inputStream) throws StorageException, IOException;
}

View File

@ -0,0 +1,13 @@
package com.rbkmoney.file.storage.util;
import com.google.common.base.Strings;
import org.apache.thrift.TException;
public class CheckerUtil {
public static void checkString(String string, String exMessage) throws TException {
if (Strings.isNullOrEmpty(string)) {
throw new TException(exMessage);
}
}
}

View File

@ -0,0 +1,35 @@
package com.rbkmoney.file.storage.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rbkmoney.geck.serializer.kit.json.JsonHandler;
import com.rbkmoney.geck.serializer.kit.json.JsonProcessor;
import com.rbkmoney.geck.serializer.kit.tbase.TBaseHandler;
import com.rbkmoney.geck.serializer.kit.tbase.TBaseProcessor;
import org.apache.thrift.TBase;
import java.io.IOException;
public class DamselUtil {
public static String toJsonString(TBase tBase) {
return toJson(tBase).toString();
}
public static JsonNode toJson(TBase tBase) {
try {
return new TBaseProcessor().process(tBase, new JsonHandler());
} catch (IOException ex) {
throw new IllegalArgumentException(ex);
}
}
public static <T extends TBase> T fromJson(String jsonString, Class<T> type) {
try {
return new JsonProcessor().process(new ObjectMapper().readTree(jsonString), new TBaseHandler<>(type));
} catch (IOException ex) {
throw new IllegalArgumentException(ex);
}
}
}

View File

@ -5,3 +5,8 @@ info.stage=dev
spring.servlet.multipart.max-file-size=128KB
spring.servlet.multipart.max-request-size=128KB
spring.servlet.multipart.enabled=true
storage.endpoint=localhost
storage.signingRegion=RU
storage.client.protocol=HTTP
storage.client.maxErrorRetry=10
storage.bucketName="files"

View File

@ -0,0 +1,81 @@
package com.rbkmoney.file.storage;
import org.junit.runner.RunWith;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ContextConfiguration(classes = FileStorageApplication.class, initializers = AbstractIntegrationTest.Initializer.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class AbstractIntegrationTest {
private static final String SIGNING_REGION = "RU";
private static final String AWS_ACCESS_KEY = "test";
private static final String AWS_SECRET_KEY = "test";
private static final String PROTOCOL = "HTTP";
private static final String MAX_ERROR_RETRY = "10";
private static final String BUCKET_NAME = "TEST";
@LocalServerPort
protected int port;
// @ClassRule
// public static GenericContainer cephContainer = new GenericContainer("dr.rbkmoney.com/ceph-demo:latest")
// .withEnv("RGW_NAME", "localhost")
// .withEnv("NETWORK_AUTO_DETECT", "4")
// .withEnv("CEPH_DEMO_UID", "ceph-test")
// .withEnv("CEPH_DEMO_ACCESS_KEY", AWS_ACCESS_KEY)
// .withEnv("CEPH_DEMO_SECRET_KEY", AWS_SECRET_KEY)
// .withEnv("CEPH_DEMO_BUCKET", BUCKET_NAME)
// .withExposedPorts(5000, 80)
// .waitingFor(
// new HttpWaitStrategy()
// .forPath("/api/v0.1/health")
// .forStatusCode(200)
// .withStartupTimeout(Duration.ofMinutes(10))
// );
public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
EnvironmentTestUtils.addEnvironment(
"testcontainers",
configurableApplicationContext.getEnvironment(),
// "storage.endpoint=" + cephContainer.getContainerIpAddress() + ":" + cephContainer.getMappedPort(80),
"storage.endpoint=localhost:32827",
"storage.signingRegion=" + SIGNING_REGION,
"storage.accessKey=" + AWS_ACCESS_KEY,
"storage.secretKey=" + AWS_SECRET_KEY,
"storage.client.protocol=" + PROTOCOL,
"storage.client.maxErrorRetry=" + MAX_ERROR_RETRY,
"storage.bucketName=" + BUCKET_NAME
);
}
}
protected Instant getDayInstant() {
return LocalDateTime.now().plusDays(1).toInstant(getZoneOffset());
}
protected Instant getSecondInstant() {
return LocalDateTime.now().plusSeconds(1).toInstant(getZoneOffset());
}
private ZoneOffset getZoneOffset() {
return ZoneOffset.systemDefault().getRules().getOffset(LocalDateTime.now());
}
}

View File

@ -0,0 +1,167 @@
package com.rbkmoney.file.storage.service;
import com.rbkmoney.file.storage.AbstractIntegrationTest;
import com.rbkmoney.file.storage.FileData;
import com.rbkmoney.file.storage.FileStorageSrv;
import com.rbkmoney.file.storage.NewFileResult;
import com.rbkmoney.woody.api.flow.error.WRuntimeException;
import com.rbkmoney.woody.thrift.impl.http.THSpawnClientBuilder;
import org.apache.thrift.TException;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Collections;
import static org.junit.Assert.assertEquals;
public class Aasd extends AbstractIntegrationTest {
private static final int TIMEOUT = 555000;
private FileStorageSrv.Iface client;
@Before
public void before() throws URISyntaxException {
client = new THSpawnClientBuilder()
.withAddress(new URI("http://localhost:" + port + "/file_storage"))
.withNetworkTimeout(TIMEOUT)
.build(FileStorageSrv.Iface.class);
}
@Test
public void extractMetadataTest() throws TException {
NewFileResult testFile = client.createNewFile("test_file", Collections.emptyMap(), getDayInstant().toString());
FileData fileData = client.getFileData(testFile.getFileData().getFileId());
assertEquals(fileData, testFile.getFileData());
}
@Test(expected = WRuntimeException.class)
public void expiredTimeForFileDataInMetadataTest() throws TException, InterruptedException {
NewFileResult testFile = client.createNewFile("test_file", Collections.emptyMap(), getSecondInstant().toString());
Thread.sleep(1000);
client.getFileData(testFile.getFileData().getFileId());
}
@Test(expected = WRuntimeException.class)
public void expiredTimeForGenerateUrlInMetadataTest() throws TException, InterruptedException {
NewFileResult testFile = client.createNewFile("test_file", Collections.emptyMap(), getSecondInstant().toString());
Thread.sleep(1000);
client.generateDownloadUrl(testFile.getFileData().getFileId(), getSecondInstant().toString());
}
@Test
public void downloadUrlTest() throws TException, IOException {
Path testFile = Files.createTempFile("", "test_file");
Files.write(testFile, new byte[0]);
Path testActualFile = Files.createTempFile("", "test_actual_file");
NewFileResult fileResult = client.createNewFile("test_file", Collections.emptyMap(), getDayInstant().toString());
String s = client.generateDownloadUrl(fileResult.getFileData().getFileId(), getDayInstant().toString());
URL url = new URL(s);
assertEquals(HttpStatus.FORBIDDEN.value(), getHttpURLConnection(url, true, "PUT").getResponseCode());
assertEquals(HttpStatus.OK.value(), getHttpURLConnection(url, false, "GET").getResponseCode());
HttpURLConnection urlConnection = getHttpURLConnection(url, false, "GET");
InputStream inputStream = urlConnection.getInputStream();
Files.copy(inputStream, testActualFile, StandardCopyOption.REPLACE_EXISTING);
assertEquals(Files.readAllLines(testFile), Files.readAllLines(testActualFile));
}
@Test
public void expiredTimeForGenerateUrlConnectionInCephTest() throws TException, IOException, InterruptedException {
NewFileResult fileResult = client.createNewFile("test_file", Collections.emptyMap(), getDayInstant().toString());
String s = client.generateDownloadUrl(fileResult.getFileData().getFileId(), getSecondInstant().toString());
URL url = new URL(s);
assertEquals(HttpStatus.OK.value(), getHttpURLConnection(url, false, "GET").getResponseCode());
Thread.sleep(2000);
assertEquals(HttpStatus.FORBIDDEN.value(), getHttpURLConnection(url, false, "GET").getResponseCode());
}
@Test
public void name() throws IOException, TException {
NewFileResult fileResult = client.createNewFile("test_file", Collections.emptyMap(), getDayInstant().toString());
String s = fileResult.getUploadUrl();
Path testFile = Files.createTempFile("", "test_file");
Files.write(testFile, "Test".getBytes());
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", new FileSystemResource(testFile.toFile()));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
restTemplate.postForEntity(s, requestEntity, Void.class);
Path testActualFile = Files.createTempFile("", "test_actual_file");
String urs = client.generateDownloadUrl(fileResult.getFileData().getFileId(), getDayInstant().toString());
URL url = new URL(urs);
HttpURLConnection urlConnection = getHttpURLConnection(url, false, "GET");
InputStream inputStream = urlConnection.getInputStream();
Files.copy(inputStream, testActualFile, StandardCopyOption.REPLACE_EXISTING);
assertEquals(Files.readAllLines(testFile), Files.readAllLines(testActualFile));
}
private void asd() {
/*
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setBufferRequestBody(false);
InputStream fis = new FileInputStream(testFile.toFile());
RequestCallback requestCallback = request -> {
request.getHeaders().add("Content-type", "application/octet-stream");
IOUtils.copy(fis, request.getBody());
};
HttpMessageConverterExtractor<String> responseExtractor =
new HttpMessageConverterExtractor<>(String.class, restTemplate.getMessageConverters());
restTemplate.setRequestFactory(requestFactory);
*/
}
private HttpURLConnection getHttpURLConnection(URL url, boolean doOutput, String method) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(doOutput);
connection.setRequestMethod(method);
return connection;
}
}

View File

@ -0,0 +1,20 @@
package com.rbkmoney.file.storage.service;
import com.rbkmoney.file.storage.AbstractIntegrationTest;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.FileNotFoundException;
public class MetadataS3ObjectsTest extends AbstractIntegrationTest {
@Autowired
private StorageService storageService;
@Test
public void name() throws FileNotFoundException {
// FileData fileData = storageService.createNewFile("test_file", Collections.emptyMap(), getInstant());
// URL url = storageService.generateDownloadUrl(fileData.getFileId(), getInstant());
}
}

View File

@ -0,0 +1,111 @@
package com.rbkmoney.file.storage.service;
import com.rbkmoney.file.storage.AbstractIntegrationTest;
import com.rbkmoney.file.storage.FileData;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Path;
public class PresignedUrlAccessRightsTest extends AbstractIntegrationTest {
@Autowired
private StorageService storageService;
@Test
public void urlForUploadTest() throws IOException {
/* String stringForWrite = "This text uploaded as an object via presigned URL.";
Path testFile = Files.createTempFile("", "test_file");
Path testActualFile = Files.createTempFile("", "test_actual_file");
try {
FileData fileData = storageService.createNewFile("test_file", Collections.emptyMap(), getInstant());
// генерация url с доступом только для выгрузки
URL url = storageService.generateUploadUrl(fileData.getFileId(), getInstant());
// ошибка при запросе по url методом get
assertEquals(HttpStatus.FORBIDDEN.value(), getHttpURLConnection(url, false, "GET").getResponseCode());
// Length Required при запросе по url методом put
assertEquals(HttpStatus.LENGTH_REQUIRED.value(), getHttpURLConnection(url, true, "PUT").getResponseCode());
// запись данных методом put
HttpURLConnection urlConnection = getHttpURLConnection(url, true, "PUT");
OutputStreamWriter out = new OutputStreamWriter(urlConnection.getOutputStream());
out.write(stringForWrite);
out.close();
// чтобы завершить загрузку вызываем getResponseCode
assertEquals(HttpStatus.OK.value(), urlConnection.getResponseCode());
// файл перезаписывается и затирает метаданные.
// запись метаданных
// storageService.rewriteFileData(fileData, getInstant());
copyFromStorageToFile(fileData, testActualFile);
// testFile пустой
assertNotEquals(Files.readAllLines(testFile), Files.readAllLines(testActualFile));
Files.write(testFile, stringForWrite.getBytes());
assertEquals(Files.readAllLines(testFile), Files.readAllLines(testActualFile));
} finally {
Files.deleteIfExists(testFile);
Files.deleteIfExists(testActualFile);
}*/
}
@Test
public void urlForDownloadTest() throws IOException {
/*Path testFile = Files.createTempFile("", "test_file");
Files.write(testFile, "4815162342".getBytes());
Path testActualFile = Files.createTempFile("", "test_actual_file");
try {
FileData fileData = storageService.createNewFile("test_file", Collections.emptyMap(), getInstant());
// запись тестового файла в цеф
storageService.uploadFile(fileData.getFileId(), testFile);
// генерация url с доступом только для загрузки
URL url = storageService.generateDownloadUrl(fileData.getFileId(), getInstant());
// ошибка при запросе по url методом put
assertEquals(HttpStatus.FORBIDDEN.value(), getHttpURLConnection(url, true, "PUT").getResponseCode());
// ок при запросе по url методом get
assertEquals(HttpStatus.OK.value(), getHttpURLConnection(url, false, "GET").getResponseCode());
// получение содержимого методом get
HttpURLConnection urlConnection = getHttpURLConnection(url, false, "GET");
InputStream inputStream = urlConnection.getInputStream();
Files.copy(inputStream, testActualFile, StandardCopyOption.REPLACE_EXISTING);
assertEquals(Files.readAllLines(testFile), Files.readAllLines(testActualFile));
} finally {
Files.deleteIfExists(testFile);
Files.deleteIfExists(testActualFile);
}*/
}
private void copyFromStorageToFile(FileData fileData, Path file) throws IOException {
/*InputStream inputStream = storageService.getFile(fileData.getFileId());
Files.copy(inputStream, file, StandardCopyOption.REPLACE_EXISTING);*/
}
private HttpURLConnection getHttpURLConnection(URL url, boolean doOutput, String method) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(doOutput);
connection.setRequestMethod(method);
return connection;
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="info">
<appender-ref ref="CONSOLE"/>
</root>
<logger name="org.springframework.jdbc.core.JdbcTemplate">
<level value="error"/>
</logger>
<logger name="org.springframework.jdbc.core.StatementCreatorUtils">
<level value="error"/>
</logger>
<logger name="org.springframework.jdbc.support.JdbcAccessor">
<level value="error"/>
</logger>
<logger name="com.amazonaws">
<level value="error"/>
</logger>
<logger name="org.apache.http">
<level value="error"/>
</logger>
</configuration>