Browse Source

# 学习S3对象储存协议
- 使用S3对象储存协议对接CloudFlare R2对象储存

yang yi 1 tháng trước cách đây
commit
f8686e34b0

+ 37 - 0
pom.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>space.anyi</groupId>
+    <artifactId>S3_learn</artifactId>
+    <version>1.0.0</version>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+    </properties>
+    <dependencies>
+        <!-- Source: https://mvnrepository.com/artifact/software.amazon.awssdk/s3 -->
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3</artifactId>
+            <version>2.25.27</version>
+        </dependency>
+        <!-- Source: https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>5.14.3</version>
+            <scope>test</scope>
+        </dependency>
+        <!-- Source: https://mvnrepository.com/artifact/org.mockito/mockito-core -->
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>5.8.0</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 76 - 0
src/main/java/space/anyi/s3/cloudflare/r2/R2Application.java

@@ -0,0 +1,76 @@
+package space.anyi.s3.cloudflare.r2;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class R2Application {
+    private static final String prefix = "imgs/";
+    private static final Set<String> IMAGE_EXTENSIONS = new HashSet<>(Arrays.asList(
+            ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"
+    ));
+
+    public static void main(String[] args) throws IOException {
+        if (args.length < 5) {
+            System.out.println("Usage: R2Application <accountId> <accessKey> <secretKey> <bucketName> <folderPath>");
+            return;
+        }
+
+        String accountId = args[0];
+        String accessKey = args[1];
+        String secretKey = args[2];
+        String bucketName = args[3];
+        String folderPath = args[4];
+
+        R2Configuration configuration = new R2Configuration(accountId, accessKey, secretKey);
+        R2Handler handler = new R2Handler(configuration);
+
+        try {
+            if (!handler.bucketExists(bucketName)) {
+                System.out.println("Bucket '" + bucketName + "' does not exist.");
+                return;
+            }
+
+            Path folder = Paths.get(folderPath);
+            if (!Files.exists(folder) || !Files.isDirectory(folder)) {
+                System.out.println("Invalid folder path: " + folderPath);
+                return;
+            }
+
+            int successCount = 0;
+            int failCount = 0;
+
+            try (DirectoryStream<Path> stream = Files.newDirectoryStream(folder)) {
+                for (Path file : stream) {
+                    if (Files.isRegularFile(file) && isImageFile(file)) {
+                        String objectName = file.getFileName().toString();
+                        System.out.println("Uploading: " + objectName);
+                        handler.deleteObject(bucketName,objectName);
+                        if (handler.uploadFile(bucketName, prefix + objectName, file)) {
+                            System.out.println("Success: " + objectName);
+                            successCount++;
+                        } else {
+                            System.out.println("Failed: " + objectName);
+                            failCount++;
+                        }
+                    }
+                }
+            }
+
+            System.out.println("\nUpload completed. Success: " + successCount + ", Failed: " + failCount);
+
+        } finally {
+            handler.shutdown();
+        }
+    }
+
+    private static boolean isImageFile(Path file) {
+        String fileName = file.getFileName().toString().toLowerCase();
+        return IMAGE_EXTENSIONS.stream().anyMatch(fileName::endsWith);
+    }
+}

+ 76 - 0
src/main/java/space/anyi/s3/cloudflare/r2/R2Configuration.java

