|
import os |
|
import ssl |
|
import sys |
|
import ftplib |
|
import zipfile |
|
import xml.etree.ElementTree as ET |
|
|
|
class ImplicitFTP_TLS(ftplib.FTP_TLS): |
|
""" |
|
FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS. |
|
see https://stackoverflow.com/a/36049814 |
|
""" |
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
self._sock = None |
|
|
|
@property |
|
def sock(self): |
|
"""Return the socket.""" |
|
return self._sock |
|
|
|
@sock.setter |
|
def sock(self, value): |
|
"""When modifying the socket, ensure that it is ssl wrapped.""" |
|
if value is not None and not isinstance(value, ssl.SSLSocket): |
|
value = self.context.wrap_socket(value) |
|
self._sock = value |
|
|
|
def download_model(host, access_code, model, download_path='.'): |
|
download_path = os.path.abspath(download_path) |
|
os.makedirs(download_path, exist_ok=True) |
|
|
|
ftp = ImplicitFTP_TLS() |
|
ftp.connect(host=host, port=990, timeout=10) |
|
ftp.login(user='bblp', passwd=access_code) |
|
ftp.prot_p() |
|
# Sometimes the 3mf is present in the FTP root, other times it's in |
|
# "cache", try both. |
|
with open(f'{download_path}/{model}', 'wb') as fp: |
|
try: |
|
ftp.retrbinary(f'RETR {model}', fp.write) |
|
except: |
|
ftp.cwd('cache') |
|
ftp.retrbinary(f'RETR {model}', fp.write) |
|
ftp.quit() |
|
|
|
def extract_image(model_path, extract_path="."): |
|
""" |
|
Extracts a single file from a 3MF (ZIP) archive. |
|
""" |
|
if not zipfile.is_zipfile(model_path): |
|
print(f"Error: {model_path} is not a valid 3MF file.") |
|
return |
|
|
|
try: |
|
with zipfile.ZipFile(model_path, 'r') as archive: |
|
# Attempt to fetch the name of the plate being printed |
|
# from one of two relationships files |
|
plate = None |
|
if '_rels/.rels' in archive.namelist(): |
|
plate = get_plate(archive.read('_rels/.rels')) |
|
elif plate is None and 'Metadata/_rels/model_settings.config.rels' in archive.namelist(): |
|
plate = get_plate(archive.read('Metadata/_rels/model_settings.config.rels'), 'http://schemas.bambulab.com/package/2021/gcode') |
|
else: |
|
plate = 'Metadata/plate_1' |
|
|
|
image_path = f'{plate}.png' |
|
|
|
if image_path in archive.namelist(): |
|
extract_name = f'{os.path.splitext(os.path.basename(model_path))[0]}.png' |
|
extract_path = os.path.abspath(extract_path) |
|
image = archive.getinfo(image_path) |
|
image.filename = extract_name |
|
archive.extract(image, extract_path) |
|
print(f"Extracted {image_path} to {extract_path}/{extract_name}") |
|
else: |
|
print(f"Error: {image_path} not found in the 3MF archive.") |
|
except Exception as e: |
|
print(f"An error occurred: {e}") |
|
|
|
def get_plate(rels, type='http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail'): |
|
try: |
|
root = ET.fromstring(rels) |
|
for child in root: |
|
if (child.attrib.get('Type') == type): |
|
return os.path.splitext(child.attrib.get('Target'))[0].removeprefix('/') |
|
except Exception as e: |
|
return None |
|
|
|
if __name__ == "__main__": |
|
if len(sys.argv) < 4: |
|
print("Host, access code and model name required") |
|
else: |
|
printer_ip = sys.argv[1] |
|
access_code = sys.argv[2] |
|
model_name = sys.argv[3] |
|
|
|
download_path = 'models' |
|
image_path = 'images' |
|
|
|
download_model(host=printer_ip, access_code=access_code, model=model_name, download_path=download_path) |
|
extract_image(f'{download_path}/{model_name}', extract_path=image_path) |