Skip to content

Commit 9303a69

Browse files
Merge upstream develop
2 parents 414d646 + c9d0277 commit 9303a69

11 files changed

Lines changed: 1689 additions & 203 deletions

File tree

Cargo.lock

Lines changed: 1265 additions & 87 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

compose.yml

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,57 +14,52 @@ services:
1414
retries: 5
1515
start_period: 10s
1616

17-
opensearch:
18-
image: opensearchproject/opensearch:2.1.0
19-
ports:
20-
- '127.0.0.1:9200:9200'
21-
environment:
22-
- cluster.name=opensearch-cluster
23-
- node.name=opensearch-node1
24-
- bootstrap.memory_lock=true
25-
- 'OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m'
26-
- 'DISABLE_INSTALL_DEMO_CONFIG=true'
27-
- 'DISABLE_SECURITY_PLUGIN=true'
28-
- 'discovery.type=single-node'
29-
healthcheck:
30-
test: ['CMD-SHELL', 'curl -s http://localhost:9200 || exit 1']
31-
interval: 5s
32-
timeout: 5s
33-
retries: 30
34-
start_period: 15s
35-
36-
legacy-server:
17+
server:
3718
build:
3819
context: .
39-
dockerfile: legacy/services/polycentric-server/Dockerfile
20+
dockerfile: services/server/Dockerfile
4021
ports:
41-
- '127.0.0.1:${POLYCENTRIC_PORT:-8787}:8787'
22+
- ${POLYCENTRIC_SERVER_PORT:-3000}:3000
4223
environment:
4324
- RUST_LOG=debug
4425
- DATABASE_URL=postgres://postgres:testing@postgres:5432
45-
- OPENSEARCH_STRING=http://opensearch:9200
46-
- ADMIN_TOKEN=123
47-
- CHALLENGE_KEY=456
48-
- STATSD_ADDRESS=127.0.0.1
26+
- CONTENT_BLOB_OS_BUCKET=polycentric-blobs
27+
- CONTENT_BLOB_OS_ENDPOINT=http://rustfs:9000
28+
- CONTENT_BLOB_OS_FORCE_PATH_STYLE=true
29+
- CONTENT_BLOB_OS_ACCESS_KEY=rustfsadmin
30+
- CONTENT_BLOB_OS_SECRET_KEY=rustfsadmin
4931
depends_on:
5032
postgres:
5133
condition: service_healthy
52-
opensearch:
53-
condition: service_healthy
34+
rustfs-init:
35+
condition: service_completed_successfully
5436

55-
legacy-verifiers:
56-
build: ./legacy/services/verifiers-server
57-
working_dir: /app/legacy/services/verifiers-server
37+
rustfs:
38+
image: rustfs/rustfs:latest
5839
volumes:
59-
- .:/app
40+
- rustfs-data:/data
6041
ports:
61-
- '127.0.0.1:${VERIFIERS_PORT:-3002}:3002'
62-
environment:
63-
- SERVER_URL=http://legacy-server:8787
64-
command: sh -c "npm install && npx nodemon src/app.ts"
42+
- 9000:9000
43+
- 9001:9001
44+
healthcheck:
45+
test: ['CMD', 'curl', '-sf', 'http://localhost:9000/health']
46+
interval: 10s
47+
timeout: 5s
48+
retries: 5
49+
start_period: 10s
50+
51+
rustfs-init:
52+
image: rustfs/rc:latest
6553
depends_on:
66-
postgres:
54+
rustfs:
6755
condition: service_healthy
56+
entrypoint: >
57+
/bin/sh -c "
58+
until rc alias set rustfs http://rustfs:9000 rustfsadmin rustfsadmin >/dev/null 2>&1; do
59+
echo 'waiting for rustfs...'; sleep 1;
60+
done;
61+
rc mb -p rustfs/polycentric-blobs"
6862
6963
volumes:
7064
postgres-data:
65+
rustfs-data:

services/server/.env.example

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,25 @@
44
# Postgres connection string used by sea-orm.
55
DATABASE_URL=postgres://postgres:testing@localhost:5432
66

