Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
ruanhaishen
redis
Commits
3113921a
Commit
3113921a
authored
Sep 02, 2009
by
antirez
Browse files
Ruby client lib updated to the latest git version
parent
123d7c93
Changes
9
Show whitespace changes
Inline
Side-by-side
client-libraries/ruby/Rakefile
View file @
3113921a
...
@@ -9,7 +9,7 @@ require 'tasks/redis.tasks'
...
@@ -9,7 +9,7 @@ require 'tasks/redis.tasks'
GEM
=
'redis'
GEM
=
'redis'
GEM_NAME
=
'redis'
GEM_NAME
=
'redis'
GEM_VERSION
=
'0.1'
GEM_VERSION
=
'0.1'
AUTHORS
=
[
'Ezra Zygmuntowicz'
,
'Taylor Weibley'
,
'Matthew Clark'
]
AUTHORS
=
[
'Ezra Zygmuntowicz'
,
'Taylor Weibley'
,
'Matthew Clark'
,
'Brian McKinney'
,
'Salvatore Sanfilippo'
,
'Luca Guidi'
]
EMAIL
=
"ez@engineyard.com"
EMAIL
=
"ez@engineyard.com"
HOMEPAGE
=
"http://github.com/ezmobius/redis-rb"
HOMEPAGE
=
"http://github.com/ezmobius/redis-rb"
SUMMARY
=
"Ruby client library for redis key value storage server"
SUMMARY
=
"Ruby client library for redis key value storage server"
...
@@ -28,7 +28,7 @@ spec = Gem::Specification.new do |s|
...
@@ -28,7 +28,7 @@ spec = Gem::Specification.new do |s|
s
.
add_dependency
"rspec"
s
.
add_dependency
"rspec"
s
.
require_path
=
'lib'
s
.
require_path
=
'lib'
s
.
autorequire
=
GEM
s
.
autorequire
=
GEM
s
.
files
=
%w(LICENSE README.markdown Rakefile)
+
Dir
.
glob
(
"{lib,spec}/**/*"
)
s
.
files
=
%w(LICENSE README.markdown Rakefile)
+
Dir
.
glob
(
"{lib,
tasks,
spec}/**/*"
)
end
end
task
:default
=>
:spec
task
:default
=>
:spec
...
...
client-libraries/ruby/examples/test_server.rb
deleted
100644 → 0
View file @
123d7c93
require
'socket'
require
'pp'
require
File
.
join
(
File
.
dirname
(
__FILE__
),
'../lib/redis'
)
#require File.join(File.dirname(__FILE__), '../lib/server')
#r = Redis.new
#loop do
# puts "--------------------------------------"
# sleep 12
#end
\ No newline at end of file
client-libraries/ruby/lib/dist_redis.rb
View file @
3113921a
...
@@ -12,16 +12,14 @@ class DistRedis
...
@@ -12,16 +12,14 @@ class DistRedis
opts
[
:hosts
].
each
do
|
h
|
opts
[
:hosts
].
each
do
|
h
|
host
,
port
=
h
.
split
(
':'
)
host
,
port
=
h
.
split
(
':'
)
hosts
<<
Redis
.
new
(
:host
=>
host
,
:port
=>
port
,
:db
=>
db
,
:timeout
=>
timeout
,
:db
=>
db
)
hosts
<<
Redis
.
new
(
:host
=>
host
,
:port
=>
port
,
:db
=>
db
,
:timeout
=>
timeout
)
end
end
@ring
=
HashRing
.
new
hosts
@ring
=
HashRing
.
new
hosts
end
end
def
node_for_key
(
key
)
def
node_for_key
(
key
)
if
key
=~
/\{(.*)?\}/
key
=
$1
if
key
=~
/\{(.*)?\}/
key
=
$1
end
@ring
.
get_node
(
key
)
@ring
.
get_node
(
key
)
end
end
...
@@ -39,30 +37,32 @@ class DistRedis
...
@@ -39,30 +37,32 @@ class DistRedis
end
end
def
keys
(
glob
)
def
keys
(
glob
)
keyz
=
[]
@ring
.
nodes
.
map
do
|
red
|
@ring
.
nodes
.
each
do
|
red
|
red
.
keys
(
glob
)
keyz
.
concat
red
.
keys
(
glob
)
end
end
keyz
end
end
def
save
def
save
@ring
.
nodes
.
each
do
|
red
|
on_each_node
:save
red
.
save
end
end
end
def
bgsave
def
bgsave
@ring
.
nodes
.
each
do
|
red
|
on_each_node
:bgsave
red
.
bgsave
end
end
end
def
quit
def
quit
@ring
.
nodes
.
each
do
|
red
|
on_each_node
:quit
red
.
quit
end
end
def
flush_all
on_each_node
:flush_all
end
alias_method
:flushall
,
:flush_all
def
flush_db
on_each_node
:flush_db
end
end
alias_method
:flushdb
,
:flush_db
def
delete_cloud!
def
delete_cloud!
@ring
.
nodes
.
each
do
|
red
|
@ring
.
nodes
.
each
do
|
red
|
...
@@ -72,6 +72,12 @@ class DistRedis
...
@@ -72,6 +72,12 @@ class DistRedis
end
end
end
end
def
on_each_node
(
command
,
*
args
)
@ring
.
nodes
.
each
do
|
red
|
red
.
send
(
command
,
*
args
)
end
end
end
end
...
...
client-libraries/ruby/lib/hash_ring.rb
View file @
3113921a
...
@@ -31,8 +31,9 @@ class HashRing
...
@@ -31,8 +31,9 @@ class HashRing
end
end
def
remove_node
(
node
)
def
remove_node
(
node
)
@nodes
.
reject!
{
|
n
|
n
.
to_s
==
node
.
to_s
}
@replicas
.
times
do
|
i
|
@replicas
.
times
do
|
i
|
key
=
Zlib
.
crc32
(
"
#{
node
}
:
#{
count
}
"
)
key
=
Zlib
.
crc32
(
"
#{
node
}
:
#{
i
}
"
)
@ring
.
delete
(
key
)
@ring
.
delete
(
key
)
@sorted_keys
.
reject!
{
|
k
|
k
==
key
}
@sorted_keys
.
reject!
{
|
k
|
k
==
key
}
end
end
...
...
client-libraries/ruby/lib/redis.rb
View file @
3113921a
...
@@ -36,7 +36,7 @@ class Redis
...
@@ -36,7 +36,7 @@ class Redis
"smove"
=>
true
"smove"
=>
true
}
}
BOOLEAN_PROCESSOR
=
lambda
{
|
r
|
r
==
0
?
false
:
r
}
BOOLEAN_PROCESSOR
=
lambda
{
|
r
|
r
==
1
}
REPLY_PROCESSOR
=
{
REPLY_PROCESSOR
=
{
"exists"
=>
BOOLEAN_PROCESSOR
,
"exists"
=>
BOOLEAN_PROCESSOR
,
...
@@ -95,21 +95,34 @@ class Redis
...
@@ -95,21 +95,34 @@ class Redis
"type?"
=>
"type"
"type?"
=>
"type"
}
}
DISABLED_COMMANDS
=
{
"monitor"
=>
true
,
"sync"
=>
true
}
def
initialize
(
options
=
{})
def
initialize
(
options
=
{})
@host
=
options
[
:host
]
||
'127.0.0.1'
@host
=
options
[
:host
]
||
'127.0.0.1'
@port
=
(
options
[
:port
]
||
6379
).
to_i
@port
=
(
options
[
:port
]
||
6379
).
to_i
@db
=
(
options
[
:db
]
||
0
).
to_i
@db
=
(
options
[
:db
]
||
0
).
to_i
@timeout
=
(
options
[
:timeout
]
||
5
).
to_i
@timeout
=
(
options
[
:timeout
]
||
5
).
to_i
$debug
=
options
[
:debug
]
@password
=
options
[
:password
]
@logger
=
options
[
:logger
]
@logger
.
info
{
self
.
to_s
}
if
@logger
connect_to_server
connect_to_server
end
end
def
to_s
def
to_s
"Redis Client connected to
#{
@host
}
:
#{
@port
}
against DB
#{
@db
}
"
"Redis Client connected to
#{
server
}
against DB
#{
@db
}
"
end
def
server
"
#{
@host
}
:
#{
@port
}
"
end
end
def
connect_to_server
def
connect_to_server
@sock
=
connect_to
(
@host
,
@port
,
@timeout
==
0
?
nil
:
@timeout
)
@sock
=
connect_to
(
@host
,
@port
,
@timeout
==
0
?
nil
:
@timeout
)
call_command
([
"auth"
,
@password
])
if
@password
call_command
([
"select"
,
@db
])
unless
@db
==
0
call_command
([
"select"
,
@db
])
unless
@db
==
0
end
end
...
@@ -147,15 +160,18 @@ class Redis
...
@@ -147,15 +160,18 @@ class Redis
end
end
def
call_command
(
argv
)
def
call_command
(
argv
)
puts
argv
.
inspect
if
$debug
@logger
.
debug
{
argv
.
inspect
}
if
@logger
# this wrapper to raw_call_command handle reconnection on socket
# this wrapper to raw_call_command handle reconnection on socket
# error. We try to reconnect just one time, otherwise let the error
# error. We try to reconnect just one time, otherwise let the error
# araise.
# araise.
connect_to_server
if
!
@sock
connect_to_server
if
!
@sock
begin
begin
raw_call_command
(
argv
.
dup
)
raw_call_command
(
argv
.
dup
)
rescue
Errno
::
ECONNRESET
,
Errno
::
EPIPE
rescue
Errno
::
ECONNRESET
,
Errno
::
EPIPE
@sock
.
close
@sock
.
close
@sock
=
nil
connect_to_server
connect_to_server
raw_call_command
(
argv
.
dup
)
raw_call_command
(
argv
.
dup
)
end
end
...
@@ -176,12 +192,13 @@ class Redis
...
@@ -176,12 +192,13 @@ class Redis
bulk
=
nil
bulk
=
nil
argv
[
0
]
=
argv
[
0
].
to_s
.
downcase
argv
[
0
]
=
argv
[
0
].
to_s
.
downcase
argv
[
0
]
=
ALIASES
[
argv
[
0
]]
if
ALIASES
[
argv
[
0
]]
argv
[
0
]
=
ALIASES
[
argv
[
0
]]
if
ALIASES
[
argv
[
0
]]
raise
"
#{
argv
[
0
]
}
command is disabled"
if
DISABLED_COMMANDS
[
argv
[
0
]]
if
BULK_COMMANDS
[
argv
[
0
]]
and
argv
.
length
>
1
if
BULK_COMMANDS
[
argv
[
0
]]
and
argv
.
length
>
1
bulk
=
argv
[
-
1
].
to_s
bulk
=
argv
[
-
1
].
to_s
argv
[
-
1
]
=
bulk
.
length
argv
[
-
1
]
=
bulk
.
respond_to?
(
:bytesize
)
?
bulk
.
bytesize
:
bulk
.
size
end
end
command
<<
argv
.
join
(
' '
)
+
"
\r\n
"
command
<<
"
#{
argv
.
join
(
' '
)
}
\r\n
"
command
<<
bulk
+
"
\r\n
"
if
bulk
command
<<
"
#{
bulk
}
\r\n
"
if
bulk
end
end
@sock
.
write
(
command
)
@sock
.
write
(
command
)
...
@@ -199,7 +216,7 @@ class Redis
...
@@ -199,7 +216,7 @@ class Redis
end
end
def
[]
(
key
)
def
[]
(
key
)
get
(
key
)
self
.
get
(
key
)
end
end
def
[]=
(
key
,
value
)
def
[]=
(
key
,
value
)
...
@@ -213,8 +230,8 @@ class Redis
...
@@ -213,8 +230,8 @@ class Redis
end
end
def
sort
(
key
,
options
=
{})
def
sort
(
key
,
options
=
{})
cmd
=
[]
cmd
=
[
"SORT"
]
cmd
<<
"SORT
#{
key
}
"
cmd
<<
key
cmd
<<
"BY
#{
options
[
:by
]
}
"
if
options
[
:by
]
cmd
<<
"BY
#{
options
[
:by
]
}
"
if
options
[
:by
]
cmd
<<
"GET
#{
[
options
[
:get
]].
flatten
*
' GET '
}
"
if
options
[
:get
]
cmd
<<
"GET
#{
[
options
[
:get
]].
flatten
*
' GET '
}
"
if
options
[
:get
]
cmd
<<
"
#{
options
[
:order
]
}
"
if
options
[
:order
]
cmd
<<
"
#{
options
[
:order
]
}
"
if
options
[
:order
]
...
@@ -230,6 +247,15 @@ class Redis
...
@@ -230,6 +247,15 @@ class Redis
call_command
(
decrement
?
[
"decrby"
,
key
,
decrement
]
:
[
"decr"
,
key
])
call_command
(
decrement
?
[
"decrby"
,
key
,
decrement
]
:
[
"decr"
,
key
])
end
end
# Similar to memcache.rb's #get_multi, returns a hash mapping
# keys to values.
def
mapped_mget
(
*
keys
)
mget
(
*
keys
).
inject
({})
do
|
hash
,
value
|
key
=
keys
.
shift
value
.
nil?
?
hash
:
hash
.
merge
(
key
=>
value
)
end
end
# Ruby defines a now deprecated type method so we need to override it here
# Ruby defines a now deprecated type method so we need to override it here
# since it will never hit method_missing
# since it will never hit method_missing
def
type
(
key
)
def
type
(
key
)
...
...
client-libraries/ruby/redis-rb.gemspec
View file @
3113921a
...
@@ -2,18 +2,18 @@
...
@@ -2,18 +2,18 @@
Gem
::
Specification
.
new
do
|
s
|
Gem
::
Specification
.
new
do
|
s
|
s
.
name
=
%q{redis}
s
.
name
=
%q{redis}
s
.
version
=
"0.
0.5
"
s
.
version
=
"0.
1
"
s
.
required_rubygems_version
=
Gem
::
Requirement
.
new
(
">= 0"
)
if
s
.
respond_to?
:required_rubygems_version
=
s
.
required_rubygems_version
=
Gem
::
Requirement
.
new
(
">= 0"
)
if
s
.
respond_to?
:required_rubygems_version
=
s
.
authors
=
[
"Ezra Zygmuntowicz"
,
"Taylor Weibley"
,
"Matthew Clark"
]
s
.
authors
=
[
"Ezra Zygmuntowicz"
,
"Taylor Weibley"
,
"Matthew Clark"
,
"Brian McKinney"
,
"Salvatore Sanfilippo"
,
"Luca Guidi"
]
#s.autorequire = %q{redis}
#
s.autorequire = %q{redis
-rb
}
s
.
date
=
%q{2009-0
3-31
}
s
.
date
=
%q{2009-0
6-23
}
s
.
description
=
%q{Ruby client library for redis key value storage server}
s
.
description
=
%q{Ruby client library for redis key value storage server}
s
.
email
=
%q{ez@engineyard.com}
s
.
email
=
%q{ez@engineyard.com}
s
.
extra_rdoc_files
=
[
"LICENSE"
]
s
.
extra_rdoc_files
=
[
"LICENSE"
]
s
.
files
=
[
"LICENSE"
,
"README.markdown"
,
"Rakefile"
,
"lib/redis.rb"
,
"lib/dist_redis.rb"
,
"lib/hash_ring.rb"
,
"lib/pipeline.rb"
,
"lib/
server
.rb"
,
"spec/redis_spec.rb"
,
"spec/spec_helper.rb"
]
s
.
files
=
[
"LICENSE"
,
"README.markdown"
,
"Rakefile"
,
"lib/dist_redis.rb"
,
"lib/hash_ring.rb"
,
"lib/pipeline.rb"
,
"lib/
redis
.rb"
,
"spec/redis_spec.rb"
,
"spec/spec_helper.rb"
]
s
.
has_rdoc
=
true
s
.
has_rdoc
=
true
s
.
homepage
=
%q{http://github.com/
winescout
/redis-rb}
s
.
homepage
=
%q{http://github.com/
ezmobius
/redis-rb}
s
.
require_paths
=
[
"lib"
]
s
.
require_paths
=
[
"lib"
]
s
.
rubygems_version
=
%q{1.3.1}
s
.
rubygems_version
=
%q{1.3.1}
s
.
summary
=
%q{Ruby client library for redis key value storage server}
s
.
summary
=
%q{Ruby client library for redis key value storage server}
...
...
client-libraries/ruby/spec/redis_spec.rb
View file @
3113921a
require
File
.
dirname
(
__FILE__
)
+
'/spec_helper'
require
File
.
dirname
(
__FILE__
)
+
'/spec_helper'
require
'logger'
class
Foo
class
Foo
attr_accessor
:bar
attr_accessor
:bar
...
@@ -33,6 +34,13 @@ describe "redis" do
...
@@ -33,6 +34,13 @@ describe "redis" do
lambda
{
Redis
.
new
:timeout
=>
0
}.
should_not
raise_error
lambda
{
Redis
.
new
:timeout
=>
0
}.
should_not
raise_error
end
end
it
"should be able to provide a logger"
do
log
=
StringIO
.
new
r
=
Redis
.
new
:db
=>
15
,
:logger
=>
Logger
.
new
(
log
)
r
.
ping
log
.
string
.
should
include
(
"ping"
)
end
it
"should be able to PING"
do
it
"should be able to PING"
do
@r
.
ping
.
should
==
'PONG'
@r
.
ping
.
should
==
'PONG'
end
end
...
@@ -162,7 +170,7 @@ describe "redis" do
...
@@ -162,7 +170,7 @@ describe "redis" do
it
"should be able to return a random key (RANDOMKEY)"
do
it
"should be able to return a random key (RANDOMKEY)"
do
3
.
times
{
@r
.
exists
(
@r
.
randomkey
).
should
be_true
}
3
.
times
{
@r
.
exists
(
@r
.
randomkey
).
should
be_true
}
end
end
#
BTM - TODO
#
it
"should be able to check the TYPE of a key"
do
it
"should be able to check the TYPE of a key"
do
@r
[
'foo'
]
=
'nik'
@r
[
'foo'
]
=
'nik'
@r
.
type
(
'foo'
).
should
==
"string"
@r
.
type
(
'foo'
).
should
==
"string"
...
@@ -352,16 +360,17 @@ describe "redis" do
...
@@ -352,16 +360,17 @@ describe "redis" do
end
end
#
#
it
"should be able to do crazy SORT queries"
do
it
"should be able to do crazy SORT queries"
do
# The 'Dogs' is capitialized on purpose
@r
[
'dog_1'
]
=
'louie'
@r
[
'dog_1'
]
=
'louie'
@r
.
rpush
'
d
ogs'
,
1
@r
.
rpush
'
D
ogs'
,
1
@r
[
'dog_2'
]
=
'lucy'
@r
[
'dog_2'
]
=
'lucy'
@r
.
rpush
'
d
ogs'
,
2
@r
.
rpush
'
D
ogs'
,
2
@r
[
'dog_3'
]
=
'max'
@r
[
'dog_3'
]
=
'max'
@r
.
rpush
'
d
ogs'
,
3
@r
.
rpush
'
D
ogs'
,
3
@r
[
'dog_4'
]
=
'taj'
@r
[
'dog_4'
]
=
'taj'
@r
.
rpush
'
d
ogs'
,
4
@r
.
rpush
'
D
ogs'
,
4
@r
.
sort
(
'
d
ogs'
,
:get
=>
'dog_*'
,
:limit
=>
[
0
,
1
]).
should
==
[
'louie'
]
@r
.
sort
(
'
D
ogs'
,
:get
=>
'dog_*'
,
:limit
=>
[
0
,
1
]).
should
==
[
'louie'
]
@r
.
sort
(
'
d
ogs'
,
:get
=>
'dog_*'
,
:limit
=>
[
0
,
1
],
:order
=>
'desc alpha'
).
should
==
[
'taj'
]
@r
.
sort
(
'
D
ogs'
,
:get
=>
'dog_*'
,
:limit
=>
[
0
,
1
],
:order
=>
'desc alpha'
).
should
==
[
'taj'
]
end
end
it
"should be able to handle array of :get using SORT"
do
it
"should be able to handle array of :get using SORT"
do
...
@@ -412,14 +421,29 @@ describe "redis" do
...
@@ -412,14 +421,29 @@ describe "redis" do
@r
.
mget
(
'foo'
,
'bar'
,
'baz'
).
should
==
[
'1000'
,
'2000'
,
nil
]
@r
.
mget
(
'foo'
,
'bar'
,
'baz'
).
should
==
[
'1000'
,
'2000'
,
nil
]
end
end
it
"should be able to mapped MGET keys"
do
@r
[
'foo'
]
=
1000
@r
[
'bar'
]
=
2000
@r
.
mapped_mget
(
'foo'
,
'bar'
).
should
==
{
'foo'
=>
'1000'
,
'bar'
=>
'2000'
}
@r
.
mapped_mget
(
'foo'
,
'baz'
,
'bar'
).
should
==
{
'foo'
=>
'1000'
,
'bar'
=>
'2000'
}
end
it
"should bgsave"
do
it
"should bgsave"
do
@r
.
bgsave
.
should
==
'OK'
@r
.
bgsave
.
should
==
'OK'
end
end
it
"should
should
be able to ECHO"
do
it
"should be able to ECHO"
do
@r
.
echo
(
"message in a bottle
\n
"
).
should
==
"message in a bottle
\n
"
@r
.
echo
(
"message in a bottle
\n
"
).
should
==
"message in a bottle
\n
"
end
end
it
"should raise error when invoke MONITOR"
do
lambda
{
@r
.
monitor
}.
should
raise_error
end
it
"should raise error when invoke SYNC"
do
lambda
{
@r
.
sync
}.
should
raise_error
end
it
"should handle multiple servers"
do
it
"should handle multiple servers"
do
require
'dist_redis'
require
'dist_redis'
@r
=
DistRedis
.
new
(
:hosts
=>
[
'localhost:6379'
,
'127.0.0.1:6379'
],
:db
=>
15
)
@r
=
DistRedis
.
new
(
:hosts
=>
[
'localhost:6379'
,
'127.0.0.1:6379'
],
:db
=>
15
)
...
@@ -444,4 +468,11 @@ describe "redis" do
...
@@ -444,4 +468,11 @@ describe "redis" do
@r
.
lpop
(
'list'
).
should
==
'42'
@r
.
lpop
(
'list'
).
should
==
'42'
end
end
it
"should AUTH when connecting with a password"
do
r
=
Redis
.
new
(
:password
=>
'secret'
)
r
.
stub!
(
:connect_to
)
r
.
should_receive
(
:call_command
).
with
([
'auth'
,
'secret'
])
r
.
connect_to_server
end
end
end
client-libraries/ruby/spec/spec_helper.rb
View file @
3113921a
require
'rubygems'
require
'rubygems'
$TESTING
=
true
$TESTING
=
true
$:
.
push
File
.
join
(
File
.
dirname
(
__FILE__
),
'..'
,
'lib'
)
$:
.
unshift
File
.
join
(
File
.
dirname
(
__FILE__
),
'..'
,
'lib'
)
require
'redis'
require
'redis'
client-libraries/ruby/tasks/redis.tasks.rb
View file @
3113921a
# Inspired by rabbitmq.rake the Redbox project at http://github.com/rick/redbox/tree/master
# Inspired by rabbitmq.rake the Redbox project at http://github.com/rick/redbox/tree/master
require
'fileutils'
require
'fileutils'
require
'open-uri'
class
RedisRunner
class
RedisRunner
...
@@ -106,10 +107,8 @@ namespace :dtach do
...
@@ -106,10 +107,8 @@ namespace :dtach do
unless
File
.
exists?
(
'/tmp/dtach-0.8.tar.gz'
)
unless
File
.
exists?
(
'/tmp/dtach-0.8.tar.gz'
)
require
'net/http'
require
'net/http'
Net
::
HTTP
.
start
(
'superb-west.dl.sourceforge.net'
)
do
|
http
|
url
=
'http://downloads.sourceforge.net/project/dtach/dtach/0.8/dtach-0.8.tar.gz'
resp
=
http
.
get
(
'/sourceforge/dtach/dtach-0.8.tar.gz'
)
open
(
'/tmp/dtach-0.8.tar.gz'
,
'wb'
)
do
|
file
|
file
.
write
(
open
(
url
).
read
)
end
open
(
'/tmp/dtach-0.8.tar.gz'
,
'wb'
)
do
|
file
|
file
.
write
(
resp
.
body
)
end
end
end
end
unless
File
.
directory?
(
'/tmp/dtach-0.8'
)
unless
File
.
directory?
(
'/tmp/dtach-0.8'
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment