|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing some FTP related utilities. |
|
8 """ |
|
9 |
|
10 import os |
|
11 |
|
12 from PyQt6.QtCore import QObject, QDate, QDateTime, QTime |
|
13 |
|
14 from EricNetwork.EricUrlInfo import EricUrlInfo, EricUrlPermission |
|
15 |
|
16 |
|
17 class FtpDirLineParserError(Exception): |
|
18 """ |
|
19 Exception class raised, if a parser issue was detected. |
|
20 """ |
|
21 pass |
|
22 |
|
23 |
|
24 class FtpDirLineParser(QObject): |
|
25 """ |
|
26 Class to parse lines returned by a FTP LIST command. |
|
27 """ |
|
28 MonthnamesNumbers = { |
|
29 "jan": 1, |
|
30 "feb": 2, |
|
31 "mar": 3, |
|
32 "apr": 4, |
|
33 "may": 5, |
|
34 "jun": 6, |
|
35 "jul": 7, |
|
36 "aug": 8, |
|
37 "sep": 9, |
|
38 "oct": 10, |
|
39 "nov": 11, |
|
40 "dec": 12, |
|
41 } |
|
42 |
|
43 def __init__(self, parent=None): |
|
44 """ |
|
45 Constructor |
|
46 |
|
47 @param parent reference to the parent object (QObject) |
|
48 """ |
|
49 super().__init__(parent) |
|
50 |
|
51 self.__parseLine = self.__parseUnixLine |
|
52 self.__modeSwitchAllowed = True |
|
53 |
|
54 def __ignoreLine(self, line): |
|
55 """ |
|
56 Private method to check, if the line should be ignored. |
|
57 |
|
58 @param line to check (string) |
|
59 @return flag indicating to ignore the line (boolean) |
|
60 """ |
|
61 return ( |
|
62 line.strip() == "" or |
|
63 line.strip().lower().startswith("total ") |
|
64 ) |
|
65 |
|
66 def __parseUnixMode(self, modeString, urlInfo): |
|
67 """ |
|
68 Private method to parse a Unix mode string modifying the |
|
69 given URL info object. |
|
70 |
|
71 @param modeString mode string to be parsed (string) |
|
72 @param urlInfo reference to the URL info object (EricUrlInfo) |
|
73 @exception FtpDirLineParserError Raised if the mode cannot be parsed. |
|
74 """ |
|
75 if len(modeString) != 10: |
|
76 raise FtpDirLineParserError( |
|
77 "invalid mode string '{0}'".format(modeString)) |
|
78 |
|
79 modeString = modeString.lower() |
|
80 |
|
81 permission = 0 |
|
82 if modeString[1] != '-': |
|
83 permission |= EricUrlPermission.READ_OWNER |
|
84 if modeString[2] != '-': |
|
85 permission |= EricUrlPermission.WRITE_OWNER |
|
86 if modeString[3] != '-': |
|
87 permission |= EricUrlPermission.EXE_OWNER |
|
88 if modeString[4] != '-': |
|
89 permission |= EricUrlPermission.READ_GROUP |
|
90 if modeString[5] != '-': |
|
91 permission |= EricUrlPermission.WRITE_GROUP |
|
92 if modeString[6] != '-': |
|
93 permission |= EricUrlPermission.EXE_GROUP |
|
94 if modeString[7] != '-': |
|
95 permission |= EricUrlPermission.READ_OTHER |
|
96 if modeString[8] != '-': |
|
97 permission |= EricUrlPermission.WRITE_OTHER |
|
98 if modeString[9] != '-': |
|
99 permission |= EricUrlPermission.EXE_OTHER |
|
100 urlInfo.setPermissions(permission) |
|
101 |
|
102 if modeString[0] == "d": |
|
103 urlInfo.setDir(True) |
|
104 urlInfo.setFile(False) |
|
105 urlInfo.setSymLink(False) |
|
106 elif modeString[0] == "l": |
|
107 urlInfo.setDir(True) |
|
108 urlInfo.setFile(False) |
|
109 urlInfo.setSymLink(True) |
|
110 elif modeString[0] == "-": |
|
111 urlInfo.setDir(False) |
|
112 urlInfo.setFile(True) |
|
113 urlInfo.setSymLink(False) |
|
114 |
|
115 def __parseUnixTime(self, monthAbbreviation, day, yearOrTime, urlInfo): |
|
116 """ |
|
117 Private method to parse a Unix date and time indication modifying |
|
118 the given URL info object. |
|
119 |
|
120 |
|
121 Date time strings in Unix-style directory listings typically |
|
122 have one of these formats: |
|
123 <ul> |
|
124 <li>"Nov 23 02:33" (month name, day of month, time)</li> |
|
125 <li>"May 26 2005" (month name, day of month, year)</li> |
|
126 </ul> |
|
127 |
|
128 @param monthAbbreviation abbreviation of the month name (string) |
|
129 @param day day of the month (string) |
|
130 @param yearOrTime string giving the year or a time (string) |
|
131 @param urlInfo reference to the URL info object (EricUrlInfo) |
|
132 @exception FtpDirLineParserError Raised if the month abbreviation is |
|
133 not recognized. |
|
134 """ |
|
135 try: |
|
136 month = FtpDirLineParser.MonthnamesNumbers[ |
|
137 monthAbbreviation.lower()] |
|
138 except KeyError: |
|
139 raise FtpDirLineParserError( |
|
140 "illegal month abbreviation '{0}'".format( |
|
141 monthAbbreviation)) |
|
142 day = int(day) |
|
143 if ':' in yearOrTime: |
|
144 year = QDate.currentDate().year() |
|
145 hour, minute = yearOrTime.split(':') |
|
146 hour = int(hour) |
|
147 minute = int(minute) |
|
148 else: |
|
149 year = int(yearOrTime) |
|
150 hour = 0 |
|
151 minute = 0 |
|
152 |
|
153 lastModified = QDateTime(QDate(year, month, day), QTime(hour, minute)) |
|
154 urlInfo.setLastModified(lastModified) |
|
155 |
|
156 def __splitUnixLine(self, line): |
|
157 """ |
|
158 Private method to split a line of a Unix like directory listing. |
|
159 |
|
160 It splits the line into meta data, number of links, user, group, size, |
|
161 month, day, year or time and name. |
|
162 |
|
163 @param line directory line to split (string) |
|
164 @return tuple of nine strings giving the meta data, |
|
165 number of links, user, group, size, month, day, year or time |
|
166 and name |
|
167 @exception FtpDirLineParserError Raised if the line is not of a |
|
168 recognized Unix format. |
|
169 """ |
|
170 # This method encapsulates the recognition of an unusual |
|
171 # Unix format variant. |
|
172 lineParts = line.split() |
|
173 fieldCountWithoutUserID = 8 |
|
174 fieldCountWithUserID = fieldCountWithoutUserID + 1 |
|
175 if len(lineParts) < fieldCountWithoutUserID: |
|
176 raise FtpDirLineParserError( |
|
177 "line '{0}' cannot be parsed".format(line)) |
|
178 |
|
179 # If we have a valid format (either with or without user id field), |
|
180 # the field with index 5 is either the month abbreviation or a day. |
|
181 try: |
|
182 int(lineParts[5]) |
|
183 except ValueError: |
|
184 # Month abbreviation, "invalid literal for int" |
|
185 lineParts = line.split(None, fieldCountWithUserID - 1) |
|
186 else: |
|
187 # Day |
|
188 lineParts = line.split(None, fieldCountWithoutUserID - 1) |
|
189 userFieldIndex = 2 |
|
190 lineParts.insert(userFieldIndex, "") |
|
191 |
|
192 return lineParts |
|
193 |
|
194 def __parseUnixLine(self, line): |
|
195 """ |
|
196 Private method to parse a Unix style directory listing line. |
|
197 |
|
198 @param line directory line to be parsed (string) |
|
199 @return URL info object containing the valid data (EricUrlInfo) |
|
200 """ |
|
201 modeString, nlink, user, group, size, month, day, yearOrTime, name = ( |
|
202 self.__splitUnixLine(line) |
|
203 ) |
|
204 |
|
205 if name in [".", ".."]: |
|
206 return None |
|
207 |
|
208 urlInfo = EricUrlInfo() |
|
209 self.__parseUnixMode(modeString, urlInfo) |
|
210 self.__parseUnixTime(month, day, yearOrTime, urlInfo) |
|
211 urlInfo.setOwner(user) |
|
212 urlInfo.setGroup(group) |
|
213 urlInfo.setSize(int(size)) |
|
214 name = name.strip() |
|
215 i = name.find(" -> ") |
|
216 if i >= 0: |
|
217 name = name[:i] |
|
218 urlInfo.setName(name) |
|
219 |
|
220 return urlInfo |
|
221 |
|
222 def __parseWindowsTime(self, date, time, urlInfo): |
|
223 """ |
|
224 Private method to parse a Windows date and time indication modifying |
|
225 the given URL info object. |
|
226 |
|
227 Date time strings in Windows-style directory listings typically |
|
228 have the format "10-23-12 03:25PM" (month-day_of_month-two_digit_year, |
|
229 hour:minute, am/pm). |
|
230 |
|
231 @param date date string (string) |
|
232 @param time time string (string) |
|
233 @param urlInfo reference to the URL info object (EricUrlInfo) |
|
234 @exception FtpDirLineParserError Raised if either of the strings is not |
|
235 recognized. |
|
236 """ |
|
237 try: |
|
238 month, day, year = [int(part) for part in date.split('-')] |
|
239 year = 1900 + year if year >= 70 else 2000 + year |
|
240 except (ValueError, IndexError): |
|
241 raise FtpDirLineParserError( |
|
242 "illegal date string '{0}'".format(month)) |
|
243 try: |
|
244 hour, minute, am_pm = time[0:2], time[3:5], time[5] |
|
245 hour = int(hour) |
|
246 minute = int(minute) |
|
247 except (ValueError, IndexError): |
|
248 raise FtpDirLineParserError( |
|
249 "illegal time string '{0}'".format(month)) |
|
250 if hour == 12 and am_pm == 'A': |
|
251 hour = 0 |
|
252 if hour != 12 and am_pm == 'P': |
|
253 hour += 12 |
|
254 |
|
255 lastModified = QDateTime(QDate(year, month, day), QTime(hour, minute)) |
|
256 urlInfo.setLastModified(lastModified) |
|
257 |
|
258 def __parseWindowsLine(self, line): |
|
259 """ |
|
260 Private method to parse a Windows style directory listing line. |
|
261 |
|
262 @param line directory line to be parsed (string) |
|
263 @return URL info object containing the valid data (EricUrlInfo) |
|
264 @exception FtpDirLineParserError Raised if the line is not of a |
|
265 recognized Windows format. |
|
266 """ |
|
267 try: |
|
268 date, time, dirOrSize, name = line.split(None, 3) |
|
269 except ValueError: |
|
270 # "unpack list of wrong size" |
|
271 raise FtpDirLineParserError( |
|
272 "line '{0}' cannot be parsed".format(line)) |
|
273 |
|
274 if name in [".", ".."]: |
|
275 return None |
|
276 |
|
277 urlInfo = EricUrlInfo() |
|
278 self.__parseWindowsTime(date, time, urlInfo) |
|
279 if dirOrSize.lower() == "<dir>": |
|
280 urlInfo.setDir(True) |
|
281 urlInfo.setFile(False) |
|
282 else: |
|
283 urlInfo.setDir(False) |
|
284 urlInfo.setFile(True) |
|
285 try: |
|
286 urlInfo.setSize(int(dirOrSize)) |
|
287 except ValueError: |
|
288 raise FtpDirLineParserError( |
|
289 "illegal size '{0}'".format(dirOrSize)) |
|
290 urlInfo.setName(name) |
|
291 |
|
292 ext = os.path.splitext(name.lower())[1] |
|
293 urlInfo.setSymLink(ext == ".lnk") |
|
294 |
|
295 permissions = ( |
|
296 EricUrlPermission.READ_OWNER | EricUrlPermission.WRITE_OWNER | |
|
297 EricUrlPermission.READ_GROUP | EricUrlPermission.WRITE_GROUP | |
|
298 EricUrlPermission.READ_OTHER | EricUrlPermission.WRITE_OTHER |
|
299 ) |
|
300 if ext in [".exe", ".com", ".bat", ".cmd"]: |
|
301 permissions |= ( |
|
302 EricUrlPermission.EXE_OWNER | |
|
303 EricUrlPermission.EXE_GROUP | |
|
304 EricUrlPermission.EXE_OTHER |
|
305 ) |
|
306 urlInfo.setPermissions(permissions) |
|
307 |
|
308 return urlInfo |
|
309 |
|
310 def parseLine(self, line): |
|
311 """ |
|
312 Public method to parse a directory listing line. |
|
313 |
|
314 This implementation support Unix and Windows style directory |
|
315 listings. It tries Unix style first and if that fails switches |
|
316 to Windows style. If that fails as well, an exception is raised. |
|
317 |
|
318 @param line directory line to be parsed (string) |
|
319 @return URL info object containing the valid data (EricUrlInfo) |
|
320 """ |
|
321 if self.__ignoreLine(line): |
|
322 return None |
|
323 |
|
324 try: |
|
325 return self.__parseLine(line) |
|
326 except FtpDirLineParserError: |
|
327 if not self.__modeSwitchAllowed: |
|
328 raise |
|
329 |
|
330 self.__parseLine = self.__parseWindowsLine |
|
331 self.__modeSwitchAllowed = False |
|
332 return self.__parseLine(line) |