sanguinews 0.60
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/sanguinews +4 -0
- data/ext/yencoded/extconf.rb +3 -0
- data/ext/yencoded/yencoded.c +89 -0
- data/ext/yencoded/yencoded.h +6 -0
- data/lib/sanguinews.rb +415 -0
- data/lib/sanguinews/file_to_upload.rb +109 -0
- data/lib/sanguinews/nntp.rb +896 -0
- data/lib/sanguinews/nntp_msg.rb +82 -0
- data/lib/sanguinews/thread-pool.rb +150 -0
- data/lib/sanguinews/version.rb +3 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8fa336b8808a65611854b554245a288b4b05f303
|
4
|
+
data.tar.gz: 0c4309243350764bae4669b537f65d4ed9b79e12
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b231b17b59ab469a3d64294c6f3befc97abf0083ebfac5fce887e74511844591708f879de799b09dc75348f202d6bfd5aa1bb05bde52b8970960214922f6abba
|
7
|
+
data.tar.gz: e551da80f3d6f1641c4e47b46636b34ffcfd196fbd9f111c6fab804823df3112418669f38ecd5e3c2c34e4f8a25407f7616d0383d4146cfa4f281f4ab07a8c7f
|
data/bin/sanguinews
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
//////////////////////////////////////////////////////////////////////////
|
2
|
+
// Yencoded - C code to yencode bindary data for sanguinews
|
3
|
+
// Copyright (c) 2013-2014, Tadeus Dobrovolskij
|
4
|
+
// This library is free software; you can redistribute it and/or modify
|
5
|
+
// it under the terms of the GNU General Public License as published by
|
6
|
+
// the Free Software Foundation; either version 2 of the License, or
|
7
|
+
// (at your option) any later version.
|
8
|
+
//
|
9
|
+
// This library is distributed in the hope that it will be useful,
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
// GNU General Public License for more details.
|
13
|
+
//
|
14
|
+
// You should have received a copy of the GNU General Public License along
|
15
|
+
// with this library; if not, write to the Free Software Foundation, Inc.,
|
16
|
+
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
17
|
+
/////////////////////////////////////////////////////////////////////////
|
18
|
+
#include <ruby.h>
|
19
|
+
#include <yencoded.h>
|
20
|
+
|
21
|
+
void Init_yencoded() {
|
22
|
+
Yencoded = rb_define_module("Yencoded");
|
23
|
+
YencodedData = rb_define_module_under(Yencoded, "Data");
|
24
|
+
rb_define_singleton_method(YencodedData, "yenc", method_yencoded_data_yenc, 2);
|
25
|
+
}
|
26
|
+
|
27
|
+
VALUE method_yencoded_data_yenc(VALUE self, VALUE data,VALUE length) {
|
28
|
+
char *bindata;
|
29
|
+
long datalen;
|
30
|
+
int i=0;
|
31
|
+
int linelen=128;
|
32
|
+
long restlen;
|
33
|
+
long destlen;
|
34
|
+
unsigned char c;
|
35
|
+
unsigned char *output;
|
36
|
+
unsigned char *start;
|
37
|
+
VALUE result;
|
38
|
+
|
39
|
+
// convert ruby variables to c variables
|
40
|
+
bindata = StringValuePtr(data);
|
41
|
+
datalen = FIX2LONG(length);
|
42
|
+
|
43
|
+
restlen = datalen; //restlen is our byte processing counter
|
44
|
+
destlen = restlen; //will be needing this for memory allocation
|
45
|
+
output = (unsigned char*)malloc(2 * destlen * sizeof(char));
|
46
|
+
start = output; //starting address will be stored here
|
47
|
+
while (restlen>0) {
|
48
|
+
c=(unsigned char) *bindata; //get byte
|
49
|
+
c=c+42; //add 42 as per yenc specs
|
50
|
+
bindata++; restlen--;
|
51
|
+
switch(c) { //special characters
|
52
|
+
case 0:
|
53
|
+
case 10:
|
54
|
+
case 13:
|
55
|
+
case 61:
|
56
|
+
destlen++; i++; //we need more memory than expected
|
57
|
+
*output=61; output++; //add escape char to output
|
58
|
+
c = c+64;
|
59
|
+
break;
|
60
|
+
case 9: //escape tab and space if the are first or last on the line
|
61
|
+
case 32:
|
62
|
+
if ((i==0)||(i==linelen-1)) {
|
63
|
+
destlen++; i++; //we need more memory than expected
|
64
|
+
*output=61; output++; //add escape char to output
|
65
|
+
c = c+64;
|
66
|
+
}
|
67
|
+
break;
|
68
|
+
case 46: //escape dot if it's in a first column
|
69
|
+
if (i==0) {
|
70
|
+
destlen++; i++; //we need more memory than expected
|
71
|
+
*output=61; output++; //add escape char to output
|
72
|
+
c = c+64;
|
73
|
+
}
|
74
|
+
break;
|
75
|
+
}
|
76
|
+
*output=c; output++; i++;
|
77
|
+
if ((i>=linelen)||(restlen==0)){
|
78
|
+
destlen++; destlen++;
|
79
|
+
*output=13; output++; //according to yenc specs we must use windows style line breaks
|
80
|
+
*output=10; output++;
|
81
|
+
i=0;
|
82
|
+
}
|
83
|
+
}
|
84
|
+
*output=0; //NULL termination is required
|
85
|
+
destlen++;
|
86
|
+
result=rb_str_new2((char*)start);
|
87
|
+
free(start);
|
88
|
+
return result;
|
89
|
+
}
|
data/lib/sanguinews.rb
ADDED
@@ -0,0 +1,415 @@
|
|
1
|
+
########################################################################
|
2
|
+
# sanguinews - usenet command line binary poster written in ruby
|
3
|
+
# Copyright (c) 2013, Tadeus Dobrovolskij
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License along
|
15
|
+
# with this program; if not, write to the Free Software Foundation, Inc.,
|
16
|
+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
17
|
+
########################################################################
|
18
|
+
|
19
|
+
require 'rubygems'
|
20
|
+
require 'bundler/setup'
|
21
|
+
require 'optparse'
|
22
|
+
require 'monitor'
|
23
|
+
require 'date'
|
24
|
+
require 'tempfile'
|
25
|
+
# Following non-standard gems are needed
|
26
|
+
require 'parseconfig'
|
27
|
+
require 'speedometer'
|
28
|
+
# Our library
|
29
|
+
require_relative 'sanguinews/thread-pool'
|
30
|
+
require_relative 'sanguinews/nntp'
|
31
|
+
require_relative 'sanguinews/nntp_msg'
|
32
|
+
require_relative 'sanguinews/file_to_upload'
|
33
|
+
require_relative 'sanguinews/yencoded'
|
34
|
+
require_relative 'sanguinews/version'
|
35
|
+
|
36
|
+
module Sanguinews
|
37
|
+
module_function
|
38
|
+
# Method returns yenc encoded string and crc32 value
|
39
|
+
def yencode(file, length, queue)
|
40
|
+
i = 1
|
41
|
+
until file.eof?
|
42
|
+
bindata = file.read(length)
|
43
|
+
# We can't take all memory, so we wait
|
44
|
+
queue.synchronize do
|
45
|
+
@cond.wait_while do
|
46
|
+
queue.length > @threads * 3
|
47
|
+
end
|
48
|
+
end
|
49
|
+
data = {}
|
50
|
+
final_data = []
|
51
|
+
len = bindata.length
|
52
|
+
data[:yenc] = Yencoded::Data.yenc(bindata, len)
|
53
|
+
data[:crc32] = Zlib.crc32(bindata, 0).to_s(16)
|
54
|
+
data[:length] = len
|
55
|
+
data[:chunk] = i
|
56
|
+
data[:file] = file
|
57
|
+
final_data[0] = form_message(data)
|
58
|
+
final_data[1] = file
|
59
|
+
queue.push(final_data)
|
60
|
+
i += 1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def form_message(data)
|
65
|
+
message = data[:yenc]
|
66
|
+
length = data[:length]
|
67
|
+
pcrc32 = data[:crc32]
|
68
|
+
file = data[:file]
|
69
|
+
chunk = data[:chunk]
|
70
|
+
crc32 = file.crc32
|
71
|
+
fsize = file.size
|
72
|
+
chunks = file.chunks
|
73
|
+
basename = file.name
|
74
|
+
# usenet works with ASCII
|
75
|
+
subject="#{@prefix}#{file.dir_prefix}\"#{basename}\" yEnc (#{chunk}/#{chunks})"
|
76
|
+
msg = NntpMsg.new(@from, @groups, subject)
|
77
|
+
msg.poster = "sanguinews v#{Sanguinews::VERSION} (ruby #{RUBY_VERSION}) - https://github.com/tdobrovolskij/sanguinews"
|
78
|
+
msg.xna = @xna
|
79
|
+
msg.message = message.force_encoding('ASCII-8BIT')
|
80
|
+
msg.yenc_body(chunk, chunks, crc32, pcrc32, length, fsize, basename)
|
81
|
+
msg = msg.return_self
|
82
|
+
{ message: msg, filename: basename, chunk: chunk, length: length }
|
83
|
+
end
|
84
|
+
|
85
|
+
def connect(x)
|
86
|
+
begin
|
87
|
+
nntp = Net::NNTP.start(@server, @port, @username, @password, @mode)
|
88
|
+
rescue
|
89
|
+
@s.log([$!, $@], stderr: true) if @debug
|
90
|
+
if @verbose
|
91
|
+
parse_error($!.to_s)
|
92
|
+
@s.log("Connection nr. #{x} has failed. Reconnecting...\n", stderr: true)
|
93
|
+
end
|
94
|
+
sleep @delay
|
95
|
+
retry
|
96
|
+
end
|
97
|
+
return nntp
|
98
|
+
end
|
99
|
+
|
100
|
+
def parse_config(config)
|
101
|
+
config = ParseConfig.new(config)
|
102
|
+
config.get_params()
|
103
|
+
@username = config['username']
|
104
|
+
@password = config['password']
|
105
|
+
@from = config['from']
|
106
|
+
@server = config['server']
|
107
|
+
@port = config['port']
|
108
|
+
@threads = config['connections'].to_i
|
109
|
+
@length = config['article_size'].to_i
|
110
|
+
@delay = config['reconnect_delay'].to_i
|
111
|
+
@groups = config['groups']
|
112
|
+
@prefix = config['prefix']
|
113
|
+
ssl = config['ssl']
|
114
|
+
if ssl == 'yes'
|
115
|
+
@mode = :tls
|
116
|
+
else
|
117
|
+
@mode = :original
|
118
|
+
end
|
119
|
+
config['xna'] == 'yes' ? @xna = true : @xna = false
|
120
|
+
config['nzb'] == 'yes' ? @nzb = true : @nzb = false
|
121
|
+
config['header_check'] == 'yes' ? @header_check = true : @header_check = false
|
122
|
+
config['debug'] == 'yes' ? @debug = true : @debug = false
|
123
|
+
end
|
124
|
+
|
125
|
+
def get_msgid(response)
|
126
|
+
msgid = ''
|
127
|
+
response.each do |r|
|
128
|
+
msgid = r.sub(/>.*/, '').tr("<", '') if r.end_with?('Article posted')
|
129
|
+
end
|
130
|
+
return msgid
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_options(args)
|
134
|
+
# version and legal info presented to user
|
135
|
+
banner = []
|
136
|
+
banner << ""
|
137
|
+
banner << "sanguinews v#{Sanguinews::VERSION}. Copyright (c) 2013-2014 Tadeus Dobrovolskij."
|
138
|
+
banner << "Comes with ABSOLUTELY NO WARRANTY. Distributed under GPL v2 license(http://www.gnu.org/licenses/gpl-2.0.txt)."
|
139
|
+
banner << "sanguinews is a simple nntp(usenet) binary poster. It supports multithreading and SSL. More info in README."
|
140
|
+
banner << ""
|
141
|
+
# option parser
|
142
|
+
options = {}
|
143
|
+
options[:filemode] = false
|
144
|
+
options[:files] = []
|
145
|
+
|
146
|
+
opt_parser = OptionParser.new do |opt|
|
147
|
+
opt.banner = "Usage: #{$0} [OPTIONS] [DIRECTORY] | -f FILE1..[FILEX]"
|
148
|
+
opt.separator ""
|
149
|
+
opt.separator "Options"
|
150
|
+
|
151
|
+
opt.on("-c", "--config CONFIG", "use different config file") do |cfg|
|
152
|
+
options[:config] = cfg
|
153
|
+
end
|
154
|
+
opt.on("-C", "--check", "check headers while uploading; slow but reliable") do
|
155
|
+
options[:header_check] = true
|
156
|
+
end
|
157
|
+
opt.on("-f", "--file FILE", "upload FILE, treat all additional parameters as files") do |file|
|
158
|
+
options[:filemode] = true
|
159
|
+
options[:files] << file
|
160
|
+
end
|
161
|
+
opt.on("-g", "--groups GROUP_LIST", "use these groups(comma separated) for upload") do |group_list|
|
162
|
+
options[:groups] = group_list
|
163
|
+
end
|
164
|
+
opt.on("-h", "--help", "help") do
|
165
|
+
banner.each do |msg|
|
166
|
+
puts msg
|
167
|
+
end
|
168
|
+
puts opt_parser
|
169
|
+
puts
|
170
|
+
exit
|
171
|
+
end
|
172
|
+
opt.on("-p", "--password PASSWORD", "use PASSWORD as your password(overwrites config file)") do |password|
|
173
|
+
options[:password] = password
|
174
|
+
end
|
175
|
+
opt.on("-u", "--user USERNAME", "use USERNAME as your username(overwrites config file)") do |username|
|
176
|
+
options[:username] = username
|
177
|
+
end
|
178
|
+
opt.on("-v", "--verbose", "be verbose?") do
|
179
|
+
options[:verbose] = true
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
begin
|
184
|
+
opt_parser.parse!(args)
|
185
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
186
|
+
puts opt_parser
|
187
|
+
exit 1
|
188
|
+
end
|
189
|
+
|
190
|
+
options[:directory] = args[0] unless options[:filemode]
|
191
|
+
|
192
|
+
# in file mode treat every additional parameter as a file
|
193
|
+
if !args.empty? && options[:filemode]
|
194
|
+
args.each do |file|
|
195
|
+
options[:files] << file.to_s
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# exit when no file list is provided
|
200
|
+
if options[:directory].nil? && options[:files].empty?
|
201
|
+
puts "You need to specify something to upload!"
|
202
|
+
puts opt_parser
|
203
|
+
exit 1
|
204
|
+
end
|
205
|
+
|
206
|
+
return options
|
207
|
+
end
|
208
|
+
|
209
|
+
def parse_error(msg, **info)
|
210
|
+
if info[:file].nil? || info[:chunk].nil?
|
211
|
+
fileinfo = ''
|
212
|
+
else
|
213
|
+
fileinfo = '(' + info[:file] + ' / Chunk: ' + info[:chunk].to_s + ')'
|
214
|
+
end
|
215
|
+
|
216
|
+
case
|
217
|
+
when /\A411/ === msg
|
218
|
+
@s.log("Invalid newsgroup specified.", stderr: true)
|
219
|
+
when /\A430/ === msg
|
220
|
+
@s.log("No such article. Maybe server is lagging...#{fileinfo}", stderr: true)
|
221
|
+
when /\A(4\d{2}\s)?437/ === msg
|
222
|
+
@s.log("Article rejected by server. Maybe it's too big.#{fileinfo}", stderr: true)
|
223
|
+
when /\A440/ === msg
|
224
|
+
@s.log("Posting not allowed.", stderr: true)
|
225
|
+
when /\A441/ === msg
|
226
|
+
@s.log("Posting failed for some reason.#{fileinfo}", stderr: true)
|
227
|
+
when /\A450/ === msg
|
228
|
+
@s.log("Not authorized.", stderr: true)
|
229
|
+
when /\A452/ === msg
|
230
|
+
@s.log("Wrong username and/or password.", stderr: true)
|
231
|
+
when /\A500/ === msg
|
232
|
+
@s.log("Command not recognized.", stderr: true)
|
233
|
+
when /\A501/ === msg
|
234
|
+
@s.log("Command syntax error.", stderr: true)
|
235
|
+
when /\A502/ === msg
|
236
|
+
@s.log("Access denied.", stderr: true)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def process_and_upload(queue, nntp_pool, info_lock, informed)
|
241
|
+
stuff = queue.pop
|
242
|
+
queue.synchronize do
|
243
|
+
@cond.signal
|
244
|
+
end
|
245
|
+
nntp = nntp_pool.pop
|
246
|
+
|
247
|
+
data = stuff[0]
|
248
|
+
file = stuff[1]
|
249
|
+
msg = data[:message]
|
250
|
+
chunk = data[:chunk]
|
251
|
+
basename = data[:filename]
|
252
|
+
length = data[:length]
|
253
|
+
full_size = msg.length
|
254
|
+
info_lock.synchronize do
|
255
|
+
if !informed[basename.to_sym]
|
256
|
+
@s.log("Uploading #{basename}\n")
|
257
|
+
@s.log(file.subject + "\n")
|
258
|
+
@s.log("Chunks: #{file.chunks}\n", stderr: true) if @verbose
|
259
|
+
informed[basename.to_sym] = true
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
@s.start
|
264
|
+
x = 1
|
265
|
+
begin
|
266
|
+
response = nntp.post msg
|
267
|
+
msgid = get_msgid(response)
|
268
|
+
if @header_check
|
269
|
+
sleep x
|
270
|
+
nntp.stat("<#{msgid}>")
|
271
|
+
end
|
272
|
+
rescue
|
273
|
+
@s.log([$!, $@], stderr: true) if @debug
|
274
|
+
if @verbose
|
275
|
+
parse_error($!.to_s, file: basename, chunk: chunk)
|
276
|
+
@s.log("Upload of chunk #{chunk} from file #{basename} unsuccessful. Retrying...\n", stderr: true)
|
277
|
+
end
|
278
|
+
sleep @delay
|
279
|
+
x += 4
|
280
|
+
retry
|
281
|
+
end
|
282
|
+
|
283
|
+
if @verbose
|
284
|
+
@s.log("Uploaded chunk Nr:#{chunk}\n", stderr: true)
|
285
|
+
end
|
286
|
+
|
287
|
+
@s.done(length)
|
288
|
+
@s.uploaded += full_size
|
289
|
+
if @nzb
|
290
|
+
file.write_segment_info(length, chunk, msgid)
|
291
|
+
end
|
292
|
+
nntp_pool.push(nntp)
|
293
|
+
end
|
294
|
+
|
295
|
+
def run!
|
296
|
+
# Parse options in config file
|
297
|
+
config = "~/.sanguinews.conf"
|
298
|
+
config = File.expand_path(config)
|
299
|
+
# variable to store if config was parsed
|
300
|
+
saw_config = false
|
301
|
+
if File.exist?(config)
|
302
|
+
saw_config = true
|
303
|
+
parse_config(config)
|
304
|
+
end
|
305
|
+
|
306
|
+
options = parse_options(ARGV)
|
307
|
+
|
308
|
+
optconfig = options[:config]
|
309
|
+
optconfig = '' if optconfig.nil?
|
310
|
+
if !File.exist?(optconfig) && !saw_config
|
311
|
+
puts "No config information specified. Aborting..."
|
312
|
+
exit
|
313
|
+
end
|
314
|
+
parse_config(optconfig) if File.exist?(optconfig)
|
315
|
+
|
316
|
+
options[:verbose] ? @verbose = true : @verbose = false
|
317
|
+
@header_check = true unless options[:header_check].nil?
|
318
|
+
filemode = options[:filemode]
|
319
|
+
|
320
|
+
@username = options[:username] unless options[:username].nil?
|
321
|
+
@password = options[:password] unless options[:password].nil?
|
322
|
+
@groups = options[:groups] unless options[:groups].nil?
|
323
|
+
directory = options[:directory] unless filemode
|
324
|
+
files = options[:files]
|
325
|
+
|
326
|
+
# skip hidden files
|
327
|
+
if !filemode
|
328
|
+
directory = directory + "/" unless directory.end_with?('/')
|
329
|
+
Dir.foreach(directory) do |item|
|
330
|
+
next if item.start_with?('.')
|
331
|
+
files << directory+item
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# "max" is needed only in dirmode
|
336
|
+
max = files.length
|
337
|
+
c = 1
|
338
|
+
|
339
|
+
unprocessed = 0
|
340
|
+
info_lock=Mutex.new
|
341
|
+
messages = Queue.new
|
342
|
+
messages.extend(MonitorMixin)
|
343
|
+
@cond = messages.new_cond
|
344
|
+
files_to_process = []
|
345
|
+
@s = Speedometer.new(units: "KB", progressbar: true)
|
346
|
+
@s.uploaded = 0
|
347
|
+
|
348
|
+
pool = Queue.new
|
349
|
+
Thread.new {
|
350
|
+
@threads.times do |x|
|
351
|
+
nntp = connect(x)
|
352
|
+
pool.push(nntp)
|
353
|
+
end
|
354
|
+
}
|
355
|
+
|
356
|
+
p = Pool.new(@threads)
|
357
|
+
informed = {}
|
358
|
+
|
359
|
+
files.each do |file|
|
360
|
+
next if !File.file?(file)
|
361
|
+
|
362
|
+
informed[file.to_sym] = false
|
363
|
+
file = FileToUpload.new(
|
364
|
+
name: file, chunk_length: @length,
|
365
|
+
prefix: @prefix, current: c, last: max, filemode: filemode,
|
366
|
+
from: @from, groups: @groups, nzb: @nzb
|
367
|
+
)
|
368
|
+
@s.to_upload += file.size
|
369
|
+
|
370
|
+
info_lock.synchronize do
|
371
|
+
unprocessed += file.chunks
|
372
|
+
end
|
373
|
+
|
374
|
+
files_to_process << file
|
375
|
+
c += 1
|
376
|
+
end
|
377
|
+
|
378
|
+
# let's give a little bit higher priority for file processing thread
|
379
|
+
@t = Thread.new {
|
380
|
+
files_to_process.each do |file|
|
381
|
+
@s.log("Calculating CRC32 value for #{file.name}\n", stderr: true) if @verbose
|
382
|
+
file.file_crc32
|
383
|
+
@s.log("Encoding #{file.name}\n")
|
384
|
+
yencode(file, @length, messages)
|
385
|
+
end
|
386
|
+
}
|
387
|
+
@t.priority += 2
|
388
|
+
|
389
|
+
until unprocessed == 0
|
390
|
+
p.schedule do
|
391
|
+
process_and_upload(messages, pool, info_lock, informed)
|
392
|
+
end
|
393
|
+
unprocessed -= 1
|
394
|
+
end
|
395
|
+
|
396
|
+
p.shutdown
|
397
|
+
|
398
|
+
until pool.empty?
|
399
|
+
nntp = pool.pop
|
400
|
+
nntp.finish
|
401
|
+
end
|
402
|
+
|
403
|
+
@s.stop
|
404
|
+
puts
|
405
|
+
|
406
|
+
files_to_process.each do |file|
|
407
|
+
if files_to_process.last == file
|
408
|
+
last = true
|
409
|
+
else
|
410
|
+
last = false
|
411
|
+
end
|
412
|
+
file.close(last)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|