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 dry_run: bool,
23}
24
25impl LocalStore {
26 pub async fn compress(&self, ns: &Namespace, dry_run: bool) -> Result<CompressReport, Error> {
29 let level = self.compression.ok_or(Error::CompressionDisabled)?;
30
31 let mut report = CompressReport {
32 dry_run,
33 ..CompressReport::default()
34 };
35
36 let Ok(mut prefixes) = fs::read_dir(self.root.join(ns.org()).join(ns.repo())).await else {
37 return Ok(report);
38 };
39
40 while let Some(prefix) = prefixes.next_entry().await? {
41 let Ok(mut fanouts) = fs::read_dir(prefix.path()).await else {
42 continue;
43 };
44
45 while let Some(fanout) = fanouts.next_entry().await? {
46 self.compress_directory(&fanout.path(), level, &mut report)
47 .await?;
48 }
49 }
50
51 if !dry_run && report.compressed > 0 {
52 self.forget(ns).await;
53 }
54
55 Ok(report)
56 }
57
58 async fn compress_directory(
59 &self,
60 directory: &Path,
61 level: i32,
62 report: &mut CompressReport,
63 ) -> Result<(), Error> {
64 let Ok(mut entries) = fs::read_dir(directory).await else {
65 return Ok(());
66 };
67
68 while let Some(entry) = entries.next_entry().await? {
69 let oid = entry.file_name().to_string_lossy().into_owned();
70 if Self::validate_oid(&oid).is_err() {
71 continue;
72 }
73
74 report.inspected += 1;
75 let path = entry.path();
76 let on_disk = entry.metadata().await?.len();
77 report.before += on_disk;
78
79 if self.is_framed(&path, on_disk).await? {
80 report.already += 1;
81 report.after += on_disk;
82 continue;
83 }
84
85 report.after += self
86 .compress_object(&path, &oid, on_disk, level, report)
87 .await?;
88 }
89
90 Ok(())
91 }
92
93 async fn is_framed(&self, path: &Path, on_disk: u64) -> Result<bool, Error> {
94 let file = fs::File::open(path).await?;
95
96 Ok(codec::Framed::open(file, on_disk).await?.is_some())
97 }
98
99 async fn compress_object(
100 &self,
101 path: &Path,
102 oid: &str,
103 on_disk: u64,
104 level: i32,
105 report: &mut CompressReport,
106 ) -> Result<u64, Error> {
107 let parent = path.parent().expect("objects live in a fanout directory");
108 let staged = self.staging_path(parent, oid);
109
110 let (digest, compressed) = match self.rewrite(path, &staged, level).await {
111 Ok(measured) => measured,
112 Err(error) => {
113 let _ = fs::remove_file(&staged).await;
114 return Err(error);
115 }
116 };
117
118 if digest != oid {
122 tracing::warn!(oid, %digest, "object does not hash to its own name, leaving it alone");
123 let _ = fs::remove_file(&staged).await;
124 report.refused += 1;
125 return Ok(on_disk);
126 }
127
128 if compressed >= on_disk || report.dry_run {
131 let _ = fs::remove_file(&staged).await;
132 if compressed >= on_disk {
133 report.left_alone += 1;
134 return Ok(on_disk);
135 }
136
137 report.compressed += 1;
138 return Ok(compressed);
139 }
140
141 self.swap_in(path, &staged, oid).await?;
142 report.compressed += 1;
143
144 Ok(compressed)
145 }
146
147 async fn swap_in(&self, path: &Path, staged: &Path, oid: &str) -> Result<(), Error> {
153 let content = self.content_path(oid);
154
155 if !shares_bytes_with(path, &content).await {
156 return Ok(fs::rename(staged, path).await?);
157 }
158
159 fs::rename(staged, &content).await?;
160
161 let parent = path.parent().expect("objects live in a fanout directory");
162 let relink = self.staging_path(parent, oid);
163 self.link(&content, &relink).await?;
164
165 Ok(fs::rename(&relink, path).await?)
166 }
167
168 async fn rewrite(
169 &self,
170 path: &Path,
171 staged: &Path,
172 level: i32,
173 ) -> Result<(String, u64), Error> {
174 let mut source = fs::File::open(path).await?;
175 let mut writer = codec::Writer::open(fs::File::create(staged).await?, level).await?;
176 let mut hasher = Sha256::new();
177 let mut buffer = vec![0u8; 1024 * 1024];
178
179 loop {
180 let read = source.read(&mut buffer).await?;
181 if read == 0 {
182 break;
183 }
184
185 hasher.update(&buffer[..read]);
186 writer.push(&buffer[..read]).await?;
187 }
188
189 writer.finish().await?;
190
191 Ok((
192 hex::encode(hasher.finalize()),
193 fs::metadata(staged).await?.len(),
194 ))
195 }
196}
197
198#[cfg(test)]
199mod tests;