7-
# Local directory where uploaded blob bodies are stored.
8-
# Keyed by `{digest_type}_{hex(digest_value)}`.
9-
BLOBS_DIR=./data/blobs
7+
# S3 bucket where uploaded blob bodies are stored. Required.
8+
# Objects are keyed by `{digest_type}_{hex(digest_value)}`.
9+
CONTENT_BLOB_OS_BUCKET=polycentric-blobs
10+
11+
# Optional. Region the bucket lives in. Defaults to `us-east-1`.
12+
# CONTENT_BLOB_OS_REGION=us-east-1
13+
14+
# Optional. Custom endpoint URL for S3-compatible stores
15+
# Leave unset for AWS S3.
16+
# CONTENT_BLOB_OS_ENDPOINT=http://localhost:9000
17+
18+
# Optional. Set to `true` for path-style addressing
19+
# Required by RustFS.
20+
# CONTENT_BLOB_OS_FORCE_PATH_STYLE=true
21+
22+
# Optional. Static credentials for the blob store. If unset, the AWS
23+
# SDK's default chain is used (shared config, container/EC2 metadata).
24+
# CONTENT_BLOB_OS_ACCESS_KEY=...
25+
# CONTENT_BLOB_OS_SECRET_KEY=...
1026

1127
# Public URL clients should use when fetching blob bodies.
1228
# Reported by ServerService.GetInfo → ServerInfo.cdn_url.

services/server/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ edition = "2024"
77
# Workspace
88
entity = { path = "./entity" }
99
# 3rd Party
10+
aws-config = { version = "1", features = ["behavior-version-latest"] }
11+
aws-sdk-s3 = "1"
1012
axum = { version = "0.8.8", features = ["http2"] }
1113
ed25519-dalek = "2.2.0"
1214
log = "0.4"
1315
polycentric-common = { path = "../../packages/rs-common" }
1416
prost = "0.14"
1517
prost-types = "0.14"
16-
sea-orm = { version = "2.0.0-rc", features = [ "sqlx-postgres", "runtime-tokio", "macros" ] }
18+
sea-orm = { version = "2.0.0-rc", features = [ "sqlx-postgres", "runtime-tokio", "macros", "mock" ] }
1719
sha2 = "0.10"
1820
time = "0.3"
1921
tokio = { version = "1.50.0", features = ["full"] }

services/server/Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
FROM rust:1.95-slim AS build
22

33
WORKDIR /app
4+
5+
RUN apt-get update && apt-get install -y protobuf-compiler
6+
47
COPY Cargo.toml Cargo.toml
58
COPY Cargo.lock Cargo.lock
69
COPY protos protos
710
COPY packages packages
811
COPY services services
912

10-
RUN apt-get update && apt-get install -y protobuf-compiler
11-
1213
RUN cargo build --release --package server
1314

1415
FROM rust:1.95-slim

services/server/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Prerequisites
44

55
- Rust (edition 2024)
6-
- Docker (for PostgreSQL)
6+
- Docker (for PostgreSQL and RustFS (Object storage))
77

88
## Getting Started
99

services/server/src/main.rs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ mod util;
77
use crate::db::client::build_db_client;
88
use crate::grpc::server::build_grpc_router;
99
use crate::routes::build_routes;
10-
use crate::service::content::content_filestore::ContentFilestore;
10+
use crate::service::content::content_filestore::{
11+
ContentFilestore, ContentFilestoreConfig,
12+
};
1113
use crate::service::server::server_service::ServerConfig;
1214
use sea_orm::DatabaseConnection;
13-
use std::path::PathBuf;
1415

1516
/// Connect to the database, retrying with backoff.
1617
async fn connect_db_with_retry() -> DatabaseConnection {
@@ -29,14 +30,6 @@ async fn connect_db_with_retry() -> DatabaseConnection {
2930
}
3031
}
3132

