x-uploads 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +171 -0
- data/LICENSE.txt +21 -0
- data/README.md +98 -0
- data/lib/x/uploads/account.rb +100 -0
- data/lib/x/uploads/alt_text_failed.rb +81 -0
- data/lib/x/uploads/api.rb +310 -0
- data/lib/x/uploads/chunked_upload_failed.rb +89 -0
- data/lib/x/uploads/chunks.rb +325 -0
- data/lib/x/uploads/error.rb +25 -0
- data/lib/x/uploads/gif.rb +88 -0
- data/lib/x/uploads/invalid_media.rb +19 -0
- data/lib/x/uploads/invalid_media_type.rb +12 -0
- data/lib/x/uploads/json_classes.rb +14 -0
- data/lib/x/uploads/media_processing_check_failed.rb +85 -0
- data/lib/x/uploads/media_processing_failed.rb +43 -0
- data/lib/x/uploads/media_processing_timeout.rb +55 -0
- data/lib/x/uploads/media_upload.rb +565 -0
- data/lib/x/uploads/metadata.rb +97 -0
- data/lib/x/uploads/missing_media_data.rb +47 -0
- data/lib/x/uploads/multipart.rb +63 -0
- data/lib/x/uploads/signature.rb +104 -0
- data/lib/x/uploads/source.rb +440 -0
- data/lib/x/uploads/uploaded_media.rb +369 -0
- data/lib/x/uploads/utils.rb +330 -0
- data/lib/x/uploads/validator.rb +391 -0
- data/lib/x/uploads/version.rb +24 -0
- data/lib/x/uploads.rb +18 -0
- data/sig/manifest.yaml +6 -0
- data/sig/x-uploads.rbs +139 -0
- metadata +98 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "invalid_media"
|
|
4
|
+
require_relative "utils"
|
|
5
|
+
|
|
6
|
+
module X
|
|
7
|
+
module Uploads
|
|
8
|
+
# The media an upload reads, given as a file path or as an IO
|
|
9
|
+
#
|
|
10
|
+
# Media given as a String, a Pathname, or any other path to a file is read from that file, and media given as an
|
|
11
|
+
# IO open on a file, such as a File or a Tempfile, is read through that IO, whether or not its name still leads
|
|
12
|
+
# to the file, as it no longer does once a Tempfile is unlinked: either is read a chunk at a time, so that media of
|
|
13
|
+
# any size uploads without being held in memory. Media given as any other IO, such as a StringIO, is read once,
|
|
14
|
+
# and held, since an IO that is not open on a file cannot be read again by position. An IO that can seek, as a
|
|
15
|
+
# File and a StringIO can, is read from its start, whatever position it holds, and given back at that position,
|
|
16
|
+
# so that the media can be checked and then uploaded, and a pipe, which cannot, is read from where it is.
|
|
17
|
+
#
|
|
18
|
+
# Internal to x-uploads: the uploaders resolve what they were given to one of these, rather than read a path
|
|
19
|
+
# themselves, so that a path and an IO upload the same way.
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
class Source
|
|
23
|
+
# Bytes read from the start of media that names no file, enough for every signature {Signature} reads
|
|
24
|
+
SNIFF_BYTES = 512
|
|
25
|
+
# The bytes a path of media is not written with, but its contents may be: a path holds no NUL byte, and names
|
|
26
|
+
# no file with a line break in any use an upload is meant for, while an image read with File.binread holds a
|
|
27
|
+
# NUL byte, and subtitles a line break
|
|
28
|
+
CONTENT_BYTES = /[\0\n]/
|
|
29
|
+
# The message of the error raised for the contents of media given where its path belongs
|
|
30
|
+
NOT_A_PATH = "media must be a path to the media, or an IO that reads it, such as a StringIO, not the contents " \
|
|
31
|
+
"of the media: a String that holds a NUL byte or a line break names no file"
|
|
32
|
+
# The paths Ruby gives the standard streams, which name no file, even when a stream reads one, as $stdin
|
|
33
|
+
# redirected from a file does
|
|
34
|
+
STREAM_PATHS = %w[<STDIN> <STDOUT> <STDERR>].freeze
|
|
35
|
+
# The words for media that names no file, in the message of an error it raises
|
|
36
|
+
UNNAMED = "the media given"
|
|
37
|
+
private_constant :SNIFF_BYTES, :CONTENT_BYTES, :NOT_A_PATH, :STREAM_PATHS, :UNNAMED
|
|
38
|
+
|
|
39
|
+
# The source of media given as a path or as an IO
|
|
40
|
+
#
|
|
41
|
+
# An IO that names a file is flushed first, so that what it has written reaches the file the upload reads.
|
|
42
|
+
#
|
|
43
|
+
# A String is a path, so one that holds a NUL byte or a line break, as the contents of media given in its place
|
|
44
|
+
# do, such as the bytes of an image or the text of subtitles, raises, rather than be looked for as a file named
|
|
45
|
+
# by all of it.
|
|
46
|
+
#
|
|
47
|
+
# @api private
|
|
48
|
+
# @param media [String, Pathname, IO, StringIO, Source] the path to the media, or an IO open on it
|
|
49
|
+
# @return [Source] the source, which is what was given if that is already one
|
|
50
|
+
# @raise [ArgumentError] if the media is neither a path nor an IO, or is a String that holds the contents of
|
|
51
|
+
# media rather than a path
|
|
52
|
+
# @raise [InvalidMedia] if the media is an IO that cannot be read, or one the system cannot say what it is open
|
|
53
|
+
# on
|
|
54
|
+
# @example The source of a file
|
|
55
|
+
# Uploads::Source.for("cat.jpg")
|
|
56
|
+
# @example The source of media held in memory
|
|
57
|
+
# Uploads::Source.for(StringIO.new(bytes))
|
|
58
|
+
def self.for(media)
|
|
59
|
+
case media
|
|
60
|
+
when Source then media
|
|
61
|
+
when String then media.b.match?(CONTENT_BYTES) ? raise(ArgumentError, NOT_A_PATH) : Path.new(media)
|
|
62
|
+
else named_or_read(media)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The source of media given as a path that is not a String, or as an IO
|
|
67
|
+
#
|
|
68
|
+
# A path, such as a Pathname, which cannot seek, is read from the file it names, as is a File or a Tempfile that
|
|
69
|
+
# was closed, which can no longer be read through, and an IO open on a file, which can, through that IO, as
|
|
70
|
+
# $stdin is when it is redirected from a file. Any other IO is read to its end, such as a StringIO, or a pipe,
|
|
71
|
+
# whose path is nil, or $stdin reading a pipe or a terminal: an IO open on something that is not a file or a
|
|
72
|
+
# directory, such as a pipe, cannot be read by position, as a file is. One whose path is nil that was closed,
|
|
73
|
+
# such as a pipe, then raises InvalidMedia, as it cannot be read.
|
|
74
|
+
#
|
|
75
|
+
# @api private
|
|
76
|
+
# @param media [Pathname, IO, StringIO, Object] the path or the IO
|
|
77
|
+
# @return [Source] the source
|
|
78
|
+
# @raise [ArgumentError] if the media is neither a path nor an IO
|
|
79
|
+
# @raise [InvalidMedia] if the media is an IO that cannot be read, or one the system cannot say what it is open
|
|
80
|
+
# on
|
|
81
|
+
def self.named_or_read(media)
|
|
82
|
+
named = media.respond_to?(:to_path) && media.to_path
|
|
83
|
+
if named && path_alone?(media)
|
|
84
|
+
Path.new(media)
|
|
85
|
+
elsif named && on_file?(media)
|
|
86
|
+
Handle.new(media)
|
|
87
|
+
elsif media.respond_to?(:read)
|
|
88
|
+
buffered(media)
|
|
89
|
+
else
|
|
90
|
+
raise ArgumentError, "media must be a path or an IO that reads one, not #{media.class}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
private_class_method :named_or_read
|
|
94
|
+
|
|
95
|
+
# Check whether media that names a file can be read from that name alone
|
|
96
|
+
# @api private
|
|
97
|
+
# @param media [Pathname, IO] the path, or the IO, which has a path
|
|
98
|
+
# @return [Boolean] true if the media cannot seek, as a path cannot, or is an IO that was closed
|
|
99
|
+
def self.path_alone?(media) = !media.respond_to?(:seek) || (media.respond_to?(:closed?) && media.closed?)
|
|
100
|
+
private_class_method :path_alone?
|
|
101
|
+
|
|
102
|
+
# Check whether an IO is open on a file or a directory, which a Handle reads
|
|
103
|
+
# @api private
|
|
104
|
+
# @param media [IO] the IO, which has a path
|
|
105
|
+
# @return [Boolean] true if the IO is open on a file or a directory, rather than a pipe, a device, or a socket
|
|
106
|
+
# @raise [InvalidMedia] if the system cannot say what the IO is open on, or the IO was closed meanwhile
|
|
107
|
+
def self.on_file?(media)
|
|
108
|
+
reading(UNNAMED) do
|
|
109
|
+
stat = media.stat
|
|
110
|
+
stat.file? || stat.directory?
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
private_class_method :on_file?
|
|
114
|
+
|
|
115
|
+
# The source of media given as an IO that is not open on a file
|
|
116
|
+
#
|
|
117
|
+
# An IO that can seek, as a StringIO can, is read from its start, as a File is, whatever position it holds, such
|
|
118
|
+
# as the end it is left at once it has been written to, and is given back at that position, so that what infers
|
|
119
|
+
# the type of the media leaves it to be uploaded. One that cannot, as a pipe cannot, is read from where it is to
|
|
120
|
+
# its end. An IO is put in binary mode first, since media is bytes, and a pipe or $stdin reads in text mode on
|
|
121
|
+
# Windows, which would turn each CRLF of the media, such as the one in the signature of a PNG, into a line feed.
|
|
122
|
+
# An IO that cannot be read, as a closed StringIO or one open for writing alone cannot, raises InvalidMedia, as
|
|
123
|
+
# a File that cannot be read does when the upload checks it, and so does one the system refuses to read, as it
|
|
124
|
+
# does a socket that is not connected, with the error of the system as its cause.
|
|
125
|
+
#
|
|
126
|
+
# @api private
|
|
127
|
+
# @param media [StringIO, IO, Object] the IO
|
|
128
|
+
# @return [Buffer] the source
|
|
129
|
+
# @raise [InvalidMedia] if the IO cannot be read
|
|
130
|
+
def self.buffered(media)
|
|
131
|
+
reading(UNNAMED) do
|
|
132
|
+
position = position_of(media)
|
|
133
|
+
media.binmode if media.is_a?(IO)
|
|
134
|
+
Buffer.new(read_whole(media, position))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
private_class_method :buffered
|
|
138
|
+
|
|
139
|
+
# Read media before a request, raising InvalidMedia if it cannot be read
|
|
140
|
+
#
|
|
141
|
+
# What an upload reads of its media before it sends a request, what the media is open on, its size, its
|
|
142
|
+
# signature, and the whole of media sent in a single request or held, is read with it, so that a file deleted
|
|
143
|
+
# once the upload has checked it, a disk that fails, or an IO closed while it is read, as by another thread,
|
|
144
|
+
# raises InvalidMedia, as media that cannot be read does, with the error as its cause. The message gives the
|
|
145
|
+
# reason of an error of the system bare, as "No such file or directory", without the function of Ruby and the
|
|
146
|
+
# path that Ruby adds to it, since the message names the media already, and that of any other error as it is.
|
|
147
|
+
# The chunks of an upload are read without it, since a chunk that cannot be read fails an upload that is already
|
|
148
|
+
# initialized, with the error as the cause of that.
|
|
149
|
+
#
|
|
150
|
+
# @api private
|
|
151
|
+
# @param description [String] the media in words, which the message of the error begins with
|
|
152
|
+
# @yield reads the media
|
|
153
|
+
# @return [Object] what the block returns
|
|
154
|
+
# @raise [InvalidMedia] if the read raises an IOError, as that of a closed IO, or an error of the system
|
|
155
|
+
# @example Read the size of a file
|
|
156
|
+
# Uploads::Source.reading("cat.jpg") { File.size("cat.jpg") }
|
|
157
|
+
def self.reading(description)
|
|
158
|
+
yield
|
|
159
|
+
rescue IOError, SystemCallError => e
|
|
160
|
+
errno = e.errno if e.is_a?(SystemCallError)
|
|
161
|
+
raise InvalidMedia, "#{description} cannot be read: #{errno ? SystemCallError.new(errno) : e}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Read an IO to its end, from its start if it can seek
|
|
165
|
+
#
|
|
166
|
+
# An IO that can seek is given back at its position, even if reading it raises.
|
|
167
|
+
#
|
|
168
|
+
# @api private
|
|
169
|
+
# @param media [StringIO, IO, Object] the IO
|
|
170
|
+
# @param position [Integer, nil] the position of the IO, or nil for an IO that cannot seek
|
|
171
|
+
# @return [String] the bytes read
|
|
172
|
+
def self.read_whole(media, position)
|
|
173
|
+
media.seek(0) if position
|
|
174
|
+
media.read.to_s.b
|
|
175
|
+
ensure
|
|
176
|
+
media.seek(position) if position
|
|
177
|
+
end
|
|
178
|
+
private_class_method :read_whole
|
|
179
|
+
|
|
180
|
+
# The position of an IO that can seek
|
|
181
|
+
# @api private
|
|
182
|
+
# @param media [StringIO, IO, Object] the IO
|
|
183
|
+
# @return [Integer, nil] the position, or nil for an IO that cannot seek
|
|
184
|
+
def self.position_of(media)
|
|
185
|
+
media.pos if media.respond_to?(:seek)
|
|
186
|
+
rescue SystemCallError
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
private_class_method :position_of
|
|
190
|
+
|
|
191
|
+
# The name of the file the media was given as
|
|
192
|
+
#
|
|
193
|
+
# Media that names no file, which {Path} alone does, has none.
|
|
194
|
+
#
|
|
195
|
+
# @api private
|
|
196
|
+
# @return [String, nil] the file name, or nil for media that names none
|
|
197
|
+
# @example The name of media given as a path
|
|
198
|
+
# Uploads::Source.for("cat.jpg").name # => "cat.jpg"
|
|
199
|
+
attr_reader :name
|
|
200
|
+
|
|
201
|
+
# The media in words, for the message of an error it raises
|
|
202
|
+
# @api private
|
|
203
|
+
# @return [String] the file name, or a phrase for media that names none
|
|
204
|
+
# @example Describe media held in memory
|
|
205
|
+
# Uploads::Source.for(StringIO.new(bytes)).description # => "the media given"
|
|
206
|
+
def description = name || UNNAMED
|
|
207
|
+
|
|
208
|
+
# The lowercase extension of the file the media names
|
|
209
|
+
#
|
|
210
|
+
# It has no dot, and is "" for media that names no file.
|
|
211
|
+
#
|
|
212
|
+
# @api private
|
|
213
|
+
# @return [String] the extension
|
|
214
|
+
# @example The extension of media given as a path
|
|
215
|
+
# Uploads::Source.for("cat.JPG").extension # => "jpg"
|
|
216
|
+
def extension = Utils.extension(name.to_s)
|
|
217
|
+
|
|
218
|
+
# The bytes a signature is read from
|
|
219
|
+
#
|
|
220
|
+
# They are fewer than SNIFF_BYTES only if the media is shorter, and none at all if it is empty.
|
|
221
|
+
#
|
|
222
|
+
# @api private
|
|
223
|
+
# @return [String] the leading bytes of the media
|
|
224
|
+
# @raise [InvalidMedia] if the system refuses to read the media, or it was closed meanwhile
|
|
225
|
+
# @example Read the signature of media
|
|
226
|
+
# Uploads::Source.for("cat.gif").sniff # => "GIF89a..."
|
|
227
|
+
def sniff = reading { read(SNIFF_BYTES, 0).to_s }
|
|
228
|
+
|
|
229
|
+
# Media read from the file it names, a chunk at a time
|
|
230
|
+
# @api private
|
|
231
|
+
class Path < Source
|
|
232
|
+
# Initialize the source of media given as a path
|
|
233
|
+
# @api private
|
|
234
|
+
# @param path [String, Pathname] the path to the media
|
|
235
|
+
# @return [Path] the source
|
|
236
|
+
# @example The source of a file
|
|
237
|
+
# Uploads::Source::Path.new("cat.jpg")
|
|
238
|
+
def initialize(path)
|
|
239
|
+
@name = File.path(path)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Whether the file exists
|
|
243
|
+
#
|
|
244
|
+
# An upload of media that is not there is refused.
|
|
245
|
+
#
|
|
246
|
+
# @api private
|
|
247
|
+
# @return [Boolean] true if the file exists
|
|
248
|
+
def exist? = File.exist?(name)
|
|
249
|
+
|
|
250
|
+
# Whether the media can be read
|
|
251
|
+
#
|
|
252
|
+
# A file that is not there, that is a directory, or that the process has no permission to read, cannot be.
|
|
253
|
+
#
|
|
254
|
+
# @api private
|
|
255
|
+
# @return [Boolean] true if the media is a file that can be read
|
|
256
|
+
def readable? = File.file?(name) && File.readable?(name)
|
|
257
|
+
|
|
258
|
+
# The size of the media in bytes
|
|
259
|
+
#
|
|
260
|
+
# It is read once, when first asked, so that an upload checks, declares, and appends the same number of bytes,
|
|
261
|
+
# whatever the file does meanwhile.
|
|
262
|
+
#
|
|
263
|
+
# @api private
|
|
264
|
+
# @return [Integer] the size in bytes
|
|
265
|
+
# @raise [InvalidMedia] if the system refuses to read the size of the file
|
|
266
|
+
def size = @size ||= reading { File.size(name) }
|
|
267
|
+
|
|
268
|
+
# The whole of the media
|
|
269
|
+
# @api private
|
|
270
|
+
# @return [String] the bytes of the media
|
|
271
|
+
# @raise [InvalidMedia] if the system refuses to read the file
|
|
272
|
+
def content = reading { File.binread(name) }
|
|
273
|
+
|
|
274
|
+
# A run of the media, which the chunks of an upload are read with, from any thread
|
|
275
|
+
# @api private
|
|
276
|
+
# @param length [Integer] the number of bytes to read
|
|
277
|
+
# @param offset [Integer] the byte to read from, which is within the media
|
|
278
|
+
# @return [String] the bytes
|
|
279
|
+
def read(length, offset) = File.binread(name, length, offset)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Media read through an IO open on a file, a chunk at a time
|
|
283
|
+
#
|
|
284
|
+
# The IO is read by position, from the start of the file, whatever position it holds, and is left at that
|
|
285
|
+
# position. Reading by position flushes what the IO has written, as reading its size does, so that is read with
|
|
286
|
+
# the rest.
|
|
287
|
+
# The file need not be named by the path the IO holds: an unlinked Tempfile is read, as is one created anonymous,
|
|
288
|
+
# whose path is its directory, and $stdin redirected from a file, whose path is "<STDIN>", neither of which so
|
|
289
|
+
# names a file.
|
|
290
|
+
#
|
|
291
|
+
# @api private
|
|
292
|
+
class Handle < Source
|
|
293
|
+
# Initialize the source of media read through an IO open on a file
|
|
294
|
+
# @api private
|
|
295
|
+
# @param io [IO] the IO, which answers to_path
|
|
296
|
+
# @return [Handle] the source
|
|
297
|
+
# @example The source of a File
|
|
298
|
+
# Uploads::Source::Handle.new(File.open("cat.jpg", "rb"))
|
|
299
|
+
def initialize(io)
|
|
300
|
+
@io = io
|
|
301
|
+
path = File.path(io)
|
|
302
|
+
@name = path unless STREAM_PATHS.include?(path) || File.directory?(path)
|
|
303
|
+
@mutex = Mutex.new
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Whether the media exists, which media open on a file always does
|
|
307
|
+
# @api private
|
|
308
|
+
# @return [Boolean] true
|
|
309
|
+
def exist? = true
|
|
310
|
+
|
|
311
|
+
# Whether the media can be read: the IO is open on a file, and open for reading
|
|
312
|
+
# @api private
|
|
313
|
+
# @return [Boolean] true if the IO is open on a file for reading
|
|
314
|
+
# @raise [InvalidMedia] if the system cannot say what the IO is open on, or the IO was closed meanwhile
|
|
315
|
+
def readable? = reading { @io.stat.file? && open_for_reading? }
|
|
316
|
+
|
|
317
|
+
# The size of the media in bytes
|
|
318
|
+
#
|
|
319
|
+
# A File or a Tempfile reads it with size, which flushes what it has written first, and an IO that answers no
|
|
320
|
+
# size, as $stdin does, from the file it is open on. It is read once, when first asked, so that an upload
|
|
321
|
+
# checks, declares, and appends the same number of bytes, whatever the file does meanwhile.
|
|
322
|
+
#
|
|
323
|
+
# @api private
|
|
324
|
+
# @return [Integer] the size in bytes
|
|
325
|
+
# @raise [InvalidMedia] if the system refuses to read the size of the file, or the IO was closed meanwhile
|
|
326
|
+
def size = @size ||= reading { @io.respond_to?(:size) ? @io.size : @io.stat.size }
|
|
327
|
+
|
|
328
|
+
# The whole of the media
|
|
329
|
+
# @api private
|
|
330
|
+
# @return [String] the bytes of the media
|
|
331
|
+
# @raise [InvalidMedia] if the system refuses to read the file, or the IO was closed meanwhile
|
|
332
|
+
def content = reading { read(size, 0) }
|
|
333
|
+
|
|
334
|
+
# A run of the media, which the chunks of an upload are read with, from any thread
|
|
335
|
+
#
|
|
336
|
+
# It is read at its offset with pread, which leaves the position of the IO alone, so that uploads of one IO,
|
|
337
|
+
# and code of the caller's own that reads it meanwhile, never read where another moved it to. An IO that cannot
|
|
338
|
+
# pread, as one on Windows cannot, is read by seeking to the offset, with the chunks of the upload in turn,
|
|
339
|
+
# and each gives the IO back at the position it held.
|
|
340
|
+
#
|
|
341
|
+
# @api private
|
|
342
|
+
# @param length [Integer] the number of bytes to read
|
|
343
|
+
# @param offset [Integer] the byte to read from, which is within the media
|
|
344
|
+
# @return [String] the bytes, which are fewer than length, or none, if the file has fewer past the offset
|
|
345
|
+
def read(length, offset)
|
|
346
|
+
return seek_and_read(length, offset) unless @io.respond_to?(:pread)
|
|
347
|
+
|
|
348
|
+
@io.pread(length, offset)
|
|
349
|
+
rescue EOFError
|
|
350
|
+
""
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
private
|
|
354
|
+
|
|
355
|
+
# Read a run of the media by seeking to its offset
|
|
356
|
+
#
|
|
357
|
+
# The IO is given back at the position it held.
|
|
358
|
+
#
|
|
359
|
+
# @api private
|
|
360
|
+
# @param length [Integer] the number of bytes to read
|
|
361
|
+
# @param offset [Integer] the byte to read from
|
|
362
|
+
# @return [String] the bytes, or none if the file ends at the offset
|
|
363
|
+
def seek_and_read(length, offset)
|
|
364
|
+
@mutex.synchronize do
|
|
365
|
+
position = @io.pos
|
|
366
|
+
begin
|
|
367
|
+
@io.seek(offset)
|
|
368
|
+
@io.read(length).to_s
|
|
369
|
+
ensure
|
|
370
|
+
@io.seek(position)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Whether the IO is open for reading, told by reading nothing from it
|
|
376
|
+
# @api private
|
|
377
|
+
# @return [Boolean] true if the IO can be read
|
|
378
|
+
def open_for_reading?
|
|
379
|
+
@io.read(0)
|
|
380
|
+
true
|
|
381
|
+
rescue IOError
|
|
382
|
+
false
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Media read from an IO that is not open on a file, and held until the upload has finished
|
|
387
|
+
# @api private
|
|
388
|
+
class Buffer < Source
|
|
389
|
+
# Initialize the source of media read from an IO
|
|
390
|
+
# @api private
|
|
391
|
+
# @param content [String] the bytes read from the IO
|
|
392
|
+
# @return [Buffer] the source
|
|
393
|
+
# @example The source of media held in memory
|
|
394
|
+
# Uploads::Source::Buffer.new(bytes)
|
|
395
|
+
def initialize(content)
|
|
396
|
+
@content = content
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Whether the media exists, which media already read always does
|
|
400
|
+
# @api private
|
|
401
|
+
# @return [Boolean] true
|
|
402
|
+
def exist? = true
|
|
403
|
+
|
|
404
|
+
# Whether the media can be read, which media already read always can
|
|
405
|
+
# @api private
|
|
406
|
+
# @return [Boolean] true
|
|
407
|
+
def readable? = true
|
|
408
|
+
|
|
409
|
+
# The size of the media in bytes
|
|
410
|
+
# @api private
|
|
411
|
+
# @return [Integer] the size in bytes
|
|
412
|
+
def size = @content.bytesize
|
|
413
|
+
|
|
414
|
+
# The whole of the media
|
|
415
|
+
# @api private
|
|
416
|
+
# @return [String] the bytes of the media
|
|
417
|
+
attr_reader :content
|
|
418
|
+
|
|
419
|
+
# A run of the media, which the chunks of an upload are read with, from any thread
|
|
420
|
+
# @api private
|
|
421
|
+
# @param length [Integer] the number of bytes to read
|
|
422
|
+
# @param offset [Integer] the byte to read from, which is within the media
|
|
423
|
+
# @return [String] the bytes
|
|
424
|
+
def read(length, offset)
|
|
425
|
+
@content.byteslice(offset, length) #: String
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
private
|
|
430
|
+
|
|
431
|
+
# Read media before a request, raising InvalidMedia if it cannot be read
|
|
432
|
+
# @api private
|
|
433
|
+
# @yield reads the media
|
|
434
|
+
# @return [Object] what the block returns
|
|
435
|
+
# @raise [InvalidMedia] if the read raises an IOError, as that of a closed IO, or an error of the system
|
|
436
|
+
def reading(&) = Source.reading(description, &)
|
|
437
|
+
end
|
|
438
|
+
private_constant :Source
|
|
439
|
+
end
|
|
440
|
+
end
|