@@ -0,0 +1,76 @@
+package space.anyi.s3.cloudflare.r2;
+
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3Configuration;
+
+import java.net.URI;
+
+public class R2Configuration {
+    private String accountId;
+    private String accessKey;
+    private String secretKey;
+    private String endpoint;
+    private S3Configuration s3Configuration;
+
+    public R2Configuration(String accountId, String accessKey, String secretKey) {
+        this.accountId = accountId;
+        this.accessKey = accessKey;
+        this.secretKey = secretKey;
+        this.endpoint = String.format("https://%s.r2.cloudflarestorage.com", accountId);
+        this.s3Configuration = S3Configuration.builder()
+                .pathStyleAccessEnabled(true)
+                .build();
+    }
+
+    public S3Client buildClient() {
+        AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);
+        return S3Client.builder()
+                .credentialsProvider(() -> credentials)
+                .region(Region.of("auto"))
+                .endpointOverride(URI.create(endpoint))
+                .serviceConfiguration(s3Configuration)
+                .build();
+    }
+
+    public String getAccountId() {
+        return accountId;
+    }
+
+    public void setAccountId(String accountId) {
+        this.accountId = accountId;
+    }
+
+    public String getAccessKey() {
+        return accessKey;
+    }
+
+    public void setAccessKey(String accessKey) {
+        this.accessKey = accessKey;
+    }
+
+    public String getSecretKey() {
+        return secretKey;
+    }
+
+    public void setSecretKey(String secretKey) {
+        this.secretKey = secretKey;
+    }
+
+    public String getEndpoint() {
+        return endpoint;
+    }
+
+    public void setEndpoint(String endpoint) {
+        this.endpoint = endpoint;
+    }
+
+    public S3Configuration getS3Configuration() {
+        return s3Configuration;
+    }
+
+    public void setS3Configuration(S3Configuration s3Configuration) {
+        this.s3Configuration = s3Configuration;
+    }
+}

+ 154 - 0
src/main/java/space/anyi/s3/cloudflare/r2/R2Handler.java

@@ -0,0 +1,154 @@
+package space.anyi.s3.cloudflare.r2;
+
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class R2Handler {
+    private final R2Configuration configuration;
+    private final S3Client s3Client;
+
+    public R2Handler(R2Configuration configuration) {
+        this.configuration = configuration;
+        this.s3Client = configuration.buildClient();
+    }
+
+    public boolean uploadObject(String bucketName, String objectName, byte[] data) {
+        try {
+            PutObjectRequest request = PutObjectRequest.builder()
+                    .bucket(bucketName)
+                    .key(objectName)
+                    .contentLength((long) data.length)
+                    .build();
+            s3Client.putObject(request, RequestBody.fromBytes(data));
+            return true;
+        } catch (S3Exception e) {
+            System.err.println("Error uploading object: " + e.awsErrorDetails().errorMessage());
+            return false;
+        }
+    }
+
+    public boolean uploadFile(String bucketName, String objectName, Path filePath) {
+        try {
+            byte[] data = Files.readAllBytes(filePath);
+            String contentType = Files.probeContentType(filePath);
+            PutObjectRequest.Builder requestBuilder = PutObjectRequest.builder()
+                    .bucket(bucketName)
+                    .key(objectName)
+                    .contentLength((long) data.length);
+            if (contentType != null) {
+                requestBuilder.contentType(contentType);
+            }
+            s3Client.putObject(requestBuilder.build(), RequestBody.fromBytes(data));
+            return true;
+        } catch (IOException e) {
+            System.err.println("Error reading file: " + e.getMessage());
+            return false;
+        } catch (S3Exception e) {
+            System.err.println("Error uploading file: " + e.awsErrorDetails().errorMessage());
+            return false;
+        }
+    }
+
+    public byte[] downloadObject(String bucketName, String objectName) {
+        try {
+            GetObjectRequest request = GetObjectRequest.builder()
+                    .bucket(bucketName)
+                    .key(objectName)
+                    .build();
+            InputStream inputStream = s3Client.getObject(request);
+            return inputStream.readAllBytes();
+        } catch (S3Exception e) {
+            System.err.println("Error downloading object: " + e.awsErrorDetails().errorMessage());
+            return null;
+        } catch (Exception e) {
+            System.err.println("Error reading object data: " + e.getMessage());
+            return null;
+        }
+    }
+
+    public boolean deleteObject(String bucketName, String objectName) {
+        try {
+            DeleteObjectRequest request = DeleteObjectRequest.builder()
+                    .bucket(bucketName)
+                    .key(objectName)
+                    .build();
+            s3Client.deleteObject(request);
+            return true;
+        } catch (S3Exception e) {
+            System.err.println("Error deleting object: " + e.awsErrorDetails().errorMessage());
+            return false;
+        }
+    }
+
+    public boolean bucketExists(String bucketName) {
+        try {
+            HeadBucketRequest request = HeadBucketRequest.builder()
+                    .bucket(bucketName)
+                    .build();
+            s3Client.headBucket(request);
+            return true;
+        } catch (S3Exception e) {
+            if (e.statusCode() == 404) {
+                return false;
+            }
+            System.err.println("Error checking bucket: " + e.awsErrorDetails().errorMessage());
+            return false;
+        }
+    }
+
+    public boolean createBucket(String bucketName) {
+        try {
+            if (!bucketExists(bucketName)) {
+                CreateBucketRequest request = CreateBucketRequest.builder()
+                        .bucket(bucketName)
+                        .build();
+                s3Client.createBucket(request);
+                return true;
+            }
+            return false;
+        } catch (S3Exception e) {
+            System.err.println("Error creating bucket: " + e.awsErrorDetails().errorMessage());
+            return false;
+        }
+    }
+
+    public ListObjectsV2Response listObjects(String bucketName) {
+        try {
+            ListObjectsV2Request request = ListObjectsV2Request.builder()
+                    .bucket(bucketName)
+                    .build();
+            return s3Client.listObjectsV2(request);
+        } catch (S3Exception e) {
+            System.err.println("Error listing objects: " + e.awsErrorDetails().errorMessage());
+            return null;
+        }
+    }
+
+    public boolean copyObject(String sourceBucket, String sourceKey, String destBucket, String destKey) {
+        try {
+            CopyObjectRequest request = CopyObjectRequest.builder()
+                    .sourceBucket(sourceBucket)
+                    .sourceKey(sourceKey)
+                    .destinationBucket(destBucket)
+                    .destinationKey(destKey)
+                    .build();
+            s3Client.copyObject(request);
+            return true;
+        } catch (S3Exception e) {
+            System.err.println("Error copying object: " + e.awsErrorDetails().errorMessage());
+            return false;
+        }
+    }
+
+    public void shutdown() {
+        if (s3Client != null) {
+            s3Client.close();
+        }
+    }
+}

