Skip to content

Commit 1f9a400

Browse files
authored
Merge pull request #64 from nstrydom2/revision
revision
2 parents 877bd77 + 03d6590 commit 1f9a400

File tree

7 files changed

+180
-37
lines changed

7 files changed

+180
-37
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ celerybeat.pid
106106
.venv
107107
env/
108108
venv/
109+
venv1/
109110
ENV/
110111
env.bak/
111112
venv.bak/
@@ -133,6 +134,8 @@ dmypy.json
133134

134135
# untracked dev files
135136
test-*.py
137+
test.txt
138+
topsecret.mkv
136139

137140
# intelliJ’s project specific settings files
138-
.idea
141+
.idea

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
## Version 0.2.7 (2021-8-31)
3+
## Version 0.2.7 (2021-9-12)
44

55
Makes further improvements to the CLI:
66

@@ -9,8 +9,11 @@ Makes further improvements to the CLI:
99
with the `--no-check` method. It's enabled by default and may require further
1010
user-input in case the target directory contains a file with the same name as
1111
the issued download command
12+
- further adds a `--user-agent` and `--proxies` flag to the CLI; these fields were also
13+
added to the `AnonFile` constructor
1214
- implements a preview command to obtain meta data without committing to a time-
1315
consuming download
16+
- commands related to the log file were also added to the CLI
1417

