///|
const WRITER_READY : Int = 0
///|
const WRITER_IN_PART : Int = 1
///|
const WRITER_BETWEEN_PARTS : Int = 2
///|
const WRITER_DONE : Int = 3
///|
const WRITER_FAILED : Int = 4
///|
pub struct MultipartWriter {
priv boundary : String
priv limits : @model.Limits
priv mut state : Int
priv mut part_count : Int
priv mut current_part_bytes : Int64
priv mut emitted : Int64
}
///|
fn[T] MultipartWriter::raise_failed(
self : MultipartWriter,
error : @model.MultipartError,
) -> T raise @model.MultipartError {
self.state = WRITER_FAILED
raise error
}
///|
fn MultipartWriter::invalid_state(
self : MultipartWriter,
operation : String,
) -> @model.MultipartError {
let phase = if self.state == WRITER_FAILED {
@model.ParserPhase::Failed
} else if self.state == WRITER_DONE {
Done
} else if self.state == WRITER_IN_PART {
Body
} else {
Headers
}
InvalidState(operation~, phase~)
}
///|
fn MultipartWriter::record_output(
self : MultipartWriter,
bytes : Bytes,
) -> Bytes raise @model.MultipartError {
let observed = self.emitted + bytes.length().to_int64()
if observed > self.limits.max_total_bytes {
self.raise_failed(
LimitExceeded(
kind=TotalBytes,
limit=self.limits.max_total_bytes,
observed~,
),
)
}
self.emitted = observed
bytes
}
///|
fn concat_bytes(parts : Array[Bytes]) -> Bytes {
let output = Buffer()
for part in parts {
output.write_bytes(part)
}
output.to_bytes()
}
///|
pub fn MultipartWriter::new(
boundary : String,
limits : @model.Limits,
) -> MultipartWriter raise @model.MultipartError {
@syntax.validate_boundary(boundary)
@model.validate_limits(limits)
{
boundary,
limits,
state: WRITER_READY,
part_count: 0,
current_part_bytes: 0L,
emitted: 0L,
}
}
///|
pub fn MultipartWriter::content_type(self : MultipartWriter) -> String {
@syntax.format_content_type(self.boundary) catch {
_ => "multipart/form-data"
}
}
///|
pub fn MultipartWriter::begin_part(
self : MultipartWriter,
meta : @model.PartMeta,
) -> Bytes raise @model.MultipartError {
if self.state != WRITER_READY && self.state != WRITER_BETWEEN_PARTS {
self.raise_failed(self.invalid_state("begin_part"))
}
let observed_parts = self.part_count + 1
if observed_parts > self.limits.max_parts {
self.raise_failed(
LimitExceeded(
kind=Parts,
limit=self.limits.max_parts.to_int64(),
observed=observed_parts.to_int64(),
),
)
}
let normalized_meta = { ..meta, index: self.part_count }
let headers = @syntax.format_part_headers(normalized_meta) catch {
error => self.raise_failed(error)
}
let delimiter = if self.state == WRITER_READY {
@utf8.encode("--\{self.boundary}\r\n")
} else {
@utf8.encode("\r\n--\{self.boundary}\r\n")
}
let output = concat_bytes([delimiter, headers, b"\r\n"])
let recorded = self.record_output(output)
self.part_count = observed_parts
self.current_part_bytes = 0L
self.state = WRITER_IN_PART
recorded
}
///|
pub fn MultipartWriter::write(
self : MultipartWriter,
chunk : Bytes,
) -> Bytes raise @model.MultipartError {
if self.state != WRITER_IN_PART {
self.raise_failed(self.invalid_state("write"))
}
let observed = self.current_part_bytes + chunk.length().to_int64()
if observed > self.limits.max_part_bytes {
self.raise_failed(
LimitExceeded(kind=PartBytes, limit=self.limits.max_part_bytes, observed~),
)
}
let recorded = self.record_output(chunk)
self.current_part_bytes = observed
recorded
}
///|
pub fn MultipartWriter::end_part(
self : MultipartWriter,
) -> Bytes raise @model.MultipartError {
if self.state != WRITER_IN_PART {
self.raise_failed(self.invalid_state("end_part"))
}
self.state = WRITER_BETWEEN_PARTS
b""
}
///|
pub fn MultipartWriter::finish(
self : MultipartWriter,
) -> Bytes raise @model.MultipartError {
if self.state != WRITER_READY && self.state != WRITER_BETWEEN_PARTS {
self.raise_failed(self.invalid_state("finish"))
}
let closing = if self.state == WRITER_READY {
@utf8.encode("--\{self.boundary}--\r\n")
} else {
@utf8.encode("\r\n--\{self.boundary}--\r\n")
}
let recorded = self.record_output(closing)
self.state = WRITER_DONE
recorded
}
///|
pub fn MultipartWriter::bytes_emitted(self : MultipartWriter) -> Int64 {
self.emitted
}