wayback_machine_downloader_straw 2.4.7 → 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
|
|
@@ -84,6 +84,10 @@ option_parser = OptionParser.new do |opts|
|
|
|
84
84
|
options[:keep] = true
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
opts.on("--delay SECONDS", Float, "Delay between downloads in seconds (default: 0)") do |t|
|
|
88
|
+
options[:delay] = t
|
|
89
|
+
end
|
|
90
|
+
|
|
87
91
|
opts.on("--rt", "--retry N", Integer, "Maximum number of retries for failed downloads (default: 3)") do |t|
|
|
88
92
|
options[:max_retries] = t
|
|
89
93
|
end
|
|
@@ -8,6 +8,7 @@ module ArchiveAPI
|
|
|
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
10
|
# But don't do this when exact_url flag is set, and never append twice
|
|
11
|
+
match_type = nil
|
|
11
12
|
if url && !@exact_url
|
|
12
13
|
normalized_url = url.to_s
|
|
13
14
|
has_wildcard = normalized_url.include?('*')
|
|
@@ -18,32 +19,38 @@ module ArchiveAPI
|
|
|
18
19
|
has_path = host_and_rest.include?('/')
|
|
19
20
|
|
|
20
21
|
unless has_wildcard || has_path
|
|
21
|
-
|
|
22
|
+
match_type = "prefix"
|
|
22
23
|
end
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
request_url = URI("https://web.archive.org/cdx/search/cdx")
|
|
26
27
|
params = [["output", "json"], ["url", url]] + parameters_for_api(page_index)
|
|
28
|
+
params << ["matchType", match_type] if match_type
|
|
27
29
|
request_url.query = URI.encode_www_form(params)
|
|
28
30
|
|
|
29
31
|
retries = 0
|
|
30
32
|
max_retries = (@max_retries || 3)
|
|
31
|
-
|
|
33
|
+
base_delay = WaybackMachineDownloader::RETRY_DELAY rescue 2
|
|
32
34
|
|
|
33
35
|
begin
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
request["Accept-Encoding"] = "gzip"
|
|
38
|
-
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)
|
|
39
39
|
|
|
40
|
-
|
|
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
|
|
41
53
|
when 200
|
|
42
|
-
body = if response['content-encoding'] == 'gzip'
|
|
43
|
-
Zlib::GzipReader.new(StringIO.new(response.body)).read
|
|
44
|
-
else
|
|
45
|
-
response.body.to_s.strip
|
|
46
|
-
end
|
|
47
54
|
return [] if body.empty?
|
|
48
55
|
begin
|
|
49
56
|
json = JSON.parse(body)
|
|
@@ -54,16 +61,22 @@ module ArchiveAPI
|
|
|
54
61
|
raise "Malformed JSON response: #{e.message}"
|
|
55
62
|
end
|
|
56
63
|
when 429, 500, 502, 503, 504
|
|
57
|
-
raise "Server error #{
|
|
64
|
+
raise "Server error #{code}: #{response.respond_to?(:message) ? response.message : ''}"
|
|
58
65
|
else
|
|
59
|
-
warn "Unexpected API response #{
|
|
66
|
+
warn "Unexpected API response #{code} for #{url}"
|
|
60
67
|
[]
|
|
61
68
|
end
|
|
62
69
|
rescue Net::ReadTimeout, Net::OpenTimeout, StandardError => e
|
|
63
70
|
if retries < max_retries
|
|
64
71
|
retries += 1
|
|
65
|
-
|
|
66
|
-
|
|
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)
|
|
67
80
|
retry
|
|
68
81
|
else
|
|
69
82
|
warn "Giving up on Wayback CDX API for #{url} after #{max_retries} attempts. (Last error: #{e.message})"
|
|
@@ -82,4 +95,19 @@ module ArchiveAPI
|
|
|
82
95
|
parameters
|
|
83
96
|
end
|
|
84
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
|
|
85
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('.')
|
|
@@ -4,11 +4,13 @@ require 'thread'
|
|
|
4
4
|
require 'net/http'
|
|
5
5
|
require 'fileutils'
|
|
6
6
|
require 'json'
|
|
7
|
+
require 'pathname'
|
|
7
8
|
require 'concurrent-ruby'
|
|
8
9
|
require 'logger'
|
|
9
10
|
require 'zlib'
|
|
10
11
|
require 'stringio'
|
|
11
12
|
require 'digest'
|
|
13
|
+
require 'etc'
|
|
12
14
|
require_relative 'wayback_machine_downloader/tidy_bytes'
|
|
13
15
|
require_relative 'wayback_machine_downloader/to_regex'
|
|
14
16
|
require_relative 'wayback_machine_downloader/archive_api'
|
|
@@ -16,6 +18,13 @@ require_relative 'wayback_machine_downloader/page_requisites'
|
|
|
16
18
|
require_relative 'wayback_machine_downloader/subdom_processor'
|
|
17
19
|
require_relative 'wayback_machine_downloader/url_rewrite'
|
|
18
20
|
|
|
21
|
+
begin
|
|
22
|
+
require 'httpx'
|
|
23
|
+
HTTPX_AVAILABLE = true
|
|
24
|
+
rescue LoadError
|
|
25
|
+
HTTPX_AVAILABLE = false
|
|
26
|
+
end
|
|
27
|
+
|
|
19
28
|
class ConnectionPool
|
|
20
29
|
MAX_AGE = 300
|
|
21
30
|
CLEANUP_INTERVAL = 60
|
|
@@ -66,7 +75,12 @@ class ConnectionPool
|
|
|
66
75
|
def stale?(entry)
|
|
67
76
|
return true if entry.nil? || entry[:http].nil?
|
|
68
77
|
http = entry[:http]
|
|
69
|
-
|
|
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
|
|
70
84
|
end
|
|
71
85
|
|
|
72
86
|
def build_connection_entry
|
|
@@ -74,7 +88,12 @@ class ConnectionPool
|
|
|
74
88
|
end
|
|
75
89
|
|
|
76
90
|
def safe_finish(http)
|
|
77
|
-
|
|
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
|
|
78
97
|
rescue StandardError
|
|
79
98
|
nil
|
|
80
99
|
end
|
|
@@ -113,14 +132,24 @@ class ConnectionPool
|
|
|
113
132
|
end
|
|
114
133
|
|
|
115
134
|
def create_connection
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
124
153
|
end
|
|
125
154
|
end
|
|
126
155
|
|
|
@@ -130,7 +159,7 @@ class WaybackMachineDownloader
|
|
|
130
159
|
include SubdomainProcessor
|
|
131
160
|
include URLRewrite
|
|
132
161
|
|
|
133
|
-
VERSION = "2.4.
|
|
162
|
+
VERSION = "2.4.8"
|
|
134
163
|
DEFAULT_TIMEOUT = 30
|
|
135
164
|
MAX_RETRIES = 3
|
|
136
165
|
RETRY_DELAY = 2
|
|
@@ -164,7 +193,14 @@ class WaybackMachineDownloader
|
|
|
164
193
|
@all = params[:all]
|
|
165
194
|
@keep_duplicates = params[:keep_duplicates] || false
|
|
166
195
|
@maximum_pages = params[:maximum_pages] ? params[:maximum_pages].to_i : 100
|
|
167
|
-
|
|
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
|
|
168
204
|
@rewritten = params[:rewritten]
|
|
169
205
|
@reset = params[:reset]
|
|
170
206
|
@keep = params[:keep]
|
|
@@ -174,6 +210,7 @@ class WaybackMachineDownloader
|
|
|
174
210
|
@connection_pool = ConnectionPool.new(CONNECTION_POOL_SIZE)
|
|
175
211
|
@db_mutex = Mutex.new
|
|
176
212
|
@rewrite = params[:rewrite] || false
|
|
213
|
+
@delay = params[:delay] ? params[:delay].to_f : 0.0
|
|
177
214
|
@recursive_subdomains = params[:recursive_subdomains] || false
|
|
178
215
|
@subdomain_depth = params[:subdomain_depth] || 1
|
|
179
216
|
@snapshot_at = params[:snapshot_at] ? params[:snapshot_at].to_i : nil
|
|
@@ -181,6 +218,12 @@ class WaybackMachineDownloader
|
|
|
181
218
|
@page_requisites = params[:page_requisites] || false
|
|
182
219
|
@pending_jobs = Concurrent::AtomicFixnum.new(0)
|
|
183
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
|
+
|
|
184
227
|
# URL for rejecting invalid/unencoded wayback urls
|
|
185
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])|([!$&'('')'*+,;=])|:|@)|\/|\?)*)?)$/
|
|
186
229
|
|
|
@@ -545,12 +588,40 @@ class WaybackMachineDownloader
|
|
|
545
588
|
end
|
|
546
589
|
|
|
547
590
|
def append_to_db(file_id)
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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)
|
|
554
625
|
end
|
|
555
626
|
end
|
|
556
627
|
end
|
|
@@ -587,9 +658,18 @@ class WaybackMachineDownloader
|
|
|
587
658
|
end
|
|
588
659
|
end
|
|
589
660
|
end
|
|
590
|
-
|
|
661
|
+
|
|
662
|
+
@@engine_printed = false
|
|
663
|
+
|
|
591
664
|
def download_files
|
|
592
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
|
+
|
|
593
673
|
puts "Downloading #{@base_url} to #{backup_path} from Wayback Machine archives."
|
|
594
674
|
|
|
595
675
|
FileUtils.mkdir_p(backup_path)
|
|
@@ -608,7 +688,7 @@ class WaybackMachineDownloader
|
|
|
608
688
|
|
|
609
689
|
# Load IDs of already downloaded files
|
|
610
690
|
downloaded_ids = load_downloaded_ids
|
|
611
|
-
|
|
691
|
+
|
|
612
692
|
# We use a thread-safe Set to track what we have queued/downloaded in this session
|
|
613
693
|
# to avoid infinite loops with page requisites
|
|
614
694
|
@session_downloaded_ids = Concurrent::Set.new
|
|
@@ -624,7 +704,7 @@ class WaybackMachineDownloader
|
|
|
624
704
|
if skipped_count > 0
|
|
625
705
|
puts "Found #{skipped_count} previously downloaded files, skipping them."
|
|
626
706
|
end
|
|
627
|
-
|
|
707
|
+
|
|
628
708
|
if remaining_count == 0 && !@page_requisites
|
|
629
709
|
puts "All matching files have already been downloaded."
|
|
630
710
|
cleanup
|
|
@@ -691,6 +771,9 @@ class WaybackMachineDownloader
|
|
|
691
771
|
download_success = false
|
|
692
772
|
downloaded_path = nil
|
|
693
773
|
|
|
774
|
+
# if delay was set by the user
|
|
775
|
+
sleep(@delay) if @delay > 0
|
|
776
|
+
|
|
694
777
|
# fast-path for resumed runs: if file already exists locally, avoid HTTP work entirely
|
|
695
778
|
existing_path = local_path_for_file_id(file_remote_info[:file_id])
|
|
696
779
|
if existing_path && File.exist?(existing_path)
|
|
@@ -707,14 +790,14 @@ class WaybackMachineDownloader
|
|
|
707
790
|
end
|
|
708
791
|
return
|
|
709
792
|
end
|
|
710
|
-
|
|
793
|
+
|
|
711
794
|
@connection_pool.with_connection do |connection|
|
|
712
795
|
result_message, downloaded_path = download_file(file_remote_info, connection)
|
|
713
|
-
|
|
796
|
+
|
|
714
797
|
if downloaded_path && File.exist?(downloaded_path)
|
|
715
798
|
download_success = true
|
|
716
799
|
end
|
|
717
|
-
|
|
800
|
+
|
|
718
801
|
@download_mutex.synchronize do
|
|
719
802
|
@processed_file_count += 1 if @processed_file_count < @total_to_download
|
|
720
803
|
# only print if it's a "User" file or a requisite we found
|
|
@@ -724,7 +807,7 @@ class WaybackMachineDownloader
|
|
|
724
807
|
|
|
725
808
|
if download_success
|
|
726
809
|
append_to_db(file_remote_info[:file_id])
|
|
727
|
-
|
|
810
|
+
|
|
728
811
|
if @page_requisites && downloaded_path && File.extname(downloaded_path) =~ /\.(html?|php|asp|aspx|jsp)$/i
|
|
729
812
|
process_page_requisites(downloaded_path, file_remote_info)
|
|
730
813
|
end
|
|
@@ -732,7 +815,7 @@ class WaybackMachineDownloader
|
|
|
732
815
|
rescue => e
|
|
733
816
|
@logger.error("Error processing file #{file_remote_info[:file_url]}: #{e.message}")
|
|
734
817
|
end
|
|
735
|
-
|
|
818
|
+
|
|
736
819
|
def process_page_requisites(file_path, parent_remote_info)
|
|
737
820
|
return unless File.exist?(file_path)
|
|
738
821
|
|
|
@@ -744,7 +827,7 @@ class WaybackMachineDownloader
|
|
|
744
827
|
# prepare base URI for resolving relative paths
|
|
745
828
|
parent_raw = parent_remote_info[:file_url]
|
|
746
829
|
parent_raw = "http://#{parent_raw}" unless parent_raw.match?(/^https?:\/\//)
|
|
747
|
-
|
|
830
|
+
|
|
748
831
|
begin
|
|
749
832
|
base_uri = URI(parent_raw)
|
|
750
833
|
# calculate the "root" host of the site we are downloading to compare later
|
|
@@ -778,7 +861,7 @@ class WaybackMachineDownloader
|
|
|
778
861
|
path = resolved_uri.path
|
|
779
862
|
ext = File.extname(path).downcase
|
|
780
863
|
if ext.empty? || ['.html', '.htm', '.php', '.asp', '.aspx'].include?(ext)
|
|
781
|
-
next
|
|
864
|
+
next
|
|
782
865
|
end
|
|
783
866
|
|
|
784
867
|
# construct the original URL to query the Wayback API
|
|
@@ -849,7 +932,7 @@ class WaybackMachineDownloader
|
|
|
849
932
|
rescue Errno::EEXIST, Errno::ENOTDIR => e
|
|
850
933
|
file_already_existing = nil
|
|
851
934
|
check_path = dir_path
|
|
852
|
-
|
|
935
|
+
|
|
853
936
|
# walk up the path to find the specific file that is blocking directory creation
|
|
854
937
|
while check_path != "." && check_path != "/"
|
|
855
938
|
if File.exist?(check_path) && !File.directory?(check_path)
|
|
@@ -864,11 +947,11 @@ class WaybackMachineDownloader
|
|
|
864
947
|
if file_already_existing
|
|
865
948
|
file_already_existing_temporary = file_already_existing + '.temp'
|
|
866
949
|
file_already_existing_permanent = file_already_existing + '/index.html'
|
|
867
|
-
|
|
950
|
+
|
|
868
951
|
FileUtils::mv file_already_existing, file_already_existing_temporary
|
|
869
952
|
FileUtils::mkdir_p file_already_existing
|
|
870
953
|
FileUtils::mv file_already_existing_temporary, file_already_existing_permanent
|
|
871
|
-
|
|
954
|
+
|
|
872
955
|
puts "#{file_already_existing} -> #{file_already_existing_permanent}"
|
|
873
956
|
# retry the directory creation now that the path is clear
|
|
874
957
|
structure_dir_path dir_path
|
|
@@ -881,12 +964,12 @@ class WaybackMachineDownloader
|
|
|
881
964
|
def rewrite_local_files
|
|
882
965
|
puts "Scanning #{backup_path} for files to rewrite..."
|
|
883
966
|
files = Dir.glob(File.join(backup_path, "**/*.{html,htm,css,js,php,asp,aspx,jsp}"))
|
|
884
|
-
|
|
967
|
+
|
|
885
968
|
puts "Found #{files.size} files. Rewriting links for local browsing..."
|
|
886
|
-
|
|
969
|
+
|
|
887
970
|
pool = Concurrent::FixedThreadPool.new(@threads_count)
|
|
888
971
|
progress = Concurrent::AtomicFixnum.new(0)
|
|
889
|
-
|
|
972
|
+
|
|
890
973
|
files.each do |file_path|
|
|
891
974
|
pool.post do
|
|
892
975
|
rewrite_urls_to_relative(file_path)
|
|
@@ -894,7 +977,7 @@ class WaybackMachineDownloader
|
|
|
894
977
|
print "\rProgress: #{current}/#{files.size}" if current % 100 == 0
|
|
895
978
|
end
|
|
896
979
|
end
|
|
897
|
-
|
|
980
|
+
|
|
898
981
|
pool.shutdown
|
|
899
982
|
pool.wait_for_termination
|
|
900
983
|
puts "\nFinished rewriting all files."
|
|
@@ -902,39 +985,58 @@ class WaybackMachineDownloader
|
|
|
902
985
|
|
|
903
986
|
def rewrite_urls_to_relative(file_path)
|
|
904
987
|
return unless File.exist?(file_path)
|
|
905
|
-
|
|
988
|
+
|
|
906
989
|
file_ext = File.extname(file_path).downcase
|
|
907
|
-
|
|
990
|
+
|
|
908
991
|
begin
|
|
909
992
|
content = File.binread(file_path)
|
|
910
993
|
|
|
911
994
|
# detect encoding for HTML files
|
|
912
995
|
if file_ext == '.html' || file_ext == '.htm' || file_ext == '.php' || file_ext == '.asp'
|
|
913
|
-
|
|
914
|
-
|
|
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)
|
|
915
1006
|
else
|
|
916
1007
|
content.force_encoding('UTF-8')
|
|
917
1008
|
end
|
|
918
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
|
+
|
|
919
1019
|
# URLs in HTML attributes
|
|
920
1020
|
content = rewrite_html_attr_urls(content)
|
|
921
|
-
|
|
1021
|
+
|
|
922
1022
|
# URLs in CSS
|
|
923
1023
|
content = rewrite_css_urls(content)
|
|
924
|
-
|
|
1024
|
+
|
|
925
1025
|
# URLs in JavaScript
|
|
926
1026
|
content = rewrite_js_urls(content)
|
|
927
|
-
|
|
928
|
-
|
|
1027
|
+
|
|
1028
|
+
root_prefix = site_root_relative_prefix(file_path)
|
|
1029
|
+
|
|
1030
|
+
# rewrite root-absolute links to paths relative to the downloaded site root
|
|
929
1031
|
content.gsub!(/(\s(?:href|src|action|data-src|data-url)=["'])\/([^"'\/][^"']*)(["'])/i) do
|
|
930
1032
|
prefix, path, suffix = $1, $2, $3
|
|
931
|
-
"#{prefix}
|
|
1033
|
+
"#{prefix}#{root_prefix}#{path}#{suffix}"
|
|
932
1034
|
end
|
|
933
|
-
|
|
934
|
-
#
|
|
1035
|
+
|
|
1036
|
+
# apply the same root-relative conversion to CSS url(...) references
|
|
935
1037
|
content.gsub!(/url\(\s*["']?\/([^"'\)\/][^"'\)]*?)["']?\s*\)/i) do
|
|
936
1038
|
path = $1
|
|
937
|
-
"url(\"
|
|
1039
|
+
"url(\"#{root_prefix}#{path}\")"
|
|
938
1040
|
end
|
|
939
1041
|
|
|
940
1042
|
# save the modified content back to the file
|
|
@@ -942,15 +1044,35 @@ class WaybackMachineDownloader
|
|
|
942
1044
|
puts "Rewrote URLs in #{file_path} to be relative."
|
|
943
1045
|
rescue Errno::ENOENT => e
|
|
944
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}")
|
|
945
1049
|
end
|
|
946
1050
|
end
|
|
947
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 './'
|
|
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
|
|
1068
|
+
end
|
|
1069
|
+
|
|
948
1070
|
def download_file (file_remote_info, http)
|
|
949
1071
|
current_encoding = "".encoding
|
|
950
1072
|
file_url = file_remote_info[:file_url].encode(current_encoding)
|
|
951
1073
|
file_id = file_remote_info[:file_id]
|
|
952
1074
|
file_timestamp = file_remote_info[:timestamp]
|
|
953
|
-
|
|
1075
|
+
|
|
954
1076
|
# sanitize file_id to ensure it is a valid path component
|
|
955
1077
|
raw_path_elements = file_id.split('/')
|
|
956
1078
|
|
|
@@ -964,7 +1086,7 @@ class WaybackMachineDownloader
|
|
|
964
1086
|
element
|
|
965
1087
|
end
|
|
966
1088
|
end
|
|
967
|
-
|
|
1089
|
+
|
|
968
1090
|
current_backup_path = backup_path
|
|
969
1091
|
|
|
970
1092
|
if file_id == ""
|
|
@@ -1079,7 +1201,7 @@ class WaybackMachineDownloader
|
|
|
1079
1201
|
end
|
|
1080
1202
|
logger
|
|
1081
1203
|
end
|
|
1082
|
-
|
|
1204
|
+
|
|
1083
1205
|
# safely sanitize a file id (or id+timestamp)
|
|
1084
1206
|
def sanitize_and_prepare_id(raw, file_url)
|
|
1085
1207
|
return nil if raw.nil?
|
|
@@ -1221,65 +1343,85 @@ class WaybackMachineDownloader
|
|
|
1221
1343
|
return :skipped_not_found
|
|
1222
1344
|
end
|
|
1223
1345
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
request["User-Agent"] = "WaybackMachineDownloader/#{VERSION}"
|
|
1227
|
-
request["Accept-Encoding"] = "gzip, deflate"
|
|
1346
|
+
if HTTPX_AVAILABLE && connection.is_a?(HTTPX::Session)
|
|
1347
|
+
response = connection.get(wayback_url)
|
|
1228
1348
|
|
|
1229
|
-
|
|
1349
|
+
raise response.error if response.is_a?(HTTPX::ErrorResponse)
|
|
1230
1350
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
if response
|
|
1235
|
-
|
|
1236
|
-
gz = Zlib::GzipReader.new(StringIO.new(body))
|
|
1237
|
-
decompressed_body = gz.read
|
|
1238
|
-
gz.close
|
|
1239
|
-
file.write(decompressed_body)
|
|
1240
|
-
rescue Zlib::GzipFile::Error => e
|
|
1241
|
-
@logger.warn("Failure decompressing gzip file #{file_url}: #{e.message}. Writing raw body.")
|
|
1242
|
-
file.write(body)
|
|
1243
|
-
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)
|
|
1244
1356
|
else
|
|
1245
|
-
|
|
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
|
|
1246
1385
|
end
|
|
1247
1386
|
end
|
|
1248
1387
|
end
|
|
1249
1388
|
|
|
1250
1389
|
if @all
|
|
1251
|
-
case
|
|
1252
|
-
when
|
|
1390
|
+
case code
|
|
1391
|
+
when 200..599
|
|
1253
1392
|
save_response_body.call
|
|
1254
|
-
if
|
|
1255
|
-
@logger.info("Saved redirect page for #{file_url} (status #{
|
|
1256
|
-
elsif
|
|
1257
|
-
@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}).")
|
|
1258
1397
|
end
|
|
1259
1398
|
return :saved
|
|
1260
1399
|
else
|
|
1261
1400
|
# for any other response type when --all is true, treat as an error to be retried or failed
|
|
1262
|
-
raise "Unhandled HTTP response: #{
|
|
1401
|
+
raise "Unhandled HTTP response: #{code}"
|
|
1263
1402
|
end
|
|
1264
1403
|
else # not @all (our default behavior)
|
|
1265
|
-
|
|
1266
|
-
when Net::HTTPSuccess
|
|
1404
|
+
if (200..299).cover?(code)
|
|
1267
1405
|
save_response_body.call
|
|
1268
1406
|
return :saved
|
|
1269
|
-
|
|
1407
|
+
elsif (300..399).cover?(code)
|
|
1270
1408
|
raise "Too many redirects for #{file_url}" if redirect_count >= 5
|
|
1271
|
-
location =
|
|
1409
|
+
location = if HTTPX_AVAILABLE && connection.is_a?(HTTPX::Session)
|
|
1410
|
+
response.headers['location']
|
|
1411
|
+
else
|
|
1412
|
+
response['location']
|
|
1413
|
+
end
|
|
1272
1414
|
@logger.warn("Redirect found for #{file_url} -> #{location}")
|
|
1273
1415
|
redirected_source = resolve_redirect_source(file_url, location)
|
|
1274
1416
|
return download_with_retry(file_path, redirected_source, file_timestamp, connection, redirect_count + 1)
|
|
1275
|
-
|
|
1417
|
+
elsif code == 429
|
|
1276
1418
|
sleep(RATE_LIMIT * 2)
|
|
1277
1419
|
raise "Rate limited, retrying..."
|
|
1278
|
-
|
|
1420
|
+
elsif code == 404
|
|
1279
1421
|
@logger.warn("File not found, skipping: #{file_url}")
|
|
1280
1422
|
return :skipped_not_found
|
|
1281
1423
|
else
|
|
1282
|
-
raise "HTTP Error: #{
|
|
1424
|
+
raise "HTTP Error: #{code}"
|
|
1283
1425
|
end
|
|
1284
1426
|
end
|
|
1285
1427
|
|
|
@@ -1297,6 +1439,7 @@ class WaybackMachineDownloader
|
|
|
1297
1439
|
end
|
|
1298
1440
|
|
|
1299
1441
|
def cleanup
|
|
1442
|
+
flush_db
|
|
1300
1443
|
@connection_pool.shutdown
|
|
1301
1444
|
|
|
1302
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
|
|
@@ -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: []
|