posthog_cli/utils/
files.rs

1use anyhow::{Context, Result};
2use sha2::Digest;
3use std::path::PathBuf;
4use walkdir::DirEntry;
5
6use crate::sourcemaps::source_pair::SourceMapContent;
7
8pub struct SourceFile<T: SourceContent> {
9    pub path: PathBuf,
10    pub content: T,
11}
12
13impl<T: SourceContent> SourceFile<T> {
14    pub fn new(path: PathBuf, content: T) -> Self {
15        SourceFile { path, content }
16    }
17
18    pub fn load(path: &PathBuf) -> Result<Self> {
19        let content = std::fs::read(path)?;
20        Ok(SourceFile::new(path.clone(), T::parse(content)?))
21    }
22
23    // TODO - I'm fairly sure the `dest` is redundant if it's None
24    pub fn save(&self, dest: Option<PathBuf>) -> Result<()> {
25        let final_path = dest.unwrap_or(self.path.clone());
26        std::fs::write(&final_path, &self.content.serialize()?)?;
27        Ok(())
28    }
29}
30
31pub trait SourceContent {
32    fn parse(content: Vec<u8>) -> Result<Self>
33    where
34        Self: Sized;
35
36    fn serialize(&self) -> Result<Vec<u8>>;
37}
38
39impl SourceContent for String {
40    fn parse(content: Vec<u8>) -> Result<Self> {
41        Ok(String::from_utf8(content)?)
42    }
43
44    fn serialize(&self) -> Result<Vec<u8>> {
45        Ok(self.clone().into_bytes())
46    }
47}
48
49impl SourceContent for SourceMapContent {
50    fn parse(content: Vec<u8>) -> Result<Self> {
51        Ok(serde_json::from_slice(&content)?)
52    }
53
54    fn serialize(&self) -> Result<Vec<u8>> {
55        Ok(serde_json::to_vec(self)?)
56    }
57}
58
59impl SourceContent for Vec<u8> {
60    fn parse(content: Vec<u8>) -> Result<Self> {
61        Ok(content)
62    }
63
64    fn serialize(&self) -> Result<Vec<u8>> {
65        Ok(self.clone())
66    }
67}
68
69pub fn delete_files(paths: Vec<PathBuf>) -> Result<()> {
70    // Delete local sourcemaps files from the sourcepair
71    for path in paths {
72        if path.exists() {
73            std::fs::remove_file(&path)
74                .context(format!("Failed to delete file: {}", path.display()))?;
75        }
76    }
77    Ok(())
78}
79
80// TODO - find a better home for this - it's not really a "file" operation,
81// but dumping it in the general "utils" feels gross
82pub fn content_hash<Iter, Item: AsRef<[u8]>>(upload_data: Iter) -> String
83where
84    Iter: IntoIterator<Item = Item>,
85{
86    let mut hasher = sha2::Sha512::new();
87    for data in upload_data {
88        hasher.update(data.as_ref());
89    }
90    format!("{:x}", hasher.finalize())
91}
92
93pub fn is_javascript_file(entry: &DirEntry) -> bool {
94    entry.file_type().is_file()
95        && entry
96            .path()
97            .extension()
98            .is_some_and(|ext| ext == "js" || ext == "mjs" || ext == "cjs")
99}