down 4.4.0 → 4.5.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 +5 -5
- data/CHANGELOG.md +10 -0
- data/README.md +9 -8
- data/down.gemspec +1 -1
- data/lib/down/backend.rb +1 -0
- data/lib/down/chunked_io.rb +10 -12
- data/lib/down/http.rb +37 -55
- data/lib/down/net_http.rb +2 -16
- data/lib/down/utils.rb +21 -0
- data/lib/down/version.rb +1 -1
- data/lib/down/wget.rb +5 -19
- metadata +8 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 82abe7a56a0950e4743ebba457ceb8cadb113303d2942ab2a2cfd34bc065d06a
|
|
4
|
+
data.tar.gz: bf7a15be5a3f4129f1ad6b85e7dba6757ac43f0877b3da4b7e18abc1703bb861
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae99273f99ef4ac8a3a23512482f1e5fa2dbaea5697f197681ad99543b7f07b9665c0532ebdf3d87521abf1d45f34ac1a52c21e314c1408e70e5299ed44ca9b7
|
|
7
|
+
data.tar.gz: 946eb4bda59b214c2aea5b885fd2d0ee9f85ac035cce635454a91e859d6d4dbf2e2a6de79ac6d9e631089aa12a7aa0fc7f2b96c73e6711890f3234eabe158489
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 4.5.0 (2018-05-11)
|
|
2
|
+
|
|
3
|
+
* Deprecate passing an `HTTP::Client` object to `Down::Http#initialize` (@janko-m)
|
|
4
|
+
|
|
5
|
+
* Add ability to pass a block to `Down::Http#initialize` for extending default options (@janko-m)
|
|
6
|
+
|
|
7
|
+
* Return empty string when length is zero in `ChunkedIO#read` and `ChunkedIO#readpartial` (@janko-m)
|
|
8
|
+
|
|
9
|
+
* Make `posix-spawn` optional (@janko-m)
|
|
10
|
+
|
|
1
11
|
## 4.4.0 (2018-04-12)
|
|
2
12
|
|
|
3
13
|
* Add `:method` option to `Down::Http` for specifying the request method (@janko-m)
|
data/README.md
CHANGED
|
@@ -7,7 +7,7 @@ HTTP library.
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
9
|
```rb
|
|
10
|
-
gem "down"
|
|
10
|
+
gem "down", "~> 4.4"
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Downloading
|
|
@@ -235,7 +235,7 @@ Down.open("...")
|
|
|
235
235
|
### open-uri + Net::HTTP
|
|
236
236
|
|
|
237
237
|
```rb
|
|
238
|
-
gem "down", "
|
|
238
|
+
gem "down", "~> 4.4"
|
|
239
239
|
```
|
|
240
240
|
```rb
|
|
241
241
|
require "down/net_http"
|
|
@@ -327,8 +327,8 @@ net_http.open("http://example.com/image.jpg")
|
|
|
327
327
|
### HTTP.rb
|
|
328
328
|
|
|
329
329
|
```rb
|
|
330
|
-
gem "down", "
|
|
331
|
-
gem "http", "~> 2
|
|
330
|
+
gem "down", "~> 4.4"
|
|
331
|
+
gem "http", "~> 3.2"
|
|
332
332
|
```
|
|
333
333
|
```rb
|
|
334
334
|
require "down/http"
|
|
@@ -340,9 +340,10 @@ io = Down::Http.open("http://nature.com/forest.jpg")
|
|
|
340
340
|
io #=> #<Down::ChunkedIO ...>
|
|
341
341
|
```
|
|
342
342
|
|
|
343
|
-
Some features that give the HTTP.rb backend an advantage over open-uri +
|
|
344
|
-
Net::HTTP include:
|
|
343
|
+
Some features that give the HTTP.rb backend an advantage over `open-uri` +
|
|
344
|
+
`Net::HTTP` include:
|
|
345
345
|
|
|
346
|
+
* Low memory usage (**10x less** than `open-uri`/`Net::HTTP`)
|
|
346
347
|
* Correct URI parsing with [Addressable::URI]
|
|
347
348
|
* Proper support for streaming downloads (`#download` and now reuse `#open`)
|
|
348
349
|
* Proper support for SSL
|
|
@@ -371,7 +372,7 @@ You can also initialize the backend with default options:
|
|
|
371
372
|
```rb
|
|
372
373
|
http = Down::Http.new(headers: { "Foo" => "Bar" })
|
|
373
374
|
# or
|
|
374
|
-
http = Down::Http.new
|
|
375
|
+
http = Down::Http.new { |client| client.timeout(connect: 3) }
|
|
375
376
|
|
|
376
377
|
http.download("http://example.com/image.jpg")
|
|
377
378
|
http.open("http://example.com/image.jpg")
|
|
@@ -393,7 +394,7 @@ down.download("http://example.org/image.jpg")
|
|
|
393
394
|
### Wget (experimental)
|
|
394
395
|
|
|
395
396
|
```rb
|
|
396
|
-
gem "down", "
|
|
397
|
+
gem "down", "~> 4.4"
|
|
397
398
|
gem "posix-spawn" # omit if on JRuby
|
|
398
399
|
gem "http_parser.rb"
|
|
399
400
|
```
|
data/down.gemspec
CHANGED
|
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
spec.require_path = "lib"
|
|
17
17
|
|
|
18
18
|
spec.add_development_dependency "minitest", "~> 5.8"
|
|
19
|
-
spec.add_development_dependency "mocha"
|
|
19
|
+
spec.add_development_dependency "mocha", "~> 1.5"
|
|
20
20
|
spec.add_development_dependency "rake"
|
|
21
21
|
spec.add_development_dependency "http", "~> 3.0"
|
|
22
22
|
spec.add_development_dependency "posix-spawn" unless RUBY_ENGINE == "jruby"
|
data/lib/down/backend.rb
CHANGED
data/lib/down/chunked_io.rb
CHANGED
|
@@ -77,7 +77,7 @@ module Down
|
|
|
77
77
|
remaining_length = length - data.bytesize if length
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
data.to_s unless length && (data.nil? || data.empty?)
|
|
80
|
+
data.to_s unless length && length > 0 && (data.nil? || data.empty?)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
# Implements IO#gets semantics. Without arguments it retrieves lines of
|
|
@@ -150,6 +150,8 @@ module Down
|
|
|
150
150
|
|
|
151
151
|
data = outbuf.clear.force_encoding(@encoding) if outbuf
|
|
152
152
|
|
|
153
|
+
return data.to_s if length == 0
|
|
154
|
+
|
|
153
155
|
if cache && !cache.eof?
|
|
154
156
|
data = cache.read(length, outbuf)
|
|
155
157
|
data.force_encoding(@encoding)
|
|
@@ -163,11 +165,13 @@ module Down
|
|
|
163
165
|
remaining_length = data && length ? length - data.bytesize : length
|
|
164
166
|
|
|
165
167
|
unless @buffer.nil? || remaining_length == 0
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
if remaining_length && remaining_length < @buffer.bytesize
|
|
169
|
+
buffered_data = @buffer.byteslice(0, remaining_length)
|
|
170
|
+
@buffer = @buffer.byteslice(remaining_length..-1)
|
|
171
|
+
else
|
|
172
|
+
buffered_data = @buffer
|
|
173
|
+
@buffer = nil
|
|
174
|
+
end
|
|
171
175
|
|
|
172
176
|
if data
|
|
173
177
|
data << buffered_data
|
|
@@ -177,12 +181,6 @@ module Down
|
|
|
177
181
|
|
|
178
182
|
cache.write(buffered_data) if cache
|
|
179
183
|
|
|
180
|
-
if buffered_data.bytesize < @buffer.bytesize
|
|
181
|
-
@buffer = @buffer.byteslice(buffered_data.bytesize..-1)
|
|
182
|
-
else
|
|
183
|
-
@buffer = nil
|
|
184
|
-
end
|
|
185
|
-
|
|
186
184
|
buffered_data.clear unless buffered_data.equal?(data)
|
|
187
185
|
end
|
|
188
186
|
|
data/lib/down/http.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
|
|
3
|
+
gem "http", ">= 2.1.0", "< 4"
|
|
4
|
+
|
|
3
5
|
require "http"
|
|
4
6
|
|
|
5
7
|
require "down/backend"
|
|
@@ -8,40 +10,39 @@ require "tempfile"
|
|
|
8
10
|
require "cgi"
|
|
9
11
|
require "base64"
|
|
10
12
|
|
|
11
|
-
if Gem::Version.new(HTTP::VERSION) < Gem::Version.new("2.1.0")
|
|
12
|
-
fail "Down::Http requires HTTP.rb version 2.1.0 or higher"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
13
|
module Down
|
|
16
14
|
class Http < Backend
|
|
17
|
-
def initialize(
|
|
18
|
-
options
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
def initialize(options = {}, &block)
|
|
16
|
+
if options.is_a?(HTTP::Client)
|
|
17
|
+
warn "[Down] Passing an HTTP::Client object to Down::Http#initialize is deprecated and won't be supported in Down 5. Use the block initialization instead."
|
|
18
|
+
options = options.default_options.to_hash
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@method = options.delete(:method) || :get
|
|
22
|
+
@client = HTTP
|
|
23
|
+
.headers("User-Agent" => "Down/#{Down::VERSION}")
|
|
24
|
+
.follow(max_hops: 2)
|
|
25
|
+
.timeout(connect: 30, write: 30, read: 30)
|
|
26
|
+
|
|
27
|
+
@client = HTTP::Client.new(@client.default_options.merge(options)) if options.any?
|
|
28
|
+
@client = block.call(@client) if block
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def download(url, max_size: nil, progress_proc: nil, content_length_proc: nil, destination: nil, **options, &block)
|
|
30
|
-
|
|
32
|
+
response = request(url, **options, &block)
|
|
31
33
|
|
|
32
|
-
content_length_proc.call(
|
|
34
|
+
content_length_proc.call(response.content_length) if content_length_proc && response.content_length
|
|
33
35
|
|
|
34
|
-
if max_size &&
|
|
36
|
+
if max_size && response.content_length && response.content_length > max_size
|
|
35
37
|
raise Down::TooLarge, "file is too large (max is #{max_size/1024/1024}MB)"
|
|
36
38
|
end
|
|
37
39
|
|
|
38
|
-
extname = File.extname(
|
|
40
|
+
extname = File.extname(response.uri.path)
|
|
39
41
|
tempfile = Tempfile.new(["down-http", extname], binmode: true)
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
chunk = io.readpartial(nil, buffer ||= String.new)
|
|
43
|
-
|
|
43
|
+
stream_body(response) do |chunk|
|
|
44
44
|
tempfile.write(chunk)
|
|
45
|
+
chunk.clear # deallocate string
|
|
45
46
|
|
|
46
47
|
progress_proc.call(tempfile.size) if progress_proc
|
|
47
48
|
|
|
@@ -53,45 +54,42 @@ module Down
|
|
|
53
54
|
tempfile.open # flush written content
|
|
54
55
|
|
|
55
56
|
tempfile.extend Down::Http::DownloadedFile
|
|
56
|
-
tempfile.url =
|
|
57
|
-
tempfile.headers =
|
|
57
|
+
tempfile.url = response.uri.to_s
|
|
58
|
+
tempfile.headers = response.headers.to_h
|
|
58
59
|
|
|
59
60
|
download_result(tempfile, destination)
|
|
60
61
|
rescue
|
|
61
62
|
tempfile.close! if tempfile
|
|
62
63
|
raise
|
|
63
|
-
ensure
|
|
64
|
-
io.close if io
|
|
65
64
|
end
|
|
66
65
|
|
|
67
66
|
def open(url, rewindable: true, **options, &block)
|
|
68
67
|
response = request(url, **options, &block)
|
|
69
68
|
|
|
70
|
-
response_error!(response) unless response.status.success?
|
|
71
|
-
|
|
72
69
|
Down::ChunkedIO.new(
|
|
73
70
|
chunks: enum_for(:stream_body, response),
|
|
74
71
|
size: response.content_length,
|
|
75
72
|
encoding: response.content_type.charset,
|
|
76
73
|
rewindable: rewindable,
|
|
77
|
-
on_close: (-> { response.connection.close } unless default_client.persistent?),
|
|
78
74
|
data: { status: response.code, headers: response.headers.to_h, response: response },
|
|
79
75
|
)
|
|
80
76
|
end
|
|
81
77
|
|
|
82
78
|
private
|
|
83
79
|
|
|
84
|
-
def
|
|
85
|
-
|
|
80
|
+
def request(url, method: @method, **options, &block)
|
|
81
|
+
response = send_request(method, url, **options, &block)
|
|
82
|
+
response_error!(response) unless response.status.success?
|
|
83
|
+
response
|
|
86
84
|
end
|
|
87
85
|
|
|
88
|
-
def
|
|
86
|
+
def send_request(method, url, **options, &block)
|
|
89
87
|
url = process_url(url, options)
|
|
90
88
|
|
|
91
|
-
client =
|
|
89
|
+
client = @client
|
|
92
90
|
client = block.call(client) if block
|
|
93
91
|
|
|
94
|
-
client.
|
|
92
|
+
client.request(method, url, options)
|
|
95
93
|
rescue => exception
|
|
96
94
|
request_error!(exception)
|
|
97
95
|
end
|
|
@@ -100,6 +98,8 @@ module Down
|
|
|
100
98
|
response.body.each(&block)
|
|
101
99
|
rescue => exception
|
|
102
100
|
request_error!(exception)
|
|
101
|
+
ensure
|
|
102
|
+
response.connection.close unless @client.persistent?
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def process_url(url, options)
|
|
@@ -147,34 +147,16 @@ module Down
|
|
|
147
147
|
attr_accessor :url, :headers
|
|
148
148
|
|
|
149
149
|
def original_filename
|
|
150
|
-
filename_from_content_disposition ||
|
|
150
|
+
Utils.filename_from_content_disposition(headers["Content-Disposition"]) ||
|
|
151
|
+
Utils.filename_from_path(HTTP::URI.parse(url).path)
|
|
151
152
|
end
|
|
152
153
|
|
|
153
154
|
def content_type
|
|
154
|
-
|
|
155
|
+
HTTP::ContentType.parse(headers["Content-Type"]).mime_type
|
|
155
156
|
end
|
|
156
157
|
|
|
157
158
|
def charset
|
|
158
|
-
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
private
|
|
162
|
-
|
|
163
|
-
def content_type_header
|
|
164
|
-
HTTP::ContentType.parse(headers["Content-Type"])
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def filename_from_content_disposition
|
|
168
|
-
content_disposition = headers["Content-Disposition"].to_s
|
|
169
|
-
filename = content_disposition[/filename="([^"]*)"/, 1] || content_disposition[/filename=(.+)/, 1]
|
|
170
|
-
filename = CGI.unescape(filename.to_s.strip)
|
|
171
|
-
filename unless filename.empty?
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def filename_from_url
|
|
175
|
-
path = HTTP::URI.parse(url).path
|
|
176
|
-
filename = path.split("/").last
|
|
177
|
-
CGI.unescape(filename) if filename
|
|
159
|
+
HTTP::ContentType.parse(headers["Content-Type"]).charset
|
|
178
160
|
end
|
|
179
161
|
end
|
|
180
162
|
end
|
data/lib/down/net_http.rb
CHANGED
|
@@ -266,27 +266,13 @@ module Down
|
|
|
266
266
|
|
|
267
267
|
module DownloadedFile
|
|
268
268
|
def original_filename
|
|
269
|
-
filename_from_content_disposition ||
|
|
269
|
+
Utils.filename_from_content_disposition(meta["content-disposition"]) ||
|
|
270
|
+
Utils.filename_from_path(base_uri.path)
|
|
270
271
|
end
|
|
271
272
|
|
|
272
273
|
def content_type
|
|
273
274
|
super unless meta["content-type"].to_s.empty?
|
|
274
275
|
end
|
|
275
|
-
|
|
276
|
-
private
|
|
277
|
-
|
|
278
|
-
def filename_from_content_disposition
|
|
279
|
-
content_disposition = meta["content-disposition"].to_s
|
|
280
|
-
filename = content_disposition[/filename="([^"]*)"/, 1] || content_disposition[/filename=(.+)/, 1]
|
|
281
|
-
filename = CGI.unescape(filename.to_s.strip)
|
|
282
|
-
filename unless filename.empty?
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def filename_from_uri
|
|
286
|
-
path = base_uri.path
|
|
287
|
-
filename = path.split("/").last
|
|
288
|
-
CGI.unescape(filename) if filename
|
|
289
|
-
end
|
|
290
276
|
end
|
|
291
277
|
end
|
|
292
278
|
end
|
data/lib/down/utils.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "cgi"
|
|
2
|
+
|
|
3
|
+
module Down
|
|
4
|
+
module Utils
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def filename_from_content_disposition(content_disposition)
|
|
8
|
+
content_disposition = content_disposition.to_s
|
|
9
|
+
|
|
10
|
+
filename = content_disposition[/filename="([^"]*)"/, 1] || content_disposition[/filename=(.+)/, 1]
|
|
11
|
+
filename = CGI.unescape(filename.to_s.strip)
|
|
12
|
+
|
|
13
|
+
filename unless filename.empty?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def filename_from_path(path)
|
|
17
|
+
filename = path.split("/").last
|
|
18
|
+
CGI.unescape(filename) if filename
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/down/version.rb
CHANGED
data/lib/down/wget.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "open3"
|
|
5
|
-
else
|
|
3
|
+
begin
|
|
6
4
|
require "posix-spawn"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
require "open3"
|
|
7
7
|
end
|
|
8
8
|
require "http_parser"
|
|
9
9
|
|
|
@@ -209,7 +209,8 @@ module Down
|
|
|
209
209
|
attr_accessor :url, :headers
|
|
210
210
|
|
|
211
211
|
def original_filename
|
|
212
|
-
filename_from_content_disposition ||
|
|
212
|
+
Utils.filename_from_content_disposition(headers["Content-Disposition"]) ||
|
|
213
|
+
Utils.filename_from_path(URI.parse(url).path)
|
|
213
214
|
end
|
|
214
215
|
|
|
215
216
|
def content_type
|
|
@@ -219,21 +220,6 @@ module Down
|
|
|
219
220
|
def charset
|
|
220
221
|
headers["Content-Type"].to_s[/;\s*charset=([^;]+)/i, 1]
|
|
221
222
|
end
|
|
222
|
-
|
|
223
|
-
private
|
|
224
|
-
|
|
225
|
-
def filename_from_content_disposition
|
|
226
|
-
content_disposition = headers["Content-Disposition"].to_s
|
|
227
|
-
filename = content_disposition[/filename="([^"]*)"/, 1] || content_disposition[/filename=(.+)/, 1]
|
|
228
|
-
filename = CGI.unescape(filename.to_s.strip)
|
|
229
|
-
filename unless filename.empty?
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def filename_from_url
|
|
233
|
-
path = URI(url).path
|
|
234
|
-
filename = path.split("/").last
|
|
235
|
-
CGI.unescape(filename) if filename
|
|
236
|
-
end
|
|
237
223
|
end
|
|
238
224
|
end
|
|
239
225
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: down
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Janko Marohnić
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2018-
|
|
11
|
+
date: 2018-05-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -28,16 +28,16 @@ dependencies:
|
|
|
28
28
|
name: mocha
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- - "
|
|
31
|
+
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
33
|
+
version: '1.5'
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
|
-
- - "
|
|
38
|
+
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
40
|
+
version: '1.5'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: rake
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -125,6 +125,7 @@ files:
|
|
|
125
125
|
- lib/down/errors.rb
|
|
126
126
|
- lib/down/http.rb
|
|
127
127
|
- lib/down/net_http.rb
|
|
128
|
+
- lib/down/utils.rb
|
|
128
129
|
- lib/down/version.rb
|
|
129
130
|
- lib/down/wget.rb
|
|
130
131
|
homepage: https://github.com/janko-m/down
|
|
@@ -147,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
147
148
|
version: '0'
|
|
148
149
|
requirements: []
|
|
149
150
|
rubyforge_project:
|
|
150
|
-
rubygems_version: 2.6
|
|
151
|
+
rubygems_version: 2.7.6
|
|
151
152
|
signing_key:
|
|
152
153
|
specification_version: 4
|
|
153
154
|
summary: Robust streaming downloads using Net::HTTP, HTTP.rb or wget.
|