diff --git a/hsweb-system/hsweb-system-file/pom.xml b/hsweb-system/hsweb-system-file/pom.xml index 5351fa264..bda09ae7e 100644 --- a/hsweb-system/hsweb-system-file/pom.xml +++ b/hsweb-system/hsweb-system-file/pom.xml @@ -54,6 +54,14 @@ spring-test test + + + software.amazon.awssdk + s3 + ${aws.sdk.version} + true + + \ No newline at end of file diff --git a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileServiceConfiguration.java b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileServiceConfiguration.java index 082c8cf86..b4d67db33 100644 --- a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileServiceConfiguration.java +++ b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileServiceConfiguration.java @@ -8,7 +8,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; + @AutoConfiguration @EnableConfigurationProperties(FileUploadProperties.class) diff --git a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java index 4cd67adfa..d9f599b26 100644 --- a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java +++ b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java @@ -1,5 +1,6 @@ package org.hswebframework.web.file; +import lombok.Data; import lombok.Getter; import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; diff --git a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/S3FileProperties.java b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/S3FileProperties.java new file mode 100644 index 000000000..40d943388 --- /dev/null +++ b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/S3FileProperties.java @@ -0,0 +1,15 @@ +package org.hswebframework.web.file; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "hsweb.file.upload.s3") +@Data +public class S3FileProperties { + private String endpoint; + private String accessKey; + private String secretKey; + private String bucket; + private String region; + private String baseUrl; +} \ No newline at end of file diff --git a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/S3FileStorageConfiguration.java b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/S3FileStorageConfiguration.java new file mode 100644 index 000000000..83b9cee1a --- /dev/null +++ b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/S3FileStorageConfiguration.java @@ -0,0 +1,43 @@ +package org.hswebframework.web.file; + +import org.hswebframework.web.file.service.FileStorageService; +import org.hswebframework.web.file.service.S3FileStorageService; +import org.hswebframework.web.file.web.ReactiveFileController; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +import java.net.URI; + +@Configuration +@ConditionalOnClass(S3Client.class) +@ConditionalOnProperty(name = "hsweb.file.storage", havingValue = "s3", matchIfMissing = false) +@EnableConfigurationProperties({S3FileProperties.class, FileUploadProperties.class}) +public class S3FileStorageConfiguration { + + + @Bean + @ConditionalOnMissingBean + public S3Client s3Client(S3FileProperties properties) { + return S3Client.builder() + .endpointOverride(URI.create(properties.getEndpoint())) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()))) + .region(Region.of(properties.getRegion())) + .build(); + } + + @Bean + public FileStorageService s3FileStorageService( + S3FileProperties s3Properties, + S3Client s3Client) { + return new S3FileStorageService(s3Properties, s3Client); + } +} diff --git a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/service/FileStorageService.java b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/service/FileStorageService.java index 8a2a93276..8888d54ae 100644 --- a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/service/FileStorageService.java +++ b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/service/FileStorageService.java @@ -3,6 +3,7 @@ import org.springframework.http.codec.multipart.FilePart; import reactor.core.publisher.Mono; +import java.io.IOException; import java.io.InputStream; /** diff --git a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/service/S3FileStorageService.java b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/service/S3FileStorageService.java new file mode 100644 index 000000000..89339a48e --- /dev/null +++ b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/service/S3FileStorageService.java @@ -0,0 +1,83 @@ +package org.hswebframework.web.file.service; + +import com.google.common.io.Files; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.hswebframework.web.file.S3FileProperties; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.codec.multipart.FilePart; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import java.util.UUID; + +@AllArgsConstructor +public class S3FileStorageService implements FileStorageService { + + private final S3FileProperties properties; + private final S3Client s3Client; + + + @Override + public Mono saveFile(FilePart filePart) { + String filename = buildFileName(filePart.filename()); + + return DataBufferUtils.join(filePart.content()) + .flatMap(dataBuffer -> { + InputStream inputStream = dataBuffer.asInputStream(true); + return saveFile(inputStream, Files.getFileExtension(filename)); + }); + } + + + @Override + @SneakyThrows + public Mono saveFile(InputStream inputStream, String fileType) { + return Mono.fromCallable(() -> { + String key = UUID.randomUUID().toString() + (fileType.startsWith(".") ? fileType : "." + fileType); + + PutObjectRequest request = PutObjectRequest.builder() + .bucket(properties.getBucket()) + .key(key) + .build(); + + s3Client.putObject(request, RequestBody.fromInputStream(inputStream, inputStream.available())); + return buildFileUrl(key); + }) + .subscribeOn(Schedulers.boundedElastic()); + } + + private String buildFileName(String originalName) { + String suffix = ""; + if (originalName != null && originalName.contains(".")) { + suffix = originalName.substring(originalName.lastIndexOf(".")); + } + return UUID.randomUUID().toString().replace("-", "") + suffix.toLowerCase(Locale.ROOT); + } + + private String buildFileUrl(String key) { + if (properties.getBaseUrl() != null && !properties.getBaseUrl().isEmpty()) { + return UriComponentsBuilder + .fromUriString(properties.getBaseUrl()) + .pathSegment(key) + .build() + .toUriString(); + } + String host = properties.getBucket() + "." + properties.getEndpoint().replaceFirst("^https?://", ""); + return UriComponentsBuilder + .newInstance() + .scheme("https") + .host(host) + .pathSegment(key) + .build() + .toUriString(); + } +} diff --git a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/web/ReactiveFileController.java b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/web/ReactiveFileController.java index c55a3ee2f..876dccf28 100644 --- a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/web/ReactiveFileController.java +++ b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/web/ReactiveFileController.java @@ -11,16 +11,17 @@ import org.hswebframework.web.authorization.exception.AccessDenyException; import org.hswebframework.web.file.FileUploadProperties; import org.hswebframework.web.file.service.FileStorageService; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.MediaType; import org.springframework.http.codec.multipart.FilePart; import org.springframework.http.codec.multipart.Part; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; -import java.io.File; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; @RestController @Resource(id = "file", name = "文件上传") @@ -33,11 +34,13 @@ public class ReactiveFileController { private final FileStorageService fileStorageService; + public ReactiveFileController(FileUploadProperties properties, FileStorageService fileStorageService) { this.properties = properties; this.fileStorageService = fileStorageService; } + @PostMapping("/static") @SneakyThrows @ResourceAction(id = "upload-static", name = "静态文件") @@ -59,4 +62,20 @@ public Mono uploadStatic(@RequestPart("file") } + @PostMapping(value = "/static/stream", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE) + @Operation(summary = "上传文件流") + public Mono uploadOssStream(ServerHttpRequest request, + @RequestParam("fileType") String fileType) { + + if (properties.denied("upload." + fileType, MediaType.APPLICATION_OCTET_STREAM)) { + return Mono.error(new AccessDenyException()); + } + + return DataBufferUtils.join(request.getBody()) + .flatMap(dataBuffer -> { + InputStream inputStream = dataBuffer.asInputStream(true); + return fileStorageService.saveFile(inputStream, fileType); + }); + } + } diff --git a/hsweb-system/hsweb-system-file/src/test/java/org/hswebframework/web/file/web/OssUploadTest.java b/hsweb-system/hsweb-system-file/src/test/java/org/hswebframework/web/file/web/OssUploadTest.java new file mode 100644 index 000000000..f047c2325 --- /dev/null +++ b/hsweb-system/hsweb-system-file/src/test/java/org/hswebframework/web/file/web/OssUploadTest.java @@ -0,0 +1,61 @@ +package org.hswebframework.web.file.web; + +import org.hswebframework.web.file.S3FileStorageConfiguration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpEntity; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.StreamUtils; +import org.springframework.web.reactive.function.BodyInserters; + +@WebFluxTest(ReactiveFileController.class) +@RunWith(SpringRunner.class) +@ImportAutoConfiguration(S3FileStorageConfiguration.class) +public class OssUploadTest { + + static { + System.setProperty("hsweb.file.upload.s3.endpoint", "https://oss-cn-beijing.aliyuncs.com"); + System.setProperty("hsweb.file.upload.s3.region", "us-east-1"); + System.setProperty("hsweb.file.upload.s3.accessKey", ""); + System.setProperty("hsweb.file.upload.s3.secretKey", ""); + System.setProperty("hsweb.file.upload.s3.bucket", "maydaysansan"); + System.setProperty("hsweb.file.storage", "s3"); + } + + @Autowired + WebTestClient client; + + @Test + public void testStatic(){ + client.post() + .uri("/file/static") + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData("file",new HttpEntity<>(new ClassPathResource("test.json")))) + .exchange() + .expectStatus() + .isOk(); + + } + + @Test + public void testStream() throws Exception { + byte[] fileBytes = StreamUtils.copyToByteArray(new ClassPathResource("test.json").getInputStream()); + + client.post() + .uri(uriBuilder -> + uriBuilder.path("/file/static/stream") + .queryParam("fileType", "json") + .build()) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .bodyValue(fileBytes) + .exchange() + .expectStatus().isOk(); + } + +} \ No newline at end of file diff --git a/hsweb-system/hsweb-system-file/src/test/java/org/hswebframework/web/file/web/ReactiveFileControllerTest.java b/hsweb-system/hsweb-system-file/src/test/java/org/hswebframework/web/file/web/ReactiveFileControllerTest.java index 67ff2654a..d1cf5b6e0 100644 --- a/hsweb-system/hsweb-system-file/src/test/java/org/hswebframework/web/file/web/ReactiveFileControllerTest.java +++ b/hsweb-system/hsweb-system-file/src/test/java/org/hswebframework/web/file/web/ReactiveFileControllerTest.java @@ -29,6 +29,7 @@ public class ReactiveFileControllerTest { static { System.setProperty("hsweb.file.upload.static-file-path","./target/upload"); + System.setProperty("hsweb.file.storage","local"); // System.setProperty("hsweb.file.upload.use-original-file-name","true"); } diff --git a/pom.xml b/pom.xml index 13a59626f..774c71a07 100644 --- a/pom.xml +++ b/pom.xml @@ -95,6 +95,8 @@ 2.7.0 4.1.111.Final Borca-SR2 + Borca-SR2 + 2.25.5