class Part

Includes
Readable: #copy_to, #save

Represents a single part within a multipart message.

Definitions

def initialize(readable, headers, boundary)

Initialize a new part with a readable stream, headers, and a boundary string.

Signature

parameter readable IO::Stream

The readable stream that contains the part's data.

parameter headers Headers

The headers associated with this part.

parameter boundary String

The boundary string used to separate parts.

Implementation

def initialize(readable, headers, boundary)
	@readable = readable
	@boundary = boundary
	@headers = headers
	@ended = false
	@is_closing = false
end

attr_reader :headers

Signature

attribute Headers

The headers associated with this part.

def each(chunk_size = 8192)

Iterate through the part content in chunks.

Signature

parameter chunk_size Integer

The size of each chunk to read.

returns Enumerable

An enumerable of content chunks if no block given.

Implementation

def each(chunk_size = 8192)
	return to_enum(:each, chunk_size) unless block_given?
	
	return unless @readable
	
	boundary_marker = "\r\n--#{@boundary}"
	
	# Stream data in chunks using read_until with a limit
	while @readable
		if chunk = @readable.read_until(boundary_marker, limit: chunk_size, chomp: true)
			# We found the boundary, check if it's a closing boundary:
			if suffix = @readable.read_until("\r\n", chomp: true)
				@is_closing = (suffix == "--")
				@ended = true
				@readable = nil
			else
				@readable = nil
				raise EOFError, "Unexpected end of stream while reading part data!"
			end
		else
			chunk = @readable.read(chunk_size)
		end
		
		if chunk
			yield chunk unless chunk.empty?
		else
			# No more data to read, break the loop:
			break
		end
	end
end

def read_empty_boundary?

Checks if the next content is an empty boundary (part with no content).

Signature

returns Boolean

True if an empty boundary was found and read, false otherwise.

Implementation

def read_empty_boundary?
	boundary_marker = "--#{@boundary}"
	if @readable.peek(boundary_marker.bytesize) == boundary_marker
		@readable.read(boundary_marker.bytesize)
		self.read_boundary_suffix
		
		return true
	end
	
	return false
end

def read_boundary_suffix

Reads the suffix after a boundary to determine if it's a closing boundary.

Implementation

def read_boundary_suffix
	# Read the rest of the boundary line to check if it's closing:
	boundary_suffix = @readable.read(2)
	if boundary_suffix == "--"
		@is_closing = true
		@ended = true
		@readable = nil
	elsif boundary_suffix == "\r\n"
		@is_closing = false
		@ended = true
		@readable = nil
	else
		@readable = nil
		raise EOFError, "Unexpected end of stream while reading part data!"
	end
end

def finish

Finishes reading this part's data and advances to the next boundary.

Signature

returns String | Nil

The remaining content of the part, or nil if already finished.

Implementation

def finish
	return unless @readable
	
	# Read all data until boundary
	data = @readable.read_until("\r\n--#{@boundary}", chomp: true)
	
	self.read_boundary_suffix
	
	return data
end

def discard

Efficiently discards all data until the next boundary is found. This is used to skip parts without reading their content into memory.

Signature

returns Nil

Implementation

def discard
	# Efficiently discard all data until boundary
	return unless @readable
	
	# Discard data until boundary
	@readable.discard_until("\r\n--#{@boundary}")
	
	self.read_boundary_suffix
	
	return nil
end

def ended?

Checks if this part has been completely read.

Signature

returns Boolean

True if this part has been completely read.

Implementation

def ended?
	@ended
end

def closing_boundary?

Checks if this part ends with a closing boundary. A closing boundary indicates that this is the last part in the multipart message.

Signature

returns Boolean

True if this part ends with a closing boundary.

Implementation

def closing_boundary?
	@is_closing || (@readable.nil? && @is_closing)
end