rust-postgres/tokio-postgres/src/lib.rs

544 lines
20 KiB
Rust
Raw Normal View History

2019-01-06 05:39:08 +00:00
//! An asynchronous, pipelined, PostgreSQL client.
//!
//! # Example
//!
//! ```no_run
//! use futures::{Future, Stream};
//! use tokio_postgres::NoTls;
//!
//! # #[cfg(not(feature = "runtime"))]
//! # let fut = futures::future::ok(());
//! # #[cfg(feature = "runtime")]
//! let fut =
//! // Connect to the database
//! tokio_postgres::connect("host=localhost user=postgres", NoTls)
//!
//! .map(|(client, connection)| {
//! // The connection object performs the actual communication with the database,
//! // so spawn it off to run on its own.
//! let connection = connection.map_err(|e| eprintln!("connection error: {}", e));
//! tokio::spawn(connection);
//!
//! // The client is what you use to make requests.
//! client
//! })
//!
//! .and_then(|mut client| {
//! // Now we can prepare a simple statement that just returns its parameter.
//! client.prepare("SELECT $1::TEXT")
//! .map(|statement| (client, statement))
//! })
//!
//! .and_then(|(mut client, statement)| {
//! // And then execute it, returning a Stream of Rows which we collect into a Vec
//! client.query(&statement, &[&"hello world"]).collect()
//! })
//!
//! // Now we can check that we got back the same string we sent over.
//! .map(|rows| {
//! let value: &str = rows[0].get(0);
//! assert_eq!(value, "hello world");
//! })
//!
//! // And report any errors that happened.
//! .map_err(|e| {
//! eprintln!("error: {}", e);
//! });
//!
//! // By default, tokio_postgres uses the tokio crate as its runtime.
//! tokio::run(fut);
//! ```
//!
2019-01-18 04:45:51 +00:00
//! # Behavior
//!
//! Calling a method like `Client::query` on its own does nothing. The associated request is not sent to the database
//! until the future returned by the method is first polled. Requests are executed in the order that they are first
//! polled, not in the order that their futures are created.
//!
2019-01-06 05:39:08 +00:00
//! # Pipelining
//!
//! The client supports *pipelined* requests. Pipelining can improve performance in use cases in which multiple,
//! independent queries need to be executed. In a traditional workflow, each query is sent to the server after the
//! previous query completes. In contrast, pipelining allows the client to send all of the queries to the server up
2019-01-08 04:43:52 +00:00
//! front, minimizing time spent by one side waiting for the other to finish sending data:
2019-01-06 05:39:08 +00:00
//!
//! ```not_rust
//! Sequential Pipelined
//! | Client | Server | | Client | Server |
//! |----------------|-----------------| |----------------|-----------------|
//! | send query 1 | | | send query 1 | |
//! | | process query 1 | | send query 2 | process query 1 |
//! | receive rows 1 | | | send query 3 | process query 2 |
//! | send query 2 | | | receive rows 1 | process query 3 |
//! | | process query 2 | | receive rows 2 | |
//! | receive rows 2 | | | receive rows 3 | |
//! | send query 3 | |
//! | | process query 3 |
//! | receive rows 3 | |
//! ```
//!
//! In both cases, the PostgreSQL server is executing the queries sequentially - pipelining just allows both sides of
//! the connection to work concurrently when possible.
//!
//! Pipelining happens automatically when futures are polled concurrently (for example, by using the futures `join`
2019-01-15 06:08:13 +00:00
//! combinator):
//!
//! ```rust
//! use futures::Future;
//! use tokio_postgres::{Client, Error, Statement};
//!
2019-03-05 05:22:50 +00:00
//! fn pipelined_prepare(
//! client: &mut Client,
//! ) -> impl Future<Item = (Statement, Statement), Error = Error>
2019-01-15 06:08:13 +00:00
//! {
//! client.prepare("SELECT * FROM foo")
//! .join(client.prepare("INSERT INTO bar (id, name) VALUES ($1, $2)"))
//! }
//! ```
2019-01-06 05:39:08 +00:00
//!
//! # Runtime
//!
//! The client works with arbitrary `AsyncRead + AsyncWrite` streams. Convenience APIs are provided to handle the
//! connection process, but these are gated by the `runtime` Cargo feature, which is enabled by default. If disabled,
//! all dependence on the tokio runtime is removed.
2019-04-02 04:56:25 +00:00
//!
//! # SSL/TLS support
//!
//! TLS support is implemented via external libraries. `Client::connect` and `Config::connect` take a TLS implementation
//! as an argument. The `NoTls` type in this crate can be used when TLS is not required. Otherwise, the
//! `postgres-openssl` and `postgres-native-tls` crates provide implementations backed by the `openssl` and `native-tls`
//! crates, respectively.
2019-06-30 04:18:28 +00:00
#![doc(html_root_url = "https://docs.rs/tokio-postgres/0.4.0-rc.3")]
2019-03-05 05:22:50 +00:00
#![warn(rust_2018_idioms, clippy::all, missing_docs)]
2019-01-18 05:11:24 +00:00
use bytes::IntoBuf;
use futures::{Future, Poll, Stream};
2018-08-11 21:32:17 +00:00
use std::error::Error as StdError;
2018-06-19 02:34:25 +00:00
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio_io::{AsyncRead, AsyncWrite};
2016-12-20 23:42:28 +00:00
2019-03-05 05:51:19 +00:00
pub use crate::config::Config;
2019-03-05 05:26:10 +00:00
use crate::error::DbError;
pub use crate::error::Error;
2019-03-05 05:51:19 +00:00
pub use crate::row::{Row, SimpleQueryRow};
2018-12-17 05:30:52 +00:00
#[cfg(feature = "runtime")]
pub use crate::socket::Socket;
2018-12-09 01:40:37 +00:00
pub use crate::stmt::Column;
2019-03-05 05:26:10 +00:00
#[cfg(feature = "runtime")]
use crate::tls::MakeTlsConnect;
pub use crate::tls::NoTls;
use crate::tls::TlsConnect;
2018-12-11 04:56:22 +00:00
use crate::types::{ToSql, Type};
2016-12-20 23:42:28 +00:00
2019-03-05 05:51:19 +00:00
pub mod config;
pub mod error;
2019-01-18 05:11:24 +00:00
pub mod impls;
2018-06-17 04:29:27 +00:00
mod proto;
2019-03-05 05:51:19 +00:00
pub mod row;
2018-12-17 05:30:52 +00:00
#[cfg(feature = "runtime")]
mod socket;
2018-12-09 01:39:20 +00:00
mod stmt;
2019-03-05 05:26:10 +00:00
pub mod tls;
2018-12-09 01:39:20 +00:00
pub mod types;
2017-07-20 04:22:27 +00:00
fn next_statement() -> String {
2018-08-16 02:53:20 +00:00
static ID: AtomicUsize = AtomicUsize::new(0);
format!("s{}", ID.fetch_add(1, Ordering::SeqCst))
}
fn next_portal() -> String {
static ID: AtomicUsize = AtomicUsize::new(0);
format!("p{}", ID.fetch_add(1, Ordering::SeqCst))
}
2019-01-08 04:43:52 +00:00
/// A convenience function which parses a connection string and connects to the database.
///
/// See the documentation for [`Config`] for details on the connection string format.
///
/// Requires the `runtime` Cargo feature (enabled by default).
///
/// [`Config`]: ./Config.t.html
#[cfg(feature = "runtime")]
2019-01-18 05:11:24 +00:00
pub fn connect<T>(config: &str, tls: T) -> impls::Connect<T>
where
2019-01-13 22:53:19 +00:00
T: MakeTlsConnect<Socket>,
{
2019-01-18 05:11:24 +00:00
impls::Connect(proto::ConnectFuture::new(tls, config.parse()))
}
2019-01-08 04:43:52 +00:00
/// An asynchronous PostgreSQL client.
///
/// The client is one half of what is returned when a connection is established. Users interact with the database
/// through this client object.
2018-06-18 12:18:04 +00:00
pub struct Client(proto::Client);
2016-12-20 23:42:28 +00:00
2018-06-17 04:29:27 +00:00
impl Client {
2019-01-08 04:43:52 +00:00
/// Creates a new prepared statement.
///
/// Prepared statements can be executed repeatedly, and may contain query parameters (indicated by `$1`, `$2`, etc),
/// which are set when executed. Prepared statements can only be used with the connection that created them.
2019-01-18 05:11:24 +00:00
pub fn prepare(&mut self, query: &str) -> impls::Prepare {
2018-06-19 02:34:25 +00:00
self.prepare_typed(query, &[])
}
2019-01-08 04:43:52 +00:00
/// Like `prepare`, but allows the types of query parameters to be explicitly specified.
///
/// The list of types may be smaller than the number of parameters - the types of the remaining parameters will be
/// inferred. For example, `client.prepare_typed(query, &[])` is equivalent to `client.prepare(query)`.
2019-01-18 05:11:24 +00:00
pub fn prepare_typed(&mut self, query: &str, param_types: &[Type]) -> impls::Prepare {
impls::Prepare(self.0.prepare(next_statement(), query, param_types))
2018-06-19 02:34:25 +00:00
}
2018-06-20 02:10:07 +00:00
2019-01-08 04:43:52 +00:00
/// Executes a statement, returning the number of rows modified.
///
/// If the statement does not modify any rows (e.g. `SELECT`), 0 is returned.
///
/// # Panics
///
/// Panics if the number of parameters provided does not match the number expected.
2019-02-01 04:59:33 +00:00
pub fn execute(&mut self, statement: &Statement, params: &[&dyn ToSql]) -> impls::Execute {
self.execute_iter(statement, params.iter().cloned())
}
/// Like [`execute`], but takes an iterator of parameters rather than a slice.
///
/// [`execute`]: #method.execute
pub fn execute_iter<'a, I>(&mut self, statement: &Statement, params: I) -> impls::Execute
where
I: IntoIterator<Item = &'a dyn ToSql>,
I::IntoIter: ExactSizeIterator,
{
2019-02-01 04:59:33 +00:00
impls::Execute(self.0.execute(&statement.0, params))
2018-06-20 02:10:07 +00:00
}
2018-06-21 00:06:11 +00:00
2019-01-08 04:43:52 +00:00
/// Executes a statement, returning a stream of the resulting rows.
///
/// # Panics
///
/// Panics if the number of parameters provided does not match the number expected.
2019-01-18 05:11:24 +00:00
pub fn query(&mut self, statement: &Statement, params: &[&dyn ToSql]) -> impls::Query {
self.query_iter(statement, params.iter().cloned())
}
/// Like [`query`], but takes an iterator of parameters rather than a slice.
///
/// [`query`]: #method.query
pub fn query_iter<'a, I>(&mut self, statement: &Statement, params: I) -> impls::Query
where
I: IntoIterator<Item = &'a dyn ToSql>,
I::IntoIter: ExactSizeIterator,
{
2019-01-18 05:11:24 +00:00
impls::Query(self.0.query(&statement.0, params))
2018-06-21 00:06:11 +00:00
}
2018-07-08 05:42:04 +00:00
2019-01-08 04:43:52 +00:00
/// Binds a statement to a set of parameters, creating a `Portal` which can be incrementally queried.
///
/// Portals only last for the duration of the transaction in which they are created - in particular, a portal
/// created outside of a transaction is immediately destroyed. Portals can only be used on the connection that
/// created them.
///
2019-01-08 04:43:52 +00:00
/// # Panics
///
/// Panics if the number of parameters provided does not match the number expected.
2019-01-18 05:11:24 +00:00
pub fn bind(&mut self, statement: &Statement, params: &[&dyn ToSql]) -> impls::Bind {
self.bind_iter(statement, params.iter().cloned())
}
/// Like [`bind`], but takes an iterator of parameters rather than a slice.
///
/// [`bind`]: #method.bind
pub fn bind_iter<'a, I>(&mut self, statement: &Statement, params: I) -> impls::Bind
where
I: IntoIterator<Item = &'a dyn ToSql>,
I::IntoIter: ExactSizeIterator,
{
2019-01-18 05:11:24 +00:00
impls::Bind(self.0.bind(&statement.0, next_portal(), params))
2018-08-16 02:53:20 +00:00
}
2019-01-08 04:43:52 +00:00
/// Continues execution of a portal, returning a stream of the resulting rows.
///
/// Unlike `query`, portals can be incrementally evaluated by limiting the number of rows returned in each call to
/// query_portal. If the requested number is negative or 0, all rows will be returned.
2019-01-18 05:11:24 +00:00
pub fn query_portal(&mut self, portal: &Portal, max_rows: i32) -> impls::QueryPortal {
impls::QueryPortal(self.0.query_portal(&portal.0, max_rows))
2018-08-16 04:00:15 +00:00
}
2019-01-08 04:43:52 +00:00
/// Executes a `COPY FROM STDIN` statement, returning the number of rows created.
///
/// The data in the provided stream is passed along to the server verbatim; it is the caller's responsibility to
/// ensure it uses the proper format.
///
/// # Panics
///
/// Panics if the number of parameters provided does not match the number expected.
2018-12-09 01:40:37 +00:00
pub fn copy_in<S>(
&mut self,
statement: &Statement,
params: &[&dyn ToSql],
stream: S,
2019-01-18 05:11:24 +00:00
) -> impls::CopyIn<S>
2018-08-11 21:32:17 +00:00
where
2018-08-13 03:23:21 +00:00
S: Stream,
2018-12-03 05:26:10 +00:00
S::Item: IntoBuf,
<S::Item as IntoBuf>::Buf: 'static + Send,
// FIXME error type?
2018-12-09 01:40:37 +00:00
S::Error: Into<Box<dyn StdError + Sync + Send>>,
{
self.copy_in_iter(statement, params.iter().cloned(), stream)
}
/// Like [`copy_in`], except that it takes an iterator of parameters rather than a slice.
///
/// [`copy_in`]: #method.copy_in
pub fn copy_in_iter<'a, I, S>(
&mut self,
statement: &Statement,
params: I,
stream: S,
) -> impls::CopyIn<S>
where
I: IntoIterator<Item = &'a dyn ToSql>,
I::IntoIter: ExactSizeIterator,
S: Stream,
S::Item: IntoBuf,
<S::Item as IntoBuf>::Buf: 'static + Send,
// FIXME error type?
S::Error: Into<Box<dyn StdError + Sync + Send>>,
2018-08-11 21:32:17 +00:00
{
2019-01-18 05:11:24 +00:00
impls::CopyIn(self.0.copy_in(&statement.0, params, stream))
2018-08-11 21:32:17 +00:00
}
2019-01-08 04:43:52 +00:00
/// Executes a `COPY TO STDOUT` statement, returning a stream of the resulting data.
///
/// # Panics
///
/// Panics if the number of parameters provided does not match the number expected.
2019-01-18 05:11:24 +00:00
pub fn copy_out(&mut self, statement: &Statement, params: &[&dyn ToSql]) -> impls::CopyOut {
self.copy_out_iter(statement, params.iter().cloned())
}
/// Like [`copy_out`], except that it takes an iterator of parameters rather than a slice.
///
/// [`copy_out`]: #method.copy_out
pub fn copy_out_iter<'a, I>(&mut self, statement: &Statement, params: I) -> impls::CopyOut
where
I: IntoIterator<Item = &'a dyn ToSql>,
I::IntoIter: ExactSizeIterator,
{
2019-01-18 05:11:24 +00:00
impls::CopyOut(self.0.copy_out(&statement.0, params))
2018-07-16 02:40:15 +00:00
}
/// Executes a sequence of SQL statements using the simple query protocol.
2019-01-08 04:43:52 +00:00
///
/// Statements should be separated by semicolons. If an error occurs, execution of the sequence will stop at that
/// point. The simple query protocol returns the values in rows as strings rather than in their binary encodings,
/// so the associated row type doesn't work with the `FromSql` trait. Rather than simply returning a stream over the
/// rows, this method returns a stream over an enum which indicates either the completion of one of the commands,
/// or a row of data. This preserves the framing between the separate statements in the request.
2019-01-08 04:43:52 +00:00
///
/// # Warning
///
/// Prepared statements should be use for any query which contains user-specified data, as they provided the
/// functionality to safely imbed that data in the request. Do not form statements via string concatenation and pass
/// them to this method!
pub fn simple_query(&mut self, query: &str) -> impls::SimpleQuery {
impls::SimpleQuery(self.0.simple_query(query))
2018-07-08 05:42:04 +00:00
}
2018-12-22 05:08:26 +00:00
/// A utility method to wrap a future in a database transaction.
///
/// The returned future will start a transaction and then run the provided future. If the future returns `Ok`, it
/// will commit the transaction, and if it returns `Err`, it will roll the transaction back.
///
/// This is simply a convenience API; it's roughly equivalent to:
///
/// ```ignore
/// client.batch_execute("BEGIN")
/// .and_then(your_future)
/// .and_then(client.batch_execute("COMMIT"))
/// .or_else(|e| client.batch_execute("ROLLBACK").then(|_| Err(e)))
/// ```
///
/// # Warning
///
/// Unlike the other futures created by a client, this future is *not* atomic with respect to other requests. If you
/// attempt to execute it concurrently with other futures created by the same connection, they will interleave!
pub fn build_transaction(&mut self) -> TransactionBuilder {
2019-01-08 04:43:52 +00:00
TransactionBuilder(self.0.clone())
}
/// Attempts to cancel an in-progress query.
///
/// The server provides no information about whether a cancellation attempt was successful or not. An error will
/// only be returned if the client was unable to connect to the database.
///
/// Requires the `runtime` Cargo feature (enabled by default).
#[cfg(feature = "runtime")]
2019-01-18 05:11:24 +00:00
pub fn cancel_query<T>(&mut self, make_tls_mode: T) -> impls::CancelQuery<T>
where
2019-01-13 22:53:19 +00:00
T: MakeTlsConnect<Socket>,
{
2019-01-18 05:11:24 +00:00
impls::CancelQuery(self.0.cancel_query(make_tls_mode))
}
2019-01-08 04:43:52 +00:00
/// Like `cancel_query`, but uses a stream which is already connected to the server rather than opening a new
/// connection itself.
2019-01-18 05:11:24 +00:00
pub fn cancel_query_raw<S, T>(&mut self, stream: S, tls_mode: T) -> impls::CancelQueryRaw<S, T>
where
S: AsyncRead + AsyncWrite,
2019-01-13 22:53:19 +00:00
T: TlsConnect<S>,
{
2019-01-18 05:11:24 +00:00
impls::CancelQueryRaw(self.0.cancel_query_raw(stream, tls_mode))
}
2019-01-08 04:43:52 +00:00
/// Determines if the connection to the server has already closed.
///
/// In that case, all future queries will fail.
2018-12-22 05:08:26 +00:00
pub fn is_closed(&self) -> bool {
self.0.is_closed()
}
2018-12-23 01:02:48 +00:00
2019-01-08 04:43:52 +00:00
/// Polls the client to check if it is idle.
///
/// A connection is idle if there are no outstanding requests, whether they have begun being polled or not. For
/// example, this can be used by a connection pool to ensure that all work done by one checkout is done before
/// making the client available for a new request. Otherwise, any non-completed work from the first request could
/// interleave with the second.
2018-12-23 01:02:48 +00:00
pub fn poll_idle(&mut self) -> Poll<(), Error> {
self.0.poll_idle()
}
2016-12-20 23:42:28 +00:00
}
2019-01-08 04:43:52 +00:00
/// A connection to a PostgreSQL database.
///
/// This is one half of what is returned when a new connection is established. It performs the actual IO with the
/// server, and should generally be spawned off onto an executor to run in the background.
///
/// `Connection` implements `Future`, and only resolves when the connection is closed, either because a fatal error has
/// occurred, or because its associated `Client` has dropped and all outstanding work has completed.
2018-06-19 23:54:29 +00:00
#[must_use = "futures do nothing unless polled"]
2019-01-13 22:53:19 +00:00
pub struct Connection<S, T>(proto::Connection<proto::MaybeTlsStream<S, T>>);
2016-12-21 03:50:44 +00:00
2019-01-13 22:53:19 +00:00
impl<S, T> Connection<S, T>
where
S: AsyncRead + AsyncWrite,
2019-01-13 22:53:19 +00:00
T: AsyncRead + AsyncWrite,
{
2019-01-08 04:43:52 +00:00
/// Returns the value of a runtime parameter for this connection.
2018-06-17 04:29:27 +00:00
pub fn parameter(&self, name: &str) -> Option<&str> {
self.0.parameter(name)
}
2018-07-07 17:11:16 +00:00
2019-01-08 04:43:52 +00:00
/// Polls for asynchronous messages from the server.
///
/// The server can send notices as well as notifications asynchronously to the client. Applications which wish to
/// examine those messages should use this method to drive the connection rather than its `Future` implementation.
2018-07-07 17:11:16 +00:00
pub fn poll_message(&mut self) -> Poll<Option<AsyncMessage>, Error> {
self.0.poll_message()
}
2016-12-20 23:42:28 +00:00
}
2019-01-13 22:53:19 +00:00
impl<S, T> Future for Connection<S, T>
where
S: AsyncRead + AsyncWrite,
2019-01-13 22:53:19 +00:00
T: AsyncRead + AsyncWrite,
{
2018-06-17 04:29:27 +00:00
type Item = ();
type Error = Error;
2018-06-17 04:29:27 +00:00
fn poll(&mut self) -> Poll<(), Error> {
self.0.poll()
2016-12-21 03:50:44 +00:00
}
}
2019-01-08 04:43:52 +00:00
/// An asynchronous message from the server.
2018-12-10 05:23:31 +00:00
#[allow(clippy::large_enum_variant)]
2018-07-07 17:11:16 +00:00
pub enum AsyncMessage {
2019-01-08 04:43:52 +00:00
/// A notice.
///
/// Notices use the same format as errors, but aren't "errors" per-se.
2018-07-07 17:11:16 +00:00
Notice(DbError),
2019-01-08 04:43:52 +00:00
/// A notification.
///
/// Connections can subscribe to notifications with the `LISTEN` command.
2018-07-07 17:11:16 +00:00
Notification(Notification),
#[doc(hidden)]
__NonExhaustive,
}
2019-01-08 04:43:52 +00:00
/// A prepared statement.
///
/// Prepared statements can only be used with the connection that created them.
#[derive(Clone)]
2018-06-19 02:34:25 +00:00
pub struct Statement(proto::Statement);
impl Statement {
2019-01-08 04:43:52 +00:00
/// Returns the expected types of the statement's parameters.
2018-06-19 02:34:25 +00:00
pub fn params(&self) -> &[Type] {
self.0.params()
}
2019-01-08 04:43:52 +00:00
/// Returns information about the columns returned when the statement is queried.
2018-06-19 02:34:25 +00:00
pub fn columns(&self) -> &[Column] {
self.0.columns()
}
}
2018-06-20 02:10:07 +00:00
2019-01-08 04:43:52 +00:00
/// A portal.
///
/// Portals can only be used with the connection that created them, and only exist for the duration of the transaction
/// in which they were created.
2018-08-16 02:53:20 +00:00
pub struct Portal(proto::Portal);
/// A builder type which can wrap a future in a database transaction.
pub struct TransactionBuilder(proto::Client);
impl TransactionBuilder {
2019-03-05 05:22:50 +00:00
/// Returns a future which wraps another in a database transaction.
pub fn build<T>(self, future: T) -> impls::Transaction<T>
where
T: Future,
// FIXME error type?
T::Error: From<Error>,
{
2019-03-05 05:22:50 +00:00
impls::Transaction(proto::TransactionFuture::new(self.0, future))
2018-07-14 21:59:37 +00:00
}
}
2019-02-01 04:59:33 +00:00
/// Message returned by the `SimpleQuery` stream.
pub enum SimpleQueryMessage {
2019-02-01 04:59:33 +00:00
/// A row of data.
Row(SimpleQueryRow),
2019-02-01 04:59:33 +00:00
/// A statement in the query has completed.
///
/// The number of rows modified or selected is returned.
CommandComplete(u64),
#[doc(hidden)]
__NonExhaustive,
2018-07-08 05:42:04 +00:00
}
2018-12-09 01:39:20 +00:00
/// An asynchronous notification.
#[derive(Clone, Debug)]
pub struct Notification {
2019-01-08 04:43:52 +00:00
process_id: i32,
channel: String,
payload: String,
}
impl Notification {
2018-12-09 01:39:20 +00:00
/// The process ID of the notifying backend process.
2019-01-08 04:43:52 +00:00
pub fn process_id(&self) -> i32 {
self.process_id
}
2018-12-09 01:39:20 +00:00
/// The name of the channel that the notify has been raised on.
2019-01-08 04:43:52 +00:00
pub fn channel(&self) -> &str {
&self.channel
}
2018-12-09 01:39:20 +00:00
/// The "payload" string passed from the notifying process.
2019-01-08 04:43:52 +00:00
pub fn payload(&self) -> &str {
&self.payload
}
2018-12-09 01:39:20 +00:00
}