outback 0.0.14 → 1.0.2
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/CHANGELOG +6 -0
- data/LICENSE +21 -0
- data/README.md +29 -3
- data/lib/outback.rb +16 -11
- data/lib/outback/archive.rb +6 -17
- data/lib/outback/backup.rb +24 -19
- data/lib/outback/cli.rb +6 -2
- data/lib/outback/configuration.rb +15 -10
- data/lib/outback/directory_source.rb +8 -7
- data/lib/outback/directory_target.rb +18 -11
- data/lib/outback/encryption_processor.rb +34 -0
- data/lib/outback/errors.rb +7 -0
- data/lib/outback/logging.rb +7 -0
- data/lib/outback/mysql_source.rb +9 -9
- data/lib/outback/processor.rb +17 -0
- data/lib/outback/s3_target.rb +18 -9
- data/lib/outback/sftp_target.rb +70 -0
- data/lib/outback/source.rb +9 -2
- data/lib/outback/source_archive.rb +17 -0
- data/lib/outback/support/attr_setter.rb +1 -1
- data/lib/outback/support/configurable.rb +5 -3
- data/lib/outback/target.rb +51 -14
- data/lib/outback/target_archive.rb +30 -0
- data/lib/outback/version.rb +3 -0
- data/lib/vendor/enumerable_ext.rb +9 -0
- data/lib/{outback/vendor → vendor}/metaclass.rb +1 -1
- data/lib/{outback/vendor → vendor}/methodphitamine.rb +1 -1
- data/lib/vendor/mysql.rb +1093 -0
- data/lib/vendor/mysql/charset.rb +325 -0
- data/lib/vendor/mysql/constants.rb +165 -0
- data/lib/vendor/mysql/error.rb +989 -0
- data/lib/vendor/mysql/packet.rb +78 -0
- data/lib/vendor/mysql/protocol.rb +770 -0
- data/lib/vendor/numeric_ext.rb +49 -0
- data/lib/vendor/string_ext.rb +19 -0
- metadata +53 -39
- data/MIT-LICENSE +0 -20
- data/VERSION +0 -1
- data/lib/outback/configuration_error.rb +0 -4
- data/lib/outback/directory_archive.rb +0 -8
- data/lib/outback/local_archive.rb +0 -6
- data/lib/outback/s3_archive.rb +0 -18
- data/lib/outback/temp_archive.rb +0 -5
- data/lib/outback/vendor/mysql.rb +0 -1214
@@ -0,0 +1,78 @@
|
|
1
|
+
# coding: ascii-8bit
|
2
|
+
class Mysql
|
3
|
+
class Packet
|
4
|
+
# convert Numeric to LengthCodedBinary
|
5
|
+
def self.lcb(num)
|
6
|
+
return "\xfb" if num.nil?
|
7
|
+
return [num].pack("C") if num < 251
|
8
|
+
return [252, num].pack("Cv") if num < 65536
|
9
|
+
return [253, num&0xffff, num>>16].pack("CvC") if num < 16777216
|
10
|
+
return [254, num&0xffffffff, num>>32].pack("CVV")
|
11
|
+
end
|
12
|
+
|
13
|
+
# convert String to LengthCodedString
|
14
|
+
def self.lcs(str)
|
15
|
+
str = Charset.to_binary str.dup
|
16
|
+
lcb(str.length)+str
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(data)
|
20
|
+
@data = data
|
21
|
+
end
|
22
|
+
|
23
|
+
def lcb
|
24
|
+
return nil if @data.empty?
|
25
|
+
case v = utiny
|
26
|
+
when 0xfb
|
27
|
+
return nil
|
28
|
+
when 0xfc
|
29
|
+
return ushort
|
30
|
+
when 0xfd
|
31
|
+
c, v = utiny, ushort
|
32
|
+
return (v << 8)+c
|
33
|
+
when 0xfe
|
34
|
+
v1, v2 = ulong, ulong
|
35
|
+
return (v2 << 32)+v1
|
36
|
+
else
|
37
|
+
return v
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def lcs
|
42
|
+
len = self.lcb
|
43
|
+
return nil unless len
|
44
|
+
@data.slice!(0, len)
|
45
|
+
end
|
46
|
+
|
47
|
+
def read(len)
|
48
|
+
@data.slice!(0, len)
|
49
|
+
end
|
50
|
+
|
51
|
+
def string
|
52
|
+
str = @data.unpack('Z*').first
|
53
|
+
@data.slice!(0, str.length+1)
|
54
|
+
str
|
55
|
+
end
|
56
|
+
|
57
|
+
def utiny
|
58
|
+
@data.slice!(0, 1).unpack('C').first
|
59
|
+
end
|
60
|
+
|
61
|
+
def ushort
|
62
|
+
@data.slice!(0, 2).unpack('v').first
|
63
|
+
end
|
64
|
+
|
65
|
+
def ulong
|
66
|
+
@data.slice!(0, 4).unpack('V').first
|
67
|
+
end
|
68
|
+
|
69
|
+
def eof?
|
70
|
+
@data[0] == ?\xfe && @data.length == 5
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_s
|
74
|
+
@data
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,770 @@
|
|
1
|
+
# coding: ascii-8bit
|
2
|
+
# Copyright (C) 2008-2012 TOMITA Masahiro
|
3
|
+
# mailto:tommy@tmtm.org
|
4
|
+
|
5
|
+
require "socket"
|
6
|
+
require "timeout"
|
7
|
+
require "digest/sha1"
|
8
|
+
require "stringio"
|
9
|
+
|
10
|
+
class Mysql
|
11
|
+
# MySQL network protocol
|
12
|
+
class Protocol
|
13
|
+
|
14
|
+
VERSION = 10
|
15
|
+
MAX_PACKET_LENGTH = 2**24-1
|
16
|
+
|
17
|
+
# Convert netdata to Ruby value
|
18
|
+
# === Argument
|
19
|
+
# data :: [Packet] packet data
|
20
|
+
# type :: [Integer] field type
|
21
|
+
# unsigned :: [true or false] true if value is unsigned
|
22
|
+
# === Return
|
23
|
+
# Object :: converted value.
|
24
|
+
def self.net2value(pkt, type, unsigned)
|
25
|
+
case type
|
26
|
+
when Field::TYPE_STRING, Field::TYPE_VAR_STRING, Field::TYPE_NEWDECIMAL, Field::TYPE_BLOB
|
27
|
+
return pkt.lcs
|
28
|
+
when Field::TYPE_TINY
|
29
|
+
v = pkt.utiny
|
30
|
+
return unsigned ? v : v < 128 ? v : v-256
|
31
|
+
when Field::TYPE_SHORT
|
32
|
+
v = pkt.ushort
|
33
|
+
return unsigned ? v : v < 32768 ? v : v-65536
|
34
|
+
when Field::TYPE_INT24, Field::TYPE_LONG
|
35
|
+
v = pkt.ulong
|
36
|
+
return unsigned ? v : v < 0x8000_0000 ? v : v-0x10000_0000
|
37
|
+
when Field::TYPE_LONGLONG
|
38
|
+
n1, n2 = pkt.ulong, pkt.ulong
|
39
|
+
v = (n2 << 32) | n1
|
40
|
+
return unsigned ? v : v < 0x8000_0000_0000_0000 ? v : v-0x10000_0000_0000_0000
|
41
|
+
when Field::TYPE_FLOAT
|
42
|
+
return pkt.read(4).unpack('e').first
|
43
|
+
when Field::TYPE_DOUBLE
|
44
|
+
return pkt.read(8).unpack('E').first
|
45
|
+
when Field::TYPE_DATE
|
46
|
+
len = pkt.utiny
|
47
|
+
y, m, d = pkt.read(len).unpack("vCC")
|
48
|
+
t = Mysql::Time.new(y, m, d, nil, nil, nil)
|
49
|
+
return t
|
50
|
+
when Field::TYPE_DATETIME, Field::TYPE_TIMESTAMP
|
51
|
+
len = pkt.utiny
|
52
|
+
y, m, d, h, mi, s, sp = pkt.read(len).unpack("vCCCCCV")
|
53
|
+
return Mysql::Time.new(y, m, d, h, mi, s, false, sp)
|
54
|
+
when Field::TYPE_TIME
|
55
|
+
len = pkt.utiny
|
56
|
+
sign, d, h, mi, s, sp = pkt.read(len).unpack("CVCCCV")
|
57
|
+
h = d.to_i * 24 + h.to_i
|
58
|
+
return Mysql::Time.new(0, 0, 0, h, mi, s, sign!=0, sp)
|
59
|
+
when Field::TYPE_YEAR
|
60
|
+
return pkt.ushort
|
61
|
+
when Field::TYPE_BIT
|
62
|
+
return pkt.lcs
|
63
|
+
else
|
64
|
+
raise "not implemented: type=#{type}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# convert Ruby value to netdata
|
69
|
+
# === Argument
|
70
|
+
# v :: [Object] Ruby value.
|
71
|
+
# === Return
|
72
|
+
# Integer :: type of column. Field::TYPE_*
|
73
|
+
# String :: netdata
|
74
|
+
# === Exception
|
75
|
+
# ProtocolError :: value too large / value is not supported
|
76
|
+
def self.value2net(v)
|
77
|
+
case v
|
78
|
+
when nil
|
79
|
+
type = Field::TYPE_NULL
|
80
|
+
val = ""
|
81
|
+
when Integer
|
82
|
+
if -0x8000_0000 <= v && v < 0x8000_0000
|
83
|
+
type = Field::TYPE_LONG
|
84
|
+
val = [v].pack('V')
|
85
|
+
elsif -0x8000_0000_0000_0000 <= v && v < 0x8000_0000_0000_0000
|
86
|
+
type = Field::TYPE_LONGLONG
|
87
|
+
val = [v&0xffffffff, v>>32].pack("VV")
|
88
|
+
elsif 0x8000_0000_0000_0000 <= v && v <= 0xffff_ffff_ffff_ffff
|
89
|
+
type = Field::TYPE_LONGLONG | 0x8000
|
90
|
+
val = [v&0xffffffff, v>>32].pack("VV")
|
91
|
+
else
|
92
|
+
raise ProtocolError, "value too large: #{v}"
|
93
|
+
end
|
94
|
+
when Float
|
95
|
+
type = Field::TYPE_DOUBLE
|
96
|
+
val = [v].pack("E")
|
97
|
+
when String
|
98
|
+
type = Field::TYPE_STRING
|
99
|
+
val = Packet.lcs(v)
|
100
|
+
when ::Time
|
101
|
+
type = Field::TYPE_DATETIME
|
102
|
+
val = [11, v.year, v.month, v.day, v.hour, v.min, v.sec, v.usec].pack("CvCCCCCV")
|
103
|
+
when Mysql::Time
|
104
|
+
type = Field::TYPE_DATETIME
|
105
|
+
val = [11, v.year, v.month, v.day, v.hour, v.min, v.sec, v.second_part].pack("CvCCCCCV")
|
106
|
+
else
|
107
|
+
raise ProtocolError, "class #{v.class} is not supported"
|
108
|
+
end
|
109
|
+
return type, val
|
110
|
+
end
|
111
|
+
|
112
|
+
attr_reader :server_info
|
113
|
+
attr_reader :server_version
|
114
|
+
attr_reader :thread_id
|
115
|
+
attr_reader :sqlstate
|
116
|
+
attr_reader :affected_rows
|
117
|
+
attr_reader :insert_id
|
118
|
+
attr_reader :server_status
|
119
|
+
attr_reader :warning_count
|
120
|
+
attr_reader :message
|
121
|
+
attr_accessor :charset
|
122
|
+
|
123
|
+
# @state variable keep state for connection.
|
124
|
+
# :INIT :: Initial state.
|
125
|
+
# :READY :: Ready for command.
|
126
|
+
# :FIELD :: After query(). retr_fields() is needed.
|
127
|
+
# :RESULT :: After retr_fields(), retr_all_records() or stmt_retr_all_records() is needed.
|
128
|
+
|
129
|
+
# make socket connection to server.
|
130
|
+
# === Argument
|
131
|
+
# host :: [String] if "localhost" or "" nil then use UNIXSocket. Otherwise use TCPSocket
|
132
|
+
# port :: [Integer] port number using by TCPSocket
|
133
|
+
# socket :: [String] socket file name using by UNIXSocket
|
134
|
+
# conn_timeout :: [Integer] connect timeout (sec).
|
135
|
+
# read_timeout :: [Integer] read timeout (sec).
|
136
|
+
# write_timeout :: [Integer] write timeout (sec).
|
137
|
+
# === Exception
|
138
|
+
# [ClientError] :: connection timeout
|
139
|
+
def initialize(host, port, socket, conn_timeout, read_timeout, write_timeout)
|
140
|
+
@insert_id = 0
|
141
|
+
@warning_count = 0
|
142
|
+
@gc_stmt_queue = [] # stmt id list which GC destroy.
|
143
|
+
set_state :INIT
|
144
|
+
@read_timeout = read_timeout
|
145
|
+
@write_timeout = write_timeout
|
146
|
+
begin
|
147
|
+
Timeout.timeout conn_timeout do
|
148
|
+
if host.nil? or host.empty? or host == "localhost"
|
149
|
+
socket ||= ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_PORT
|
150
|
+
@sock = UNIXSocket.new socket
|
151
|
+
else
|
152
|
+
port ||= ENV["MYSQL_TCP_PORT"] || (Socket.getservbyname("mysql","tcp") rescue MYSQL_TCP_PORT)
|
153
|
+
@sock = TCPSocket.new host, port
|
154
|
+
end
|
155
|
+
end
|
156
|
+
rescue Timeout::Error
|
157
|
+
raise ClientError, "connection timeout"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def close
|
162
|
+
@sock.close
|
163
|
+
end
|
164
|
+
|
165
|
+
# initial negotiate and authenticate.
|
166
|
+
# === Argument
|
167
|
+
# user :: [String / nil] username
|
168
|
+
# passwd :: [String / nil] password
|
169
|
+
# db :: [String / nil] default database name. nil: no default.
|
170
|
+
# flag :: [Integer] client flag
|
171
|
+
# charset :: [Mysql::Charset / nil] charset for connection. nil: use server's charset
|
172
|
+
# === Exception
|
173
|
+
# ProtocolError :: The old style password is not supported
|
174
|
+
def authenticate(user, passwd, db, flag, charset)
|
175
|
+
check_state :INIT
|
176
|
+
@authinfo = [user, passwd, db, flag, charset]
|
177
|
+
reset
|
178
|
+
init_packet = InitialPacket.parse read
|
179
|
+
@server_info = init_packet.server_version
|
180
|
+
@server_version = init_packet.server_version.split(/\D/)[0,3].inject{|a,b|a.to_i*100+b.to_i}
|
181
|
+
@thread_id = init_packet.thread_id
|
182
|
+
client_flags = CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_TRANSACTIONS | CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
183
|
+
client_flags |= CLIENT_CONNECT_WITH_DB if db
|
184
|
+
client_flags |= flag
|
185
|
+
@charset = charset
|
186
|
+
unless @charset
|
187
|
+
@charset = Charset.by_number(init_packet.server_charset)
|
188
|
+
@charset.encoding # raise error if unsupported charset
|
189
|
+
end
|
190
|
+
netpw = encrypt_password passwd, init_packet.scramble_buff
|
191
|
+
write AuthenticationPacket.serialize(client_flags, 1024**3, @charset.number, user, netpw, db)
|
192
|
+
raise ProtocolError, 'The old style password is not supported' if read.to_s == "\xfe"
|
193
|
+
set_state :READY
|
194
|
+
end
|
195
|
+
|
196
|
+
# Quit command
|
197
|
+
def quit_command
|
198
|
+
synchronize do
|
199
|
+
reset
|
200
|
+
write [COM_QUIT].pack("C")
|
201
|
+
close
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Query command
|
206
|
+
# === Argument
|
207
|
+
# query :: [String] query string
|
208
|
+
# === Return
|
209
|
+
# [Integer / nil] number of fields of results. nil if no results.
|
210
|
+
def query_command(query)
|
211
|
+
check_state :READY
|
212
|
+
begin
|
213
|
+
reset
|
214
|
+
write [COM_QUERY, @charset.convert(query)].pack("Ca*")
|
215
|
+
get_result
|
216
|
+
rescue
|
217
|
+
set_state :READY
|
218
|
+
raise
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# get result of query.
|
223
|
+
# === Return
|
224
|
+
# [integer / nil] number of fields of results. nil if no results.
|
225
|
+
def get_result
|
226
|
+
begin
|
227
|
+
res_packet = ResultPacket.parse read
|
228
|
+
if res_packet.field_count.to_i > 0 # result data exists
|
229
|
+
set_state :FIELD
|
230
|
+
return res_packet.field_count
|
231
|
+
end
|
232
|
+
if res_packet.field_count.nil? # LOAD DATA LOCAL INFILE
|
233
|
+
filename = res_packet.message
|
234
|
+
File.open(filename){|f| write f}
|
235
|
+
write nil # EOF mark
|
236
|
+
read
|
237
|
+
end
|
238
|
+
@affected_rows, @insert_id, @server_status, @warning_count, @message =
|
239
|
+
res_packet.affected_rows, res_packet.insert_id, res_packet.server_status, res_packet.warning_count, res_packet.message
|
240
|
+
set_state :READY
|
241
|
+
return nil
|
242
|
+
rescue
|
243
|
+
set_state :READY
|
244
|
+
raise
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Retrieve n fields
|
249
|
+
# === Argument
|
250
|
+
# n :: [Integer] number of fields
|
251
|
+
# === Return
|
252
|
+
# [Array of Mysql::Field] field list
|
253
|
+
def retr_fields(n)
|
254
|
+
check_state :FIELD
|
255
|
+
begin
|
256
|
+
fields = n.times.map{Field.new FieldPacket.parse(read)}
|
257
|
+
read_eof_packet
|
258
|
+
set_state :RESULT
|
259
|
+
fields
|
260
|
+
rescue
|
261
|
+
set_state :READY
|
262
|
+
raise
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Retrieve all records for simple query
|
267
|
+
# === Argument
|
268
|
+
# fields :: [Array<Mysql::Field>] number of fields
|
269
|
+
# === Return
|
270
|
+
# [Array of Array of String] all records
|
271
|
+
def retr_all_records(fields)
|
272
|
+
check_state :RESULT
|
273
|
+
enc = charset.encoding
|
274
|
+
begin
|
275
|
+
all_recs = []
|
276
|
+
until (pkt = read).eof?
|
277
|
+
all_recs.push RawRecord.new(pkt, fields, enc)
|
278
|
+
end
|
279
|
+
pkt.read(3)
|
280
|
+
@server_status = pkt.utiny
|
281
|
+
all_recs
|
282
|
+
ensure
|
283
|
+
set_state :READY
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Field list command
|
288
|
+
# === Argument
|
289
|
+
# table :: [String] table name.
|
290
|
+
# field :: [String / nil] field name that may contain wild card.
|
291
|
+
# === Return
|
292
|
+
# [Array of Field] field list
|
293
|
+
def field_list_command(table, field)
|
294
|
+
synchronize do
|
295
|
+
reset
|
296
|
+
write [COM_FIELD_LIST, table, 0, field].pack("Ca*Ca*")
|
297
|
+
fields = []
|
298
|
+
until (data = read).eof?
|
299
|
+
fields.push Field.new(FieldPacket.parse(data))
|
300
|
+
end
|
301
|
+
return fields
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Process info command
|
306
|
+
# === Return
|
307
|
+
# [Array of Field] field list
|
308
|
+
def process_info_command
|
309
|
+
check_state :READY
|
310
|
+
begin
|
311
|
+
reset
|
312
|
+
write [COM_PROCESS_INFO].pack("C")
|
313
|
+
field_count = read.lcb
|
314
|
+
fields = field_count.times.map{Field.new FieldPacket.parse(read)}
|
315
|
+
read_eof_packet
|
316
|
+
set_state :RESULT
|
317
|
+
return fields
|
318
|
+
rescue
|
319
|
+
set_state :READY
|
320
|
+
raise
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Ping command
|
325
|
+
def ping_command
|
326
|
+
simple_command [COM_PING].pack("C")
|
327
|
+
end
|
328
|
+
|
329
|
+
# Kill command
|
330
|
+
def kill_command(pid)
|
331
|
+
simple_command [COM_PROCESS_KILL, pid].pack("CV")
|
332
|
+
end
|
333
|
+
|
334
|
+
# Refresh command
|
335
|
+
def refresh_command(op)
|
336
|
+
simple_command [COM_REFRESH, op].pack("CC")
|
337
|
+
end
|
338
|
+
|
339
|
+
# Set option command
|
340
|
+
def set_option_command(opt)
|
341
|
+
simple_command [COM_SET_OPTION, opt].pack("Cv")
|
342
|
+
end
|
343
|
+
|
344
|
+
# Shutdown command
|
345
|
+
def shutdown_command(level)
|
346
|
+
simple_command [COM_SHUTDOWN, level].pack("CC")
|
347
|
+
end
|
348
|
+
|
349
|
+
# Statistics command
|
350
|
+
def statistics_command
|
351
|
+
simple_command [COM_STATISTICS].pack("C")
|
352
|
+
end
|
353
|
+
|
354
|
+
# Stmt prepare command
|
355
|
+
# === Argument
|
356
|
+
# stmt :: [String] prepared statement
|
357
|
+
# === Return
|
358
|
+
# [Integer] statement id
|
359
|
+
# [Integer] number of parameters
|
360
|
+
# [Array of Field] field list
|
361
|
+
def stmt_prepare_command(stmt)
|
362
|
+
synchronize do
|
363
|
+
reset
|
364
|
+
write [COM_STMT_PREPARE, charset.convert(stmt)].pack("Ca*")
|
365
|
+
res_packet = PrepareResultPacket.parse read
|
366
|
+
if res_packet.param_count > 0
|
367
|
+
res_packet.param_count.times{read} # skip parameter packet
|
368
|
+
read_eof_packet
|
369
|
+
end
|
370
|
+
if res_packet.field_count > 0
|
371
|
+
fields = res_packet.field_count.times.map{Field.new FieldPacket.parse(read)}
|
372
|
+
read_eof_packet
|
373
|
+
else
|
374
|
+
fields = []
|
375
|
+
end
|
376
|
+
return res_packet.statement_id, res_packet.param_count, fields
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
# Stmt execute command
|
381
|
+
# === Argument
|
382
|
+
# stmt_id :: [Integer] statement id
|
383
|
+
# values :: [Array] parameters
|
384
|
+
# === Return
|
385
|
+
# [Integer] number of fields
|
386
|
+
def stmt_execute_command(stmt_id, values)
|
387
|
+
check_state :READY
|
388
|
+
begin
|
389
|
+
reset
|
390
|
+
write ExecutePacket.serialize(stmt_id, Mysql::Stmt::CURSOR_TYPE_NO_CURSOR, values)
|
391
|
+
get_result
|
392
|
+
rescue
|
393
|
+
set_state :READY
|
394
|
+
raise
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Retrieve all records for prepared statement
|
399
|
+
# === Argument
|
400
|
+
# fields :: [Array of Mysql::Fields] field list
|
401
|
+
# charset :: [Mysql::Charset]
|
402
|
+
# === Return
|
403
|
+
# [Array of Array of Object] all records
|
404
|
+
def stmt_retr_all_records(fields, charset)
|
405
|
+
check_state :RESULT
|
406
|
+
enc = charset.encoding
|
407
|
+
begin
|
408
|
+
all_recs = []
|
409
|
+
until (pkt = read).eof?
|
410
|
+
all_recs.push StmtRawRecord.new(pkt, fields, enc)
|
411
|
+
end
|
412
|
+
all_recs
|
413
|
+
ensure
|
414
|
+
set_state :READY
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
# Stmt close command
|
419
|
+
# === Argument
|
420
|
+
# stmt_id :: [Integer] statement id
|
421
|
+
def stmt_close_command(stmt_id)
|
422
|
+
synchronize do
|
423
|
+
reset
|
424
|
+
write [COM_STMT_CLOSE, stmt_id].pack("CV")
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
def gc_stmt(stmt_id)
|
429
|
+
@gc_stmt_queue.push stmt_id
|
430
|
+
end
|
431
|
+
|
432
|
+
private
|
433
|
+
|
434
|
+
def check_state(st)
|
435
|
+
raise 'command out of sync' unless @state == st
|
436
|
+
end
|
437
|
+
|
438
|
+
def set_state(st)
|
439
|
+
@state = st
|
440
|
+
if st == :READY
|
441
|
+
gc_disabled = GC.disable
|
442
|
+
begin
|
443
|
+
while st = @gc_stmt_queue.shift
|
444
|
+
reset
|
445
|
+
write [COM_STMT_CLOSE, st].pack("CV")
|
446
|
+
end
|
447
|
+
ensure
|
448
|
+
GC.enable unless gc_disabled
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def synchronize
|
454
|
+
begin
|
455
|
+
check_state :READY
|
456
|
+
return yield
|
457
|
+
ensure
|
458
|
+
set_state :READY
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
# Reset sequence number
|
463
|
+
def reset
|
464
|
+
@seq = 0 # packet counter. reset by each command
|
465
|
+
end
|
466
|
+
|
467
|
+
# Read one packet data
|
468
|
+
# === Return
|
469
|
+
# [Packet] packet data
|
470
|
+
# === Exception
|
471
|
+
# [ProtocolError] invalid packet sequence number
|
472
|
+
def read
|
473
|
+
data = ''
|
474
|
+
len = nil
|
475
|
+
begin
|
476
|
+
Timeout.timeout @read_timeout do
|
477
|
+
header = @sock.read(4)
|
478
|
+
raise EOFError unless header && header.length == 4
|
479
|
+
len1, len2, seq = header.unpack("CvC")
|
480
|
+
len = (len2 << 8) + len1
|
481
|
+
raise ProtocolError, "invalid packet: sequence number mismatch(#{seq} != #{@seq}(expected))" if @seq != seq
|
482
|
+
@seq = (@seq + 1) % 256
|
483
|
+
ret = @sock.read(len)
|
484
|
+
raise EOFError unless ret && ret.length == len
|
485
|
+
data.concat ret
|
486
|
+
end
|
487
|
+
rescue EOFError
|
488
|
+
raise ClientError::ServerGoneError, 'MySQL server has gone away'
|
489
|
+
rescue Timeout::Error
|
490
|
+
raise ClientError, "read timeout"
|
491
|
+
end while len == MAX_PACKET_LENGTH
|
492
|
+
|
493
|
+
@sqlstate = "00000"
|
494
|
+
|
495
|
+
# Error packet
|
496
|
+
if data[0] == ?\xff
|
497
|
+
f, errno, marker, @sqlstate, message = data.unpack("Cvaa5a*")
|
498
|
+
unless marker == "#"
|
499
|
+
f, errno, message = data.unpack("Cva*") # Version 4.0 Error
|
500
|
+
@sqlstate = ""
|
501
|
+
end
|
502
|
+
message.force_encoding(@charset.encoding)
|
503
|
+
if Mysql::ServerError::ERROR_MAP.key? errno
|
504
|
+
raise Mysql::ServerError::ERROR_MAP[errno].new(message, @sqlstate)
|
505
|
+
end
|
506
|
+
raise Mysql::ServerError.new(message, @sqlstate)
|
507
|
+
end
|
508
|
+
Packet.new(data)
|
509
|
+
end
|
510
|
+
|
511
|
+
# Write one packet data
|
512
|
+
# === Argument
|
513
|
+
# data :: [String / IO] packet data. If data is nil, write empty packet.
|
514
|
+
def write(data)
|
515
|
+
begin
|
516
|
+
@sock.sync = false
|
517
|
+
if data.nil?
|
518
|
+
Timeout.timeout @write_timeout do
|
519
|
+
@sock.write [0, 0, @seq].pack("CvC")
|
520
|
+
end
|
521
|
+
@seq = (@seq + 1) % 256
|
522
|
+
else
|
523
|
+
data = StringIO.new data if data.is_a? String
|
524
|
+
while d = data.read(MAX_PACKET_LENGTH)
|
525
|
+
Timeout.timeout @write_timeout do
|
526
|
+
@sock.write [d.length%256, d.length/256, @seq].pack("CvC")
|
527
|
+
@sock.write d
|
528
|
+
end
|
529
|
+
@seq = (@seq + 1) % 256
|
530
|
+
end
|
531
|
+
end
|
532
|
+
@sock.sync = true
|
533
|
+
Timeout.timeout @write_timeout do
|
534
|
+
@sock.flush
|
535
|
+
end
|
536
|
+
rescue Errno::EPIPE
|
537
|
+
raise ClientError::ServerGoneError, 'MySQL server has gone away'
|
538
|
+
rescue Timeout::Error
|
539
|
+
raise ClientError, "write timeout"
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
# Read EOF packet
|
544
|
+
# === Exception
|
545
|
+
# [ProtocolError] packet is not EOF
|
546
|
+
def read_eof_packet
|
547
|
+
raise ProtocolError, "packet is not EOF" unless read.eof?
|
548
|
+
end
|
549
|
+
|
550
|
+
# Send simple command
|
551
|
+
# === Argument
|
552
|
+
# packet :: [String] packet data
|
553
|
+
# === Return
|
554
|
+
# [String] received data
|
555
|
+
def simple_command(packet)
|
556
|
+
synchronize do
|
557
|
+
reset
|
558
|
+
write packet
|
559
|
+
read.to_s
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
# Encrypt password
|
564
|
+
# === Argument
|
565
|
+
# plain :: [String] plain password.
|
566
|
+
# scramble :: [String] scramble code from initial packet.
|
567
|
+
# === Return
|
568
|
+
# [String] encrypted password
|
569
|
+
def encrypt_password(plain, scramble)
|
570
|
+
return "" if plain.nil? or plain.empty?
|
571
|
+
hash_stage1 = Digest::SHA1.digest plain
|
572
|
+
hash_stage2 = Digest::SHA1.digest hash_stage1
|
573
|
+
return hash_stage1.unpack("C*").zip(Digest::SHA1.digest(scramble+hash_stage2).unpack("C*")).map{|a,b| a^b}.pack("C*")
|
574
|
+
end
|
575
|
+
|
576
|
+
# Initial packet
|
577
|
+
class InitialPacket
|
578
|
+
def self.parse(pkt)
|
579
|
+
protocol_version = pkt.utiny
|
580
|
+
server_version = pkt.string
|
581
|
+
thread_id = pkt.ulong
|
582
|
+
scramble_buff = pkt.read(8)
|
583
|
+
f0 = pkt.utiny
|
584
|
+
server_capabilities = pkt.ushort
|
585
|
+
server_charset = pkt.utiny
|
586
|
+
server_status = pkt.ushort
|
587
|
+
f1 = pkt.read(13)
|
588
|
+
rest_scramble_buff = pkt.string
|
589
|
+
raise ProtocolError, "unsupported version: #{protocol_version}" unless protocol_version == VERSION
|
590
|
+
raise ProtocolError, "invalid packet: f0=#{f0}" unless f0 == 0
|
591
|
+
scramble_buff.concat rest_scramble_buff
|
592
|
+
self.new protocol_version, server_version, thread_id, server_capabilities, server_charset, server_status, scramble_buff
|
593
|
+
end
|
594
|
+
|
595
|
+
attr_reader :protocol_version, :server_version, :thread_id, :server_capabilities, :server_charset, :server_status, :scramble_buff
|
596
|
+
|
597
|
+
def initialize(*args)
|
598
|
+
@protocol_version, @server_version, @thread_id, @server_capabilities, @server_charset, @server_status, @scramble_buff = args
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
# Result packet
|
603
|
+
class ResultPacket
|
604
|
+
def self.parse(pkt)
|
605
|
+
field_count = pkt.lcb
|
606
|
+
if field_count == 0
|
607
|
+
affected_rows = pkt.lcb
|
608
|
+
insert_id = pkt.lcb
|
609
|
+
server_status = pkt.ushort
|
610
|
+
warning_count = pkt.ushort
|
611
|
+
message = pkt.lcs
|
612
|
+
return self.new(field_count, affected_rows, insert_id, server_status, warning_count, message)
|
613
|
+
elsif field_count.nil? # LOAD DATA LOCAL INFILE
|
614
|
+
return self.new(nil, nil, nil, nil, nil, pkt.to_s)
|
615
|
+
else
|
616
|
+
return self.new(field_count)
|
617
|
+
end
|
618
|
+
end
|
619
|
+
|
620
|
+
attr_reader :field_count, :affected_rows, :insert_id, :server_status, :warning_count, :message
|
621
|
+
|
622
|
+
def initialize(*args)
|
623
|
+
@field_count, @affected_rows, @insert_id, @server_status, @warning_count, @message = args
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
# Field packet
|
628
|
+
class FieldPacket
|
629
|
+
def self.parse(pkt)
|
630
|
+
first = pkt.lcs
|
631
|
+
db = pkt.lcs
|
632
|
+
table = pkt.lcs
|
633
|
+
org_table = pkt.lcs
|
634
|
+
name = pkt.lcs
|
635
|
+
org_name = pkt.lcs
|
636
|
+
f0 = pkt.utiny
|
637
|
+
charsetnr = pkt.ushort
|
638
|
+
length = pkt.ulong
|
639
|
+
type = pkt.utiny
|
640
|
+
flags = pkt.ushort
|
641
|
+
decimals = pkt.utiny
|
642
|
+
f1 = pkt.ushort
|
643
|
+
|
644
|
+
raise ProtocolError, "invalid packet: f1=#{f1}" unless f1 == 0
|
645
|
+
default = pkt.lcs
|
646
|
+
return self.new(db, table, org_table, name, org_name, charsetnr, length, type, flags, decimals, default)
|
647
|
+
end
|
648
|
+
|
649
|
+
attr_reader :db, :table, :org_table, :name, :org_name, :charsetnr, :length, :type, :flags, :decimals, :default
|
650
|
+
|
651
|
+
def initialize(*args)
|
652
|
+
@db, @table, @org_table, @name, @org_name, @charsetnr, @length, @type, @flags, @decimals, @default = args
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
# Prepare result packet
|
657
|
+
class PrepareResultPacket
|
658
|
+
def self.parse(pkt)
|
659
|
+
raise ProtocolError, "invalid packet" unless pkt.utiny == 0
|
660
|
+
statement_id = pkt.ulong
|
661
|
+
field_count = pkt.ushort
|
662
|
+
param_count = pkt.ushort
|
663
|
+
f = pkt.utiny
|
664
|
+
warning_count = pkt.ushort
|
665
|
+
raise ProtocolError, "invalid packet" unless f == 0x00
|
666
|
+
self.new statement_id, field_count, param_count, warning_count
|
667
|
+
end
|
668
|
+
|
669
|
+
attr_reader :statement_id, :field_count, :param_count, :warning_count
|
670
|
+
|
671
|
+
def initialize(*args)
|
672
|
+
@statement_id, @field_count, @param_count, @warning_count = args
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
# Authentication packet
|
677
|
+
class AuthenticationPacket
|
678
|
+
def self.serialize(client_flags, max_packet_size, charset_number, username, scrambled_password, databasename)
|
679
|
+
[
|
680
|
+
client_flags,
|
681
|
+
max_packet_size,
|
682
|
+
Packet.lcb(charset_number),
|
683
|
+
"", # always 0x00 * 23
|
684
|
+
username,
|
685
|
+
Packet.lcs(scrambled_password),
|
686
|
+
databasename
|
687
|
+
].pack("VVa*a23Z*A*Z*")
|
688
|
+
end
|
689
|
+
end
|
690
|
+
|
691
|
+
# Execute packet
|
692
|
+
class ExecutePacket
|
693
|
+
def self.serialize(statement_id, cursor_type, values)
|
694
|
+
nbm = null_bitmap values
|
695
|
+
netvalues = ""
|
696
|
+
types = values.map do |v|
|
697
|
+
t, n = Protocol.value2net v
|
698
|
+
netvalues.concat n if v
|
699
|
+
t
|
700
|
+
end
|
701
|
+
[Mysql::COM_STMT_EXECUTE, statement_id, cursor_type, 1, nbm, 1, types.pack("v*"), netvalues].pack("CVCVa*Ca*a*")
|
702
|
+
end
|
703
|
+
|
704
|
+
# make null bitmap
|
705
|
+
#
|
706
|
+
# If values is [1, nil, 2, 3, nil] then returns "\x12"(0b10010).
|
707
|
+
def self.null_bitmap(values)
|
708
|
+
bitmap = values.enum_for(:each_slice,8).map do |vals|
|
709
|
+
vals.reverse.inject(0){|b, v|(b << 1 | (v ? 0 : 1))}
|
710
|
+
end
|
711
|
+
return bitmap.pack("C*")
|
712
|
+
end
|
713
|
+
|
714
|
+
end
|
715
|
+
end
|
716
|
+
|
717
|
+
class RawRecord
|
718
|
+
def initialize(packet, fields, encoding)
|
719
|
+
@packet, @fields, @encoding = packet, fields, encoding
|
720
|
+
end
|
721
|
+
|
722
|
+
def to_a
|
723
|
+
@fields.map do |f|
|
724
|
+
if s = @packet.lcs
|
725
|
+
unless f.type == Field::TYPE_BIT or f.charsetnr == Charset::BINARY_CHARSET_NUMBER
|
726
|
+
s = Charset.convert_encoding(s, @encoding)
|
727
|
+
end
|
728
|
+
end
|
729
|
+
s
|
730
|
+
end
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
class StmtRawRecord
|
735
|
+
# === Argument
|
736
|
+
# pkt :: [Packet]
|
737
|
+
# fields :: [Array of Fields]
|
738
|
+
# encoding:: [Encoding]
|
739
|
+
def initialize(packet, fields, encoding)
|
740
|
+
@packet, @fields, @encoding = packet, fields, encoding
|
741
|
+
end
|
742
|
+
|
743
|
+
# Parse statement result packet
|
744
|
+
# === Return
|
745
|
+
# [Array of Object] one record
|
746
|
+
def parse_record_packet
|
747
|
+
@packet.utiny # skip first byte
|
748
|
+
null_bit_map = @packet.read((@fields.length+7+2)/8).unpack("b*").first
|
749
|
+
rec = @fields.each_with_index.map do |f, i|
|
750
|
+
if null_bit_map[i+2] == ?1
|
751
|
+
nil
|
752
|
+
else
|
753
|
+
unsigned = f.flags & Field::UNSIGNED_FLAG != 0
|
754
|
+
v = Protocol.net2value(@packet, f.type, unsigned)
|
755
|
+
if v.is_a? Numeric or v.is_a? Mysql::Time
|
756
|
+
v
|
757
|
+
elsif f.type == Field::TYPE_BIT or f.charsetnr == Charset::BINARY_CHARSET_NUMBER
|
758
|
+
Charset.to_binary(v)
|
759
|
+
else
|
760
|
+
Charset.convert_encoding(v, @encoding)
|
761
|
+
end
|
762
|
+
end
|
763
|
+
end
|
764
|
+
rec
|
765
|
+
end
|
766
|
+
|
767
|
+
alias to_a parse_record_packet
|
768
|
+
|
769
|
+
end
|
770
|
+
end
|