1# Copyright (c) 2018 Ultimaker B.V. 2# Uranium is released under the terms of the LGPLv3 or higher. 3 4import os.path 5from PyQt5.QtCore import QMimeDatabase, QMimeType 6from typing import cast, List, Optional 7 8 9class MimeTypeNotFoundError(Exception): 10 """Raised when a MIME type can not be found.""" 11 12 pass 13 14 15class MimeType: 16 """Simple value type class that encapsulates MIME type data.""" 17 18 def __init__(self, name: str, comment: str, suffixes: Optional[List[str]], preferred_suffix: str = None) -> None: 19 """Constructor 20 21 :param name: The MIME type name, like "text/plain". 22 :param comment: A description of the MIME type. 23 :param suffixes: A list of possible suffixes for the type. 24 :param preferred_suffix: The preferred suffix for the type. Defaults to 25 ``suffixes[0]`` if not specified. 26 """ 27 28 if name is None: 29 raise ValueError("Name cannot be None") 30 31 if comment is None: 32 raise ValueError("Comment cannot be None") 33 34 self.__name = name 35 self.__comment = comment 36 self.__suffixes = suffixes if isinstance(suffixes, list) else [] 37 38 if self.__suffixes: 39 if preferred_suffix: 40 if not preferred_suffix in self.__suffixes: 41 raise ValueError("Preferred suffix is not a valid suffix") 42 self.__preferred_suffix = preferred_suffix 43 else: 44 self.__preferred_suffix = self.__suffixes[0] 45 else: 46 self.__preferred_suffix = "" 47 48 @property 49 def name(self) -> str: 50 """The name that identifies the MIME type.""" 51 52 return self.__name 53 54 @property 55 def comment(self) -> str: 56 """The comment that describes of the MIME type.""" 57 58 return self.__comment 59 60 @property 61 def suffixes(self) -> List[str]: 62 """The list of file name suffixes for the MIME type. 63 64 Example: ["cfg", "tar.gz"] 65 """ 66 67 return self.__suffixes 68 69 @property 70 def preferredSuffix(self) -> str: 71 """The preferred file name suffix for the MIME type. 72 73 Example: "cfg" or "tar.gz". 74 """ 75 76 return self.__preferred_suffix 77 78 def __repr__(self) -> str: 79 """Gives a programmer-readable representation of the MIME type. 80 81 :return: A string representing the MIME type. 82 """ 83 84 return "<MimeType name={0}>".format(self.__name) 85 86 def __eq__(self, other: object) -> bool: 87 """Indicates whether this MIME type is equal to another MIME type. 88 89 They are equal if the names match, since MIME types should have unique 90 names. 91 92 :return: ``True`` if the two MIME types are equal, or ``False`` 93 otherwise. 94 """ 95 96 if type(other) is not type(self): 97 return False 98 other = cast(MimeType, other) 99 return self.__name == other.name 100 101 def stripExtension(self, file_name: str) -> str: 102 """Strip the extension from a file name when it corresponds to one of the 103 suffixes of this MIME type. 104 105 :param file_name: The file name to strip of extension. 106 :return: ``file_name`` without extension, or ``file_name`` when it does 107 not match. 108 """ 109 110 for suffix in self.__suffixes: 111 suffix = suffix.lower() 112 if file_name.lower().endswith(suffix, file_name.find(".")): 113 index = file_name.lower().rfind("." + suffix) 114 return file_name[0:index] 115 116 return file_name 117 118 119 @staticmethod 120 def fromQMimeType(qt_mime: QMimeType) -> "MimeType": 121 """Create a ``MimeType`` object from a ``QMimeType`` object. 122 123 :param qt_mime: The ``QMimeType`` object to convert. 124 :return: A new ``MimeType`` object with properties equal to the 125 ``QMimeType`` object. 126 """ 127 128 return MimeType( 129 name = qt_mime.name(), 130 comment = qt_mime.comment(), 131 suffixes = qt_mime.suffixes(), 132 preferred_suffix = qt_mime.preferredSuffix() 133 ) 134 135 136class MimeTypeDatabase: 137 """Handles lookup of MIME types for files with support for custom MIME types. 138 139 This class wraps around ``QMimeDatabase`` and extends it with support for 140 custom MIME types defined at runtime. 141 142 :note Custom MIME types are currently only detected based on extension. 143 """ 144 145 @classmethod 146 def getMimeType(cls, name: str) -> MimeType: 147 """Get a MIME type by name. 148 149 This will return a ``MimeType`` object corresponding to the specified 150 name. 151 152 :param name: The name of the MIME type to return. 153 :return: A ``MimeType`` object corresponding to the specified name. 154 :exception MimeTypeNotFoundError Raised when the specified MIME type 155 cannot be found. 156 """ 157 158 for custom_mime in cls.__custom_mimetypes: 159 if custom_mime.name == name: 160 return custom_mime 161 162 mime = cls.__system_database.mimeTypeForName(name) 163 if mime.isValid(): 164 return MimeType.fromQMimeType(mime) 165 166 raise MimeTypeNotFoundError("Could not find mime type named {0}".format(name)) 167 168 MimeTypeNotFoundError = MimeTypeNotFoundError 169 170 @classmethod 171 def getMimeTypeForFile(cls, file_name: str) -> MimeType: 172 """Get a MIME type for a specific file. 173 174 :param file_name: The name of the file to get the MIME type for. 175 :return: A MimeType object that contains the detected MIME type for the file. 176 :exception MimeTypeNotFoundError Raised when no MIME type can be found 177 for the specified file. 178 """ 179 180 # Properly normalize the file name to only be the base name of a path if we pass a path. 181 file_name = os.path.basename(file_name) 182 183 matches = [] # type: List[MimeType] 184 for mime_type in cls.__custom_mimetypes: 185 # Check if the file name ends with the suffixes, starting at the first . encountered. 186 # This means that "suffix" will not match, ".suffix" will and "suffix.something.suffix" will also match 187 if file_name.lower().endswith(tuple(mime_type.suffixes), file_name.find(".")): 188 matches.append(mime_type) 189 190 if len(matches) > 1: 191 longest_suffix = "" 192 longest_mime = None 193 for match in matches: 194 max_suffix = max(match.suffixes) 195 if len(max_suffix) > len(longest_suffix): 196 longest_suffix = max_suffix 197 longest_mime = match 198 return cast(MimeType, longest_mime) 199 elif matches: 200 return matches[0] 201 202 mime = cls.__system_database.mimeTypeForFile(file_name) 203 if not mime.isDefault() and mime.isValid(): 204 return MimeType.fromQMimeType(mime) 205 206 raise MimeTypeNotFoundError("Could not find a valid MIME type for {0}".format(file_name)) 207 208 @classmethod 209 def addMimeType(cls, mime_type: MimeType) -> None: 210 """Add a custom MIME type that can be detected. 211 212 :param mime_type: The custom MIME type to add. 213 """ 214 215 cls.__custom_mimetypes.append(mime_type) 216 217 @classmethod 218 def removeMimeType(cls, mime_type: MimeType) -> None: 219 if mime_type in cls.__custom_mimetypes: 220 cls.__custom_mimetypes.remove(mime_type) 221 222 __system_database = QMimeDatabase() 223 __custom_mimetypes = [] # type: List[MimeType] 224