|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing some file system commands for MicroPython. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 |
|
12 import os |
|
13 import stat |
|
14 |
|
15 from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject |
|
16 |
|
17 from .MicroPythonFileSystemUtilities import ( |
|
18 mtime2string, mode2string, decoratedName, listdirStat |
|
19 ) |
|
20 |
|
21 |
|
22 # TODO: modify to use MicroPythonCommandsInterface |
|
23 class MicroPythonFileManager(QObject): |
|
24 """ |
|
25 Class implementing an interface to the device file system commands with |
|
26 some additional sugar. |
|
27 |
|
28 @signal longListFiles(result) emitted with a tuple of tuples containing the |
|
29 name, mode, size and time for each directory entry |
|
30 @signal currentDir(dirname) emitted to report the current directory of the |
|
31 device |
|
32 @signal currentDirChanged(dirname) emitted to report back a change of the |
|
33 current directory |
|
34 @signal getFileDone(deviceFile, localFile) emitted after the file was |
|
35 fetched from the connected device and written to the local file system |
|
36 @signal putFileDone(localFile, deviceFile) emitted after the file was |
|
37 copied to the connected device |
|
38 @signal deleteFileDone(deviceFile) emitted after the file has been deleted |
|
39 on the connected device |
|
40 @signal rsyncDone(localName, deviceName) emitted after the rsync operation |
|
41 has been completed |
|
42 @signal rsyncProgressMessage(msg) emitted to send a message about what |
|
43 rsync is doing |
|
44 @signal removeDirectoryDone() emitted after a directory has been deleted |
|
45 @signal createDirectoryDone() emitted after a directory was created |
|
46 @signal fsinfoDone(fsinfo) emitted after the file system information was |
|
47 obtained |
|
48 |
|
49 @signal synchTimeDone() emitted after the time was synchronizde to the |
|
50 device |
|
51 @signal showTimeDone(dateTime) emitted after the date and time was fetched |
|
52 from the connected device |
|
53 @signal showVersionDone(versionInfo) emitted after the version information |
|
54 was fetched from the connected device |
|
55 @signal showImplementationDone(name,version) emitted after the |
|
56 implementation information has been obtained |
|
57 |
|
58 @signal error(exc) emitted with a failure message to indicate a failure |
|
59 during the most recent operation |
|
60 """ |
|
61 longListFiles = pyqtSignal(tuple) |
|
62 currentDir = pyqtSignal(str) |
|
63 currentDirChanged = pyqtSignal(str) |
|
64 getFileDone = pyqtSignal(str, str) |
|
65 putFileDone = pyqtSignal(str, str) |
|
66 deleteFileDone = pyqtSignal(str) |
|
67 rsyncDone = pyqtSignal(str, str) |
|
68 rsyncProgressMessage = pyqtSignal(str) |
|
69 removeDirectoryDone = pyqtSignal() |
|
70 createDirectoryDone = pyqtSignal() |
|
71 fsinfoDone = pyqtSignal(tuple) |
|
72 |
|
73 synchTimeDone = pyqtSignal() |
|
74 showTimeDone = pyqtSignal(str) |
|
75 showVersionDone = pyqtSignal(dict) |
|
76 showImplementationDone = pyqtSignal(str, str) |
|
77 |
|
78 error = pyqtSignal(str, str) |
|
79 |
|
80 def __init__(self, commandsInterface, parent=None): |
|
81 """ |
|
82 Constructor |
|
83 |
|
84 @param commandsInterface reference to the commands interface object |
|
85 @type MicroPythonCommandsInterface |
|
86 @param parent reference to the parent object |
|
87 @type QObject |
|
88 """ |
|
89 super(MicroPythonFileManager, self).__init__(parent) |
|
90 |
|
91 self.__commandsInterface = commandsInterface |
|
92 |
|
93 @pyqtSlot(str) |
|
94 def lls(self, dirname): |
|
95 """ |
|
96 Public slot to get a long listing of the given directory. |
|
97 |
|
98 @param dirname name of the directory to list |
|
99 @type str |
|
100 """ |
|
101 try: |
|
102 filesList = self.__commandsInterface.lls(dirname) |
|
103 result = [(decoratedName(name, mode), |
|
104 mode2string(mode), |
|
105 str(size), |
|
106 mtime2string(mtime)) for |
|
107 name, (mode, size, mtime) in filesList] |
|
108 self.longListFiles.emit(tuple(result)) |
|
109 except Exception as exc: |
|
110 self.error.emit("lls", str(exc)) |
|
111 |
|
112 @pyqtSlot() |
|
113 def pwd(self): |
|
114 """ |
|
115 Public slot to get the current directory of the device. |
|
116 """ |
|
117 try: |
|
118 pwd = self.__commandsInterface.pwd() |
|
119 self.currentDir.emit(pwd) |
|
120 except Exception as exc: |
|
121 self.error.emit("pwd", str(exc)) |
|
122 |
|
123 @pyqtSlot(str) |
|
124 def cd(self, dirname): |
|
125 """ |
|
126 Public slot to change the current directory of the device. |
|
127 |
|
128 @param dirname name of the desired current directory |
|
129 @type str |
|
130 """ |
|
131 try: |
|
132 self.__commandsInterface.cd(dirname) |
|
133 self.currentDirChanged.emit(dirname) |
|
134 except Exception as exc: |
|
135 self.error.emit("cd", str(exc)) |
|
136 |
|
137 @pyqtSlot(str) |
|
138 @pyqtSlot(str, str) |
|
139 def get(self, deviceFileName, hostFileName=""): |
|
140 """ |
|
141 Public slot to get a file from the connected device. |
|
142 |
|
143 @param deviceFileName name of the file on the device |
|
144 @type str |
|
145 @param hostFileName name of the local file |
|
146 @type str |
|
147 """ |
|
148 if hostFileName and os.path.isdir(hostFileName): |
|
149 # only a local directory was given |
|
150 hostFileName = os.path.join(hostFileName, |
|
151 os.path.basename(deviceFileName)) |
|
152 try: |
|
153 self.__commandsInterface.get(deviceFileName, hostFileName) |
|
154 self.getFileDone.emit(deviceFileName, hostFileName) |
|
155 except Exception as exc: |
|
156 self.error.emit("get", str(exc)) |
|
157 |
|
158 @pyqtSlot(str) |
|
159 @pyqtSlot(str, str) |
|
160 def put(self, hostFileName, deviceFileName=""): |
|
161 """ |
|
162 Public slot to put a file onto the device. |
|
163 |
|
164 @param hostFileName name of the local file |
|
165 @type str |
|
166 @param deviceFileName name of the file on the connected device |
|
167 @type str |
|
168 """ |
|
169 try: |
|
170 self.__commandsInterface.put(hostFileName, deviceFileName) |
|
171 self.putFileDone.emit(hostFileName, deviceFileName) |
|
172 except Exception as exc: |
|
173 self.error.emit("put", str(exc)) |
|
174 |
|
175 @pyqtSlot(str) |
|
176 def delete(self, deviceFileName): |
|
177 """ |
|
178 Public slot to delete a file on the device. |
|
179 |
|
180 @param deviceFileName name of the file on the connected device |
|
181 @type str |
|
182 """ |
|
183 try: |
|
184 self.__commandsInterface.rm(deviceFileName) |
|
185 self.deleteFileDone.emit(deviceFileName) |
|
186 except Exception as exc: |
|
187 self.error.emit("delete", str(exc)) |
|
188 |
|
189 def __rsync(self, hostDirectory, deviceDirectory, mirror=True): |
|
190 """ |
|
191 Private method to synchronize a local directory to the device. |
|
192 |
|
193 @param hostDirectory name of the local directory |
|
194 @type str |
|
195 @param deviceDirectory name of the directory on the device |
|
196 @type str |
|
197 @param mirror flag indicating to mirror the local directory to |
|
198 the device directory |
|
199 @type bool |
|
200 @return list of errors |
|
201 @rtype list of str |
|
202 """ |
|
203 errors = [] |
|
204 |
|
205 if not os.path.isdir(hostDirectory): |
|
206 return [self.tr( |
|
207 "The given name '{0}' is not a directory or does not exist.") |
|
208 .format(hostDirectory) |
|
209 ] |
|
210 |
|
211 self.rsyncProgressMessage.emit( |
|
212 self.tr("Synchronizing <b>{0}</b>.").format(deviceDirectory) |
|
213 ) |
|
214 |
|
215 sourceDict = {} |
|
216 sourceFiles = listdirStat(hostDirectory) |
|
217 for name, nstat in sourceFiles: |
|
218 sourceDict[name] = nstat |
|
219 |
|
220 destinationDict = {} |
|
221 try: |
|
222 destinationFiles = self.__commandsInterface.lls(deviceDirectory, |
|
223 fullstat=True) |
|
224 except Exception as exc: |
|
225 return [str(exc)] |
|
226 if destinationFiles is None: |
|
227 # the destination directory does not exist |
|
228 try: |
|
229 self.__commandsInterface.mkdir(deviceDirectory) |
|
230 except Exception as exc: |
|
231 return [str(exc)] |
|
232 else: |
|
233 for name, nstat in destinationFiles: |
|
234 destinationDict[name] = nstat |
|
235 |
|
236 destinationSet = set(destinationDict.keys()) |
|
237 sourceSet = set(sourceDict.keys()) |
|
238 toAdd = sourceSet - destinationSet # add to dev |
|
239 toDelete = destinationSet - sourceSet # delete from dev |
|
240 toUpdate = destinationSet.intersection(sourceSet) # update files |
|
241 |
|
242 for sourceBasename in toAdd: |
|
243 # name exists in source but not in device |
|
244 sourceFilename = os.path.join(hostDirectory, sourceBasename) |
|
245 destFilename = deviceDirectory + "/" + sourceBasename |
|
246 self.rsyncProgressMessage.emit( |
|
247 self.tr("Adding <b>{0}</b>...").format(destFilename)) |
|
248 if os.path.isfile(sourceFilename): |
|
249 try: |
|
250 self.__commandsInterface.put(sourceFilename, destFilename) |
|
251 except Exception as exc: |
|
252 # just note issues but ignore them otherwise |
|
253 errors.append(str(exc)) |
|
254 if os.path.isdir(sourceFilename): |
|
255 # recurse |
|
256 errs = self.__rsync(sourceFilename, destFilename, |
|
257 mirror=mirror) |
|
258 # just note issues but ignore them otherwise |
|
259 errors.extend(errs) |
|
260 |
|
261 if mirror: |
|
262 for destBasename in toDelete: |
|
263 # name exists in device but not local, delete |
|
264 destFilename = deviceDirectory + "/" + destBasename |
|
265 self.rsyncProgressMessage.emit( |
|
266 self.tr("Removing <b>{0}</b>...").format(destFilename)) |
|
267 try: |
|
268 self.__commandsInterface.rmrf(destFilename, recursive=True, |
|
269 force=True) |
|
270 except Exception as exc: |
|
271 # just note issues but ignore them otherwise |
|
272 errors.append(str(exc)) |
|
273 |
|
274 for sourceBasename in toUpdate: |
|
275 # names exist in both; do an update |
|
276 sourceStat = sourceDict[sourceBasename] |
|
277 destStat = destinationDict[sourceBasename] |
|
278 sourceFilename = os.path.join(hostDirectory, sourceBasename) |
|
279 destFilename = deviceDirectory + "/" + sourceBasename |
|
280 destMode = destStat[0] |
|
281 if os.path.isdir(sourceFilename): |
|
282 if stat.S_ISDIR(destMode): |
|
283 # both are directories => recurs |
|
284 errs = self.__rsync(sourceFilename, destFilename, |
|
285 mirror=mirror) |
|
286 # just note issues but ignore them otherwise |
|
287 errors.extend(errs) |
|
288 else: |
|
289 self.rsyncProgressMessage.emit( |
|
290 self.tr("Source <b>{0}</b> is a directory and" |
|
291 " destination <b>{1}</b> is a file. Ignoring" |
|
292 " it.") |
|
293 .format(sourceFilename, destFilename) |
|
294 ) |
|
295 else: |
|
296 if stat.S_ISDIR(destMode): |
|
297 self.rsyncProgressMessage.emit( |
|
298 self.tr("Source <b>{0}</b> is a file and destination" |
|
299 " <b>{1}</b> is a directory. Ignoring it.") |
|
300 .format(sourceFilename, destFilename) |
|
301 ) |
|
302 else: |
|
303 if sourceStat[8] > destStat[8]: # mtime |
|
304 self.rsyncProgressMessage.emit( |
|
305 self.tr("Updating <b>{0}</b>...") |
|
306 .format(destFilename) |
|
307 ) |
|
308 try: |
|
309 self.__commandsInterface.put(sourceFilename, |
|
310 destFilename) |
|
311 except Exception as exc: |
|
312 errors.append(str(exc)) |
|
313 |
|
314 self.rsyncProgressMessage.emit( |
|
315 self.tr("Done synchronizing <b>{0}</b>.").format(deviceDirectory) |
|
316 ) |
|
317 |
|
318 return errors |
|
319 |
|
320 @pyqtSlot(str, str) |
|
321 @pyqtSlot(str, str, bool) |
|
322 def rsync(self, hostDirectory, deviceDirectory, mirror=True): |
|
323 """ |
|
324 Public slot to synchronize a local directory to the device. |
|
325 |
|
326 @param hostDirectory name of the local directory |
|
327 @type str |
|
328 @param deviceDirectory name of the directory on the device |
|
329 @type str |
|
330 @param mirror flag indicating to mirror the local directory to |
|
331 the device directory |
|
332 @type bool |
|
333 """ |
|
334 errors = self.__rsync(hostDirectory, deviceDirectory, mirror=mirror) |
|
335 if errors: |
|
336 self.error.emit("rsync", "\n".join(errors)) |
|
337 |
|
338 self.rsyncDone.emit(hostDirectory, deviceDirectory) |
|
339 |
|
340 @pyqtSlot(str) |
|
341 def mkdir(self, dirname): |
|
342 """ |
|
343 Public slot to create a new directory. |
|
344 |
|
345 @param dirname name of the directory to create |
|
346 @type str |
|
347 """ |
|
348 try: |
|
349 self.__commandsInterface.mkdir(dirname) |
|
350 self.createDirectoryDone.emit() |
|
351 except Exception as exc: |
|
352 self.error.emit("mkdir", str(exc)) |
|
353 |
|
354 @pyqtSlot(str) |
|
355 @pyqtSlot(str, bool) |
|
356 def rmdir(self, dirname, recursive=False): |
|
357 """ |
|
358 Public slot to (recursively) remove a directory. |
|
359 |
|
360 @param dirname name of the directory to be removed |
|
361 @type str |
|
362 @param recursive flag indicating a recursive removal |
|
363 @type bool |
|
364 """ |
|
365 try: |
|
366 if recursive: |
|
367 self.__commandsInterface.rmrf(dirname, recursive=True, |
|
368 force=True) |
|
369 else: |
|
370 self.__commandsInterface.rmdir(dirname) |
|
371 self.removeDirectoryDone.emit() |
|
372 except Exception as exc: |
|
373 self.error.emit("rmdir", str(exc)) |
|
374 |
|
375 def fileSystemInfo(self): |
|
376 """ |
|
377 Public method to obtain information about the currently mounted file |
|
378 systems. |
|
379 """ |
|
380 try: |
|
381 fsinfo = self.__commandsInterface.fileSystemInfo() |
|
382 self.fsinfoDone.emit(fsinfo) |
|
383 except Exception as exc: |
|
384 self.error.emit("fileSystemInfo", str(exc)) |
|
385 |
|
386 ################################################################## |
|
387 ## some non-filesystem related methods below |
|
388 ################################################################## |
|
389 |
|
390 @pyqtSlot() |
|
391 def synchronizeTime(self): |
|
392 """ |
|
393 Public slot to set the time of the connected device to the local |
|
394 computer's time. |
|
395 """ |
|
396 try: |
|
397 self.__commandsInterface.syncTime() |
|
398 self.synchTimeDone.emit() |
|
399 except Exception as exc: |
|
400 self.error.emit("rmdir", str(exc)) |
|
401 |
|
402 @pyqtSlot() |
|
403 def showTime(self): |
|
404 """ |
|
405 Public slot to get the current date and time of the device. |
|
406 """ |
|
407 try: |
|
408 dt = self.__commandsInterface.showTime() |
|
409 self.showTimeDone.emit(dt) |
|
410 except Exception as exc: |
|
411 self.error.emit("showTime", str(exc)) |
|
412 |
|
413 @pyqtSlot() |
|
414 def showVersion(self): |
|
415 """ |
|
416 Public slot to get the version info for the MicroPython run by the |
|
417 connected device. |
|
418 """ |
|
419 try: |
|
420 versionInfo = self.__commandsInterface.version() |
|
421 self.showVersionDone.emit(versionInfo) |
|
422 except Exception as exc: |
|
423 self.error.emit("showVersion", str(exc)) |
|
424 |
|
425 @pyqtSlot() |
|
426 def showImplementation(self): |
|
427 """ |
|
428 Public slot to obtain some implementation related information. |
|
429 """ |
|
430 try: |
|
431 impInfo = self.__commandsInterface.getImplementation() |
|
432 if impInfo["name"] == "micropython": |
|
433 name = "MicroPython" |
|
434 elif impInfo["name"] == "circuitpython": |
|
435 name = "CircuitPython" |
|
436 elif impInfo["name"] == "unknown": |
|
437 name = self.tr("unknown") |
|
438 else: |
|
439 name = impInfo["name"] |
|
440 if impInfo["version"] == "unknown": |
|
441 version = self.tr("unknown") |
|
442 else: |
|
443 version = impInfo["version"] |
|
444 self.showImplementationDone.emit(name, version) |
|
445 except Exception as exc: |
|
446 self.error.emit("showVersion", str(exc)) |