Compare commits
No commits in common. "main" and "v1.0.1" have entirely different histories.
52
README.md
52
README.md
@ -2,13 +2,6 @@
|
|||||||
|
|
||||||
The [ICANN Centralized Zone Data Service](https://czds.icann.org) *(CZDS)* allows *approved* users to request and download DNS zone files in bulk, provided they represent a legitimate company or academic institution and their intended use is legal and ethical. Once ICANN approves the request, this tool streamlines the retrieval of extensive domain name system data, facilitating research and security analysis in the realm of internet infrastructure.
|
The [ICANN Centralized Zone Data Service](https://czds.icann.org) *(CZDS)* allows *approved* users to request and download DNS zone files in bulk, provided they represent a legitimate company or academic institution and their intended use is legal and ethical. Once ICANN approves the request, this tool streamlines the retrieval of extensive domain name system data, facilitating research and security analysis in the realm of internet infrastructure.
|
||||||
|
|
||||||
## Features
|
|
||||||
* Asynchronous downloads with configurable concurrency
|
|
||||||
* Support for both CSV and JSON report formats
|
|
||||||
* Optional gzip decompression of zone files
|
|
||||||
* Environment variable support for credentials
|
|
||||||
* Comprehensive error handling and logging
|
|
||||||
|
|
||||||
## Zone Information
|
## Zone Information
|
||||||
Zone files are updated once every 24 hours, specifically from 00:00 UTC to 06:00 UTC. Access to these zones is granted in increments, and the total time for approval across all zones may extend to a month or longer. It is typical for more than 90% of requested zones to receive approval. Access to certain zone files may require additional application forms with the TLD organization. Please be aware that access to certain zones is time-bound, expiring at the beginning of the following year, or up to a decade after the initial approval has been confirmed.
|
Zone files are updated once every 24 hours, specifically from 00:00 UTC to 06:00 UTC. Access to these zones is granted in increments, and the total time for approval across all zones may extend to a month or longer. It is typical for more than 90% of requested zones to receive approval. Access to certain zone files may require additional application forms with the TLD organization. Please be aware that access to certain zones is time-bound, expiring at the beginning of the following year, or up to a decade after the initial approval has been confirmed.
|
||||||
|
|
||||||
@ -22,53 +15,36 @@ pip install czds-api
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
### Command Line Interface
|
###### Command line
|
||||||
```bash
|
```bash
|
||||||
czds [-h] [-u USERNAME] [-p PASSWORD] [-z] [-c CONCURRENCY] [-d] [-k] [-r] [-s] [-f {csv,json}] [-o OUTPUT]
|
czds [--username <username> --password <password>] [--concurrency <int>]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Arguments
|
You can also set the `CZDS_USER` & `CZDS_PASS` environment variables to automatically authenticate:
|
||||||
| Argument | Description | Default |
|
|
||||||
|-----------------------|----------------------------------------------|-------------------|
|
|
||||||
| `-h`, `--help` | Show help message and exit | |
|
|
||||||
| `-u`, `--username` | ICANN Username | `$CZDS_USER` |
|
|
||||||
| `-p`, `--password` | ICANN Password | `$CZDS_PASS` |
|
|
||||||
| `-z`, `--zones` | Download zone files | |
|
|
||||||
| `-c`, `--concurrency` | Number of concurrent downloads | `3` |
|
|
||||||
| `-d`, `--decompress` | Decompress zone files after download | |
|
|
||||||
| `-k`, `--keep` | Keep original gzip files after decompression | |
|
|
||||||
| `-r`, `--report` | Download the zone stats report | |
|
|
||||||
| `-s`, `--scrub` | Scrub username from the report | |
|
|
||||||
| `-f`, `--format` | Report output format (csv/json) | `csv` |
|
|
||||||
| `-o`, `--output` | Output directory | Current directory |
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
```bash
|
```bash
|
||||||
export CZDS_USER='your_username'
|
export CZDS_USER='your_username'
|
||||||
export CZDS_PASS='your_password'
|
export CZDS_PASS='your_password'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Python Module
|
###### As a Python module
|
||||||
```python
|
```python
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from czds import CZDS
|
from czds import CZDS
|
||||||
|
|
||||||
async with CZDS(username, password) as client:
|
CZDS_client = CZDS(username, password)
|
||||||
# Download zone stats report
|
|
||||||
await client.get_report('report.csv', scrub=True, format='json')
|
|
||||||
|
|
||||||
# Download zone files
|
CZDS_client.download_report('report.csv')
|
||||||
zone_links = await client.fetch_zone_links()
|
|
||||||
await client.download_zones(zone_links, 'zones', concurrency=3, decompress=True)
|
zone_links = CZDS_client.fetch_zone_links()
|
||||||
|
|
||||||
|
os.makedirs('zones', exist_ok=True)
|
||||||
|
|
||||||
|
for zone_link in zone_links:
|
||||||
|
CZDS_client.download_zone(zone_link, 'zones')
|
||||||
```
|
```
|
||||||
|
|
||||||
## Zone Information
|
|
||||||
Zone files are updated once every 24 hours, specifically from 00:00 UTC to 06:00 UTC. Access to these zones is granted in increments, and the total time for approval across all zones may extend to a month or longer. It is typical for more than 90% of requested zones to receive approval. Access to certain zone files may require additional application forms with the TLD organization. Please be aware that access to certain zones is time-bound, expiring at the beginning of the following year, or up to a decade after the initial approval has been confirmed.
|
|
||||||
|
|
||||||
At the time of writing this repository, the CZDS offers access to 1,151 zones in total.
|
|
||||||
|
|
||||||
1,079 have been approved, 55 are still pending *(after 3 months)*, 10 have been revoked because the TLDs are longer active, and 6 have been denied. Zones that have expired automatically had the expiration extended for me without doing anything, aside from 13 zones that remained expired. I have included a recent [stats file](./extras/stats.csv) directly from my ICANN account.
|
|
||||||
|
|
||||||
## Respects & extras
|
## Respects & extras
|
||||||
While ICANN does have an official [czds-api-client-python](https://github.com/icann/czds-api-client-python) repository, I rewrote it from scratch to be more streamline & included a [POSIX version](./extras/czds) for portability. There is some [official documentation](https://raw.githubusercontent.com/icann/czds-api-client-java/master/docs/ICANN_CZDS_api.pdf) that was referenced in the creation of the POSIX version. Either way, big props to ICANN for allowing me to use the CZDS for research purposes!
|
While ICANN does have an official [czds-api-client-python](https://github.com/icann/czds-api-client-python) repository, I rewrote it from scratch to be more streamline & included a [POSIX version](./extras/czds) for portability. There is some [official documentation](https://raw.githubusercontent.com/icann/czds-api-client-java/master/docs/ICANN_CZDS_api.pdf) that was referenced in the creation of the POSIX version. Either way, big props to ICANN for allowing me to use the CZDS for research purposes!
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
from .client import CZDS
|
from .client import CZDS
|
||||||
|
|
||||||
|
|
||||||
__version__ = '1.1.0'
|
__version__ = '1.0.1'
|
||||||
__author__ = 'acidvegas'
|
__author__ = 'acidvegas'
|
||||||
__email__ = 'acid.vegas@acid.vegas'
|
__email__ = 'acid.vegas@acid.vegas'
|
||||||
__github__ = 'https://github.com/acidvegas/czds'
|
__github__ = 'https://github.com/acidvegas/czds'
|
105
czds/__main__.py
105
czds/__main__.py
@ -3,7 +3,7 @@
|
|||||||
# czds/__main__.py
|
# czds/__main__.py
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import concurrent.futures
|
||||||
import getpass
|
import getpass
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -12,70 +12,71 @@ import time
|
|||||||
from .client import CZDS
|
from .client import CZDS
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
def run_czds(username: str, password: str, concurrency: int) -> None:
|
||||||
|
'''
|
||||||
|
Main function to download all zone files
|
||||||
|
|
||||||
|
:param username: ICANN Username
|
||||||
|
:param password: ICANN Password
|
||||||
|
:param concurrency: Number of concurrent downloads
|
||||||
|
'''
|
||||||
|
|
||||||
|
now = time.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
logging.info('Authenticating with ICANN API...')
|
||||||
|
|
||||||
|
CZDS_client = CZDS(username, password)
|
||||||
|
|
||||||
|
logging.debug('Created CZDS client')
|
||||||
|
|
||||||
|
output_directory = os.path.join(os.getcwd(), 'zones', now)
|
||||||
|
os.makedirs(output_directory, exist_ok=True)
|
||||||
|
|
||||||
|
logging.info('Fetching zone stats report...')
|
||||||
|
|
||||||
|
try:
|
||||||
|
CZDS_client.download_report(os.path.join(output_directory, '.report.csv'))
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f'Failed to download zone stats report: {e}')
|
||||||
|
|
||||||
|
logging.info('Fetching zone links...')
|
||||||
|
|
||||||
|
try:
|
||||||
|
zone_links = CZDS_client.fetch_zone_links()
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f'Failed to fetch zone links: {e}')
|
||||||
|
|
||||||
|
logging.info(f'Fetched {len(zone_links):,} zone links')
|
||||||
|
|
||||||
|
logging.info('Downloading zone files...')
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
|
||||||
|
future_to_url = {executor.submit(CZDS_client.download_zone, url, output_directory): url for url in sorted(zone_links)}
|
||||||
|
for future in concurrent.futures.as_completed(future_to_url):
|
||||||
|
url = future_to_url[future]
|
||||||
|
try:
|
||||||
|
filepath = future.result()
|
||||||
|
logging.info(f'Completed downloading {url} to file {filepath}')
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'{url} generated an exception: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
'''Entry point for the command line interface'''
|
'''Entry point for the command line interface'''
|
||||||
|
|
||||||
# Create argument parser
|
|
||||||
parser = argparse.ArgumentParser(description='ICANN API for the Centralized Zones Data Service')
|
parser = argparse.ArgumentParser(description='ICANN API for the Centralized Zones Data Service')
|
||||||
|
|
||||||
# Authentication
|
|
||||||
parser.add_argument('-u', '--username', default=os.getenv('CZDS_USER'), help='ICANN Username')
|
parser.add_argument('-u', '--username', default=os.getenv('CZDS_USER'), help='ICANN Username')
|
||||||
parser.add_argument('-p', '--password', default=os.getenv('CZDS_PASS'), help='ICANN Password')
|
parser.add_argument('-p', '--password', default=os.getenv('CZDS_PASS'), help='ICANN Password')
|
||||||
|
|
||||||
# Zone download options
|
|
||||||
parser.add_argument('-z', '--zones', action='store_true', help='Download zone files')
|
|
||||||
parser.add_argument('-c', '--concurrency', type=int, default=3, help='Number of concurrent downloads')
|
parser.add_argument('-c', '--concurrency', type=int, default=3, help='Number of concurrent downloads')
|
||||||
parser.add_argument('-d', '--decompress', action='store_true', help='Decompress zone files after download')
|
|
||||||
parser.add_argument('-k', '--keep', action='store_true', help='Keep the original gzip files after decompression')
|
|
||||||
|
|
||||||
# Report options
|
|
||||||
parser.add_argument('-r', '--report', action='store_true', help='Download the zone stats report')
|
|
||||||
parser.add_argument('-s', '--scrub', action='store_true', help='Scrub the username from the report')
|
|
||||||
parser.add_argument('-f', '--format', choices=['csv', 'json'], default='csv', help='Report output format')
|
|
||||||
|
|
||||||
# Output options
|
|
||||||
parser.add_argument('-o', '--output', default=os.getcwd(), help='Output directory')
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
# Get username and password
|
|
||||||
username = args.username or input('ICANN Username: ')
|
username = args.username or input('ICANN Username: ')
|
||||||
password = args.password or getpass.getpass('ICANN Password: ')
|
password = args.password or getpass.getpass('ICANN Password: ')
|
||||||
|
|
||||||
# Create output directory
|
run_czds(username, password, args.concurrency)
|
||||||
now = time.strftime('%Y-%m-%d')
|
|
||||||
output_directory = os.path.join(args.output, 'zones', now)
|
|
||||||
os.makedirs(output_directory, exist_ok=True)
|
|
||||||
|
|
||||||
logging.info('Authenticating with ICANN API...')
|
|
||||||
|
|
||||||
async with CZDS(username, password) as client:
|
|
||||||
# Download zone stats report if requested
|
|
||||||
if args.report:
|
|
||||||
logging.info('Fetching zone stats report...')
|
|
||||||
try:
|
|
||||||
output = os.path.join(output_directory, '.report.csv')
|
|
||||||
await client.get_report(output, scrub=args.scrub, format=args.format)
|
|
||||||
logging.info(f'Zone stats report saved to {output}')
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f'Failed to download zone stats report: {e}')
|
|
||||||
|
|
||||||
# Download zone files if requested
|
|
||||||
if args.zones:
|
|
||||||
logging.info('Fetching zone links...')
|
|
||||||
try:
|
|
||||||
zone_links = await client.fetch_zone_links()
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f'Failed to fetch zone links: {e}')
|
|
||||||
|
|
||||||
logging.info(f'Downloading {len(zone_links):,} zone files...')
|
|
||||||
await client.download_zones(zone_links, output_directory, args.concurrency, decompress=args.decompress, cleanup=not args.keep)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
asyncio.run(main())
|
main()
|
168
czds/client.py
168
czds/client.py
@ -2,19 +2,9 @@
|
|||||||
# ICANN API for the Centralized Zones Data Service - developed by acidvegas (https://git.acid.vegas/czds)
|
# ICANN API for the Centralized Zones Data Service - developed by acidvegas (https://git.acid.vegas/czds)
|
||||||
# czds/client.py
|
# czds/client.py
|
||||||
|
|
||||||
import asyncio
|
import json
|
||||||
import os
|
import os
|
||||||
import gzip
|
import urllib.request
|
||||||
|
|
||||||
try:
|
|
||||||
import aiohttp
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError('missing aiohttp library (pip install aiohttp)')
|
|
||||||
|
|
||||||
try:
|
|
||||||
import aiofiles
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError('missing aiofiles library (pip install aiofiles)')
|
|
||||||
|
|
||||||
|
|
||||||
class CZDS:
|
class CZDS:
|
||||||
@ -29,166 +19,86 @@ class CZDS:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.headers = {'Authorization': f'Bearer {self.authenticate(username, password)}'}
|
||||||
self.headers = None # Store the authorization header for reuse
|
|
||||||
self.session = None # Store the client session for reuse
|
|
||||||
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
def authenticate(self, username: str, password: str) -> str:
|
||||||
'''Async context manager entry'''
|
'''
|
||||||
|
Authenticate with the ICANN API and return the access token
|
||||||
|
|
||||||
self.session = aiohttp.ClientSession()
|
:param username: ICANN Username
|
||||||
self.headers = {'Authorization': f'Bearer {await self.authenticate()}'}
|
:param password: ICANN Password
|
||||||
|
'''
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
'''Async context manager exit'''
|
|
||||||
|
|
||||||
if self.session:
|
|
||||||
await self.session.close()
|
|
||||||
|
|
||||||
|
|
||||||
async def authenticate(self) -> str:
|
|
||||||
'''Authenticate with the ICANN API and return the access token'''
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = {'username': self.username, 'password': self.password}
|
data = json.dumps({'username': username, 'password': password}).encode('utf-8')
|
||||||
|
headers = {'Content-Type': 'application/json'}
|
||||||
|
request = urllib.request.Request('https://account-api.icann.org/api/authenticate', data=data, headers=headers)
|
||||||
|
|
||||||
async with self.session.post('https://account-api.icann.org/api/authenticate', json=data) as response:
|
with urllib.request.urlopen(request) as response:
|
||||||
if response.status != 200:
|
response = response.read().decode('utf-8')
|
||||||
raise Exception(f'Authentication failed: {response.status} {await response.text()}')
|
|
||||||
|
|
||||||
result = await response.json()
|
|
||||||
|
|
||||||
return result['accessToken']
|
|
||||||
|
|
||||||
|
return json.loads(response)['accessToken']
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f'Failed to authenticate with ICANN API: {e}')
|
raise Exception(f'Failed to authenticate with ICANN API: {e}')
|
||||||
|
|
||||||
|
|
||||||
async def fetch_zone_links(self) -> list:
|
def fetch_zone_links(self) -> list:
|
||||||
'''Fetch the list of zone files available for download'''
|
'''Fetch the list of zone files available for download'''
|
||||||
|
|
||||||
async with self.session.get('https://czds-api.icann.org/czds/downloads/links', headers=self.headers) as response:
|
request = urllib.request.Request('https://czds-api.icann.org/czds/downloads/links', headers=self.headers)
|
||||||
|
|
||||||
|
with urllib.request.urlopen(request) as response:
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
raise Exception(f'Failed to fetch zone links: {response.status} {await response.text()}')
|
raise Exception(f'Failed to fetch zone links: {response.status} {response.reason}')
|
||||||
|
|
||||||
return await response.json()
|
return json.loads(response.read().decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
async def get_report(self, filepath: str = None, scrub: bool = True, format: str = 'csv') -> str | dict:
|
def download_report(self, filepath: str):
|
||||||
'''
|
'''
|
||||||
Downloads the zone report stats from the API and scrubs the report for privacy
|
Downloads the zone report stats from the API and scrubs the report for privacy
|
||||||
|
|
||||||
:param filepath: Filepath to save the scrubbed report
|
:param filepath: Filepath to save the scrubbed report
|
||||||
:param scrub: Whether to scrub the username from the report
|
|
||||||
:param format: Output format ('csv' or 'json')
|
|
||||||
:return: Report content as CSV string or JSON dict
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
async with self.session.get('https://czds-api.icann.org/czds/requests/report', headers=self.headers) as response:
|
request = urllib.request.Request('https://czds-api.icann.org/czds/requests/report', headers=self.headers)
|
||||||
|
|
||||||
|
with urllib.request.urlopen(request) as response:
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
raise Exception(f'Failed to download the zone stats report: {response.status} {await response.text()}')
|
raise Exception(f'Failed to download the zone stats report: {response.status} {response.reason}')
|
||||||
|
|
||||||
content = await response.text()
|
content = response.read().decode('utf-8')
|
||||||
|
|
||||||
if scrub:
|
with open(filepath, 'w') as file:
|
||||||
content = content.replace(self.username, 'nobody@no.name')
|
file.write(content.replace(self.username, 'nobody@no.name'))
|
||||||
|
|
||||||
if format.lower() == 'json':
|
|
||||||
rows = [row.split(',') for row in content.strip().split('\n')]
|
|
||||||
header = rows[0]
|
|
||||||
content = [dict(zip(header, row)) for row in rows[1:]]
|
|
||||||
|
|
||||||
if filepath:
|
|
||||||
async with aiofiles.open(filepath, 'w') as file:
|
|
||||||
if format.lower() == 'json':
|
|
||||||
import json
|
|
||||||
await file.write(json.dumps(content, indent=4))
|
|
||||||
else:
|
|
||||||
await file.write(content)
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
async def gzip_decompress(self, filepath: str, cleanup: bool = True):
|
def download_zone(self, url: str, output_directory: str) -> str:
|
||||||
'''
|
|
||||||
Decompress a gzip file in place
|
|
||||||
|
|
||||||
:param filepath: Path to the gzip file
|
|
||||||
:param cleanup: Whether to remove the original gzip file after decompression
|
|
||||||
'''
|
|
||||||
|
|
||||||
output_path = filepath[:-3] # Remove .gz extension
|
|
||||||
|
|
||||||
async with aiofiles.open(filepath, 'rb') as f_in:
|
|
||||||
content = await f_in.read()
|
|
||||||
with gzip.open(content, 'rb') as gz:
|
|
||||||
async with aiofiles.open(output_path, 'wb') as f_out:
|
|
||||||
await f_out.write(gz.read())
|
|
||||||
|
|
||||||
if cleanup:
|
|
||||||
os.remove(filepath)
|
|
||||||
|
|
||||||
|
|
||||||
async def download_zone(self, url: str, output_directory: str, decompress: bool = False, cleanup: bool = True, semaphore: asyncio.Semaphore = None):
|
|
||||||
'''
|
'''
|
||||||
Download a single zone file
|
Download a single zone file
|
||||||
|
|
||||||
:param url: URL to download
|
:param url: URL to download
|
||||||
:param output_directory: Directory to save the zone file
|
:param output_directory: Directory to save the zone file
|
||||||
:param decompress: Whether to decompress the gzip file after download
|
|
||||||
:param cleanup: Whether to remove the original gzip file after decompression
|
|
||||||
:param semaphore: Optional semaphore for controlling concurrency
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
async def _download():
|
request = urllib.request.Request(url, headers=self.headers)
|
||||||
async with self.session.get(url, headers=self.headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
raise Exception(f'Failed to download {url}: {response.status} {await response.text()}')
|
|
||||||
|
|
||||||
if not (content_disposition := response.headers.get('Content-Disposition')):
|
with urllib.request.urlopen(request) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
raise Exception(f'Failed to download {url}: {response.status} {response.reason}')
|
||||||
|
|
||||||
|
if not (content_disposition := response.getheader('Content-Disposition')):
|
||||||
raise ValueError('Missing Content-Disposition header')
|
raise ValueError('Missing Content-Disposition header')
|
||||||
|
|
||||||
filename = content_disposition.split('filename=')[-1].strip('"')
|
filename = content_disposition.split('filename=')[-1].strip('"')
|
||||||
filepath = os.path.join(output_directory, filename)
|
filepath = os.path.join(output_directory, filename)
|
||||||
|
|
||||||
async with aiofiles.open(filepath, 'wb') as file:
|
with open(filepath, 'wb') as file:
|
||||||
while True:
|
while True:
|
||||||
chunk = await response.content.read(8192)
|
chunk = response.read(1024)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
await file.write(chunk)
|
file.write(chunk)
|
||||||
|
|
||||||
if decompress:
|
|
||||||
await self.gzip_decompress(filepath, cleanup)
|
|
||||||
filepath = filepath[:-3] # Remove .gz extension
|
|
||||||
|
|
||||||
return filepath
|
return filepath
|
||||||
|
|
||||||
if semaphore:
|
|
||||||
async with semaphore:
|
|
||||||
return await _download()
|
|
||||||
else:
|
|
||||||
return await _download()
|
|
||||||
|
|
||||||
|
|
||||||
async def download_zones(self, zone_links: list, output_directory: str, concurrency: int, decompress: bool = False, cleanup: bool = True):
|
|
||||||
'''
|
|
||||||
Download multiple zone files concurrently
|
|
||||||
|
|
||||||
:param zone_links: List of zone URLs to download
|
|
||||||
:param output_directory: Directory to save the zone files
|
|
||||||
:param concurrency: Number of concurrent downloads
|
|
||||||
:param decompress: Whether to decompress the gzip files after download
|
|
||||||
:param cleanup: Whether to remove the original gzip files after decompression
|
|
||||||
'''
|
|
||||||
|
|
||||||
os.makedirs(output_directory, exist_ok=True)
|
|
||||||
|
|
||||||
semaphore = asyncio.Semaphore(concurrency)
|
|
||||||
tasks = [self.download_zone(url, output_directory, decompress, cleanup, semaphore) for url in zone_links]
|
|
||||||
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
Metadata-Version: 2.2
|
Metadata-Version: 2.2
|
||||||
Name: czds-api
|
Name: czds-api
|
||||||
Version: 1.0.1
|
Version: 1.0.0
|
||||||
Summary: ICANN API for the Centralized Zones Data Service
|
Summary: ICANN API for the Centralized Zones Data Service
|
||||||
Home-page: https://github.com/acidvegas/czds
|
Home-page: https://github.com/acidvegas/czds
|
||||||
Author: acidvegas
|
Author: acidvegas
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
aiohttp
|
|
||||||
aiofiles
|
|
6
setup.py
6
setup.py
@ -11,7 +11,7 @@ with open('README.md', 'r', encoding='utf-8') as fh:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='czds-api',
|
name='czds-api',
|
||||||
version='1.1.0',
|
version='1.0.1',
|
||||||
author='acidvegas',
|
author='acidvegas',
|
||||||
author_email='acid.vegas@acid.vegas',
|
author_email='acid.vegas@acid.vegas',
|
||||||
description='ICANN API for the Centralized Zones Data Service',
|
description='ICANN API for the Centralized Zones Data Service',
|
||||||
@ -46,8 +46,4 @@ setup(
|
|||||||
'czds=czds.__main__:main',
|
'czds=czds.__main__:main',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
install_requires=[
|
|
||||||
'aiohttp>=3.8.0',
|
|
||||||
'aiofiles>=23.2.1',
|
|
||||||
],
|
|
||||||
)
|
)
|
Loading…
Reference in New Issue
Block a user