lfsx_server/storage/
rewrite.rs1use std::path::Path;
2
3use serde::Serialize;
4use sha2::{Digest, Sha256};
5use tokio::fs;
6use tokio::io::AsyncReadExt;
7
8use super::codec;
9use super::{LocalStore, shares_bytes_with};
10use crate::error::Error;
11use crate::namespace::Namespace;
12
13#[derive(Debug, Default, Serialize, PartialEq, Eq)]
14pub struct CompressReport {
15 pub inspected: u64,
16 pub compressed: u64,
17 pub already: u64,
18 pub left_alone: u64,
19 pub refused: u64,
20 pub before: u64,
21 pub after: u64,
22 pub incomplete: bool,
23 pub dry_run: bool,
24}
25
26impl LocalStore {
27 pub async fn compress(&self, ns: &Namespace, dry_run: bool) -> Result<CompressReport, Error> {
30 let level = self.compression.ok_or(Error::CompressionDisabled)?;
31
32 let walk = self.objects_of(ns).await;
33 let mut report = CompressReport {
34 dry_run,
35 incomplete: !walk.complete,
36 ..CompressReport::default()
37 };
38
39 for found in walk.objects {
40 report.inspected += 1;
41 let on_disk = fs::metadata(&found.path).await?.len();
42 report.before += on_disk;
43
44 if self.is_framed(&found.path, on_disk).await? {
45 report.already += 1;
46 report.after += on_disk;
47 continue;
48 }
49
50 report.after += self
51 .compress_object(&found.path, &found.oid, on_disk, level, &mut report)
52 .await?;
53 }
54
55 if !dry_run && report.compressed > 0 {
56 self.forget(ns).await;
57 }
58
59 Ok(report)
60 }
61
62 async fn is_framed(&self, path: &Path, on_disk: u64) -> Result<bool, Error> {
63 let file = fs::File::open(path).await?;
64
65 Ok(codec::Framed::open(file, on_disk).await?.is_some())
66 }
67
68 async fn compress_object(
69 &self,
70 path: &Path,
71 oid: &str,
72 on_disk: u64,
73 level: i32,
74 report: &mut CompressReport,
75 ) -> Result<u64, Error> {
76 let parent = path.parent().expect("objects live in a fanout directory");
77 let staged = self.staging_path(parent, oid);
78
79 let (digest, compressed) = match self.rewrite(path, &staged, level).await {
80 Ok(measured) => measured,
81 Err(error) => {
82 let _ = fs::remove_file(&staged).await;
83 return Err(error);
84 }
85 };
86
87 if digest != oid {
91 tracing::warn!(oid, %digest, "object does not hash to its own name, leaving it alone");
92 let _ = fs::remove_file(&staged).await;
93 report.refused += 1;
94 return Ok(on_disk);
95 }
96
97 if compressed >= on_disk || report.dry_run {
100 let _ = fs::remove_file(&staged).await;
101 if compressed >= on_disk {
102 report.left_alone += 1;
103 return Ok(on_disk);
104 }
105
106 report.compressed += 1;
107 return Ok(compressed);
108 }
109
110 self.swap_in(path, &staged, oid).await?;
111 report.compressed += 1;
112
113 Ok(compressed)
114 }
115
116 async fn swap_in(&self, path: &Path, staged: &Path, oid: &str) -> Result<(), Error> {
122 let content = self.content_path(oid);
123
124 if !shares_bytes_with(path, &content).await {
125 return Ok(fs::rename(staged, path).await?);
126 }
127
128 fs::rename(staged, &content).await?;
129
130 let parent = path.parent().expect("objects live in a fanout directory");
131 let relink = self.staging_path(parent, oid);
132 self.link(&content, &relink).await?;
133
134 Ok(fs::rename(&relink, path).await?)
135 }
136
137 async fn rewrite(
138 &self,
139 path: &Path,
140 staged: &Path,
141 level: i32,
142 ) -> Result<(String, u64), Error> {
143 let mut source = fs::File::open(path).await?;
144 let mut writer = codec::Writer::open(fs::File::create(staged).await?, level).await?;
145 let mut hasher = Sha256::new();
146 let mut buffer = vec![0u8; 1024 * 1024];
147
148 loop {
149 let read = source.read(&mut buffer).await?;
150 if read == 0 {
151 break;
152 }
153
154 hasher.update(&buffer[..read]);
155 writer.push(&buffer[..read]).await?;
156 }
157
158 writer.finish().await?;
159
160 Ok((
161 hex::encode(hasher.finalize()),
162 fs::metadata(staged).await?.len(),
163 ))
164 }
165}
166
167#[cfg(test)]
168mod tests;