Line data Source code
1 : //!
2 : //! Generate a tarball with files needed to bootstrap ComputeNode.
3 : //!
4 : //! TODO: this module has nothing to do with PostgreSQL pg_basebackup.
5 : //! It could use a better name.
6 : //!
7 : //! Stateless Postgres compute node is launched by sending a tarball
8 : //! which contains non-relational data (multixacts, clog, filenodemaps, twophase files),
9 : //! generated pg_control and dummy segment of WAL.
10 : //! This module is responsible for creation of such tarball
11 : //! from data stored in object storage.
12 : //!
13 : use anyhow::{anyhow, bail, ensure, Context};
14 : use bytes::{BufMut, Bytes, BytesMut};
15 : use fail::fail_point;
16 : use pageserver_api::key::{key_to_slru_block, Key};
17 : use postgres_ffi::pg_constants;
18 : use std::fmt::Write as FmtWrite;
19 : use std::time::SystemTime;
20 : use tokio::io;
21 : use tokio::io::AsyncWrite;
22 : use tracing::*;
23 :
24 : use tokio_tar::{Builder, EntryType, Header};
25 :
26 : use crate::context::RequestContext;
27 : use crate::pgdatadir_mapping::Version;
28 : use crate::tenant::Timeline;
29 : use pageserver_api::reltag::{RelTag, SlruKind};
30 :
31 : use postgres_ffi::dispatch_pgversion;
32 : use postgres_ffi::pg_constants::{DEFAULTTABLESPACE_OID, GLOBALTABLESPACE_OID};
33 : use postgres_ffi::pg_constants::{PGDATA_SPECIAL_FILES, PGDATA_SUBDIRS, PG_HBA};
34 : use postgres_ffi::relfile_utils::{INIT_FORKNUM, MAIN_FORKNUM};
35 : use postgres_ffi::TransactionId;
36 : use postgres_ffi::XLogFileName;
37 : use postgres_ffi::PG_TLI;
38 : use postgres_ffi::{BLCKSZ, RELSEG_SIZE, WAL_SEGMENT_SIZE};
39 : use utils::lsn::Lsn;
40 :
41 : /// Create basebackup with non-rel data in it.
42 : /// Only include relational data if 'full_backup' is true.
43 : ///
44 : /// Currently we use empty 'req_lsn' in two cases:
45 : /// * During the basebackup right after timeline creation
46 : /// * When working without safekeepers. In this situation it is important to match the lsn
47 : /// we are taking basebackup on with the lsn that is used in pageserver's walreceiver
48 : /// to start the replication.
49 0 : pub async fn send_basebackup_tarball<'a, W>(
50 0 : write: &'a mut W,
51 0 : timeline: &'a Timeline,
52 0 : req_lsn: Option<Lsn>,
53 0 : prev_lsn: Option<Lsn>,
54 0 : full_backup: bool,
55 0 : ctx: &'a RequestContext,
56 0 : ) -> anyhow::Result<()>
57 0 : where
58 0 : W: AsyncWrite + Send + Sync + Unpin,
59 0 : {
60 : // Compute postgres doesn't have any previous WAL files, but the first
61 : // record that it's going to write needs to include the LSN of the
62 : // previous record (xl_prev). We include prev_record_lsn in the
63 : // "zenith.signal" file, so that postgres can read it during startup.
64 : //
65 : // We don't keep full history of record boundaries in the page server,
66 : // however, only the predecessor of the latest record on each
67 : // timeline. So we can only provide prev_record_lsn when you take a
68 : // base backup at the end of the timeline, i.e. at last_record_lsn.
69 : // Even at the end of the timeline, we sometimes don't have a valid
70 : // prev_lsn value; that happens if the timeline was just branched from
71 : // an old LSN and it doesn't have any WAL of its own yet. We will set
72 : // prev_lsn to Lsn(0) if we cannot provide the correct value.
73 0 : let (backup_prev, backup_lsn) = if let Some(req_lsn) = req_lsn {
74 : // Backup was requested at a particular LSN. The caller should've
75 : // already checked that it's a valid LSN.
76 :
77 : // If the requested point is the end of the timeline, we can
78 : // provide prev_lsn. (get_last_record_rlsn() might return it as
79 : // zero, though, if no WAL has been generated on this timeline
80 : // yet.)
81 0 : let end_of_timeline = timeline.get_last_record_rlsn();
82 0 : if req_lsn == end_of_timeline.last {
83 0 : (end_of_timeline.prev, req_lsn)
84 : } else {
85 0 : (Lsn(0), req_lsn)
86 : }
87 : } else {
88 : // Backup was requested at end of the timeline.
89 0 : let end_of_timeline = timeline.get_last_record_rlsn();
90 0 : (end_of_timeline.prev, end_of_timeline.last)
91 : };
92 :
93 : // Consolidate the derived and the provided prev_lsn values
94 0 : let prev_lsn = if let Some(provided_prev_lsn) = prev_lsn {
95 0 : if backup_prev != Lsn(0) {
96 0 : ensure!(backup_prev == provided_prev_lsn);
97 0 : }
98 0 : provided_prev_lsn
99 : } else {
100 0 : backup_prev
101 : };
102 :
103 0 : info!(
104 0 : "taking basebackup lsn={}, prev_lsn={} (full_backup={})",
105 0 : backup_lsn, prev_lsn, full_backup
106 0 : );
107 :
108 0 : let basebackup = Basebackup {
109 0 : ar: Builder::new_non_terminated(write),
110 0 : timeline,
111 0 : lsn: backup_lsn,
112 0 : prev_record_lsn: prev_lsn,
113 0 : full_backup,
114 0 : ctx,
115 0 : };
116 0 : basebackup
117 0 : .send_tarball()
118 0 : .instrument(info_span!("send_tarball", backup_lsn=%backup_lsn))
119 0 : .await
120 0 : }
121 :
122 : /// This is short-living object only for the time of tarball creation,
123 : /// created mostly to avoid passing a lot of parameters between various functions
124 : /// used for constructing tarball.
125 : struct Basebackup<'a, W>
126 : where
127 : W: AsyncWrite + Send + Sync + Unpin,
128 : {
129 : ar: Builder<&'a mut W>,
130 : timeline: &'a Timeline,
131 : lsn: Lsn,
132 : prev_record_lsn: Lsn,
133 : full_backup: bool,
134 : ctx: &'a RequestContext,
135 : }
136 :
137 : /// A sink that accepts SLRU blocks ordered by key and forwards
138 : /// full segments to the archive.
139 : struct SlruSegmentsBuilder<'a, 'b, W>
140 : where
141 : W: AsyncWrite + Send + Sync + Unpin,
142 : {
143 : ar: &'a mut Builder<&'b mut W>,
144 : buf: Vec<u8>,
145 : current_segment: Option<(SlruKind, u32)>,
146 : total_blocks: usize,
147 : }
148 :
149 : impl<'a, 'b, W> SlruSegmentsBuilder<'a, 'b, W>
150 : where
151 : W: AsyncWrite + Send + Sync + Unpin,
152 : {
153 0 : fn new(ar: &'a mut Builder<&'b mut W>) -> Self {
154 0 : Self {
155 0 : ar,
156 0 : buf: Vec::new(),
157 0 : current_segment: None,
158 0 : total_blocks: 0,
159 0 : }
160 0 : }
161 :
162 0 : async fn add_block(&mut self, key: &Key, block: Bytes) -> anyhow::Result<()> {
163 0 : let (kind, segno, _) = key_to_slru_block(*key)?;
164 :
165 0 : match kind {
166 : SlruKind::Clog => {
167 0 : ensure!(block.len() == BLCKSZ as usize || block.len() == BLCKSZ as usize + 8);
168 : }
169 : SlruKind::MultiXactMembers | SlruKind::MultiXactOffsets => {
170 0 : ensure!(block.len() == BLCKSZ as usize);
171 : }
172 : }
173 :
174 0 : let segment = (kind, segno);
175 0 : match self.current_segment {
176 0 : None => {
177 0 : self.current_segment = Some(segment);
178 0 : self.buf
179 0 : .extend_from_slice(block.slice(..BLCKSZ as usize).as_ref());
180 0 : }
181 0 : Some(current_seg) if current_seg == segment => {
182 0 : self.buf
183 0 : .extend_from_slice(block.slice(..BLCKSZ as usize).as_ref());
184 0 : }
185 : Some(_) => {
186 0 : self.flush().await?;
187 :
188 0 : self.current_segment = Some(segment);
189 0 : self.buf
190 0 : .extend_from_slice(block.slice(..BLCKSZ as usize).as_ref());
191 : }
192 : }
193 :
194 0 : Ok(())
195 0 : }
196 :
197 0 : async fn flush(&mut self) -> anyhow::Result<()> {
198 0 : let nblocks = self.buf.len() / BLCKSZ as usize;
199 0 : let (kind, segno) = self.current_segment.take().unwrap();
200 0 : let segname = format!("{}/{:>04X}", kind.to_str(), segno);
201 0 : let header = new_tar_header(&segname, self.buf.len() as u64)?;
202 0 : self.ar.append(&header, self.buf.as_slice()).await?;
203 :
204 0 : self.total_blocks += nblocks;
205 0 : debug!("Added to basebackup slru {} relsize {}", segname, nblocks);
206 :
207 0 : self.buf.clear();
208 0 :
209 0 : Ok(())
210 0 : }
211 :
212 0 : async fn finish(mut self) -> anyhow::Result<()> {
213 0 : let res = if self.current_segment.is_none() || self.buf.is_empty() {
214 0 : Ok(())
215 : } else {
216 0 : self.flush().await
217 : };
218 :
219 0 : info!("Collected {} SLRU blocks", self.total_blocks);
220 :
221 0 : res
222 0 : }
223 : }
224 :
225 : impl<'a, W> Basebackup<'a, W>
226 : where
227 : W: AsyncWrite + Send + Sync + Unpin,
228 : {
229 0 : async fn send_tarball(mut self) -> anyhow::Result<()> {
230 : // TODO include checksum
231 :
232 0 : let lazy_slru_download = self.timeline.get_lazy_slru_download() && !self.full_backup;
233 :
234 : // Create pgdata subdirs structure
235 0 : for dir in PGDATA_SUBDIRS.iter() {
236 0 : let header = new_tar_header_dir(dir)?;
237 0 : self.ar
238 0 : .append(&header, &mut io::empty())
239 0 : .await
240 0 : .context("could not add directory to basebackup tarball")?;
241 : }
242 :
243 : // Send config files.
244 0 : for filepath in PGDATA_SPECIAL_FILES.iter() {
245 0 : if *filepath == "pg_hba.conf" {
246 0 : let data = PG_HBA.as_bytes();
247 0 : let header = new_tar_header(filepath, data.len() as u64)?;
248 0 : self.ar
249 0 : .append(&header, data)
250 0 : .await
251 0 : .context("could not add config file to basebackup tarball")?;
252 : } else {
253 0 : let header = new_tar_header(filepath, 0)?;
254 0 : self.ar
255 0 : .append(&header, &mut io::empty())
256 0 : .await
257 0 : .context("could not add config file to basebackup tarball")?;
258 : }
259 : }
260 0 : if !lazy_slru_download {
261 : // Gather non-relational files from object storage pages.
262 0 : let slru_partitions = self
263 0 : .timeline
264 0 : .get_slru_keyspace(Version::Lsn(self.lsn), self.ctx)
265 0 : .await?
266 0 : .partition(Timeline::MAX_GET_VECTORED_KEYS * BLCKSZ as u64);
267 0 :
268 0 : let mut slru_builder = SlruSegmentsBuilder::new(&mut self.ar);
269 :
270 0 : for part in slru_partitions.parts {
271 0 : let blocks = self.timeline.get_vectored(part, self.lsn, self.ctx).await?;
272 :
273 0 : for (key, block) in blocks {
274 0 : slru_builder.add_block(&key, block?).await?;
275 : }
276 : }
277 0 : slru_builder.finish().await?;
278 0 : }
279 :
280 0 : let mut min_restart_lsn: Lsn = Lsn::MAX;
281 : // Create tablespace directories
282 0 : for ((spcnode, dbnode), has_relmap_file) in
283 0 : self.timeline.list_dbdirs(self.lsn, self.ctx).await?
284 : {
285 0 : self.add_dbdir(spcnode, dbnode, has_relmap_file).await?;
286 :
287 : // If full backup is requested, include all relation files.
288 : // Otherwise only include init forks of unlogged relations.
289 0 : let rels = self
290 0 : .timeline
291 0 : .list_rels(spcnode, dbnode, Version::Lsn(self.lsn), self.ctx)
292 0 : .await?;
293 0 : for &rel in rels.iter() {
294 : // Send init fork as main fork to provide well formed empty
295 : // contents of UNLOGGED relations. Postgres copies it in
296 : // `reinit.c` during recovery.
297 0 : if rel.forknum == INIT_FORKNUM {
298 : // I doubt we need _init fork itself, but having it at least
299 : // serves as a marker relation is unlogged.
300 0 : self.add_rel(rel, rel).await?;
301 0 : self.add_rel(rel, rel.with_forknum(MAIN_FORKNUM)).await?;
302 0 : continue;
303 0 : }
304 0 :
305 0 : if self.full_backup {
306 0 : if rel.forknum == MAIN_FORKNUM && rels.contains(&rel.with_forknum(INIT_FORKNUM))
307 : {
308 : // skip this, will include it when we reach the init fork
309 0 : continue;
310 0 : }
311 0 : self.add_rel(rel, rel).await?;
312 0 : }
313 : }
314 :
315 0 : for (path, content) in self.timeline.list_aux_files(self.lsn, self.ctx).await? {
316 0 : if path.starts_with("pg_replslot") {
317 0 : let offs = pg_constants::REPL_SLOT_ON_DISK_OFFSETOF_RESTART_LSN;
318 0 : let restart_lsn = Lsn(u64::from_le_bytes(
319 0 : content[offs..offs + 8].try_into().unwrap(),
320 0 : ));
321 0 : info!("Replication slot {} restart LSN={}", path, restart_lsn);
322 0 : min_restart_lsn = Lsn::min(min_restart_lsn, restart_lsn);
323 0 : }
324 0 : let header = new_tar_header(&path, content.len() as u64)?;
325 0 : self.ar
326 0 : .append(&header, &*content)
327 0 : .await
328 0 : .context("could not add aux file to basebackup tarball")?;
329 : }
330 : }
331 0 : if min_restart_lsn != Lsn::MAX {
332 0 : info!(
333 0 : "Min restart LSN for logical replication is {}",
334 0 : min_restart_lsn
335 0 : );
336 0 : let data = min_restart_lsn.0.to_le_bytes();
337 0 : let header = new_tar_header("restart.lsn", data.len() as u64)?;
338 0 : self.ar
339 0 : .append(&header, &data[..])
340 0 : .await
341 0 : .context("could not add restart.lsn file to basebackup tarball")?;
342 0 : }
343 0 : for xid in self
344 0 : .timeline
345 0 : .list_twophase_files(self.lsn, self.ctx)
346 0 : .await?
347 : {
348 0 : self.add_twophase_file(xid).await?;
349 : }
350 :
351 0 : fail_point!("basebackup-before-control-file", |_| {
352 0 : bail!("failpoint basebackup-before-control-file")
353 0 : });
354 :
355 : // Generate pg_control and bootstrap WAL segment.
356 0 : self.add_pgcontrol_file().await?;
357 0 : self.ar.finish().await?;
358 0 : debug!("all tarred up!");
359 0 : Ok(())
360 0 : }
361 :
362 : /// Add contents of relfilenode `src`, naming it as `dst`.
363 0 : async fn add_rel(&mut self, src: RelTag, dst: RelTag) -> anyhow::Result<()> {
364 0 : let nblocks = self
365 0 : .timeline
366 0 : .get_rel_size(src, Version::Lsn(self.lsn), false, self.ctx)
367 0 : .await?;
368 :
369 : // If the relation is empty, create an empty file
370 0 : if nblocks == 0 {
371 0 : let file_name = dst.to_segfile_name(0);
372 0 : let header = new_tar_header(&file_name, 0)?;
373 0 : self.ar.append(&header, &mut io::empty()).await?;
374 0 : return Ok(());
375 0 : }
376 0 :
377 0 : // Add a file for each chunk of blocks (aka segment)
378 0 : let mut startblk = 0;
379 0 : let mut seg = 0;
380 0 : while startblk < nblocks {
381 0 : let endblk = std::cmp::min(startblk + RELSEG_SIZE, nblocks);
382 0 :
383 0 : let mut segment_data: Vec<u8> = vec![];
384 0 : for blknum in startblk..endblk {
385 0 : let img = self
386 0 : .timeline
387 0 : .get_rel_page_at_lsn(src, blknum, Version::Lsn(self.lsn), false, self.ctx)
388 0 : .await?;
389 0 : segment_data.extend_from_slice(&img[..]);
390 : }
391 :
392 0 : let file_name = dst.to_segfile_name(seg as u32);
393 0 : let header = new_tar_header(&file_name, segment_data.len() as u64)?;
394 0 : self.ar.append(&header, segment_data.as_slice()).await?;
395 :
396 0 : seg += 1;
397 0 : startblk = endblk;
398 : }
399 :
400 0 : Ok(())
401 0 : }
402 :
403 : //
404 : // Include database/tablespace directories.
405 : //
406 : // Each directory contains a PG_VERSION file, and the default database
407 : // directories also contain pg_filenode.map files.
408 : //
409 0 : async fn add_dbdir(
410 0 : &mut self,
411 0 : spcnode: u32,
412 0 : dbnode: u32,
413 0 : has_relmap_file: bool,
414 0 : ) -> anyhow::Result<()> {
415 0 : let relmap_img = if has_relmap_file {
416 0 : let img = self
417 0 : .timeline
418 0 : .get_relmap_file(spcnode, dbnode, Version::Lsn(self.lsn), self.ctx)
419 0 : .await?;
420 :
421 0 : ensure!(
422 0 : img.len()
423 0 : == dispatch_pgversion!(
424 0 : self.timeline.pg_version,
425 0 : pgv::bindings::SIZEOF_RELMAPFILE
426 : )
427 : );
428 :
429 0 : Some(img)
430 : } else {
431 0 : None
432 : };
433 :
434 0 : if spcnode == GLOBALTABLESPACE_OID {
435 0 : let pg_version_str = match self.timeline.pg_version {
436 0 : 14 | 15 => self.timeline.pg_version.to_string(),
437 0 : ver => format!("{ver}\x0A"),
438 : };
439 0 : let header = new_tar_header("PG_VERSION", pg_version_str.len() as u64)?;
440 0 : self.ar.append(&header, pg_version_str.as_bytes()).await?;
441 :
442 0 : info!("timeline.pg_version {}", self.timeline.pg_version);
443 :
444 0 : if let Some(img) = relmap_img {
445 : // filenode map for global tablespace
446 0 : let header = new_tar_header("global/pg_filenode.map", img.len() as u64)?;
447 0 : self.ar.append(&header, &img[..]).await?;
448 : } else {
449 0 : warn!("global/pg_filenode.map is missing");
450 : }
451 : } else {
452 : // User defined tablespaces are not supported. However, as
453 : // a special case, if a tablespace/db directory is
454 : // completely empty, we can leave it out altogether. This
455 : // makes taking a base backup after the 'tablespace'
456 : // regression test pass, because the test drops the
457 : // created tablespaces after the tests.
458 : //
459 : // FIXME: this wouldn't be necessary, if we handled
460 : // XLOG_TBLSPC_DROP records. But we probably should just
461 : // throw an error on CREATE TABLESPACE in the first place.
462 0 : if !has_relmap_file
463 0 : && self
464 0 : .timeline
465 0 : .list_rels(spcnode, dbnode, Version::Lsn(self.lsn), self.ctx)
466 0 : .await?
467 0 : .is_empty()
468 : {
469 0 : return Ok(());
470 0 : }
471 0 : // User defined tablespaces are not supported
472 0 : ensure!(spcnode == DEFAULTTABLESPACE_OID);
473 :
474 : // Append dir path for each database
475 0 : let path = format!("base/{}", dbnode);
476 0 : let header = new_tar_header_dir(&path)?;
477 0 : self.ar.append(&header, &mut io::empty()).await?;
478 :
479 0 : if let Some(img) = relmap_img {
480 0 : let dst_path = format!("base/{}/PG_VERSION", dbnode);
481 :
482 0 : let pg_version_str = match self.timeline.pg_version {
483 0 : 14 | 15 => self.timeline.pg_version.to_string(),
484 0 : ver => format!("{ver}\x0A"),
485 : };
486 0 : let header = new_tar_header(&dst_path, pg_version_str.len() as u64)?;
487 0 : self.ar.append(&header, pg_version_str.as_bytes()).await?;
488 :
489 0 : let relmap_path = format!("base/{}/pg_filenode.map", dbnode);
490 0 : let header = new_tar_header(&relmap_path, img.len() as u64)?;
491 0 : self.ar.append(&header, &img[..]).await?;
492 0 : }
493 : };
494 0 : Ok(())
495 0 : }
496 :
497 : //
498 : // Extract twophase state files
499 : //
500 0 : async fn add_twophase_file(&mut self, xid: TransactionId) -> anyhow::Result<()> {
501 0 : let img = self
502 0 : .timeline
503 0 : .get_twophase_file(xid, self.lsn, self.ctx)
504 0 : .await?;
505 :
506 0 : let mut buf = BytesMut::new();
507 0 : buf.extend_from_slice(&img[..]);
508 0 : let crc = crc32c::crc32c(&img[..]);
509 0 : buf.put_u32_le(crc);
510 0 : let path = format!("pg_twophase/{:>08X}", xid);
511 0 : let header = new_tar_header(&path, buf.len() as u64)?;
512 0 : self.ar.append(&header, &buf[..]).await?;
513 :
514 0 : Ok(())
515 0 : }
516 :
517 : //
518 : // Add generated pg_control file and bootstrap WAL segment.
519 : // Also send zenith.signal file with extra bootstrap data.
520 : //
521 0 : async fn add_pgcontrol_file(&mut self) -> anyhow::Result<()> {
522 0 : // add zenith.signal file
523 0 : let mut zenith_signal = String::new();
524 0 : if self.prev_record_lsn == Lsn(0) {
525 0 : if self.lsn == self.timeline.get_ancestor_lsn() {
526 0 : write!(zenith_signal, "PREV LSN: none")?;
527 : } else {
528 0 : write!(zenith_signal, "PREV LSN: invalid")?;
529 : }
530 : } else {
531 0 : write!(zenith_signal, "PREV LSN: {}", self.prev_record_lsn)?;
532 : }
533 0 : self.ar
534 0 : .append(
535 0 : &new_tar_header("zenith.signal", zenith_signal.len() as u64)?,
536 0 : zenith_signal.as_bytes(),
537 : )
538 0 : .await?;
539 :
540 0 : let checkpoint_bytes = self
541 0 : .timeline
542 0 : .get_checkpoint(self.lsn, self.ctx)
543 0 : .await
544 0 : .context("failed to get checkpoint bytes")?;
545 0 : let pg_control_bytes = self
546 0 : .timeline
547 0 : .get_control_file(self.lsn, self.ctx)
548 0 : .await
549 0 : .context("failed get control bytes")?;
550 :
551 0 : let (pg_control_bytes, system_identifier) = postgres_ffi::generate_pg_control(
552 0 : &pg_control_bytes,
553 0 : &checkpoint_bytes,
554 0 : self.lsn,
555 0 : self.timeline.pg_version,
556 0 : )?;
557 :
558 : //send pg_control
559 0 : let header = new_tar_header("global/pg_control", pg_control_bytes.len() as u64)?;
560 0 : self.ar.append(&header, &pg_control_bytes[..]).await?;
561 :
562 : //send wal segment
563 0 : let segno = self.lsn.segment_number(WAL_SEGMENT_SIZE);
564 0 : let wal_file_name = XLogFileName(PG_TLI, segno, WAL_SEGMENT_SIZE);
565 0 : let wal_file_path = format!("pg_wal/{}", wal_file_name);
566 0 : let header = new_tar_header(&wal_file_path, WAL_SEGMENT_SIZE as u64)?;
567 :
568 0 : let wal_seg = postgres_ffi::generate_wal_segment(
569 0 : segno,
570 0 : system_identifier,
571 0 : self.timeline.pg_version,
572 0 : self.lsn,
573 0 : )
574 0 : .map_err(|e| anyhow!(e).context("Failed generating wal segment"))?;
575 0 : ensure!(wal_seg.len() == WAL_SEGMENT_SIZE);
576 0 : self.ar.append(&header, &wal_seg[..]).await?;
577 0 : Ok(())
578 0 : }
579 : }
580 :
581 : //
582 : // Create new tarball entry header
583 : //
584 0 : fn new_tar_header(path: &str, size: u64) -> anyhow::Result<Header> {
585 0 : let mut header = Header::new_gnu();
586 0 : header.set_size(size);
587 0 : header.set_path(path)?;
588 0 : header.set_mode(0b110000000); // -rw-------
589 0 : header.set_mtime(
590 0 : // use currenttime as last modified time
591 0 : SystemTime::now()
592 0 : .duration_since(SystemTime::UNIX_EPOCH)
593 0 : .unwrap()
594 0 : .as_secs(),
595 0 : );
596 0 : header.set_cksum();
597 0 : Ok(header)
598 0 : }
599 :
600 0 : fn new_tar_header_dir(path: &str) -> anyhow::Result<Header> {
601 0 : let mut header = Header::new_gnu();
602 0 : header.set_size(0);
603 0 : header.set_path(path)?;
604 0 : header.set_mode(0o755); // -rw-------
605 0 : header.set_entry_type(EntryType::dir());
606 0 : header.set_mtime(
607 0 : // use currenttime as last modified time
608 0 : SystemTime::now()
609 0 : .duration_since(SystemTime::UNIX_EPOCH)
610 0 : .unwrap()
611 0 : .as_secs(),
612 0 : );
613 0 : header.set_cksum();
614 0 : Ok(header)
615 0 : }
|