1use super::BACKUP_VERSION;
7use crate::archive_options::ArchiveOptions;
8use crate::{ArchiveError, NONCE_SIZE, snapshot, util::GenericArrayExt};
9use aes_gcm::aead::Aead;
10use async_compression::futures::write::ZstdEncoder;
11use futures::{FutureExt, io::AllowStdIo};
12use futures_util::AsyncWriteExt;
13use prost::Message;
14#[allow(deprecated)]
15use sha2::digest::generic_array::GenericArray;
16use std::io;
17use xmtp_db::ConnectionExt;
18use xmtp_proto::xmtp::device_sync::{BackupElement, BackupMetadataSave, backup_element::Element};
19
20pub fn export(
27 options: ArchiveOptions,
28 db: impl ConnectionExt,
29 key: &[u8],
30 sink: impl io::Write,
31) -> Result<BackupMetadataSave, ArchiveError> {
32 let mut writer = ElementWriter::new(key, sink)?;
33 let metadata = snapshot::read(&db, &options, |element| writer.write(element))?;
34 writer.finish()?;
35 Ok(metadata)
36}
37
38#[allow(deprecated)]
43pub struct ElementWriter<W: io::Write> {
44 cipher: aes_gcm::Aes256Gcm,
45 nonce: GenericArray<u8, aes_gcm::aead::consts::U12>,
46 zstd: ZstdEncoder<AllowStdIo<W>>,
47}
48
49impl<W: io::Write> ElementWriter<W> {
50 pub fn new(key: &[u8], mut sink: W) -> Result<Self, ArchiveError> {
52 let cipher = crate::cipher(key)?;
53 let nonce = xmtp_common::rand_array::<NONCE_SIZE>();
54 sink.write_all(&BACKUP_VERSION.to_le_bytes())?;
55 sink.write_all(&nonce)?;
56 #[allow(deprecated)]
57 Ok(Self {
58 cipher,
59 nonce: GenericArray::clone_from_slice(&nonce),
60 zstd: ZstdEncoder::new(AllowStdIo::new(sink)),
61 })
62 }
63
64 pub fn write(&mut self, element: Element) -> Result<(), ArchiveError> {
67 let plaintext = BackupElement {
68 element: Some(element),
69 }
70 .encode_to_vec();
71 let ciphertext = self.cipher.encrypt(&self.nonce, &*plaintext)?;
72 self.nonce.increment();
73 let length = u32::try_from(ciphertext.len())
74 .map_err(|_| io::Error::other("archive frame exceeds u32 length"))?;
75 ready(self.zstd.write_all(&length.to_le_bytes()))?;
76 Ok(ready(self.zstd.write_all(&ciphertext))?)
77 }
78
79 pub fn finish(mut self) -> Result<(), ArchiveError> {
81 Ok(ready(self.zstd.close())?)
82 }
83}
84
85#[cfg(not(target_arch = "wasm32"))]
88pub struct ArchiveExporter;
89
90#[cfg(not(target_arch = "wasm32"))]
91impl ArchiveExporter {
92 pub async fn export_to_file(
100 options: ArchiveOptions,
101 db: impl ConnectionExt + 'static,
102 path: impl AsRef<std::path::Path>,
103 key: &[u8],
104 ) -> Result<BackupMetadataSave, ArchiveError> {
105 let path = path.as_ref().to_owned();
106 offload(key, move |key, cancel| {
107 write_file(options, db, &path, key, cancel)
108 })
109 .await
110 }
111
112 pub async fn export_to_bytes(
116 options: ArchiveOptions,
117 db: impl ConnectionExt + 'static,
118 key: &[u8],
119 ) -> Result<Vec<u8>, ArchiveError> {
120 offload(key, move |key, cancel| {
121 let mut bytes = Vec::new();
122 export(options, db, key, Cancellable(&mut bytes, cancel))?;
123 Ok(bytes)
124 })
125 .await
126 }
127}
128
129#[cfg(not(target_arch = "wasm32"))]
132async fn offload<T: Send + 'static>(
133 key: &[u8],
134 f: impl FnOnce(&[u8], &tokio_util::sync::CancellationToken) -> Result<T, ArchiveError>
135 + Send
136 + 'static,
137) -> Result<T, ArchiveError> {
138 crate::check_key(key)?;
139 let key = key.to_vec();
140 let cancel = tokio_util::sync::CancellationToken::new();
141 let _cancel_on_drop = cancel.clone().drop_guard();
142 tokio::task::spawn_blocking(move || f(&key, &cancel))
143 .await
144 .map_err(io::Error::other)?
145}
146
147#[cfg(not(target_arch = "wasm32"))]
151pub(crate) fn write_file(
152 options: ArchiveOptions,
153 db: impl ConnectionExt,
154 path: &std::path::Path,
155 key: &[u8],
156 cancel: &tokio_util::sync::CancellationToken,
157) -> Result<BackupMetadataSave, ArchiveError> {
158 let mut partial = path.as_os_str().to_owned();
159 partial.push(format!(
160 ".{:016x}.partial",
161 u64::from_le_bytes(xmtp_common::rand_array())
162 ));
163 let partial = std::path::PathBuf::from(partial);
164 let mut create = std::fs::OpenOptions::new();
165 create.write(true).create_new(true);
166 #[cfg(unix)]
167 std::os::unix::fs::OpenOptionsExt::mode(&mut create, 0o600);
168 let file = create.open(&partial)?;
171 let exported = (|| -> Result<_, ArchiveError> {
172 let mut file = io::BufWriter::new(file);
173 let metadata = export(options, db, key, Cancellable(&mut file, cancel))?;
174 file.into_inner()
175 .map_err(io::IntoInnerError::into_error)?
176 .sync_all()?;
177 live(cancel)?;
178 std::fs::rename(&partial, path)?;
179 Ok(metadata)
180 })();
181 if exported.is_err()
182 && let Err(e) = std::fs::remove_file(&partial)
183 {
184 tracing::warn!(path = %partial.display(), error = %e, "failed export left a partial archive");
185 }
186 exported
187}
188
189#[cfg(not(target_arch = "wasm32"))]
191fn live(cancel: &tokio_util::sync::CancellationToken) -> io::Result<()> {
192 if cancel.is_cancelled() {
193 return Err(io::Error::other("archive export cancelled"));
194 }
195 Ok(())
196}
197
198#[cfg(not(target_arch = "wasm32"))]
200struct Cancellable<'a, W>(W, &'a tokio_util::sync::CancellationToken);
201
202#[cfg(not(target_arch = "wasm32"))]
203impl<W: io::Write> io::Write for Cancellable<'_, W> {
204 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
205 live(self.1)?;
206 self.0.write(buf)
207 }
208
209 fn flush(&mut self) -> io::Result<()> {
210 self.0.flush()
211 }
212}
213
214fn ready(op: impl Future<Output = io::Result<()>>) -> io::Result<()> {
216 op.now_or_never().unwrap_or_else(|| {
217 Err(io::Error::other(
218 "synchronous archive sink returned pending",
219 ))
220 })
221}