Add read_file_as_chunks method to ReaderService

To read large files by small pieces.
This commit is contained in:
Denis Kolodin 2019-02-17 23:33:35 +03:00
parent 797a43bb12
commit c63636b22a
3 changed files with 125 additions and 21 deletions

View File

@ -11,7 +11,8 @@
- Added handling of files of `input` element. There is new `ChangeData::Files` variant
of `onchange` handler.
- Added `ReaderService` to read data from `File` instances.
- Added `ReaderService` to read data from `File` instances. It supports two methods: `read_file`
to read an entire file at a time and `read_file_by_chunks` to read a file by small pieces of data.
- New example `file_upload` that prints sizes of uploaded files.

View File

@ -1,16 +1,21 @@
use yew::{html, ChangeData, Component, ComponentLink, Html, Renderable, ShouldRender};
use yew::services::reader::{File, FileData, ReaderService, ReaderTask};
use yew::services::reader::{File, FileChunk, FileData, ReaderService, ReaderTask};
pub struct Model {
link: ComponentLink<Model>,
reader: ReaderService,
tasks: Vec<ReaderTask>,
files: Vec<FileData>,
files: Vec<String>,
by_chunks: bool,
}
type Chunks = bool;
pub enum Msg {
Loaded(FileData),
Files(Vec<File>),
Chunk(FileChunk),
Files(Vec<File>, Chunks),
ToggleByChunks,
}
impl Component for Model {
@ -23,21 +28,37 @@ impl Component for Model {
link,
tasks: vec![],
files: vec![],
by_chunks: false,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Loaded(file) => {
self.files.push(file);
let info = format!("file: {:?}", file);
self.files.push(info);
}
Msg::Files(files) => {
Msg::Chunk(chunk) => {
let info = format!("chunk: {:?}", chunk);
self.files.push(info);
}
Msg::Files(files, chunks) => {
for file in files.into_iter() {
let callback = self.link.send_back(Msg::Loaded);
let task = self.reader.read_file(file, callback);
let task = {
if chunks {
let callback = self.link.send_back(Msg::Chunk);
self.reader.read_file_by_chunks(file, callback, 10)
} else {
let callback = self.link.send_back(Msg::Loaded);
self.reader.read_file(file, callback)
}
};
self.tasks.push(task);
}
}
Msg::ToggleByChunks => {
self.by_chunks = !self.by_chunks;
}
}
true
}
@ -45,15 +66,22 @@ impl Component for Model {
impl Renderable<Model> for Model {
fn view(&self) -> Html<Self> {
let flag = self.by_chunks;
html! {
<div>
<input type="file", multiple=true, onchange=|value| {
let mut result = Vec::new();
if let ChangeData::Files(files) = value {
result.extend(files);
}
Msg::Files(result)
},/>
<div>
<input type="file", multiple=true, onchange=|value| {
let mut result = Vec::new();
if let ChangeData::Files(files) = value {
result.extend(files);
}
Msg::Files(result, flag)
},/>
</div>
<div>
<label>{ "By chunks" }</label>
<input type="checkbox", checked=flag, onclick=|_| Msg::ToggleByChunks, />
</div>
<ul>
{ for self.files.iter().map(|f| self.view_file(f)) }
</ul>
@ -63,9 +91,9 @@ impl Renderable<Model> for Model {
}
impl Model {
fn view_file(&self, file: &FileData) -> Html<Self> {
fn view_file(&self, data: &str) -> Html<Self> {
html! {
<li>{ format!("file: {}, size: {}", file.name, file.content.len()) }</li>
<li>{ data }</li>
}
}
}

View File

@ -1,6 +1,7 @@
//! Service to load files using `FileReader`.
pub use stdweb::web::File;
use std::cmp;
pub use stdweb::web::{Blob, IBlob, File};
use stdweb::web::{
IEventTarget,
FileReader,
@ -11,10 +12,11 @@ use stdweb::web::{
use stdweb::web::event::{
LoadEndEvent,
};
use stdweb::unstable::TryInto;
use callback::Callback;
use super::Task;
/// Struct that represents data of file.
/// Struct that represents data of a file.
#[derive(Clone, Debug)]
pub struct FileData {
/// Name of loaded file.
@ -23,6 +25,25 @@ pub struct FileData {
pub content: Vec<u8>,
}
/// Struct that represents a chunk of a file.
#[derive(Clone, Debug)]
pub enum FileChunk {
/// Reading of chunks started. Equals **0%** progress.
Started {
/// Name of loaded file.
name: String,
},
/// The next data chunk that read. Also provides a progress value.
DataChunk {
/// The chunk of binary data.
data: Vec<u8>,
/// The progress value in interval: `0 < progress <= 1`.
progress: f32,
},
/// Reading of chunks finished. Equals **100%** progress.
Finished,
}
/// A reader service attached to a user context.
#[derive(Default)]
pub struct ReaderService {}
@ -33,12 +54,12 @@ impl ReaderService {
Self {}
}
/// Reads all bytes from files and returns them with a callback.
/// Reads all bytes from a file and returns them with a callback.
pub fn read_file(&mut self, file: File, callback: Callback<FileData>) -> ReaderTask {
let file_reader = FileReader::new();
let reader = file_reader.clone();
let name = file.name();
file_reader.add_event_listener(move |event: LoadEndEvent| {
file_reader.add_event_listener(move |_event: LoadEndEvent| {
match reader.result() {
Some(FileReaderResult::String(_)) => {
unreachable!();
@ -59,6 +80,60 @@ impl ReaderService {
file_reader,
}
}
/// Reads data chunks from a file and returns them with a callback.
pub fn read_file_by_chunks(&mut self, file: File, callback: Callback<FileChunk>, chunk_size: usize) -> ReaderTask {
let file_reader = FileReader::new();
let name = file.name();
let mut position = 0;
let total_size = file.len() as usize;
let reader = file_reader.clone();
file_reader.add_event_listener(move |_event: LoadEndEvent| {
match reader.result() {
// This branch is used to start reading
Some(FileReaderResult::String(_)) => {
let started = FileChunk::Started {
name: name.clone(),
};
callback.emit(started);
}
// This branch is used to send a chunk value
Some(FileReaderResult::ArrayBuffer(buffer)) => {
let array: TypedArray<u8> = buffer.into();
let chunk = FileChunk::DataChunk {
data: array.to_vec(),
progress: position as f32 / total_size as f32,
};
callback.emit(chunk);
}
None => { }
}
// Read the next chunk
if position < total_size {
let file = &file;
let from = position;
let to = cmp::min(position + chunk_size, total_size);
position = to;
// TODO Implement `slice` method in `stdweb`
let blob: Blob = (js! {
return @{file}.slice(@{from as u32}, @{to as u32});
})
.try_into()
.unwrap();
reader.read_as_array_buffer(&blob).unwrap();
} else {
let finished = FileChunk::Finished;
callback.emit(finished);
}
});
let blob: Blob = (js! {
return (new Blob());
}).try_into().unwrap();
file_reader.read_as_text(&blob).unwrap();
ReaderTask {
file_reader,
}
}
}
/// A handle to control reading.