Add TOASTY_WRITE_TRUNCATE_AFTER flag for HTTP PUT semantics

Adds support for truncating files after write operations, enabling proper
HTTP PUT semantics where the entire file content should be replaced.

Implementation:
- Added TOASTY_WRITE_TRUNCATE_AFTER flag (0x02) to ToastyFS API
- Updated file_tree_write() to accept truncate_after parameter:
  * When true, sets file size to exactly offset+length
  * Removes chunks beyond the new file size
  * Marks removed chunks for garbage collection
- Metadata server extracts and passes truncate flag to file_tree_write()
- WAL replay passes false for truncate_after (already handled in original write)
- Web proxy PUT now uses both CREATE_IF_MISSING and TRUNCATE_AFTER flags
- Updated documentation in PROTOCOL.txt, DESIGN.txt, and web/DESIGN.txt

Benefits:
- Proper HTTP PUT semantics (replace entire file content)
- Efficient: single write operation truncates and updates file
- Works with both sync and async APIs
- Garbage collection automatically removes orphaned chunks
This commit is contained in:
Claude
2025-12-03 19:49:48 +00:00
parent 8160c5ac51
commit 4bcee9a617
9 changed files with 61 additions and 20 deletions
+19 -2
View File
@@ -369,7 +369,8 @@ int file_tree_write(
uint64_t* new_gen,
SHA256* hashes,
SHA256* removed_hashes,
int* num_removed)
int* num_removed,
bool truncate_after)
{
// WRITE operations cannot use expect_gen=0
if (expect_gen == NO_GENERATION)
@@ -430,8 +431,24 @@ int file_tree_write(
// Update file size (last byte written + 1)
uint64_t new_size = off + len;
if (new_size > f->file_size)
if (truncate_after) {
// With truncation, set file size to exactly new_size and remove chunks beyond
uint64_t new_num_chunks = last_chunk_index + 1;
// Add any chunks beyond the write to the overwritten list (they'll be removed)
for (uint64_t i = new_num_chunks; i < f->num_chunks; i++) {
if (num_overwritten_hashes < 100) { // Respect the limit
overwritten_hashes[num_overwritten_hashes++] = f->chunks[i];
}
}
f->num_chunks = new_num_chunks;
f->file_size = new_size;
} else {
// Without truncation, only grow the file
if (new_size > f->file_size)
f->file_size = new_size;
}
// Now check which old hashes are no longer used
// anywhere in the tree
+2 -1
View File
@@ -77,7 +77,8 @@ int file_tree_write(FileTree *ft, string path,
uint64_t *new_gen,
SHA256 *hashes,
SHA256 *removed_hashes,
int *num_removed);
int *num_removed,
bool truncate_after);
int file_tree_read(FileTree *ft, string path,
uint64_t off, uint64_t len, uint64_t *gen, uint64_t *chunk_size,
+7 -3
View File
@@ -634,13 +634,17 @@ process_client_write(MetadataServer *state, int conn_idx, ByteView msg)
assert(0); // TODO
}
// Extract flag values
#define TOASTY_WRITE_CREATE_IF_MISSING (1 << 0)
#define TOASTY_WRITE_TRUNCATE_AFTER (1 << 1)
bool truncate_after = (flags & TOASTY_WRITE_TRUNCATE_AFTER) != 0;
uint64_t new_gen;
int ret = file_tree_write(&state->file_tree, path, offset, length,
num_chunks, expect_gen, &new_gen, new_hashes, removed_hashes, &num_removed);
num_chunks, expect_gen, &new_gen, new_hashes, removed_hashes, &num_removed, truncate_after);
// If write failed because file doesn't exist and CREATE_IF_MISSING flag is set,
// create the file and retry the write
#define TOASTY_WRITE_CREATE_IF_MISSING (1 << 0)
if (ret == FILETREE_NOENT && (flags & TOASTY_WRITE_CREATE_IF_MISSING)) {
// Create the file with default chunk size of 4096 bytes
uint64_t chunk_size = 4096;
@@ -656,7 +660,7 @@ process_client_write(MetadataServer *state, int conn_idx, ByteView msg)
if (create_ret == 0) {
// File created successfully, retry the write with the new generation
ret = file_tree_write(&state->file_tree, path, offset, length,
num_chunks, create_gen, &new_gen, new_hashes, removed_hashes, &num_removed);
num_chunks, create_gen, &new_gen, new_hashes, removed_hashes, &num_removed, truncate_after);
}
}
+2 -1
View File
@@ -473,7 +473,8 @@ int wal_open(WAL *wal, FileTree *file_tree, string file_path, int entry_limit)
file_tree_delete_entity(file_tree, entry.path, entry.expect_gen);
break;
case WAL_ENTRY_WRITE:
file_tree_write(file_tree, entry.path, entry.offset, entry.length, entry.num_chunks, entry.expect_gen, &gen, entry.hashes, NULL, NULL);
// WAL replay: use false for truncate_after since truncation was already handled
file_tree_write(file_tree, entry.path, entry.offset, entry.length, entry.num_chunks, entry.expect_gen, &gen, entry.hashes, NULL, NULL, false);
break;
default:
UNREACHABLE;