This shifts to use the model-view-controller pattern for the list of addons, and moves to using a full model class rather than an indexed array for the data storage and management. This enables much more information to be stored as part of the new AddonManagerRepo data type. It now wraps the Macro class for macros, supports Preference Packs, and provides access to the Metadata object.
250 lines
10 KiB
Python
250 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
#***************************************************************************
|
|
#* *
|
|
#* Copyright (c) 2018 Gaël Écorchard <galou_breizh@yahoo.fr> *
|
|
#* *
|
|
#* This program is free software; you can redistribute it and/or modify *
|
|
#* it under the terms of the GNU Lesser General Public License (LGPL) *
|
|
#* as published by the Free Software Foundation; either version 2 of *
|
|
#* the License, or (at your option) any later version. *
|
|
#* for detail see the LICENCE text file. *
|
|
#* *
|
|
#* This program is distributed in the hope that it will be useful, *
|
|
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
#* GNU Library General Public License for more details. *
|
|
#* *
|
|
#* You should have received a copy of the GNU Library General Public *
|
|
#* License along with this program; if not, write to the Free Software *
|
|
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
#* USA *
|
|
#* *
|
|
#***************************************************************************
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import codecs
|
|
import shutil
|
|
|
|
import FreeCAD
|
|
|
|
from addonmanager_utilities import translate
|
|
from addonmanager_utilities import urlopen
|
|
from addonmanager_utilities import remove_directory_if_empty
|
|
|
|
try:
|
|
from HTMLParser import HTMLParser
|
|
unescape = HTMLParser().unescape
|
|
except ImportError:
|
|
from html import unescape
|
|
|
|
# @package AddonManager_macro
|
|
# \ingroup ADDONMANAGER
|
|
# \brief Unified handler for FreeCAD macros that can be obtained from
|
|
# different sources
|
|
# @{
|
|
|
|
class Macro(object):
|
|
"""This class provides a unified way to handle macros coming from different sources"""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.on_wiki = False
|
|
self.on_git = False
|
|
self.desc = ""
|
|
self.code = ""
|
|
self.url = ""
|
|
self.version = ""
|
|
self.src_filename = ""
|
|
self.other_files = []
|
|
self.parsed = False
|
|
|
|
def __eq__(self, other):
|
|
return self.filename == other.filename
|
|
|
|
@property
|
|
def filename(self):
|
|
if self.on_git:
|
|
return os.path.basename(self.src_filename)
|
|
return (self.name + ".FCMacro").replace(" ", "_")
|
|
|
|
def is_installed(self):
|
|
if self.on_git and not self.src_filename:
|
|
return False
|
|
return (os.path.exists(os.path.join(FreeCAD.getUserMacroDir(True), self.filename)) or os.path.exists(os.path.join(FreeCAD.getUserMacroDir(True), "Macro_" + self.filename)))
|
|
|
|
def fill_details_from_file(self, filename):
|
|
with open(filename) as f:
|
|
# Number of parsed fields of metadata. For now, __Comment__,
|
|
# __Web__, __Version__, __Files__.
|
|
number_of_required_fields = 4
|
|
re_desc = re.compile(r"^__Comment__\s*=\s*(['\"])(.*)\1")
|
|
re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1")
|
|
re_version = re.compile(r"^__Version__\s*=\s*(['\"])(.*)\1")
|
|
re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1")
|
|
for line in f.readlines():
|
|
match = re.match(re_desc, line)
|
|
if match:
|
|
self.desc = match.group(2)
|
|
number_of_required_fields -= 1
|
|
match = re.match(re_url, line)
|
|
if match:
|
|
self.url = match.group(2)
|
|
number_of_required_fields -= 1
|
|
match = re.match(re_version, line)
|
|
if match:
|
|
self.version = match.group(2)
|
|
number_of_required_fields -= 1
|
|
match = re.match(re_files, line)
|
|
if match:
|
|
self.other_files = [of.strip() for of in match.group(2).split(",")]
|
|
number_of_required_fields -= 1
|
|
if number_of_required_fields <= 0:
|
|
break
|
|
f.seek(0)
|
|
self.code = f.read()
|
|
self.parsed = True
|
|
|
|
def fill_details_from_wiki(self, url):
|
|
code = ""
|
|
u = urlopen(url)
|
|
if u is None:
|
|
FreeCAD.Console.PrintWarning("AddonManager: Debug: connection is lost (proxy setting changed?)", url, "\n")
|
|
return
|
|
p = u.read()
|
|
if isinstance(p, bytes):
|
|
p = p.decode("utf-8")
|
|
u.close()
|
|
# check if the macro page has its code hosted elsewhere, download if
|
|
# needed
|
|
if "rawcodeurl" in p:
|
|
rawcodeurl = re.findall("rawcodeurl.*?href=\"(http.*?)\">", p)
|
|
if rawcodeurl:
|
|
rawcodeurl = rawcodeurl[0]
|
|
u2 = urlopen(rawcodeurl)
|
|
if u2 is None:
|
|
FreeCAD.Console.PrintWarning("AddonManager: Debug: unable to open URL", rawcodeurl, "\n")
|
|
return
|
|
# code = u2.read()
|
|
# github is slow to respond... We need to use this trick below
|
|
response = ""
|
|
block = 8192
|
|
# expected = int(u2.headers["content-length"])
|
|
while True:
|
|
# print("expected:", expected, "got:", len(response))
|
|
data = u2.read(block)
|
|
if not data:
|
|
break
|
|
if isinstance(data, bytes):
|
|
data = data.decode("utf-8")
|
|
response += data
|
|
if response:
|
|
code = response
|
|
u2.close()
|
|
if not code:
|
|
code = re.findall(r"<pre>(.*?)</pre>", p.replace("\n", "--endl--"))
|
|
if code:
|
|
# take the biggest code block
|
|
code = sorted(code, key=len)[-1]
|
|
code = code.replace("--endl--", "\n")
|
|
# Clean HTML escape codes.
|
|
code = unescape(code)
|
|
code = code.replace(b"\xc2\xa0".decode("utf-8"), " ")
|
|
else:
|
|
FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Unable to fetch the code of this macro.") + "\n")
|
|
|
|
desc = re.findall(r"<td class=\"ctEven left macro-description\">(.*?)</td>", p.replace("\n", " "))
|
|
if desc:
|
|
desc = desc[0]
|
|
else:
|
|
FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
|
|
"Unable to retrieve a description for this macro.") + "\n")
|
|
desc = "No description available"
|
|
self.desc = desc
|
|
self.url = url
|
|
if isinstance(code, list):
|
|
flat_code = ""
|
|
for chunk in code:
|
|
flat_code += chunk
|
|
code = flat_code
|
|
self.code = code
|
|
self.parsed = True
|
|
|
|
def install(self, macro_dir:str) -> bool:
|
|
"""Install a macro and all its related files
|
|
|
|
Returns True if the macro was installed correctly.
|
|
|
|
Parameters
|
|
----------
|
|
- macro_dir: the directory to install into
|
|
"""
|
|
|
|
if not self.code:
|
|
return False
|
|
if not os.path.isdir(macro_dir):
|
|
try:
|
|
os.makedirs(macro_dir)
|
|
except OSError:
|
|
FreeCAD.Console.PrintError(f"Failed to create {macro_dir}\n")
|
|
return False
|
|
macro_path = os.path.join(macro_dir, self.filename)
|
|
try:
|
|
with codecs.open(macro_path, 'w', 'utf-8') as macrofile:
|
|
macrofile.write(self.code)
|
|
except IOError:
|
|
FreeCAD.Console.PrintError(f"Failed to write {macro_path}\n")
|
|
return False
|
|
# Copy related files, which are supposed to be given relative to
|
|
# self.src_filename.
|
|
base_dir = os.path.dirname(self.src_filename)
|
|
for other_file in self.other_files:
|
|
dst_dir = os.path.join(macro_dir, os.path.dirname(other_file))
|
|
if not os.path.isdir(dst_dir):
|
|
try:
|
|
os.makedirs(dst_dir)
|
|
except OSError:
|
|
FreeCAD.Console.PrintError(f"Failed to create {dst_dir}\n")
|
|
return False
|
|
src_file = os.path.join(base_dir, other_file)
|
|
dst_file = os.path.join(macro_dir, other_file)
|
|
try:
|
|
shutil.copy(src_file, dst_file)
|
|
except IOError:
|
|
FreeCAD.Console.PrintError(f"Failed to copy {src_file} to {dst_file}\n")
|
|
return False
|
|
|
|
FreeCAD.Console.PrintMessage(f"Macro {self.name} was installed successfully.\n")
|
|
return True
|
|
|
|
|
|
def remove(self) -> bool:
|
|
"""Remove a macro and all its related files
|
|
|
|
Returns True if the macro was removed correctly.
|
|
"""
|
|
|
|
if not self.is_installed():
|
|
# Macro not installed, nothing to do.
|
|
return True
|
|
macro_dir = FreeCAD.getUserMacroDir(True)
|
|
macro_path = os.path.join(macro_dir, self.filename)
|
|
macro_path_with_macro_prefix = os.path.join(macro_dir, 'Macro_' + self.filename)
|
|
if os.path.exists(macro_path):
|
|
os.remove(macro_path)
|
|
elif os.path.exists(macro_path_with_macro_prefix):
|
|
os.remove(macro_path_with_macro_prefix)
|
|
# Remove related files, which are supposed to be given relative to
|
|
# self.src_filename.
|
|
for other_file in self.other_files:
|
|
dst_file = os.path.join(macro_dir, other_file)
|
|
try:
|
|
os.remove(dst_file)
|
|
remove_directory_if_empty(os.path.dirname(dst_file))
|
|
except Exception:
|
|
FreeCAD.Console.PrintWarning(f"Failed to remove macro file '{dst_file}': it might not exist, or its permissions changed\n")
|
|
return True
|
|
|
|
# @}
|