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