Skip to main content

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}