rffw 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.md +60 -0
- data/Rakefile +41 -0
- data/bin/rffw +16 -0
- data/lib/rffw.rb +7 -0
- data/lib/rffw/app.rb +36 -0
- data/lib/rffw/app/app_handler.rb +59 -0
- data/lib/rffw/app/data/images/bg.png +0 -0
- data/lib/rffw/app/data/index.html +56 -0
- data/lib/rffw/app/data/javascripts/application.js +110 -0
- data/lib/rffw/app/data/stylesheets/style.css +338 -0
- data/lib/rffw/app/data/template.html +42 -0
- data/lib/rffw/app/db.rb +19 -0
- data/lib/rffw/app/description_handler.rb +12 -0
- data/lib/rffw/app/dir_handler.rb +36 -0
- data/lib/rffw/app/record.rb +43 -0
- data/lib/rffw/app/show_handler.rb +25 -0
- data/lib/rffw/app/upload_handler.rb +32 -0
- data/lib/rffw/app/upload_status_handler.rb +40 -0
- data/lib/rffw/app/urlencode_chars.data +224 -0
- data/lib/rffw/app/view_helpers.rb +30 -0
- data/lib/rffw/parser.rb +9 -0
- data/lib/rffw/parser/http_request.rb +137 -0
- data/lib/rffw/parser/http_response.rb +29 -0
- data/lib/rffw/parser/mime_parser.rb +46 -0
- data/lib/rffw/server.rb +15 -0
- data/lib/rffw/server/buffered_client.rb +57 -0
- data/lib/rffw/server/client.rb +53 -0
- data/lib/rffw/server/http_client.rb +49 -0
- data/lib/rffw/server/server.rb +46 -0
- data/lib/rffw/version.rb +3 -0
- data/rffw.db +0 -0
- data/rffw.gemspec +21 -0
- data/test/fixtures/image.jpg +0 -0
- data/test/fixtures/mime_image.data +0 -0
- data/test/fixtures/mime_image.png +0 -0
- data/test/fixtures/post_form_data.http +14 -0
- data/test/fixtures/raw_request.txt +8 -0
- data/test/fixtures/request.http +9 -0
- data/test/fixtures/upload.http +0 -0
- data/test/fixtures/upload_status_request.http +10 -0
- data/test/helper.rb +97 -0
- data/test/test_buffered_client.rb +50 -0
- data/test/test_client.rb +47 -0
- data/test/test_db.rb +21 -0
- data/test/test_http_client.rb +26 -0
- data/test/test_http_request.rb +52 -0
- data/test/test_mime_parser.rb +22 -0
- data/test/test_record.rb +49 -0
- data/test/test_upload_status_handler.rb +18 -0
- data/test/test_uploader_handler.rb +10 -0
- data/test/test_view_helpers.rb +24 -0
- metadata +122 -0
data/lib/rffw/version.rb
ADDED
data/rffw.db
ADDED
Binary file
|
data/rffw.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "rffw/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "rffw"
|
7
|
+
s.version = RFFW::VERSION
|
8
|
+
s.authors = ["Guillermo Álvarez"]
|
9
|
+
s.email = ["guillermo@cientifico.net"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Recive files from webserver}
|
12
|
+
s.description = %q{A simple program that listen at port 80 for uploads, and save the uploaded files to disk}
|
13
|
+
|
14
|
+
s.rubyforge_project = "rffw"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,14 @@
|
|
1
|
+
POST /description HTTP/1.1
|
2
|
+
Host: localhost:8080
|
3
|
+
Origin: http://localhost:8080
|
4
|
+
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50
|
5
|
+
Content-Type: application/x-www-form-urlencoded
|
6
|
+
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
7
|
+
Referer: http://localhost:8080/
|
8
|
+
Accept-Language: en-us
|
9
|
+
Accept-Encoding: gzip, deflate
|
10
|
+
Cookie: __utmz=111872281.1311554639.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=111872281.2010257729.1311554639.1312453997.1312460077.19
|
11
|
+
Content-Length: 16
|
12
|
+
Connection: keep-alive
|
13
|
+
|
14
|
+
description=hola
|
@@ -0,0 +1,8 @@
|
|
1
|
+
GET / HTTP/1.1
|
2
|
+
Host: localhost:9999
|
3
|
+
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1
|
4
|
+
Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
|
5
|
+
Accept-Language: en-us
|
6
|
+
Accept-Encoding: gzip, deflate
|
7
|
+
Connection: keep-alive
|
8
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
GET / HTTP/1.1
|
2
|
+
Host: localhost:8081
|
3
|
+
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50
|
4
|
+
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
5
|
+
Accept-Language: en-us
|
6
|
+
Accept-Encoding: gzip, deflate
|
7
|
+
Cookie: __utmz=111872281.1311554639.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=111872281.2010257729.1311554639.1312453997.1312460077.19
|
8
|
+
Connection: keep-alive
|
9
|
+
|
Binary file
|
@@ -0,0 +1,10 @@
|
|
1
|
+
GET /upload_status.js?81242840-2736-2e05-7316-1c1ee25f7cff HTTP/1.1
|
2
|
+
Host: localhost:8080
|
3
|
+
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50
|
4
|
+
Accept: */*
|
5
|
+
Referer: http://localhost:8080/
|
6
|
+
Accept-Language: en-us
|
7
|
+
Accept-Encoding: gzip, deflate
|
8
|
+
Cookie: __utmz=111872281.1311554639.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=111872281.2010257729.1311554639.1312453997.1312460077.19
|
9
|
+
Connection: keep-alive
|
10
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'minitest/unit'
|
2
|
+
require 'minitest/mock'
|
3
|
+
require 'rffw'
|
4
|
+
require 'stringio'
|
5
|
+
require 'tmpdir'
|
6
|
+
|
7
|
+
class MiniTest::Unit::TestCase
|
8
|
+
|
9
|
+
def fixture(name)
|
10
|
+
File.expand_path("../fixtures/#{name}", __FILE__)
|
11
|
+
end
|
12
|
+
|
13
|
+
def read(name)
|
14
|
+
File.read(fixture(name))
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
class IntegrationTest < MiniTest::Unit::TestCase
|
21
|
+
|
22
|
+
def setup
|
23
|
+
@port = 64444
|
24
|
+
@server_pid = fork do
|
25
|
+
$stdout = File.open("/dev/null",'w')
|
26
|
+
$stderr = $stdout
|
27
|
+
RFFW::App.start(@port,"127.0.0.1", Dir.mktmpdir("rffw_test"))
|
28
|
+
exit(0)
|
29
|
+
end
|
30
|
+
sleep 0.1
|
31
|
+
end
|
32
|
+
|
33
|
+
def teardown
|
34
|
+
Process.kill('SIGINT', @server_pid)
|
35
|
+
Process.wait(@server_pid)
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
def upload_file(name)
|
41
|
+
Fiber.new do
|
42
|
+
fixture = read(name)
|
43
|
+
socket = TCPSocket.new('127.0.0.1', @port)
|
44
|
+
socket.write fixture[0..(fixture.size/3)]
|
45
|
+
Fiber.yield
|
46
|
+
|
47
|
+
socket.write fixture[(fixture.size/3)..(fixture.size/3*2)]
|
48
|
+
Fiber.yield
|
49
|
+
|
50
|
+
socket.write fixture[(fixture.size/3*2)..-1]
|
51
|
+
Fiber.yield
|
52
|
+
|
53
|
+
begin
|
54
|
+
data = socket.read_nonblock(1024*1024*1024)
|
55
|
+
rescue Errno::EAGAIN
|
56
|
+
retry
|
57
|
+
end
|
58
|
+
|
59
|
+
Fiber.yield data
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def send_request(name)
|
64
|
+
socket = TCPSocket.new('127.0.0.1', @port)
|
65
|
+
socket.write read(name)
|
66
|
+
begin
|
67
|
+
content = socket.read_nonblock(1024*1024*1024).tap{ socket.close }.split("\r\n").last.strip
|
68
|
+
rescue EOFError,Errno::EAGAIN
|
69
|
+
retry
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def upload_file(name)
|
74
|
+
Fiber.new do
|
75
|
+
fixture = read(name)
|
76
|
+
socket = TCPSocket.new('127.0.0.1', @port)
|
77
|
+
socket.write fixture[0..(fixture.size/3)]
|
78
|
+
Fiber.yield
|
79
|
+
|
80
|
+
socket.write fixture[(fixture.size/3)..(fixture.size/3*2)]
|
81
|
+
Fiber.yield
|
82
|
+
|
83
|
+
socket.write fixture[(fixture.size/3*2)..-1]
|
84
|
+
Fiber.yield
|
85
|
+
|
86
|
+
data = nil
|
87
|
+
while(data == nil)
|
88
|
+
data = socket.read_nonblock(1000000) rescue nil
|
89
|
+
end
|
90
|
+
data
|
91
|
+
|
92
|
+
Fiber.yield data
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class RFFW::Server::BufferedClientTest < MiniTest::Unit::TestCase
|
4
|
+
include RFFW::Server
|
5
|
+
def setup
|
6
|
+
@socket = StringIO.new
|
7
|
+
@client = BufferedClient.new(@socket)
|
8
|
+
def @client.read_nonblock(arg) ; "h"*1000 end
|
9
|
+
def @client.close ; end
|
10
|
+
end
|
11
|
+
|
12
|
+
def teardown
|
13
|
+
BufferedClient.disconnect_all!
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_on_small_data
|
17
|
+
@client.on_data
|
18
|
+
assert_equal "h"*1000, @client.buffer
|
19
|
+
assert !@client.usign_temp_file?
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_on_data
|
23
|
+
def @client.read_nonblock(arg) ; "h"*2000 end
|
24
|
+
@client.on_data
|
25
|
+
assert_equal "h"*2000, @client.buffer
|
26
|
+
assert @client.usign_temp_file?
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_on_data_appending
|
30
|
+
@client.on_data
|
31
|
+
assert !@client.usign_temp_file?
|
32
|
+
@client.on_data
|
33
|
+
assert @client.usign_temp_file?
|
34
|
+
assert_equal "h"*2000, @client.buffer
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_clear_buffer
|
38
|
+
@client.on_data
|
39
|
+
@client.clear_buffer
|
40
|
+
assert_equal '', @client.buffer
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_remove_file_on_clear_buffer
|
44
|
+
@client.on_data
|
45
|
+
@client.clear_buffer
|
46
|
+
assert !@client.usign_temp_file?
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
end
|
data/test/test_client.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class RFFW::Server::ClientTest < MiniTest::Unit::TestCase
|
4
|
+
include RFFW::Server
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@socket = StringIO.new
|
8
|
+
def @socket.close; nil end
|
9
|
+
end
|
10
|
+
|
11
|
+
def teardown
|
12
|
+
Client.disconnect_all!
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_initialize
|
16
|
+
client = Client.new(@socket)
|
17
|
+
assert client.instance_of?(Client)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_find
|
21
|
+
client = Client.new(@socket)
|
22
|
+
assert_equal Client.find(client), client
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_is_enumerable
|
26
|
+
assert Client.singleton_class.included_modules.include?(Enumerable)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_each
|
30
|
+
expected = [Client.new(@socket), Client.new(@socket)]
|
31
|
+
result = []
|
32
|
+
Client.each{|c| result << c }
|
33
|
+
assert_equal expected, result
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_all
|
37
|
+
expected = [Client.new(@socket), Client.new(@socket)]
|
38
|
+
assert_equal expected, Client.all
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_on_disconnect
|
42
|
+
client = Client.new(@socket)
|
43
|
+
assert Client.all.any?
|
44
|
+
client.disconnect!
|
45
|
+
assert Client.all.empty?
|
46
|
+
end
|
47
|
+
end
|
data/test/test_db.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class RFFW::App::DbTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
include RFFW::App
|
6
|
+
|
7
|
+
def setup
|
8
|
+
Dir.chdir(Dir.mktmpdir("rffw-test")) do
|
9
|
+
@db = Db.new('test_db')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def test_get_set
|
15
|
+
assert_equal( {}, @db['12345'])
|
16
|
+
assert_equal( {:a => 3}, (@db['12345'] = {:a => 3}), 'should retun value on set')
|
17
|
+
assert_equal( {:a => 3}, @db['12345'], 'should return previous stored value on get')
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class RFFW::Server::HttpClientTest < MiniTest::Unit::TestCase
|
4
|
+
include RFFW::Server
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@mock_socket = MiniTest::Mock.new
|
8
|
+
@mock_socket.expect(:close, nil)
|
9
|
+
@mock_socket.expect(:read_nonblock, File.read(fixture('raw_request.txt')),[String])
|
10
|
+
@mock_socket.expect(:closed?, nil)
|
11
|
+
@mock_socket.expect(:write, nil,[String])
|
12
|
+
end
|
13
|
+
|
14
|
+
def teardown
|
15
|
+
HttpClient.disconnect_all!
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_request
|
19
|
+
client = HttpClient.new(@mock_socket)
|
20
|
+
def client.buffer; File.read(File.expand_path("../fixtures/raw_request.txt",__FILE__)) ; end
|
21
|
+
client.on_data
|
22
|
+
assert File.read(fixture("raw_request.txt")), client.on_data
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class RFFW::Server::HttpRequestTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
include RFFW::Parser
|
6
|
+
|
7
|
+
def setup
|
8
|
+
@simple_request = File.read(File.expand_path("../fixtures/raw_request.txt",__FILE__))
|
9
|
+
@request_headers = {"HTTP_METHOD"=>"GET",
|
10
|
+
"HTTP_PATH"=>"/",
|
11
|
+
"HTTP_VERSION"=>"1.1",
|
12
|
+
"Host"=>"localhost:9999",
|
13
|
+
"User-Agent"=>
|
14
|
+
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1",
|
15
|
+
"Accept"=>
|
16
|
+
"application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
|
17
|
+
"Accept-Language"=>"en-us",
|
18
|
+
"Accept-Encoding"=>"gzip, deflate",
|
19
|
+
"Connection"=>"keep-alive"}
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_parse_simple_request
|
23
|
+
request = HttpRequest.new(@simple_request.dup)
|
24
|
+
assert request.headers.is_a?(Hash), "headers should be a hash"
|
25
|
+
assert request.body.is_a?(String), "body should be a string"
|
26
|
+
assert request.finish?, "request should be finished"
|
27
|
+
assert_equal(@request_headers, request.headers)
|
28
|
+
%w(attachments upload_content_lenght body_size upload_progress).each do |method|
|
29
|
+
assert_raises(HttpRequest::OnlyForPosts, "#{method} should raise exception for get requests.") { request.send(method.to_sym) }
|
30
|
+
end
|
31
|
+
|
32
|
+
assert_equal "1.1", request.version
|
33
|
+
assert_equal "/", request.path
|
34
|
+
assert_equal true, request.keep_alive?
|
35
|
+
assert_equal "GET", request.method
|
36
|
+
assert_equal '', request.query
|
37
|
+
assert_equal '/', request.url
|
38
|
+
assert_kind_of URI, request.uri
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_incomplete_request
|
42
|
+
raw_request = @simple_request[0..(@simple_request.size/2)]
|
43
|
+
request = HttpRequest.new(raw_request)
|
44
|
+
assert !request.finish?
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_post_form_data
|
48
|
+
request = HttpRequest.new(read('post_form_data.http'))
|
49
|
+
assert_equal( {"description"=>"hola"}, request.post_form_data)
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class RFFW::Server::MimeTest < MiniTest::Unit::TestCase
|
4
|
+
include RFFW::Parser
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@data = File.read(File.expand_path('../fixtures/mime_image.data', __FILE__))
|
8
|
+
@image_data = File.read(File.expand_path('../fixtures/mime_image.png', __FILE__)).force_encoding("BINARY")
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_mime_parser
|
12
|
+
attachments = MimeParser.parse(@data)
|
13
|
+
attachment = attachments.first
|
14
|
+
expected_header = "form-data; name=\"file\"; filename=\"Screen shot 2011-08-05 at 5.42.34 AM.png\""
|
15
|
+
image_type = "image/png"
|
16
|
+
|
17
|
+
assert_equal 1, attachments.size
|
18
|
+
assert_equal expected_header, attachment.content_disposition
|
19
|
+
assert_equal image_type, attachment.content_type
|
20
|
+
assert_equal @image_data, attachment.to_s, "to be equal"
|
21
|
+
end
|
22
|
+
end
|
data/test/test_record.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class RecordTest < MiniTest::Unit::TestCase
|
4
|
+
include RFFW::App
|
5
|
+
|
6
|
+
def setup
|
7
|
+
RFFW::App.start_db(Dir.mktmpdir("rffw_test"))
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_update_or_create_by_id_on_create
|
11
|
+
record = Record.update_or_create_by_id('1234', {"mime_type" => "text/plain", "filename" => "hello_world.txt"}, 'HOLA MUNDO')
|
12
|
+
assert_equal '1234', record.id
|
13
|
+
assert_equal 'text/plain', record.mime_type
|
14
|
+
assert_equal 'hello_world.txt', record.filename
|
15
|
+
assert_equal 'HOLA MUNDO', record.body
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
def test_update_or_create_by_id_on_update
|
21
|
+
record = Record.update_or_create_by_id('1234', {"mime_type" => "text/plain", "filename" => "hello_world.txt"}, 'HOLA MUNDO')
|
22
|
+
record = Record.update_or_create_by_id('1234', {"description" => "good file"})
|
23
|
+
|
24
|
+
assert_equal 'good file', record.description
|
25
|
+
assert_equal '1234', record.id
|
26
|
+
assert_equal 'text/plain', record.mime_type
|
27
|
+
assert_equal 'hello_world.txt', record.filename
|
28
|
+
assert_equal 'HOLA MUNDO', record.body
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_find_by_id
|
32
|
+
record = Record.update_or_create_by_id('1234', {"mime_type" => "text/plain", "filename" => "hello_world.txt"}, 'HOLA MUNDO')
|
33
|
+
|
34
|
+
record = Record.find_by_id('1234')
|
35
|
+
assert_equal nil, record.description
|
36
|
+
assert_equal '1234', record.id
|
37
|
+
assert_equal 'text/plain', record.mime_type
|
38
|
+
assert_equal 'hello_world.txt', record.filename
|
39
|
+
assert_equal 'HOLA MUNDO', record.body
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_new
|
43
|
+
record = Record.new('123', {'asdf' => 3})
|
44
|
+
assert record.new?, "should be new"
|
45
|
+
|
46
|
+
record = Record.update_or_create_by_id('890123', {'asdf' => 3})
|
47
|
+
assert !record.new?, "should not be new on existing record"
|
48
|
+
end
|
49
|
+
end
|