gloo_file/
blob.rs

1use crate::Sealed;
2use std::{
3    ops::Deref,
4    time::{Duration, SystemTime, UNIX_EPOCH},
5};
6
7use wasm_bindgen::{prelude::*, throw_str, JsCast};
8
9/// This trait is used to overload the `Blob::new_with_options` function, allowing a variety of
10/// types to be used to create a `Blob`. Ignore this, and use &\[u8], &str, etc to create a `Blob`.
11///
12/// The trait is sealed: it can only be implemented by types in this
13/// crate, as this crate relies on invariants regarding the `JsValue` returned from `into_jsvalue`.
14pub trait BlobContents: Sealed {
15    /// # Safety
16    ///
17    /// For `&[u8]` and `&str`, the returned `Uint8Array` must be modified,
18    /// and must not be kept past the lifetime of the original slice.
19    unsafe fn into_jsvalue(self) -> JsValue;
20}
21
22impl<'a> Sealed for &'a str {}
23impl<'a> BlobContents for &'a str {
24    unsafe fn into_jsvalue(self) -> JsValue {
25        // Converting a Rust string to a JS string re-encodes it from UTF-8 to UTF-16,
26        // and `Blob` re-encodes JS strings from UTF-16 to UTF-8.
27        // So, it's better to just pass the original bytes of the Rust string to `Blob`
28        // and avoid the round trip through UTF-16.
29        self.as_bytes().into_jsvalue()
30    }
31}
32
33impl<'a> Sealed for &'a [u8] {}
34impl<'a> BlobContents for &'a [u8] {
35    unsafe fn into_jsvalue(self) -> JsValue {
36        js_sys::Uint8Array::view(self).into()
37    }
38}
39
40impl Sealed for js_sys::ArrayBuffer {}
41impl BlobContents for js_sys::ArrayBuffer {
42    unsafe fn into_jsvalue(self) -> JsValue {
43        self.into()
44    }
45}
46
47impl Sealed for js_sys::JsString {}
48impl BlobContents for js_sys::JsString {
49    unsafe fn into_jsvalue(self) -> JsValue {
50        self.into()
51    }
52}
53
54impl Sealed for Blob {}
55impl BlobContents for Blob {
56    unsafe fn into_jsvalue(self) -> JsValue {
57        self.into()
58    }
59}
60
61/// A [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob).
62///
63/// `Blob`s can be created directly from `&str`, `&[u8]`, and `js_sys::ArrayBuffer`s using the
64/// `Blob::new` or `Blob::new_with_options` functions.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct Blob {
67    inner: web_sys::Blob,
68}
69
70impl Blob {
71    /// Create a new `Blob` from a `&str`, `&[u8]` or `js_sys::ArrayBuffer`.
72    pub fn new<T>(content: T) -> Blob
73    where
74        T: BlobContents,
75    {
76        Blob::new_with_options(content, None)
77    }
78
79    /// Like `new`, but allows specifying the MIME type (also known as *content type* or *media
80    /// type*) of the `Blob`.
81    pub fn new_with_options<T>(content: T, mime_type: Option<&str>) -> Blob
82    where
83        T: BlobContents,
84    {
85        let mut properties = web_sys::BlobPropertyBag::new();
86        if let Some(mime_type) = mime_type {
87            properties.type_(mime_type);
88        }
89
90        // SAFETY: The slice will live for the duration of this function call,
91        // and `new Blob()` will not modify the bytes or keep a reference to them past the end of the call.
92        let parts = js_sys::Array::of1(&unsafe { content.into_jsvalue() });
93        let inner = web_sys::Blob::new_with_u8_array_sequence_and_options(&parts, &properties);
94
95        Blob::from(inner.unwrap_throw())
96    }
97
98    pub fn slice(&self, start: u64, end: u64) -> Self {
99        let start = safe_u64_to_f64(start);
100        let end = safe_u64_to_f64(end);
101
102        let b: &web_sys::Blob = self.as_ref();
103        Blob::from(b.slice_with_f64_and_f64(start, end).unwrap_throw())
104    }
105
106    /// The number of bytes in the Blob/File.
107    pub fn size(&self) -> u64 {
108        safe_f64_to_u64(self.inner.size())
109    }
110
111    /// The statically typed MIME type (also known as *content type* or *media type*) of the `File`
112    /// or `Blob`.
113    #[cfg(feature = "mime")]
114    pub fn mime_type(&self) -> Result<mime::Mime, mime::FromStrError> {
115        self.raw_mime_type().parse()
116    }
117
118    /// The raw MIME type (also known as *content type* or *media type*) of the `File` or
119    /// `Blob`.
120    pub fn raw_mime_type(&self) -> String {
121        self.inner.type_()
122    }
123}
124
125impl From<web_sys::Blob> for Blob {
126    fn from(blob: web_sys::Blob) -> Self {
127        Blob { inner: blob }
128    }
129}
130
131impl From<web_sys::File> for Blob {
132    fn from(file: web_sys::File) -> Self {
133        Blob { inner: file.into() }
134    }
135}
136
137impl From<Blob> for web_sys::Blob {
138    fn from(blob: Blob) -> Self {
139        blob.inner
140    }
141}
142
143impl From<Blob> for JsValue {
144    fn from(blob: Blob) -> Self {
145        blob.inner.into()
146    }
147}
148
149impl AsRef<web_sys::Blob> for Blob {
150    fn as_ref(&self) -> &web_sys::Blob {
151        self.inner.as_ref()
152    }
153}
154
155impl AsRef<JsValue> for Blob {
156    fn as_ref(&self) -> &JsValue {
157        self.inner.as_ref()
158    }
159}
160
161/// A [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File).
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct File {
164    // the trick here is that we know the contents of `inner` are a file, even though that type
165    // information is not stored. It is the same trick as is used in `web_sys`.
166    inner: Blob,
167}
168
169impl File {
170    /// Create a new `File` with the given name and contents.
171    ///
172    /// `contents` can be `&str`, `&[u8]`, or `js_sys::ArrayBuffer`.
173    pub fn new<T>(name: &str, contents: T) -> File
174    where
175        T: BlobContents,
176    {
177        Self::new_with_options(name, contents, None, None)
178    }
179
180    /// Like `File::new`, but allows customizing the MIME type (also
181    /// known as *content type* or *media type*), and the last modified time.
182    ///
183    /// `std::time::SystemTime` is a low level type, use a crate like
184    /// [`chrono`](https://docs.rs/chrono/0.4.10/chrono/) to work with a more user-friendly
185    /// representation of time.
186    ///
187    /// # Examples
188    ///
189    /// ```rust,no_run
190    /// use chrono::prelude::*;
191    /// use gloo_file::File;
192    ///
193    /// // Just create a dummy `gloo::file::File` for demonstration purposes.
194    /// let example_file = File::new_with_options(
195    ///     "motivation.txt",
196    ///     "live your best life",
197    ///     Some("text/plain"),
198    ///     Some(Utc::now().into())
199    /// );
200    /// assert_eq!(example_file.name(), String::from("motivation.txt"));
201    /// assert_eq!(example_file.raw_mime_type(), String::from("text/plain"));
202    /// ```
203    pub fn new_with_options<T>(
204        name: &str,
205        contents: T,
206        mime_type: Option<&str>,
207        last_modified_time: Option<SystemTime>,
208    ) -> File
209    where
210        T: BlobContents,
211    {
212        let mut options = web_sys::FilePropertyBag::new();
213        if let Some(mime_type) = mime_type {
214            options.type_(mime_type);
215        }
216
217        if let Some(last_modified_time) = last_modified_time {
218            let duration = match last_modified_time.duration_since(UNIX_EPOCH) {
219                Ok(duration) => safe_u128_to_f64(duration.as_millis()),
220                Err(time_err) => -safe_u128_to_f64(time_err.duration().as_millis()),
221            };
222            options.last_modified(duration);
223        }
224
225        // SAFETY: The original reference will live for the duration of this function call,
226        // and `new File()` won't mutate the `Uint8Array` or keep a reference to it past the end of this call.
227        let parts = js_sys::Array::of1(&unsafe { contents.into_jsvalue() });
228        let inner = web_sys::File::new_with_u8_array_sequence_and_options(&parts, name, &options)
229            .unwrap_throw();
230
231        File::from(inner)
232    }
233
234    /// Gets the file name.
235    pub fn name(&self) -> String {
236        let f: &web_sys::File = self.as_ref();
237        f.name()
238    }
239
240    /// Gets the time that the file was last modified.
241    ///
242    /// `std::time::SystemTime` is a low level type, use a crate like
243    /// [`chrono`](https://docs.rs/chrono/0.4.10/chrono/) to work with more user-friendly
244    /// representations of time. For example:
245    ///
246    /// ```rust,no_run
247    /// use chrono::prelude::*;
248    /// use gloo_file::File;
249    ///
250    /// // Just create a dummy `gloo::file::File` for demonstration purposes.
251    /// let example_file = File::new("test_file.txt", "<almost empty contents>");
252    /// let date: DateTime<Utc> = example_file.last_modified_time().into();
253    /// ```
254    pub fn last_modified_time(&self) -> SystemTime {
255        let f: &web_sys::File = self.as_ref();
256        match f.last_modified() {
257            pos if pos >= 0.0 => UNIX_EPOCH + Duration::from_millis(safe_f64_to_u64(pos)),
258            neg => UNIX_EPOCH - Duration::from_millis(safe_f64_to_u64(-neg)),
259        }
260    }
261
262    /// Create a new `File` from a sub-part of this `File`.
263    pub fn slice(&self, start: u64, end: u64) -> Self {
264        let blob = self.deref().slice(start, end);
265
266        let raw_mime_type = self.raw_mime_type();
267        let mime_type = if raw_mime_type.is_empty() {
268            None
269        } else {
270            Some(raw_mime_type)
271        };
272
273        File::new_with_options(
274            &self.name(),
275            blob,
276            mime_type.as_deref(),
277            Some(self.last_modified_time()),
278        )
279    }
280}
281
282impl From<web_sys::File> for File {
283    fn from(file: web_sys::File) -> Self {
284        File {
285            inner: Blob::from(web_sys::Blob::from(file)),
286        }
287    }
288}
289
290impl Deref for File {
291    type Target = Blob;
292
293    fn deref(&self) -> &Self::Target {
294        &self.inner
295    }
296}
297
298impl AsRef<web_sys::File> for File {
299    fn as_ref(&self) -> &web_sys::File {
300        <Blob as AsRef<web_sys::Blob>>::as_ref(&self.inner).unchecked_ref()
301    }
302}
303
304impl AsRef<web_sys::Blob> for File {
305    fn as_ref(&self) -> &web_sys::Blob {
306        self.inner.as_ref()
307    }
308}
309
310impl From<File> for Blob {
311    fn from(file: File) -> Self {
312        file.inner
313    }
314}
315
316// utility methods
317// ===============
318
319/// JavaScript only has `f64`, which has a maximum accurate integer size of`2^53 - 1`. So we use
320/// this to safely convert from larger integers to `f64`.  See
321/// [Number.MAX_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
322fn safe_u64_to_f64(number: u64) -> f64 {
323    // Max integer stably representable by f64
324    if number > (js_sys::Number::MAX_SAFE_INTEGER as u64) {
325        throw_str("a rust number was too large and could not be represented in JavaScript");
326    }
327    number as f64
328}
329
330fn safe_u128_to_f64(number: u128) -> f64 {
331    // Max integer stably representable by f64
332    const MAX_SAFE_INTEGER: u128 = js_sys::Number::MAX_SAFE_INTEGER as u128; // (2^53 - 1)
333    if number > MAX_SAFE_INTEGER {
334        throw_str("a rust number was too large and could not be represented in JavaScript");
335    }
336    number as f64
337}
338
339/// Like safe_u64_to_f64, but additionally checks that the number is an integer.
340fn safe_f64_to_u64(number: f64) -> u64 {
341    // Max integer stably representable by f64
342    if number > js_sys::Number::MAX_SAFE_INTEGER {
343        throw_str("a rust number was too large and could not be represented in JavaScript");
344    }
345
346    if number.fract() != 0.0 {
347        throw_str(
348            "a number could not be converted to an integer because it was not a whole number",
349        );
350    }
351    number as u64
352}