1use std::path::PathBuf;
19use std::sync::Arc;
20
21use http::Uri;
22use opendal_core::raw::*;
23use opendal_core::*;
24use redis::Client;
25use redis::ConnectionAddr;
26use redis::ConnectionInfo;
27use redis::IntoConnectionInfo;
28use redis::ProtocolVersion;
29use redis::RedisConnectionInfo;
30use redis::cluster::ClusterClientBuilder;
31
32use super::REDIS_SCHEME;
33use super::config::RedisConfig;
34use super::core::*;
35use super::deleter::RedisDeleter;
36use super::reader::*;
37use super::writer::RedisWriter;
38
39const DEFAULT_REDIS_ENDPOINT: &str = "tcp://127.0.0.1:6379";
40const DEFAULT_REDIS_PORT: u16 = 6379;
41
42#[doc = include_str!("docs.md")]
44#[derive(Debug, Default)]
45pub struct RedisBuilder {
46 pub(super) config: RedisConfig,
47 pub(super) default_ttl: Option<Duration>,
48}
49
50impl RedisBuilder {
51 pub fn endpoint(mut self, endpoint: &str) -> Self {
59 if !endpoint.is_empty() {
60 self.config.endpoint = Some(endpoint.to_owned());
61 }
62 self
63 }
64
65 pub fn cluster_endpoints(mut self, cluster_endpoints: &str) -> Self {
74 if !cluster_endpoints.is_empty() {
75 self.config.cluster_endpoints = Some(cluster_endpoints.to_owned());
76 }
77 self
78 }
79
80 pub fn username(mut self, username: &str) -> Self {
84 if !username.is_empty() {
85 self.config.username = Some(username.to_owned());
86 }
87 self
88 }
89
90 pub fn password(mut self, password: &str) -> Self {
94 if !password.is_empty() {
95 self.config.password = Some(password.to_owned());
96 }
97 self
98 }
99
100 pub fn db(mut self, db: i64) -> Self {
104 self.config.db = db;
105 self
106 }
107
108 pub fn default_ttl(mut self, ttl: Duration) -> Self {
112 self.default_ttl = Some(ttl);
113 self
114 }
115
116 pub fn root(mut self, root: &str) -> Self {
120 self.config.root = if root.is_empty() {
121 None
122 } else {
123 Some(root.to_string())
124 };
125
126 self
127 }
128
129 #[must_use]
137 pub fn connection_pool_max_size(mut self, max_size: usize) -> Self {
138 assert!(max_size > 0, "max_size must be greater than zero!");
139 self.config.connection_pool_max_size = Some(max_size);
140 self
141 }
142}
143
144impl Builder for RedisBuilder {
145 type Config = RedisConfig;
146
147 fn build(self) -> Result<impl Service> {
148 let default_ttl = match self.default_ttl {
149 Some(ttl) => Some(ttl),
150 None => self
151 .config
152 .default_ttl
153 .map(signed_duration_to_duration)
154 .transpose()?,
155 };
156 let root = normalize_root(
157 self.config
158 .root
159 .clone()
160 .unwrap_or_else(|| "/".to_string())
161 .as_str(),
162 );
163
164 if let Some(endpoints) = self.config.cluster_endpoints.clone() {
165 let mut cluster_endpoints: Vec<ConnectionInfo> = Vec::default();
166 for endpoint in endpoints.split(',') {
167 cluster_endpoints.push(self.get_connection_info(endpoint.to_string())?);
168 }
169 let mut client_builder = ClusterClientBuilder::new(cluster_endpoints);
170 if let Some(username) = &self.config.username {
171 client_builder = client_builder.username(username.clone());
172 }
173 if let Some(password) = &self.config.password {
174 client_builder = client_builder.password(password.clone());
175 }
176 let client = client_builder.build().map_err(format_redis_error)?;
177
178 Ok(RedisBackend::new(RedisCore::new(
179 endpoints,
180 None,
181 Some(client),
182 default_ttl,
183 self.config.connection_pool_max_size,
184 ))
185 .with_normalized_root(root))
186 } else {
187 let endpoint = self
188 .config
189 .endpoint
190 .clone()
191 .unwrap_or_else(|| DEFAULT_REDIS_ENDPOINT.to_string());
192
193 let client =
194 Client::open(self.get_connection_info(endpoint.clone())?).map_err(|e| {
195 Error::new(ErrorKind::ConfigInvalid, "invalid or unsupported scheme")
196 .with_context("service", REDIS_SCHEME)
197 .with_context("endpoint", self.config.endpoint.as_ref().unwrap())
198 .with_context("db", self.config.db.to_string())
199 .set_source(e)
200 })?;
201
202 Ok(RedisBackend::new(RedisCore::new(
203 endpoint,
204 Some(client),
205 None,
206 default_ttl,
207 self.config.connection_pool_max_size,
208 ))
209 .with_normalized_root(root))
210 }
211 }
212}
213
214impl RedisBuilder {
215 fn get_connection_info(&self, endpoint: String) -> Result<ConnectionInfo> {
216 let ep_url = endpoint.parse::<Uri>().map_err(|e| {
217 Error::new(ErrorKind::ConfigInvalid, "endpoint is invalid")
218 .with_context("service", REDIS_SCHEME)
219 .with_context("endpoint", endpoint)
220 .set_source(e)
221 })?;
222
223 let con_addr = match ep_url.scheme_str() {
224 Some("tcp") | Some("redis") | None => {
225 let host = ep_url
226 .host()
227 .map(|h| h.to_string())
228 .unwrap_or_else(|| "127.0.0.1".to_string());
229 let port = ep_url.port_u16().unwrap_or(DEFAULT_REDIS_PORT);
230 ConnectionAddr::Tcp(host, port)
231 }
232 Some("rediss") => {
233 let host = ep_url
234 .host()
235 .map(|h| h.to_string())
236 .unwrap_or_else(|| "127.0.0.1".to_string());
237 let port = ep_url.port_u16().unwrap_or(DEFAULT_REDIS_PORT);
238 ConnectionAddr::TcpTls {
239 host,
240 port,
241 insecure: false,
242 tls_params: None,
243 }
244 }
245 Some("unix") | Some("redis+unix") => {
246 let path = PathBuf::from(ep_url.path());
247 ConnectionAddr::Unix(path)
248 }
249 Some(s) => {
250 return Err(
251 Error::new(ErrorKind::ConfigInvalid, "invalid or unsupported scheme")
252 .with_context("service", REDIS_SCHEME)
253 .with_context("scheme", s),
254 );
255 }
256 };
257
258 let mut redis_info = RedisConnectionInfo::default()
259 .set_db(self.config.db)
260 .set_protocol(ProtocolVersion::RESP2);
261 if let Some(username) = &self.config.username {
262 redis_info = redis_info.set_username(username);
263 }
264 if let Some(password) = &self.config.password {
265 redis_info = redis_info.set_password(password);
266 }
267 let connection_info = con_addr
268 .clone()
269 .into_connection_info()
270 .map_err(|err| {
271 Error::new(ErrorKind::ConfigInvalid, "invalid connection address")
272 .with_context("service", REDIS_SCHEME)
273 .with_context("address", con_addr)
274 .with_context("error", err)
275 })?
276 .set_redis_settings(redis_info);
277
278 Ok(connection_info)
279 }
280}
281
282#[derive(Debug, Clone)]
284pub struct RedisBackend {
285 pub(crate) core: Arc<RedisCore>,
286 pub(crate) root: String,
287 pub(crate) info: ServiceInfo,
288 pub(crate) capability: Capability,
289}
290
291impl RedisBackend {
292 fn new(core: RedisCore) -> Self {
293 let info = ServiceInfo::new(REDIS_SCHEME, "/", core.addr());
294 let capability = Capability {
295 read: true,
296 write: true,
297 delete: true,
298 stat: true,
299 write_can_empty: true,
300 shared: true,
301 ..Default::default()
302 };
303
304 Self {
305 core: Arc::new(core),
306 root: "/".to_string(),
307 info,
308 capability,
309 }
310 }
311
312 fn with_normalized_root(mut self, root: String) -> Self {
313 self.info = self.info.with_root(&root);
314 self.root = root;
315 self
316 }
317}
318
319impl Service for RedisBackend {
320 type Reader = oio::StreamReader<RedisReader>;
321 type Writer = RedisWriter;
322 type Lister = ();
323 type Deleter = oio::OneShotDeleter<RedisDeleter>;
324 type Copier = ();
325 type Composer = ();
326
327 fn info(&self) -> ServiceInfo {
328 self.info.clone()
329 }
330
331 fn capability(&self) -> Capability {
332 self.capability
333 }
334
335 async fn create_dir(
336 &self,
337 _ctx: &OperationContext,
338 _path: &str,
339 _args: OpCreateDir,
340 ) -> Result<RpCreateDir> {
341 Err(Error::new(
342 ErrorKind::Unsupported,
343 "operation is not supported",
344 ))
345 }
346
347 async fn stat(&self, _ctx: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
348 let p = build_abs_path(&self.root, path);
349
350 if p == build_abs_path(&self.root, "") {
351 Ok(RpStat::new(MetadataBuilder::dir().build()))
352 } else {
353 match self.core.len(&p).await? {
354 Some(len) => Ok(RpStat::new({
355 let metadata = MetadataBuilder::file(len as u64);
356 metadata.build()
357 })),
358 None => Err(Error::new(ErrorKind::NotFound, "key not found in redis")),
359 }
360 }
361 }
362
363 fn read(&self, _ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
364 let output: oio::StreamReader<RedisReader> = {
365 Ok(oio::StreamReader::new(RedisReader::new(
366 self.clone(),
367 path,
368 args,
369 )))
370 }?;
371
372 Ok(output)
373 }
374
375 fn write(&self, _ctx: &OperationContext, path: &str, _: OpWrite) -> Result<Self::Writer> {
376 let output: RedisWriter = {
377 let p = build_abs_path(&self.root, path);
378 Ok(RedisWriter::new(self.core.clone(), p))
379 }?;
380
381 Ok(output)
382 }
383
384 fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
385 let output: oio::OneShotDeleter<RedisDeleter> = {
386 Ok(oio::OneShotDeleter::new(RedisDeleter::new(
387 self.core.clone(),
388 self.root.clone(),
389 )))
390 }?;
391
392 Ok(output)
393 }
394
395 fn list(&self, _ctx: &OperationContext, _path: &str, _args: OpList) -> Result<Self::Lister> {
396 Err(Error::new(
397 ErrorKind::Unsupported,
398 "operation is not supported",
399 ))
400 }
401
402 fn copy(
403 &self,
404 _ctx: &OperationContext,
405 _from: &str,
406 _to: &str,
407 _args: OpCopy,
408 ) -> Result<Self::Copier> {
409 Err(Error::new(
410 ErrorKind::Unsupported,
411 "operation is not supported",
412 ))
413 }
414
415 async fn rename(
416 &self,
417 _ctx: &OperationContext,
418 _from: &str,
419 _to: &str,
420 _args: OpRename,
421 ) -> Result<RpRename> {
422 Err(Error::new(
423 ErrorKind::Unsupported,
424 "operation is not supported",
425 ))
426 }
427
428 async fn presign(
429 &self,
430 _ctx: &OperationContext,
431 _path: &str,
432 _args: OpPresign,
433 ) -> Result<RpPresign> {
434 Err(Error::new(
435 ErrorKind::Unsupported,
436 "operation is not supported",
437 ))
438 }
439}