protoflow_blocks/blocks/sys/
write_file.rs

1// This is free and unencumbered software released into the public domain.
2
3extern crate std;
4
5use crate::{
6    prelude::{vec, Bytes, String},
7    StdioConfig, StdioError, StdioSystem, System,
8};
9use protoflow_core::{Block, BlockResult, BlockRuntime, InputPort};
10use protoflow_derive::Block;
11use simple_mermaid::mermaid;
12
13#[derive(Clone, Copy, Debug)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub struct WriteFlags {
16    pub create: bool,
17    pub append: bool,
18}
19
20impl Default for WriteFlags {
21    fn default() -> Self {
22        Self {
23            create: true,
24            append: true,
25        }
26    }
27}
28
29/// A block that writes or appends bytes to the contents of a file.
30///
31/// # Block Diagram
32#[doc = mermaid!("../../../doc/sys/write_file.mmd")]
33///
34/// # Sequence Diagram
35#[doc = mermaid!("../../../doc/sys/write_file.seq.mmd" framed)]
36///
37/// # Examples
38///
39/// ## Using the block in a system
40///
41/// ```rust
42/// # use protoflow_blocks::*;
43/// # fn main() {
44/// System::build(|s| {
45///     // TODO
46/// });
47/// # }
48/// ```
49///
50/// ## Running the block via the CLI
51///
52/// ```console
53/// $ protoflow execute WriteFile path=/tmp/file.txt
54/// ```
55///
56#[derive(Block, Clone)]
57pub struct WriteFile {
58    /// The path to the file to write to.
59    #[input]
60    pub path: InputPort<String>,
61
62    /// The input message stream.
63    #[input]
64    pub input: InputPort<Bytes>,
65
66    #[parameter]
67    pub flags: WriteFlags,
68}
69
70impl WriteFile {
71    pub fn new(path: InputPort<String>, input: InputPort<Bytes>) -> Self {
72        Self::with_params(path, input, None)
73    }
74
75    pub fn with_params(
76        path: InputPort<String>,
77        input: InputPort<Bytes>,
78        flags: Option<WriteFlags>,
79    ) -> Self {
80        Self {
81            path,
82            input,
83            flags: flags.unwrap_or_default(),
84        }
85    }
86
87    pub fn with_system(system: &System, flags: Option<WriteFlags>) -> Self {
88        use crate::SystemBuilding;
89        Self::with_params(system.input(), system.input(), flags)
90    }
91
92    pub fn with_flags(self, flags: WriteFlags) -> Self {
93        WriteFile { flags, ..self }
94    }
95}
96
97impl Block for WriteFile {
98    fn execute(&mut self, runtime: &dyn BlockRuntime) -> BlockResult {
99        use std::io::prelude::Write;
100
101        runtime.wait_for(&self.path)?;
102
103        let Some(path) = self.path.recv()? else {
104            return Ok(());
105        };
106        let mut file = std::fs::OpenOptions::new()
107            .write(true)
108            .create(self.flags.create)
109            .append(self.flags.append)
110            .truncate(!self.flags.append)
111            .open(path)?;
112
113        while let Some(message) = self.input.recv()? {
114            file.write_all(&message)?;
115        }
116
117        drop(file);
118
119        self.input.close()?;
120        Ok(())
121    }
122}
123
124#[cfg(feature = "std")]
125impl StdioSystem for WriteFile {
126    fn build_system(config: StdioConfig) -> Result<System, StdioError> {
127        use crate::{CoreBlocks, SysBlocks, SystemBuilding};
128
129        config.allow_only(vec!["path", "create", "append"])?;
130        let path = config.get_string("path")?;
131
132        let create = config.get_opt::<bool>("create")?;
133        let append = config.get_opt::<bool>("append")?;
134
135        let default_flags = WriteFlags::default();
136
137        let flags = WriteFlags {
138            create: create.unwrap_or(default_flags.create),
139            append: append.unwrap_or(default_flags.append),
140        };
141
142        Ok(System::build(|s| {
143            let stdin = config.read_stdin(s);
144            let path_const = s.const_string(path);
145            let write_file = s.write_file().with_flags(flags);
146
147            s.connect(&path_const.output, &write_file.path);
148            s.connect(&stdin.output, &write_file.input);
149        }))
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    extern crate std;
156    use crate::{CoreBlocks, IoBlocks, SysBlocks, System, SystemBuilding, WriteFile};
157
158    #[test]
159    fn instantiate_block() {
160        // Check that the block is constructible:
161        let _ = System::build(|s| {
162            let _ = s.block(WriteFile::with_system(s, None));
163        });
164    }
165
166    #[test]
167    fn run_block() {
168        use std::{fs::File, io::Read, string::String};
169
170        let temp_dir = std::env::temp_dir();
171        let output_path = temp_dir.join("write-file-test.txt");
172
173        // ok to fail:
174        let _ = std::fs::remove_file(&output_path);
175
176        System::run(|s| {
177            let path = s.const_string(output_path.display());
178            let content = s.const_string("Hello world!");
179            let line_encoder = s.encode_lines();
180            let write_file = s.write_file();
181            s.connect(&content.output, &line_encoder.input);
182            s.connect(&path.output, &write_file.path);
183            s.connect(&line_encoder.output, &write_file.input);
184        })
185        .expect("system execution failed");
186
187        let mut file = File::open(&output_path).expect("failed to open file for system output");
188
189        let mut file_content = String::new();
190        file.read_to_string(&mut file_content)
191            .expect("failed to read system output");
192
193        assert_eq!("Hello world!\n", file_content);
194
195        drop(file);
196
197        std::fs::remove_file(&output_path).expect("failed to remove temp file");
198    }
199}