wayback_machine_downloader_straw 2.4.6 → 2.4.8
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12259647dc99ef1c263b3d797098233be4f194bf1a4e9bc047e1dfb4d0fc9468
|
|
4
|
+
data.tar.gz: 5d9d6bd47efca6b43a5c1d6c2606bb2871edea0b4dc781f206bbab8c9c66aaa0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 670efe274663448a9967d2e39920ce234af1ba5c0aae82135c56f64899904f510096ac19b6367a5b90af43d3878b156a21b2fb1138a2e567041b55205fc97c67
|
|
7
|
+
data.tar.gz: 2d4d6e927664ea831c0611bbbc89d74ee6c144629f8afb2b2a58428cff1f78846832979bd994dfd97a51cff03ee52ab9c5c09beebc1b007248f1c043aa3bdd2a
|
|
@@ -48,6 +48,10 @@ option_parser = OptionParser.new do |opts|
|
|
|
48
48
|
options[:all] = true
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
opts.on("--keep-duplicates", "Do not collapse duplicate CDX captures by digest") do |t|
|
|
52
|
+
options[:keep_duplicates] = true
|
|
53
|
+
end
|
|
54
|
+
|
|
51
55
|
opts.on("-c", "--concurrency NUMBER", Integer, "Number of multiple files to download at a time", "Default is one file at a time (ie. 20)") do |t|
|
|
52
56
|
options[:threads_count] = t
|
|
53
57
|
end
|
|
@@ -80,6 +84,10 @@ option_parser = OptionParser.new do |opts|
|
|
|
80
84
|
options[:keep] = true
|
|
81
85
|
end
|
|
82
86
|
|
|
87
|
+
opts.on("--delay SECONDS", Float, "Delay between downloads in seconds (default: 0)") do |t|
|
|
88
|
+
options[:delay] = t
|
|
89
|
+
end
|
|
90
|
+
|
|
83
91
|
opts.on("--rt", "--retry N", Integer, "Maximum number of retries for failed downloads (default: 3)") do |t|
|
|
84
92
|
options[:max_retries] = t
|
|
85
93
|
end
|
|
@@ -4,36 +4,53 @@ require 'uri'
|
|
|
4
4
|
module ArchiveAPI
|
|
5
5
|
|
|
6
6
|
def get_raw_list_from_api(url, page_index, http)
|
|
7
|
-
# Automatically append /*
|
|
7
|
+
# Automatically append /* for host-only URLs
|
|
8
8
|
# This is a workaround for an issue with the API and *some* domains.
|
|
9
9
|
# See https://github.com/StrawberryMaster/wayback-machine-downloader/issues/6
|
|
10
|
-
# But don't do this when exact_url flag is set
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
# But don't do this when exact_url flag is set, and never append twice
|
|
11
|
+
match_type = nil
|
|
12
|
+
if url && !@exact_url
|
|
13
|
+
normalized_url = url.to_s
|
|
14
|
+
has_wildcard = normalized_url.include?('*')
|
|
15
|
+
host_and_rest = normalized_url
|
|
16
|
+
.sub(/\Ahttps?:\/\//i, '')
|
|
17
|
+
.split(/[?#]/, 2)
|
|
18
|
+
.first
|
|
19
|
+
has_path = host_and_rest.include?('/')
|
|
20
|
+
|
|
21
|
+
unless has_wildcard || has_path
|
|
22
|
+
match_type = "prefix"
|
|
23
|
+
end
|
|
13
24
|
end
|
|
14
25
|
|
|
15
26
|
request_url = URI("https://web.archive.org/cdx/search/cdx")
|
|
16
27
|
params = [["output", "json"], ["url", url]] + parameters_for_api(page_index)
|
|
28
|
+
params << ["matchType", match_type] if match_type
|
|
17
29
|
request_url.query = URI.encode_www_form(params)
|
|
18
30
|
|
|
19
31
|
retries = 0
|
|
20
32
|
max_retries = (@max_retries || 3)
|
|
21
|
-
|
|
33
|
+
base_delay = WaybackMachineDownloader::RETRY_DELAY rescue 2
|
|
22
34
|
|
|
23
35
|
begin
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
request["Accept-Encoding"] = "gzip"
|
|
28
|
-
response = http.request(request)
|
|
36
|
+
if HTTPX_AVAILABLE && http.is_a?(HTTPX::Session)
|
|
37
|
+
response = http.get(request_url)
|
|
38
|
+
raise response.error if response.is_a?(HTTPX::ErrorResponse)
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
code = response.status
|
|
41
|
+
body = response.body.to_s.strip
|
|
42
|
+
else
|
|
43
|
+
request = Net::HTTP::Get.new(request_url)
|
|
44
|
+
request["User-Agent"] = "wmd-straw/#{WaybackMachineDownloader::VERSION rescue '2.4.8'}"
|
|
45
|
+
request["Connection"] = "keep-alive"
|
|
46
|
+
request["Accept-Encoding"] = "gzip, deflate"
|
|
47
|
+
response = http.request(request)
|
|
48
|
+
code = response.code.to_i
|
|
49
|
+
body = decompress_body(response)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
case code
|
|
31
53
|
when 200
|
|
32
|
-
body = if response['content-encoding'] == 'gzip'
|
|
33
|
-
Zlib::GzipReader.new(StringIO.new(response.body)).read
|
|
34
|
-
else
|
|
35
|
-
response.body.to_s.strip
|
|
36
|
-
end
|
|
37
54
|
return [] if body.empty?
|
|
38
55
|
begin
|
|
39
56
|
json = JSON.parse(body)
|
|
@@ -44,16 +61,22 @@ module ArchiveAPI
|
|
|
44
61
|
raise "Malformed JSON response: #{e.message}"
|
|
45
62
|
end
|
|
46
63
|
when 429, 500, 502, 503, 504
|
|
47
|
-
raise "Server error #{
|
|
64
|
+
raise "Server error #{code}: #{response.respond_to?(:message) ? response.message : ''}"
|
|
48
65
|
else
|
|
49
|
-
warn "Unexpected API response #{
|
|
66
|
+
warn "Unexpected API response #{code} for #{url}"
|
|
50
67
|
[]
|
|
51
68
|
end
|
|
52
69
|
rescue Net::ReadTimeout, Net::OpenTimeout, StandardError => e
|
|
53
70
|
if retries < max_retries
|
|
54
71
|
retries += 1
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
|
|
73
|
+
jitter = rand(0.0..1.0)
|
|
74
|
+
sleep_time = (base_delay * (2 ** (retries - 1))) + jitter
|
|
75
|
+
|
|
76
|
+
warn "Error talking to Wayback CDX API (#{e.class}: #{e.message}) for #{url}. " \
|
|
77
|
+
"Retrying in #{sleep_time.round(2)}s (attempt #{retries}/#{max_retries})..."
|
|
78
|
+
|
|
79
|
+
sleep(sleep_time)
|
|
57
80
|
retry
|
|
58
81
|
else
|
|
59
82
|
warn "Giving up on Wayback CDX API for #{url} after #{max_retries} attempts. (Last error: #{e.message})"
|
|
@@ -63,12 +86,28 @@ module ArchiveAPI
|
|
|
63
86
|
end
|
|
64
87
|
|
|
65
88
|
def parameters_for_api(page_index)
|
|
66
|
-
parameters = [["fl", "timestamp,original"], ["
|
|
67
|
-
parameters.push(["
|
|
89
|
+
parameters = [["fl", "timestamp,original"], ["gzip", "true"]]
|
|
90
|
+
parameters.push(["collapse", "digest"]) unless @keep_duplicates || @all_timestamps
|
|
91
|
+
parameters.push(["filter", "statuscode:2..|30[12378]"]) unless @all
|
|
68
92
|
parameters.push(["from", @from_timestamp.to_s]) if @from_timestamp && @from_timestamp != 0
|
|
69
93
|
parameters.push(["to", @to_timestamp.to_s]) if @to_timestamp && @to_timestamp != 0
|
|
70
94
|
parameters.push(["page", page_index]) if page_index
|
|
71
95
|
parameters
|
|
72
96
|
end
|
|
73
97
|
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def decompress_body(response)
|
|
101
|
+
body = response.body.to_s
|
|
102
|
+
return body if body.empty?
|
|
103
|
+
|
|
104
|
+
case response['content-encoding']
|
|
105
|
+
when 'gzip'
|
|
106
|
+
Zlib::GzipReader.new(StringIO.new(body)).read rescue body
|
|
107
|
+
when 'deflate'
|
|
108
|
+
Zlib::Inflate.inflate(body) rescue body
|
|
109
|
+
else
|
|
110
|
+
body.strip
|
|
111
|
+
end
|
|
112
|
+
end
|
|
74
113
|
end
|
|
@@ -40,11 +40,17 @@ module SubdomainProcessor
|
|
|
40
40
|
private
|
|
41
41
|
|
|
42
42
|
def extract_base_domain(url)
|
|
43
|
-
|
|
43
|
+
# ensure the URL has a scheme for URI parsing
|
|
44
|
+
normalized_url = url.match?(/^https?:\/\//i) ? url : "http://#{url}"
|
|
45
|
+
uri = URI.parse(normalized_url) rescue nil
|
|
44
46
|
return nil unless uri
|
|
45
47
|
|
|
46
|
-
host
|
|
47
|
-
host = host.
|
|
48
|
+
# extract the host (and default to parsing path if host is missing)
|
|
49
|
+
host = uri.host || (uri.path || '').split('/').first
|
|
50
|
+
return nil unless host
|
|
51
|
+
|
|
52
|
+
# strip port numbers if present
|
|
53
|
+
host = host.split(':').first.downcase
|
|
48
54
|
|
|
49
55
|
# extract the base domain (e.g., "example.com" from "sub.example.com")
|
|
50
56
|
parts = host.split('.')
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require 'thread'
|
|
4
4
|
require 'net/http'
|
|
5
|
-
require 'open-uri'
|
|
6
5
|
require 'fileutils'
|
|
7
|
-
require 'cgi'
|
|
8
6
|
require 'json'
|
|
9
|
-
require '
|
|
7
|
+
require 'pathname'
|
|
10
8
|
require 'concurrent-ruby'
|
|
11
9
|
require 'logger'
|
|
12
10
|
require 'zlib'
|
|
13
11
|
require 'stringio'
|
|
14
12
|
require 'digest'
|
|
13
|
+
require 'etc'
|
|
15
14
|
require_relative 'wayback_machine_downloader/tidy_bytes'
|
|
16
15
|
require_relative 'wayback_machine_downloader/to_regex'
|
|
17
16
|
require_relative 'wayback_machine_downloader/archive_api'
|
|
@@ -19,6 +18,13 @@ require_relative 'wayback_machine_downloader/page_requisites'
|
|
|
19
18
|
require_relative 'wayback_machine_downloader/subdom_processor'
|
|
20
19
|
require_relative 'wayback_machine_downloader/url_rewrite'
|
|
21
20
|
|
|
21
|
+
begin
|
|
22
|
+
require 'httpx'
|
|
23
|
+
HTTPX_AVAILABLE = true
|
|
24
|
+
rescue LoadError
|
|
25
|
+
HTTPX_AVAILABLE = false
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
class ConnectionPool
|
|
23
29
|
MAX_AGE = 300
|
|
24
30
|
CLEANUP_INTERVAL = 60
|
|
@@ -69,7 +75,12 @@ class ConnectionPool
|
|
|
69
75
|
def stale?(entry)
|
|
70
76
|
return true if entry.nil? || entry[:http].nil?
|
|
71
77
|
http = entry[:http]
|
|
72
|
-
|
|
78
|
+
|
|
79
|
+
if HTTPX_AVAILABLE && http.is_a?(HTTPX::Session)
|
|
80
|
+
Time.now - entry[:created_at] > MAX_AGE
|
|
81
|
+
else
|
|
82
|
+
!http.started? || (Time.now - entry[:created_at] > MAX_AGE)
|
|
83
|
+
end
|
|
73
84
|
end
|
|
74
85
|
|
|
75
86
|
def build_connection_entry
|
|
@@ -77,7 +88,12 @@ class ConnectionPool
|
|
|
77
88
|
end
|
|
78
89
|
|
|
79
90
|
def safe_finish(http)
|
|
80
|
-
|
|
91
|
+
return if http.nil?
|
|
92
|
+
if HTTPX_AVAILABLE && http.is_a?(HTTPX::Session)
|
|
93
|
+
http.close
|
|
94
|
+
else
|
|
95
|
+
http.finish if http&.started?
|
|
96
|
+
end
|
|
81
97
|
rescue StandardError
|
|
82
98
|
nil
|
|
83
99
|
end
|
|
@@ -116,14 +132,24 @@ class ConnectionPool
|
|
|
116
132
|
end
|
|
117
133
|
|
|
118
134
|
def create_connection
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
if HTTPX_AVAILABLE
|
|
136
|
+
HTTPX.with(
|
|
137
|
+
timeout: {
|
|
138
|
+
connect_timeout: DEFAULT_TIMEOUT,
|
|
139
|
+
read_timeout: DEFAULT_TIMEOUT,
|
|
140
|
+
write_timeout: DEFAULT_TIMEOUT
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
else
|
|
144
|
+
http = Net::HTTP.new("web.archive.org", 443)
|
|
145
|
+
http.use_ssl = true
|
|
146
|
+
http.read_timeout = DEFAULT_TIMEOUT
|
|
147
|
+
http.open_timeout = DEFAULT_TIMEOUT
|
|
148
|
+
http.keep_alive_timeout = 30
|
|
149
|
+
http.max_retries = MAX_RETRIES
|
|
150
|
+
http.start
|
|
151
|
+
http
|
|
152
|
+
end
|
|
127
153
|
end
|
|
128
154
|
end
|
|
129
155
|
|
|
@@ -133,7 +159,7 @@ class WaybackMachineDownloader
|
|
|
133
159
|
include SubdomainProcessor
|
|
134
160
|
include URLRewrite
|
|
135
161
|
|
|
136
|
-
VERSION = "2.4.
|
|
162
|
+
VERSION = "2.4.8"
|
|
137
163
|
DEFAULT_TIMEOUT = 30
|
|
138
164
|
MAX_RETRIES = 3
|
|
139
165
|
RETRY_DELAY = 2
|
|
@@ -146,7 +172,7 @@ class WaybackMachineDownloader
|
|
|
146
172
|
|
|
147
173
|
attr_accessor :base_url, :exact_url, :directory, :all_timestamps,
|
|
148
174
|
:from_timestamp, :to_timestamp, :only_filter, :exclude_filter,
|
|
149
|
-
:all, :maximum_pages, :threads_count, :logger, :reset, :keep, :rewrite,
|
|
175
|
+
:all, :keep_duplicates, :maximum_pages, :threads_count, :logger, :reset, :keep, :rewrite,
|
|
150
176
|
:snapshot_at, :page_requisites
|
|
151
177
|
|
|
152
178
|
def initialize params
|
|
@@ -165,8 +191,16 @@ class WaybackMachineDownloader
|
|
|
165
191
|
@only_filter = params[:only_filter]
|
|
166
192
|
@exclude_filter = params[:exclude_filter]
|
|
167
193
|
@all = params[:all]
|
|
194
|
+
@keep_duplicates = params[:keep_duplicates] || false
|
|
168
195
|
@maximum_pages = params[:maximum_pages] ? params[:maximum_pages].to_i : 100
|
|
169
|
-
|
|
196
|
+
default_threads = begin
|
|
197
|
+
[Etc.nprocessors, 4].max
|
|
198
|
+
rescue StandardError
|
|
199
|
+
4
|
|
200
|
+
end
|
|
201
|
+
threads_param = params[:threads_count] ? params[:threads_count].to_i : 0
|
|
202
|
+
threads_param = default_threads if threads_param <= 0
|
|
203
|
+
@threads_count = [threads_param, 1].max
|
|
170
204
|
@rewritten = params[:rewritten]
|
|
171
205
|
@reset = params[:reset]
|
|
172
206
|
@keep = params[:keep]
|
|
@@ -176,6 +210,7 @@ class WaybackMachineDownloader
|
|
|
176
210
|
@connection_pool = ConnectionPool.new(CONNECTION_POOL_SIZE)
|
|
177
211
|
@db_mutex = Mutex.new
|
|
178
212
|
@rewrite = params[:rewrite] || false
|
|
213
|
+
@delay = params[:delay] ? params[:delay].to_f : 0.0
|
|
179
214
|
@recursive_subdomains = params[:recursive_subdomains] || false
|
|
180
215
|
@subdomain_depth = params[:subdomain_depth] || 1
|
|
181
216
|
@snapshot_at = params[:snapshot_at] ? params[:snapshot_at].to_i : nil
|
|
@@ -183,6 +218,12 @@ class WaybackMachineDownloader
|
|
|
183
218
|
@page_requisites = params[:page_requisites] || false
|
|
184
219
|
@pending_jobs = Concurrent::AtomicFixnum.new(0)
|
|
185
220
|
|
|
221
|
+
@db_buffer = []
|
|
222
|
+
@db_buffer_mutex = Mutex.new
|
|
223
|
+
@db_last_flush = Time.now
|
|
224
|
+
@db_flush_threshold = 50 # flush to disk every 50 completed files
|
|
225
|
+
@db_flush_interval = 5.0 # or flush every 5 seconds even if count isn't met
|
|
226
|
+
|
|
186
227
|
# URL for rejecting invalid/unencoded wayback urls
|
|
187
228
|
@url_regexp = /^(([A-Za-z][A-Za-z0-9+.-]*):((\/\/(((([A-Za-z0-9._~-])|(%[ABCDEFabcdef0-9][ABCDEFabcdef0-9])|([!$&'('')'*+,;=]))+)(:([0-9]*))?)(((\/((([A-Za-z0-9._~-])|(%[ABCDEFabcdef0-9][ABCDEFabcdef0-9])|([!$&'('')'*+,;=])|:|@)*))*)))|((\/(((([A-Za-z0-9._~-])|(%[ABCDEFabcdef0-9][ABCDEFabcdef0-9])|([!$&'('')'*+,;=])|:|@)+)(\/((([A-Za-z0-9._~-])|(%[ABCDEFabcdef0-9][ABCDEFabcdef0-9])|([!$&'('')'*+,;=])|:|@)*))*)?))|((((([A-Za-z0-9._~-])|(%[ABCDEFabcdef0-9][ABCDEFabcdef0-9])|([!$&'('')'*+,;=])|:|@)+)(\/((([A-Za-z0-9._~-])|(%[ABCDEFabcdef0-9][ABCDEFabcdef0-9])|([!$&'('')'*+,;=])|:|@)*))*)))(\?((([A-Za-z0-9._~-])|(%[ABCDEFabcdef0-9][ABCDEFabcdef0-9])|([!$&'('')'*+,;=])|:|@)|\/|\?)*)?(\#((([A-Za-z0-9._~-])|(%[ABCDEFabcdef0-9][ABCDEFabcdef0-9])|([!$&'('')'*+,;=])|:|@)|\/|\?)*)?)$/
|
|
188
229
|
|
|
@@ -293,10 +334,8 @@ class WaybackMachineDownloader
|
|
|
293
334
|
|
|
294
335
|
# if snapshot_at is set, limit CDX queries to snapshots at or before that timestamp
|
|
295
336
|
original_to = @to_timestamp
|
|
296
|
-
skip_cache = false
|
|
297
337
|
if @snapshot_at
|
|
298
338
|
@to_timestamp = @snapshot_at
|
|
299
|
-
skip_cache = true
|
|
300
339
|
end
|
|
301
340
|
|
|
302
341
|
puts "Getting snapshot pages from Wayback Machine API..."
|
|
@@ -329,7 +368,7 @@ class WaybackMachineDownloader
|
|
|
329
368
|
Concurrent::Future.execute(executor: fetch_pool) do
|
|
330
369
|
result = nil
|
|
331
370
|
@connection_pool.with_connection do |connection|
|
|
332
|
-
result = get_raw_list_from_api(
|
|
371
|
+
result = get_raw_list_from_api(@base_url, page, connection)
|
|
333
372
|
end
|
|
334
373
|
result ||= []
|
|
335
374
|
[page, result]
|
|
@@ -382,10 +421,8 @@ class WaybackMachineDownloader
|
|
|
382
421
|
# save the fetched list to the cache file
|
|
383
422
|
begin
|
|
384
423
|
FileUtils.mkdir_p(File.dirname(cdx_path))
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
puts "Saved snapshot list to #{cdx_path}"
|
|
388
|
-
end
|
|
424
|
+
File.write(cdx_path, JSON.pretty_generate(snapshot_list_to_consider.to_a)) # Convert Concurrent::Array back to Array for JSON
|
|
425
|
+
puts "Saved snapshot list to #{cdx_path}"
|
|
389
426
|
rescue => e
|
|
390
427
|
puts "Error saving snapshot cache to #{cdx_path}: #{e.message}"
|
|
391
428
|
ensure
|
|
@@ -532,7 +569,16 @@ class WaybackMachineDownloader
|
|
|
532
569
|
if File.exist?(db_path) && !@reset
|
|
533
570
|
puts "Loading list of already downloaded files from #{db_path}"
|
|
534
571
|
begin
|
|
535
|
-
File.foreach(db_path)
|
|
572
|
+
File.foreach(db_path) do |line|
|
|
573
|
+
id = line.strip
|
|
574
|
+
# only trust DB entries that actually exist on disk; this helps when resuming
|
|
575
|
+
path = local_path_for_file_id(id)
|
|
576
|
+
if path && File.exist?(path)
|
|
577
|
+
downloaded_ids.add(id)
|
|
578
|
+
else
|
|
579
|
+
puts "Found DB entry but file missing, will requeue: #{id}" if @logger && @logger.level == Logger::DEBUG
|
|
580
|
+
end
|
|
581
|
+
end
|
|
536
582
|
rescue => e
|
|
537
583
|
puts "Error reading downloaded files list #{db_path}: #{e.message}. Assuming no files downloaded."
|
|
538
584
|
downloaded_ids.clear
|
|
@@ -542,12 +588,40 @@ class WaybackMachineDownloader
|
|
|
542
588
|
end
|
|
543
589
|
|
|
544
590
|
def append_to_db(file_id)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
591
|
+
flush_needed = false
|
|
592
|
+
|
|
593
|
+
@db_buffer_mutex.synchronize do
|
|
594
|
+
@db_buffer << file_id
|
|
595
|
+
if @db_buffer.size >= @db_flush_threshold || (Time.now - @db_last_flush) >= @db_flush_interval
|
|
596
|
+
flush_needed = true
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
flush_db if flush_needed
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def flush_db
|
|
604
|
+
lines_to_write = nil
|
|
605
|
+
|
|
606
|
+
@db_buffer_mutex.synchronize do
|
|
607
|
+
return if @db_buffer.empty?
|
|
608
|
+
lines_to_write = @db_buffer.dup
|
|
609
|
+
@db_buffer.clear
|
|
610
|
+
@db_last_flush = Time.now
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
return if lines_to_write.nil? || lines_to_write.empty?
|
|
614
|
+
|
|
615
|
+
begin
|
|
616
|
+
FileUtils.mkdir_p(File.dirname(db_path))
|
|
617
|
+
File.open(db_path, 'a') do |f|
|
|
618
|
+
lines_to_write.each { |id| f.puts(id) }
|
|
619
|
+
end
|
|
620
|
+
rescue StandardError => e
|
|
621
|
+
@logger.error("Failed to write batch to #{db_path}: #{e.message}")
|
|
622
|
+
|
|
623
|
+
@db_buffer_mutex.synchronize do
|
|
624
|
+
@db_buffer.unshift(*lines_to_write)
|
|
551
625
|
end
|
|
552
626
|
end
|
|
553
627
|
end
|
|
@@ -584,9 +658,18 @@ class WaybackMachineDownloader
|
|
|
584
658
|
end
|
|
585
659
|
end
|
|
586
660
|
end
|
|
587
|
-
|
|
661
|
+
|
|
662
|
+
@@engine_printed = false
|
|
663
|
+
|
|
588
664
|
def download_files
|
|
589
665
|
start_time = Time.now
|
|
666
|
+
|
|
667
|
+
unless @@engine_printed
|
|
668
|
+
engine_name = HTTPX_AVAILABLE ? "HTTPX" : "Net::HTTP"
|
|
669
|
+
puts "Connection engine used: #{color(engine_name, :green)}"
|
|
670
|
+
@@engine_printed = true
|
|
671
|
+
end
|
|
672
|
+
|
|
590
673
|
puts "Downloading #{@base_url} to #{backup_path} from Wayback Machine archives."
|
|
591
674
|
|
|
592
675
|
FileUtils.mkdir_p(backup_path)
|
|
@@ -605,7 +688,7 @@ class WaybackMachineDownloader
|
|
|
605
688
|
|
|
606
689
|
# Load IDs of already downloaded files
|
|
607
690
|
downloaded_ids = load_downloaded_ids
|
|
608
|
-
|
|
691
|
+
|
|
609
692
|
# We use a thread-safe Set to track what we have queued/downloaded in this session
|
|
610
693
|
# to avoid infinite loops with page requisites
|
|
611
694
|
@session_downloaded_ids = Concurrent::Set.new
|
|
@@ -621,7 +704,7 @@ class WaybackMachineDownloader
|
|
|
621
704
|
if skipped_count > 0
|
|
622
705
|
puts "Found #{skipped_count} previously downloaded files, skipping them."
|
|
623
706
|
end
|
|
624
|
-
|
|
707
|
+
|
|
625
708
|
if remaining_count == 0 && !@page_requisites
|
|
626
709
|
puts "All matching files have already been downloaded."
|
|
627
710
|
cleanup
|
|
@@ -687,14 +770,34 @@ class WaybackMachineDownloader
|
|
|
687
770
|
def process_single_file(file_remote_info)
|
|
688
771
|
download_success = false
|
|
689
772
|
downloaded_path = nil
|
|
690
|
-
|
|
773
|
+
|
|
774
|
+
# if delay was set by the user
|
|
775
|
+
sleep(@delay) if @delay > 0
|
|
776
|
+
|
|
777
|
+
# fast-path for resumed runs: if file already exists locally, avoid HTTP work entirely
|
|
778
|
+
existing_path = local_path_for_file_id(file_remote_info[:file_id])
|
|
779
|
+
if existing_path && File.exist?(existing_path)
|
|
780
|
+
result_message = "#{color("[EXISTS]", :cyan)} #{file_remote_info[:file_url]} (#{@processed_file_count + 1}/#{@total_to_download})"
|
|
781
|
+
@download_mutex.synchronize do
|
|
782
|
+
@processed_file_count += 1 if @processed_file_count < @total_to_download
|
|
783
|
+
puts result_message
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
append_to_db(file_remote_info[:file_id])
|
|
787
|
+
|
|
788
|
+
if @page_requisites && File.extname(existing_path) =~ /\.(html?|php|asp|aspx|jsp)$/i
|
|
789
|
+
process_page_requisites(existing_path, file_remote_info)
|
|
790
|
+
end
|
|
791
|
+
return
|
|
792
|
+
end
|
|
793
|
+
|
|
691
794
|
@connection_pool.with_connection do |connection|
|
|
692
795
|
result_message, downloaded_path = download_file(file_remote_info, connection)
|
|
693
|
-
|
|
796
|
+
|
|
694
797
|
if downloaded_path && File.exist?(downloaded_path)
|
|
695
798
|
download_success = true
|
|
696
799
|
end
|
|
697
|
-
|
|
800
|
+
|
|
698
801
|
@download_mutex.synchronize do
|
|
699
802
|
@processed_file_count += 1 if @processed_file_count < @total_to_download
|
|
700
803
|
# only print if it's a "User" file or a requisite we found
|
|
@@ -704,7 +807,7 @@ class WaybackMachineDownloader
|
|
|
704
807
|
|
|
705
808
|
if download_success
|
|
706
809
|
append_to_db(file_remote_info[:file_id])
|
|
707
|
-
|
|
810
|
+
|
|
708
811
|
if @page_requisites && downloaded_path && File.extname(downloaded_path) =~ /\.(html?|php|asp|aspx|jsp)$/i
|
|
709
812
|
process_page_requisites(downloaded_path, file_remote_info)
|
|
710
813
|
end
|
|
@@ -712,7 +815,7 @@ class WaybackMachineDownloader
|
|
|
712
815
|
rescue => e
|
|
713
816
|
@logger.error("Error processing file #{file_remote_info[:file_url]}: #{e.message}")
|
|
714
817
|
end
|
|
715
|
-
|
|
818
|
+
|
|
716
819
|
def process_page_requisites(file_path, parent_remote_info)
|
|
717
820
|
return unless File.exist?(file_path)
|
|
718
821
|
|
|
@@ -724,7 +827,7 @@ class WaybackMachineDownloader
|
|
|
724
827
|
# prepare base URI for resolving relative paths
|
|
725
828
|
parent_raw = parent_remote_info[:file_url]
|
|
726
829
|
parent_raw = "http://#{parent_raw}" unless parent_raw.match?(/^https?:\/\//)
|
|
727
|
-
|
|
830
|
+
|
|
728
831
|
begin
|
|
729
832
|
base_uri = URI(parent_raw)
|
|
730
833
|
# calculate the "root" host of the site we are downloading to compare later
|
|
@@ -758,7 +861,7 @@ class WaybackMachineDownloader
|
|
|
758
861
|
path = resolved_uri.path
|
|
759
862
|
ext = File.extname(path).downcase
|
|
760
863
|
if ext.empty? || ['.html', '.htm', '.php', '.asp', '.aspx'].include?(ext)
|
|
761
|
-
next
|
|
864
|
+
next
|
|
762
865
|
end
|
|
763
866
|
|
|
764
867
|
# construct the original URL to query the Wayback API
|
|
@@ -829,7 +932,7 @@ class WaybackMachineDownloader
|
|
|
829
932
|
rescue Errno::EEXIST, Errno::ENOTDIR => e
|
|
830
933
|
file_already_existing = nil
|
|
831
934
|
check_path = dir_path
|
|
832
|
-
|
|
935
|
+
|
|
833
936
|
# walk up the path to find the specific file that is blocking directory creation
|
|
834
937
|
while check_path != "." && check_path != "/"
|
|
835
938
|
if File.exist?(check_path) && !File.directory?(check_path)
|
|
@@ -844,11 +947,11 @@ class WaybackMachineDownloader
|
|
|
844
947
|
if file_already_existing
|
|
845
948
|
file_already_existing_temporary = file_already_existing + '.temp'
|
|
846
949
|
file_already_existing_permanent = file_already_existing + '/index.html'
|
|
847
|
-
|
|
950
|
+
|
|
848
951
|
FileUtils::mv file_already_existing, file_already_existing_temporary
|
|
849
952
|
FileUtils::mkdir_p file_already_existing
|
|
850
953
|
FileUtils::mv file_already_existing_temporary, file_already_existing_permanent
|
|
851
|
-
|
|
954
|
+
|
|
852
955
|
puts "#{file_already_existing} -> #{file_already_existing_permanent}"
|
|
853
956
|
# retry the directory creation now that the path is clear
|
|
854
957
|
structure_dir_path dir_path
|
|
@@ -861,12 +964,12 @@ class WaybackMachineDownloader
|
|
|
861
964
|
def rewrite_local_files
|
|
862
965
|
puts "Scanning #{backup_path} for files to rewrite..."
|
|
863
966
|
files = Dir.glob(File.join(backup_path, "**/*.{html,htm,css,js,php,asp,aspx,jsp}"))
|
|
864
|
-
|
|
967
|
+
|
|
865
968
|
puts "Found #{files.size} files. Rewriting links for local browsing..."
|
|
866
|
-
|
|
969
|
+
|
|
867
970
|
pool = Concurrent::FixedThreadPool.new(@threads_count)
|
|
868
971
|
progress = Concurrent::AtomicFixnum.new(0)
|
|
869
|
-
|
|
972
|
+
|
|
870
973
|
files.each do |file_path|
|
|
871
974
|
pool.post do
|
|
872
975
|
rewrite_urls_to_relative(file_path)
|
|
@@ -874,7 +977,7 @@ class WaybackMachineDownloader
|
|
|
874
977
|
print "\rProgress: #{current}/#{files.size}" if current % 100 == 0
|
|
875
978
|
end
|
|
876
979
|
end
|
|
877
|
-
|
|
980
|
+
|
|
878
981
|
pool.shutdown
|
|
879
982
|
pool.wait_for_termination
|
|
880
983
|
puts "\nFinished rewriting all files."
|
|
@@ -882,39 +985,58 @@ class WaybackMachineDownloader
|
|
|
882
985
|
|
|
883
986
|
def rewrite_urls_to_relative(file_path)
|
|
884
987
|
return unless File.exist?(file_path)
|
|
885
|
-
|
|
988
|
+
|
|
886
989
|
file_ext = File.extname(file_path).downcase
|
|
887
|
-
|
|
990
|
+
|
|
888
991
|
begin
|
|
889
992
|
content = File.binread(file_path)
|
|
890
993
|
|
|
891
994
|
# detect encoding for HTML files
|
|
892
995
|
if file_ext == '.html' || file_ext == '.htm' || file_ext == '.php' || file_ext == '.asp'
|
|
893
|
-
|
|
894
|
-
|
|
996
|
+
encoding_match = content.match(/<meta.*?charset=["'\s]?([^"'\s>;]+)/i)
|
|
997
|
+
encoding_name = encoding_match ? encoding_match.captures.first : 'UTF-8'
|
|
998
|
+
|
|
999
|
+
begin
|
|
1000
|
+
encoding = Encoding.find(encoding_name)
|
|
1001
|
+
rescue ArgumentError
|
|
1002
|
+
encoding = Encoding::UTF_8
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
content.force_encoding(encoding)
|
|
895
1006
|
else
|
|
896
1007
|
content.force_encoding('UTF-8')
|
|
897
1008
|
end
|
|
898
1009
|
|
|
1010
|
+
# convert the content to valid UTF-8
|
|
1011
|
+
if !content.valid_encoding? || content.encoding != Encoding::UTF_8
|
|
1012
|
+
begin
|
|
1013
|
+
content = content.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: '')
|
|
1014
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError, ArgumentError
|
|
1015
|
+
content = content.tidy_bytes
|
|
1016
|
+
end
|
|
1017
|
+
end
|
|
1018
|
+
|
|
899
1019
|
# URLs in HTML attributes
|
|
900
1020
|
content = rewrite_html_attr_urls(content)
|
|
901
|
-
|
|
1021
|
+
|
|
902
1022
|
# URLs in CSS
|
|
903
1023
|
content = rewrite_css_urls(content)
|
|
904
|
-
|
|
1024
|
+
|
|
905
1025
|
# URLs in JavaScript
|
|
906
1026
|
content = rewrite_js_urls(content)
|
|
907
|
-
|
|
908
|
-
|
|
1027
|
+
|
|
1028
|
+
root_prefix = site_root_relative_prefix(file_path)
|
|
1029
|
+
|
|
1030
|
+
# rewrite root-absolute links to paths relative to the downloaded site root
|
|
909
1031
|
content.gsub!(/(\s(?:href|src|action|data-src|data-url)=["'])\/([^"'\/][^"']*)(["'])/i) do
|
|
910
1032
|
prefix, path, suffix = $1, $2, $3
|
|
911
|
-
"#{prefix}
|
|
1033
|
+
"#{prefix}#{root_prefix}#{path}#{suffix}"
|
|
912
1034
|
end
|
|
913
|
-
|
|
914
|
-
#
|
|
1035
|
+
|
|
1036
|
+
# apply the same root-relative conversion to CSS url(...) references
|
|
915
1037
|
content.gsub!(/url\(\s*["']?\/([^"'\)\/][^"'\)]*?)["']?\s*\)/i) do
|
|
916
1038
|
path = $1
|
|
917
|
-
"url(\"
|
|
1039
|
+
"url(\"#{root_prefix}#{path}\")"
|
|
918
1040
|
end
|
|
919
1041
|
|
|
920
1042
|
# save the modified content back to the file
|
|
@@ -922,7 +1044,27 @@ class WaybackMachineDownloader
|
|
|
922
1044
|
puts "Rewrote URLs in #{file_path} to be relative."
|
|
923
1045
|
rescue Errno::ENOENT => e
|
|
924
1046
|
@logger.warn("Error reading file #{file_path}: #{e.message}")
|
|
1047
|
+
rescue StandardError => e
|
|
1048
|
+
@logger.error("Failed to rewrite URLs in #{file_path}: #{e.message}")
|
|
1049
|
+
end
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
def site_root_relative_prefix(file_path)
|
|
1053
|
+
file_dir = File.dirname(File.expand_path(file_path))
|
|
1054
|
+
root_dir = File.expand_path(backup_path)
|
|
1055
|
+
|
|
1056
|
+
begin
|
|
1057
|
+
relative_dir = Pathname.new(file_dir).relative_path_from(Pathname.new(root_dir)).to_s
|
|
1058
|
+
rescue ArgumentError
|
|
1059
|
+
return './'
|
|
925
1060
|
end
|
|
1061
|
+
|
|
1062
|
+
return './' if relative_dir == '.' || relative_dir.empty?
|
|
1063
|
+
|
|
1064
|
+
depth = relative_dir.split(/[\\\/]+/).reject(&:empty?).length
|
|
1065
|
+
return './' if depth <= 0
|
|
1066
|
+
|
|
1067
|
+
'../' * depth
|
|
926
1068
|
end
|
|
927
1069
|
|
|
928
1070
|
def download_file (file_remote_info, http)
|
|
@@ -930,7 +1072,7 @@ class WaybackMachineDownloader
|
|
|
930
1072
|
file_url = file_remote_info[:file_url].encode(current_encoding)
|
|
931
1073
|
file_id = file_remote_info[:file_id]
|
|
932
1074
|
file_timestamp = file_remote_info[:timestamp]
|
|
933
|
-
|
|
1075
|
+
|
|
934
1076
|
# sanitize file_id to ensure it is a valid path component
|
|
935
1077
|
raw_path_elements = file_id.split('/')
|
|
936
1078
|
|
|
@@ -944,7 +1086,7 @@ class WaybackMachineDownloader
|
|
|
944
1086
|
element
|
|
945
1087
|
end
|
|
946
1088
|
end
|
|
947
|
-
|
|
1089
|
+
|
|
948
1090
|
current_backup_path = backup_path
|
|
949
1091
|
|
|
950
1092
|
if file_id == ""
|
|
@@ -994,6 +1136,27 @@ class WaybackMachineDownloader
|
|
|
994
1136
|
end
|
|
995
1137
|
end
|
|
996
1138
|
|
|
1139
|
+
# derive the local filesystem path for a sanitized `file_id` stored in the DB
|
|
1140
|
+
def local_path_for_file_id(file_id)
|
|
1141
|
+
return nil if file_id.nil?
|
|
1142
|
+
current_backup_path = backup_path
|
|
1143
|
+
|
|
1144
|
+
# file_id coming from DB is expected to already be sanitized
|
|
1145
|
+
raw_path_elements = file_id.split('/')
|
|
1146
|
+
|
|
1147
|
+
if file_id == ""
|
|
1148
|
+
dir_path = current_backup_path
|
|
1149
|
+
return File.join(dir_path, 'index.html')
|
|
1150
|
+
elsif file_id[-1] == '/' || (raw_path_elements.last && !raw_path_elements.last.include?('.'))
|
|
1151
|
+
dir_path = File.join(current_backup_path, *raw_path_elements)
|
|
1152
|
+
return File.join(dir_path, 'index.html')
|
|
1153
|
+
else
|
|
1154
|
+
filename = raw_path_elements.pop
|
|
1155
|
+
dir_path = File.join(current_backup_path, *raw_path_elements)
|
|
1156
|
+
return File.join(dir_path, filename)
|
|
1157
|
+
end
|
|
1158
|
+
end
|
|
1159
|
+
|
|
997
1160
|
def color(text, color_code)
|
|
998
1161
|
return text if Gem.win_platform? && !ENV['ENABLE_ANSI']
|
|
999
1162
|
codes = { red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37 }
|
|
@@ -1038,7 +1201,7 @@ class WaybackMachineDownloader
|
|
|
1038
1201
|
end
|
|
1039
1202
|
logger
|
|
1040
1203
|
end
|
|
1041
|
-
|
|
1204
|
+
|
|
1042
1205
|
# safely sanitize a file id (or id+timestamp)
|
|
1043
1206
|
def sanitize_and_prepare_id(raw, file_url)
|
|
1044
1207
|
return nil if raw.nil?
|
|
@@ -1111,6 +1274,46 @@ class WaybackMachineDownloader
|
|
|
1111
1274
|
end
|
|
1112
1275
|
end
|
|
1113
1276
|
|
|
1277
|
+
def build_wayback_url(source_url, file_timestamp)
|
|
1278
|
+
source = source_url.to_s
|
|
1279
|
+
return source if wayback_archive_url?(source)
|
|
1280
|
+
|
|
1281
|
+
if source.start_with?('/web/')
|
|
1282
|
+
return "https://web.archive.org#{source}"
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
if @rewritten
|
|
1286
|
+
"https://web.archive.org/web/#{file_timestamp}/#{source}"
|
|
1287
|
+
else
|
|
1288
|
+
"https://web.archive.org/web/#{file_timestamp}id_/#{source}"
|
|
1289
|
+
end
|
|
1290
|
+
end
|
|
1291
|
+
|
|
1292
|
+
def wayback_archive_url?(url)
|
|
1293
|
+
url.to_s.match?(%r{\Ahttps?://web\.archive\.org/web/})
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1296
|
+
def extract_original_url(url)
|
|
1297
|
+
match = url.to_s.match(%r{\Ahttps?://web\.archive\.org/web/\d{1,14}(?:[a-z_]*)/(https?://.+)\z})
|
|
1298
|
+
match && match[1]
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
def resolve_redirect_source(current_source_url, location)
|
|
1302
|
+
return nil if location.nil? || location.empty?
|
|
1303
|
+
|
|
1304
|
+
location = location.to_s
|
|
1305
|
+
return location if wayback_archive_url?(location)
|
|
1306
|
+
|
|
1307
|
+
if location.start_with?('/web/')
|
|
1308
|
+
return "https://web.archive.org#{location}"
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
base_url = extract_original_url(current_source_url) || current_source_url.to_s
|
|
1312
|
+
URI.join(base_url, location).to_s
|
|
1313
|
+
rescue URI::InvalidURIError
|
|
1314
|
+
location
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1114
1317
|
# wrap URL in parentheses if it contains characters that commonly break unquoted
|
|
1115
1318
|
# Windows CMD usage (e.g., &). This is only for display; user still must quote
|
|
1116
1319
|
# when invoking manually.
|
|
@@ -1122,11 +1325,7 @@ class WaybackMachineDownloader
|
|
|
1122
1325
|
def download_with_retry(file_path, file_url, file_timestamp, connection, redirect_count = 0)
|
|
1123
1326
|
retries = 0
|
|
1124
1327
|
begin
|
|
1125
|
-
wayback_url =
|
|
1126
|
-
"https://web.archive.org/web/#{file_timestamp}/#{file_url}"
|
|
1127
|
-
else
|
|
1128
|
-
"https://web.archive.org/web/#{file_timestamp}id_/#{file_url}"
|
|
1129
|
-
end
|
|
1328
|
+
wayback_url = build_wayback_url(file_url, file_timestamp)
|
|
1130
1329
|
|
|
1131
1330
|
# Escape characters that are not valid in URI()
|
|
1132
1331
|
wayback_url = wayback_url.gsub(' ', '%20').gsub('[', '%5B').gsub(']', '%5D')
|
|
@@ -1144,64 +1343,85 @@ class WaybackMachineDownloader
|
|
|
1144
1343
|
return :skipped_not_found
|
|
1145
1344
|
end
|
|
1146
1345
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
request["User-Agent"] = "WaybackMachineDownloader/#{VERSION}"
|
|
1150
|
-
request["Accept-Encoding"] = "gzip, deflate"
|
|
1346
|
+
if HTTPX_AVAILABLE && connection.is_a?(HTTPX::Session)
|
|
1347
|
+
response = connection.get(wayback_url)
|
|
1151
1348
|
|
|
1152
|
-
|
|
1349
|
+
raise response.error if response.is_a?(HTTPX::ErrorResponse)
|
|
1153
1350
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
if response
|
|
1158
|
-
|
|
1159
|
-
gz = Zlib::GzipReader.new(StringIO.new(body))
|
|
1160
|
-
decompressed_body = gz.read
|
|
1161
|
-
gz.close
|
|
1162
|
-
file.write(decompressed_body)
|
|
1163
|
-
rescue Zlib::GzipFile::Error => e
|
|
1164
|
-
@logger.warn("Failure decompressing gzip file #{file_url}: #{e.message}. Writing raw body.")
|
|
1165
|
-
file.write(body)
|
|
1166
|
-
end
|
|
1351
|
+
code = response.status
|
|
1352
|
+
|
|
1353
|
+
save_response_body = lambda do
|
|
1354
|
+
if response.respond_to?(:copy_to)
|
|
1355
|
+
response.copy_to(file_path)
|
|
1167
1356
|
else
|
|
1168
|
-
|
|
1357
|
+
response.body.copy_to(file_path)
|
|
1358
|
+
end
|
|
1359
|
+
end
|
|
1360
|
+
else
|
|
1361
|
+
request = Net::HTTP::Get.new(URI(wayback_url))
|
|
1362
|
+
request["Connection"] = "keep-alive"
|
|
1363
|
+
request["User-Agent"] = "WaybackMachineDownloader/#{VERSION}"
|
|
1364
|
+
request["Accept-Encoding"] = "gzip, deflate"
|
|
1365
|
+
|
|
1366
|
+
response = connection.request(request)
|
|
1367
|
+
code = response.code.to_i
|
|
1368
|
+
|
|
1369
|
+
save_response_body = lambda do
|
|
1370
|
+
File.open(file_path, "wb") do |file|
|
|
1371
|
+
body = response.body
|
|
1372
|
+
if response['content-encoding'] == 'gzip' && body && !body.empty?
|
|
1373
|
+
begin
|
|
1374
|
+
gz = Zlib::GzipReader.new(StringIO.new(body))
|
|
1375
|
+
decompressed_body = gz.read
|
|
1376
|
+
gz.close
|
|
1377
|
+
file.write(decompressed_body)
|
|
1378
|
+
rescue Zlib::GzipFile::Error => e
|
|
1379
|
+
@logger.warn("Failure decompressing gzip file #{file_url}: #{e.message}. Writing raw body.")
|
|
1380
|
+
file.write(body)
|
|
1381
|
+
end
|
|
1382
|
+
else
|
|
1383
|
+
file.write(body) if body
|
|
1384
|
+
end
|
|
1169
1385
|
end
|
|
1170
1386
|
end
|
|
1171
1387
|
end
|
|
1172
1388
|
|
|
1173
1389
|
if @all
|
|
1174
|
-
case
|
|
1175
|
-
when
|
|
1390
|
+
case code
|
|
1391
|
+
when 200..599
|
|
1176
1392
|
save_response_body.call
|
|
1177
|
-
if
|
|
1178
|
-
@logger.info("Saved redirect page for #{file_url} (status #{
|
|
1179
|
-
elsif
|
|
1180
|
-
@logger.info("Saved error page for #{file_url} (status #{
|
|
1393
|
+
if (300..399).cover?(code)
|
|
1394
|
+
@logger.info("Saved redirect page for #{file_url} (status #{code}).")
|
|
1395
|
+
elsif (400..599).cover?(code)
|
|
1396
|
+
@logger.info("Saved error page for #{file_url} (status #{code}).")
|
|
1181
1397
|
end
|
|
1182
1398
|
return :saved
|
|
1183
1399
|
else
|
|
1184
1400
|
# for any other response type when --all is true, treat as an error to be retried or failed
|
|
1185
|
-
raise "Unhandled HTTP response: #{
|
|
1401
|
+
raise "Unhandled HTTP response: #{code}"
|
|
1186
1402
|
end
|
|
1187
1403
|
else # not @all (our default behavior)
|
|
1188
|
-
|
|
1189
|
-
when Net::HTTPSuccess
|
|
1404
|
+
if (200..299).cover?(code)
|
|
1190
1405
|
save_response_body.call
|
|
1191
1406
|
return :saved
|
|
1192
|
-
|
|
1407
|
+
elsif (300..399).cover?(code)
|
|
1193
1408
|
raise "Too many redirects for #{file_url}" if redirect_count >= 5
|
|
1194
|
-
location =
|
|
1409
|
+
location = if HTTPX_AVAILABLE && connection.is_a?(HTTPX::Session)
|
|
1410
|
+
response.headers['location']
|
|
1411
|
+
else
|
|
1412
|
+
response['location']
|
|
1413
|
+
end
|
|
1195
1414
|
@logger.warn("Redirect found for #{file_url} -> #{location}")
|
|
1196
|
-
|
|
1197
|
-
|
|
1415
|
+
redirected_source = resolve_redirect_source(file_url, location)
|
|
1416
|
+
return download_with_retry(file_path, redirected_source, file_timestamp, connection, redirect_count + 1)
|
|
1417
|
+
elsif code == 429
|
|
1198
1418
|
sleep(RATE_LIMIT * 2)
|
|
1199
1419
|
raise "Rate limited, retrying..."
|
|
1200
|
-
|
|
1420
|
+
elsif code == 404
|
|
1201
1421
|
@logger.warn("File not found, skipping: #{file_url}")
|
|
1202
1422
|
return :skipped_not_found
|
|
1203
1423
|
else
|
|
1204
|
-
raise "HTTP Error: #{
|
|
1424
|
+
raise "HTTP Error: #{code}"
|
|
1205
1425
|
end
|
|
1206
1426
|
end
|
|
1207
1427
|
|
|
@@ -1219,6 +1439,7 @@ class WaybackMachineDownloader
|
|
|
1219
1439
|
end
|
|
1220
1440
|
|
|
1221
1441
|
def cleanup
|
|
1442
|
+
flush_db
|
|
1222
1443
|
@connection_pool.shutdown
|
|
1223
1444
|
|
|
1224
1445
|
if @failed_downloads.any?
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wayback_machine_downloader_straw
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.4.
|
|
4
|
+
version: 2.4.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- strawberrymaster
|
|
@@ -18,7 +18,7 @@ dependencies:
|
|
|
18
18
|
version: '1.3'
|
|
19
19
|
- - ">="
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
|
-
version: 1.3.
|
|
21
|
+
version: 1.3.6
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -28,7 +28,7 @@ dependencies:
|
|
|
28
28
|
version: '1.3'
|
|
29
29
|
- - ">="
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: 1.3.
|
|
31
|
+
version: 1.3.6
|
|
32
32
|
- !ruby/object:Gem::Dependency
|
|
33
33
|
name: rake
|
|
34
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -94,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '0'
|
|
96
96
|
requirements: []
|
|
97
|
-
rubygems_version: 4.0.
|
|
97
|
+
rubygems_version: 4.0.10
|
|
98
98
|
specification_version: 4
|
|
99
99
|
summary: Download an entire website from the Wayback Machine.
|
|
100
100
|
test_files: []
|