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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +6 -0
  3. data/LICENSE +21 -0
  4. data/README.md +29 -3
  5. data/lib/outback.rb +16 -11
  6. data/lib/outback/archive.rb +6 -17
  7. data/lib/outback/backup.rb +24 -19
  8. data/lib/outback/cli.rb +6 -2
  9. data/lib/outback/configuration.rb +15 -10
  10. data/lib/outback/directory_source.rb +8 -7
  11. data/lib/outback/directory_target.rb +18 -11
  12. data/lib/outback/encryption_processor.rb +34 -0
  13. data/lib/outback/errors.rb +7 -0
  14. data/lib/outback/logging.rb +7 -0
  15. data/lib/outback/mysql_source.rb +9 -9
  16. data/lib/outback/processor.rb +17 -0
  17. data/lib/outback/s3_target.rb +18 -9
  18. data/lib/outback/sftp_target.rb +70 -0
  19. data/lib/outback/source.rb +9 -2
  20. data/lib/outback/source_archive.rb +17 -0
  21. data/lib/outback/support/attr_setter.rb +1 -1
  22. data/lib/outback/support/configurable.rb +5 -3
  23. data/lib/outback/target.rb +51 -14
  24. data/lib/outback/target_archive.rb +30 -0
  25. data/lib/outback/version.rb +3 -0
  26. data/lib/vendor/enumerable_ext.rb +9 -0
  27. data/lib/{outback/vendor → vendor}/metaclass.rb +1 -1
  28. data/lib/{outback/vendor → vendor}/methodphitamine.rb +1 -1
  29. data/lib/vendor/mysql.rb +1093 -0
  30. data/lib/vendor/mysql/charset.rb +325 -0
  31. data/lib/vendor/mysql/constants.rb +165 -0
  32. data/lib/vendor/mysql/error.rb +989 -0
  33. data/lib/vendor/mysql/packet.rb +78 -0
  34. data/lib/vendor/mysql/protocol.rb +770 -0
  35. data/lib/vendor/numeric_ext.rb +49 -0
  36. data/lib/vendor/string_ext.rb +19 -0
  37. metadata +53 -39
  38. data/MIT-LICENSE +0 -20
  39. data/VERSION +0 -1
  40. data/lib/outback/configuration_error.rb +0 -4
  41. data/lib/outback/directory_archive.rb +0 -8
  42. data/lib/outback/local_archive.rb +0 -6
  43. data/lib/outback/s3_archive.rb +0 -18
  44. data/lib/outback/temp_archive.rb +0 -5
  45. 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