+ 72 - 0
src/test/java/space/anyi/s3/cloudflare/r2/R2ConfigurationTest.java

@@ -0,0 +1,72 @@
+package space.anyi.s3.cloudflare.r2;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3Configuration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class R2ConfigurationTest {
+
+    private R2Configuration configuration;
+
+    @BeforeEach
+    void setUp() {
+        configuration = new R2Configuration("test-account-id", "test-access-key", "test-secret-key");
+    }
+
+    @Test
+    void testConstructorSetsAccountId() {
+        assertEquals("test-account-id", configuration.getAccountId());
+    }
+
+    @Test
+    void testConstructorSetsAccessKey() {
+        assertEquals("test-access-key", configuration.getAccessKey());
+    }
+
+    @Test
+    void testConstructorSetsSecretKey() {
+        assertEquals("test-secret-key", configuration.getSecretKey());
+    }
+
+    @Test
+    void testConstructorSetsCorrectEndpoint() {
+        String expectedEndpoint = "https://test-account-id.r2.cloudflarestorage.com";
+        assertEquals(expectedEndpoint, configuration.getEndpoint());
+    }
+
+    @Test
+    void testConstructorSetsClientConfiguration() {
+        assertNotNull(configuration.getS3Configuration());
+    }
+
+    @Test
+    void testBuildClientReturnsNonNull() {
+        S3Client client = configuration.buildClient();
+        assertNotNull(client);
+    }
+
+    @Test
+    void testSettersAndGetters() {
+        configuration.setAccountId("new-account-id");
+        configuration.setAccessKey("new-access-key");
+        configuration.setSecretKey("new-secret-key");
+        configuration.setEndpoint("https://new-endpoint.com");
+
+        assertEquals("new-account-id", configuration.getAccountId());
+        assertEquals("new-access-key", configuration.getAccessKey());
+        assertEquals("new-secret-key", configuration.getSecretKey());
+        assertEquals("https://new-endpoint.com", configuration.getEndpoint());
+    }
+
+    @Test
+    void testS3ConfigurationCanBeReplaced() {
+        S3Configuration newConfig = S3Configuration.builder()
+                .pathStyleAccessEnabled(true)
+                .build();
+        configuration.setS3Configuration(newConfig);
+        assertSame(newConfig, configuration.getS3Configuration());
+    }
+}

+ 265 - 0
src/test/java/space/anyi/s3/cloudflare/r2/R2HandlerTest.java