1518
As for the main library, the following changes have been added since the last release:
1619

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ the `AnonFile` API visit [anonfiles.com](https://anonfiles.com/docs/api).
8484
## Command Line Interface
8585

8686
```bash
87-
# get help
88-
anonfile [download|upload] --help
87+
# open help page for specific commands
88+
anonfile [download|upload|preview|log] --help
8989

9090
# note: both methods expect at least one argument, but can take on more
9191
anonfile download --url https://anonfiles.com/93k5x1ucu0/test_txt

src/anonfile/__init__.py

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import argparse
66
import sys
7+
from collections import namedtuple
78
from distutils.util import strtobool
89
from pathlib import Path
910
from typing import List
@@ -12,22 +13,32 @@
1213
from .anonfile import __version__
1314

1415

15-
def __print_dict(dictionary: dict, indent: int=4) -> None:
16-
print("{\n%s\n}" % '\n'.join([f"\033[36m{indent*' '}{key}\033[0m: \033[32m{value}\033[0m" for key, value in dictionary.items()]))
16+
def __print_dict(dictionary: dict, indent: int = 4) -> None:
17+
print("{\n%s\n}" % '\n'.join([f"\033[36m{indent * ' '}{key}\033[0m: \033[32m{value}\033[0m" for key, value in dictionary.items()]))
18+
1719

1820
def __from_file(path: Path) -> List[str]:
1921
with open(path, mode='r', encoding='utf-8') as file_handler:
2022
return [line.rstrip() for line in file_handler.readlines() if line[0] != '#']
2123

24+
25+
def format_proxies(proxies: str) -> dict:
26+
return {prot: f"{prot}://{ip}" for (prot, ip) in [proxy.split('://') for proxy in proxies.split()]}
27+
28+
2229
def main():
2330
parser = argparse.ArgumentParser(prog=package_name)
2431
parser._positionals.title = 'Commands'
2532
parser._optionals.title = 'Arguments'
2633

2734
parser.add_argument('-v', '--version', action='version', version=f"%(prog)s {__version__}")
28-
parser.add_argument('-V', '--verbose', default=True, action=argparse.BooleanOptionalAction, help="increase output verbosity")
29-
parser.add_argument('-l', '--logging', default=True, action=argparse.BooleanOptionalAction, help="enable URL logging")
35+
parser.add_argument('-V', '--verbose', default=True, action='store_true', help="increase output verbosity (default)")
36+
parser.add_argument('--no-verbose', dest='verbose', action='store_false', help="run commands silently")
37+
parser.add_argument('-l', '--logging', default=True, action='store_true', help="enable URL logging (default)")
38+
parser.add_argument('--no-logging', dest='logging', action='store_false', help="disable all logging activities")
3039
parser.add_argument('-t', '--token', type=str, default='secret', help="configure an API token (optional)")
40+
parser.add_argument('-a', '--user-agent', type=str, default=None, help="configure custom User-Agent (optional)")
41+
parser.add_argument('-p', '--proxies', type=str, default=None, help="configure HTTP and/or HTTPS proxies (optional)")
3142

3243
subparser = parser.add_subparsers(dest='command')
3344
upload_parser = subparser.add_parser('upload', help="upload a file to https://anonfiles.com")
@@ -40,15 +51,24 @@ def main():
4051
download_parser.add_argument('-u', '--url', nargs='*', type=str, help="one or more URLs to download")
4152
download_parser.add_argument('-f', '--batch-file', type=Path, nargs='?', help="file containing URLs to download, one URL per line")
4253
download_parser.add_argument('-p', '--path', type=Path, default=Path.cwd(), help="download directory (CWD by default)")
43-
download_parser.add_argument('-c', '--check', default=True, action=argparse.BooleanOptionalAction, help="check for duplicates")
54+
download_parser.add_argument('-c', '--check', default=True, action='store_true', help="check for duplicates (default)")
55+
download_parser.add_argument('--no-check', dest='check', action='store_false', help="disable checking for duplicates")
56+
57+
log_parser = subparser.add_parser('log', help="access the anonfile logger")
58+
log_parser.add_argument('--reset', action='store_true', help="reset all log file entries")
59+
log_parser.add_argument('--path', action='store_true', help="return the log file path")
60+
log_parser.add_argument('--read', action='store_true', help='read the log file')
4461

4562
try:
4663
args = parser.parse_args()
47-
anon = AnonFile(args.token)
64+
anon = AnonFile(args.token, user_agent=args.user_agent, proxies=format_proxies(args.proxies) if args.proxies else None)
4865

4966
if args.command is None:
5067
raise UserWarning("missing a command")
5168

69+
if args.user_agent is not None:
70+
anon.user_agent = args.user_agent
71+
5272
if args.command == 'upload':
5373
for file in args.file:
5474
upload = anon.upload(file, progressbar=args.verbose, enable_logging=args.logging)
@@ -57,7 +77,7 @@ def main():
5777
if args.command == 'preview':
5878
for url in args.url:
5979
preview = anon.preview(url)
60-
values = ['online' if preview.status else 'offline', preview.file_path.name, preview.url.geturl(), preview.ddl, preview.id, f"{preview.size}B"]
80+
values = ['online' if preview.status else 'offline', preview.file_path.name, preview.url.geturl(), preview.ddl.geturl(), preview.id, f"{preview.size}B"]
6181

6282
if args.verbose:
6383
__print_dict(dict(zip(['Status', 'File Path', 'URL', 'DDL', 'ID', 'Size'], values)))
@@ -76,6 +96,30 @@ def main():
7696
else:
7797
print(f"File: {download(url).file_path}")
7898

99+
if args.command == 'log':
100+
if args.reset:
101+
open(get_logfile_path(), mode='w', encoding='utf-8').close()
102+
if args.path:
103+
print(get_logfile_path())
104+
if args.read:
105+
with open(get_logfile_path(), mode='r', encoding='utf-8') as file_handler:
106+
log = file_handler.readlines()
107+
108+
if not log:
109+
msg = "Nothing to read because the log file is empty"
110+
print(f"\033[33m{'[ WARNING ]'.ljust(12, ' ')}\033[0m{msg}")
111+
return
112+
113+
parse = lambda line: line.strip('\n').split('::')
114+
Entry = namedtuple('Entry', 'timestamp method url')
115+
116+
tabulate = "{:<19} {:<8} {:<30}".format
117+
118+
print(f"\033[32m{tabulate('Date', 'Method', 'URL')}\033[0m")
119+
120+
for line in log:
121+
entry = Entry(parse(line)[0], parse(line)[1], parse(line)[2])
122+
print(tabulate(entry.timestamp, entry.method, entry.url))
79123

80124
except UserWarning as bad_human:
81125
print(f"error: {bad_human}")

src/anonfile/anonfile.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
import sys
3535
from dataclasses import dataclass
3636
from pathlib import Path
37-
from typing import List, Tuple
37+
from typing import List, Tuple, Union
3838
from urllib.parse import ParseResult, urljoin, urlparse
3939
from urllib.request import getproxies
4040

@@ -89,9 +89,13 @@ def get_logfile_path() -> Path:
8989

9090
@dataclass(frozen=True)
9191
class ParseResponse:
92+
"""
93+
Data class that is primarily used as a structured return type for the upload,
94+
preview and download methods.
95+
"""
9296
response: Response
9397
file_path: Path
94-
ddl: str
98+
ddl: ParseResult
9599

96100
@property
97101
def json(self) -> dict:
@@ -103,7 +107,7 @@ def json(self) -> dict:
103107
@property
104108
def status(self) -> bool:
105109
"""
106-
Return the upload status. If `false`, an error message indicating the
110+
Return the upload status. If `False`, an error message indicating the
107111
cause for the malfunction will be redirected to `sys.stderr`.
108112
"""
109113
status = bool(self.json['status'])
@@ -146,32 +150,61 @@ def size(self) -> int:
146150
"""
147151
return int(self.json['data']['file']['metadata']['size']['bytes'])
148152

153+
def __str__(self) -> str:
154+
return str(self.name)
155+
156+
def __repr__(self) -> str:
157+
return f"{self.__class__.__name__}(ID={self.id})"
158+
149159
#endregion
150160

151161
class AnonFile:
162+
"""
163+
The unofficial Python API for https://anonfiles.com.
164+
165+
Basic Usage
166+
-----------
167+
168+
```
169+
from anonfile import AnonFile
170+
171+
anon = AnonFile()
172+
preview = anon.preview('https://anonfiles.com/b7NaVd0cu3/topsecret_mkv')
173+
174+
# topsecret.mkv
175+
print(preview)
176+
```
177+
178+
Docs
179+
----
180+
See full documentation at <https://www.hentai-chan.dev/projects/anonfile>.
181+
"""
152182
_timeout = (5, 5)
153183
_total = 5
154184
_status_forcelist = [413, 429, 500, 502, 503, 504]
155185
_backoff_factor = 1
156186
_user_agent = None
187+
_proxies = None
157188

158189
API = "https://api.anonfiles.com/"
159190

160-
__slots__ = ['endpoint', 'token', 'timeout', 'total', 'status_forcelist', 'backoff_factor', 'user_agent']
191+
__slots__ = ['endpoint', 'token', 'timeout', 'total', 'status_forcelist', 'backoff_factor', 'user_agent', 'proxies']
161192

162193
def __init__(self,
163194
token: str="undefined",
164195
timeout: Tuple[float,float]=_timeout,
165196
total: int=_total,
166197
status_forcelist: List[int]=_status_forcelist,
167198
backoff_factor: int=_backoff_factor,
168-
user_agent: str=_user_agent) -> AnonFile:
199+
user_agent: str=_user_agent,
200+
proxies: dict=_proxies) -> AnonFile:
169201
self.token = token
170202
self.timeout = timeout
171203
self.total = total,
172204
self.status_forcelist = status_forcelist,
173205
self.backoff_factor = backoff_factor
174206
self.user_agent = user_agent
207+
self.proxies = proxies
175208

176209
@staticmethod
177210
def __progressbar_options(iterable, desc, unit, color: str="\033[32m", char='\u25CB', total=None, disable=False) -> dict:
@@ -219,7 +252,7 @@ def __get(self, url: str, **kwargs) -> Response:
219252
Returns the GET request encoded in `utf-8`. Adds proxies to this session
220253
on the fly if urllib is able to pick up the system's proxy settings.
221254
"""
222-
response = self.session.get(url, timeout=self.timeout, proxies=getproxies(), **kwargs)
255+
response = self.session.get(url, timeout=self.timeout, proxies=self.proxies or getproxies(), **kwargs)
223256
response.encoding = 'utf-8'
224257
return response
225258

@@ -231,7 +264,7 @@ def __callback(monitor: MultipartEncoderMonitor, tqdm_handler: tqdm):
231264
tqdm_handler.total = monitor.len
232265
tqdm_handler.update(monitor.bytes_read - tqdm_handler.n)
233266

234-
def upload(self, path: str, progressbar: bool=False, enable_logging: bool=False) -> ParseResponse:
267+
def upload(self, path: Union[str, Path], progressbar: bool=False, enable_logging: bool=False) -> ParseResponse:
235268
"""
236269
Upload a file located in `path` to http://anonfiles.com. Set
237270
`enable_logging` to `True` to store the URL in a global config file.
@@ -252,13 +285,14 @@ def upload(self, path: str, progressbar: bool=False, enable_logging: bool=False)
252285
Note
253286
----
254287
Although `anonfile` offers unlimited bandwidth, uploads cannot exceed a
255-
file size of 20GB in theory. Due to technical difficulties in the implementation
288+
file size of 20GB in theory. Due to technical difficulties in the implementation,
256289
the upper cap occurs much earlier at around 500MB.
257290
"""
291+
path = Path(path)
258292
size = os.stat(path).st_size
259-
options = AnonFile.__progressbar_options(None, f"Upload: {Path(path).name}", unit='B', total=size, disable=progressbar)
293+
options = AnonFile.__progressbar_options(None, f"Upload: {path.name}", unit='B', total=size, disable=progressbar)
260294
with open(path, mode='rb') as file_handler:
261-
fields = {'file': (Path(path).name, file_handler, 'application/octet-stream')}
295+
fields = {'file': (path.name, file_handler, 'application/octet-stream')}
262296
with tqdm(**options) as tqdm_handler:
263297
encoder_monitor = MultipartEncoderMonitor.from_fields(fields, callback=lambda monitor: AnonFile.__callback(monitor, tqdm_handler))
264298
response = self.session.post(
@@ -271,9 +305,9 @@ def upload(self, path: str, progressbar: bool=False, enable_logging: bool=False)
271305
verify=True
272306
)
273307
logger.log(logging.INFO if enable_logging else logging.NOTSET, "upload::%s", response.json()['data']['file']['url']['full'])
274-
return ParseResponse(response, Path(path), None)
308+
return ParseResponse(response, path, None)
275309

276-
def preview(self, url: str, path: Path=Path.cwd()) -> ParseResponse:
310+
def preview(self, url: str, path: Union[str, Path]=Path.cwd()) -> ParseResponse:
277311
"""
278312
Obtain meta data associated with this `url` without commiting to a time-
279313
consuming download.
@@ -293,11 +327,11 @@ def preview(self, url: str, path: Path=Path.cwd()) -> ParseResponse:
293327
"""
294328
with self.__get(urljoin(AnonFile.API, f"v2/file/{urlparse(url).path.split('/')[1]}/info")) as response:
295329
links = re.findall(r'''.*?href=['"](.*?)['"].*?''', html.unescape(self.__get(url).text), re.I)
296-
ddl = next(filter(lambda link: 'cdn-' in link, links))
297-
file_path = path.joinpath(Path(urlparse(ddl).path).name)
330+
ddl = urlparse(next(filter(lambda link: 'cdn-' in link, links)))
331+
file_path = Path(path).joinpath(Path(ddl.path).name)
298332
return ParseResponse(response, file_path, ddl)
299333

300-
def download(self, url: str, path: Path=Path.cwd(), progressbar: bool=False, enable_logging: bool=False) -> ParseResponse:
334+
def download(self, url: str, path: Union[str, Path]=Path.cwd(), progressbar: bool=False, enable_logging: bool=False) -> ParseResponse:
301335
"""
302336
Download a file from https://anonfiles.com given a `url`. Set the download
303337
directory in `path` (uses the current working directory by default). Set
@@ -328,7 +362,7 @@ def download(self, url: str, path: Path=Path.cwd(), progressbar: bool=False, ena
328362
options = AnonFile.__progressbar_options(None, f"Download {download.id}", unit='B', total=download.size, disable=progressbar)
329363
with open(download.file_path, mode='wb') as file_handler:
330364
with tqdm(**options) as tqdm_handler:
331-
with self.__get(download.ddl, stream=True) as response:
365+
with self.__get(download.ddl.geturl(), stream=True) as response:
332366
for chunk in response.iter_content(1024*1024):
333367
tqdm_handler.update(len(chunk))
334368
file_handler.write(chunk)

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ def pytest_addoption(parser):
88
help="Secret anonfiles.com API token."
99
)
1010

11+
1112
def pytest_generate_tests(metafunc):
1213
if 'token' in metafunc.fixturenames:
1314
metafunc.parametrize('token', metafunc.config.getoption('token'))

0 commit comments

Comments
 (0)