Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/runtime/s3.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,8 @@ Like `Bun.file()`, `S3File` extends [`Blob`](https://developer.mozilla.org/en-US

Because `S3File` extends `Blob`, `S3File` instances work with `fetch()`, `Response`, and other web APIs that accept `Blob` instances.

Structured cloning is not supported. An `S3File` is bound to the credentials and options of the client that created it, so `structuredClone()` and `postMessage()` throw a `DataCloneError`. To use a file from a worker, send the key and call `file()` on a client in the worker.

### Partial reads with `slice`

To read a partial range of a file, use the `slice` method.
Expand Down
59 changes: 36 additions & 23 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,14 @@ pub(crate) trait BlobExt {
cx: &bun_jsc::JsThread<'_>,
);
fn get_content_type(&self) -> Option<Utf8Bytes<'_>>;
fn _on_structured_clone_serialize<W: bun_io::Write>(&self, writer: &mut W)
-> crate::Result<()>;
fn _on_structured_clone_serialize<W: bun_io::Write>(
&self,
store_tag: store::SerializeTag,
writer: &mut W,
) -> crate::Result<()>;
fn on_structured_clone_serialize(
&self,
_global_this: &JSGlobalObject,
global_this: &JSGlobalObject,
ctx: *mut c_void,
write_bytes: crate::generated_classes::WriteBytesFn,
);
Expand Down Expand Up @@ -703,13 +706,10 @@ impl BlobExt for Blob {
}
fn _on_structured_clone_serialize<W: bun_io::Write>(
&self,
store_tag: store::SerializeTag,
writer: &mut W,
) -> crate::Result<()> {
let is_memory_backed = if let Some(store) = self.store.get() {
matches!(store.data, store::Data::Bytes(_))
} else {
false
};
let is_memory_backed = matches!(store_tag, store::SerializeTag::Bytes);

writer.write_int_le::<u8>(SERIALIZATION_VERSION)?;
writer.write_int_le::<u64>(if is_memory_backed {
Expand All @@ -723,16 +723,6 @@ impl BlobExt for Blob {
writer.write_all(ct)?;
writer.write_int_le::<u8>(self.content_type_was_set.get() as u8)?;

let store_tag: store::SerializeTag = if let Some(store) = self.store.get() {
if matches!(store.data, store::Data::File(_)) {
store::SerializeTag::File
} else {
store::SerializeTag::Bytes
}
} else {
store::SerializeTag::Empty
};

writer.write_int_le::<u8>(store_tag as u8)?;

if let Some(store) = self.store.get() {
Expand All @@ -750,7 +740,8 @@ impl BlobExt for Blob {
// on the wire and the receiver stats it locally, like v3.
writer.write_int_le::<u64>(self.size.get())?;
self.resolve_size();
store.serialize(writer)?;
// resolve_size() writes into this store: borrow the File only after it.
store.data.as_file().serialize(writer)?;
}
}

Expand All @@ -773,15 +764,25 @@ impl BlobExt for Blob {

fn on_structured_clone_serialize(
&self,
_global_this: &JSGlobalObject,
global_this: &JSGlobalObject,
ctx: *mut c_void,
write_bytes: crate::generated_classes::WriteBytesFn,
) {
let Some(store_tag) = structured_clone_store_tag(self) else {
// CloneSerializer runs RETURN_IF_EXCEPTION right after this hook returns.
let _ = global_this.throw_dom_exception(
bun_jsc::DOMExceptionCode::DataCloneError,
format_args!(
"S3File cannot be cloned: it is bound to the credentials and options of the S3Client that created it. Pass the key instead and open it again with s3.file() where it is needed."
Comment thread
robobun marked this conversation as resolved.
Resolved
),
);
return;
};
let mut writer = StructuredCloneWriter {
ctx,
impl_: write_bytes,
};
let _ = self._on_structured_clone_serialize(&mut writer);
let _ = self._on_structured_clone_serialize(store_tag, &mut writer);
}

fn on_structured_clone_deserialize(
Expand Down Expand Up @@ -3763,6 +3764,18 @@ impl bun_io::Write for StructuredCloneWriter {
}
}

/// `None` for an S3 store, whatever wraps it: the record cannot carry the client's credentials.
fn structured_clone_store_tag(blob: &Blob) -> Option<store::SerializeTag> {
let Some(store) = blob.store() else {
return Some(store::SerializeTag::Empty);
};
match store.data.tag() {
store::DataTag::Bytes => Some(store::SerializeTag::Bytes),
store::DataTag::File => Some(store::SerializeTag::File),
store::DataTag::S3 => None,
}
}

// Only ever called with f64 (Blob.last_modified). A concrete impl
// because Rust forbids `[u8; size_of::<F>()]`
// without `generic_const_exprs`. Bit-cast → native-endian bytes.
Expand Down Expand Up @@ -3870,7 +3883,7 @@ fn on_structured_clone_deserialize<B: AsRef<[u8]>>(
break 'file Blob::new(Blob::find_or_create_file_from_path(
&mut path_or_fd,
global_this,
true,
false,
));
}
PathOrFileDescriptorSerializeTag::Path => {
Expand All @@ -3886,7 +3899,7 @@ fn on_structured_clone_deserialize<B: AsRef<[u8]>>(
break 'file Blob::new(Blob::find_or_create_file_from_path(
&mut dest,
global_this,
true,
false,
));
}
}
Expand Down
72 changes: 25 additions & 47 deletions src/runtime/webcore/blob/Store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ pub(crate) trait StoreExt {
fn init_mmap(slice: &'static mut [u8]) -> RefPtr<Store>
where
Self: Sized;
fn serialize(&self, writer: &mut impl bun_io::Write) -> Result<(), crate::Error>;
}

pub(crate) trait S3Ext {
Expand Down Expand Up @@ -86,6 +85,8 @@ pub(crate) trait S3Ext {

pub(crate) trait FileExt {
fn unlink(&self, cx: &bun_jsc::JsThread<'_>) -> JsResult<JSValue>;
/// Body of a `SerializeTag::File` structured-clone record.
fn serialize(&self, writer: &mut impl bun_io::Write) -> Result<(), crate::Error>;
}

pub(crate) trait BytesExt {
Expand Down Expand Up @@ -169,52 +170,6 @@ impl StoreExt for Store {
is_all_ascii: IsAllAscii::default(),
})
}

fn serialize(&self, writer: &mut impl bun_io::Write) -> Result<(), crate::Error> {
match &self.data {
Data::File(file) => {
let pathlike_tag: PathOrFileDescriptorSerializeTag =
if matches!(file.pathlike, PathOrFileDescriptor::Fd(_)) {
PathOrFileDescriptorSerializeTag::Fd
} else {
PathOrFileDescriptorSerializeTag::Path
};
writer.write_int_le::<u8>(pathlike_tag as u8)?;

match &file.pathlike {
PathOrFileDescriptor::Fd(fd) => {
// Write the raw bytes of the FD wrapper. `bun_sys::Fd` is
// `#[repr(transparent)]` over an integer (`i32` posix /
// `u64` windows), so its native-endian byte image is
// exactly the inner field's `to_ne_bytes()`.
writer.write_all(&fd.0.to_ne_bytes())?;
}
PathOrFileDescriptor::Path(path) => {
let path_slice = path.slice();
writer.write_int_le::<u32>(path_slice.len() as u32)?;
writer.write_all(path_slice)?;
}
}
}
Data::S3(s3) => {
let pathlike_tag = PathOrFileDescriptorSerializeTag::Path;
writer.write_int_le::<u8>(pathlike_tag as u8)?;

let path_slice = s3.pathlike.slice();
writer.write_int_le::<u32>(path_slice.len() as u32)?;
writer.write_all(path_slice)?;
}
Data::Bytes(bytes) => {
let slice = bytes.slice();
writer.write_int_le::<u32>(slice.len() as u32)?;
writer.write_all(slice)?;

writer.write_int_le::<u32>(bytes.stored_name.len() as u32)?;
writer.write_all(&bytes.stored_name)?;
}
}
Ok(())
}
}

impl FileExt for File {
Expand Down Expand Up @@ -244,6 +199,29 @@ impl FileExt for File {
)),
}
}

fn serialize(&self, writer: &mut impl bun_io::Write) -> Result<(), crate::Error> {
let pathlike_tag: PathOrFileDescriptorSerializeTag =
if matches!(self.pathlike, PathOrFileDescriptor::Fd(_)) {
PathOrFileDescriptorSerializeTag::Fd
} else {
PathOrFileDescriptorSerializeTag::Path
};
writer.write_int_le::<u8>(pathlike_tag as u8)?;

match &self.pathlike {
PathOrFileDescriptor::Fd(fd) => {
// `Fd` is `#[repr(transparent)]`; read back with `read_struct::<Fd>()`.
writer.write_all(&fd.0.to_ne_bytes())?;
}
PathOrFileDescriptor::Path(path) => {
let path_slice = path.slice();
writer.write_int_le::<u32>(path_slice.len() as u32)?;
writer.write_all(path_slice)?;
}
}
Ok(())
}
}

impl S3Ext for S3 {
Expand Down
Loading
Loading