@@ -0,0 +1,265 @@
+package space.anyi.s3.cloudflare.r2;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+class R2HandlerTest {
+
+    @Mock
+    private R2Configuration configuration;
+
+    @Mock
+    private S3Client s3Client;
+
+    private R2Handler handler;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        when(configuration.buildClient()).thenReturn(s3Client);
+        handler = new R2Handler(configuration);
+    }
+
+    @Test
+    void testConstructorInitializesHandler() {
+        assertNotNull(handler);
+        verify(configuration, times(1)).buildClient();
+    }
+
+    @Test
+    void testUploadObjectSuccess() {
+        byte[] data = "test data".getBytes();
+        when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
+                .thenReturn(PutObjectResponse.builder().build());
+
+        boolean result = handler.uploadObject("test-bucket", "test-object", data);
+
+        assertTrue(result);
+        verify(s3Client, times(1)).putObject(any(PutObjectRequest.class), any(RequestBody.class));
+    }
+
+    @Test
+    void testUploadObjectFailure() {
+        byte[] data = "test data".getBytes();
+        when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
+                .thenThrow(S3Exception.builder()
+                        .awsErrorDetails(AwsErrorDetails.builder().errorMessage("Upload failed").build())
+                        .build());
+
+        boolean result = handler.uploadObject("test-bucket", "test-object", data);
+
+        assertFalse(result);
+    }
+
+    @Test
+    void testDownloadObjectSuccess() {
+        byte[] expectedData = "test data".getBytes();
+        ResponseInputStream<GetObjectResponse> responseInputStream = 
+                new ResponseInputStream<>(GetObjectResponse.builder().build(), 
+                        new java.io.ByteArrayInputStream(expectedData));
+        when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(responseInputStream);
+
+        byte[] result = handler.downloadObject("test-bucket", "test-object");
+
+        assertNotNull(result);
+        assertArrayEquals(expectedData, result);
+    }
+
+    @Test
+    void testDownloadObjectFailure() {
+        when(s3Client.getObject(any(GetObjectRequest.class)))
+                .thenThrow(S3Exception.builder()
+                        .awsErrorDetails(AwsErrorDetails.builder().errorMessage("Download failed").build())
+                        .build());
+
+        byte[] result = handler.downloadObject("test-bucket", "test-object");
+
+        assertNull(result);
+    }
+
+    @Test
+    void testDeleteObjectSuccess() {
+        when(s3Client.deleteObject(any(DeleteObjectRequest.class)))
+                .thenReturn(DeleteObjectResponse.builder().build());
+
+        boolean result = handler.deleteObject("test-bucket", "test-object");
+
+        assertTrue(result);
+        verify(s3Client, times(1)).deleteObject(any(DeleteObjectRequest.class));
+    }
+
+    @Test
+    void testDeleteObjectFailure() {
+        when(s3Client.deleteObject(any(DeleteObjectRequest.class)))
+                .thenThrow(S3Exception.builder()
+                        .awsErrorDetails(AwsErrorDetails.builder().errorMessage("Delete failed").build())
+                        .build());
+
+        boolean result = handler.deleteObject("test-bucket", "test-object");
+
+        assertFalse(result);
+    }
+
+    @Test
+    void testBucketExistsTrue() {
+        when(s3Client.headBucket(any(HeadBucketRequest.class)))
+                .thenReturn(HeadBucketResponse.builder().build());
+
+        boolean result = handler.bucketExists("test-bucket");
+
+        assertTrue(result);
+    }
+
+    @Test
+    void testBucketExistsFalse() {
+        when(s3Client.headBucket(any(HeadBucketRequest.class)))
+                .thenThrow(S3Exception.builder().statusCode(404).build());
+
+        boolean result = handler.bucketExists("test-bucket");
+
+        assertFalse(result);
+    }
+
+    @Test
+    void testCreateBucketSuccess() {
+        when(s3Client.headBucket(any(HeadBucketRequest.class)))
+                .thenThrow(S3Exception.builder().statusCode(404).build());
+        when(s3Client.createBucket(any(CreateBucketRequest.class)))
+                .thenReturn(CreateBucketResponse.builder().build());
+
+        boolean result = handler.createBucket("new-bucket");
+
+        assertTrue(result);
+        verify(s3Client, times(1)).createBucket(any(CreateBucketRequest.class));
+    }
+
+    @Test
+    void testCreateBucketAlreadyExists() {
+        when(s3Client.headBucket(any(HeadBucketRequest.class)))
+                .thenReturn(HeadBucketResponse.builder().build());
+
+        boolean result = handler.createBucket("existing-bucket");
+
+        assertFalse(result);
+        verify(s3Client, never()).createBucket(any(CreateBucketRequest.class));
+    }
+
+    @Test
+    void testListObjectsSuccess() {
+        ListObjectsV2Response mockResponse = ListObjectsV2Response.builder()
+                .contents(new java.util.ArrayList<>())
+                .build();
+        when(s3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(mockResponse);
+
+        ListObjectsV2Response result = handler.listObjects("test-bucket");
+
+        assertNotNull(result);
+        verify(s3Client, times(1)).listObjectsV2(any(ListObjectsV2Request.class));
+    }
+
+    @Test
+    void testListObjectsFailure() {
+        when(s3Client.listObjectsV2(any(ListObjectsV2Request.class)))
+                .thenThrow(S3Exception.builder()
+                        .awsErrorDetails(AwsErrorDetails.builder().errorMessage("List failed").build())
+                        .build());
+
+        ListObjectsV2Response result = handler.listObjects("test-bucket");
+
+        assertNull(result);
+    }
+
+    @Test
+    void testCopyObjectSuccess() {
+        when(s3Client.copyObject(any(CopyObjectRequest.class)))
+                .thenReturn(CopyObjectResponse.builder().build());
+
+        boolean result = handler.copyObject("source-bucket", "source-key", "dest-bucket", "dest-key");
+
+        assertTrue(result);
+        verify(s3Client, times(1)).copyObject(any(CopyObjectRequest.class));
+    }
+
+    @Test
+    void testCopyObjectFailure() {
+        when(s3Client.copyObject(any(CopyObjectRequest.class)))
+                .thenThrow(S3Exception.builder()
+                        .awsErrorDetails(AwsErrorDetails.builder().errorMessage("Copy failed").build())
+                        .build());
+
+        boolean result = handler.copyObject("source-bucket", "source-key", "dest-bucket", "dest-key");
+
+        assertFalse(result);
+    }
+
+    @Test
+    void testShutdown() {
+        handler.shutdown();
+
+        verify(s3Client, times(1)).close();
+    }
+
+    @Test
+    void testUploadFileSuccess() {
+        java.nio.file.Path tempFile = null;
+        try {
+            tempFile = java.nio.file.Files.createTempFile("test", ".txt");
+            java.nio.file.Files.writeString(tempFile, "test content");
+
+            when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
+                    .thenReturn(PutObjectResponse.builder().build());
+
+            boolean result = handler.uploadFile("test-bucket", "test-object", tempFile);
+
+            assertTrue(result);
+            verify(s3Client, times(1)).putObject(any(PutObjectRequest.class), any(RequestBody.class));
+        } catch (Exception e) {
+            fail("Test failed due to exception: " + e.getMessage());
+        } finally {
+            if (tempFile != null) {
+                try {
+                    java.nio.file.Files.deleteIfExists(tempFile);
+                } catch (Exception ignored) {
+                }
+            }
+        }
+    }
+
+    @Test
+    void testUploadFileFailure() {
+        java.nio.file.Path tempFile = null;
+        try {
+            tempFile = java.nio.file.Files.createTempFile("test", ".txt");
+            java.nio.file.Files.writeString(tempFile, "test content");
+
+            when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
+                    .thenThrow(S3Exception.builder()
+                            .awsErrorDetails(AwsErrorDetails.builder().errorMessage("Upload failed").build())
+                            .build());
+
+            boolean result = handler.uploadFile("test-bucket", "test-object", tempFile);
+
+            assertFalse(result);
+        } catch (Exception e) {
+            fail("Test failed due to exception: " + e.getMessage());
+        } finally {
+            if (tempFile != null) {
+                try {
+                    java.nio.file.Files.deleteIfExists(tempFile);
+                } catch (Exception ignored) {
+                }
+            }
+        }
+    }
+}