mz_testdrive/action/
file.rs1use std::path::{self, PathBuf};
11use std::str::FromStr;
12
13use anyhow::bail;
14use async_compression::tokio::write::{BzEncoder, GzipEncoder, XzEncoder, ZstdEncoder};
15use tokio::fs::{self, OpenOptions};
16use tokio::io::{AsyncWrite, AsyncWriteExt, BufWriter};
17
18use crate::action::{ControlFlow, State};
19use crate::format::bytes;
20use crate::parser::BuiltinCommand;
21
22pub enum Compression {
23 Bzip2,
24 Gzip,
25 Xz,
26 Zstd,
27 None,
28}
29
30impl FromStr for Compression {
31 type Err = anyhow::Error;
32
33 fn from_str(s: &str) -> Result<Self, anyhow::Error> {
34 match s {
35 "bzip2" => Ok(Compression::Bzip2),
36 "gzip" => Ok(Compression::Gzip),
37 "xz" => Ok(Compression::Xz),
38 "zstd" => Ok(Compression::Zstd),
39 "none" => Ok(Compression::None),
40 f => bail!("unknown compression format: {}", f),
41 }
42 }
43}
44
45pub(crate) fn build_compression(cmd: &mut BuiltinCommand) -> Result<Compression, anyhow::Error> {
46 match cmd.args.opt_string("compression") {
47 Some(s) => s.parse(),
48 None => Ok(Compression::None),
49 }
50}
51
52pub(crate) struct Contents {
61 header: Option<String>,
62 lines: Vec<Vec<u8>>,
64 repeat: usize,
65 trailing_newline: bool,
66}
67
68impl Contents {
69 pub(crate) fn parse(cmd: &mut BuiltinCommand) -> Result<Contents, anyhow::Error> {
70 let header = cmd.args.opt_string("header");
71 let trailing_newline = cmd.args.opt_bool("trailing-newline")?.unwrap_or(true);
72 let repeat: usize = cmd.args.opt_parse("repeat")?.unwrap_or(1);
73
74 let mut lines = vec![];
75 for line in &cmd.input {
76 lines.push(bytes::unescape(line.as_bytes())?);
77 }
78
79 Ok(Contents {
80 header,
81 lines,
82 repeat,
83 trailing_newline,
84 })
85 }
86
87 fn output_lines(&self) -> impl Iterator<Item = &[u8]> {
90 let header = self.header.as_deref().map(str::as_bytes).into_iter();
91 let body = (0..self.repeat).flat_map(move |_| self.lines.iter().map(Vec::as_slice));
92 header.chain(body)
93 }
94
95 pub(crate) async fn write_to<W>(&self, writer: &mut W) -> Result<(), anyhow::Error>
98 where
99 W: AsyncWrite + Unpin,
100 {
101 let mut wrote_line = false;
102 for line in self.output_lines() {
103 if wrote_line {
106 writer.write_all(b"\n").await?;
107 }
108 writer.write_all(line).await?;
109 wrote_line = true;
110 }
111 if wrote_line && self.trailing_newline {
112 writer.write_all(b"\n").await?;
113 }
114 Ok(())
115 }
116}
117
118fn build_path(state: &State, cmd: &mut BuiltinCommand) -> Result<PathBuf, anyhow::Error> {
119 let path = cmd.args.string("path")?;
120 let container = cmd.args.opt_string("container");
121
122 if path.contains(path::MAIN_SEPARATOR) {
123 bail!("separators in paths are forbidden")
125 }
126
127 match container.as_deref() {
128 None => Ok(state.temp_path.join(path)),
129 Some("fivetran") => Ok(PathBuf::from(&state.fivetran_destination_files_path).join(path)),
130 Some(x) => bail!("Unrecognized container '{x}'"),
131 }
132}
133
134pub async fn run_append(
135 mut cmd: BuiltinCommand,
136 state: &State,
137) -> Result<ControlFlow, anyhow::Error> {
138 let path = build_path(state, &mut cmd)?;
139 let compression = build_compression(&mut cmd)?;
140 let contents = Contents::parse(&mut cmd)?;
141 cmd.args.done()?;
142
143 println!("Appending to file {}", path.display());
144 let file = OpenOptions::new()
145 .create(true)
146 .append(true)
147 .open(&path)
148 .await?;
149
150 let mut file: Box<dyn AsyncWrite + Unpin + Send> = match compression {
151 Compression::Gzip => Box::new(GzipEncoder::new(file)),
152 Compression::Bzip2 => Box::new(BzEncoder::new(file)),
153 Compression::Xz => Box::new(XzEncoder::new(file)),
154 Compression::Zstd => Box::new(ZstdEncoder::new(file)),
155 Compression::None => Box::new(BufWriter::new(file)),
160 };
161
162 contents.write_to(&mut file).await?;
163 file.shutdown().await?;
164
165 Ok(ControlFlow::Continue)
166}
167
168pub async fn run_delete(
169 mut cmd: BuiltinCommand,
170 state: &State,
171) -> Result<ControlFlow, anyhow::Error> {
172 let path = build_path(state, &mut cmd)?;
173 cmd.args.done()?;
174 println!("Deleting file {}", path.display());
175 fs::remove_file(&path).await?;
176 Ok(ControlFlow::Continue)
177}