mirror of
https://github.com/StrawberryMaster/wayback-machine-downloader.git
synced 2025-12-29 16:16:06 +00:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34f22c128c | ||
|
|
71bdc7c2de | ||
|
|
4b1ec1e1cc | ||
|
|
d7a63361e3 | ||
|
|
b1974a8dfa | ||
|
|
012b295aed | ||
|
|
dec9083b43 | ||
|
|
c517bd20d3 | ||
|
|
fc8d8a9441 | ||
|
|
fa306ac92b | ||
|
|
8c27aaebc9 | ||
|
|
40e9c9bb51 | ||
|
|
6bc08947b7 | ||
|
|
c731e0c7bd | ||
|
|
9fd2a7f8d1 | ||
|
|
6ad312f31f | ||
|
|
62ea35daa6 | ||
|
|
1f4202908f | ||
|
|
bed3f6101c | ||
|
|
754df6b8d6 | ||
|
|
801fb77f79 | ||
|
|
e9849e6c9c | ||
|
|
bc868e6b39 | ||
|
|
2bf04aff48 | ||
|
|
51becde916 | ||
|
|
c30ee73977 | ||
|
|
d3466b3387 | ||
|
|
0250579f0e | ||
|
|
0663c1c122 | ||
|
|
93115f70ec | ||
|
|
3d37ae10fd | ||
|
|
bff10e7260 | ||
|
|
3d181ce84c | ||
|
|
999aa211ae | ||
|
|
ffdce7e4ec | ||
|
|
e4487baafc | ||
|
|
82ff2de3dc | ||
|
|
fd329afdd2 | ||
|
|
038785557d | ||
|
|
2eead8cc27 | ||
|
|
7e5cdd54fb | ||
|
|
4160ff5e4a | ||
|
|
f03d92a3c4 | ||
|
|
2490109cfe | ||
|
|
c3c5b8446a | ||
|
|
18357a77ed | ||
|
|
3fdfd70fc1 | ||
|
|
2bf74b4173 | ||
|
|
79cbb639e7 | ||
|
|
071d208b31 | ||
|
|
1681a12579 | ||
|
|
f38756dd76 | ||
|
|
9452411e32 | ||
|
|
61e22cfe25 | ||
|
|
183ed61104 | ||
|
|
e6ecf32a43 | ||
|
|
375c6314ad | ||
|
|
6e2739f5a8 | ||
|
|
caba6a665f | ||
|
|
ab4324c0eb | ||
|
|
e28d7d578b | ||
|
|
a7a25574cf | ||
|
|
23cc3d69b1 | ||
|
|
01fa1f8c9f | ||
|
|
d2f98d9428 | ||
|
|
c7a5381eaf | ||
|
|
9709834e20 | ||
|
|
77998372cb | ||
|
|
2c789b7df6 | ||
|
|
1ef8c14c48 | ||
|
|
780e45343f | ||
|
|
42e6d62284 | ||
|
|
543161d7fb | ||
|
|
99a6de981e | ||
|
|
d85c880d23 | ||
|
|
917f4f8798 | ||
|
|
787bc2e535 | ||
|
|
4db13a7792 | ||
|
|
31d51728af | ||
|
|
febffe5de4 | ||
|
|
27dd619aa4 | ||
|
|
576298dca8 | ||
|
|
dc71d1d167 | ||
|
|
13e88ce04a | ||
|
|
c7fc7c7b58 | ||
|
|
5aebf83fca | ||
|
|
b1080f0219 | ||
|
|
dde36ea840 | ||
|
|
acec026ce1 | ||
|
|
ec3fd2dcaa | ||
|
|
6518ecf215 | ||
|
|
f5572d6129 | ||
|
|
fc4ccf62e2 | ||
|
|
84bf76363c | ||
|
|
0c701ee890 | ||
|
|
c953d038e2 | ||
|
|
b726e94947 | ||
|
|
f86302e7aa | ||
|
|
791068e9bd | ||
|
|
456e08e745 | ||
|
|
90069fad41 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
*.md
|
||||
*.yml
|
||||
|
||||
.github
|
||||
websites
|
||||
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
DB_HOST="db"
|
||||
DB_USER="root"
|
||||
DB_PASSWORD="example1234"
|
||||
DB_NAME="wayback"
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -18,6 +18,11 @@ Gemfile.lock
|
||||
.ruby-version
|
||||
.rbenv*
|
||||
|
||||
## ENV
|
||||
*.env*
|
||||
!.env*.example
|
||||
|
||||
|
||||
## RCOV
|
||||
coverage.data
|
||||
|
||||
@@ -27,3 +32,7 @@ tmp
|
||||
*.rbc
|
||||
|
||||
test.rb
|
||||
|
||||
# Dev environment
|
||||
.vscode
|
||||
*.code-workspace
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -1,7 +1,14 @@
|
||||
FROM ruby:2.3-alpine
|
||||
FROM ruby:3.4.5-alpine
|
||||
USER root
|
||||
WORKDIR /build
|
||||
|
||||
COPY Gemfile /build/
|
||||
COPY *.gemspec /build/
|
||||
|
||||
RUN bundle config set jobs "$(nproc)" \
|
||||
&& bundle install
|
||||
|
||||
COPY . /build
|
||||
|
||||
WORKDIR /
|
||||
ENTRYPOINT [ "/build/bin/wayback_machine_downloader" ]
|
||||
WORKDIR /build
|
||||
ENTRYPOINT [ "/build/bin/wayback_machine_downloader", "--directory", "/build/websites" ]
|
||||
|
||||
142
README.md
142
README.md
@@ -1,6 +1,5 @@
|
||||
# Wayback Machine Downloader
|
||||
|
||||

