pywaybackup 4.2.0__tar.gz → 4.2.2__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.
- {pywaybackup-4.2.0/pywaybackup.egg-info → pywaybackup-4.2.2}/PKG-INFO +49 -1
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/README.md +48 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pyproject.toml +2 -2
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/PyWayBackup.py +63 -9
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/Snapshot.py +8 -2
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/SnapshotCollection.py +26 -4
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/Worker.py +3 -2
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/archive_download.py +7 -4
- pywaybackup-4.2.2/pywaybackup/arguments.py +72 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/helper.py +28 -1
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/interactive.py +43 -35
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/main.py +1 -1
- {pywaybackup-4.2.0 → pywaybackup-4.2.2/pywaybackup.egg-info}/PKG-INFO +49 -1
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup.egg-info/SOURCES.txt +1 -2
- pywaybackup-4.2.0/pywaybackup/arg_parser.py +0 -55
- pywaybackup-4.2.0/pywaybackup/arg_specs.py +0 -257
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/LICENSE +0 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/Exception.py +0 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/Verbosity.py +0 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/__init__.py +0 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/archive_save.py +0 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/db.py +0 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup/files.py +0 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup.egg-info/dependency_links.txt +0 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup.egg-info/entry_points.txt +0 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup.egg-info/requires.txt +0 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/pywaybackup.egg-info/top_level.txt +0 -0
- {pywaybackup-4.2.0 → pywaybackup-4.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pywaybackup
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.2
|
|
4
4
|
Summary: Query and download archive.org as simple as possible.
|
|
5
5
|
Author-email: bitdruid <bitdruid@outlook.com>
|
|
6
6
|
License: MIT License
|
|
@@ -75,6 +75,16 @@ Prebuilt executables for Windows, Linux and macOS are attached to each [release]
|
|
|
75
75
|
- Run from a terminal with arguments like the pip version: `waybackup -h`
|
|
76
76
|
- Or start it without arguments (e.g. double-click on Windows) to enter **interactive mode** — the tool will prompt you for URL, mode and optional settings.
|
|
77
77
|
|
|
78
|
+
On Linux/macOS the downloaded file has to be made executable first:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
curl -L -o waybackup https://github.com/bitdruid/python-wayback-machine-downloader/releases/latest/download/waybackup-linux
|
|
82
|
+
chmod +x waybackup
|
|
83
|
+
./waybackup -h
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Move it to a directory in your `PATH` (e.g. `~/.local/bin`) to call it as `waybackup` from anywhere.
|
|
87
|
+
|
|
78
88
|
### Manual
|
|
79
89
|
|
|
80
90
|
1. Clone the repository <br>
|
|
@@ -100,6 +110,8 @@ You can import pywaybackup into your own scripts and run it. Args are the same a
|
|
|
100
110
|
Additional args:
|
|
101
111
|
- `silent` (default False): If True, suppresses all output to the console.
|
|
102
112
|
- `debug` (default True): If False, disables writing errors to the error log file.
|
|
113
|
+
- `progress_callback` (default None): A function receiving the `status()` dict on every progress update.
|
|
114
|
+
- `progress_interval` (default 5): Seconds between callback updates while snapshots are downloading.
|
|
103
115
|
|
|
104
116
|
Use:
|
|
105
117
|
- `run()`
|
|
@@ -160,6 +172,39 @@ output:
|
|
|
160
172
|
}
|
|
161
173
|
```
|
|
162
174
|
|
|
175
|
+
Instead of polling `status()`, pass a callback to receive the `status` dict every `progress_interval` seconds (works on `silent` and `progress` disabled).
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from pywaybackup import PyWayBackup
|
|
179
|
+
|
|
180
|
+
def on_progress(status):
|
|
181
|
+
print(status)
|
|
182
|
+
|
|
183
|
+
backup = PyWayBackup(
|
|
184
|
+
url="https://example.com",
|
|
185
|
+
last=True,
|
|
186
|
+
silent=True,
|
|
187
|
+
progress=False,
|
|
188
|
+
progress_callback=on_progress,
|
|
189
|
+
progress_interval=5
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
backup.run()
|
|
193
|
+
```
|
|
194
|
+
output:
|
|
195
|
+
```bash
|
|
196
|
+
{'task': 'downloading cdx', 'current': 0, 'total': 0, 'progress': '0'}
|
|
197
|
+
{'task': 'preparing snapshots', 'current': 0, 'total': 0, 'progress': '0'}
|
|
198
|
+
{'task': 'downloading snapshots', 'current': 15, 'total': 84, 'progress': '18%'}
|
|
199
|
+
{'task': 'downloading snapshots', 'current': 54, 'total': 84, 'progress': '64%'}
|
|
200
|
+
{'task': 'downloading snapshots', 'current': 84, 'total': 84, 'progress': '100%'}
|
|
201
|
+
{'task': 'done', 'current': 84, 'total': 84, 'progress': '100%'}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Any callable taking one argument works - a function, a bound method, or a lambda for something short.
|
|
205
|
+
|
|
206
|
+
An exception raised inside the callback is logged and ignored - it will not abort a running job. When using `run(daemon=True)`, the callback runs inside the spawned process, so its side effects are not visible to the parent.
|
|
207
|
+
|
|
163
208
|
## cli
|
|
164
209
|
|
|
165
210
|
- `-h`, `--help`: Show the help message and exit. Version info is shown in the help header.
|
|
@@ -245,6 +290,9 @@ Parameters will change the download behavior for snapshots.
|
|
|
245
290
|
- **`--no-redirect`**:<br>
|
|
246
291
|
Disables following redirects of snapshots. Can prevent timestamp-folder mismatches caused by redirects.
|
|
247
292
|
|
|
293
|
+
- **`--no-merge-www`**:<br>
|
|
294
|
+
Keeps `www.example.com` and `example.com` in separate folders. By default both are treated as the same site and written into one folder, as archive.org returns them mixed together for a single query. Only use this if the two hosts served genuinely different content.
|
|
295
|
+
|
|
248
296
|
- **`--retry`** `<attempts>`:<br>
|
|
249
297
|
Retry attempts for failed downloads.
|
|
250
298
|
|
|
@@ -38,6 +38,16 @@ Prebuilt executables for Windows, Linux and macOS are attached to each [release]
|
|
|
38
38
|
- Run from a terminal with arguments like the pip version: `waybackup -h`
|
|
39
39
|
- Or start it without arguments (e.g. double-click on Windows) to enter **interactive mode** — the tool will prompt you for URL, mode and optional settings.
|
|
40
40
|
|
|
41
|
+
On Linux/macOS the downloaded file has to be made executable first:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
curl -L -o waybackup https://github.com/bitdruid/python-wayback-machine-downloader/releases/latest/download/waybackup-linux
|
|
45
|
+
chmod +x waybackup
|
|
46
|
+
./waybackup -h
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Move it to a directory in your `PATH` (e.g. `~/.local/bin`) to call it as `waybackup` from anywhere.
|
|
50
|
+
|
|
41
51
|
### Manual
|
|
42
52
|
|
|
43
53
|
1. Clone the repository <br>
|
|
@@ -63,6 +73,8 @@ You can import pywaybackup into your own scripts and run it. Args are the same a
|
|
|
63
73
|
Additional args:
|
|
64
74
|
- `silent` (default False): If True, suppresses all output to the console.
|
|
65
75
|
- `debug` (default True): If False, disables writing errors to the error log file.
|
|
76
|
+
- `progress_callback` (default None): A function receiving the `status()` dict on every progress update.
|
|
77
|
+
- `progress_interval` (default 5): Seconds between callback updates while snapshots are downloading.
|
|
66
78
|
|
|
67
79
|
Use:
|
|
68
80
|
- `run()`
|
|
@@ -123,6 +135,39 @@ output:
|
|
|
123
135
|
}
|
|
124
136
|
```
|
|
125
137
|
|
|
138
|
+
Instead of polling `status()`, pass a callback to receive the `status` dict every `progress_interval` seconds (works on `silent` and `progress` disabled).
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from pywaybackup import PyWayBackup
|
|
142
|
+
|
|
143
|
+
def on_progress(status):
|
|
144
|
+
print(status)
|
|
145
|
+
|
|
146
|
+
backup = PyWayBackup(
|
|
147
|
+
url="https://example.com",
|
|
148
|
+
last=True,
|
|
149
|
+
silent=True,
|
|
150
|
+
progress=False,
|
|
151
|
+
progress_callback=on_progress,
|
|
152
|
+
progress_interval=5
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
backup.run()
|
|
156
|
+
```
|
|
157
|
+
output:
|
|
158
|
+
```bash
|
|
159
|
+
{'task': 'downloading cdx', 'current': 0, 'total': 0, 'progress': '0'}
|
|
160
|
+
{'task': 'preparing snapshots', 'current': 0, 'total': 0, 'progress': '0'}
|
|
161
|
+
{'task': 'downloading snapshots', 'current': 15, 'total': 84, 'progress': '18%'}
|
|
162
|
+
{'task': 'downloading snapshots', 'current': 54, 'total': 84, 'progress': '64%'}
|
|
163
|
+
{'task': 'downloading snapshots', 'current': 84, 'total': 84, 'progress': '100%'}
|
|
164
|
+
{'task': 'done', 'current': 84, 'total': 84, 'progress': '100%'}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Any callable taking one argument works - a function, a bound method, or a lambda for something short.
|
|
168
|
+
|
|
169
|
+
An exception raised inside the callback is logged and ignored - it will not abort a running job. When using `run(daemon=True)`, the callback runs inside the spawned process, so its side effects are not visible to the parent.
|
|
170
|
+
|
|
126
171
|
## cli
|
|
127
172
|
|
|
128
173
|
- `-h`, `--help`: Show the help message and exit. Version info is shown in the help header.
|
|
@@ -208,6 +253,9 @@ Parameters will change the download behavior for snapshots.
|
|
|
208
253
|
- **`--no-redirect`**:<br>
|
|
209
254
|
Disables following redirects of snapshots. Can prevent timestamp-folder mismatches caused by redirects.
|
|
210
255
|
|
|
256
|
+
- **`--no-merge-www`**:<br>
|
|
257
|
+
Keeps `www.example.com` and `example.com` in separate folders. By default both are treated as the same site and written into one folder, as archive.org returns them mixed together for a single query. Only use this if the two hosts served genuinely different content.
|
|
258
|
+
|
|
211
259
|
- **`--retry`** `<attempts>`:<br>
|
|
212
260
|
Retry attempts for failed downloads.
|
|
213
261
|
|
|
@@ -16,7 +16,7 @@ dependencies = [
|
|
|
16
16
|
"python-magic-standalone==0.4.28",
|
|
17
17
|
]
|
|
18
18
|
name = "pywaybackup"
|
|
19
|
-
version = "4.2.
|
|
19
|
+
version = "4.2.2"
|
|
20
20
|
description = "Query and download archive.org as simple as possible."
|
|
21
21
|
authors = [{ name = "bitdruid", email = "bitdruid@outlook.com" }]
|
|
22
22
|
license = { file = "LICENSE" }
|
|
@@ -34,4 +34,4 @@ homepage = "https://github.com/bitdruid/python-wayback-machine-downloader"
|
|
|
34
34
|
|
|
35
35
|
[tool.ruff]
|
|
36
36
|
line-length = 120
|
|
37
|
-
exclude = ["pywaybackup/
|
|
37
|
+
exclude = ["pywaybackup/arguments.py"]
|
|
@@ -2,16 +2,17 @@ import multiprocessing
|
|
|
2
2
|
import os
|
|
3
3
|
import signal
|
|
4
4
|
import sys
|
|
5
|
+
import threading
|
|
5
6
|
import time
|
|
6
7
|
from importlib.metadata import version
|
|
7
|
-
from typing import Union
|
|
8
|
+
from typing import Callable, Optional, Union
|
|
8
9
|
|
|
9
10
|
import pywaybackup.archive_save as archive_save
|
|
10
11
|
from pywaybackup.archive_download import DownloadArchive
|
|
11
12
|
from pywaybackup.db import Database as db
|
|
12
13
|
from pywaybackup.Exception import Exception as ex
|
|
13
14
|
from pywaybackup.files import CDXfile, CDXquery, CSVfile
|
|
14
|
-
from pywaybackup.helper import sanitize_filename, url_split
|
|
15
|
+
from pywaybackup.helper import normalize_domain, sanitize_filename, url_split
|
|
15
16
|
from pywaybackup.SnapshotCollection import SnapshotCollection
|
|
16
17
|
from pywaybackup.Verbosity import Verbosity as vb
|
|
17
18
|
|
|
@@ -89,6 +90,7 @@ class PyWayBackup:
|
|
|
89
90
|
log (bool): Enable writing logs to a file.
|
|
90
91
|
progress (bool): Show a progress bar.
|
|
91
92
|
no_redirect (bool): Disable handling redirects.
|
|
93
|
+
no_merge_www (bool): Keep www and non-www snapshots in separate folders instead of merging them.
|
|
92
94
|
retry (int): Retry attempts for failed downloads.
|
|
93
95
|
workers (int): Number of download workers (default: 1).
|
|
94
96
|
delay (int): Delay between download requests in seconds.
|
|
@@ -96,6 +98,9 @@ class PyWayBackup:
|
|
|
96
98
|
keep (bool): Retain all job metadata after completion.
|
|
97
99
|
silent (bool): Suppress all output (for programmatic use).
|
|
98
100
|
debug (bool): Enable debug mode.
|
|
101
|
+
progress_callback (callable): Called with the status() dict on every task change and, during the
|
|
102
|
+
download phase, every `progress_interval` seconds. Fires independently of `silent`/`progress`.
|
|
103
|
+
progress_interval (float): Seconds between callback ticks while downloading snapshots (default: 5).
|
|
99
104
|
**kwargs: Catch-all for future expansion or external integration.
|
|
100
105
|
|
|
101
106
|
Methods:
|
|
@@ -127,6 +132,7 @@ class PyWayBackup:
|
|
|
127
132
|
log: bool = False,
|
|
128
133
|
progress: bool = False,
|
|
129
134
|
no_redirect: bool = False,
|
|
135
|
+
no_merge_www: bool = False,
|
|
130
136
|
retry: int = 0,
|
|
131
137
|
workers: int = 1,
|
|
132
138
|
delay: int = 0,
|
|
@@ -135,6 +141,8 @@ class PyWayBackup:
|
|
|
135
141
|
keep: bool = False,
|
|
136
142
|
silent: bool = True,
|
|
137
143
|
debug: bool = False,
|
|
144
|
+
progress_callback: Optional[Callable[[dict], None]] = None,
|
|
145
|
+
progress_interval: float = 5.0,
|
|
138
146
|
**kwargs: dict,
|
|
139
147
|
):
|
|
140
148
|
self._url = url
|
|
@@ -155,6 +163,7 @@ class PyWayBackup:
|
|
|
155
163
|
self._log = log
|
|
156
164
|
self._progress = progress
|
|
157
165
|
self._no_redirect = no_redirect
|
|
166
|
+
self._merge_www = not no_merge_www
|
|
158
167
|
self._retry = retry
|
|
159
168
|
self._workers = workers
|
|
160
169
|
self._delay = delay
|
|
@@ -166,6 +175,8 @@ class PyWayBackup:
|
|
|
166
175
|
# module exclusive
|
|
167
176
|
self._silent = silent
|
|
168
177
|
self._debug = debug
|
|
178
|
+
self._progress_callback = progress_callback
|
|
179
|
+
self._progress_interval = progress_interval
|
|
169
180
|
|
|
170
181
|
# internal
|
|
171
182
|
self._status = _Status()
|
|
@@ -328,7 +339,7 @@ class PyWayBackup:
|
|
|
328
339
|
SnapshotCollection: The initialized and loaded snapshot collection.
|
|
329
340
|
"""
|
|
330
341
|
collection = SnapshotCollection()
|
|
331
|
-
collection.load(mode=self._mode, cdxfile=self._cdxfile, csvfile=self._csvfile)
|
|
342
|
+
collection.load(mode=self._mode, cdxfile=self._cdxfile, csvfile=self._csvfile, merge_www=self._merge_www)
|
|
332
343
|
collection.print_calculation()
|
|
333
344
|
return collection
|
|
334
345
|
|
|
@@ -350,9 +361,43 @@ class PyWayBackup:
|
|
|
350
361
|
delay=self._delay,
|
|
351
362
|
wait=self._wait,
|
|
352
363
|
workers=self._workers,
|
|
364
|
+
merge_www=self._merge_www,
|
|
353
365
|
)
|
|
354
366
|
downloader.run(SnapshotCollection=collection)
|
|
355
367
|
|
|
368
|
+
def _notify(self, task: str = None):
|
|
369
|
+
"""
|
|
370
|
+
Set the current task (if given) and push the status to the progress callback.
|
|
371
|
+
|
|
372
|
+
Never gated by `silent`/`progress` - those only control the tqdm bar, while
|
|
373
|
+
embedders using the callback are exactly the ones running without one. A
|
|
374
|
+
failing callback is logged and swallowed, it must not abort a running job.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
task (str, optional): New task name to set before notifying.
|
|
378
|
+
"""
|
|
379
|
+
if task is not None:
|
|
380
|
+
self._status.task = task
|
|
381
|
+
if not self._progress_callback:
|
|
382
|
+
return
|
|
383
|
+
try:
|
|
384
|
+
self._progress_callback(self._status.status)
|
|
385
|
+
except Exception as e:
|
|
386
|
+
ex.exception(message="\nprogress_callback raised", e=e)
|
|
387
|
+
|
|
388
|
+
def _notify_loop(self, stop: threading.Event):
|
|
389
|
+
"""
|
|
390
|
+
Push a status update every `progress_interval` seconds until stopped.
|
|
391
|
+
|
|
392
|
+
Runs in a daemon thread during the download phase, which can last hours
|
|
393
|
+
without any task change to notify on.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
stop (threading.Event): Set by the workflow to end the loop.
|
|
397
|
+
"""
|
|
398
|
+
while not stop.wait(self._progress_interval):
|
|
399
|
+
self._notify()
|
|
400
|
+
|
|
356
401
|
def _workflow(self):
|
|
357
402
|
"""
|
|
358
403
|
Executes the steps required to perform the backup process:
|
|
@@ -365,18 +410,23 @@ class PyWayBackup:
|
|
|
365
410
|
|
|
366
411
|
"""
|
|
367
412
|
collection = None
|
|
413
|
+
ticker = None
|
|
414
|
+
ticker_stop = threading.Event()
|
|
368
415
|
try:
|
|
369
416
|
self._startup()
|
|
370
417
|
|
|
371
|
-
self.
|
|
418
|
+
self._notify(task="downloading cdx")
|
|
372
419
|
cdx = self._prep_cdx()
|
|
373
420
|
|
|
374
421
|
if cdx:
|
|
375
|
-
self.
|
|
422
|
+
self._notify(task="preparing snapshots")
|
|
376
423
|
collection = self._prep_collection()
|
|
377
424
|
|
|
378
425
|
if collection:
|
|
379
|
-
self.
|
|
426
|
+
self._notify(task="downloading snapshots")
|
|
427
|
+
if self._progress_callback:
|
|
428
|
+
ticker = threading.Thread(target=self._notify_loop, args=(ticker_stop,), daemon=True)
|
|
429
|
+
ticker.start()
|
|
380
430
|
self._dl_download(collection=collection)
|
|
381
431
|
|
|
382
432
|
except KeyboardInterrupt:
|
|
@@ -386,6 +436,10 @@ class PyWayBackup:
|
|
|
386
436
|
self._keep = True
|
|
387
437
|
ex.exception(message="", e=e)
|
|
388
438
|
finally:
|
|
439
|
+
ticker_stop.set() # stop ticking before the db teardown below
|
|
440
|
+
if ticker:
|
|
441
|
+
ticker.join()
|
|
442
|
+
self._notify() # last frame before "done" freezes the counts
|
|
389
443
|
# if a collection was created during the workflow, close its DB session cleanly
|
|
390
444
|
try:
|
|
391
445
|
if collection:
|
|
@@ -412,7 +466,7 @@ class PyWayBackup:
|
|
|
412
466
|
... }
|
|
413
467
|
"""
|
|
414
468
|
files = {
|
|
415
|
-
"snapshots": os.path.join(self._output, self._domain),
|
|
469
|
+
"snapshots": os.path.join(self._output, normalize_domain(self._domain, merge_www=self._merge_www)),
|
|
416
470
|
"cdxfile": self._cdxfile.filepath,
|
|
417
471
|
"dbfile": self._dbfile,
|
|
418
472
|
"csvfile": self._csvfile.filepath,
|
|
@@ -492,7 +546,7 @@ class PyWayBackup:
|
|
|
492
546
|
|
|
493
547
|
def _startup(self):
|
|
494
548
|
if db.query_exist:
|
|
495
|
-
self.
|
|
549
|
+
self._notify(task="resuming")
|
|
496
550
|
vb.write(
|
|
497
551
|
content=f"\nDOWNLOAD job exist - processed: {db.query_progress}\nResuming download... (to reset the job use '--reset')"
|
|
498
552
|
)
|
|
@@ -506,7 +560,7 @@ class PyWayBackup:
|
|
|
506
560
|
time.sleep(1)
|
|
507
561
|
|
|
508
562
|
def _shutdown(self):
|
|
509
|
-
self.
|
|
563
|
+
self._notify(task="done") # counts stay frozen at the last live update
|
|
510
564
|
collection = SnapshotCollection()
|
|
511
565
|
collection.close()
|
|
512
566
|
self._csvfile.store_result()
|
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
import threading
|
|
3
3
|
|
|
4
4
|
from pywaybackup.db import Database, select, update, waybackup_snapshots, and_
|
|
5
|
-
from pywaybackup.helper import url_split
|
|
5
|
+
from pywaybackup.helper import normalize_domain, url_split
|
|
6
6
|
from pywaybackup.Verbosity import Verbosity as vb
|
|
7
7
|
|
|
8
8
|
|
|
@@ -22,7 +22,7 @@ class Snapshot:
|
|
|
22
22
|
|
|
23
23
|
__sqlite_lock = threading.Lock()
|
|
24
24
|
|
|
25
|
-
def __init__(self, db: Database, output: str, mode: str):
|
|
25
|
+
def __init__(self, db: Database, output: str, mode: str, merge_www: bool = True):
|
|
26
26
|
"""
|
|
27
27
|
Initialize a Snapshot instance and fetch its database row if available.
|
|
28
28
|
|
|
@@ -30,10 +30,12 @@ class Snapshot:
|
|
|
30
30
|
db (Database): Database connection/session manager.
|
|
31
31
|
output (str): Output directory for downloaded files.
|
|
32
32
|
mode (str): Download mode ('first', 'last', or default).
|
|
33
|
+
merge_www (bool): Write www and non-www snapshots into the same folder.
|
|
33
34
|
"""
|
|
34
35
|
self._db = db
|
|
35
36
|
self.output = output
|
|
36
37
|
self.mode = mode
|
|
38
|
+
self.merge_www = merge_www
|
|
37
39
|
|
|
38
40
|
self._redirect_url = None
|
|
39
41
|
self._redirect_timestamp = None
|
|
@@ -164,10 +166,14 @@ class Snapshot:
|
|
|
164
166
|
If mode is 'first' or 'last', the path does not include the timestamp.
|
|
165
167
|
Otherwise, the timestamp is included in the path.
|
|
166
168
|
|
|
169
|
+
The domain is normalized so that the www and non-www variants of a site
|
|
170
|
+
end up in the same folder instead of two separate ones.
|
|
171
|
+
|
|
167
172
|
Returns:
|
|
168
173
|
str: Absolute path to the output file for the snapshot.
|
|
169
174
|
"""
|
|
170
175
|
domain, subdir, filename = url_split(self.url_archive.split("id_/")[1], index=True)
|
|
176
|
+
domain = normalize_domain(domain, merge_www=self.merge_www)
|
|
171
177
|
|
|
172
178
|
if self.mode == "last" or self.mode == "first":
|
|
173
179
|
download_dir = os.path.join(self.output, domain, subdir)
|
|
@@ -28,6 +28,9 @@ class SnapshotCollection:
|
|
|
28
28
|
|
|
29
29
|
self._snapshot_faulty = 0 # error while parsing cdx line
|
|
30
30
|
|
|
31
|
+
self._merge_www = True # treat www and non-www as the same url
|
|
32
|
+
|
|
33
|
+
self._filter_mailto = 0 # mailto: links in the cdx results
|
|
31
34
|
self._filter_duplicates = 0 # with identical url_archive
|
|
32
35
|
self._filter_mode = 0 # all snapshots filtered by the MODE (last or first)
|
|
33
36
|
self._filter_skip = 0 # content of the csv file
|
|
@@ -60,12 +63,13 @@ class SnapshotCollection:
|
|
|
60
63
|
self.db.write_progress(self._snapshot_handled, self._snapshot_total)
|
|
61
64
|
self.db.session.close()
|
|
62
65
|
|
|
63
|
-
def load(self, mode: str, cdxfile: CDXfile, csvfile: CSVfile):
|
|
66
|
+
def load(self, mode: str, cdxfile: CDXfile, csvfile: CSVfile, merge_www: bool = True):
|
|
64
67
|
"""
|
|
65
68
|
Insert the content of the cdx and csv file into the snapshot table.
|
|
66
69
|
"""
|
|
67
70
|
self.cdxfile = cdxfile
|
|
68
71
|
self.csvfile = csvfile
|
|
72
|
+
self._merge_www = merge_www
|
|
69
73
|
if mode == "first":
|
|
70
74
|
self._mode_first = True
|
|
71
75
|
if mode == "last":
|
|
@@ -114,6 +118,9 @@ class SnapshotCollection:
|
|
|
114
118
|
"statuscode": line[3],
|
|
115
119
|
"origin": line[4],
|
|
116
120
|
}
|
|
121
|
+
# cdx results contain mailto: links, which are no downloadable resources
|
|
122
|
+
if line["origin"].lower().startswith("mailto"):
|
|
123
|
+
return None
|
|
117
124
|
url_archive = f"https://web.archive.org/web/{line['timestamp']}id_/{line['origin']}"
|
|
118
125
|
statuscode = line["statuscode"] if line["statuscode"] in ("301", "404") else None
|
|
119
126
|
return {
|
|
@@ -189,10 +196,14 @@ class SnapshotCollection:
|
|
|
189
196
|
line = line.rsplit(",", 1)[0]
|
|
190
197
|
|
|
191
198
|
try:
|
|
192
|
-
|
|
199
|
+
parsed = __parse_line(line)
|
|
193
200
|
except json.decoder.JSONDecodeError:
|
|
194
201
|
self._snapshot_faulty += 1
|
|
195
202
|
continue
|
|
203
|
+
if parsed is None:
|
|
204
|
+
self._filter_mailto += 1
|
|
205
|
+
continue
|
|
206
|
+
line_batch.append(parsed)
|
|
196
207
|
|
|
197
208
|
if len(line_batch) >= line_batchsize:
|
|
198
209
|
total_inserted += _insert_batch_safe(line_batch=line_batch)
|
|
@@ -253,6 +264,11 @@ class SnapshotCollection:
|
|
|
253
264
|
|
|
254
265
|
- MODE_LAST → keep only the latest snapshot (highest timestamp) per url_origin.
|
|
255
266
|
- MODE_FIRST → keep only the earliest snapshot (lowest timestamp) per url_origin.
|
|
267
|
+
|
|
268
|
+
Grouped by the url as it maps to disk, not by the raw url_origin: the output
|
|
269
|
+
path drops the scheme and normalizes the domain, so `http://www.example.com/`
|
|
270
|
+
and `https://example.com/` are one and the same file. Grouping by the raw value
|
|
271
|
+
would download both and let the second silently overwrite the first.
|
|
256
272
|
"""
|
|
257
273
|
|
|
258
274
|
def _filter_mode():
|
|
@@ -261,11 +277,15 @@ class SnapshotCollection:
|
|
|
261
277
|
ordering = (
|
|
262
278
|
waybackup_snapshots.timestamp.desc() if self._mode_last else waybackup_snapshots.timestamp.asc()
|
|
263
279
|
)
|
|
264
|
-
#
|
|
280
|
+
# same url as far as the output path is concerned (see helper.normalize_domain)
|
|
281
|
+
url_key = func.replace(func.lower(waybackup_snapshots.url_origin), "https://", "http://")
|
|
282
|
+
if self._merge_www:
|
|
283
|
+
url_key = func.replace(url_key, "http://www.", "http://")
|
|
284
|
+
# assign row numbers per url
|
|
265
285
|
rownum = (
|
|
266
286
|
func.row_number()
|
|
267
287
|
.over(
|
|
268
|
-
partition_by=
|
|
288
|
+
partition_by=url_key,
|
|
269
289
|
order_by=ordering,
|
|
270
290
|
)
|
|
271
291
|
.label("rn")
|
|
@@ -377,6 +397,8 @@ class SnapshotCollection:
|
|
|
377
397
|
vb.write(content="\nSnapshot calculation:")
|
|
378
398
|
vb.write(content=f"-----> {'in CDX file'.ljust(18)}: {self._cdx_total:,}")
|
|
379
399
|
|
|
400
|
+
if self._filter_mailto > 0:
|
|
401
|
+
vb.write(content=f"-----> {'removed mailto'.ljust(18)}: {self._filter_mailto:,}")
|
|
380
402
|
if self._filter_duplicates > 0:
|
|
381
403
|
vb.write(content=f"-----> {'removed duplicates'.ljust(18)}: {self._filter_duplicates:,}")
|
|
382
404
|
if self._filter_mode > 0:
|
|
@@ -11,10 +11,11 @@ class Worker:
|
|
|
11
11
|
Worker buffers its messages in a Message object. Output has to be done with write() method.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
def __init__(self, id: int, output: str, mode: str):
|
|
14
|
+
def __init__(self, id: int, output: str, mode: str, merge_www: bool = True):
|
|
15
15
|
self.id = id
|
|
16
16
|
self.output = output
|
|
17
17
|
self.mode = mode
|
|
18
|
+
self.merge_www = merge_www
|
|
18
19
|
self.message = Message(self)
|
|
19
20
|
|
|
20
21
|
def init(self):
|
|
@@ -43,7 +44,7 @@ class Worker:
|
|
|
43
44
|
pass
|
|
44
45
|
|
|
45
46
|
def assign_snapshot(self, total_amount: int):
|
|
46
|
-
self.snapshot = Snapshot(self.db, output=self.output, mode=self.mode)
|
|
47
|
+
self.snapshot = Snapshot(self.db, output=self.output, mode=self.mode, merge_www=self.merge_www)
|
|
47
48
|
self.total_amount = total_amount
|
|
48
49
|
if not self.snapshot.counter: # counter only if a row was fetched
|
|
49
50
|
self.snapshot = None
|
|
@@ -81,7 +81,7 @@ class DownloadArchive:
|
|
|
81
81
|
sc (SnapshotCollection): The snapshot collection being processed.
|
|
82
82
|
"""
|
|
83
83
|
|
|
84
|
-
def __init__(self, mode: str, output: str, retry: int, no_redirect: bool, delay: int, wait: int, workers: int):
|
|
84
|
+
def __init__(self, mode: str, output: str, retry: int, no_redirect: bool, delay: int, wait: int, workers: int, merge_www: bool = True):
|
|
85
85
|
"""
|
|
86
86
|
Initialize the download manager with configuration options.
|
|
87
87
|
|
|
@@ -92,9 +92,11 @@ class DownloadArchive:
|
|
|
92
92
|
no_redirect (bool): Disable redirect handling if True.
|
|
93
93
|
delay (int): Delay between downloads in seconds.
|
|
94
94
|
workers (int): Number of worker threads.
|
|
95
|
+
merge_www (bool): Write www and non-www snapshots into the same folder.
|
|
95
96
|
"""
|
|
96
97
|
self.mode = mode
|
|
97
98
|
self.output = output
|
|
99
|
+
self.merge_www = merge_www
|
|
98
100
|
self.retry = retry
|
|
99
101
|
self.no_redirect = no_redirect
|
|
100
102
|
self.delay = delay
|
|
@@ -127,7 +129,7 @@ class DownloadArchive:
|
|
|
127
129
|
|
|
128
130
|
threads = []
|
|
129
131
|
for i in range(self.workers):
|
|
130
|
-
worker = Worker(id=i + 1, output=self.output, mode=self.mode)
|
|
132
|
+
worker = Worker(id=i + 1, output=self.output, mode=self.mode, merge_www=self.merge_www)
|
|
131
133
|
vb.write(verbose=True, content=f"\n-----> Starting Worker: {worker.id}")
|
|
132
134
|
thread = threading.Thread(target=self._download_loop, args=(worker,), daemon=True)
|
|
133
135
|
threads.append(thread)
|
|
@@ -338,9 +340,10 @@ class DownloadArchive:
|
|
|
338
340
|
self.__download_response(context=context, worker=worker)
|
|
339
341
|
location = context.response.getheader("Location")
|
|
340
342
|
if location:
|
|
341
|
-
|
|
343
|
+
resolved_location = urljoin(context.snapshot_url, location)
|
|
344
|
+
context.encoded_download_url = context.encode_url(resolved_location)
|
|
342
345
|
worker.message.store(verbose=True, result="", info="TO", content=location)
|
|
343
|
-
worker.snapshot.redirect_timestamp = url_get_timestamp(
|
|
346
|
+
worker.snapshot.redirect_timestamp = url_get_timestamp(resolved_location)
|
|
344
347
|
worker.snapshot.redirect_url = context.snapshot_url
|
|
345
348
|
else:
|
|
346
349
|
break
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from argparse import RawTextHelpFormatter
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import version
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
10
|
+
"""
|
|
11
|
+
Build the argument parser. Single source of truth for all cli arguments.
|
|
12
|
+
|
|
13
|
+
Split from parsing so that Interactive can read the argument definitions
|
|
14
|
+
without argparse touching sys.argv. Interactive walks the argument groups
|
|
15
|
+
below as they are, so a new argument shows up there without further work.
|
|
16
|
+
"""
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
description=f"<<< python-wayback-machine-downloader v{version('pywaybackup')} >>>\nby @bitdruid -> https://github.com/bitdruid",
|
|
19
|
+
formatter_class=RawTextHelpFormatter,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
required = parser.add_argument_group("required (one exclusive)")
|
|
23
|
+
required.add_argument("-u", "--url", type=str, metavar="", help="url (with subdir/subdomain) to download")
|
|
24
|
+
exclusive_required = required.add_mutually_exclusive_group(required=True)
|
|
25
|
+
exclusive_required.add_argument("-a", "--all", action="store_true", help="download snapshots of all timestamps")
|
|
26
|
+
exclusive_required.add_argument("-l", "--last", action="store_true", help="download the last version of each file snapshot")
|
|
27
|
+
exclusive_required.add_argument("-f", "--first", action="store_true", help="download the first version of each file snapshot")
|
|
28
|
+
exclusive_required.add_argument("-s", "--save", action="store_true", help="save a page to the wayback machine")
|
|
29
|
+
|
|
30
|
+
optional = parser.add_argument_group("optional query parameters")
|
|
31
|
+
optional.add_argument("-e", "--explicit", action="store_true", help="search only for the explicit given url")
|
|
32
|
+
optional.add_argument("-r", "--range", type=int, metavar="", help="range in years to search")
|
|
33
|
+
optional.add_argument("--start", type=int, metavar="", help="start timestamp format: YYYYMMDDHHMMSS")
|
|
34
|
+
optional.add_argument("--end", type=int, metavar="", help="end timestamp format: YYYYMMDDHHMMSS")
|
|
35
|
+
optional.add_argument("--limit", type=int, nargs="?", const=True, metavar="int", help="limit the number of snapshots to download")
|
|
36
|
+
optional.add_argument("--filetype", type=str, metavar="", help="filetypes to download comma separated (js,css,...)")
|
|
37
|
+
optional.add_argument("--statuscode", type=str, metavar="", help="statuscodes to download comma separated (200,404,...)")
|
|
38
|
+
|
|
39
|
+
behavior = parser.add_argument_group("manipulate behavior")
|
|
40
|
+
behavior.add_argument("-o", "--output", type=str, metavar="", help="output for all files - defaults to current directory")
|
|
41
|
+
behavior.add_argument("-m", "--metadata", type=str, metavar="", help="change directory for db/cdx/csv/log files")
|
|
42
|
+
behavior.add_argument("-v", "--verbose", type=str, nargs="?", const="default", metavar="", help="verbosity level: low, default, high (default if flag set without value)")
|
|
43
|
+
behavior.add_argument("--log", action="store_true", help="save a log file into the output folder")
|
|
44
|
+
behavior.add_argument("--progress", action="store_true", help="show a progress bar")
|
|
45
|
+
behavior.add_argument("--no-redirect", action="store_true", help="do not follow redirects by archive.org")
|
|
46
|
+
behavior.add_argument("--no-merge-www", action="store_true", help="keep www and non-www snapshots in separate folders")
|
|
47
|
+
behavior.add_argument("--retry", type=int, default=0, metavar="", help="retry failed downloads (opt tries as int, else infinite)")
|
|
48
|
+
behavior.add_argument("--workers", type=int, default=1, metavar="", help="number of workers (simultaneous downloads)")
|
|
49
|
+
behavior.add_argument("--delay", type=int, default=0, metavar="", help="delay between each download in seconds")
|
|
50
|
+
behavior.add_argument("--wait", type=int, default=15, metavar="", help="seconds to wait before renewing connection after HTTP errors or snapshot download errors (default: 15)")
|
|
51
|
+
|
|
52
|
+
special = parser.add_argument_group("special")
|
|
53
|
+
special.add_argument("--reset", action="store_true", help="reset the job and ignore existing cdx/db/csv files")
|
|
54
|
+
special.add_argument("--keep", action="store_true", help="keep all files after the job finished")
|
|
55
|
+
|
|
56
|
+
return parser
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Arguments:
|
|
60
|
+
def __init__(self):
|
|
61
|
+
parser = build_parser()
|
|
62
|
+
|
|
63
|
+
args = parser.parse_args(args=None if sys.argv[1:] else ["--help"]) # if no arguments are given, print help
|
|
64
|
+
|
|
65
|
+
args.silent = False
|
|
66
|
+
args.debug = True
|
|
67
|
+
|
|
68
|
+
self.args = args
|
|
69
|
+
|
|
70
|
+
def get_args(self) -> dict:
|
|
71
|
+
"""Returns the parsed arguments as a dictionary."""
|
|
72
|
+
return vars(self.args)
|
|
@@ -34,13 +34,40 @@ def sanitize_url(url: str) -> str:
|
|
|
34
34
|
def url_get_timestamp(url):
|
|
35
35
|
"""
|
|
36
36
|
Extract the timestamp from a wayback machine URL.
|
|
37
|
+
|
|
38
|
+
Returns an empty string if the URL does not contain a `web/<timestamp>` segment
|
|
39
|
+
(e.g. relative or external redirect targets).
|
|
37
40
|
"""
|
|
38
|
-
|
|
41
|
+
parts = url.split("web/")
|
|
42
|
+
if len(parts) < 2:
|
|
43
|
+
return ""
|
|
44
|
+
timestamp = parts[1].split("/")[0]
|
|
39
45
|
if "id_" in url:
|
|
40
46
|
timestamp = timestamp.split("id_")[0]
|
|
41
47
|
return timestamp
|
|
42
48
|
|
|
43
49
|
|
|
50
|
+
def normalize_domain(domain: str, merge_www: bool = True) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Normalize a domain for use as a folder name.
|
|
53
|
+
|
|
54
|
+
The cdx api canonicalizes hosts, so a single query returns `example.com`,
|
|
55
|
+
`www.example.com`, `www.example.com.` and `www.EXAMPLE.com` mixed together.
|
|
56
|
+
Without normalizing these all become separate output folders for the same site.
|
|
57
|
+
|
|
58
|
+
The `www.` prefix is only stripped if a dot remains, so `www.com` stays intact.
|
|
59
|
+
Real subdomains (`blog.example.com`) are left alone.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
domain (str): The domain to normalize.
|
|
63
|
+
merge_www (bool): If False, keeps the `www.` prefix (--no-merge-www).
|
|
64
|
+
"""
|
|
65
|
+
domain = domain.lower().rstrip(".")
|
|
66
|
+
if merge_www and domain.startswith("www.") and "." in domain[4:]:
|
|
67
|
+
domain = domain[4:]
|
|
68
|
+
return domain
|
|
69
|
+
|
|
70
|
+
|
|
44
71
|
def url_split(url, index=False):
|
|
45
72
|
"""
|
|
46
73
|
Split a URL into domain, subdir, and filename.
|
|
@@ -3,13 +3,14 @@ Interactive mode: prompt the user for arguments instead of parsing sys.argv.
|
|
|
3
3
|
|
|
4
4
|
Used when waybackup is launched without CLI arguments (e.g. double-clicking
|
|
5
5
|
the Windows .exe). Produces the same dict shape as Arguments.get_args() so
|
|
6
|
-
PyWayBackup(**args) works either way.
|
|
7
|
-
|
|
6
|
+
PyWayBackup(**args) works either way. The arguments and their grouping are read
|
|
7
|
+
from the argparse parser, so a new argument is offered here automatically - it
|
|
8
|
+
only has to be added in arguments.py.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from importlib.metadata import version
|
|
11
12
|
|
|
12
|
-
from pywaybackup.
|
|
13
|
+
from pywaybackup.arguments import build_parser
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class Interactive:
|
|
@@ -17,28 +18,35 @@ class Interactive:
|
|
|
17
18
|
print(f"<<< python-wayback-machine-downloader v{version('pywaybackup')} >>>")
|
|
18
19
|
print("Interactive mode - press Ctrl+C to abort.\n")
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
parser = build_parser()
|
|
22
|
+
actions = [action for action in parser._actions if action.dest != "help"]
|
|
23
|
+
args = {action.dest: action.default for action in actions}
|
|
24
|
+
|
|
25
|
+
# 1. Required url
|
|
26
|
+
url = _action_by_dest(actions, "url")
|
|
27
|
+
args["url"] = self._prompt_required(url.help)
|
|
28
|
+
|
|
29
|
+
# 2. Required exclusive group(s) - pick exactly one member
|
|
30
|
+
exclusive = set()
|
|
31
|
+
for group in parser._mutually_exclusive_groups:
|
|
32
|
+
members = group._group_actions
|
|
33
|
+
exclusive.update(action.dest for action in members)
|
|
34
|
+
if not group.required:
|
|
35
|
+
continue
|
|
36
|
+
choice = self._prompt_choice("Mode", [(a.dest, a.help) for a in members])
|
|
37
|
+
for action in members:
|
|
38
|
+
args[action.dest] = action.dest == choice
|
|
39
|
+
|
|
40
|
+
# 3. Every other argument, offered group by group as defined in the parser
|
|
41
|
+
handled = exclusive | {"url"}
|
|
42
|
+
for group in parser._action_groups:
|
|
43
|
+
members = [a for a in group._group_actions if a.dest not in handled and a.dest != "help"]
|
|
44
|
+
if not members:
|
|
45
|
+
continue
|
|
46
|
+
if not self._prompt_yes_no(f"\nConfigure {group.title}?", default=False):
|
|
47
|
+
continue
|
|
48
|
+
for action in members:
|
|
49
|
+
args[action.dest] = self._prompt_for(action, args[action.dest])
|
|
42
50
|
|
|
43
51
|
# internal flags (parity with Arguments.py)
|
|
44
52
|
args["silent"] = False
|
|
@@ -50,11 +58,11 @@ class Interactive:
|
|
|
50
58
|
def get_args(self) -> dict:
|
|
51
59
|
return self.args
|
|
52
60
|
|
|
53
|
-
def _prompt_for(self,
|
|
54
|
-
label =
|
|
55
|
-
if
|
|
56
|
-
return self._prompt_yes_no(
|
|
57
|
-
if
|
|
61
|
+
def _prompt_for(self, action, current):
|
|
62
|
+
label = action.help
|
|
63
|
+
if action.nargs == 0: # store_true
|
|
64
|
+
return self._prompt_yes_no(label, default=bool(current))
|
|
65
|
+
if action.type is int:
|
|
58
66
|
if current is None:
|
|
59
67
|
return self._prompt_optional_int(label)
|
|
60
68
|
return self._prompt_int(label, default=current)
|
|
@@ -137,8 +145,8 @@ class Interactive:
|
|
|
137
145
|
print(f" (please enter one of: {', '.join(sorted(valid))})")
|
|
138
146
|
|
|
139
147
|
|
|
140
|
-
def
|
|
141
|
-
for
|
|
142
|
-
if
|
|
143
|
-
return
|
|
144
|
-
raise KeyError(
|
|
148
|
+
def _action_by_dest(actions, dest):
|
|
149
|
+
for action in actions:
|
|
150
|
+
if action.dest == dest:
|
|
151
|
+
return action
|
|
152
|
+
raise KeyError(dest)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pywaybackup
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.2
|
|
4
4
|
Summary: Query and download archive.org as simple as possible.
|
|
5
5
|
Author-email: bitdruid <bitdruid@outlook.com>
|
|
6
6
|
License: MIT License
|
|
@@ -75,6 +75,16 @@ Prebuilt executables for Windows, Linux and macOS are attached to each [release]
|
|
|
75
75
|
- Run from a terminal with arguments like the pip version: `waybackup -h`
|
|
76
76
|
- Or start it without arguments (e.g. double-click on Windows) to enter **interactive mode** — the tool will prompt you for URL, mode and optional settings.
|
|
77
77
|
|
|
78
|
+
On Linux/macOS the downloaded file has to be made executable first:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
curl -L -o waybackup https://github.com/bitdruid/python-wayback-machine-downloader/releases/latest/download/waybackup-linux
|
|
82
|
+
chmod +x waybackup
|
|
83
|
+
./waybackup -h
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Move it to a directory in your `PATH` (e.g. `~/.local/bin`) to call it as `waybackup` from anywhere.
|
|
87
|
+
|
|
78
88
|
### Manual
|
|
79
89
|
|
|
80
90
|
1. Clone the repository <br>
|
|
@@ -100,6 +110,8 @@ You can import pywaybackup into your own scripts and run it. Args are the same a
|
|
|
100
110
|
Additional args:
|
|
101
111
|
- `silent` (default False): If True, suppresses all output to the console.
|
|
102
112
|
- `debug` (default True): If False, disables writing errors to the error log file.
|
|
113
|
+
- `progress_callback` (default None): A function receiving the `status()` dict on every progress update.
|
|
114
|
+
- `progress_interval` (default 5): Seconds between callback updates while snapshots are downloading.
|
|
103
115
|
|
|
104
116
|
Use:
|
|
105
117
|
- `run()`
|
|
@@ -160,6 +172,39 @@ output:
|
|
|
160
172
|
}
|
|
161
173
|
```
|
|
162
174
|
|
|
175
|
+
Instead of polling `status()`, pass a callback to receive the `status` dict every `progress_interval` seconds (works on `silent` and `progress` disabled).
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from pywaybackup import PyWayBackup
|
|
179
|
+
|
|
180
|
+
def on_progress(status):
|
|
181
|
+
print(status)
|
|
182
|
+
|
|
183
|
+
backup = PyWayBackup(
|
|
184
|
+
url="https://example.com",
|
|
185
|
+
last=True,
|
|
186
|
+
silent=True,
|
|
187
|
+
progress=False,
|
|
188
|
+
progress_callback=on_progress,
|
|
189
|
+
progress_interval=5
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
backup.run()
|
|
193
|
+
```
|
|
194
|
+
output:
|
|
195
|
+
```bash
|
|
196
|
+
{'task': 'downloading cdx', 'current': 0, 'total': 0, 'progress': '0'}
|
|
197
|
+
{'task': 'preparing snapshots', 'current': 0, 'total': 0, 'progress': '0'}
|
|
198
|
+
{'task': 'downloading snapshots', 'current': 15, 'total': 84, 'progress': '18%'}
|
|
199
|
+
{'task': 'downloading snapshots', 'current': 54, 'total': 84, 'progress': '64%'}
|
|
200
|
+
{'task': 'downloading snapshots', 'current': 84, 'total': 84, 'progress': '100%'}
|
|
201
|
+
{'task': 'done', 'current': 84, 'total': 84, 'progress': '100%'}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Any callable taking one argument works - a function, a bound method, or a lambda for something short.
|
|
205
|
+
|
|
206
|
+
An exception raised inside the callback is logged and ignored - it will not abort a running job. When using `run(daemon=True)`, the callback runs inside the spawned process, so its side effects are not visible to the parent.
|
|
207
|
+
|
|
163
208
|
## cli
|
|
164
209
|
|
|
165
210
|
- `-h`, `--help`: Show the help message and exit. Version info is shown in the help header.
|
|
@@ -245,6 +290,9 @@ Parameters will change the download behavior for snapshots.
|
|
|
245
290
|
- **`--no-redirect`**:<br>
|
|
246
291
|
Disables following redirects of snapshots. Can prevent timestamp-folder mismatches caused by redirects.
|
|
247
292
|
|
|
293
|
+
- **`--no-merge-www`**:<br>
|
|
294
|
+
Keeps `www.example.com` and `example.com` in separate folders. By default both are treated as the same site and written into one folder, as archive.org returns them mixed together for a single query. Only use this if the two hosts served genuinely different content.
|
|
295
|
+
|
|
248
296
|
- **`--retry`** `<attempts>`:<br>
|
|
249
297
|
Retry attempts for failed downloads.
|
|
250
298
|
|
|
@@ -10,8 +10,7 @@ pywaybackup/Worker.py
|
|
|
10
10
|
pywaybackup/__init__.py
|
|
11
11
|
pywaybackup/archive_download.py
|
|
12
12
|
pywaybackup/archive_save.py
|
|
13
|
-
pywaybackup/
|
|
14
|
-
pywaybackup/arg_specs.py
|
|
13
|
+
pywaybackup/arguments.py
|
|
15
14
|
pywaybackup/db.py
|
|
16
15
|
pywaybackup/files.py
|
|
17
16
|
pywaybackup/helper.py
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import sys
|
|
3
|
-
from argparse import RawTextHelpFormatter
|
|
4
|
-
from importlib.metadata import version
|
|
5
|
-
|
|
6
|
-
from pywaybackup.arg_specs import ARG_GROUPS, ARG_SPECS, EXCLUSIVE_GROUPS
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Arguments:
|
|
10
|
-
def __init__(self):
|
|
11
|
-
parser = argparse.ArgumentParser(
|
|
12
|
-
description=f"<<< python-wayback-machine-downloader v{version('pywaybackup')} >>>\nby @bitdruid -> https://github.com/bitdruid",
|
|
13
|
-
formatter_class=RawTextHelpFormatter,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
groups = {name: parser.add_argument_group(name) for name in ARG_GROUPS}
|
|
17
|
-
|
|
18
|
-
exclusive = {
|
|
19
|
-
ex_name: groups[ex_meta["parent_group"]].add_mutually_exclusive_group(required=ex_meta["required"])
|
|
20
|
-
for ex_name, ex_meta in EXCLUSIVE_GROUPS.items()
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
for spec in ARG_SPECS:
|
|
24
|
-
target = exclusive[spec.exclusive_group] if spec.exclusive_group else groups[spec.group]
|
|
25
|
-
target.add_argument(*spec.flags, **_argparse_kwargs(spec))
|
|
26
|
-
|
|
27
|
-
args = parser.parse_args(args=None if sys.argv[1:] else ["--help"]) # if no arguments are given, print help
|
|
28
|
-
|
|
29
|
-
args.silent = False
|
|
30
|
-
args.debug = True
|
|
31
|
-
|
|
32
|
-
self.args = args
|
|
33
|
-
|
|
34
|
-
def get_args(self) -> dict:
|
|
35
|
-
"""Returns the parsed arguments as a dictionary."""
|
|
36
|
-
return vars(self.args)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _argparse_kwargs(spec) -> dict:
|
|
40
|
-
"""Translate an ArgSpec into kwargs for argparse.add_argument()."""
|
|
41
|
-
kwargs = {"help": spec.help}
|
|
42
|
-
if spec.action == "store_true":
|
|
43
|
-
kwargs["action"] = "store_true"
|
|
44
|
-
kwargs["default"] = bool(spec.default)
|
|
45
|
-
elif spec.action == "optional_value":
|
|
46
|
-
kwargs["type"] = spec.type
|
|
47
|
-
kwargs["nargs"] = "?"
|
|
48
|
-
kwargs["const"] = spec.const
|
|
49
|
-
kwargs["metavar"] = spec.metavar
|
|
50
|
-
kwargs["default"] = spec.default
|
|
51
|
-
else:
|
|
52
|
-
kwargs["type"] = spec.type
|
|
53
|
-
kwargs["metavar"] = spec.metavar
|
|
54
|
-
kwargs["default"] = spec.default
|
|
55
|
-
return kwargs
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Single source of truth for waybackup CLI arguments.
|
|
3
|
-
|
|
4
|
-
Both Arguments (argparse) and Interactive (input prompts) consume this list,
|
|
5
|
-
so a new flag only needs to be added in one place. PyWayBackup.__init__ keeps
|
|
6
|
-
its explicit signature (vscode autocomplete).
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from typing import Any, List, Optional
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@dataclass
|
|
14
|
-
class ArgSpec:
|
|
15
|
-
name: str # internal key (matches PyWayBackup.__init__ kwarg)
|
|
16
|
-
flags: List[str] # CLI flags, e.g. ["-u", "--url"]
|
|
17
|
-
group: str # argparse group label
|
|
18
|
-
help: str # CLI help text
|
|
19
|
-
prompt: Optional[str] = None # interactive prompt label (None = skip in interactive)
|
|
20
|
-
type: Optional[type] = None # str / int / None for store_true
|
|
21
|
-
default: Any = None
|
|
22
|
-
action: str = "store" # "store" | "store_true" | "optional_value"
|
|
23
|
-
const: Any = None # used when action="optional_value"
|
|
24
|
-
metavar: str = "" # argparse metavar; ignored for store_true
|
|
25
|
-
exclusive_group: Optional[str] = None # name of a mutex group below
|
|
26
|
-
advanced: bool = False # show in interactive only when advanced opts enabled
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# Mutually exclusive groups, keyed by name. Specs join them via exclusive_group=...
|
|
30
|
-
EXCLUSIVE_GROUPS = {
|
|
31
|
-
"mode": {"required": True, "parent_group": "required (one exclusive)"},
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
# argparse groups in display order
|
|
35
|
-
ARG_GROUPS = [
|
|
36
|
-
"required (one exclusive)",
|
|
37
|
-
"optional query parameters",
|
|
38
|
-
"manipulate behavior",
|
|
39
|
-
"special",
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
ARG_SPECS: List[ArgSpec] = [
|
|
44
|
-
# required
|
|
45
|
-
ArgSpec(
|
|
46
|
-
name="url",
|
|
47
|
-
flags=["-u", "--url"],
|
|
48
|
-
group="required (one exclusive)",
|
|
49
|
-
help="url (with subdir/subdomain) to download",
|
|
50
|
-
prompt="URL to download (with subdir/subdomain)",
|
|
51
|
-
type=str,
|
|
52
|
-
),
|
|
53
|
-
ArgSpec(
|
|
54
|
-
name="all",
|
|
55
|
-
flags=["-a", "--all"],
|
|
56
|
-
group="required (one exclusive)",
|
|
57
|
-
help="download snapshots of all timestamps",
|
|
58
|
-
action="store_true",
|
|
59
|
-
default=False,
|
|
60
|
-
exclusive_group="mode",
|
|
61
|
-
),
|
|
62
|
-
ArgSpec(
|
|
63
|
-
name="last",
|
|
64
|
-
flags=["-l", "--last"],
|
|
65
|
-
group="required (one exclusive)",
|
|
66
|
-
help="download the last version of each file snapshot",
|
|
67
|
-
action="store_true",
|
|
68
|
-
default=False,
|
|
69
|
-
exclusive_group="mode",
|
|
70
|
-
),
|
|
71
|
-
ArgSpec(
|
|
72
|
-
name="first",
|
|
73
|
-
flags=["-f", "--first"],
|
|
74
|
-
group="required (one exclusive)",
|
|
75
|
-
help="download the first version of each file snapshot",
|
|
76
|
-
action="store_true",
|
|
77
|
-
default=False,
|
|
78
|
-
exclusive_group="mode",
|
|
79
|
-
),
|
|
80
|
-
ArgSpec(
|
|
81
|
-
name="save",
|
|
82
|
-
flags=["-s", "--save"],
|
|
83
|
-
group="required (one exclusive)",
|
|
84
|
-
help="save a page to the wayback machine",
|
|
85
|
-
action="store_true",
|
|
86
|
-
default=False,
|
|
87
|
-
exclusive_group="mode",
|
|
88
|
-
),
|
|
89
|
-
# -------------------- optional query --------------------
|
|
90
|
-
ArgSpec(
|
|
91
|
-
name="explicit",
|
|
92
|
-
flags=["-e", "--explicit"],
|
|
93
|
-
group="optional query parameters",
|
|
94
|
-
help="search only for the explicit given url",
|
|
95
|
-
action="store_true",
|
|
96
|
-
default=False,
|
|
97
|
-
),
|
|
98
|
-
ArgSpec(
|
|
99
|
-
name="range",
|
|
100
|
-
flags=["-r", "--range"],
|
|
101
|
-
group="optional query parameters",
|
|
102
|
-
help="range in years to search",
|
|
103
|
-
prompt="Range in years to search",
|
|
104
|
-
type=int,
|
|
105
|
-
advanced=True,
|
|
106
|
-
),
|
|
107
|
-
ArgSpec(
|
|
108
|
-
name="start",
|
|
109
|
-
flags=["--start"],
|
|
110
|
-
group="optional query parameters",
|
|
111
|
-
help="start timestamp format: YYYYMMDDHHMMSS",
|
|
112
|
-
type=int,
|
|
113
|
-
),
|
|
114
|
-
ArgSpec(
|
|
115
|
-
name="end",
|
|
116
|
-
flags=["--end"],
|
|
117
|
-
group="optional query parameters",
|
|
118
|
-
help="end timestamp format: YYYYMMDDHHMMSS",
|
|
119
|
-
type=int,
|
|
120
|
-
),
|
|
121
|
-
ArgSpec(
|
|
122
|
-
name="limit",
|
|
123
|
-
flags=["--limit"],
|
|
124
|
-
group="optional query parameters",
|
|
125
|
-
help="limit the number of snapshots to download",
|
|
126
|
-
prompt="Limit number of snapshots",
|
|
127
|
-
type=int,
|
|
128
|
-
action="optional_value",
|
|
129
|
-
const=True,
|
|
130
|
-
metavar="int",
|
|
131
|
-
advanced=True,
|
|
132
|
-
),
|
|
133
|
-
ArgSpec(
|
|
134
|
-
name="filetype",
|
|
135
|
-
flags=["--filetype"],
|
|
136
|
-
group="optional query parameters",
|
|
137
|
-
help="filetypes to download comma separated (js,css,...)",
|
|
138
|
-
type=str,
|
|
139
|
-
),
|
|
140
|
-
ArgSpec(
|
|
141
|
-
name="statuscode",
|
|
142
|
-
flags=["--statuscode"],
|
|
143
|
-
group="optional query parameters",
|
|
144
|
-
help="statuscodes to download comma separated (200,404,...)",
|
|
145
|
-
type=str,
|
|
146
|
-
),
|
|
147
|
-
# behavior
|
|
148
|
-
ArgSpec(
|
|
149
|
-
name="output",
|
|
150
|
-
flags=["-o", "--output"],
|
|
151
|
-
group="manipulate behavior",
|
|
152
|
-
help="output for all files - defaults to current directory",
|
|
153
|
-
prompt="Output directory",
|
|
154
|
-
type=str,
|
|
155
|
-
advanced=True,
|
|
156
|
-
),
|
|
157
|
-
ArgSpec(
|
|
158
|
-
name="metadata",
|
|
159
|
-
flags=["-m", "--metadata"],
|
|
160
|
-
group="manipulate behavior",
|
|
161
|
-
help="change directory for db/cdx/csv/log files",
|
|
162
|
-
type=str,
|
|
163
|
-
),
|
|
164
|
-
ArgSpec(
|
|
165
|
-
name="verbose",
|
|
166
|
-
flags=["-v", "--verbose"],
|
|
167
|
-
group="manipulate behavior",
|
|
168
|
-
help="verbosity level: low, default, high (default if flag set without value)",
|
|
169
|
-
type=str,
|
|
170
|
-
action="optional_value",
|
|
171
|
-
const="default",
|
|
172
|
-
),
|
|
173
|
-
ArgSpec(
|
|
174
|
-
name="log",
|
|
175
|
-
flags=["--log"],
|
|
176
|
-
group="manipulate behavior",
|
|
177
|
-
help="save a log file into the output folder",
|
|
178
|
-
prompt="Save log file?",
|
|
179
|
-
action="store_true",
|
|
180
|
-
default=False,
|
|
181
|
-
advanced=True,
|
|
182
|
-
),
|
|
183
|
-
ArgSpec(
|
|
184
|
-
name="progress",
|
|
185
|
-
flags=["--progress"],
|
|
186
|
-
group="manipulate behavior",
|
|
187
|
-
help="show a progress bar",
|
|
188
|
-
prompt="Show progress bar?",
|
|
189
|
-
action="store_true",
|
|
190
|
-
default=False,
|
|
191
|
-
advanced=True,
|
|
192
|
-
),
|
|
193
|
-
ArgSpec(
|
|
194
|
-
name="no_redirect",
|
|
195
|
-
flags=["--no-redirect"],
|
|
196
|
-
group="manipulate behavior",
|
|
197
|
-
help="do not follow redirects by archive.org",
|
|
198
|
-
action="store_true",
|
|
199
|
-
default=False,
|
|
200
|
-
),
|
|
201
|
-
ArgSpec(
|
|
202
|
-
name="retry",
|
|
203
|
-
flags=["--retry"],
|
|
204
|
-
group="manipulate behavior",
|
|
205
|
-
help="retry failed downloads (opt tries as int, else infinite)",
|
|
206
|
-
type=int,
|
|
207
|
-
default=0,
|
|
208
|
-
),
|
|
209
|
-
ArgSpec(
|
|
210
|
-
name="workers",
|
|
211
|
-
flags=["--workers"],
|
|
212
|
-
group="manipulate behavior",
|
|
213
|
-
help="number of workers (simultaneous downloads)",
|
|
214
|
-
prompt="Workers (parallel downloads)",
|
|
215
|
-
type=int,
|
|
216
|
-
default=1,
|
|
217
|
-
advanced=True,
|
|
218
|
-
),
|
|
219
|
-
ArgSpec(
|
|
220
|
-
name="delay",
|
|
221
|
-
flags=["--delay"],
|
|
222
|
-
group="manipulate behavior",
|
|
223
|
-
help="delay between each download in seconds",
|
|
224
|
-
type=int,
|
|
225
|
-
default=0,
|
|
226
|
-
),
|
|
227
|
-
ArgSpec(
|
|
228
|
-
name="wait",
|
|
229
|
-
flags=["--wait"],
|
|
230
|
-
group="manipulate behavior",
|
|
231
|
-
help="seconds to wait before renewing connection after HTTP errors or snapshot download errors (default: 15)",
|
|
232
|
-
type=int,
|
|
233
|
-
default=15,
|
|
234
|
-
),
|
|
235
|
-
# special
|
|
236
|
-
ArgSpec(
|
|
237
|
-
name="reset",
|
|
238
|
-
flags=["--reset"],
|
|
239
|
-
group="special",
|
|
240
|
-
help="reset the job and ignore existing cdx/db/csv files",
|
|
241
|
-
action="store_true",
|
|
242
|
-
default=False,
|
|
243
|
-
),
|
|
244
|
-
ArgSpec(
|
|
245
|
-
name="keep",
|
|
246
|
-
flags=["--keep"],
|
|
247
|
-
group="special",
|
|
248
|
-
help="keep all files after the job finished",
|
|
249
|
-
action="store_true",
|
|
250
|
-
default=False,
|
|
251
|
-
),
|
|
252
|
-
]
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def default_args() -> dict:
|
|
256
|
-
"""Return a dict of {name: default} for every spec — the canonical empty arg payload."""
|
|
257
|
-
return {spec.name: spec.default for spec in ARG_SPECS}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|