# CoStore
**Repository Path**: AhooWang/CoStore
## Basic Information
- **Project Name**: CoStore
- **Description**: A Kotlin multi-module library providing unified object storage abstractions for S3 and OSS-compatible backends.
- **Primary Language**: Kotlin
- **License**: Apache-2.0
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2026-04-14
- **Last Updated**: 2026-04-16
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
A Kotlin multi-module library providing unified object storage abstractions for S3 and OSS-compatible backends. JVM 17.
[](https://github.com/Ahoo-Wang/CoStore/blob/main/LICENSE)
[](https://central.sonatype.com/artifact/me.ahoo.costore/costore-core)
[](https://codecov.io/gh/Ahoo-Wang/CoStore)
---
## Features
- **Unified API** for S3 and Aliyun OSS providers
- **Four programming models** — Sync, Async (`CompletableFuture`), Reactive (`Mono`), Coroutines
- **Provider pattern** for flexible credential management
- **Type-safe request/response** model pairs
- **Spring Boot auto-configuration** for quick integration
---
## Modules
| Module | Description |
|--------|-------------|
| `:core` | Interfaces, models, provider abstractions, error types |
| `:s3` | AWS S3 implementation (AWS SDK v2) |
| `:oss` | Aliyun OSS implementation (Aliyun OSS SDK) |
| `:bom` | Bill of Materials for dependency management |
| `:spring-boot-starter` | Spring Boot auto-configuration |
---
## Quick Start
### Gradle
```kotlin
dependencies {
implementation("me.ahoo.costore:costore-s3")
implementation("me.ahoo.costore:costore-oss")
}
```
### Create a Store
**S3:**
```kotlin
val credentials = S3Credentials(
accessKeyId = "your-access-key-id",
secretAccessKey = "your-secret-access-key",
region = "us-east-1"
)
val provider = S3ObjectStoreProvider()
val store: ObjectStore = provider.sync(credentials)
```
**OSS:**
```kotlin
val credentials = OssCredentials(
endpoint = "https://oss-cn-hangzhou.aliyuncs.com",
accessKeyId = "your-access-key-id",
secretAccessKey = "your-secret-access-key"
)
val provider = OssObjectStoreProvider()
val store: ObjectStore = provider.sync(credentials)
```
---
## Programming Models
### Sync (Blocking)
```kotlin
val response = store.getObject(GetObjectRequest(bucket, key))
response.use { obj -> // StoredObject implements Closeable - MUST close!
obj.content.readBytes()
}
store.putObject(PutObjectRequest(
bucket = bucket,
key = key,
content = contentStream,
contentLength = contentSize, // Required for presigned URLs
contentType = "application/json"
))
store.deleteObject(DeleteObjectRequest(bucket, key))
```
### Async (`CompletableFuture`)
```kotlin
val asyncStore: AsyncObjectStore = provider.async(credentials)
val future: CompletableFuture = asyncStore.getObject(GetObjectRequest(bucket, key))
```
### Reactive (`Mono`)
```kotlin
val reactiveStore: ReactiveObjectStore = provider.reactive(credentials)
val mono: Mono = reactiveStore.getObject(GetObjectRequest(bucket, key))
```
### Coroutines (Suspend Functions)
```kotlin
val coroutinesStore: CoroutinesObjectStore = provider.coroutines(credentials)
val response = coroutinesStore.getObject(GetObjectRequest(bucket, key))
```
### Adapters
Convert between models on an existing store:
```kotlin
val syncStore: ObjectStore = s3Provider.sync(credentials)
val asyncStore: AsyncObjectStore = syncStore.asAsync()
val reactiveStore: ReactiveObjectStore = syncStore.asReactive()
val coroutinesStore: CoroutinesObjectStore = syncStore.asCoroutines()
```
---
## Operations
| Operation | Method | Description |
|-----------|--------|-------------|
| **Get** | `getObject(request)` | Download an object (content + metadata) |
| **Put** | `putObject(request)` | Upload an object |
| **Delete** | `deleteObject(request)` | Delete an object |
| **List** | `listObjects(request)` | List objects with prefix/delimiter |
| **Head** | `headObject(request)` | Get metadata without content |
| **Presign Get** | `presignGetObject(request)` | Generate a pre-signed GET URL |
| **Presign Put** | `presignPutObject(request)` | Generate a pre-signed PUT URL |
| **Presign Delete** | `presignDeleteObject(request)` | Generate a pre-signed DELETE URL (S3 only) |
---
## Request / Response Models
```kotlin
// Get
val getResponse: GetObjectResponse = store.getObject(GetObjectRequest(bucket, key))
getResponse.use { obj -> // StoredObject implements Closeable - MUST close!
obj.content.readBytes()
}
// getResponse.metadata: StoredObjectMetadata (bucket, key, contentLength, contentType, eTag, lastModified, metadata)
// Put
val putResponse = store.putObject(PutObjectRequest(
bucket = bucket,
key = key,
content = inputStream,
contentLength = contentSize, // Required - needed for presigned URLs and streaming
contentType = "application/json",
metadata = mapOf("x-custom" to "value")
))
// putResponse.eTag, putResponse.versionId
// Delete
val deleteResponse = store.deleteObject(DeleteObjectRequest(bucket, key))
// deleteResponse.deleteMarker, deleteResponse.versionId
// List
val listResponse = store.listObjects(ListObjectsRequest(bucket, prefix = "prefix/"))
// listResponse.objects: List
// listResponse.commonPrefixes: List
// listResponse.isTruncated, listResponse.nextMarker
// Head
val headResponse = store.headObject(HeadObjectRequest(bucket, key))
// headResponse.contentLength, headResponse.contentType, headResponse.lastModified, etc.
// Presign
val presignResponse = store.presignGetObject(PresignGetObjectRequest(bucket, key, Duration.ofMinutes(15)))
// presignResponse.url, presignResponse.expiration, presignResponse.headers
```
### Validation
Request models validate inputs in their `init` blocks:
- `bucket` - must not be blank or contain `\n`, `\r`, `\t`
- `key` - must not be blank or contain `\n`, `\r`, `\t`
- `maxKeys` (ListObjectsRequest) - must be 1-1000
---
## Pre-signed URLs
Pre-signed URLs grant temporary access to private objects without credentials.
```kotlin
// S3 presigned PUT (includes Content-Type header)
val presignedPut = store.presignPutObject(PresignPutObjectRequest(
bucket = bucket,
key = key,
expiration = Duration.ofMinutes(15),
contentType = "application/json",
metadata = mapOf("x-custom" to "value")
))
// presignedPut.url, presignedPut.headers["Content-Type"]
// S3 presigned DELETE
val presignedDelete = store.presignDeleteObject(PresignDeleteObjectRequest(bucket, key, Duration.ofMinutes(15)))
// Note: OSS does not support presigned DELETE URLs
```
---
## Error Handling
```kotlin
try {
store.getObject(GetObjectRequest(bucket, "nonexistent-key"))
} catch (e: ObjectNotFoundError) {
println("Bucket: ${e.bucket}, Key: ${e.key}")
} catch (e: CoStoreError) {
println("Error: ${e.message}")
}
```
---
## Spring Boot Integration
### Gradle
```kotlin
dependencies {
implementation("me.ahoo.costore:spring-boot-starter")
}
```
### Configuration
**S3:**
```yaml
costore:
s3:
access-key-id: ${AWS_ACCESS_KEY_ID}
secret-access-key: ${AWS_SECRET_ACCESS_KEY}
region: us-east-1
endpoint: https://... # optional, for S3-compatible stores
```
**OSS:**
```yaml
costore:
oss:
endpoint: https://oss-cn-hangzhou.aliyuncs.com
access-key-id: ${OSS_ACCESS_KEY_ID}
secret-access-key: ${OSS_SECRET_ACCESS_KEY}
```
### Usage
```kotlin
@Configuration
class StorageConfig {
@Bean
fun objectStore(store: ObjectStore): ObjectStore = store
}
```
Auto-configuration is activated automatically when the respective Spring Boot starter is on the classpath and the required properties are set (`costore.s3.region` for S3, `costore.oss.endpoint` for OSS).
---
## Build
```bash
./gradlew assemble # Build without tests
./gradlew build # Full build with tests
./gradlew test # Run all tests
./gradlew :core:test # Test specific module (:core, :s3, :oss)
./gradlew detekt # Lint
./gradlew :core:koverHtmlReportJvm # Coverage report for core
./gradlew :code-coverage-report:codeCoverageReport # Aggregated coverage
./gradlew publishToMavenLocal
```
---
## License
Apache License 2.0