pywaybackup 0.6.3__tar.gz → 0.7.1__tar.gz

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.
Files changed (25) hide show
  1. {pywaybackup-0.6.3/pywaybackup.egg-info → pywaybackup-0.7.1}/PKG-INFO +9 -10
  2. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/README.md +8 -9
  3. pywaybackup-0.7.1/pywaybackup/SnapshotCollection.py +110 -0
  4. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/pywaybackup/Verbosity.py +13 -8
  5. pywaybackup-0.7.1/pywaybackup/__version__.py +1 -0
  6. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/pywaybackup/archive.py +111 -124
  7. pywaybackup-0.6.3/pywaybackup/main.py → pywaybackup-0.7.1/pywaybackup/arguments.py +7 -29
  8. pywaybackup-0.7.1/pywaybackup/main.py +31 -0
  9. {pywaybackup-0.6.3 → pywaybackup-0.7.1/pywaybackup.egg-info}/PKG-INFO +9 -10
  10. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/pywaybackup.egg-info/SOURCES.txt +1 -0
  11. pywaybackup-0.7.1/requirements.txt +3 -0
  12. pywaybackup-0.6.3/pywaybackup/SnapshotCollection.py +0 -83
  13. pywaybackup-0.6.3/pywaybackup/__version__.py +0 -1
  14. pywaybackup-0.6.3/requirements.txt +0 -2
  15. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/.gitignore +0 -0
  16. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/LICENSE +0 -0
  17. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/dev/pip_build.sh +0 -0
  18. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/dev/venv_create.sh +0 -0
  19. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/pywaybackup/__init__.py +0 -0
  20. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/pywaybackup.egg-info/dependency_links.txt +0 -0
  21. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/pywaybackup.egg-info/entry_points.txt +0 -0
  22. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/pywaybackup.egg-info/requires.txt +0 -0
  23. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/pywaybackup.egg-info/top_level.txt +0 -0
  24. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/setup.cfg +0 -0
  25. {pywaybackup-0.6.3 → pywaybackup-0.7.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pywaybackup
3
- Version: 0.6.3
3
+ Version: 0.7.1
4
4
  Summary: Download snapshots from the Wayback Machine
5
5
  Home-page: https://github.com/bitdruid/python-wayback-machine-downloader
6
6
  Author: bitdruid
@@ -14,7 +14,7 @@ License-File: LICENSE
14
14
 
15
15
  [![PyPI](https://img.shields.io/pypi/v/pywaybackup)](https://pypi.org/project/pywaybackup/)
16
16
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/pywaybackup)](https://pypi.org/project/pywaybackup/)
17
- ![Release](https://img.shields.io/badge/Release-alpha-red)
17
+ ![Release](https://img.shields.io/badge/Release-beta-orange)
18
18
  ![Python Version](https://img.shields.io/badge/Python-3.6-blue)
19
19
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
20
20
 
@@ -43,6 +43,8 @@ Internet-archive is a nice source for several OSINT-information. This script is
43
43
 
44
44
  This script allows you to download content from the Wayback Machine (archive.org). You can use it to download either the latest version or all versions of web page snapshots within a specified range.
45
45
 
46
+ <!-- ## Info -->
47
+
46
48
  ### Arguments
47
49
 
48
50
  - `-h`, `--help`: Show the help message and exit.
@@ -54,8 +56,8 @@ This script allows you to download content from the Wayback Machine (archive.org
54
56
 
55
57
  #### Mode Selection (Choose One)
56
58
 
57
- - `-c`, `--current`: Download the latest version of each file snapshot.
58
- - `-f`, `--full`: Download snapshots of all timestamps.
59
+ - `-c`, `--current`: Download the latest version of each file snapshot. You will get a rebuild of the current website with all available files.
60
+ - `-f`, `--full`: Download snapshots of all timestamps. You will get a folder per timestamp with the files available at that time.
59
61
  - `-s`, `--save`: Save a page to the Wayback Machine. (beta)
60
62
 
61
63
  #### Optional Arguments
@@ -73,6 +75,7 @@ Specify the range in years or a specific timestamp either start, end or both. If
73
75
 
74
76
  #### Additional
75
77
 
78
+ - `--no-redirect`: Do not follow redirects of snapshots. Archive.org sometimes redirects to a different snapshot for several reasons. Downloading redirects may lead to timestamp-folders which contain some files with a different timestamp. This does not matter if you only want to download the latest version (`-c`).
76
79
  - `--verbosity [LEVEL]`: Set the verbosity: json (print json response), progress (show progress bar) or standard (default).
77
80
  - `--retry [RETRY_FAILED]`: Retry failed downloads. You can specify the number of retry attempts as an integer.
78
81
  - `--worker [AMOUNT]`: The number of worker to use for downloading (simultaneous downloads). Default is 1. Beware: Using too many worker will lead into refused connections from the Wayback Machine. Duration about 1.5 minutes.
@@ -85,8 +88,8 @@ Download latest snapshot of all files:<br>
85
88
  Download latest snapshot of all files with retries:<br>
86
89
  `waybackup -u http://example.com -c --retry 3`
87
90
 
88
- Download all snapshots sorted per timestamp with a specified range:<br>
89
- `waybackup -u http://example.com -f -r 5`
91
+ Download all snapshots sorted per timestamp with a specified range and follow redirects:<br>
92
+ `waybackup -u http://example.com -f -r 5 --redirect`
90
93
 
91
94
  Download all snapshots sorted per timestamp with a specified range and save to a specified folder with 3 worker:<br>
92
95
  `waybackup -u http://example.com -f -r 5 -o /home/user/Downloads/snapshots --worker 3`
@@ -94,10 +97,6 @@ Download all snapshots sorted per timestamp with a specified range and save to a
94
97
  List available snapshots per timestamp without downloading:<br>
95
98
  `waybackup -u http://example.com -f -l`
96
99
 
97
- ## Info
98
-
99
- The script automatically follows redirects of snapshots.
100
-
101
100
  ## Contributing
102
101
 
103
102
  I'm always happy for some feature requests to improve the usability of this script.
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![PyPI](https://img.shields.io/pypi/v/pywaybackup)](https://pypi.org/project/pywaybackup/)
4
4
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/pywaybackup)](https://pypi.org/project/pywaybackup/)
5
- ![Release](https://img.shields.io/badge/Release-alpha-red)
5
+ ![Release](https://img.shields.io/badge/Release-beta-orange)
6
6
  ![Python Version](https://img.shields.io/badge/Python-3.6-blue)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
 
@@ -31,6 +31,8 @@ Internet-archive is a nice source for several OSINT-information. This script is
31
31
 
32
32
  This script allows you to download content from the Wayback Machine (archive.org). You can use it to download either the latest version or all versions of web page snapshots within a specified range.
33
33
 
34
+ <!-- ## Info -->
35
+
34
36
  ### Arguments
35
37
 
36
38
  - `-h`, `--help`: Show the help message and exit.
@@ -42,8 +44,8 @@ This script allows you to download content from the Wayback Machine (archive.org
42
44
 
43
45
  #### Mode Selection (Choose One)
44
46
 
45
- - `-c`, `--current`: Download the latest version of each file snapshot.
46
- - `-f`, `--full`: Download snapshots of all timestamps.
47
+ - `-c`, `--current`: Download the latest version of each file snapshot. You will get a rebuild of the current website with all available files.
48
+ - `-f`, `--full`: Download snapshots of all timestamps. You will get a folder per timestamp with the files available at that time.
47
49
  - `-s`, `--save`: Save a page to the Wayback Machine. (beta)
48
50
 
49
51
  #### Optional Arguments
@@ -61,6 +63,7 @@ Specify the range in years or a specific timestamp either start, end or both. If
61
63
 
62
64
  #### Additional
63
65
 
66
+ - `--no-redirect`: Do not follow redirects of snapshots. Archive.org sometimes redirects to a different snapshot for several reasons. Downloading redirects may lead to timestamp-folders which contain some files with a different timestamp. This does not matter if you only want to download the latest version (`-c`).
64
67
  - `--verbosity [LEVEL]`: Set the verbosity: json (print json response), progress (show progress bar) or standard (default).
65
68
  - `--retry [RETRY_FAILED]`: Retry failed downloads. You can specify the number of retry attempts as an integer.
66
69
  - `--worker [AMOUNT]`: The number of worker to use for downloading (simultaneous downloads). Default is 1. Beware: Using too many worker will lead into refused connections from the Wayback Machine. Duration about 1.5 minutes.
@@ -73,8 +76,8 @@ Download latest snapshot of all files:<br>
73
76
  Download latest snapshot of all files with retries:<br>
74
77
  `waybackup -u http://example.com -c --retry 3`
75
78
 
76
- Download all snapshots sorted per timestamp with a specified range:<br>
77
- `waybackup -u http://example.com -f -r 5`
79
+ Download all snapshots sorted per timestamp with a specified range and follow redirects:<br>
80
+ `waybackup -u http://example.com -f -r 5 --redirect`
78
81
 
79
82
  Download all snapshots sorted per timestamp with a specified range and save to a specified folder with 3 worker:<br>
80
83
  `waybackup -u http://example.com -f -r 5 -o /home/user/Downloads/snapshots --worker 3`
@@ -82,10 +85,6 @@ Download all snapshots sorted per timestamp with a specified range and save to a
82
85
  List available snapshots per timestamp without downloading:<br>
83
86
  `waybackup -u http://example.com -f -l`
84
87
 
85
- ## Info
86
-
87
- The script automatically follows redirects of snapshots.
88
-
89
88
  ## Contributing
90
89
 
91
90
  I'm always happy for some feature requests to improve the usability of this script.
@@ -0,0 +1,110 @@
1
+ from urllib.parse import urlparse
2
+ import os
3
+
4
+ class SnapshotCollection:
5
+
6
+ CDX_LIST = []
7
+ SNAPSHOT_COLLECTION = []
8
+ MODE_CURRENT = 0
9
+
10
+ @classmethod
11
+ def create_list_full(cls, cdxResult):
12
+ cls.CDX_LIST = sorted([{"timestamp": snapshot[0], "url": snapshot[1], "status": snapshot[2], "mimetype": snapshot[3], "digest": snapshot[4]} for i, snapshot in enumerate(cdxResult.json()[1:])], key=lambda k: k['timestamp'], reverse=True)
13
+
14
+ @classmethod
15
+ def create_list_current(cls):
16
+ cls.MODE_CURRENT = 1
17
+ cdxResult_list_filtered = []
18
+ url_set = set()
19
+ for snapshot in cls.CDX_LIST:
20
+ if snapshot["url"] not in url_set:
21
+ cdxResult_list_filtered.append(snapshot)
22
+ url_set.add(snapshot["url"])
23
+ cls.CDX_LIST = cdxResult_list_filtered
24
+
25
+ @classmethod
26
+ def count_list(cls):
27
+ return len(cls.CDX_LIST)
28
+
29
+ @classmethod
30
+ def create_collection(cls):
31
+ for cdx_entry in cls.CDX_LIST:
32
+ timestamp, url = cdx_entry["timestamp"], cdx_entry["url"]
33
+ url_archive = f"http://web.archive.org/web/{timestamp}{cls._url_get_filetype(url)}/{url}"
34
+ collection_entry = {
35
+ "id": len(cls.SNAPSHOT_COLLECTION),
36
+ "timestamp": timestamp,
37
+ "url_archive": url_archive,
38
+ "url_origin": url,
39
+ "file": False,
40
+ "redirect": False,
41
+ "response": False
42
+ }
43
+ cls.SNAPSHOT_COLLECTION.append(collection_entry)
44
+
45
+ @classmethod
46
+ def snapshot_entry_create_output(cls, collection_entry: dict, output: str) -> str:
47
+ """
48
+ Create the output path for a snapshot entry of the collection according to the mode.
49
+
50
+ Input:
51
+ - collection_entry: A single snapshot entry of the collection (dict).
52
+ - output: The output directory (str).
53
+
54
+ Output:
55
+ - download_file: The output path for the snapshot entry (str) with filename.
56
+ """
57
+ timestamp, url = collection_entry["timestamp"], collection_entry["url_origin"]
58
+ domain, subdir, filename = cls._url_split(url)
59
+ if cls.MODE_CURRENT:
60
+ download_dir = os.path.join(output, domain, subdir)
61
+ else:
62
+ download_dir = os.path.join(output, domain, timestamp, subdir)
63
+ download_file = os.path.join(download_dir, filename)
64
+ return download_file
65
+
66
+ @classmethod
67
+ def snapshot_entry_modify(cls, collection_entry: dict, key: str, value: str):
68
+ """
69
+ Modify a key-value pair in a snapshot entry of the collection (dict).
70
+
71
+ - Append a new key-value pair if the key does not exist.
72
+ - Modify an existing key-value pair if the key exists.
73
+ """
74
+ collection_entry[key] = value
75
+
76
+ @classmethod
77
+ def url_get_timestamp(cls, url):
78
+ """
79
+ Extract the timestamp from a wayback machine URL.
80
+ """
81
+ timestamp = url.split("web.archive.org/web/")[1].split("/")[0]
82
+ timestamp = ''.join([char for char in timestamp if char.isdigit()])
83
+ return timestamp
84
+
85
+ @classmethod
86
+ def _url_get_filetype(cls, url):
87
+ file_extension = os.path.splitext(url)[1][1:]
88
+ urltype_mapping = {
89
+ "jpg": "im_",
90
+ "jpeg": "im_",
91
+ "png": "im_",
92
+ "gif": "im_",
93
+ "svg": "im_",
94
+ "ico": "im_",
95
+ "css": "cs_"
96
+ #"js": "js_"
97
+ }
98
+ urltype = urltype_mapping.get(file_extension, "id_")
99
+ return urltype
100
+
101
+ @classmethod
102
+ def _url_split(cls, url):
103
+ """
104
+ Split a URL into domain, subdir and filename.
105
+ """
106
+ parsed_url = urlparse(url)
107
+ domain = parsed_url.netloc
108
+ subdir = parsed_url.path.strip("/").rsplit("/", 1)[0]
109
+ filename = parsed_url.path.split("/")[-1] or "index.html"
110
+ return domain, subdir, filename
@@ -1,7 +1,6 @@
1
1
  import tqdm
2
2
  import json
3
- import time
4
- import pywaybackup.SnapshotCollection as sc
3
+ from pywaybackup.SnapshotCollection import SnapshotCollection as sc
5
4
 
6
5
  class Verbosity:
7
6
 
@@ -11,8 +10,7 @@ class Verbosity:
11
10
  pbar = None
12
11
 
13
12
  @classmethod
14
- def open(cls, args: list, snapshots: sc.SnapshotCollection):
15
- cls.snapshots = snapshots
13
+ def open(cls, args: list):
16
14
  cls.args = args
17
15
  if cls.args == "progress":
18
16
  cls.mode = "progress"
@@ -23,16 +21,23 @@ class Verbosity:
23
21
 
24
22
  @classmethod
25
23
  def close(cls):
26
- if cls.mode == "json":
27
- print(json.dumps(cls.snapshots.SNAPSHOT_COLLECTION, indent=4, sort_keys=True))
28
- elif cls.mode == "standard":
24
+ if cls.mode == "progress":
25
+ cls.pbar.close()
26
+ if cls.mode == "progress" or cls.mode == "standard":
27
+ successed = len([snapshot for snapshot in sc.SNAPSHOT_COLLECTION if snapshot["file"]])
28
+ failed = len([snapshot for snapshot in sc.SNAPSHOT_COLLECTION if not snapshot["file"]])
29
+ print(f"\nFiles downloaded: {successed}")
30
+ print(f"Files missing: {failed}")
29
31
  print("")
32
+ if cls.mode == "json":
33
+ print(json.dumps(sc.SNAPSHOT_COLLECTION, indent=4, sort_keys=True))
30
34
 
31
35
  @classmethod
32
36
  def write(cls, message: str = None, progress: int = None):
33
37
  if cls.mode == "progress":
34
38
  if progress == 0:
35
- maxval = cls.snapshots.count_list()
39
+ print("")
40
+ maxval = sc.count_list()
36
41
  cls.pbar = tqdm.tqdm(total=maxval, desc="Downloading", unit=" snapshot", ascii="░▒█")
37
42
  elif progress == 1:
38
43
  cls.pbar.update(1)
@@ -0,0 +1 @@
1
+ __version__ = "0.7.1"
@@ -1,4 +1,3 @@
1
- #import threading
2
1
  import requests
3
2
  import os
4
3
  import gzip
@@ -8,7 +7,8 @@ import http.client
8
7
  from urllib.parse import urljoin
9
8
  from datetime import datetime, timezone
10
9
 
11
- import pywaybackup.SnapshotCollection as sc
10
+ from pywaybackup.SnapshotCollection import SnapshotCollection as sc
11
+
12
12
  from pywaybackup.Verbosity import Verbosity as v
13
13
 
14
14
 
@@ -42,7 +42,7 @@ def save_page(url: str):
42
42
  if response_status == 302:
43
43
  location = response.getheader("Location")
44
44
  v.write("\n-----> Response: 302 (redirect to snapshot)")
45
- snapshot_timestamp = datetime.strptime(location.split('/web/')[1].split('/')[0], '%Y%m%d%H%M%S').strftime('%Y-%m-%d %H:%M:%S')
45
+ snapshot_timestamp = datetime.strptime(sc.url_get_timestamp(location), '%Y%m%d%H%M%S').strftime('%Y-%m-%d %H:%M:%S')
46
46
  current_timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
47
47
  timestamp_difference = (datetime.strptime(current_timestamp, '%Y-%m-%d %H:%M:%S') - datetime.strptime(snapshot_timestamp, '%Y-%m-%d %H:%M:%S')).seconds / 60
48
48
  timestamp_difference = int(round(timestamp_difference, 0))
@@ -70,13 +70,14 @@ def save_page(url: str):
70
70
 
71
71
 
72
72
 
73
- def print_list(snapshots):
73
+ def print_list():
74
74
  v.write("")
75
- if not snapshots:
76
- v.write("No snapshots found")
75
+ count = sc.count_list()
76
+ if count == 0:
77
+ v.write("\nNo snapshots found")
77
78
  else:
78
- __import__('pprint').pprint(snapshots.CDX_LIST)
79
- v.write(f"\n-----> {snapshots.count_list()} snapshots listed")
79
+ __import__('pprint').pprint(sc.CDX_LIST)
80
+ v.write(f"\n-----> {count} snapshots listed")
80
81
 
81
82
 
82
83
 
@@ -84,20 +85,21 @@ def print_list(snapshots):
84
85
 
85
86
  # create filelist
86
87
  # timestamp format yyyyMMddhhmmss
87
- def query_list(snapshots: sc.SnapshotCollection, url: str, range: int, start: int, end: int, explicit: bool, mode: str):
88
+ def query_list(url: str, range: int, start: int, end: int, explicit: bool, mode: str):
88
89
  try:
89
90
  v.write("\nQuerying snapshots...")
90
- range = ""
91
+ query_range = ""
91
92
  if not range:
92
- if start: range = range + f"&from={start}"
93
- if end: range = range + f"&to={end}"
94
- else: range = "&from=" + str(datetime.now().year - range)
93
+ if start: query_range = query_range + f"&from={start}"
94
+ if end: query_range = query_range + f"&to={end}"
95
+ else:
96
+ query_range = "&from=" + str(datetime.now().year - range)
95
97
  cdx_url = f"*.{url}/*" if not explicit else f"{url}"
96
- cdxQuery = f"https://web.archive.org/cdx/search/xd?output=json&url={cdx_url}{range}&fl=timestamp,original&filter=!statuscode:200"
98
+ cdxQuery = f"https://web.archive.org/cdx/search/xd?output=json&url={cdx_url}{query_range}&fl=timestamp,original,statuscode,mimetype,digest&filter!=statuscode:200"
97
99
  cdxResult = requests.get(cdxQuery)
98
- snapshots.create_full(cdxResult)
99
- if mode == "current": snapshots.create_current()
100
- v.write(f"\n-----> {snapshots.count_list()} snapshots found")
100
+ sc.create_list_full(cdxResult)
101
+ sc.create_list_current() if mode == "current" else None
102
+ v.write(f"\n-----> {sc.count_list()} snapshots found")
101
103
  except requests.exceptions.ConnectionError as e:
102
104
  v.write(f"\n-----> ERROR: could not query snapshots:\n{e}"); exit()
103
105
 
@@ -105,108 +107,68 @@ def query_list(snapshots: sc.SnapshotCollection, url: str, range: int, start: in
105
107
 
106
108
 
107
109
 
108
-
109
- def remove_empty_folders(path, remove_root=True):
110
- v.write("")
111
- v.write("Removing empty output folders...")
112
- count = 0
113
- if not os.path.isdir(path):
114
- return
115
- # remove empty subfolders
116
- for root, dirs, files in os.walk(path, topdown=False):
117
- for dir in dirs:
118
- dir_path = os.path.join(root, dir)
119
- if not os.listdir(dir_path):
120
- try:
121
- os.rmdir(dir_path)
122
- v.write(f"-----> {dir_path}")
123
- count += 1
124
- except OSError as e:
125
- v.write(f"Error removing {dir_path}: {e}")
126
- # remove empty root folder
127
- if remove_root and not os.listdir(path):
128
- try:
129
- os.rmdir(path)
130
- v.write(f"-----> {path}")
131
- count += 1
132
- except OSError as e:
133
- v.write(f"Error removing {path}: {e}")
134
- if count == 0:
135
- v.write("No empty folders found")
136
-
137
-
138
-
139
-
140
-
141
110
  # example download: http://web.archive.org/web/20190815104545id_/https://www.google.com/
142
- def download_list(snapshots, output, retry, worker):
111
+ def download_list(output, retry, no_redirect, worker):
143
112
  """
144
113
  Download a list of urls in format: [{"timestamp": "20190815104545", "url": "https://www.google.com/"}]
145
114
  """
146
- if snapshots.count_list() == 0:
147
- v.write("\nNo snapshots found to download")
115
+ if sc.count_list() == 0:
116
+ v.write("\nNothing to download");
148
117
  return
149
- v.write("\nDownloading latest snapshots of each file...", progress=0)
150
- download_list = snapshots.CDX_LIST
118
+ v.write("\nDownloading snapshots...", progress=0)
151
119
  if worker > 1:
152
120
  v.write(f"\n-----> Simultaneous downloads: {worker}")
153
- batch_size = snapshots.count_list() // worker + 1
121
+ batch_size = sc.count_list() // worker + 1
154
122
  else:
155
- batch_size = snapshots.count_list()
156
- batch_list = [download_list[i:i + batch_size] for i in range(0, len(download_list), batch_size)]
123
+ batch_size = sc.count_list()
124
+ sc.create_collection()
125
+ batch_list = [sc.SNAPSHOT_COLLECTION[i:i + batch_size] for i in range(0, len(sc.SNAPSHOT_COLLECTION), batch_size)]
157
126
  threads = []
158
127
  worker = 0
159
128
  for batch in batch_list:
160
129
  worker += 1
161
- thread = threading.Thread(target=download_loop, args=(snapshots, batch, output, worker, retry))
130
+ thread = threading.Thread(target=download_loop, args=(batch, output, worker, retry, no_redirect))
162
131
  threads.append(thread)
163
132
  thread.start()
164
133
  for thread in threads:
165
134
  thread.join()
166
- failed_urls = len([url for url in snapshots.SNAPSHOT_COLLECTION if not url["success"]])
167
- if failed_urls: v.write(f"\n-----> Failed downloads: {failed_urls}")
168
135
 
169
- def download_loop(snapshots, cdx_list, output, worker, retry, attempt=1, connection=None):
136
+ def download_loop(snapshot_batch, output, worker, retry, no_redirect, attempt=1, connection=None):
170
137
  """
171
138
  Download a list of URLs in a recursive loop. If a download fails, the function will retry the download.
172
139
  The "snapshot_collection" dictionary will be updated with the download status and file information.
173
- Information for each entry is written by "create_entry" and "snapshot_collection_write" functions.
140
+ Information for each entry is written by "create_entry" and "snapshot_dict_append" functions.
174
141
  """
175
- max_attempt = retry + 1
142
+ max_attempt = retry if retry > 0 else retry + 1
176
143
  failed_urls = []
177
144
  if not connection:
178
145
  connection = http.client.HTTPSConnection("web.archive.org")
179
- if attempt > max_attempt:
146
+ if attempt > max_attempt:
180
147
  connection.close()
181
- v.write(f"\n-----> Worker: {worker} - Failed downloads: {len(cdx_list)}")
148
+ v.write(f"\n-----> Worker: {worker} - Failed downloads: {len(snapshot_batch)}")
182
149
  return
183
- else:
184
- for cdx_entry in cdx_list:
185
- status = f"\n-----> Attempt: [{attempt}/{max_attempt}] Snapshot [{cdx_list.index(cdx_entry)+1}/{len(cdx_list)}] - Worker: {worker}"
186
- download_entry = snapshots.create_entry(cdx_entry, output)
187
- snapshots.snapshot_collection_write(download_entry)
188
- download_status=download(download_entry, connection, status)
189
- if not download_status:
190
- snapshots.snapshot_collection_update(download_entry["id"], "success", False)
191
- snapshots.snapshot_collection_update(download_entry["id"], "file", None)
192
- snapshots.snapshot_collection_update(download_entry["id"], "retry", attempt)
193
- failed_urls.append(cdx_entry);
194
- if download_status:
195
- snapshots.snapshot_collection_update(download_entry["id"], "success", True)
196
- snapshots.snapshot_collection_update(download_entry["id"], "file", download_entry["file"])
197
- v.write(progress=1)
198
- attempt += 1
199
- if failed_urls: download_loop(snapshots, failed_urls, output, worker, retry, attempt, connection)
200
-
201
- def download(download_entry, connection, status_message):
150
+ for snapshot in snapshot_batch:
151
+ status = f"\n-----> Attempt: [{attempt}/{max_attempt}] Snapshot [{snapshot_batch.index(snapshot)+1}/{len(snapshot_batch)}] - Worker: {worker}"
152
+ download_status = download(output, snapshot, connection, status, no_redirect)
153
+ if not download_status:
154
+ failed_urls.append(snapshot)
155
+ if download_status:
156
+ v.write(progress=1)
157
+ attempt += 1
158
+ if failed_urls:
159
+ if not attempt > max_attempt:
160
+ v.write(f"\n-----> Worker: {worker} - Retry Timeout: 10 seconds")
161
+ time.sleep(15)
162
+ download_loop(failed_urls, output, worker, retry, no_redirect, attempt, connection)
163
+
164
+ def download(output, snapshot_entry, connection, status_message, no_redirect=False):
202
165
  """
203
166
  Download a single URL and save it to the specified filepath.
204
167
  If there is a redirect, the function will follow the redirect and update the download URL.
205
168
  gzip decompression is used if the response is encoded.
206
169
  According to the response status, the function will write a status message to the console and append a failed URL.
207
170
  """
208
- download_url = download_entry["url"]
209
- download_file = download_entry["file"]
171
+ download_url = snapshot_entry["url_archive"]
210
172
  max_retries = 2
211
173
  sleep_time = 45
212
174
  headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'}
@@ -216,23 +178,31 @@ def download(download_entry, connection, status_message):
216
178
  response = connection.getresponse()
217
179
  response_data = response.read()
218
180
  response_status = response.status
219
- if response_status == 302:
220
- status_message = f"{status_message}\n" + \
221
- f"REDIRECT -> HTTP: {response.status}"
222
- while response_status == 302:
223
- connection.request("GET", download_url, headers=headers)
224
- response = connection.getresponse()
225
- response_data = response.read()
226
- response_status = response.status
227
- location = response.getheader("Location")
228
- if location:
229
- status_message = f"{status_message}\n" + \
230
- f" -> URL: {location}"
231
- location = urljoin(download_url, location)
232
- download_url = location
233
- else:
234
- break
235
- if response_status != 404:
181
+ response_status_message = parse_response_code(response_status)
182
+ sc.snapshot_entry_modify(snapshot_entry, "response", response_status)
183
+ if not no_redirect:
184
+ if response_status == 302:
185
+ status_message = f"{status_message}\n" + \
186
+ f"REDIRECT -> HTTP: {response.status}"
187
+ while response_status == 302:
188
+ connection.request("GET", download_url, headers=headers)
189
+ response = connection.getresponse()
190
+ response_data = response.read()
191
+ response_status = response.status
192
+ location = response.getheader("Location")
193
+ if location:
194
+ status_message = f"{status_message}\n" + \
195
+ f" -> URL: {location}"
196
+ location = urljoin(download_url, location)
197
+ download_url = location
198
+ sc.snapshot_entry_modify(snapshot_entry, "redirect", True)
199
+ sc.snapshot_entry_modify(snapshot_entry, "redirect_timestamp", sc.url_get_timestamp(location))
200
+ sc.snapshot_entry_modify(snapshot_entry, "redirect_url", location)
201
+ else:
202
+ break
203
+ if response_status == 200:
204
+ sc.snapshot_entry_modify(snapshot_entry, "file", sc.snapshot_entry_create_output(snapshot_entry, output))
205
+ download_file = snapshot_entry["file"]
236
206
  os.makedirs(os.path.dirname(download_file), exist_ok=True)
237
207
  with open(download_file, 'wb') as file:
238
208
  if response.getheader('Content-Encoding') == 'gzip':
@@ -240,33 +210,50 @@ def download(download_entry, connection, status_message):
240
210
  file.write(response_data)
241
211
  else:
242
212
  file.write(response_data)
243
- if response_status == 200:
244
- status_message = f"{status_message}\n" + \
245
- f"SUCCESS -> HTTP: {response.status}\n" + \
246
- f" -> URL: {download_url}\n" + \
247
- f" -> FILE: {download_file}"
248
- elif response_status == 404:
249
- status_message = f"{status_message}\n" + \
250
- f"NOT FOUND -> HTTP: {response.status}\n" + \
251
- f" -> URL: {download_url}"
213
+ if os.path.isfile(download_file):
214
+ status_message = f"{status_message}\n" + \
215
+ f"SUCCESS -> HTTP: {response_status} - {response_status_message}\n" + \
216
+ f" -> URL: {download_url}\n" + \
217
+ f" -> FILE: {download_file}"
218
+ v.write(status_message)
219
+ return True
252
220
  else:
253
221
  status_message = f"{status_message}\n" + \
254
- f"UNEXPECTED -> HTTP: {response.status}\n" + \
255
- f" -> URL: {download_url}\n" + \
256
- f" -> FILE: {download_file}"
222
+ f"UNEXPECTED -> HTTP: {response_status} - {response_status_message}\n" + \
223
+ f" -> URL: {download_url}\n"
224
+ v.write(status_message)
225
+ return True
226
+ # exception returns false and appends the url to the failed list
227
+ except http.client.HTTPException as e:
228
+ status_message = f"{status_message}\n" + \
229
+ f"EXCEPTION -> ({i+1}/{max_retries}), append to failed_urls: {download_url}\n" + \
230
+ f" -> {e}"
257
231
  v.write(status_message)
258
- return True
232
+ return False
233
+ # connection refused waits and retries
259
234
  except ConnectionRefusedError as e:
260
235
  status_message = f"{status_message}\n" + \
261
236
  f"REFUSED -> ({i+1}/{max_retries}), reconnect in {sleep_time} seconds...\n" + \
262
237
  f" -> {e}"
263
238
  v.write(status_message)
264
239
  time.sleep(sleep_time)
265
- except http.client.HTTPException as e:
266
- status_message = f"{status_message}\n" + \
267
- f"EXCEPTION -> ({i+1}/{max_retries}), append to failed_urls: {download_url}\n" + \
268
- f" -> {e}"
269
- v.write(status_message)
270
- return False
271
240
  v.write(f"FAILED -> download, append to failed_urls: {download_url}")
272
- return False
241
+ return False
242
+
243
+ RESPONSE_CODE_DICT = {
244
+ 200: "OK",
245
+ 301: "Moved Permanently",
246
+ 302: "Found (redirect)",
247
+ 400: "Bad Request",
248
+ 403: "Forbidden",
249
+ 404: "Not Found",
250
+ 500: "Internal Server Error",
251
+ 503: "Service Unavailable"
252
+ }
253
+ def parse_response_code(response_code: int):
254
+ """
255
+ Parse the response code of the Wayback Machine and return a human-readable message.
256
+ """
257
+ if response_code in RESPONSE_CODE_DICT:
258
+ return RESPONSE_CODE_DICT[response_code]
259
+ return "Unknown response code"
@@ -1,21 +1,18 @@
1
- import pywaybackup.archive as archive
2
- import pywaybackup.SnapshotCollection as sc
3
1
  import argparse
4
- import os
5
-
6
- from pywaybackup.Verbosity import Verbosity as v
7
-
8
2
  from pywaybackup.__version__ import __version__
9
3
 
10
- def main():
4
+ def parse():
5
+
11
6
  parser = argparse.ArgumentParser(description='Download from wayback machine (archive.org)')
12
7
  parser.add_argument('-a', '--about', action='version', version='%(prog)s ' + __version__ + ' by @bitdruid -> https://github.com/bitdruid')
8
+
13
9
  required = parser.add_argument_group('required')
14
10
  required.add_argument('-u', '--url', type=str, help='URL to use')
15
11
  exclusive_required = required.add_mutually_exclusive_group(required=True)
16
12
  exclusive_required.add_argument('-c', '--current', action='store_true', help='Download the latest version of each file snapshot (opt range in y)')
17
13
  exclusive_required.add_argument('-f', '--full', action='store_true', help='Download snapshots of all timestamps (opt range in y)')
18
14
  exclusive_required.add_argument('-s', '--save', action='store_true', help='Save a page to the wayback machine')
15
+
19
16
  optional = parser.add_argument_group('optional')
20
17
  optional.add_argument('-l', '--list', action='store_true', help='Only print snapshots (opt range in y)')
21
18
  optional.add_argument('-e', '--explicit', action='store_true', help='Search only for the explicit given url')
@@ -23,32 +20,13 @@ def main():
23
20
  optional.add_argument('-r', '--range', type=int, help='Range in years to search')
24
21
  optional.add_argument('--start', type=int, help='Start timestamp format: YYYYMMDDhhmmss')
25
22
  optional.add_argument('--end', type=int, help='End timestamp format: YYYYMMDDhhmmss')
23
+
26
24
  special = parser.add_argument_group('special')
25
+ special.add_argument('--no-redirect', action='store_true', help='Do not follow redirects by archive.org')
27
26
  special.add_argument('--verbosity', type=str, default="standard", choices=["standard", "progress", "json"], help='Verbosity level')
28
27
  special.add_argument('--retry', type=int, default=0, metavar="X-TIMES", help='Retry failed downloads (opt tries as int, else infinite)')
29
28
  special.add_argument('--worker', type=int, default=1, metavar="AMOUNT", help='Number of worker (simultaneous downloads)')
30
29
 
31
30
  args = parser.parse_args()
32
- snapshots = sc.SnapshotCollection()
33
- v.open(args.verbosity, snapshots)
34
-
35
- if args.current:
36
- mode = "current"
37
- elif args.full:
38
- mode = "full"
39
-
40
- if args.save:
41
- archive.save_page(args.url)
42
- else:
43
- if args.output is None:
44
- args.output = os.path.join(os.getcwd(), "waybackup_snapshots")
45
- archive.query_list(snapshots, args.url, args.range, args.start, args.end, args.explicit, mode)
46
- if args.list:
47
- archive.print_list(snapshots)
48
- else:
49
- archive.download_list(snapshots, args.output, args.retry, args.worker)
50
- archive.remove_empty_folders(args.output)
51
- v.close()
52
31
 
53
- if __name__ == "__main__":
54
- main()
32
+ return args
@@ -0,0 +1,31 @@
1
+ import pywaybackup.archive as archive
2
+ import pywaybackup.SnapshotCollection as sc
3
+ import os
4
+
5
+ from pywaybackup.arguments import parse
6
+ from pywaybackup.Verbosity import Verbosity as v
7
+
8
+ def main():
9
+ args = parse()
10
+ v.open(args.verbosity)
11
+
12
+ if args.current:
13
+ mode = "current"
14
+ elif args.full:
15
+ mode = "full"
16
+
17
+ if args.save:
18
+ archive.save_page(args.url)
19
+ else:
20
+ if args.output is None:
21
+ args.output = os.path.join(os.getcwd(), "waybackup_snapshots")
22
+ archive.query_list(args.url, args.range, args.start, args.end, args.explicit, mode)
23
+ if args.list:
24
+ archive.print_list()
25
+ else:
26
+ archive.download_list(args.output, args.retry, args.no_redirect, args.worker)
27
+ #archive.remove_empty_folders(args.output)
28
+ v.close()
29
+
30
+ if __name__ == "__main__":
31
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pywaybackup
3
- Version: 0.6.3
3
+ Version: 0.7.1
4
4
  Summary: Download snapshots from the Wayback Machine
5
5
  Home-page: https://github.com/bitdruid/python-wayback-machine-downloader
6
6
  Author: bitdruid
@@ -14,7 +14,7 @@ License-File: LICENSE
14
14
 
15
15
  [![PyPI](https://img.shields.io/pypi/v/pywaybackup)](https://pypi.org/project/pywaybackup/)
16
16
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/pywaybackup)](https://pypi.org/project/pywaybackup/)
17
- ![Release](https://img.shields.io/badge/Release-alpha-red)
17
+ ![Release](https://img.shields.io/badge/Release-beta-orange)
18
18
  ![Python Version](https://img.shields.io/badge/Python-3.6-blue)
19
19
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
20
20
 
@@ -43,6 +43,8 @@ Internet-archive is a nice source for several OSINT-information. This script is
43
43
 
44
44
  This script allows you to download content from the Wayback Machine (archive.org). You can use it to download either the latest version or all versions of web page snapshots within a specified range.
45
45
 
46
+ <!-- ## Info -->
47
+
46
48
  ### Arguments
47
49
 
48
50
  - `-h`, `--help`: Show the help message and exit.
@@ -54,8 +56,8 @@ This script allows you to download content from the Wayback Machine (archive.org
54
56
 
55
57
  #### Mode Selection (Choose One)
56
58
 
57
- - `-c`, `--current`: Download the latest version of each file snapshot.
58
- - `-f`, `--full`: Download snapshots of all timestamps.
59
+ - `-c`, `--current`: Download the latest version of each file snapshot. You will get a rebuild of the current website with all available files.
60
+ - `-f`, `--full`: Download snapshots of all timestamps. You will get a folder per timestamp with the files available at that time.
59
61
  - `-s`, `--save`: Save a page to the Wayback Machine. (beta)
60
62
 
61
63
  #### Optional Arguments
@@ -73,6 +75,7 @@ Specify the range in years or a specific timestamp either start, end or both. If
73
75
 
74
76
  #### Additional
75
77
 
78
+ - `--no-redirect`: Do not follow redirects of snapshots. Archive.org sometimes redirects to a different snapshot for several reasons. Downloading redirects may lead to timestamp-folders which contain some files with a different timestamp. This does not matter if you only want to download the latest version (`-c`).
76
79
  - `--verbosity [LEVEL]`: Set the verbosity: json (print json response), progress (show progress bar) or standard (default).
77
80
  - `--retry [RETRY_FAILED]`: Retry failed downloads. You can specify the number of retry attempts as an integer.
78
81
  - `--worker [AMOUNT]`: The number of worker to use for downloading (simultaneous downloads). Default is 1. Beware: Using too many worker will lead into refused connections from the Wayback Machine. Duration about 1.5 minutes.
@@ -85,8 +88,8 @@ Download latest snapshot of all files:<br>
85
88
  Download latest snapshot of all files with retries:<br>
86
89
  `waybackup -u http://example.com -c --retry 3`
87
90
 
88
- Download all snapshots sorted per timestamp with a specified range:<br>
89
- `waybackup -u http://example.com -f -r 5`
91
+ Download all snapshots sorted per timestamp with a specified range and follow redirects:<br>
92
+ `waybackup -u http://example.com -f -r 5 --redirect`
90
93
 
91
94
  Download all snapshots sorted per timestamp with a specified range and save to a specified folder with 3 worker:<br>
92
95
  `waybackup -u http://example.com -f -r 5 -o /home/user/Downloads/snapshots --worker 3`
@@ -94,10 +97,6 @@ Download all snapshots sorted per timestamp with a specified range and save to a
94
97
  List available snapshots per timestamp without downloading:<br>
95
98
  `waybackup -u http://example.com -f -l`
96
99
 
97
- ## Info
98
-
99
- The script automatically follows redirects of snapshots.
100
-
101
100
  ## Contributing
102
101
 
103
102
  I'm always happy for some feature requests to improve the usability of this script.
@@ -10,6 +10,7 @@ pywaybackup/Verbosity.py
10
10
  pywaybackup/__init__.py
11
11
  pywaybackup/__version__.py
12
12
  pywaybackup/archive.py
13
+ pywaybackup/arguments.py
13
14
  pywaybackup/main.py
14
15
  pywaybackup.egg-info/PKG-INFO
15
16
  pywaybackup.egg-info/SOURCES.txt
@@ -0,0 +1,3 @@
1
+ requests==2.31.0
2
+ tqdm==4.66.2
3
+ #beautifulsoup4==4.12.3
@@ -1,83 +0,0 @@
1
- from urllib.parse import urlparse
2
- import os
3
-
4
- class SnapshotCollection:
5
-
6
- CDX_JSON = []
7
- CDX_LIST = []
8
-
9
- SNAPSHOT_COLLECTION = []
10
-
11
- MODE_CURRENT = 0
12
-
13
- def __init__(self):
14
- pass
15
-
16
- def create_full(self, cdxResult):
17
- self.CDX_JSON = cdxResult.json()[1:]
18
- self.CDX_LIST = [{"timestamp": snapshot[0], "url": snapshot[1]} for i, snapshot in enumerate(self.CDX_JSON)]
19
- self.CDX_LIST = sorted(self.CDX_LIST, key=lambda k: k['timestamp'], reverse=True)
20
-
21
- def create_current(self):
22
- self.MODE_CURRENT = 1
23
- self.CDX_LIST = sorted(self.CDX_LIST, key=lambda k: k['timestamp'], reverse=True)
24
- cdxResult_list_filtered = []
25
- url_set = set()
26
- for snapshot in self.CDX_LIST:
27
- if snapshot["url"] not in url_set:
28
- cdxResult_list_filtered.append(snapshot)
29
- url_set.add(snapshot["url"])
30
- self.CDX_LIST = cdxResult_list_filtered
31
-
32
- def create_entry(self, cdx_entry: dict, output: str) -> dict:
33
- timestamp, url = cdx_entry["timestamp"], cdx_entry["url"]
34
- url_type = self.__get_url_filetype(url)
35
- download_url = f"http://web.archive.org/web/{timestamp}{url_type}/{url}"
36
- domain, subdir, filename = self.__split_url(url)
37
- if self.MODE_CURRENT: download_dir = os.path.join(output, domain, subdir)
38
- else: download_dir = os.path.join(output, domain, timestamp, subdir)
39
- download_file = os.path.join(download_dir, filename)
40
- cdx_entry = {
41
- "id": len(self.SNAPSHOT_COLLECTION),
42
- "url": download_url,
43
- "file": download_file,
44
- "timestamp": timestamp,
45
- "origin_url": url,
46
- "success": False,
47
- "retry": 0
48
- }
49
- return cdx_entry
50
-
51
- def count_list(self):
52
- return len(self.CDX_LIST)
53
-
54
- def snapshot_collection_write(self, query_entry: dict):
55
- if query_entry["id"] not in self.SNAPSHOT_COLLECTION:
56
- self.SNAPSHOT_COLLECTION.append(query_entry)
57
-
58
- def snapshot_collection_update(self, id: int, key: str, value: str):
59
- index = next((index for (index, d) in enumerate(self.SNAPSHOT_COLLECTION) if d["id"] == id), None)
60
- if index is not None:
61
- self.SNAPSHOT_COLLECTION[index][key] = value
62
-
63
- def __get_url_filetype(self, url):
64
- file_extension = os.path.splitext(url)[1][1:]
65
- urltype_mapping = {
66
- "jpg": "im_",
67
- "jpeg": "im_",
68
- "png": "im_",
69
- "gif": "im_",
70
- "svg": "im_",
71
- "ico": "im_",
72
- "css": "cs_",
73
- "js": "js_"
74
- }
75
- urltype = urltype_mapping.get(file_extension, "id_")
76
- return urltype
77
-
78
- def __split_url(self, url):
79
- parsed_url = urlparse(url)
80
- domain = parsed_url.netloc
81
- subdir = parsed_url.path.strip("/").rsplit("/", 1)[0]
82
- filename = parsed_url.path.split("/")[-1] or "index.html"
83
- return domain, subdir, filename
@@ -1 +0,0 @@
1
- __version__ = "0.6.3"
@@ -1,2 +0,0 @@
1
- requests==2.31.0
2
- tqdm==4.66.2
File without changes
File without changes
File without changes
File without changes