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