pywaybackup 1.3.2__tar.gz → 1.4.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.
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/.github/ISSUE_TEMPLATE/bug.md +3 -1
- {pywaybackup-1.3.2/pywaybackup.egg-info → pywaybackup-1.4.1}/PKG-INFO +14 -4
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/README.md +13 -3
- pywaybackup-1.4.1/pywaybackup/Arguments.py +102 -0
- pywaybackup-1.4.1/pywaybackup/Converter.py +182 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/pywaybackup/SnapshotCollection.py +2 -2
- pywaybackup-1.4.1/pywaybackup/Verbosity.py +114 -0
- pywaybackup-1.4.1/pywaybackup/__version__.py +1 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/pywaybackup/archive.py +150 -149
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/pywaybackup/helper.py +20 -5
- pywaybackup-1.4.1/pywaybackup/main.py +37 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1/pywaybackup.egg-info}/PKG-INFO +14 -4
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/pywaybackup.egg-info/SOURCES.txt +2 -1
- pywaybackup-1.3.2/pywaybackup/Verbosity.py +0 -50
- pywaybackup-1.3.2/pywaybackup/__version__.py +0 -1
- pywaybackup-1.3.2/pywaybackup/arguments.py +0 -45
- pywaybackup-1.3.2/pywaybackup/main.py +0 -63
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/.gitignore +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/LICENSE +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/dev/pip_build.sh +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/dev/venv_create.sh +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/pywaybackup/Exception.py +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/pywaybackup/__init__.py +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/pywaybackup.egg-info/dependency_links.txt +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/pywaybackup.egg-info/entry_points.txt +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/pywaybackup.egg-info/requires.txt +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/pywaybackup.egg-info/top_level.txt +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/requirements.txt +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/setup.cfg +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/setup.py +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/test/cdx.example.com.200 +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/test/cdx.example.com.404 +0 -0
- {pywaybackup-1.3.2 → pywaybackup-1.4.1}/test/cdx.example.com.small.200 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pywaybackup
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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
|
|
@@ -99,12 +99,22 @@ Disables following redirects of snapshots. Useful for preventing timestamp-folde
|
|
|
99
99
|
|
|
100
100
|
- **`--verbosity`** `<level>`:<br>
|
|
101
101
|
Sets verbosity level. Options are `json` (prints JSON response) or `progress` (shows progress bar).
|
|
102
|
+
<!-- Alternatively set verbosity level to `trace` for a very detailed output. -->
|
|
103
|
+
|
|
104
|
+
- **`--log`** `<path>`:<br>
|
|
105
|
+
Path defaults to output-dir. Saves a log file with the output of the tool. Named as `waybackup_<sanitized_url>.log`.
|
|
106
|
+
|
|
107
|
+
- **`--workers`** `<count>`:<br>
|
|
108
|
+
Sets the number of simultaneous download workers. Default is 1, safe range is about 10. Be cautious as too many workers may lead to refused connections from the Wayback Machine.
|
|
102
109
|
|
|
103
110
|
- **`--retry`** `<attempts>`:<br>
|
|
104
111
|
Specifies number of retry attempts for failed downloads.
|
|
105
|
-
|
|
106
|
-
- **`--
|
|
107
|
-
|
|
112
|
+
|
|
113
|
+
- **`--delay`** `<seconds>`:<br>
|
|
114
|
+
Specifies delay between download requests in seconds. Default is no delay (0).
|
|
115
|
+
|
|
116
|
+
<!-- - **`--convert-links`**:<br>
|
|
117
|
+
If set, all links in the downloaded files will be converted to local links. This is useful for offline browsing. The links are converted to the local path structure. Show output with `--verbosity trace`. -->
|
|
108
118
|
|
|
109
119
|
**CDX Query Handling:**
|
|
110
120
|
- **`--cdxbackup`** `<path>`:<br>
|
|
@@ -83,12 +83,22 @@ Disables following redirects of snapshots. Useful for preventing timestamp-folde
|
|
|
83
83
|
|
|
84
84
|
- **`--verbosity`** `<level>`:<br>
|
|
85
85
|
Sets verbosity level. Options are `json` (prints JSON response) or `progress` (shows progress bar).
|
|
86
|
+
<!-- Alternatively set verbosity level to `trace` for a very detailed output. -->
|
|
87
|
+
|
|
88
|
+
- **`--log`** `<path>`:<br>
|
|
89
|
+
Path defaults to output-dir. Saves a log file with the output of the tool. Named as `waybackup_<sanitized_url>.log`.
|
|
90
|
+
|
|
91
|
+
- **`--workers`** `<count>`:<br>
|
|
92
|
+
Sets the number of simultaneous download workers. Default is 1, safe range is about 10. Be cautious as too many workers may lead to refused connections from the Wayback Machine.
|
|
86
93
|
|
|
87
94
|
- **`--retry`** `<attempts>`:<br>
|
|
88
95
|
Specifies number of retry attempts for failed downloads.
|
|
89
|
-
|
|
90
|
-
- **`--
|
|
91
|
-
|
|
96
|
+
|
|
97
|
+
- **`--delay`** `<seconds>`:<br>
|
|
98
|
+
Specifies delay between download requests in seconds. Default is no delay (0).
|
|
99
|
+
|
|
100
|
+
<!-- - **`--convert-links`**:<br>
|
|
101
|
+
If set, all links in the downloaded files will be converted to local links. This is useful for offline browsing. The links are converted to the local path structure. Show output with `--verbosity trace`. -->
|
|
92
102
|
|
|
93
103
|
**CDX Query Handling:**
|
|
94
104
|
- **`--cdxbackup`** `<path>`:<br>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
from pywaybackup.helper import url_split, sanitize_filename
|
|
7
|
+
|
|
8
|
+
from pywaybackup.__version__ import __version__
|
|
9
|
+
|
|
10
|
+
class Arguments:
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
|
|
14
|
+
parser = argparse.ArgumentParser(description='Download from wayback machine (archive.org)')
|
|
15
|
+
parser.add_argument('-a', '--about', action='version', version='%(prog)s ' + __version__ + ' by @bitdruid -> https://github.com/bitdruid')
|
|
16
|
+
parser.add_argument('-d', '--debug', action='store_true', help='Debug mode (Always full traceback and creates an error.log')
|
|
17
|
+
|
|
18
|
+
required = parser.add_argument_group('required (one exclusive)')
|
|
19
|
+
required.add_argument('-u', '--url', type=str, metavar="", help='url (with subdir/subdomain) to download')
|
|
20
|
+
exclusive_required = required.add_mutually_exclusive_group(required=True)
|
|
21
|
+
exclusive_required.add_argument('-c', '--current', action='store_true', help='download the latest version of each file snapshot')
|
|
22
|
+
exclusive_required.add_argument('-f', '--full', action='store_true', help='download snapshots of all timestamps')
|
|
23
|
+
exclusive_required.add_argument('-s', '--save', action='store_true', help='save a page to the wayback machine')
|
|
24
|
+
|
|
25
|
+
optional = parser.add_argument_group('optional query parameters')
|
|
26
|
+
optional.add_argument('-l', '--list', action='store_true', help='only print snapshots (opt range in y)')
|
|
27
|
+
optional.add_argument('-e', '--explicit', action='store_true', help='search only for the explicit given url')
|
|
28
|
+
optional.add_argument('-o', '--output', type=str, metavar="", help='output folder - defaults to current directory')
|
|
29
|
+
optional.add_argument('-r', '--range', type=int, metavar="", help='range in years to search')
|
|
30
|
+
optional.add_argument('--start', type=int, metavar="", help='start timestamp format: YYYYMMDDhhmmss')
|
|
31
|
+
optional.add_argument('--end', type=int, metavar="", help='end timestamp format: YYYYMMDDhhmmss')
|
|
32
|
+
|
|
33
|
+
special = parser.add_argument_group('manipulate behavior')
|
|
34
|
+
special.add_argument('--csv', type=str, nargs='?', const=True, metavar='path', help='save a csv file with the json output - defaults to output folder')
|
|
35
|
+
special.add_argument('--skip', type=str, nargs='?', const=True, metavar='path', help='skips existing files in the output folder by checking the .csv file - defaults to output folder')
|
|
36
|
+
special.add_argument('--no-redirect', action='store_true', help='do not follow redirects by archive.org')
|
|
37
|
+
special.add_argument('--verbosity', type=str, default="info", metavar="", help='["progress", "json"] for different output or ["trace"] for very detailed output')
|
|
38
|
+
special.add_argument('--log', type=str, nargs='?', const=True, metavar='path', help='save a log file - defaults to output folder')
|
|
39
|
+
special.add_argument('--retry', type=int, default=0, metavar="", help='retry failed downloads (opt tries as int, else infinite)')
|
|
40
|
+
special.add_argument('--workers', type=int, default=1, metavar="", help='number of workers (simultaneous downloads)')
|
|
41
|
+
# special.add_argument('--convert-links', action='store_true', help='Convert all links in the files to local paths. Requires -c/--current')
|
|
42
|
+
special.add_argument('--delay', type=int, default=0, metavar="", help='delay between each download in seconds')
|
|
43
|
+
|
|
44
|
+
cdx = parser.add_argument_group('cdx (one exclusive)')
|
|
45
|
+
exclusive_cdx = cdx.add_mutually_exclusive_group()
|
|
46
|
+
exclusive_cdx.add_argument('--cdxbackup', type=str, nargs='?', const=True, metavar='path', help='Save the cdx query-result to a file for recurent use - defaults to output folder')
|
|
47
|
+
exclusive_cdx.add_argument('--cdxinject', type=str, nargs='?', const=True, metavar='path', help='Inject a cdx backup-file to download according to the given url')
|
|
48
|
+
|
|
49
|
+
auto = parser.add_argument_group('auto')
|
|
50
|
+
auto.add_argument('--auto', action='store_true', help='includes automatic csv, skip and cdxbackup/cdxinject to resume a stopped download')
|
|
51
|
+
|
|
52
|
+
args = parser.parse_args(args=None if sys.argv[1:] else ['--help']) # if no arguments are given, print help
|
|
53
|
+
|
|
54
|
+
# if args.convert_links and not args.current:
|
|
55
|
+
# parser.error("--convert-links can only be used with the -c/--current option")
|
|
56
|
+
|
|
57
|
+
self.args = args
|
|
58
|
+
|
|
59
|
+
def get_args(self):
|
|
60
|
+
return self.args
|
|
61
|
+
|
|
62
|
+
class Configuration:
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def init(cls):
|
|
66
|
+
|
|
67
|
+
cls.args = Arguments().get_args()
|
|
68
|
+
for key, value in vars(cls.args).items():
|
|
69
|
+
setattr(Configuration, key, value)
|
|
70
|
+
|
|
71
|
+
# args now attributes of Configuration // Configuration.output, ...
|
|
72
|
+
cls.command = ' '.join(sys.argv[1:])
|
|
73
|
+
cls.domain, cls.subdir, cls.filename = url_split(cls.url)
|
|
74
|
+
|
|
75
|
+
if cls.output is None:
|
|
76
|
+
cls.output = os.path.join(os.getcwd(), "waybackup_snapshots")
|
|
77
|
+
os.makedirs(cls.output, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
if cls.log is True:
|
|
80
|
+
cls.log = os.path.join(cls.output, f"waybackup_{sanitize_filename(cls.url)}.log")
|
|
81
|
+
|
|
82
|
+
if cls.full:
|
|
83
|
+
cls.mode = "full"
|
|
84
|
+
if cls.current:
|
|
85
|
+
cls.mode = "current"
|
|
86
|
+
|
|
87
|
+
if cls.auto:
|
|
88
|
+
cls.skip = cls.output
|
|
89
|
+
cls.csv = cls.output
|
|
90
|
+
cls.cdxbackup = cls.output
|
|
91
|
+
cls.cdxinject = os.path.join(cls.output, f"waybackup_{sanitize_filename(cls.url)}.cdx")
|
|
92
|
+
else:
|
|
93
|
+
if cls.skip is True:
|
|
94
|
+
cls.skip = cls.output
|
|
95
|
+
if cls.csv is True:
|
|
96
|
+
cls.csv = cls.output
|
|
97
|
+
if cls.cdxbackup is True:
|
|
98
|
+
cls.cdxbackup = cls.output
|
|
99
|
+
if cls.cdxinject is True:
|
|
100
|
+
cls.cdxinject = cls.output
|
|
101
|
+
|
|
102
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import errno
|
|
3
|
+
import magic
|
|
4
|
+
from pywaybackup.helper import url_split
|
|
5
|
+
|
|
6
|
+
from pywaybackup.Arguments import Configuration as config
|
|
7
|
+
from pywaybackup.Verbosity import Verbosity as vb
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
class Converter:
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def define_root_steps(cls, filepath) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Define the steps (../) to the root directory.
|
|
16
|
+
"""
|
|
17
|
+
abs_path = os.path.abspath(filepath)
|
|
18
|
+
webroot_path = os.path.abspath(f"{config.output}/{config.domain}/") # webroot is the domain folder in the output
|
|
19
|
+
# common path between the two
|
|
20
|
+
common_path = os.path.commonpath([abs_path, webroot_path])
|
|
21
|
+
# steps up to the common path
|
|
22
|
+
rel_path_from_common = os.path.relpath(abs_path, common_path)
|
|
23
|
+
steps_up = rel_path_from_common.count(os.path.sep)
|
|
24
|
+
if steps_up <= 1: # if the file is in the root of the domain
|
|
25
|
+
return "./"
|
|
26
|
+
return "../" * steps_up
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def links(cls, filepath, status_message=None):
|
|
34
|
+
"""
|
|
35
|
+
Convert all links in a HTML / CSS / JS file to local paths.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def extract_urls(content) -> list:
|
|
40
|
+
"""
|
|
41
|
+
Extract all links from a file.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
#content = re.sub(r'\s+', '', content)
|
|
45
|
+
#content = re.sub(r'\n', '', content)
|
|
46
|
+
|
|
47
|
+
html_types = ["src", "href", "poster", "data-src"]
|
|
48
|
+
css_types = ["url"]
|
|
49
|
+
links = []
|
|
50
|
+
for html_type in html_types:
|
|
51
|
+
# possible formatings of the value: "url", 'url', url
|
|
52
|
+
matches = re.findall(f"{html_type}=[\"']?([^\"'>]+)", content)
|
|
53
|
+
links += matches
|
|
54
|
+
for css_type in css_types:
|
|
55
|
+
# possible formatings of the value: url(url) url('url') url("url") // ends with )
|
|
56
|
+
matches = re.findall(rf"{css_type}\((['\"]?)([^'\"\)]+)\1\)", content)
|
|
57
|
+
links += [match[1] for match in matches]
|
|
58
|
+
links = list(set(links))
|
|
59
|
+
return links
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def local_url(original_url, domain, count) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Convert a given url to a local path.
|
|
65
|
+
"""
|
|
66
|
+
original_url_domain = url_split(original_url)[0]
|
|
67
|
+
|
|
68
|
+
# check if the url is external or internal (external is returned as is because no need to convert)
|
|
69
|
+
external = False
|
|
70
|
+
if original_url_domain != domain:
|
|
71
|
+
if "://" in original_url:
|
|
72
|
+
external = True
|
|
73
|
+
if original_url.startswith("//"):
|
|
74
|
+
external = True
|
|
75
|
+
if external:
|
|
76
|
+
status_message.trace(status="", type=f"{count}/{len(links)}", message="External url")
|
|
77
|
+
return original_url
|
|
78
|
+
|
|
79
|
+
# convert the url to a relative path to the local root (download dir) if it's a valid path, else return the original url
|
|
80
|
+
original_url_file = os.path.join(config.output, config.domain, normalize_url(original_url))
|
|
81
|
+
if validate_path(original_url_file):
|
|
82
|
+
if original_url.startswith("/"): # if only starts with /
|
|
83
|
+
original_url = f"{cls.define_root_steps(filepath)}{original_url.lstrip('/')}"
|
|
84
|
+
if original_url.startswith(".//"):
|
|
85
|
+
original_url = f"{cls.define_root_steps(filepath)}{original_url.lstrip('./')}"
|
|
86
|
+
if original_url_domain == domain: # if url is like https://domain.com/path/to/file
|
|
87
|
+
original_url = f"{cls.define_root_steps(filepath)}{original_url.split(domain)[1].lstrip('/')}"
|
|
88
|
+
if original_url.startswith("../"): # if file is already ../ check if it's not too many steps up
|
|
89
|
+
original_url = f"{cls.define_root_steps(filepath)}{original_url.split('../')[-1].lstrip('/')}"
|
|
90
|
+
else:
|
|
91
|
+
status_message.trace(status="", type="", message=f"{count}/{len(links)}: URL is not a valid path")
|
|
92
|
+
|
|
93
|
+
return original_url
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def normalize_url(url) -> str:
|
|
100
|
+
"""
|
|
101
|
+
Normalize a given url by removing it's protocol, domain and parent directorie references.
|
|
102
|
+
|
|
103
|
+
Example1:
|
|
104
|
+
- Example input: https://domain.com/path/to/file
|
|
105
|
+
- Example output: /path/to/file
|
|
106
|
+
|
|
107
|
+
Example2
|
|
108
|
+
- input: ../path/to/file
|
|
109
|
+
- output: /path/to/file
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
url = "/" + url.split("../")[-1]
|
|
113
|
+
except IndexError:
|
|
114
|
+
pass
|
|
115
|
+
if url.startswith("//"):
|
|
116
|
+
url = "/" + url.split("//")[1]
|
|
117
|
+
parsed_url = url_split(url)
|
|
118
|
+
return f"{parsed_url[1]}/{parsed_url[2]}"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def is_pathname_valid(pathname: str) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
Check if a given pathname is valid.
|
|
124
|
+
"""
|
|
125
|
+
if not isinstance(pathname, str) or not pathname:
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
os.lstat(pathname)
|
|
130
|
+
except OSError as exc:
|
|
131
|
+
if exc.errno == errno.ENOENT:
|
|
132
|
+
return True
|
|
133
|
+
elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}:
|
|
134
|
+
return False
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
def is_path_creatable(pathname: str) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Check if a given path is creatable.
|
|
140
|
+
"""
|
|
141
|
+
dirname = os.path.dirname(pathname) or os.getcwd()
|
|
142
|
+
return os.access(dirname, os.W_OK)
|
|
143
|
+
|
|
144
|
+
def is_path_exists_or_creatable(pathname: str) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Check if a given path exists or is creatable.
|
|
147
|
+
"""
|
|
148
|
+
return is_pathname_valid(pathname) or is_path_creatable(pathname)
|
|
149
|
+
|
|
150
|
+
def validate_path(filepath: str) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Validate if a given path can exist.
|
|
153
|
+
"""
|
|
154
|
+
return is_path_exists_or_creatable(filepath)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if os.path.isfile(filepath):
|
|
161
|
+
if magic.from_file(filepath, mime=True).split("/")[1] == "javascript":
|
|
162
|
+
status_message.trace(status="Error", type="", message="JS-file is not supported")
|
|
163
|
+
return
|
|
164
|
+
try:
|
|
165
|
+
with open(filepath, "r") as file:
|
|
166
|
+
domain = config.domain
|
|
167
|
+
content = file.read()
|
|
168
|
+
links = extract_urls(content)
|
|
169
|
+
status_message.store(message=f"\n-----> Convert: [{len(links)}] links in file")
|
|
170
|
+
count = 1
|
|
171
|
+
for original_link in links:
|
|
172
|
+
status_message.trace(status="ORIG", type=f"{count}/{len(links)}", message=original_link)
|
|
173
|
+
new_link = local_url(original_link, domain, count)
|
|
174
|
+
if new_link != original_link:
|
|
175
|
+
status_message.trace(status="CONV", type=f"{count}/{len(links)}", message=new_link)
|
|
176
|
+
content = content.replace(original_link, new_link)
|
|
177
|
+
count += 1
|
|
178
|
+
file = open(filepath, "w")
|
|
179
|
+
file.write(content)
|
|
180
|
+
file.close()
|
|
181
|
+
except UnicodeDecodeError:
|
|
182
|
+
status_message.trace(status="Error", type="", message="Could not decode file to convert links")
|
|
@@ -70,10 +70,10 @@ class SnapshotCollection:
|
|
|
70
70
|
download_dir = os.path.join(output, domain, timestamp, subdir)
|
|
71
71
|
download_file = os.path.abspath(os.path.join(download_dir, filename))
|
|
72
72
|
return download_file
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
|
|
75
75
|
@classmethod
|
|
76
|
-
def
|
|
76
|
+
def entry_modify(cls, collection_entry: dict, key: str, value: str):
|
|
77
77
|
"""
|
|
78
78
|
Modify a key-value pair in a snapshot entry of the collection (dict).
|
|
79
79
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import tqdm
|
|
2
|
+
import json
|
|
3
|
+
from pywaybackup.SnapshotCollection import SnapshotCollection as sc
|
|
4
|
+
|
|
5
|
+
class Verbosity:
|
|
6
|
+
|
|
7
|
+
LEVELS = ["trace", "info"]
|
|
8
|
+
level = None
|
|
9
|
+
|
|
10
|
+
mode = None
|
|
11
|
+
args = None
|
|
12
|
+
pbar = None
|
|
13
|
+
|
|
14
|
+
log = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def init(cls, v_args: list, log=None):
|
|
18
|
+
cls.args = v_args
|
|
19
|
+
cls.log = open(log, "w") if log else None
|
|
20
|
+
if cls.args == "progress":
|
|
21
|
+
cls.mode = "progress"
|
|
22
|
+
elif cls.args == "json":
|
|
23
|
+
cls.mode = "json"
|
|
24
|
+
cls.level = cls.args if cls.args in cls.LEVELS else "info"
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def fini(cls):
|
|
28
|
+
if cls.mode == "progress":
|
|
29
|
+
if cls.pbar is not None:
|
|
30
|
+
cls.pbar.close()
|
|
31
|
+
if cls.mode == "json":
|
|
32
|
+
print(json.dumps(sc.SNAPSHOT_COLLECTION, indent=4, sort_keys=True))
|
|
33
|
+
if cls.log:
|
|
34
|
+
cls.log.close()
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def write(cls, status="", type="", message=""):
|
|
38
|
+
"""
|
|
39
|
+
Write a log line based on the provided status, type, and message.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
status (str): The status of the log line. (e.g. "SUCCESS", "REDIRECT")
|
|
43
|
+
type (str): The type of the log line. (e.g. "URL", "FILE")
|
|
44
|
+
message (str): The message to be logged. (e.g. actual url, file path)
|
|
45
|
+
"""
|
|
46
|
+
logline = cls.generate_logline(status=status, type=type, message=message)
|
|
47
|
+
if cls.mode != "progress" and cls.mode != "json":
|
|
48
|
+
if logline:
|
|
49
|
+
print(logline)
|
|
50
|
+
if cls.log:
|
|
51
|
+
cls.log.write(logline + "\n")
|
|
52
|
+
cls.log.flush()
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def progress(cls, progress: int):
|
|
56
|
+
if cls.mode == "progress":
|
|
57
|
+
if cls.pbar is None and progress == 0:
|
|
58
|
+
maxval = sc.count(collection=True)
|
|
59
|
+
cls.pbar = tqdm.tqdm(total=maxval, desc="Downloading", unit=" snapshot", ascii="░▒█")
|
|
60
|
+
if cls.pbar is not None and progress is not None and progress > 0:
|
|
61
|
+
cls.pbar.update(progress)
|
|
62
|
+
cls.pbar.refresh()
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def generate_logline(cls, status: str = "", type: str = "", message: str = ""):
|
|
66
|
+
|
|
67
|
+
if not status and not type:
|
|
68
|
+
return message
|
|
69
|
+
|
|
70
|
+
status_length = 11
|
|
71
|
+
type_length = 5
|
|
72
|
+
|
|
73
|
+
status = status.ljust(status_length)
|
|
74
|
+
type = type.ljust(type_length)
|
|
75
|
+
|
|
76
|
+
log_entry = f"{status} -> {type}: {message}"
|
|
77
|
+
|
|
78
|
+
return log_entry
|
|
79
|
+
|
|
80
|
+
class Message(Verbosity):
|
|
81
|
+
"""
|
|
82
|
+
Message class representing a message-buffer for the Verbosity class.
|
|
83
|
+
|
|
84
|
+
If a message should be stored and stacked for later output.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self):
|
|
88
|
+
self.message = {}
|
|
89
|
+
|
|
90
|
+
def __str__(self):
|
|
91
|
+
return self.message
|
|
92
|
+
|
|
93
|
+
def store(self, status: str = "", type: str = "", message: str = "", level: str = "info"):
|
|
94
|
+
if level not in self.message:
|
|
95
|
+
self.message[level] = []
|
|
96
|
+
self.message[level].append(super().generate_logline(status, type, message))
|
|
97
|
+
|
|
98
|
+
def clear(self):
|
|
99
|
+
self.message = {}
|
|
100
|
+
|
|
101
|
+
def write(self):
|
|
102
|
+
for level in self.message:
|
|
103
|
+
if self.check_level(level):
|
|
104
|
+
for message in self.message[level]:
|
|
105
|
+
super().write(message=message)
|
|
106
|
+
self.clear()
|
|
107
|
+
|
|
108
|
+
def check_level(self, level: str):
|
|
109
|
+
return super().LEVELS.index(level) >= super().LEVELS.index(self.level)
|
|
110
|
+
|
|
111
|
+
def trace(self, status: str = "", type: str = "", message: str = ""):
|
|
112
|
+
self.store(status, type, message, "trace")
|
|
113
|
+
|
|
114
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.4.1"
|