32-
/// Resolve the directory where uploaded blobs are persisted. Override
33-
/// with the `BLOBS_DIR` env var; defaults to `./data/blobs`.
34-
fn blobs_dir() -> PathBuf {
35-
std::env::var("BLOBS_DIR")
36-
.map(PathBuf::from)
37-
.unwrap_or_else(|_| PathBuf::from("./data/blobs"))
38-
}
39-
4033
/// Build the server config served by `ServerService.GetInfo`. Version
4134
/// comes from the crate's `Cargo.toml`; `CDN_URL` overrides the public
4235
/// URL clients use to fetch blob bodies.
@@ -53,7 +46,9 @@ async fn main() {
5346
util::dotenv::load(".env");
5447

5548
let db = connect_db_with_retry().await;
56-
let filestore = ContentFilestore::new(blobs_dir());
49+
let filestore_cfg = ContentFilestoreConfig::from_env()
50+
.expect("blob store configuration error");
51+
let filestore = ContentFilestore::new(filestore_cfg).await;
5752
let server_cfg = server_config();
5853

5954
let grpc_router =
Lines changed: 128 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,150 @@
1+
use crate::service::content::content_helpers::blob_key;
12
use crate::service::proto::ContentDigest;
2-
use crate::util;
3+
use aws_config::{BehaviorVersion, Region};
4+
use aws_sdk_s3::Client;
5+
use aws_sdk_s3::config::Credentials;
6+
use aws_sdk_s3::error::SdkError;
7+
use aws_sdk_s3::primitives::ByteStream;
8+
use std::error::Error;
9+
use std::fmt::Write;
310
use std::io;
4-
use std::path::PathBuf;
5-
use tokio::fs;
6-
use tokio::io::AsyncWriteExt;
7-
8-
/// Local filesystem-backed blob store. Each body is written to a file
9-
/// keyed by `{digest_type}_{hex(digest_value)}` under `root`.
10-
///
11-
/// This is the placeholder object-storage backend; swapping to S3 /
12-
/// GCS is a matter of replacing this type.
11+
12+
fn err_chain<E: Error>(e: &E) -> String {
13+
let mut out = e.to_string();
14+
let mut src = e.source();
15+
while let Some(cur) = src {
16+
let _ = write!(out, ": {cur}");
17+
src = cur.source();
18+
}
19+
out
20+
}
21+
22+
fn sdk_err<E: Error>(e: E) -> io::Error {
23+
io::Error::other(err_chain(&e))
24+
}
25+
26+
/// S3-backed blob store config. When `access_key`/`secret_key` are
27+
/// unset, the AWS SDK's default credential chain is used (shared
28+
/// config, container/EC2 metadata, etc.).
29+
#[derive(Debug, Clone)]
30+
pub struct ContentFilestoreConfig {
31+
pub bucket: String,
32+
pub region: String,
33+
pub endpoint: Option<String>,
34+
/// RustFS and other S3-compatible stores require path-style
35+
pub force_path_style: bool,
36+
pub access_key: Option<String>,
37+
pub secret_key: Option<String>,
38+
}
39+
40+
impl ContentFilestoreConfig {
41+
pub fn from_env() -> Result<Self, String> {
42+
let bucket = std::env::var("CONTENT_BLOB_OS_BUCKET")
43+
.map_err(|_| "CONTENT_BLOB_OS_BUCKET is required".to_string())?;
44+
let region = std::env::var("CONTENT_BLOB_OS_REGION")
45+
.unwrap_or_else(|_| "us-east-1".to_string());
46+
let endpoint = std::env::var("CONTENT_BLOB_OS_ENDPOINT").ok();
47+
let force_path_style =
48+
std::env::var("CONTENT_BLOB_OS_FORCE_PATH_STYLE")
49+
.map(|v| matches!(v.as_str(), "true" | "1"))
50+
.unwrap_or(false);
51+
let access_key = std::env::var("CONTENT_BLOB_OS_ACCESS_KEY").ok();
52+
let secret_key = std::env::var("CONTENT_BLOB_OS_SECRET_KEY").ok();
53+
Ok(Self {
54+
bucket,
55+
region,
56+
endpoint,
57+
force_path_style,
58+
access_key,
59+
secret_key,
60+
})
61+
}
62+
}
63+
64+
/// S3-backed blob store. Each body is stored under the configured
65+
/// bucket keyed by `{digest_type}_{hex(digest_value)}`.
1366
#[derive(Debug, Clone)]
1467
pub struct ContentFilestore {
15-
root: PathBuf,
68+
client: Client,
69+
bucket: String,
1670
}
1771

1872
impl ContentFilestore {
19-
pub fn new(root: PathBuf) -> Self {
20-
Self { root }
73+
pub async fn new(cfg: ContentFilestoreConfig) -> Self {
74+
let mut loader = aws_config::defaults(BehaviorVersion::latest())
75+
.region(Region::new(cfg.region));
76+
if let Some(endpoint) = &cfg.endpoint {
77+
loader = loader.endpoint_url(endpoint);
78+
}
79+
if let (Some(access), Some(secret)) =
80+
(cfg.access_key.as_deref(), cfg.secret_key.as_deref())
81+
{
82+
loader = loader.credentials_provider(Credentials::new(
83+
access,
84+
secret,
85+
None,
86+
None,
87+
"content-blob-os",
88+
));
89+
}
90+
let aws = loader.load().await;
91+
92+
let s3_cfg = aws_sdk_s3::config::Builder::from(&aws)
93+
.force_path_style(cfg.force_path_style)
94+
.build();
95+
96+
Self {
97+
client: Client::from_conf(s3_cfg),
98+
bucket: cfg.bucket,
99+
}
21100
}
22101

23-
/// Write `body` to the store under the digest's key. Overwrites any
24-
/// existing file — re-uploading the same body is a no-op.
102+
/// Upload `body` under the digest's key. Re-uploading the same
103+
/// body overwrites the existing object.
25104
pub async fn write_blob(
26105
&self,
27106
digest: &ContentDigest,
28-
body: &[u8],
107+
body: Vec<u8>,
29108
) -> io::Result<()> {
30-
fs::create_dir_all(&self.root).await?;
31-
32-
let path = self.root.join(Self::blob_filename(digest));
33-
34-
let mut file = fs::OpenOptions::new()
35-
.create(true)
36-
.write(true)
37-
.truncate(true)
38-
.open(&path)
39-
.await?;
40-
file.write_all(body).await?;
41-
file.flush().await?;
109+
let key = blob_key(digest);
110+
self.client
111+
.put_object()
112+
.bucket(&self.bucket)
113+
.key(key)
114+
.body(ByteStream::from(body))
115+
.send()
116+
.await
117+
.map_err(sdk_err)?;
42118
Ok(())
43119
}
44120

45-
/// Read a blob body by its digest. Returns `NotFound` if no file
46-
/// exists for the given digest.
121+
/// Read a blob body by its digest. Returns `NotFound` when the
122+
/// object does not exist in the bucket.
47123
pub async fn read_blob(
48124
&self,
49125
digest: &ContentDigest,
50126
) -> io::Result<Vec<u8>> {
51-
let path = self.root.join(Self::blob_filename(digest));
52-
fs::read(&path).await
53-
}
54-
55-
fn blob_filename(digest: &ContentDigest) -> String {
56-
format!("{}_{}", digest.r#type, util::hex::encode(&digest.value))
127+
let key = blob_key(digest);
128+
let resp = self
129+
.client
130+
.get_object()
131+
.bucket(&self.bucket)
132+
.key(key)
133+
.send()
134+
.await
135+
.map_err(|e| match e {
136+
SdkError::ServiceError(svc) if svc.err().is_no_such_key() => {
137+
io::Error::new(io::ErrorKind::NotFound, "blob not found")
138+
}
139+
other => sdk_err(other),
140+
})?;
141+
let bytes = resp
142+
.body
143+
.collect()
144+
.await
145+
.map_err(sdk_err)?
146+
.into_bytes()
147+
.to_vec();
148+
Ok(bytes)
57149
}
58150
}

0 commit comments

Comments
 (0)