# How to Configure File Storage with X File Storage for S3 Compatibility in ContiNew Admin

> Learn to configure S3-compatible file storage in ContiNew Admin with X File Storage. Set up OSS storage records and generate an AWS SDK 2.x S3Client for efficient uploads.

- Repository: [OpenContiNew/continew-admin](https://github.com/continew-org/continew-admin)
- Tags: how-to-guide
- Published: 2026-02-28

---

**To configure S3-compatible storage in ContiNew Admin, create an OSS-type storage record with your endpoint and credentials, which the system loads into X File Storage via `StorageServiceImpl.load()` to generate an AWS SDK 2.x `S3Client` for multipart uploads.**

ContiNew Admin leverages the **X File Storage** library from the Dromara ecosystem to abstract file operations across multiple backends. When you need to integrate Amazon S3, MinIO, or any S3-compatible service, you configure an **OSS** (Object Storage Service) entry that dynamically builds an `AmazonS3Config` and delegates operations to `S3StorageHandler`.

## Creating an OSS Storage Record

ContiNew Admin stores storage configurations in the database and loads them at runtime. To enable S3 compatibility, you must create a record with `type = OSS`.

### Required Configuration Fields

When creating the storage entry—whether through the admin interface or the REST API—populate these fields exactly as specified:

- **type**: Fixed value `OSS` (case-sensitive, validated by `StorageTypeEnum.OSS.validate`)
- **code**: Unique platform identifier used by X File Storage (e.g., `my-s3`)
- **endpoint**: Base URL of the S3-compatible service without trailing slash (e.g., `https://s3.amazonaws.com` or `https://minio.example.com`)
- **accessKey**: IAM access key ID
- **secretKey**: Secret access key (must be RSA-encrypted before transmission; decrypted server-side in `StorageServiceImpl.beforeCreate`)
- **bucketName**: Target bucket name, must end with a trailing slash `/` (e.g., `my-bucket/`)
- **domain** (optional): Public CDN prefix for direct URLs, must end with `/`
- **recycleBinEnabled / recycleBinPath** (optional): Enable soft-delete functionality with a path ending in `/`

### Via the Admin UI

Navigate to **System → Storage**, click **Create**, select **OSS**, and complete the form. The frontend automatically encrypts the `secretKey` using the RSA public key bundled with the application before sending the payload to `POST /api/system/storage`.

### Via REST API

```bash
curl -X POST "http://localhost:8080/api/system/storage" \
     -H "Content-Type: application/json" \
     -d '{
           "type": "OSS",
           "code": "my-s3",
           "endpoint": "https://s3.amazonaws.com",
           "accessKey": "AKIAIOSFODNN7EXAMPLE",
           "secretKey": "<RSA-ENCRYPTED-SECRET>",
           "bucketName": "production-uploads/",
           "domain": "https://cdn.example.com/",
           "recycleBinEnabled": true,
           "recycleBinPath": "trash/"
         }'

```

The server decrypts the secret key in `StorageServiceImpl.beforeCreate` (lines 66-73) before persisting to the database.

## Loading the Storage into X File Storage

When the application context refreshes or a storage is enabled, `StorageServiceImpl.load()` instantiates the X File Storage platform:

```java
public void load(StorageDO storage) {
    switch (storage.getType()) {
        case OSS -> {
            FileStorageProperties.AmazonS3Config config = new FileStorageProperties.AmazonS3Config();
            config.setPlatform(storage.getCode());
            config.setAccessKey(storage.getAccessKey());
            config.setSecretKey(storage.getSecretKey());
            config.setEndPoint(storage.getEndpoint());
            config.setBucketName(storage.getBucketName());
            fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(
                Collections.singletonList(config), null));
        }
        // ...
    }
}

```

This method (located in [`continew-system/src/main/java/top/continew/admin/system/service/impl/StorageServiceImpl.java`](https://github.com/continew-org/continew-admin/blob/main/continew-system/src/main/java/top/continew/admin/system/service/impl/StorageServiceImpl.java)) bridges the ContiNew Admin domain model with X File Storage's builder API, registering the platform for subsequent file operations.

## S3 Client Initialization and Caching

`S3ClientFactory` (in [`continew-system/src/main/java/top/continew/admin/system/factory/S3ClientFactory.java`](https://github.com/continew-org/continew-admin/blob/main/continew-system/src/main/java/top/continew/admin/system/factory/S3ClientFactory.java)) manages AWS SDK 2.x client instances with a unique cache key combining endpoint and access key:

```java
public S3Client getClient(StorageDO storage) {
    String key = storage.getEndpoint() + "|" + storage.getAccessKey();
    return CLIENT_CACHE.computeIfAbsent(key, k -> {
        StaticCredentialsProvider auth = StaticCredentialsProvider.create(
            AwsBasicCredentials.create(storage.getAccessKey(), storage.getSecretKey()));
        return S3Client.builder()
            .credentialsProvider(auth)
            .endpointOverride(URI.create(storage.getEndpoint()))
            .region(Region.US_EAST_1)
            .serviceConfiguration(S3Configuration.builder()
                .chunkedEncodingEnabled(false).build())
            .build();
    });
}

```

This design supports **simultaneous connections to multiple S3-compatible endpoints** (e.g., AWS S3 and a private MinIO cluster) within the same application instance.

## File Operations and Multipart Uploads

All S3-specific logic resides in `S3StorageHandler` ([`continew-system/src/main/java/top/continew/admin/system/handler/impl/S3StorageHandler.java`](https://github.com/continew-org/continew-admin/blob/main/continew-system/src/main/java/top/continew/admin/system/handler/impl/S3StorageHandler.java)). This handler normalizes object keys and manages the multipart upload lifecycle:

- **Key normalization**: `buildS3Key()` strips leading slashes and collapses duplicate slashes (e.g., `/folder//file.png` becomes `folder/file.png`)
- **Initiate upload**: `initMultipartUpload()` creates a `CreateMultipartUploadRequest` and stores the returned `uploadId`
- **Part upload**: `uploadPart()` streams `MultipartFile` bytes via `UploadPartRequest`
- **Completion**: `completeMultipartUpload()` aggregates part ETags into a `CompletedMultipartUpload`
- **Cleanup**: `cleanPart()` aborts incomplete multipart uploads to prevent orphaned storage charges

## Practical Implementation Example

After configuring the storage record with code `my-s3`, use the `FileService` API to upload files:

```java
import top.continew.admin.system.service.StorageService;
import top.continew.admin.system.service.FileService;
import org.dromara.x.file.storage.core.FileInfo;
import org.springframework.web.multipart.MultipartFile;

@Autowired
private StorageService storageService;

@Autowired
private FileService fileService;

public String uploadToS3(MultipartFile file) {
    // Retrieve the configured storage (null uses default)
    StorageDO storage = storageService.getByCode("my-s3");
    
    // Execute upload; parentPath can be empty for root
    FileInfo fileInfo = fileService.upload(
        file,           // MultipartFile from controller
        "",             // parentPath (optional subdirectory)
        storage.getCode() // platform code
    );
    
    // Returns public URL from configured domain or S3 endpoint
    return fileInfo.getUrl();
}

```

The `FileService.upload` method dispatches to `S3StorageHandler` based on the storage type enum (`StorageDO.getType()`), handling all multipart negotiation transparently.

## Summary

- **OSS storage type**: Use `type = OSS` in `StorageTypeEnum` for all S3-compatible services
- **Trailing slashes required**: Configure `bucketName` and optional `domain`/`recycleBinPath` with trailing `/` characters
- **Client-side encryption**: Always encrypt `secretKey` using RSA before API submission; decryption occurs in `StorageServiceImpl.beforeCreate`
- **Dynamic loading**: `StorageServiceImpl.load()` builds `AmazonS3Config` objects that X File Storage transforms into SDK clients via `S3ClientFactory`
- **Key normalization**: `S3StorageHandler.buildS3Key()` ensures S3 object keys comply with path conventions
- **Multipart support**: Large files automatically use multipart upload with `initMultipartUpload`, `uploadPart`, and `completeMultipartUpload` operations

## Frequently Asked Questions

### How do I configure MinIO instead of AWS S3?

Set the **endpoint** to your MinIO server URL (e.g., `https://minio.example.com`) and provide the MinIO access/secret keys. ContiNew Admin treats all S3-compatible services identically once the OSS record is created; `S3ClientFactory` automatically configures the endpoint override in the AWS SDK builder.

### Why does my upload fail with "Invalid bucket name" errors?

Verify that your **bucketName** field ends with a trailing slash (`/`). The system concatenates this value directly with the object key in `S3StorageHandler`. Additionally, ensure the **endpoint** contains no trailing slash, as this creates malformed URLs when the SDK appends bucket paths.

### Can I use multiple S3 buckets simultaneously?

Yes. Create separate storage records with unique **code** values (e.g., `s3-primary`, `s3-archive`). `StorageServiceImpl.load()` registers each as a distinct platform in X File Storage. When uploading, specify the desired platform code in `FileService.upload()`, or set a default storage in the system configuration.

### Is the secret key stored securely in the database?

Yes. The client encrypts the secret using the server's RSA public key before transmission. Upon receipt, `StorageServiceImpl.beforeCreate` decrypts it using the private key, then X File Storage may re-encrypt or handle it according to your configured `FileStorageProperties` security settings. Always ensure the RSA key pair is properly rotated according to your security policy.