nats/object_store.rs
1// Copyright 2020-2022 The NATS Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14//! Support for Object Store.
15
16use crate::header::HeaderMap;
17use crate::jetstream::{
18 DateTime, DiscardPolicy, JetStream, PushSubscription, StorageType, StreamConfig,
19 SubscribeOptions,
20};
21use crate::Message;
22use base64::URL_SAFE;
23use lazy_static::lazy_static;
24use regex::Regex;
25use ring::digest::SHA256;
26use serde::{Deserialize, Serialize};
27use std::cmp;
28use std::io::{self, ErrorKind};
29use std::time::Duration;
30use time::serde::rfc3339;
31use time::OffsetDateTime;
32
33const DEFAULT_CHUNK_SIZE: usize = 128 * 1024;
34const NATS_ROLLUP: &str = "Nats-Rollup";
35const ROLLUP_SUBJECT: &str = "sub";
36
37lazy_static! {
38 static ref BUCKET_NAME_RE: Regex = Regex::new(r#"\A[a-zA-Z0-9_-]+\z"#).unwrap();
39 static ref OBJECT_NAME_RE: Regex = Regex::new(r#"\A[-/_=\.a-zA-Z0-9]+\z"#).unwrap();
40}
41
42fn is_valid_bucket_name(bucket_name: &str) -> bool {
43 BUCKET_NAME_RE.is_match(bucket_name)
44}
45
46fn is_valid_object_name(object_name: &str) -> bool {
47 if object_name.is_empty() || object_name.starts_with('.') || object_name.ends_with('.') {
48 return false;
49 }
50
51 OBJECT_NAME_RE.is_match(object_name)
52}
53
54fn encode_object_name(object_name: &str) -> String {
55 base64::encode_config(object_name, URL_SAFE)
56}
57
58/// Configuration values for object store buckets.
59#[derive(Debug, Default, Clone, Serialize, Deserialize)]
60pub struct Config {
61 /// Name of the storage bucket.
62 pub bucket: String,
63 /// A short description of the purpose of this storage bucket.
64 pub description: Option<String>,
65 /// Maximum age of any value in the bucket, expressed in nanoseconds
66 pub max_age: Duration,
67 /// The type of storage backend, `File` (default) and `Memory`
68 pub storage: StorageType,
69 /// How many replicas to keep for each value in a cluster, maximum 5.
70 pub num_replicas: usize,
71}
72
73impl JetStream {
74 /// Creates a new object store bucket.
75 ///
76 /// # Example
77 ///
78 /// ```no_run
79 /// # use nats::object_store::Config;
80 /// # fn main() -> std::io::Result<()> {
81 /// # let client = nats::connect("demo.nats.io")?;
82 /// # let context = nats::jetstream::new(client);
83 /// #
84 /// let bucket = context.create_object_store(&Config {
85 /// bucket: "create_object_store".to_string(),
86 /// ..Default::default()
87 /// })?;
88 ///
89 /// # context.delete_object_store("create_object_store")?;
90 /// # Ok(())
91 /// # }
92 /// ```
93 pub fn create_object_store(&self, config: &Config) -> io::Result<ObjectStore> {
94 if !self.connection.is_server_compatible_version(2, 6, 2) {
95 return Err(io::Error::new(
96 io::ErrorKind::Other,
97 "object-store requires at least server version 2.6.2",
98 ));
99 }
100
101 if !is_valid_bucket_name(&config.bucket) {
102 return Err(io::Error::new(
103 io::ErrorKind::InvalidInput,
104 "invalid bucket name",
105 ));
106 }
107
108 let bucket_name = config.bucket.clone();
109 let stream_name = format!("OBJ_{bucket_name}");
110 let chunk_subject = format!("$O.{bucket_name}.C.>");
111 let meta_subject = format!("$O.{bucket_name}.M.>");
112
113 self.add_stream(&StreamConfig {
114 name: stream_name,
115 description: config.description.clone(),
116 subjects: vec![chunk_subject, meta_subject],
117 max_age: config.max_age,
118 storage: config.storage,
119 num_replicas: config.num_replicas,
120 discard: DiscardPolicy::New,
121 allow_rollup: true,
122 ..Default::default()
123 })?;
124
125 Ok(ObjectStore::new(bucket_name, self.clone()))
126 }
127
128 /// Bind to an existing object store bucket.
129 ///
130 /// # Example
131 ///
132 /// ```no_run
133 /// # use nats::object_store::Config;
134 /// # fn main() -> std::io::Result<()> {
135 /// # let client = nats::connect("demo.nats.io")?;
136 /// # let context = nats::jetstream::new(client);
137 /// #
138 /// context.create_object_store(&Config {
139 /// bucket: "object_store".to_string(),
140 /// ..Default::default()
141 /// })?;
142 ///
143 /// let bucket = context.object_store("object_store")?;
144 ///
145 /// # context.delete_object_store("object_store")?;
146 /// # Ok(())
147 /// # }
148 /// ```
149 pub fn object_store(&self, bucket_name: &str) -> io::Result<ObjectStore> {
150 if !self.connection.is_server_compatible_version(2, 6, 2) {
151 return Err(io::Error::new(
152 io::ErrorKind::Other,
153 "object-store requires at least server version 2.6.2",
154 ));
155 }
156
157 if !is_valid_bucket_name(bucket_name) {
158 return Err(io::Error::new(
159 io::ErrorKind::InvalidInput,
160 "invalid bucket name",
161 ));
162 }
163
164 let stream_name = format!("OBJ_{bucket_name}");
165 self.stream_info(stream_name)?;
166
167 Ok(ObjectStore::new(bucket_name.to_string(), self.clone()))
168 }
169
170 /// Delete the underlying stream for the named object.
171 ///
172 /// # Example
173 ///
174 /// ```no_run
175 /// use nats::object_store::Config;
176 /// # fn main() -> std::io::Result<()> {
177 /// # let client = nats::connect("demo.nats.io")?;
178 /// # let context = nats::jetstream::new(client);
179 /// #
180 /// # let bucket = context.create_object_store(&Config {
181 /// # bucket: "delete_object_store".to_string(),
182 /// # ..Default::default()
183 /// # })?;
184 ///
185 /// context.delete_object_store("delete_object_store")?;
186 ///
187 /// # Ok(())
188 /// # }
189 /// ```
190 pub fn delete_object_store(&self, bucket_name: &str) -> io::Result<()> {
191 let stream_name = format!("OBJ_{bucket_name}");
192 self.delete_stream(stream_name)?;
193
194 Ok(())
195 }
196}
197
198/// Meta and instance information about an object.
199#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
200pub struct ObjectInfo {
201 /// Name of the object
202 pub name: String,
203 /// A short human readable description of the object.
204 pub description: Option<String>,
205 /// Link this object points to, if any.
206 pub link: Option<ObjectLink>,
207 /// Name of the bucket the object is stored in.
208 pub bucket: String,
209 /// Unique identifier used to uniquely identify this version of the object.
210 pub nuid: String,
211 /// Size in bytes of the object.
212 pub size: usize,
213 /// Number of chunks the object is stored in.
214 pub chunks: usize,
215 /// Date and time the object was last modified.
216 #[serde(with = "rfc3339", rename = "mtime")]
217 pub modified: DateTime,
218 /// Digest of the object stream.
219 pub digest: String,
220 /// Set to true if the object has been deleted.
221 #[serde(default)]
222 pub deleted: bool,
223}
224
225/// Meta information about an object.
226#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
227pub struct ObjectMeta {
228 /// Name of the object
229 pub name: String,
230 /// A short human readable description of the object.
231 pub description: Option<String>,
232 /// Link this object points to, if any.
233 pub link: Option<ObjectLink>,
234}
235
236impl From<&str> for ObjectMeta {
237 fn from(s: &str) -> ObjectMeta {
238 ObjectMeta {
239 name: s.to_string(),
240 ..Default::default()
241 }
242 }
243}
244
245/// A link to another object, potentially in another bucket.
246#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
247pub struct ObjectLink {
248 /// Name of the object
249 pub name: String,
250 /// Name of the bucket the object is stored in.
251 pub bucket: Option<String>,
252}
253
254/// A blob store capable of storing large objects efficiently in streams.
255pub struct ObjectStore {
256 name: String,
257 context: JetStream,
258}
259
260/// Represents an object stored in a bucket.
261pub struct Object {
262 /// Information about given object.
263 pub info: ObjectInfo,
264 subscription: PushSubscription,
265 remaining_bytes: Vec<u8>,
266 has_pending_messages: bool,
267 digest: Option<ring::digest::Context>,
268}
269
270impl Object {
271 pub(crate) fn new(subscription: PushSubscription, info: ObjectInfo) -> Self {
272 Object {
273 subscription,
274 info,
275 remaining_bytes: Vec::new(),
276 has_pending_messages: true,
277 digest: Some(ring::digest::Context::new(&SHA256)),
278 }
279 }
280
281 /// Returns information about the object.
282 pub fn info(&self) -> &ObjectInfo {
283 &self.info
284 }
285}
286
287impl io::Read for Object {
288 /// Read the data chunks for a given Object from attached subscription and copy it to provided buffer.
289 fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
290 // read data accumulated in remaining bytes into the buffer.
291 if !self.remaining_bytes.is_empty() {
292 let len = cmp::min(buffer.len(), self.remaining_bytes.len());
293 buffer[..len].copy_from_slice(&self.remaining_bytes[..len]);
294 self.remaining_bytes = self.remaining_bytes[len..].to_vec();
295 return Ok(len);
296 }
297
298 // fetch messages from subject.
299 // Run at each `read` call until there are no more pending messages for a given Object.
300 if self.has_pending_messages {
301 let maybe_message = self.subscription.next();
302 if let Some(message) = maybe_message {
303 let len = cmp::min(buffer.len(), message.data.len());
304 buffer[..len].copy_from_slice(&message.data[..len]);
305 if let Some(context) = &mut self.digest {
306 context.update(&message.data);
307 }
308 self.remaining_bytes.extend_from_slice(&message.data[len..]);
309
310 if let Some(message_info) = message.jetstream_message_info() {
311 if message_info.pending == 0 {
312 let digest = self.digest.take().map(|context| context.finish());
313 if let Some(digest) = digest {
314 if format!("SHA-256={}", base64::encode_config(digest, URL_SAFE))
315 != self.info.digest
316 {
317 return Err(io::Error::new(ErrorKind::InvalidData, "wrong digest"));
318 }
319 } else {
320 return Err(io::Error::new(
321 ErrorKind::InvalidData,
322 "digest should be Some",
323 ));
324 }
325 self.has_pending_messages = false;
326 }
327 }
328 return Ok(len);
329 }
330 }
331
332 Ok(0)
333 }
334}
335
336impl ObjectStore {
337 /// Instantiates a new object store
338 pub(crate) fn new(name: String, context: JetStream) -> Self {
339 ObjectStore { name, context }
340 }
341
342 /// Retrieve the current information for the object.
343 ///
344 /// # Examples
345 ///
346 /// ```no_run
347 /// # use nats::object_store::Config;
348 /// # fn main() -> std::io::Result<()> {
349 /// # let client = nats::connect("demo.nats.io")?;
350 /// # let context = nats::jetstream::new(client);
351 /// #
352 /// let bucket = context.create_object_store(&Config {
353 /// bucket: "info".to_string(),
354 /// ..Default::default()
355 /// })?;
356 ///
357 /// let bytes = vec![0];
358 /// let info = bucket.put("foo", &mut bytes.as_slice())?;
359 /// assert_eq!(info.name, "foo");
360 /// assert_eq!(info.size, bytes.len());
361 ///
362 /// # context.delete_object_store("info")?;
363 /// # Ok(())
364 /// # }
365 /// ```
366 pub fn info(&self, object_name: &str) -> io::Result<ObjectInfo> {
367 // LoOkup the stream to get the bound subject.
368 let object_name = encode_object_name(object_name);
369 if !is_valid_object_name(&object_name) {
370 return Err(io::Error::new(
371 io::ErrorKind::InvalidInput,
372 "invalid object name",
373 ));
374 }
375
376 // Grab last meta value we have.
377 let stream_name = format!("OBJ_{}", &self.name);
378 let subject = format!("$O.{}.M.{}", &self.name, &object_name);
379
380 let message = self.context.get_last_message(stream_name, &subject)?;
381 let object_info = serde_json::from_slice::<ObjectInfo>(&message.data)?;
382
383 Ok(object_info)
384 }
385
386 /// Seals the object store from further modifications.
387 pub fn seal(&self) -> io::Result<()> {
388 let stream_name = format!("OBJ_{}", self.name);
389 let stream_info = self.context.stream_info(stream_name)?;
390
391 let mut stream_config = stream_info.config;
392 stream_config.sealed = true;
393
394 self.context.update_stream(&stream_config)?;
395
396 Ok(())
397 }
398
399 /// Put will place the contents from the given reader into this object-store.
400 ///
401 /// # Example
402 ///
403 /// ```no_run
404 /// # use nats::object_store::Config;
405 /// # fn main() -> std::io::Result<()> {
406 /// # let client = nats::connect("demo.nats.io")?;
407 /// # let context = nats::jetstream::new(client);
408 /// #
409 /// let bucket = context.create_object_store(&Config {
410 /// bucket: "put".to_string(),
411 /// ..Default::default()
412 /// })?;
413 ///
414 /// let bytes = vec![0, 1, 2, 3, 4];
415 /// let info = bucket.put("foo", &mut bytes.as_slice())?;
416 /// assert_eq!(bucket.info("foo").unwrap(), info);
417 ///
418 /// # context.delete_object_store("put")?;
419 /// # Ok(())
420 /// # }
421 /// ```
422 pub fn put<T>(&self, meta: T, data: &mut impl io::Read) -> io::Result<ObjectInfo>
423 where
424 ObjectMeta: From<T>,
425 {
426 let object_meta: ObjectMeta = meta.into();
427 let object_name = encode_object_name(&object_meta.name);
428 if !is_valid_object_name(&object_name) {
429 return Err(io::Error::new(
430 io::ErrorKind::InvalidInput,
431 "invalid object name",
432 ));
433 }
434
435 // Fetch any existing object info, if there is any for later use.
436 let maybe_existing_object_info = match self.info(&object_name) {
437 Ok(object_info) => Some(object_info),
438 Err(_) => None,
439 };
440
441 let object_nuid = nuid::next();
442 let chunk_subject = format!("$O.{}.C.{}", &self.name, &object_nuid);
443
444 let mut object_chunks = 0;
445 let mut object_size = 0;
446
447 let mut context = ring::digest::Context::new(&SHA256);
448 let mut buffer = [0; DEFAULT_CHUNK_SIZE];
449
450 loop {
451 let n = data.read(&mut buffer)?;
452 if n == 0 {
453 break;
454 }
455 context.update(&buffer[..n]);
456
457 object_size += n;
458 object_chunks += 1;
459
460 self.context.publish(&chunk_subject, &buffer[..n])?;
461 }
462
463 let digest = context.finish();
464 // Create a random subject prefixed with the object stream name.
465 let subject = format!("$O.{}.M.{}", &self.name, &object_name);
466 let object_info = ObjectInfo {
467 name: object_meta.name,
468 description: object_meta.description,
469 link: object_meta.link,
470 bucket: self.name.clone(),
471 nuid: object_nuid.to_string(),
472 chunks: object_chunks,
473 size: object_size,
474 digest: format!("SHA-256={}", base64::encode_config(digest, URL_SAFE)),
475 modified: OffsetDateTime::now_utc(),
476 deleted: false,
477 };
478
479 let data = serde_json::to_vec(&object_info)?;
480 let mut headers = HeaderMap::default();
481 headers.insert(NATS_ROLLUP, ROLLUP_SUBJECT.to_string());
482
483 let message = Message::new(&subject, None, data, Some(headers));
484
485 // Publish metadata
486 self.context.publish_message(&message)?;
487
488 // Purge any old chunks.
489 if let Some(existing_object_info) = maybe_existing_object_info {
490 let stream_name = format!("OBJ_{}", self.name);
491 let chunk_subject = format!("$O.{}.C.{}", &self.name, &existing_object_info.nuid);
492
493 self.context
494 .purge_stream_subject(stream_name, &chunk_subject)?;
495 }
496
497 Ok(object_info)
498 }
499
500 /// Get an existing object by name.
501 ///
502 /// # Example
503 ///
504 /// ```no_run
505 /// use std::io::Read;
506 /// # use nats::object_store::Config;
507 /// # fn main() -> std::io::Result<()> {
508 /// # let client = nats::connect("demo.nats.io")?;
509 /// # let context = nats::jetstream::new(client);
510 /// #
511 /// let bucket = context.create_object_store(&Config {
512 /// bucket: "get".to_string(),
513 /// ..Default::default()
514 /// })?;
515 ///
516 /// let bytes = vec![0, 1, 2, 3, 4];
517 /// let info = bucket.put("foo", &mut bytes.as_slice())?;
518 ///
519 /// let mut result = Vec::new();
520 /// bucket.get("foo").unwrap().read_to_end(&mut result)?;
521 ///
522 /// # context.delete_object_store("get")?;
523 /// # Ok(())
524 /// # }
525 /// ```
526 pub fn get(&self, object_name: &str) -> io::Result<Object> {
527 let object_info = self.info(object_name)?;
528 if let Some(link) = object_info.link {
529 return self.get(&link.name);
530 }
531
532 let chunk_subject = format!("$O.{}.C.{}", self.name, object_info.nuid);
533 let subscription = self
534 .context
535 .subscribe_with_options(&chunk_subject, &SubscribeOptions::ordered())?;
536
537 Ok(Object::new(subscription, object_info))
538 }
539
540 /// Places a delete marker and purges the data stream associated with the key.
541 ///
542 /// # Example
543 ///
544 /// ```no_run
545 /// use std::io::Read;
546 /// # use nats::object_store::Config;
547 /// # fn main() -> std::io::Result<()> {
548 /// # let client = nats::connect("demo.nats.io")?;
549 /// # let context = nats::jetstream::new(client);
550 /// #
551 /// let bucket = context.create_object_store(&Config {
552 /// bucket: "delete".to_string(),
553 /// ..Default::default()
554 /// })?;
555 ///
556 /// let bytes = vec![0, 1, 2, 3, 4];
557 /// bucket.put("foo", &mut bytes.as_slice())?;
558 ///
559 /// bucket.delete("foo")?;
560 ///
561 /// let info = bucket.info("foo")?;
562 /// assert!(info.deleted);
563 /// assert_eq!(info.size, 0);
564 /// assert_eq!(info.chunks, 0);
565 ///
566 /// # context.delete_object_store("delete")?;
567 /// # Ok(())
568 /// # }
569 /// ```
570 pub fn delete(&self, object_name: &str) -> io::Result<()> {
571 let mut object_info = self.info(object_name)?;
572 object_info.chunks = 0;
573 object_info.size = 0;
574 object_info.deleted = true;
575
576 let data = serde_json::to_vec(&object_info)?;
577
578 let mut headers = HeaderMap::default();
579 headers.insert(NATS_ROLLUP, ROLLUP_SUBJECT.to_string());
580
581 let subject = format!("$O.{}.M.{}", &self.name, &encode_object_name(object_name));
582 let message = Message::new(&subject, None, data, Some(headers));
583
584 self.context.publish_message(&message)?;
585
586 let stream_name = format!("OBJ_{}", self.name);
587 let chunk_subject = format!("$O.{}.C.{}", self.name, object_info.nuid);
588
589 self.context
590 .purge_stream_subject(stream_name, &chunk_subject)?;
591
592 Ok(())
593 }
594
595 /// Watch for changes in the underlying store and receive meta information updates.
596 ///
597 /// # Example
598 ///
599 /// ```no_run
600 /// use std::io::Read;
601 /// # use nats::object_store::Config;
602 /// # fn main() -> std::io::Result<()> {
603 /// # let client = nats::connect("demo.nats.io")?;
604 /// # let context = nats::jetstream::new(client);
605 /// #
606 /// let bucket = context.create_object_store(&Config {
607 /// bucket: "watch".to_string(),
608 /// ..Default::default()
609 /// })?;
610 ///
611 /// let mut watch = bucket.watch()?;
612 ///
613 /// let bytes = vec![0, 1, 2, 3, 4];
614 /// bucket.put("foo", &mut bytes.as_slice())?;
615 ///
616 /// let info = watch.next().unwrap();
617 /// assert_eq!(info.name, "foo");
618 /// assert_eq!(info.size, bytes.len());
619 ///
620 /// let bytes = vec![0];
621 /// bucket.put("bar", &mut bytes.as_slice())?;
622 ///
623 /// let info = watch.next().unwrap();
624 /// assert_eq!(info.name, "bar");
625 /// assert_eq!(info.size, bytes.len());
626 ///
627 /// # context.delete_object_store("watch")?;
628 /// # Ok(())
629 /// # }
630 /// ```
631 pub fn watch(&self) -> io::Result<Watch> {
632 let subject = format!("$O.{}.M.>", &self.name);
633 let subscription = self.context.subscribe_with_options(
634 &subject,
635 &SubscribeOptions::ordered().deliver_last_per_subject(),
636 )?;
637
638 Ok(Watch { subscription })
639 }
640}
641
642/// Iterator returned by `watch`
643pub struct Watch {
644 subscription: PushSubscription,
645}
646
647impl Iterator for Watch {
648 type Item = ObjectInfo;
649
650 fn next(&mut self) -> Option<Self::Item> {
651 match self.subscription.next() {
652 Some(message) => Some(serde_json::from_slice(&message.data).unwrap()),
653 None => None,
654 }
655 }
656}