1# encoding: utf-8
2
3"""
4Provides a general interface to a *physical* OPC package, such as a zip file.
5"""
6
7from __future__ import absolute_import
8
9import os
10
11from zipfile import ZipFile, is_zipfile, ZIP_DEFLATED
12
13from .compat import is_string
14from .exceptions import PackageNotFoundError
15from .packuri import CONTENT_TYPES_URI
16
17
18class PhysPkgReader(object):
19    """
20    Factory for physical package reader objects.
21    """
22    def __new__(cls, pkg_file):
23        # if *pkg_file* is a string, treat it as a path
24        if is_string(pkg_file):
25            if os.path.isdir(pkg_file):
26                reader_cls = _DirPkgReader
27            elif is_zipfile(pkg_file):
28                reader_cls = _ZipPkgReader
29            else:
30                raise PackageNotFoundError(
31                    "Package not found at '%s'" % pkg_file
32                )
33        else:  # assume it's a stream and pass it to Zip reader to sort out
34            reader_cls = _ZipPkgReader
35
36        return super(PhysPkgReader, cls).__new__(reader_cls)
37
38
39class PhysPkgWriter(object):
40    """
41    Factory for physical package writer objects.
42    """
43    def __new__(cls, pkg_file):
44        return super(PhysPkgWriter, cls).__new__(_ZipPkgWriter)
45
46
47class _DirPkgReader(PhysPkgReader):
48    """
49    Implements |PhysPkgReader| interface for an OPC package extracted into a
50    directory.
51    """
52    def __init__(self, path):
53        """
54        *path* is the path to a directory containing an expanded package.
55        """
56        super(_DirPkgReader, self).__init__()
57        self._path = os.path.abspath(path)
58
59    def blob_for(self, pack_uri):
60        """
61        Return contents of file corresponding to *pack_uri* in package
62        directory.
63        """
64        path = os.path.join(self._path, pack_uri.membername)
65        with open(path, 'rb') as f:
66            blob = f.read()
67        return blob
68
69    def close(self):
70        """
71        Provides interface consistency with |ZipFileSystem|, but does
72        nothing, a directory file system doesn't need closing.
73        """
74        pass
75
76    @property
77    def content_types_xml(self):
78        """
79        Return the `[Content_Types].xml` blob from the package.
80        """
81        return self.blob_for(CONTENT_TYPES_URI)
82
83    def rels_xml_for(self, source_uri):
84        """
85        Return rels item XML for source with *source_uri*, or None if the
86        item has no rels item.
87        """
88        try:
89            rels_xml = self.blob_for(source_uri.rels_uri)
90        except IOError:
91            rels_xml = None
92        return rels_xml
93
94
95class _ZipPkgReader(PhysPkgReader):
96    """
97    Implements |PhysPkgReader| interface for a zip file OPC package.
98    """
99    def __init__(self, pkg_file):
100        super(_ZipPkgReader, self).__init__()
101        self._zipf = ZipFile(pkg_file, 'r')
102
103    def blob_for(self, pack_uri):
104        """
105        Return blob corresponding to *pack_uri*. Raises |ValueError| if no
106        matching member is present in zip archive.
107        """
108        return self._zipf.read(pack_uri.membername)
109
110    def close(self):
111        """
112        Close the zip archive, releasing any resources it is using.
113        """
114        self._zipf.close()
115
116    @property
117    def content_types_xml(self):
118        """
119        Return the `[Content_Types].xml` blob from the zip package.
120        """
121        return self.blob_for(CONTENT_TYPES_URI)
122
123    def rels_xml_for(self, source_uri):
124        """
125        Return rels item XML for source with *source_uri* or None if no rels
126        item is present.
127        """
128        try:
129            rels_xml = self.blob_for(source_uri.rels_uri)
130        except KeyError:
131            rels_xml = None
132        return rels_xml
133
134
135class _ZipPkgWriter(PhysPkgWriter):
136    """
137    Implements |PhysPkgWriter| interface for a zip file OPC package.
138    """
139    def __init__(self, pkg_file):
140        super(_ZipPkgWriter, self).__init__()
141        self._zipf = ZipFile(pkg_file, 'w', compression=ZIP_DEFLATED)
142
143    def close(self):
144        """
145        Close the zip archive, flushing any pending physical writes and
146        releasing any resources it's using.
147        """
148        self._zipf.close()
149
150    def write(self, pack_uri, blob):
151        """
152        Write *blob* to this zip package with the membername corresponding to
153        *pack_uri*.
154        """
155        self._zipf.writestr(pack_uri.membername, blob)
156