Skip to content

Commit cb25169

Browse files
authored
Update add_named_blob to create directory under aliases atomically (#36)
* Update `add_named_blob` so blob alias dirs are created atomically * Add some comments to `add_named_blob` function * Update `add_named_blob` to copy+hash the contents in one pass
1 parent c2317e3 commit cb25169

4 files changed

Lines changed: 91 additions & 24 deletions

File tree

crates/brioche-autopack/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,16 +1043,17 @@ fn add_named_blob_from(
10431043
Path::new(filename)
10441044
};
10451045

1046-
let mut file = std::fs::File::open(path)?;
1046+
let file = std::fs::File::open(path)?;
10471047
let metadata = file.metadata()?;
10481048

10491049
let permissions = metadata.permissions();
10501050
let mode = permissions.mode();
10511051
let is_executable = mode & 0o111 != 0;
10521052

1053+
let file_reader = std::io::BufReader::new(file);
10531054
let resource_path = brioche_resources::add_named_blob(
10541055
&ctx.config.resource_dir,
1055-
&mut file,
1056+
file_reader,
10561057
is_executable,
10571058
alias_name,
10581059
)?;

crates/brioche-packer/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,10 @@ fn run_update_source(args: &UpdateSourceArgs) -> eyre::Result<()> {
240240
let new_source_permissions = new_source.metadata()?.permissions();
241241
let is_executable = is_executable(&new_source_permissions);
242242

243+
let new_source_reader = std::io::BufReader::new(new_source);
243244
let new_source_resource = brioche_resources::add_named_blob(
244245
&output_resource_dir,
245-
&new_source,
246+
new_source_reader,
246247
is_executable,
247248
new_name,
248249
)?;

crates/brioche-resources/src/lib.rs

Lines changed: 84 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -106,50 +106,114 @@ fn find_resource_dirs_from_program(
106106

107107
pub fn add_named_blob(
108108
resource_dir: &Path,
109-
mut contents: impl std::io::Seek + std::io::Read,
109+
mut contents: impl std::io::BufRead,
110110
executable: bool,
111111
name: &Path,
112112
) -> Result<PathBuf, AddBlobError> {
113-
let mut hasher = blake3::Hasher::new();
114-
std::io::copy(&mut contents, &mut hasher)?;
115-
let hash = hasher.finalize();
116-
117-
let blob_suffix = if executable { ".x" } else { "" };
118-
let blob_name = format!("{hash}{blob_suffix}");
119-
120-
contents.seek(std::io::SeekFrom::Start(0))?;
121-
113+
// Create the 'blobs' directory
122114
let blob_dir = resource_dir.join("blobs");
123-
let blob_path = blob_dir.join(&blob_name);
124-
let blob_temp_id = ulid::Ulid::new();
125-
let blob_temp_path = blob_dir.join(format!("{blob_name}-{blob_temp_id}"));
126115
std::fs::create_dir_all(&blob_dir)?;
127116

117+
// Open a temporary file to copy the contents to
118+
let blob_temp_id = ulid::Ulid::new();
119+
let blob_temp_path = blob_dir.join(blob_temp_id.to_string());
128120
let mut blob_file_options = std::fs::OpenOptions::new();
129121
blob_file_options.create_new(true).write(true);
130122
if executable {
131123
blob_file_options.mode(0o777);
132124
}
133125
let mut blob_file = blob_file_options.open(&blob_temp_path)?;
134-
std::io::copy(&mut contents, &mut blob_file)?;
126+
127+
// Read the contents, both copying it to the temporary file and hashing
128+
// as we go
129+
let mut hasher = blake3::Hasher::new();
130+
loop {
131+
let buf = contents.fill_buf()?;
132+
if buf.is_empty() {
133+
break;
134+
}
135+
136+
hasher.update(buf);
137+
blob_file.write_all(buf)?;
138+
139+
let consumed = buf.len();
140+
contents.consume(consumed);
141+
}
142+
143+
// Get the hash of the contents, which we'll use as the blob's name
144+
let hash = hasher.finalize();
145+
146+
// Get the final blob's filename. We use a suffix to distinguish identical
147+
// blobs with different permissions
148+
let blob_suffix = if executable { ".x" } else { "" };
149+
let blob_name = format!("{hash}{blob_suffix}");
150+
let blob_path = blob_dir.join(&blob_name);
151+
152+
// Rename the blob to its final path
135153
drop(blob_file);
136154
std::fs::rename(&blob_temp_path, &blob_path)?;
137155

138-
let alias_dir = resource_dir.join("aliases").join(name).join(&blob_name);
139-
std::fs::create_dir_all(&alias_dir)?;
156+
// Create a temporary directory for the alias dir
157+
let alias_temp_dir = resource_dir.join(format!("{blob_name}-{blob_temp_id}-alias"));
158+
let alias_temp_path = alias_temp_dir.join(name);
159+
std::fs::create_dir(&alias_temp_dir)?;
140160

141-
let temp_alias_path = alias_dir.join(format!("{}-{blob_temp_id}", name.display()));
142-
let alias_path = alias_dir.join(name);
161+
// Create the symlink within the temporary dir
162+
let alias_parent_dir = resource_dir.join("aliases").join(name);
163+
let alias_dir = alias_parent_dir.join(&blob_name);
143164
let blob_pack_relative_path = pathdiff::diff_paths(&blob_path, &alias_dir)
144165
.expect("blob path is not a prefix of alias path");
145-
std::os::unix::fs::symlink(blob_pack_relative_path, &temp_alias_path)?;
146-
std::fs::rename(&temp_alias_path, &alias_path)?;
166+
std::os::unix::fs::symlink(&blob_pack_relative_path, &alias_temp_path)?;
167+
168+
// Create directory for the alias dir
169+
std::fs::create_dir_all(&alias_parent_dir)?;
147170

171+
// Rename the temp dir to the final alias path. This ensures that the alias
172+
// dir itself is atomic, and never appears empty
173+
let alias_path = alias_dir.join(name);
174+
let result = std::fs::rename(&alias_temp_dir, alias_dir);
175+
match result {
176+
Ok(()) => {
177+
// Alias dir created successfully
178+
}
179+
Err(err)
180+
if err.kind() == std::io::ErrorKind::AlreadyExists
181+
|| err.kind() == std::io::ErrorKind::DirectoryNotEmpty =>
182+
{
183+
// Could not rename temp alias dir to final path. On Unix, this
184+
// means that the alias dir already exists and is non-empty
185+
186+
// Clean up the temporary dir first
187+
std::fs::remove_dir_all(&alias_temp_dir)?;
188+
189+
// Try to create the symlink again-- this time in its final path
190+
let result = std::os::unix::fs::symlink(&blob_pack_relative_path, &alias_path);
191+
match result {
192+
Ok(()) => {
193+
// Symlink created successfully. This means the alias
194+
// dir already existed and was not empty, but contained
195+
// something else? This probably shouldn't happen...
196+
}
197+
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
198+
// Path already exists, nothing to do
199+
}
200+
Err(err) => {
201+
return Err(err.into());
202+
}
203+
}
204+
}
205+
Err(err) => {
206+
return Err(err.into());
207+
}
208+
}
209+
210+
// Return the symlink alias path relative to the resource dir
148211
let alias_path = alias_path
149212
.strip_prefix(resource_dir)
150213
.expect("alias path is not in resource dir");
151214
Ok(alias_path.to_owned())
152215
}
216+
153217
pub fn add_named_resource_directory(
154218
resource_dir: &Path,
155219
source: &Path,

crates/brioche-strip/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,10 @@ fn finish_remapped_file(remapped_file: RemapFile) -> eyre::Result<()> {
438438
// Add the temp file as a new resource. We re-use the
439439
// original program's name and permissions
440440
temp_file.rewind()?;
441+
let temp_file_reader = std::io::BufReader::new(temp_file);
441442
let new_source_resource = brioche_resources::add_named_blob(
442443
&output_resource_dir,
443-
&mut temp_file,
444+
temp_file_reader,
444445
is_executable,
445446
program_name,
446447
)?;

0 commit comments

Comments
 (0)