Skip to main content

rocketmq_runtime/common/
file_utils.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::io::{self};
16use std::path::Path;
17
18use crate::LocalMetadataFileSystem;
19use crate::MetadataFileSystem;
20use crate::RuntimeError;
21use crate::RuntimeOperation;
22use crate::RuntimeResult;
23use parking_lot::Mutex;
24use tracing::warn;
25
26static LOCK: Mutex<()> = Mutex::new(());
27
28/// Reads a file into a UTF-8 string.
29pub fn file_to_string(file_name: impl AsRef<Path>) -> RuntimeResult<String> {
30    let path = file_name.as_ref();
31    match std::fs::read_to_string(path) {
32        Ok(sr) => Ok(sr),
33        Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
34            warn!("file not exist: {}", path.display());
35            Ok(String::new())
36        }
37        Err(e) => Err(RuntimeError::io(RuntimeOperation::ReadFile, e)),
38    }
39}
40/// Replaces a file with UTF-8 content, keeping the previous bytes in a sibling
41/// `.bak` file.
42///
43/// Both files are written through the atomic metadata replacement protocol:
44/// a synchronized temporary file, an atomic rename, and a parent-directory
45/// synchronization where the platform supports it. Calls through this helper
46/// are serialized within the process. The call blocks, so asynchronous code
47/// runs it through a blocking lane such as
48/// [`ChildServiceContext::metadata_io`](crate::ChildServiceContext::metadata_io),
49/// or persists through a [`MetadataIoActor`](crate::MetadataIoActor).
50///
51/// # Errors
52///
53/// Returns an I/O-classified failure when the previous content cannot be read,
54/// or a metadata persistence failure when either replacement fails.
55pub fn string_to_file(str_content: &str, file_name: impl AsRef<Path>) -> RuntimeResult<()> {
56    let _lock = LOCK.lock();
57
58    let file_path = file_name.as_ref();
59    let mut bak_file = file_path.as_os_str().to_os_string();
60    bak_file.push(".bak");
61
62    // Create a backup if the file exists
63    if file_path.exists() {
64        let previous =
65            std::fs::read(file_path).map_err(|error| RuntimeError::io(RuntimeOperation::ReadFileBackup, error))?;
66        LocalMetadataFileSystem
67            .persist_atomic(Path::new(&bak_file), &previous)
68            .map_err(metadata_io_error)?;
69    }
70
71    LocalMetadataFileSystem
72        .persist_atomic(file_path, str_content.as_bytes())
73        .map_err(metadata_io_error)
74}
75
76/// Creates the metadata io error value.
77pub fn metadata_io_error(error: impl std::error::Error + Send + Sync + 'static) -> RuntimeError {
78    RuntimeError::internal(RuntimeOperation::PersistMetadata, error)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_file_to_string() {
87        // Create a temporary file for testing
88        let temp_file = tempfile::NamedTempFile::new().unwrap();
89        let file_path = temp_file.path().to_str().unwrap();
90
91        // Write some content to the file
92        let content = "Hello, World!";
93        std::fs::write(file_path, content).unwrap();
94
95        // Call the file_to_string function
96        let result = file_to_string(file_path);
97
98        // Check if the result is Ok and contains the expected content
99        assert!(result.is_ok());
100        assert_eq!(result.unwrap(), content);
101    }
102
103    #[test]
104    fn test_string_to_file() {
105        // Create a temporary file for testing
106        let temp_file = tempfile::NamedTempFile::new().unwrap();
107        let file_path = temp_file.path().to_str().unwrap();
108
109        // Call the string_to_file function
110        let content = "Hello, World!";
111        let result = string_to_file(content, file_path);
112
113        // Check if the result is Ok and the file was created with the expected content
114        assert!(result.is_ok());
115        assert_eq!(std::fs::read_to_string(file_path).unwrap(), content);
116    }
117
118    #[test]
119    fn replacing_a_file_keeps_the_previous_content_as_a_backup() {
120        let directory = tempfile::tempdir().unwrap();
121        let file_path = directory.path().join("config.json");
122        let backup_path = directory.path().join("config.json.bak");
123
124        string_to_file("first", &file_path).unwrap();
125        assert!(!backup_path.exists(), "a new file has no previous content to keep");
126
127        string_to_file("second", &file_path).unwrap();
128        assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "second");
129        assert_eq!(std::fs::read_to_string(&backup_path).unwrap(), "first");
130    }
131
132    #[test]
133    fn test_file_to_string_not_found() {
134        let result = file_to_string("/nonexistent/path/file.txt");
135        assert!(result.is_ok());
136        assert_eq!(result.unwrap(), "");
137    }
138}