darkwing/server/services/
local_file_services.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
use async_trait::async_trait;
use darkwing_diff::{apply, diff, Patch, DEFAULT_ALGO};
use mockall::automock;
use std::sync::Arc;
use tokio::io::AsyncReadExt;

use crate::server::error::AppResult;

pub type DynLocalFileService = Arc<dyn LocalFileServiceTrait + Send + Sync>;

#[automock]
#[async_trait]
pub trait LocalFileServiceTrait {
  async fn calc_hash_from_path(&self, path: String) -> AppResult<String>;

  async fn calc_hash(&self, mut file: tokio::fs::File) -> AppResult<String>;

  async fn calc_diff(&self, first: String, second: String) -> AppResult<Patch>;

  async fn apply_patch(
    &self,
    base: String,
    delta: Patch,
    applied_path: String,
  ) -> AppResult<()>;
}

#[derive(Clone)]
pub struct LocalFileService {}

impl LocalFileService {
  pub fn new() -> Self {
    Self {}
  }

  fn calc_hash_from_bytes(&self, data: &[u8]) -> String {
    let hash = md5::compute(data);
    hex::encode(hash.0)
  }
}

#[async_trait]
impl LocalFileServiceTrait for LocalFileService {
  async fn calc_hash_from_path(&self, path: String) -> AppResult<String> {
    let file = tokio::fs::File::open(path).await?;
    self.calc_hash(file).await
  }

  /// Calculates md5 hash of a file.
  async fn calc_hash(&self, mut file: tokio::fs::File) -> AppResult<String> {
    let mut data = Vec::new();
    file.read_to_end(&mut data).await?;

    let hash = self.calc_hash_from_bytes(&data);

    Ok(hash)
  }

  async fn calc_diff(&self, first: String, second: String) -> AppResult<Patch> {
    let first = std::fs::read(first)?;
    let second = std::fs::read(second)?;

    diff(&first, &second, DEFAULT_ALGO.0, DEFAULT_ALGO.1).map_err(|e| e.into())
  }

  async fn apply_patch(
    &self,
    base: String,
    delta: Patch,
    applied_path: String,
  ) -> AppResult<()> {
    let first = std::fs::read(base)?;

    let applied = apply(&first, &delta)?;

    std::fs::write(applied_path, applied)?;

    Ok(())
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use std::io::Write;
  use tempfile::NamedTempFile;
  use tokio::fs::File;

  #[tokio::test]
  async fn test_calc_hash() -> AppResult<()> {
    // Create a temporary file
    let mut temp_file = NamedTempFile::new()?;
    let test_content = b"Hello, world!";
    temp_file.write_all(test_content)?;
    temp_file.flush()?;

    // Open the file asynchronously
    let file = File::open(temp_file.path()).await?;

    // Create LocalFileService instance
    let service = LocalFileService::new();

    // Calculate hash
    let hash = service.calc_hash(file).await?;

    // Expected MD5 hash for "Hello, world!"
    let expected_hash = "6cd3556deb0da54bca060b4c39479839";

    assert_eq!(hash, expected_hash);

    Ok(())
  }

  #[tokio::test]
  async fn test_calc_hash_empty_file() -> AppResult<()> {
    // Create an empty temporary file
    let temp_file = NamedTempFile::new()?;

    // Open the file asynchronously
    let file = File::open(temp_file.path()).await?;

    // Create LocalFileService instance
    let service = LocalFileService::new();

    // Calculate hash
    let hash = service.calc_hash(file).await?;

    // Expected MD5 hash for an empty file
    let expected_hash = "d41d8cd98f00b204e9800998ecf8427e";

    assert_eq!(hash, expected_hash);

    Ok(())
  }

  #[tokio::test]
  async fn test_calc_hash_with_special_characters() -> AppResult<()> {
    let mut temp_file = NamedTempFile::new()?;
    let test_content = "Hello, 世界! Special chars: !@#$%^&*()".as_bytes();
    temp_file.write_all(test_content)?;
    temp_file.flush()?;

    let service = LocalFileService::new();
    let hash = service
      .calc_hash(File::open(temp_file.path()).await?)
      .await?;

    // Verify hash is generated and has correct format
    assert_eq!(hash.len(), 32);
    assert!(hex::decode(&hash).is_ok());

    Ok(())
  }
}