|
||||
[](https://rubygems.org/gems/wayback_machine_downloader_straw)
|
||||
|
||||
This is a fork of the [Wayback Machine Downloader](https://github.com/hartator/wayback-machine-downloader). With this, you can download a website from the Internet Archive Wayback Machine.
|
||||
|
||||
@@ -10,7 +9,7 @@ Included here is partial content from other forks, namely those @ [ShiftaDeband]
|
||||
|
||||
Download a website's latest snapshot:
|
||||
```bash
|
||||
ruby wayback_machine_downloader https://example.com
|
||||
wayback_machine_downloader https://example.com
|
||||
```
|
||||
Your files will save to `./websites/example.com/` with their original structure preserved.
|
||||
|
||||
@@ -19,6 +18,18 @@ Your files will save to `./websites/example.com/` with their original structure
|
||||
- Ruby 2.3+ ([download Ruby here](https://www.ruby-lang.org/en/downloads/))
|
||||
- Bundler gem (`gem install bundler`)
|
||||
|
||||
### Quick install
|
||||
It took a while, but we have a gem for this! Install it with:
|
||||
```bash
|
||||
gem install wayback_machine_downloader_straw
|
||||
```
|
||||
To run most commands, just like in the original WMD, you can use:
|
||||
```bash
|
||||
wayback_machine_downloader https://example.com
|
||||
```
|
||||
Do note that you can also manually download this repository and run commands here by appending `ruby` before a command, e.g. `ruby wayback_machine_downloader https://example.com`.
|
||||
**Note**: this gem may conflict with hartator's wayback_machine_downloader gem, and so you may have to uninstall it for this WMD fork to work. A good way to know is if a command fails; it will list the gem version as 2.3.1 or earlier, while this WMD fork uses 2.3.2 or above.
|
||||
|
||||
### Step-by-step setup
|
||||
1. **Install Ruby**:
|
||||
```bash
|
||||
@@ -31,6 +42,11 @@ Your files will save to `./websites/example.com/` with their original structure
|
||||
bundle install
|
||||
```
|
||||
|
||||
If you encounter an error like cannot load such file -- concurrent-ruby, manually install the missing gem:
|
||||
```bash
|
||||
gem install concurrent-ruby
|
||||
```
|
||||
|
||||
3. **Run it**:
|
||||
```bash
|
||||
cd path/to/wayback-machine-downloader/bin
|
||||
@@ -48,16 +64,49 @@ docker build -t wayback_machine_downloader .
|
||||
docker run -it --rm wayback_machine_downloader [options] URL
|
||||
```
|
||||
|
||||
As an example of how this works without cloning this repo, this command fetches smallrockets.com until the year 2013:
|
||||
|
||||
```bash
|
||||
docker run -v .:/websites ghcr.io/strawberrymaster/wayback-machine-downloader:master wayback_machine_downloader --to 20130101 smallrockets.com
|
||||
```
|
||||
|
||||
### 🐳 Using Docker Compose
|
||||
You can also use Docker Compose, which provides a lot of benefits for extending more functionalities (such as implementing storing previous downloads in a database):
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
wayback_machine_downloader:
|
||||
build:
|
||||
context: .
|
||||
tty: true
|
||||
image: wayback_machine_downloader:latest
|
||||
container_name: wayback_machine_downloader
|
||||
volumes:
|
||||
- .:/build:rw
|
||||
- ./websites:/build/websites:rw
|
||||
```
|
||||
#### Usage:
|
||||
Now you can create a Docker image as named "wayback_machine_downloader" with the following command:
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
After that you can run the exists container with the following command:
|
||||
```bash
|
||||
docker compose run --rm wayback_machine_downloader https://example.com [options]
|
||||
```
|
||||
|
||||
## ⚙️ Configuration
|
||||
There are a few constants that can be edited in the `wayback_machine_downloader.rb` file for your convenience. The default values may be conservative, so you can adjust them to your needs. They are:
|
||||
|
||||
```ruby
|
||||
DEFAULT_TIMEOUT = 30 # HTTP timeout (in seconds)
|
||||
MAX_RETRIES = 3 # Failed request retries
|
||||
RETRY_DELAY = 2 # Wait between retries
|
||||
RATE_LIMIT = 0.25 # Throttle between requests
|
||||
CONNECTION_POOL_SIZE = 10 # No. of simultaneous connections
|
||||
MEMORY_BUFFER_SIZE = 16384 # Size of download buffer
|
||||
MAX_RETRIES = 3 # Number of times to retry failed requests
|
||||
RETRY_DELAY = 2 # Wait time between retries (seconds)
|
||||
RATE_LIMIT = 0.25 # Throttle between requests (seconds)
|
||||
CONNECTION_POOL_SIZE = 10 # Maximum simultaneous connections
|
||||
MEMORY_BUFFER_SIZE = 16384 # Download buffer size (bytes)
|
||||
STATE_CDX_FILENAME = '.cdx.json' # Stores snapshot listing
|
||||
STATE_DB_FILENAME = '.downloaded.txt' # Tracks completed downloads
|
||||
```
|
||||
|
||||
## 🛠️ Advanced usage
|
||||
@@ -71,6 +120,7 @@ MEMORY_BUFFER_SIZE = 16384 # Size of download buffer
|
||||
| `-t TS`, `--to TS` | Stop at timestamp |
|
||||
| `-e`, `--exact-url` | Download exact URL only |
|
||||
| `-r`, `--rewritten` | Download rewritten Wayback Archive files only |
|
||||
| `-rt`, `--retry NUM` | Number of tries in case a download fails (default: 1) |
|
||||
|
||||
**Example** - Download files to `downloaded-backup` folder
|
||||
```bash
|
||||
@@ -116,6 +166,8 @@ ruby wayback_machine_downloader https://example.com --rewritten
|
||||
```
|
||||
Useful if you want to download the rewritten files from the Wayback Machine instead of the original ones.
|
||||
|
||||
---
|
||||
|
||||
### Filtering Content
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
@@ -150,6 +202,8 @@ Or if you want to download everything except images:
|
||||
ruby wayback_machine_downloader https://example.com --exclude "/\.(gif|jpg|jpeg)$/i"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Performance
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
@@ -164,10 +218,12 @@ Will specify the number of multiple files you want to download at the same time.
|
||||
|
||||
**Example 2** - 300 snapshot pages:
|
||||
```bash
|
||||
ruby wayback_machine_downloader https://example.com --snapshot-pages 300
|
||||
ruby wayback_machine_downloader https://example.com --maximum-snapshot 300
|
||||
```
|
||||
Will specify the maximum number of snapshot pages to consider. Count an average of 150,000 snapshots per page. 100 is the default maximum number of snapshot pages and should be sufficient for most websites. Use a bigger number if you want to download a very large website.
|
||||
|
||||
---
|
||||
|
||||
### Diagnostics
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
@@ -186,6 +242,72 @@ ruby wayback_machine_downloader https://example.com --list
|
||||
```
|
||||
It will just display the files to be downloaded with their snapshot timestamps and urls. The output format is JSON. It won't download anything. It's useful for debugging or to connect to another application.
|
||||
|
||||
---
|
||||
|
||||
### Job management
|
||||
The downloader automatically saves its progress (`.cdx.json` for snapshot list, `.downloaded.txt` for completed files) in the output directory. If you run the same command again pointing to the same output directory, it will resume where it left off, skipping already downloaded files.
|
||||
|
||||
> [!NOTE]
|
||||
> Automatic resumption can be affected by changing the URL, mode selection (like `--all-timestamps`), filtering selections, or other options. If you want to ensure a clean start, use the `--reset` option.
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--reset` | Delete state files (`.cdx.json`, `.downloaded.txt`) and restart the download from scratch. Does not delete already downloaded website files. |
|
||||
| `--keep` | Keep state files (`.cdx.json`, `.downloaded.txt`) even after a successful download. By default, these are deleted upon successful completion. |
|
||||
|
||||
**Example** - Restart a download job from the beginning:
|
||||
```bash
|
||||
ruby wayback_machine_downloader https://example.com --reset
|
||||
```
|
||||
This is useful if you suspect the state files are corrupted or want to ensure a completely fresh download process without deleting the files you already have.
|
||||
|
||||
**Example 2** - Keep state files after download:
|
||||
```bash
|
||||
ruby wayback_machine_downloader https://example.com --keep
|
||||
```
|
||||
This can be useful for debugging or if you plan to extend the download later with different parameters (e.g., adding `--to` timestamp) while leveraging the existing snapshot list.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### SSL certificate errors
|
||||
If you encounter an SSL error like:
|
||||
```
|
||||
SSL_connect returned=1 errno=0 state=error: certificate verify failed (unable to get certificate CRL)
|
||||
```
|
||||
|
||||
This is a known issue with **OpenSSL 3.6.0** when used with certain Ruby installations, and not a bug with this WMD work specifically. (See [ruby/openssl#949](https://github.com/ruby/openssl/issues/949) for details.)
|
||||
|
||||
The workaround is to create a file named `fix_ssl_store.rb` with the following content:
|
||||
```ruby
|
||||
require "openssl"
|
||||
store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
|
||||
OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:cert_store] = store
|
||||
```
|
||||
|
||||
and run wayback-machine-downloader with:
|
||||
```bash
|
||||
RUBYOPT="-r./fix_ssl_store.rb" wayback_machine_downloader "http://example.com"
|
||||
```
|
||||
|
||||
#### Verifying the issue
|
||||
You can test if your Ruby environment has this issue by running:
|
||||
|
||||
```ruby
|
||||
require "net/http"
|
||||
require "uri"
|
||||
|
||||
uri = URI("https://web.archive.org/")
|
||||
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
||||
resp = http.get("/")
|
||||
puts "GET / => #{resp.code}"
|
||||
end
|
||||
```
|
||||
If this fails with the same SSL error, the workaround above will fix it.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
|
||||
@@ -59,7 +59,31 @@ option_parser = OptionParser.new do |opts|
|
||||
end
|
||||
|
||||
opts.on("-r", "--rewritten", "Downloads the rewritten Wayback Machine files instead of the original files") do |t|
|
||||
options[:rewritten] = t
|
||||
options[:rewritten] = true
|
||||
end
|
||||
|
||||
opts.on("--local", "Rewrite URLs to make them relative for local browsing") do |t|
|
||||
options[:rewrite] = true
|
||||
end
|
||||
|
||||
opts.on("--reset", "Delete state files (.cdx.json, .downloaded.txt) and restart the download from scratch") do |t|
|
||||
options[:reset] = true
|
||||
end
|
||||
|
||||
opts.on("--keep", "Keep state files (.cdx.json, .downloaded.txt) after a successful download") do |t|
|
||||
options[:keep] = true
|
||||
end
|
||||
|
||||
opts.on("--rt", "--retry N", Integer, "Maximum number of retries for failed downloads (default: 3)") do |t|
|
||||
options[:max_retries] = t
|
||||
end
|
||||
|
||||
opts.on("--recursive-subdomains", "Recursively download content from subdomains") do |t|
|
||||
options[:recursive_subdomains] = true
|
||||
end
|
||||
|
||||
opts.on("--subdomain-depth DEPTH", Integer, "Maximum depth for subdomain recursion (default: 1)") do |t|
|
||||
options[:subdomain_depth] = t
|
||||
end
|
||||
|
||||
opts.on("-v", "--version", "Display version") do |t|
|
||||
|
||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
wayback_machine_downloader:
|
||||
build:
|
||||
context: .
|
||||
tty: true
|
||||
image: wayback_machine_downloader:latest
|
||||
container_name: wayback_machine_downloader
|
||||
volumes:
|
||||
- .:/build:rw
|
||||
- ./websites:/build/websites:rw
|
||||
9
entrypoint.sh
Normal file
9
entrypoint.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$ENVIRONMENT" == "development" ]; then
|
||||
echo "Running in development mode. Starting rerun..."
|
||||
exec rerun --dir /build --ignore "websites/*" -- /build/bin/wayback_machine_downloader "$@"
|
||||
else
|
||||
echo "Not in development mode. Skipping rerun."
|
||||
exec /build/bin/wayback_machine_downloader "$@"
|
||||
fi
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,15 @@ require 'uri'
|
||||
module ArchiveAPI
|
||||
|
||||
def get_raw_list_from_api(url, page_index, http)
|
||||
request_url = URI("https://web.archive.org/cdx/search/xd")
|
||||
# Automatically append /* if the URL doesn't contain a path after the domain
|
||||
# This is a workaround for an issue with the API and *some* domains.
|
||||
# See https://github.com/StrawberryMaster/wayback-machine-downloader/issues/6
|
||||
# But don't do this when exact_url flag is set
|
||||
if url && !url.match(/^https?:\/\/.*\//i) && !@exact_url
|
||||
url = "#{url}/*"
|
||||
end
|
||||
|
||||
request_url = URI("https://web.archive.org/cdx/search/cdx")
|
||||
params = [["output", "json"], ["url", url]] + parameters_for_api(page_index)
|
||||
request_url.query = URI.encode_www_form(params)
|
||||
|
||||
@@ -17,7 +25,7 @@ module ArchiveAPI
|
||||
# Check if the response contains the header ["timestamp", "original"]
|
||||
json.shift if json.first == ["timestamp", "original"]
|
||||
json
|
||||
rescue JSON::ParserError, StandardError => e
|
||||
rescue JSON::ParserError => e
|
||||
warn "Failed to fetch data from API: #{e.message}"
|
||||
[]
|
||||
end
|
||||
|
||||
238
lib/wayback_machine_downloader/subdom_processor.rb
Normal file
238
lib/wayback_machine_downloader/subdom_processor.rb
Normal file
@@ -0,0 +1,238 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SubdomainProcessor
|
||||
def process_subdomains
|
||||
return unless @recursive_subdomains
|
||||
|
||||
puts "Starting subdomain processing..."
|
||||
|
||||
# extract base domain from the URL for comparison
|
||||
base_domain = extract_base_domain(@base_url)
|
||||
@processed_domains = Set.new([base_domain])
|
||||
@subdomain_queue = Queue.new
|
||||
|
||||
# scan downloaded files for subdomain links
|
||||
initial_files = Dir.glob(File.join(backup_path, "**/*.{html,htm,css,js}"))
|
||||
puts "Scanning #{initial_files.size} downloaded files for subdomain links..."
|
||||
|
||||
subdomains_found = scan_files_for_subdomains(initial_files, base_domain)
|
||||
|
||||
if subdomains_found.empty?
|
||||
puts "No subdomains found in downloaded content."
|
||||
return
|
||||
end
|
||||
|
||||
puts "Found #{subdomains_found.size} subdomains to process: #{subdomains_found.join(', ')}"
|
||||
|
||||
# add found subdomains to the queue
|
||||
subdomains_found.each do |subdomain|
|
||||
full_domain = "#{subdomain}.#{base_domain}"
|
||||
@subdomain_queue << "https://#{full_domain}/"
|
||||
end
|
||||
|
||||
# process the subdomain queue
|
||||
download_subdomains(base_domain)
|
||||
|
||||
# after all downloads, rewrite all URLs to make local references
|
||||
rewrite_subdomain_links(base_domain) if @rewrite
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_base_domain(url)
|
||||
uri = URI.parse(url.gsub(/^https?:\/\//, '').split('/').first) rescue nil
|
||||
return nil unless uri
|
||||
|
||||
host = uri.host || uri.path.split('/').first
|
||||
host = host.downcase
|
||||
|
||||
# extract the base domain (e.g., "example.com" from "sub.example.com")
|
||||
parts = host.split('.')
|
||||
return host if parts.size <= 2
|
||||
|
||||
# for domains like co.uk, we want to keep the last 3 parts
|
||||
if parts[-2].length <= 3 && parts[-1].length <= 3 && parts.size > 2
|
||||
parts.last(3).join('.')
|
||||
else
|
||||
parts.last(2).join('.')
|
||||
end
|
||||
end
|
||||
|
||||
def scan_files_for_subdomains(files, base_domain)
|
||||
return [] unless base_domain
|
||||
|
||||
subdomains = Set.new
|
||||
|
||||
files.each do |file_path|
|
||||
next unless File.exist?(file_path)
|
||||
|
||||
begin
|
||||
content = File.read(file_path)
|
||||
|
||||
# extract URLs from HTML href/src attributes
|
||||
content.scan(/(?:href|src|action|data-src)=["']https?:\/\/([^\/."']+)\.#{Regexp.escape(base_domain)}[\/"]/) do |match|
|
||||
subdomain = match[0].downcase
|
||||
next if subdomain == 'www' # skip www subdomain
|
||||
subdomains.add(subdomain)
|
||||
end
|
||||
|
||||
# extract URLs from CSS
|
||||
content.scan(/url\(["']?https?:\/\/([^\/."']+)\.#{Regexp.escape(base_domain)}[\/"]/) do |match|
|
||||
subdomain = match[0].downcase
|
||||
next if subdomain == 'www' # skip www subdomain
|
||||
subdomains.add(subdomain)
|
||||
end
|
||||
|
||||
# extract URLs from JavaScript strings
|
||||
content.scan(/["']https?:\/\/([^\/."']+)\.#{Regexp.escape(base_domain)}[\/"]/) do |match|
|
||||
subdomain = match[0].downcase
|
||||
next if subdomain == 'www' # skip www subdomain
|
||||
subdomains.add(subdomain)
|
||||
end
|
||||
rescue => e
|
||||
puts "Error scanning file #{file_path}: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
subdomains.to_a
|
||||
end
|
||||
|
||||
def download_subdomains(base_domain)
|
||||
puts "Starting subdomain downloads..."
|
||||
depth = 0
|
||||
max_depth = @subdomain_depth || 1
|
||||
|
||||
while depth < max_depth && !@subdomain_queue.empty?
|
||||
current_batch = []
|
||||
|
||||
# get all subdomains at current depth
|
||||
while !@subdomain_queue.empty?
|
||||
current_batch << @subdomain_queue.pop
|
||||
end
|
||||
|
||||
puts "Processing #{current_batch.size} subdomains at depth #{depth + 1}..."
|
||||
|
||||
# download each subdomain
|
||||
current_batch.each do |subdomain_url|
|
||||
download_subdomain(subdomain_url, base_domain)
|
||||
end
|
||||
|
||||
# if we need to go deeper, scan the newly downloaded files
|
||||
if depth + 1 < max_depth
|
||||
# get all files in the subdomains directory
|
||||
new_files = Dir.glob(File.join(backup_path, "subdomains", "**/*.{html,htm,css,js}"))
|
||||
new_subdomains = scan_files_for_subdomains(new_files, base_domain)
|
||||
|
||||
# filter out already processed subdomains
|
||||
new_subdomains.each do |subdomain|
|
||||
full_domain = "#{subdomain}.#{base_domain}"
|
||||
unless @processed_domains.include?(full_domain)
|
||||
@processed_domains.add(full_domain)
|
||||
@subdomain_queue << "https://#{full_domain}/"
|
||||
end
|
||||
end
|
||||
|
||||
puts "Found #{@subdomain_queue.size} new subdomains at depth #{depth + 1}" if !@subdomain_queue.empty?
|
||||
end
|
||||
|
||||
depth += 1
|
||||
end
|
||||
end
|
||||
|
||||
def download_subdomain(subdomain_url, base_domain)
|
||||
begin
|
||||
uri = URI.parse(subdomain_url)
|
||||
subdomain_host = uri.host
|
||||
|
||||
# skip if already processed
|
||||
if @processed_domains.include?(subdomain_host)
|
||||
puts "Skipping already processed subdomain: #{subdomain_host}"
|
||||
return
|
||||
end
|
||||
|
||||
@processed_domains.add(subdomain_host)
|
||||
puts "Downloading subdomain: #{subdomain_url}"
|
||||
|
||||
# create the directory for this subdomain
|
||||
subdomain_dir = File.join(backup_path, "subdomains", subdomain_host)
|
||||
FileUtils.mkdir_p(subdomain_dir)
|
||||
|
||||
# create subdomain downloader with appropriate options
|
||||
subdomain_options = {
|
||||
base_url: subdomain_url,
|
||||
directory: subdomain_dir,
|
||||
from_timestamp: @from_timestamp,
|
||||
to_timestamp: @to_timestamp,
|
||||
all: @all,
|
||||
threads_count: @threads_count,
|
||||
maximum_pages: [@maximum_pages / 2, 10].max,
|
||||
rewrite: @rewrite,
|
||||
# don't recursively process subdomains from here
|
||||
recursive_subdomains: false
|
||||
}
|
||||
|
||||
# download the subdomain content
|
||||
subdomain_downloader = WaybackMachineDownloader.new(subdomain_options)
|
||||
subdomain_downloader.download_files
|
||||
|
||||
puts "Completed download of subdomain: #{subdomain_host}"
|
||||
rescue => e
|
||||
puts "Error downloading subdomain #{subdomain_url}: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
def rewrite_subdomain_links(base_domain)
|
||||
puts "Rewriting all files to use local subdomain references..."
|
||||
|
||||
all_files = Dir.glob(File.join(backup_path, "**/*.{html,htm,css,js}"))
|
||||
subdomains = @processed_domains.reject { |domain| domain == base_domain }
|
||||
|
||||
puts "Found #{all_files.size} files to check for rewriting"
|
||||
puts "Will rewrite links for subdomains: #{subdomains.join(', ')}"
|
||||
|
||||
rewritten_count = 0
|
||||
|
||||
all_files.each do |file_path|
|
||||
next unless File.exist?(file_path)
|
||||
|
||||
begin
|
||||
content = File.read(file_path)
|
||||
original_content = content.dup
|
||||
|
||||
# replace subdomain URLs with local paths
|
||||
subdomains.each do |subdomain_host|
|
||||
# for HTML attributes (href, src, etc.)
|
||||
content.gsub!(/(\s(?:href|src|action|data-src|data-url)=["'])https?:\/\/#{Regexp.escape(subdomain_host)}([^"']*)(["'])/i) do
|
||||
prefix, path, suffix = $1, $2, $3
|
||||
path = "/index.html" if path.empty? || path == "/"
|
||||
"#{prefix}../subdomains/#{subdomain_host}#{path}#{suffix}"
|
||||
end
|
||||
|
||||
# for CSS url()
|
||||
content.gsub!(/url\(\s*["']?https?:\/\/#{Regexp.escape(subdomain_host)}([^"'\)]*?)["']?\s*\)/i) do
|
||||
path = $1
|
||||
path = "/index.html" if path.empty? || path == "/"
|
||||
"url(\"../subdomains/#{subdomain_host}#{path}\")"
|
||||
end
|
||||
|
||||
# for JavaScript strings
|
||||
content.gsub!(/(["'])https?:\/\/#{Regexp.escape(subdomain_host)}([^"']*)(["'])/i) do
|
||||
quote_start, path, quote_end = $1, $2, $3
|
||||
path = "/index.html" if path.empty? || path == "/"
|
||||
"#{quote_start}../subdomains/#{subdomain_host}#{path}#{quote_end}"
|
||||
end
|
||||
end
|
||||
|
||||
# save if modified
|
||||
if content != original_content
|
||||
File.write(file_path, content)
|
||||
rewritten_count += 1
|
||||
end
|
||||
rescue => e
|
||||
puts "Error rewriting file #{file_path}: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
puts "Rewrote links in #{rewritten_count} files"
|
||||
end
|
||||
end
|
||||
@@ -1,73 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# essentially, this is for converting a string with a potentially
|
||||
# broken or unknown encoding into a valid UTF-8 string
|
||||
# @todo: consider using charlock_holmes for this in the future
|
||||
module TidyBytes
|
||||
# precomputing CP1252 to UTF-8 mappings for bytes 128-159
|
||||
CP1252_MAP = (128..159).map do |byte|
|
||||
case byte
|
||||
when 128 then [226, 130, 172] # EURO SIGN
|
||||
when 130 then [226, 128, 154] # SINGLE LOW-9 QUOTATION MARK
|
||||
when 131 then [198, 146] # LATIN SMALL LETTER F WITH HOOK
|
||||
when 132 then [226, 128, 158] # DOUBLE LOW-9 QUOTATION MARK
|
||||
when 133 then [226, 128, 166] # HORIZONTAL ELLIPSIS
|
||||
when 134 then [226, 128, 160] # DAGGER
|
||||
when 135 then [226, 128, 161] # DOUBLE DAGGER
|
||||
when 136 then [203, 134] # MODIFIER LETTER CIRCUMFLEX ACCENT
|
||||
when 137 then [226, 128, 176] # PER MILLE SIGN
|
||||
when 138 then [197, 160] # LATIN CAPITAL LETTER S WITH CARON
|
||||
when 139 then [226, 128, 185] # SINGLE LEFT-POINTING ANGLE QUOTATION MARK
|
||||
when 140 then [197, 146] # LATIN CAPITAL LIGATURE OE
|
||||
when 142 then [197, 189] # LATIN CAPITAL LETTER Z WITH CARON
|
||||
when 145 then [226, 128, 152] # LEFT SINGLE QUOTATION MARK
|
||||
when 146 then [226, 128, 153] # RIGHT SINGLE QUOTATION MARK
|
||||
when 147 then [226, 128, 156] # LEFT DOUBLE QUOTATION MARK
|
||||
when 148 then [226, 128, 157] # RIGHT DOUBLE QUOTATION MARK
|
||||
when 149 then [226, 128, 162] # BULLET
|
||||
when 150 then [226, 128, 147] # EN DASH
|
||||
when 151 then [226, 128, 148] # EM DASH
|
||||
when 152 then [203, 156] # SMALL TILDE
|
||||
when 153 then [226, 132, 162] # TRADE MARK SIGN
|
||||
when 154 then [197, 161] # LATIN SMALL LETTER S WITH CARON
|
||||
when 155 then [226, 128, 186] # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
|
||||
when 156 then [197, 147] # LATIN SMALL LIGATURE OE
|
||||
when 158 then [197, 190] # LATIN SMALL LETTER Z WITH CARON
|
||||
when 159 then [197, 184] # LATIN SMALL LETTER Y WITH DIAERESIS
|
||||
end
|
||||
end.freeze
|
||||
UNICODE_REPLACEMENT_CHARACTER = "<EFBFBD>"
|
||||
|
||||
# precomputing all possible byte conversions
|
||||
CP1252_TO_UTF8 = Array.new(256) do |b|
|
||||
if (128..159).cover?(b)
|
||||
CP1252_MAP[b - 128]&.pack('C*')
|
||||
elsif b < 128
|
||||
b.chr
|
||||
else
|
||||
b < 192 ? [194, b].pack('C*') : [195, b - 64].pack('C*')
|
||||
# common encodings to try for best multilingual compatibility
|
||||
COMMON_ENCODINGS = [
|
||||
Encoding::UTF_8,
|
||||
Encoding::Windows_1251, # Cyrillic/Russian legacy
|
||||
Encoding::GB18030, # Simplified Chinese
|
||||
Encoding::Shift_JIS, # Japanese
|
||||
Encoding::EUC_KR, # Korean
|
||||
Encoding::ISO_8859_1, # Western European
|
||||
Encoding::Windows_1252 # Western European/Latin1 superset
|
||||
].select { |enc| Encoding.name_list.include?(enc.name) }
|
||||
|
||||
# returns true if the string appears to be binary (has null bytes)
|
||||
def binary_data?
|
||||
self.include?("\x00".b)
|
||||
end
|
||||
|
||||
# attempts to return a valid UTF-8 version of the string
|
||||
def tidy_bytes
|
||||
return self if self.encoding == Encoding::UTF_8 && self.valid_encoding?
|
||||
return self.dup.force_encoding("BINARY") if binary_data?
|
||||
|
||||
str = self.dup
|
||||
COMMON_ENCODINGS.each do |enc|
|
||||
str.force_encoding(enc)
|
||||
begin
|
||||
utf8 = str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: UNICODE_REPLACEMENT_CHARACTER)
|
||||
return utf8 if utf8.valid_encoding? && !utf8.include?(UNICODE_REPLACEMENT_CHARACTER)
|
||||
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
||||
# try next encoding
|
||||
end
|
||||
end
|
||||
end.freeze
|
||||
|
||||
# if no clean conversion found, try again but accept replacement characters
|
||||
str = self.dup
|
||||
COMMON_ENCODINGS.each do |enc|
|
||||
str.force_encoding(enc)
|
||||
begin
|
||||
utf8 = str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: UNICODE_REPLACEMENT_CHARACTER)
|
||||
return utf8 if utf8.valid_encoding?
|
||||
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
||||
# try next encoding
|
||||
end
|
||||
end
|
||||
|
||||
# fallback: replace all invalid/undefined bytes
|
||||
str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: UNICODE_REPLACEMENT_CHARACTER)
|
||||
end
|
||||
|
||||
def tidy_bytes!
|
||||
replace(self.tidy_bytes)
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
def tidy_bytes(force = false)
|
||||
return nil if empty?
|
||||
|
||||
if force
|
||||
buffer = String.new(capacity: bytesize)
|
||||
each_byte { |b| buffer << CP1252_TO_UTF8[b] }
|
||||
return buffer.force_encoding(Encoding::UTF_8)
|
||||
end
|
||||
base.send(:include, InstanceMethods)
|
||||
end
|
||||
|
||||
begin
|
||||
encode('UTF-8')
|
||||
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
||||
buffer = String.new(capacity: bytesize)
|
||||
scrub { |b| CP1252_TO_UTF8[b.ord] }
|
||||
end
|
||||
end
|
||||
module InstanceMethods
|
||||
def tidy_bytes
|
||||
TidyBytes.instance_method(:tidy_bytes).bind(self).call
|
||||
end
|
||||
|
||||
def tidy_bytes!(force = false)
|
||||
result = tidy_bytes(force)
|
||||
result ? replace(result) : self
|
||||
end
|
||||
def tidy_bytes!
|
||||
TidyBytes.instance_method(:tidy_bytes!).bind(self).call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
74
lib/wayback_machine_downloader/url_rewrite.rb
Normal file
74
lib/wayback_machine_downloader/url_rewrite.rb
Normal file
@@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# URLs in HTML attributes
|
||||
def rewrite_html_attr_urls(content)
|
||||
|
||||
content.gsub!(/(\s(?:href|src|action|data-src|data-url)=["'])https?:\/\/web\.archive\.org\/web\/[0-9]+(?:id_)?\/([^"']+)(["'])/i) do
|
||||
prefix, url, suffix = $1, $2, $3
|
||||
|
||||
if url.start_with?('http')
|
||||
begin
|
||||
uri = URI.parse(url)
|
||||
path = uri.path
|
||||
path = path[1..-1] if path.start_with?('/')
|
||||
"#{prefix}#{path}#{suffix}"
|
||||
rescue
|
||||
"#{prefix}#{url}#{suffix}"
|
||||
end
|
||||
elsif url.start_with?('/')
|
||||
"#{prefix}./#{url[1..-1]}#{suffix}"
|
||||
else
|
||||
"#{prefix}#{url}#{suffix}"
|
||||
end
|
||||
end
|
||||
content
|
||||
end
|
||||
|
||||
# URLs in CSS
|
||||
def rewrite_css_urls(content)
|
||||
|
||||
content.gsub!(/url\(\s*["']?https?:\/\/web\.archive\.org\/web\/[0-9]+(?:id_)?\/([^"'\)]+)["']?\s*\)/i) do
|
||||
url = $1
|
||||
|
||||
if url.start_with?('http')
|
||||
begin
|
||||
uri = URI.parse(url)
|
||||
path = uri.path
|
||||
path = path[1..-1] if path.start_with?('/')
|
||||
"url(\"#{path}\")"
|
||||
rescue
|
||||
"url(\"#{url}\")"
|
||||
end
|
||||
elsif url.start_with?('/')
|
||||
"url(\"./#{url[1..-1]}\")"
|
||||
else
|
||||
"url(\"#{url}\")"
|
||||
end
|
||||
end
|
||||
content
|
||||
end
|
||||
|
||||
# URLs in JavaScript
|
||||
def rewrite_js_urls(content)
|
||||
|
||||
content.gsub!(/(["'])https?:\/\/web\.archive\.org\/web\/[0-9]+(?:id_)?\/([^"']+)(["'])/i) do
|
||||
quote_start, url, quote_end = $1, $2, $3
|
||||
|
||||
if url.start_with?('http')
|
||||
begin
|
||||
uri = URI.parse(url)
|
||||
path = uri.path
|
||||
path = path[1..-1] if path.start_with?('/')
|
||||
"#{quote_start}#{path}#{quote_end}"
|
||||
rescue
|
||||
"#{quote_start}#{url}#{quote_end}"
|
||||
end
|
||||
elsif url.start_with?('/')
|
||||
"#{quote_start}./#{url[1..-1]}#{quote_end}"
|
||||
else
|
||||
"#{quote_start}#{url}#{quote_end}"
|
||||
end
|
||||
end
|
||||
|
||||
content
|
||||
end
|
||||
@@ -1,17 +1,15 @@
|
||||
require './lib/wayback_machine_downloader'
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "wayback_machine_downloader"
|
||||
s.version = WaybackMachineDownloader::VERSION
|
||||
s.name = "wayback_machine_downloader_straw"
|
||||
s.version = "2.4.4"
|
||||
s.executables << "wayback_machine_downloader"
|
||||
s.summary = "Download an entire website from the Wayback Machine."
|
||||
s.description = "Download an entire website from the Wayback Machine. Wayback Machine by Internet Archive (archive.org) is an awesome tool to view any website at any point of time but lacks an export feature. Wayback Machine Downloader brings exactly this."
|
||||
s.authors = ["hartator"]
|
||||
s.email = "hartator@gmail.com"
|
||||
s.files = ["lib/wayback_machine_downloader.rb", "lib/wayback_machine_downloader/tidy_bytes.rb", "lib/wayback_machine_downloader/to_regex.rb", "lib/wayback_machine_downloader/archive_api.rb"]
|
||||
s.homepage = "https://github.com/hartator/wayback-machine-downloader"
|
||||
s.description = "Download complete websites from the Internet Archive's Wayback Machine. While the Wayback Machine (archive.org) excellently preserves web history, it lacks a built-in export functionality; this gem does just that, allowing you to download entire archived websites. (This is a significant rewrite of the original wayback_machine_downloader gem by hartator, with enhanced features and performance improvements.)"
|
||||
s.authors = ["strawberrymaster"]
|
||||
s.email = "strawberrymaster@vivaldi.net"
|
||||
s.files = ["lib/wayback_machine_downloader.rb", "lib/wayback_machine_downloader/tidy_bytes.rb", "lib/wayback_machine_downloader/to_regex.rb", "lib/wayback_machine_downloader/archive_api.rb", "lib/wayback_machine_downloader/subdom_processor.rb", "lib/wayback_machine_downloader/url_rewrite.rb"]
|
||||
s.homepage = "https://github.com/StrawberryMaster/wayback-machine-downloader"
|
||||
s.license = "MIT"
|
||||
s.required_ruby_version = ">= 1.9.2"
|
||||
s.required_ruby_version = ">= 3.4.3"
|
||||
s.add_runtime_dependency "concurrent-ruby", "~> 1.3", ">= 1.3.4"
|
||||
s.add_development_dependency "rake", "~> 12.2"
|
||||
s.add_development_dependency "minitest", "~> 5.2"
|
||||
|
||||
Reference in New Issue
Block a user