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