webget-mirror 0.0.1
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.md +3 -0
- data/Manifest.txt +16 -0
- data/README.md +19 -0
- data/Rakefile +34 -0
- data/lib/webget/mirror/collect_page_info.rb +107 -0
- data/lib/webget/mirror/database/models.rb +123 -0
- data/lib/webget/mirror/database/open.rb +63 -0
- data/lib/webget/mirror/database/schema.rb +67 -0
- data/lib/webget/mirror/download_page.rb +83 -0
- data/lib/webget/mirror/find_links.rb +222 -0
- data/lib/webget/mirror/mirror.rb +246 -0
- data/lib/webget/mirror/utils.rb +32 -0
- data/lib/webget/mirror/version.rb +22 -0
- data/lib/webget/mirror/website.rb +204 -0
- data/lib/webget/mirror.rb +55 -0
- data/lib/webget-mirror.rb +2 -0
- metadata +153 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class Webget
|
|
4
|
+
class Mirror
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## double assert
|
|
8
|
+
## assert - double check
|
|
9
|
+
## make sure url.path does NOT start with // or
|
|
10
|
+
## /// !!
|
|
11
|
+
## and does NOT end_with /
|
|
12
|
+
## pages
|
|
13
|
+
|
|
14
|
+
def _broken_path?( path )
|
|
15
|
+
path.start_with?( '//' ) ||
|
|
16
|
+
path.end_with?( '/' ) ||
|
|
17
|
+
!path.start_with?( '/' ) ## note - MUST start with single slash (/)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## get all links
|
|
23
|
+
## ignore anchor links and
|
|
24
|
+
## split into internal and external
|
|
25
|
+
def _find_links( site:,
|
|
26
|
+
doc:,
|
|
27
|
+
url:,
|
|
28
|
+
verbose: true )
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## note - base_url is the doc(ument) url
|
|
32
|
+
## e.g. https://rsssf.org/curtour.html
|
|
33
|
+
base_url = URI( url )
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if _broken_path?( base_url.path )
|
|
37
|
+
puts "!! normalized base_url.path expected - got:"
|
|
38
|
+
pp url
|
|
39
|
+
pp base_url
|
|
40
|
+
exit 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
## note: Array#compact removes all nil values from an array.
|
|
45
|
+
## if no href in a - nokigiri return nil
|
|
46
|
+
##
|
|
47
|
+
## might still incl. empty string ("") - remove too - why? why not?
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
## fix - change to css('a[href]') or such ??
|
|
51
|
+
### document.css("a[href]").each do |a|
|
|
52
|
+
## links = doc.css('a').map { |a| a['href'] }.compact
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
links = doc.css( 'a[href]' ).map do |a|
|
|
56
|
+
## strip leading & trailing spaces e.g.
|
|
57
|
+
## "http://www.danskfodbold.dk "
|
|
58
|
+
## is invalid url!!!
|
|
59
|
+
a['href'].strip
|
|
60
|
+
end.reject do |href|
|
|
61
|
+
# skip
|
|
62
|
+
# - empty strings,
|
|
63
|
+
# - page anchors
|
|
64
|
+
href.empty? || href.start_with?('#') ||
|
|
65
|
+
|
|
66
|
+
## skip mailto links/javascript snippets
|
|
67
|
+
href.match?( /\A(?:mailto|javascript)/i ) ||
|
|
68
|
+
|
|
69
|
+
## also skip broken mailto links
|
|
70
|
+
## that is, missing mailto
|
|
71
|
+
## e.g.
|
|
72
|
+
href.include?( '@' )
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## split into internal & external
|
|
77
|
+
## make links absolute
|
|
78
|
+
## ignore anchor links (see above)
|
|
79
|
+
|
|
80
|
+
pages = []
|
|
81
|
+
externals = []
|
|
82
|
+
|
|
83
|
+
links.each do |href|
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
## auto-fix ("site-wide") known quirks:
|
|
87
|
+
href = site.autofix_href.call( href ) if site.autofix_href.is_a?( Proc )
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
page_url = nil
|
|
91
|
+
begin
|
|
92
|
+
|
|
93
|
+
## special case
|
|
94
|
+
## check for protocol-relative // e.g. //hello.html
|
|
95
|
+
## NOT handled by URI
|
|
96
|
+
## URI makes hello.html into host !!!
|
|
97
|
+
## host is hello.html and path is nil
|
|
98
|
+
## only works properly with triple ///
|
|
99
|
+
## e.g. ///hello.html
|
|
100
|
+
## now host is nil, and path is /hello.html
|
|
101
|
+
|
|
102
|
+
## URI.join(URI("https://example.com/page.html"), "//cdn.example.com/file.js")
|
|
103
|
+
## # => #<URI::HTTPS https://cdn.example.com/file.js> ✓ Works!
|
|
104
|
+
##
|
|
105
|
+
## But with just the string:
|
|
106
|
+
## URI("//cdn.example.com/file.js")
|
|
107
|
+
## Parses incorrectly—no scheme, treats cdn.example.com as host !!!!!
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## The browser breaks down //path/page.html like this:
|
|
111
|
+
## - Protocol: Inherited from the current page (e.g., https:).
|
|
112
|
+
## - Domain (Authority): path
|
|
113
|
+
## - File Path: /page.html
|
|
114
|
+
##
|
|
115
|
+
## If your website is hosted on https://example.com and
|
|
116
|
+
## a user clicks <a href="//path/page.html">, the browser will try
|
|
117
|
+
## to navigate to https://path/page.html.
|
|
118
|
+
## Unless you own a domain name that is literally just path,
|
|
119
|
+
## this will result in a "Site cannot be reached" error.
|
|
120
|
+
###
|
|
121
|
+
### "legacy" protocol relative is "//://" !!!!
|
|
122
|
+
##
|
|
123
|
+
## move notes from here to dedicated notes page!!
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if href.start_with?("//")
|
|
128
|
+
puts "!!! debug break on href starting with //:"
|
|
129
|
+
pp href
|
|
130
|
+
pp url
|
|
131
|
+
pp base_url
|
|
132
|
+
exit 1
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
## check if href is absolute?
|
|
138
|
+
href_url = URI( href )
|
|
139
|
+
|
|
140
|
+
## assume already absolute
|
|
141
|
+
if href_url.scheme && href_url.host
|
|
142
|
+
page_url = href_url
|
|
143
|
+
else
|
|
144
|
+
## try to make absolute (relative to base_url)
|
|
145
|
+
page_url = URI.join(base_url, href_url)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
rescue => ex
|
|
149
|
+
## skip bad urls and log
|
|
150
|
+
|
|
151
|
+
msg = "bad url in #{base_url.path}:\n#{href}\nex:#{ex}\n"
|
|
152
|
+
|
|
153
|
+
## note - only report in verbose mode (fresh download or such)!!!
|
|
154
|
+
if verbose
|
|
155
|
+
log( msg )
|
|
156
|
+
puts "!! " + msg
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
next
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
###
|
|
163
|
+
## fix-fix-fix
|
|
164
|
+
## check for optional www too
|
|
165
|
+
## assume same for now ??
|
|
166
|
+
## or better add to autofix
|
|
167
|
+
## if www.rsssf.org change to rsssf.org
|
|
168
|
+
|
|
169
|
+
if page_url.host == site.host ## e.g. 'rsssf.org'
|
|
170
|
+
if page_url.path == base_url.path
|
|
171
|
+
puts " anchor #{href} => #{page_url.fragment}" if verbose
|
|
172
|
+
else
|
|
173
|
+
puts " internal page #{href} => #{page_url.path}" if verbose
|
|
174
|
+
|
|
175
|
+
## note - for internal pages
|
|
176
|
+
## for now no SUPPORT for query
|
|
177
|
+
## e.g. foo=1&bar=2
|
|
178
|
+
if page_url.query
|
|
179
|
+
## change to ValueError or such - why? why not?
|
|
180
|
+
## raise ArgumentError, "query in internal page links not yet supported, sorry - got #{page_url}"
|
|
181
|
+
msg = "query in internal page links not yet supported, sorry - got #{page_url}"
|
|
182
|
+
puts "!! WARN - #{msg}"
|
|
183
|
+
log( msg )
|
|
184
|
+
next
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if _broken_path?( page_url.path )
|
|
188
|
+
puts "!! normalized page_url.path expected - got:"
|
|
189
|
+
pp page_url.path
|
|
190
|
+
pp page_url
|
|
191
|
+
puts "base_url:"
|
|
192
|
+
pp url
|
|
193
|
+
pp base_url
|
|
194
|
+
exit 1
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
pages << page_url.path
|
|
198
|
+
end
|
|
199
|
+
else
|
|
200
|
+
puts "!! external #{href} => #{page_url}" if verbose
|
|
201
|
+
externals << page_url.to_s
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
## make uniq
|
|
206
|
+
pages = pages.uniq
|
|
207
|
+
externals = externals.uniq
|
|
208
|
+
|
|
209
|
+
if verbose
|
|
210
|
+
puts " #{pages.size} internal & #{externals.size} external link(s) found in #{base_url.path}:"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
pp pages
|
|
214
|
+
pp externals
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
[pages, externals]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
end ## class Mirror
|
|
222
|
+
end ## class Webget
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
|
|
2
|
+
## use recursive_download_page or such - why? why not?
|
|
3
|
+
## or add recursive flag
|
|
4
|
+
|
|
5
|
+
=begin
|
|
6
|
+
visited: 100 (downloaded: 98) - 35586 page(s) indexed (9158 cached, 26428 missing)
|
|
7
|
+
[98/26526 - 0.00%] 2:14 mins - 1.38 secs/page, estimate: 608:53 mins
|
|
8
|
+
=end
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Webget
|
|
12
|
+
class Mirror
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
## use limit for batch - why? why not?
|
|
16
|
+
## start of / try a batch of a hundred
|
|
17
|
+
def _mirror_pages( site:,
|
|
18
|
+
force: false,
|
|
19
|
+
batch: 1000 )
|
|
20
|
+
|
|
21
|
+
visited = 0
|
|
22
|
+
downloaded = 0
|
|
23
|
+
|
|
24
|
+
time_start = Time.now
|
|
25
|
+
|
|
26
|
+
loop do
|
|
27
|
+
|
|
28
|
+
## (i) prioritize main pages (e.g. use start_pages_path)
|
|
29
|
+
## /curdom.html
|
|
30
|
+
## /curtour.html
|
|
31
|
+
## /histdom.html
|
|
32
|
+
## /intclub.html
|
|
33
|
+
## /intland.html
|
|
34
|
+
page_recs = MirrorDb::Model::Page.where( cached: false,
|
|
35
|
+
path: site.start_pages_path
|
|
36
|
+
).limit( batch )
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## (ii) prefer pages (e.g. use boost_pages_path_like)
|
|
40
|
+
## starting with /tables,/tables[a-z]/
|
|
41
|
+
if page_recs.size == 0 && site.boost_pages_path_like?
|
|
42
|
+
page_recs = MirrorDb::Model::Page.where( cached: false ).
|
|
43
|
+
where( 'path LIKE ?',
|
|
44
|
+
site.boost_pages_path_like ).limit( batch )
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
## (iii) retry "unconstrained" if nothing found matching (i & ii)
|
|
48
|
+
if page_recs.size == 0
|
|
49
|
+
page_recs = MirrorDb::Model::Page.where( cached: false ).limit( batch )
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### no more pages - done - break out of loop and say goodbye
|
|
54
|
+
break if page_recs.size == 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
page_recs.each_with_index do |page_rec,i|
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
## fix-fix-fix - change to mime type - why? why not?
|
|
63
|
+
## allow pages with no extensions!!!
|
|
64
|
+
|
|
65
|
+
### special case for non .html/.htm pages (e.g. .pdf others too??)
|
|
66
|
+
## do NOT download / mirror / cache for now
|
|
67
|
+
if page_rec.not_html?
|
|
68
|
+
page_rec.update!( cached: true )
|
|
69
|
+
next
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## note - on download (not if cached)
|
|
74
|
+
## encoding
|
|
75
|
+
## might be get changed
|
|
76
|
+
## ALWAYS use updated encoding!!
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
## note - workaround for windows
|
|
80
|
+
## on windows File.exist? (and Webcache.cached?)
|
|
81
|
+
## is case-insensitive
|
|
82
|
+
## e.g. /USAdave/ is the same as /usadave/
|
|
83
|
+
##
|
|
84
|
+
## as a workaround ALWAYS hardcode 404
|
|
85
|
+
## for /USAdave/ to get (and record) 404 (and not CACHE HITS!!)
|
|
86
|
+
## e.g. try https://rsssf.org/USAdave/cncc.html => 404 (NOT FOUND)
|
|
87
|
+
## https://rsssf.org/usadave/cncc.html => 200 (OK)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## note - url e.g. https://rsssf.org
|
|
91
|
+
## path MUST start with / e.g. /curtour.html
|
|
92
|
+
## resulting in https://rsssf.org/curtour.html
|
|
93
|
+
|
|
94
|
+
url = site.base_url+page_rec.path
|
|
95
|
+
|
|
96
|
+
## if %r{/USAdave/}.match?(page_rec.path)
|
|
97
|
+
## ['', {status: 404}]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
## check if not in cache
|
|
102
|
+
## note - use force == true to always (force) download
|
|
103
|
+
|
|
104
|
+
html, response_meta = _download_page( url,
|
|
105
|
+
encoding: page_rec.encoding,
|
|
106
|
+
force: force )
|
|
107
|
+
|
|
108
|
+
if response_meta
|
|
109
|
+
downloaded += 1
|
|
110
|
+
puts " --- " + fmt_time_diff( time_start, count: downloaded )
|
|
111
|
+
|
|
112
|
+
###
|
|
113
|
+
## special case
|
|
114
|
+
## check for 404 NOT FOUND
|
|
115
|
+
if response_meta[:http_status] == 404
|
|
116
|
+
page_rec.update!( http_status: 404,
|
|
117
|
+
cached: true )
|
|
118
|
+
|
|
119
|
+
next ### note - skip further processing on 404 (no links etc.)!!
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
html = site.errata( html, url: url ) if site.errata?
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
## Standard HTML4-style parsing (default)
|
|
131
|
+
## doc = Nokogiri::HTML(malformed_html)
|
|
132
|
+
## -or-
|
|
133
|
+
## More robust HTML5 parsing
|
|
134
|
+
##doc = Nokogiri::HTML5(malformed_html)
|
|
135
|
+
|
|
136
|
+
doc = Nokogiri::HTML( html )
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## get (page meta info)
|
|
140
|
+
## title, tabs (count), html_doctype, html_charset
|
|
141
|
+
page_info = _collect_page_info( doc, html: html )
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## note - if response meta data present than fresh download (not cached)
|
|
146
|
+
## cached = response_meta ? false : true
|
|
147
|
+
|
|
148
|
+
## turn on verbose mode only if page downloaded (not on cache hit)
|
|
149
|
+
verbose = response_meta ? true : false
|
|
150
|
+
## verbose = true
|
|
151
|
+
|
|
152
|
+
internals, _ = _find_links( site: site,
|
|
153
|
+
doc: doc,
|
|
154
|
+
url: url,
|
|
155
|
+
verbose: verbose
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## add links to db
|
|
160
|
+
internals.each do |path|
|
|
161
|
+
internal_rec = MirrorDb::Model::Page.find_or_create_by!(
|
|
162
|
+
path: path ) do |rec|
|
|
163
|
+
puts " add linked page #{rec.path}"
|
|
164
|
+
|
|
165
|
+
rec.encoding = site.page_encoding( rec.path )
|
|
166
|
+
rec.cached = false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
## puts " add link from #{page_rec.path} to #{internal_rec.path} to mirror.db"
|
|
170
|
+
### note allow - find (may happen after "crash" or interrupt)
|
|
171
|
+
link_rec = MirrorDb::Model::Link.find_or_create_by!(
|
|
172
|
+
from_page_id: page_rec.id,
|
|
173
|
+
to_page_id: internal_rec.id )
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
puts " [#{i+1}/#{page_recs.size}] update page #{page_rec.path} w/ #{internals.size} page(s) linked - >#{page_info[:title] || 'n/a'}<"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
###
|
|
180
|
+
## note - remove cached (flag) and replace with http_status => nil|200|404|etc?
|
|
181
|
+
## that is, cached = false => nil
|
|
182
|
+
## cached = true => 200|404|etc - why? why not??
|
|
183
|
+
attribs = {
|
|
184
|
+
cached: true
|
|
185
|
+
}
|
|
186
|
+
## add (optional) page_info attribus
|
|
187
|
+
more_attribs = {
|
|
188
|
+
title: page_info[:title], ## note - might be missing (nil) in some pages
|
|
189
|
+
html_doctype: page_info[:html_doctype],
|
|
190
|
+
html_charset: page_info[:html_charset],
|
|
191
|
+
tabs: page_info[:tabs]
|
|
192
|
+
}
|
|
193
|
+
attribs = attribs.merge( more_attribs )
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
## check for encoding when fresh download (via response meta data)
|
|
197
|
+
if response_meta
|
|
198
|
+
more_attribs = {
|
|
199
|
+
encoding: response_meta[:encoding] ? response_meta[:encoding].downcase : nil,
|
|
200
|
+
encoding_source: response_meta[:encoding_source], ## bom|html|http|user|fallback
|
|
201
|
+
encoding_valid: response_meta[:encoding_valid], ## nil|true|false
|
|
202
|
+
|
|
203
|
+
ascii7bit: response_meta[:ascii7bit], ## nil|true|false
|
|
204
|
+
## note - convert 10 - 212=>8, 233=>2 (use only first total count; no details)
|
|
205
|
+
chars_8bit: response_meta[:chars_8bit] ? response_meta[:chars_8bit].to_i(10) : nil,
|
|
206
|
+
utf8_replace: response_meta[:utf8_replace],
|
|
207
|
+
|
|
208
|
+
http_content_type: response_meta[:http_content_type],
|
|
209
|
+
http_content_length: response_meta[:http_content_length],
|
|
210
|
+
http_status: response_meta[:http_status]
|
|
211
|
+
}
|
|
212
|
+
attribs = attribs.merge( more_attribs )
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
page_rec.update!( **attribs )
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
visited += 1
|
|
221
|
+
|
|
222
|
+
if visited % 100 == 0
|
|
223
|
+
puts "\n visited: #{visited} (downloaded: #{downloaded}) - " +
|
|
224
|
+
" #{MirrorDb::Model::Page.count} page(s) indexed " +
|
|
225
|
+
"(#{MirrorDb::Model::Page.cached.count} cached, " +
|
|
226
|
+
"#{MirrorDb::Model::Page.not_cached.count} missing)"
|
|
227
|
+
|
|
228
|
+
puts " " + fmt_time_diff( time_start, step: downloaded,
|
|
229
|
+
count: downloaded+MirrorDb::Model::Page.not_cached.count )
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
puts "\n visited: #{visited} (downloaded: #{downloaded}) - " +
|
|
239
|
+
" #{MirrorDb::Model::Page.count} page(s) indexed " +
|
|
240
|
+
"(#{MirrorDb::Model::Page.cached.count} cached, " +
|
|
241
|
+
"#{MirrorDb::Model::Page.not_cached.count} missing)"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
end ## class Mirror
|
|
246
|
+
end ## class Webget
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class Webget
|
|
4
|
+
class Mirror
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def fmt_time_diff( time_start, time_end=Time.now, count:, step: nil )
|
|
9
|
+
time_diff = time_end - time_start
|
|
10
|
+
buf = String.new
|
|
11
|
+
|
|
12
|
+
if count == 0 || step == 0
|
|
13
|
+
buf += " %d:%02d mins" % [time_diff/60, time_diff%60]
|
|
14
|
+
elsif step
|
|
15
|
+
buf += " [#{step}/#{count} - %5.2f%%]" % [step*100/count]
|
|
16
|
+
|
|
17
|
+
buf += " %d:%02d mins" % [time_diff/60, time_diff%60]
|
|
18
|
+
buf += " - %5.2f secs/page" % [time_diff/step]
|
|
19
|
+
|
|
20
|
+
time_estimate = (time_diff/step) * count
|
|
21
|
+
buf += ", estimate: %d:%02d mins" % [time_estimate/60, time_estimate%60]
|
|
22
|
+
else
|
|
23
|
+
buf += " %d:%02d mins" % [time_diff/60, time_diff%60]
|
|
24
|
+
buf += " - %5.2f secs/page (#{count} pages)" % [time_diff/count]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
buf
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
end ## class Mirror
|
|
32
|
+
end ## class Webget
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
class Webget
|
|
3
|
+
class Mirror
|
|
4
|
+
MAJOR = 0 ## todo: namespace inside version or something - why? why not??
|
|
5
|
+
MINOR = 0
|
|
6
|
+
PATCH = 1
|
|
7
|
+
VERSION = [MAJOR,MINOR,PATCH].join('.')
|
|
8
|
+
|
|
9
|
+
def self.version
|
|
10
|
+
VERSION
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# version string for generator meta tag (includes ruby version)
|
|
14
|
+
def self.banner
|
|
15
|
+
"webget-mirror/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.root
|
|
19
|
+
File.expand_path( File.dirname(File.dirname(File.dirname(File.dirname(__FILE__)))) )
|
|
20
|
+
end
|
|
21
|
+
end # class Mirror
|
|
22
|
+
end # class Webget
|