Merge branch 'main' into dependencies
This commit is contained in:
commit
56bfecfe7f
9 changed files with 161 additions and 42 deletions
4
.github/workflows/rust.yml
vendored
4
.github/workflows/rust.yml
vendored
|
|
@ -28,7 +28,7 @@ jobs:
|
||||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
- run: cargo test
|
- run: cargo test --features all
|
||||||
|
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
|
|
@ -42,4 +42,4 @@ jobs:
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
components: clippy
|
components: clippy
|
||||||
- run: cargo clippy --tests -- -Dclippy::all
|
- run: cargo clippy --features all --tests -- -Dclippy::all
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["text", "binary"]
|
default = ["text", "binary"]
|
||||||
|
# contains all the parsers
|
||||||
|
all = ["text", "binary"]
|
||||||
text = ["dep:atoi_radix10"]
|
text = ["dep:atoi_radix10"]
|
||||||
binary = []
|
binary = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ pub const WEB_HOST: &str = "127.0.0.1:3000";
|
||||||
pub const IMAGE_SAVE_INTERVAL: Duration = Duration::from_secs(5);
|
pub const IMAGE_SAVE_INTERVAL: Duration = Duration::from_secs(5);
|
||||||
pub const JPEG_UPDATE_INTERVAL: Duration = Duration::from_millis(17);
|
pub const JPEG_UPDATE_INTERVAL: Duration = Duration::from_millis(17);
|
||||||
pub const WEB_UPDATE_INTERVAL: Duration = Duration::from_millis(50);
|
pub const WEB_UPDATE_INTERVAL: Duration = Duration::from_millis(50);
|
||||||
|
pub const STDOUT_STATISTICS_INTERVAL: Duration = Duration::from_millis(5000);
|
||||||
|
|
||||||
pub const HELP_TEXT: &[u8] = b"Flurry is a pixelflut implementation, this means you can use commands to get and set pixels in the canvas
|
pub const HELP_TEXT: &[u8] = b"Flurry is a pixelflut implementation, this means you can use commands to get and set pixels in the canvas
|
||||||
SIZE returns the size of the canvas
|
SIZE returns the size of the canvas
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use crate::{
|
||||||
grid::{self, Flut},
|
grid::{self, Flut},
|
||||||
increment_counter,
|
increment_counter,
|
||||||
protocols::{BinaryParser, IOProtocol, Parser, Responder, TextParser},
|
protocols::{BinaryParser, IOProtocol, Parser, Responder, TextParser},
|
||||||
set_pixel_rgba, Canvas, Color, Command, Coordinate, Protocol, Response,
|
set_pixel_rgba, Canvas, Color, Command, Coordinate, Protocol, ProtocolStatus, Response,
|
||||||
};
|
};
|
||||||
|
|
||||||
macro_rules! build_parser_type_enum {
|
macro_rules! build_parser_type_enum {
|
||||||
|
|
@ -26,16 +26,27 @@ macro_rules! build_parser_type_enum {
|
||||||
|
|
||||||
impl std::default::Default for ParserTypes {
|
impl std::default::Default for ParserTypes {
|
||||||
// add code here
|
// add code here
|
||||||
|
#[allow(unreachable_code)]
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
$(
|
$(
|
||||||
#[cfg(feature = $feat)]
|
#[cfg(feature = $feat)]
|
||||||
#[allow(unreachable_code)]
|
|
||||||
return ParserTypes::$name(<$t>::default());
|
return ParserTypes::$name(<$t>::default());
|
||||||
)*
|
)*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParserTypes {
|
impl ParserTypes {
|
||||||
|
pub fn get_status() -> Vec<ProtocolStatus> {
|
||||||
|
vec![
|
||||||
|
$(
|
||||||
|
#[cfg(feature = $feat)]
|
||||||
|
ProtocolStatus::Enabled($feat),
|
||||||
|
#[cfg(not(feature = $feat))]
|
||||||
|
ProtocolStatus::Disabled($feat),
|
||||||
|
)*
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn announce() {
|
pub fn announce() {
|
||||||
$(
|
$(
|
||||||
#[cfg(feature = $feat)]
|
#[cfg(feature = $feat)]
|
||||||
|
|
@ -88,6 +99,14 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn protocols_command(&mut self) -> io::Result<()> {
|
||||||
|
match_parser! {
|
||||||
|
parser: self.parser => parser.unparse(Response::Protocols(ParserTypes::get_status()), &mut self.writer).await?
|
||||||
|
};
|
||||||
|
self.writer.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn size_command(&mut self, canvas: Canvas) -> io::Result<()> {
|
async fn size_command(&mut self, canvas: Canvas) -> io::Result<()> {
|
||||||
let (x, y) = self.grids[canvas as usize].get_size();
|
let (x, y) = self.grids[canvas as usize].get_size();
|
||||||
match_parser!(parser: self.parser => parser.unparse(
|
match_parser!(parser: self.parser => parser.unparse(
|
||||||
|
|
@ -166,6 +185,7 @@ where
|
||||||
match parsed {
|
match parsed {
|
||||||
Ok(Command::Help) => self.help_command().await?,
|
Ok(Command::Help) => self.help_command().await?,
|
||||||
Ok(Command::Size(canvas)) => self.size_command(canvas).await?,
|
Ok(Command::Size(canvas)) => self.size_command(canvas).await?,
|
||||||
|
Ok(Command::Protocols) => self.protocols_command().await?,
|
||||||
Ok(Command::GetPixel(canvas, x, y)) => self.get_pixel_command(canvas, x, y).await?,
|
Ok(Command::GetPixel(canvas, x, y)) => self.get_pixel_command(canvas, x, y).await?,
|
||||||
Ok(Command::SetPixel(canvas, x, y, color)) => self.set_pixel_command(canvas, x, y, &color),
|
Ok(Command::SetPixel(canvas, x, y, color)) => self.set_pixel_command(canvas, x, y, &color),
|
||||||
Ok(Command::ChangeCanvas(canvas)) => {
|
Ok(Command::ChangeCanvas(canvas)) => {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,12 @@ fn increment_counter(amount: u64) {
|
||||||
COUNTER.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
|
COUNTER.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum ProtocolStatus {
|
||||||
|
Enabled(&'static str),
|
||||||
|
Disabled(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Protocol {
|
pub enum Protocol {
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -62,6 +68,7 @@ pub enum Protocol {
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Help,
|
Help,
|
||||||
|
Protocols,
|
||||||
Size(Canvas),
|
Size(Canvas),
|
||||||
GetPixel(Canvas, Coordinate, Coordinate),
|
GetPixel(Canvas, Coordinate, Coordinate),
|
||||||
SetPixel(Canvas, Coordinate, Coordinate, Color),
|
SetPixel(Canvas, Coordinate, Coordinate, Color),
|
||||||
|
|
@ -72,6 +79,7 @@ pub enum Command {
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Response {
|
pub enum Response {
|
||||||
Help,
|
Help,
|
||||||
|
Protocols(Vec<ProtocolStatus>),
|
||||||
Size(Coordinate, Coordinate),
|
Size(Coordinate, Coordinate),
|
||||||
GetPixel(Coordinate, Coordinate, [u8; 3]),
|
GetPixel(Coordinate, Coordinate, [u8; 3]),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use flurry::{
|
use flurry::{
|
||||||
config::{GRID_LENGTH, HOST, IMAGE_SAVE_INTERVAL, JPEG_UPDATE_INTERVAL},
|
config::{
|
||||||
|
GRID_LENGTH, HOST, IMAGE_SAVE_INTERVAL, JPEG_UPDATE_INTERVAL, STDOUT_STATISTICS_INTERVAL,
|
||||||
|
},
|
||||||
flutclient::{FlutClient, ParserTypes},
|
flutclient::{FlutClient, ParserTypes},
|
||||||
grid::{self, Flut},
|
grid::{self, Flut},
|
||||||
webapi::WebApiContext,
|
webapi::WebApiContext,
|
||||||
|
|
@ -20,7 +22,7 @@ use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _
|
||||||
|
|
||||||
/// This function logs the current amount of changed pixels to stdout every second
|
/// This function logs the current amount of changed pixels to stdout every second
|
||||||
async fn pixel_change_stdout_log() -> AsyncResult<Never> {
|
async fn pixel_change_stdout_log() -> AsyncResult<Never> {
|
||||||
let mut interval = tokio::time::interval(Duration::from_millis(1000));
|
let mut interval = tokio::time::interval(STDOUT_STATISTICS_INTERVAL);
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
let cnt = COUNTER.load(std::sync::atomic::Ordering::Relaxed);
|
let cnt = COUNTER.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::{Canvas, Color, Command, Response};
|
||||||
use super::{IOProtocol, Parser, Responder};
|
use super::{IOProtocol, Parser, Responder};
|
||||||
|
|
||||||
const SIZE_BIN: u8 = 115;
|
const SIZE_BIN: u8 = 115;
|
||||||
|
const PROTOCOLS_BIN: u8 = 116;
|
||||||
const HELP_BIN: u8 = 104;
|
const HELP_BIN: u8 = 104;
|
||||||
const GET_PX_BIN: u8 = 32;
|
const GET_PX_BIN: u8 = 32;
|
||||||
const SET_PX_RGB_BIN: u8 = 128;
|
const SET_PX_RGB_BIN: u8 = 128;
|
||||||
|
|
@ -22,6 +23,7 @@ impl<R: AsyncBufRead + AsyncBufReadExt + std::marker::Unpin> Parser<R> for Binar
|
||||||
match fst {
|
match fst {
|
||||||
Ok(command) => match command {
|
Ok(command) => match command {
|
||||||
HELP_BIN => Ok(Command::Help),
|
HELP_BIN => Ok(Command::Help),
|
||||||
|
PROTOCOLS_BIN => Ok(Command::Protocols),
|
||||||
SIZE_BIN => {
|
SIZE_BIN => {
|
||||||
let canvas = reader.read_u8().await?;
|
let canvas = reader.read_u8().await?;
|
||||||
Ok(Command::Size(canvas))
|
Ok(Command::Size(canvas))
|
||||||
|
|
@ -106,6 +108,23 @@ To set a pixel using RGB, use ({SET_PX_RGB_BIN:02X}) (u8 canvas) (x as u16_le) (
|
||||||
);
|
);
|
||||||
writer.write_all(help_text.as_bytes()).await
|
writer.write_all(help_text.as_bytes()).await
|
||||||
}
|
}
|
||||||
|
Response::Protocols(protos) => {
|
||||||
|
for protocol in protos {
|
||||||
|
match protocol {
|
||||||
|
crate::ProtocolStatus::Enabled(proto) => {
|
||||||
|
writer
|
||||||
|
.write_all(format!("Enabled: {}\n", proto).as_bytes())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
crate::ProtocolStatus::Disabled(proto) => {
|
||||||
|
writer
|
||||||
|
.write_all(format!("Disabled: {}\n", proto).as_bytes())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Response::Size(x, y) => {
|
Response::Size(x, y) => {
|
||||||
writer.write_u16(x).await?;
|
writer.write_u16(x).await?;
|
||||||
writer.write_u16(y).await
|
writer.write_u16(y).await
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,8 @@ impl<R: AsyncBufRead + AsyncBufReadExt + std::marker::Unpin> Parser<R> for TextP
|
||||||
if reader.read_line(&mut line).await.is_ok() {
|
if reader.read_line(&mut line).await.is_ok() {
|
||||||
if line.starts_with("HELP") {
|
if line.starts_with("HELP") {
|
||||||
return Ok(Command::Help);
|
return Ok(Command::Help);
|
||||||
|
} else if line.starts_with("PROTOCOLS") {
|
||||||
|
return Ok(Command::Protocols);
|
||||||
} else if line.starts_with("SIZE") {
|
} else if line.starts_with("SIZE") {
|
||||||
return Ok(Command::Size(self.canvas));
|
return Ok(Command::Size(self.canvas));
|
||||||
} else if line.starts_with("PX ") {
|
} else if line.starts_with("PX ") {
|
||||||
|
|
@ -146,6 +148,23 @@ impl<W: AsyncWriteExt + std::marker::Unpin> Responder<W> for TextParser {
|
||||||
async fn unparse(&self, response: Response, writer: &mut W) -> io::Result<()> {
|
async fn unparse(&self, response: Response, writer: &mut W) -> io::Result<()> {
|
||||||
match response {
|
match response {
|
||||||
Response::Help => writer.write_all(HELP_TEXT).await,
|
Response::Help => writer.write_all(HELP_TEXT).await,
|
||||||
|
Response::Protocols(protos) => {
|
||||||
|
for protocol in protos {
|
||||||
|
match protocol {
|
||||||
|
crate::ProtocolStatus::Enabled(proto) => {
|
||||||
|
writer
|
||||||
|
.write_all(format!("Enabled: {}\n", proto).as_bytes())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
crate::ProtocolStatus::Disabled(proto) => {
|
||||||
|
writer
|
||||||
|
.write_all(format!("Disabled: {}\n", proto).as_bytes())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Response::Size(x, y) => writer.write_all(format!("SIZE {x} {y}\n").as_bytes()).await,
|
Response::Size(x, y) => writer.write_all(format!("SIZE {x} {y}\n").as_bytes()).await,
|
||||||
Response::GetPixel(x, y, color) => {
|
Response::GetPixel(x, y, color) => {
|
||||||
writer
|
writer
|
||||||
|
|
@ -209,6 +228,20 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_px_set_w_parse_caps() {
|
||||||
|
let parser = TextParser::default();
|
||||||
|
let reader = tokio_test::io::Builder::new()
|
||||||
|
.read(b"PX 28283 29991 AB\n")
|
||||||
|
.build();
|
||||||
|
let mut bufreader = BufReader::new(reader);
|
||||||
|
let thingy = parser.parse(&mut bufreader).await;
|
||||||
|
assert_eq!(
|
||||||
|
thingy.unwrap(),
|
||||||
|
Command::SetPixel(0, 28283, 29991, Color::W8(0xAB))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_px_set_rgb_parse() {
|
async fn test_px_set_rgb_parse() {
|
||||||
let parser = TextParser::default();
|
let parser = TextParser::default();
|
||||||
|
|
@ -223,6 +256,20 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_px_set_rgb_parse_caps() {
|
||||||
|
let parser = TextParser::default();
|
||||||
|
let reader = tokio_test::io::Builder::new()
|
||||||
|
.read(b"PX 28283 29991 8800FA\n")
|
||||||
|
.build();
|
||||||
|
let mut bufreader = BufReader::new(reader);
|
||||||
|
let thingy = parser.parse(&mut bufreader).await;
|
||||||
|
assert_eq!(
|
||||||
|
thingy.unwrap(),
|
||||||
|
Command::SetPixel(0, 28283, 29991, Color::RGB24(0x88, 0x00, 0xfa))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_px_set_rgba_parse() {
|
async fn test_px_set_rgba_parse() {
|
||||||
let parser = TextParser::default();
|
let parser = TextParser::default();
|
||||||
|
|
@ -237,6 +284,20 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_px_set_rgba_parse_caps() {
|
||||||
|
let parser = TextParser::default();
|
||||||
|
let reader = tokio_test::io::Builder::new()
|
||||||
|
.read(b"PX 28283 29991 AB0c3F88\n")
|
||||||
|
.build();
|
||||||
|
let mut bufreader = BufReader::new(reader);
|
||||||
|
let thingy = parser.parse(&mut bufreader).await;
|
||||||
|
assert_eq!(
|
||||||
|
thingy.unwrap(),
|
||||||
|
Command::SetPixel(0, 28283, 29991, Color::RGBA32(0xab, 0x0c, 0x3f, 0x88))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_px_get_parse() {
|
async fn test_px_get_parse() {
|
||||||
let parser = TextParser::default();
|
let parser = TextParser::default();
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,42 @@ impl Multipart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Multipart {
|
||||||
|
fn write_multipart_frame<T>(
|
||||||
|
obj: T,
|
||||||
|
boundary: Vec<u8>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
first: bool,
|
||||||
|
) -> Result<Vec<u8>, axum::Error>
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = u8>,
|
||||||
|
{
|
||||||
|
let mut frame_vec = Vec::new();
|
||||||
|
if first {
|
||||||
|
frame_vec.extend_from_slice(b"--");
|
||||||
|
} else {
|
||||||
|
frame_vec.extend_from_slice(b"\r\n--");
|
||||||
|
}
|
||||||
|
frame_vec.extend(boundary);
|
||||||
|
frame_vec.extend_from_slice(b"\r\n");
|
||||||
|
for (header_name, header_value) in headers {
|
||||||
|
match header_name {
|
||||||
|
Some(header) => {
|
||||||
|
frame_vec.extend_from_slice(header.as_str().as_bytes());
|
||||||
|
frame_vec.extend_from_slice(b": ");
|
||||||
|
frame_vec.extend_from_slice(header_value.as_bytes());
|
||||||
|
frame_vec.extend_from_slice(b"\r\n");
|
||||||
|
}
|
||||||
|
None => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frame_vec.extend_from_slice(b"\r\n");
|
||||||
|
frame_vec.extend(obj);
|
||||||
|
|
||||||
|
Ok(frame_vec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> StreamingFormat<T> for Multipart
|
impl<T> StreamingFormat<T> for Multipart
|
||||||
where
|
where
|
||||||
T: Send + Sync + IntoIterator<Item = u8> + 'static,
|
T: Send + Sync + IntoIterator<Item = u8> + 'static,
|
||||||
|
|
@ -37,40 +73,6 @@ where
|
||||||
stream: futures::stream::BoxStream<'b, Result<T, axum::Error>>,
|
stream: futures::stream::BoxStream<'b, Result<T, axum::Error>>,
|
||||||
_options: &'a axum_streams::StreamBodyAsOptions,
|
_options: &'a axum_streams::StreamBodyAsOptions,
|
||||||
) -> futures::stream::BoxStream<'b, Result<axum::body::Bytes, axum::Error>> {
|
) -> futures::stream::BoxStream<'b, Result<axum::body::Bytes, axum::Error>> {
|
||||||
fn write_multipart_frame<T>(
|
|
||||||
obj: T,
|
|
||||||
boundary: Vec<u8>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
first: bool,
|
|
||||||
) -> Result<Vec<u8>, axum::Error>
|
|
||||||
where
|
|
||||||
T: IntoIterator<Item = u8>,
|
|
||||||
{
|
|
||||||
let mut frame_vec = Vec::new();
|
|
||||||
if first {
|
|
||||||
frame_vec.extend_from_slice(b"--");
|
|
||||||
} else {
|
|
||||||
frame_vec.extend_from_slice(b"\r\n--");
|
|
||||||
}
|
|
||||||
frame_vec.extend(boundary);
|
|
||||||
frame_vec.extend_from_slice(b"\r\n");
|
|
||||||
for (header_name, header_value) in headers {
|
|
||||||
match header_name {
|
|
||||||
Some(header) => {
|
|
||||||
frame_vec.extend_from_slice(header.as_str().as_bytes());
|
|
||||||
frame_vec.extend_from_slice(b": ");
|
|
||||||
frame_vec.extend_from_slice(header_value.as_bytes());
|
|
||||||
frame_vec.extend_from_slice(b"\r\n");
|
|
||||||
}
|
|
||||||
None => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
frame_vec.extend_from_slice(b"\r\n");
|
|
||||||
frame_vec.extend(obj);
|
|
||||||
|
|
||||||
Ok(frame_vec)
|
|
||||||
}
|
|
||||||
|
|
||||||
let boundary = self.boundary.clone();
|
let boundary = self.boundary.clone();
|
||||||
let headers = self.headers.clone();
|
let headers = self.headers.clone();
|
||||||
let first = self.first;
|
let first = self.first;
|
||||||
|
|
@ -79,8 +81,12 @@ where
|
||||||
stream.map(move |obj_res| match obj_res {
|
stream.map(move |obj_res| match obj_res {
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
Ok(obj) => {
|
Ok(obj) => {
|
||||||
let picture_framed =
|
let picture_framed = Multipart::write_multipart_frame(
|
||||||
write_multipart_frame(obj, boundary.clone(), headers.clone(), first);
|
obj,
|
||||||
|
boundary.clone(),
|
||||||
|
headers.clone(),
|
||||||
|
first,
|
||||||
|
);
|
||||||
|
|
||||||
picture_framed.map(axum::body::Bytes::from)
|
picture_framed.map(axum::body::Bytes::from)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue