|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2012 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a network reply class for directory resources. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 try: |
|
12 str = unicode |
|
13 except NameError: |
|
14 pass |
|
15 |
|
16 from PyQt5.QtCore import Qt, QByteArray, QTimer, QDir, QIODevice, QUrl, QBuffer |
|
17 from PyQt5.QtGui import QPixmap |
|
18 from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest |
|
19 from PyQt5.QtWebKit import QWebSettings |
|
20 |
|
21 import UI.PixmapCache |
|
22 |
|
23 import Utilities |
|
24 |
|
25 |
|
26 dirListPage_html = """\ |
|
27 <?xml version="1.0" encoding="UTF-8" ?> |
|
28 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" |
|
29 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> |
|
30 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> |
|
31 <head> |
|
32 <title>{0}</title> |
|
33 <style type="text/css"> |
|
34 body {{ |
|
35 padding: 3em 0em; |
|
36 background: -webkit-gradient(linear, left top, left bottom, from(#85784A), |
|
37 to(#FDFDFD), color-stop(0.5, #FDFDFD)); |
|
38 background-repeat: repeat-x; |
|
39 }} |
|
40 #box {{ |
|
41 background: white; |
|
42 border: 1px solid #85784A; |
|
43 width: 80%; |
|
44 padding: 30px; |
|
45 margin: auto; |
|
46 -webkit-border-radius: 0.8em; |
|
47 }} |
|
48 h1 {{ |
|
49 font-size: 130%; |
|
50 font-weight: bold; |
|
51 border-bottom: 1px solid #85784A; |
|
52 }} |
|
53 th {{ |
|
54 background-color: #B8B096; |
|
55 color: black; |
|
56 }} |
|
57 table {{ |
|
58 border: solid 1px #85784A; |
|
59 margin: 5px 0; |
|
60 width: 100%; |
|
61 }} |
|
62 tr.odd {{ |
|
63 background-color: white; |
|
64 color: black; |
|
65 }} |
|
66 tr.even {{ |
|
67 background-color: #CEC9B8; |
|
68 color: black; |
|
69 }} |
|
70 .modified {{ |
|
71 text-align: left; |
|
72 vertical-align: top; |
|
73 white-space: nowrap; |
|
74 }} |
|
75 .size {{ |
|
76 text-align: right; |
|
77 vertical-align: top; |
|
78 white-space: nowrap; |
|
79 padding-right: 22px; |
|
80 }} |
|
81 .name {{ |
|
82 text-align: left; |
|
83 vertical-align: top; |
|
84 white-space: pre-wrap; |
|
85 width: 100% |
|
86 }} |
|
87 {1} |
|
88 </style> |
|
89 </head> |
|
90 <body> |
|
91 <div id="box"> |
|
92 <h1>{2}</h1> |
|
93 {3} |
|
94 <table align="center" cellspacing="0" width="90%"> |
|
95 {4} |
|
96 </table> |
|
97 </div> |
|
98 </body> |
|
99 </html> |
|
100 """ |
|
101 |
|
102 |
|
103 class FileReply(QNetworkReply): |
|
104 """ |
|
105 Class implementing a network reply for directory resources. |
|
106 """ |
|
107 def __init__(self, url, parent=None): |
|
108 """ |
|
109 Constructor |
|
110 |
|
111 @param url requested FTP URL (QUrl) |
|
112 @param parent reference to the parent object (QObject) |
|
113 """ |
|
114 super(FileReply, self).__init__(parent) |
|
115 |
|
116 self.__content = QByteArray() |
|
117 self.__units = ["Bytes", "KB", "MB", "GB", "TB", |
|
118 "PB", "EB", "ZB", "YB"] |
|
119 |
|
120 if url.path() == "": |
|
121 url.setPath("/") |
|
122 self.setUrl(url) |
|
123 |
|
124 QTimer.singleShot(0, self.__loadDirectory) |
|
125 |
|
126 def abort(self): |
|
127 """ |
|
128 Public slot to abort the operation. |
|
129 """ |
|
130 # do nothing |
|
131 pass |
|
132 |
|
133 def bytesAvailable(self): |
|
134 """ |
|
135 Public method to determined the bytes available for being read. |
|
136 |
|
137 @return bytes available (integer) |
|
138 """ |
|
139 return self.__content.size() |
|
140 |
|
141 def isSequential(self): |
|
142 """ |
|
143 Public method to check for sequential access. |
|
144 |
|
145 @return flag indicating sequential access (boolean) |
|
146 """ |
|
147 return True |
|
148 |
|
149 def readData(self, maxlen): |
|
150 """ |
|
151 Public method to retrieve data from the reply object. |
|
152 |
|
153 @param maxlen maximum number of bytes to read (integer) |
|
154 @return string containing the data (bytes) |
|
155 """ |
|
156 if self.__content.size(): |
|
157 len_ = min(maxlen, self.__content.size()) |
|
158 buffer = bytes(self.__content[:len_]) |
|
159 self.__content.remove(0, len_) |
|
160 return buffer |
|
161 else: |
|
162 return b"" |
|
163 |
|
164 def __cssLinkClass(self, icon, size=32): |
|
165 """ |
|
166 Private method to generate a link class with an icon. |
|
167 |
|
168 @param icon icon to be included (QIcon) |
|
169 @param size size of the icon to be generated (integer) |
|
170 @return CSS class string (string) |
|
171 """ |
|
172 cssString = \ |
|
173 """a.{{0}} {{{{\n"""\ |
|
174 """ padding-left: {0}px;\n"""\ |
|
175 """ background: transparent url(data:image/png;base64,{1})"""\ |
|
176 """ no-repeat center left;\n"""\ |
|
177 """ font-weight: bold;\n"""\ |
|
178 """}}}}\n""" |
|
179 pixmap = icon.pixmap(size, size) |
|
180 imageBuffer = QBuffer() |
|
181 imageBuffer.open(QIODevice.ReadWrite) |
|
182 if not pixmap.save(imageBuffer, "PNG"): |
|
183 # write a blank pixmap on error |
|
184 pixmap = QPixmap(size, size) |
|
185 pixmap.fill(Qt.transparent) |
|
186 imageBuffer.buffer().clear() |
|
187 pixmap.save(imageBuffer, "PNG") |
|
188 return cssString.format( |
|
189 size + 4, |
|
190 str(imageBuffer.buffer().toBase64(), encoding="ascii")) |
|
191 |
|
192 def __loadDirectory(self): |
|
193 """ |
|
194 Private slot loading the directory and preparing the listing page. |
|
195 """ |
|
196 qdir = QDir(self.url().toLocalFile()) |
|
197 dirItems = qdir.entryInfoList( |
|
198 QDir.AllEntries | QDir.Hidden | QDir.NoDotAndDotDot, |
|
199 QDir.Name | QDir.DirsFirst) |
|
200 |
|
201 u = self.url() |
|
202 if not u.path().endswith("/"): |
|
203 u.setPath(u.path() + "/") |
|
204 |
|
205 baseUrl = self.url().toString() |
|
206 basePath = u.path() |
|
207 |
|
208 linkClasses = {} |
|
209 iconSize = QWebSettings.globalSettings().fontSize( |
|
210 QWebSettings.DefaultFontSize) |
|
211 |
|
212 parent = u.resolved(QUrl("..")) |
|
213 if parent.isParentOf(u): |
|
214 icon = UI.PixmapCache.getIcon("up.png") |
|
215 linkClasses["link_parent"] = \ |
|
216 self.__cssLinkClass(icon, iconSize).format("link_parent") |
|
217 parentStr = self.tr( |
|
218 """ <p><a class="link_parent" href="{0}">""" |
|
219 """Change to parent directory</a></p>""" |
|
220 ).format(parent.toString()) |
|
221 else: |
|
222 parentStr = "" |
|
223 |
|
224 row = \ |
|
225 """ <tr class="{0}">"""\ |
|
226 """<td class="name"><a class="{1}" href="{2}">{3}</a></td>"""\ |
|
227 """<td class="size">{4}</td>"""\ |
|
228 """<td class="modified">{5}</td>"""\ |
|
229 """</tr>\n""" |
|
230 table = self.tr( |
|
231 """ <tr>""" |
|
232 """<th align="left">Name</th>""" |
|
233 """<th>Size</th>""" |
|
234 """<th align="left">Last modified</th>""" |
|
235 """</tr>\n""" |
|
236 ) |
|
237 |
|
238 i = 0 |
|
239 for item in dirItems: |
|
240 name = item.fileName() |
|
241 if item.isDir() and not name.endswith("/"): |
|
242 name += "/" |
|
243 child = u.resolved(QUrl(name.replace(":", "%3A"))) |
|
244 |
|
245 if item.isFile(): |
|
246 size = item.size() |
|
247 unit = 0 |
|
248 while size: |
|
249 newSize = size // 1024 |
|
250 if newSize and unit < len(self.__units): |
|
251 size = newSize |
|
252 unit += 1 |
|
253 else: |
|
254 break |
|
255 |
|
256 sizeStr = self.tr("{0} {1}", "size unit")\ |
|
257 .format(size, self.__units[unit]) |
|
258 linkClass = "link_file" |
|
259 if linkClass not in linkClasses: |
|
260 icon = UI.PixmapCache.getIcon("fileMisc.png") |
|
261 linkClasses[linkClass] = \ |
|
262 self.__cssLinkClass(icon, iconSize).format(linkClass) |
|
263 else: |
|
264 sizeStr = "" |
|
265 linkClass = "link_dir" |
|
266 if linkClass not in linkClasses: |
|
267 icon = UI.PixmapCache.getIcon("dirClosed.png") |
|
268 linkClasses[linkClass] = \ |
|
269 self.__cssLinkClass(icon, iconSize).format(linkClass) |
|
270 table += row.format( |
|
271 i == 0 and "odd" or "even", |
|
272 linkClass, |
|
273 child.toString(), |
|
274 Utilities.html_encode(item.fileName()), |
|
275 sizeStr, |
|
276 item.lastModified().toString("yyyy-MM-dd hh:mm"), |
|
277 ) |
|
278 i = 1 - i |
|
279 |
|
280 content = dirListPage_html.format( |
|
281 Utilities.html_encode(baseUrl), |
|
282 "".join(linkClasses.values()), |
|
283 self.tr("Listing of {0}").format(basePath), |
|
284 parentStr, |
|
285 table |
|
286 ) |
|
287 self.__content = QByteArray(content.encode("utf8")) |
|
288 self.__content.append(512 * b' ') |
|
289 |
|
290 self.open(QIODevice.ReadOnly | QIODevice.Unbuffered) |
|
291 self.setHeader( |
|
292 QNetworkRequest.ContentTypeHeader, "text/html; charset=UTF-8") |
|
293 self.setHeader( |
|
294 QNetworkRequest.ContentLengthHeader, self.__content.size()) |
|
295 self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200) |
|
296 self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Ok") |
|
297 self.metaDataChanged.emit() |
|
298 self.downloadProgress.emit( |
|
299 self.__content.size(), self.__content.size()) |
|
300 self.readyRead.emit() |
|
301 self.finished.emit() |