eric6/MicroPython/MicroPythonFileSystem.py

branch
micropython
changeset 7081
ed510767c096
parent 7080
9a3adf033f90
child 7082
ec199ef0cfc6
equal deleted inserted replaced
7080:9a3adf033f90 7081:ed510767c096
9 9
10 from __future__ import unicode_literals 10 from __future__ import unicode_literals
11 11
12 import ast 12 import ast
13 import time 13 import time
14 import os
14 import stat 15 import stat
15 import os
16 16
17 from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QThread 17 from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QThread
18 18
19 from .MicroPythonSerialPort import MicroPythonSerialPort 19 from .MicroPythonSerialPort import MicroPythonSerialPort
20 from .MicroPythonFileSystemUtilities import (
21 mtime2string, mode2string, decoratedName, listdirStat
22 )
20 23
21 24
22 class MicroPythonFileSystem(QObject): 25 class MicroPythonFileSystem(QObject):
23 """ 26 """
24 Class implementing some file system commands for MicroPython. 27 Class implementing some file system commands for MicroPython.
25 28
26 Some FTP like commands are provided to perform operations on the file 29 Commands are provided to perform operations on the file system of a
27 system of a connected MicroPython device. Supported commands are: 30 connected MicroPython device. Supported commands are:
28 <ul> 31 <ul>
29 <li>ls: directory listing</li> 32 <li>ls: directory listing</li>
30 <li>lls: directory listing with meta data</li> 33 <li>lls: directory listing with meta data</li>
31 <li>cd: change directory</li> 34 <li>cd: change directory</li>
32 <li>pwd: get the current directory</li> 35 <li>pwd: get the current directory</li>
33 <li>put: copy a file to the connected device</li> 36 <li>put: copy a file to the connected device</li>
34 <li>get: get a file from the connected device</li> 37 <li>get: get a file from the connected device</li>
35 <li>rm: remove a file from the connected device</li> 38 <li>rm: remove a file from the connected device</li>
39 <li>rmrf: remove a file/directory recursively (like 'rm -rf' in bash)
36 <li>mkdir: create a new directory</li> 40 <li>mkdir: create a new directory</li>
37 <li>rmdir: remove an empty directory</li> 41 <li>rmdir: remove an empty directory</li>
42 </ul>
43
44 There are additional commands related to time and version.
45 <ul>
38 <li>version: get version info about MicroPython</li> 46 <li>version: get version info about MicroPython</li>
47 <li>syncTime: synchronize the time of the connected device</li>
48 <li>showTime: show the current time of the connected device</li>
39 </ul> 49 </ul>
40 """ 50 """
41 def __init__(self, parent=None): 51 def __init__(self, parent=None):
42 """ 52 """
43 Constructor 53 Constructor
174 out, err = self.__execute(commands) 184 out, err = self.__execute(commands)
175 if err: 185 if err:
176 raise IOError(self.__shortError(err)) 186 raise IOError(self.__shortError(err))
177 return ast.literal_eval(out.decode("utf-8")) 187 return ast.literal_eval(out.decode("utf-8"))
178 188
179 def lls(self, dirname=""): 189 def lls(self, dirname="", fullstat=False):
180 """ 190 """
181 Public method to get a long directory listing of the connected device 191 Public method to get a long directory listing of the connected device
182 including meta data. 192 including meta data.
183 193
184 @param dirname name of the directory to be listed 194 @param dirname name of the directory to be listed
185 @type str 195 @type str
186 @return list containing the the directory listing with tuple entries 196 @param fullstat flag indicating to return the full stat() tuple
187 of the name and and a tuple of mode, size and time 197 @type bool
188 @rtype tuple of str 198 @return list containing the directory listing with tuple entries of
199 the name and and a tuple of mode, size and time (if fullstat is
200 false) or the complete stat() tuple. 'None' is returned in case the
201 directory doesn't exist.
202 @rtype tuple of (str, tuple)
189 @exception IOError raised to indicate an issue with the device 203 @exception IOError raised to indicate an issue with the device
190 """ 204 """
191 commands = [ 205 commands = [
192 "import os", 206 "import os",
193 "\n".join([ 207 "\n".join([
201 "\n".join([ 215 "\n".join([
202 "def listdir_stat(dirname):", 216 "def listdir_stat(dirname):",
203 " try:", 217 " try:",
204 " files = os.listdir(dirname)", 218 " files = os.listdir(dirname)",
205 " except OSError:", 219 " except OSError:",
206 " return []", 220 " return None",
207 " if dirname in ('', '/'):", 221 " if dirname in ('', '/'):",
208 " return list((f, stat(f)) for f in files)", 222 " return list((f, stat(f)) for f in files)",
209 " return list((f, stat(dirname + '/' + f)) for f in files)", 223 " return list((f, stat(dirname + '/' + f)) for f in files)",
210 ]), 224 ]),
211 "print(listdir_stat('{0}'))".format(dirname), 225 "print(listdir_stat('{0}'))".format(dirname),
212 ] 226 ]
213 out, err = self.__execute(commands) 227 out, err = self.__execute(commands)
214 if err: 228 if err:
215 raise IOError(self.__shortError(err)) 229 raise IOError(self.__shortError(err))
216 fileslist = ast.literal_eval(out.decode("utf-8")) 230 fileslist = ast.literal_eval(out.decode("utf-8"))
217 return [(f, (s[0], s[6], s[8])) for f, s in fileslist] 231 if fileslist is None:
232 return None
233 else:
234 if fullstat:
235 return fileslist
236 else:
237 return [(f, (s[0], s[6], s[8])) for f, s in fileslist]
218 238
219 def cd(self, dirname): 239 def cd(self, dirname):
220 """ 240 """
221 Public method to change the current directory on the connected device. 241 Public method to change the current directory on the connected device.
222 242
266 "os.remove('{0}')".format(filename), 286 "os.remove('{0}')".format(filename),
267 ] 287 ]
268 out, err = self.__execute(commands) 288 out, err = self.__execute(commands)
269 if err: 289 if err:
270 raise IOError(self.__shortError(err)) 290 raise IOError(self.__shortError(err))
291
292 def rmrf(self, name, recursive=False, force=False):
293 """
294 Public method to remove a file or directory recursively.
295
296 @param name of the file or directory to remove
297 @type str
298 @param recursive flag indicating a recursive deletion
299 @type bool
300 @param force flag indicating to ignore errors
301 @type bool
302 @return flag indicating success
303 @rtype bool
304 @exception IOError raised to indicate an issue with the device
305 """
306 assert name
307
308 commands = [
309 "import os"
310 "\n".join([
311 "def remove_file(name, recursive=False, force=False):",
312 " try:",
313 " mode = os.stat(name)[0]",
314 " if mode & 0x4000 != 0:",
315 " if recursive:",
316 " for file in os.listdir(name):",
317 " success = remove_file(name + '/' + file,"
318 " recursive, force)",
319 " if not success and not force:",
320 " return False",
321 " os.rmdir(name)",
322 " else:",
323 " if not force:",
324 " return False",
325 " else:",
326 " os.remove(name)",
327 " except:",
328 " if not force:",
329 " return False",
330 " return True",
331 ]),
332 "print(remove_file('{0}', {1}, {2}))".format(name, recursive,
333 force),
334 ]
335 out, err = self.__execute(commands)
336 if err:
337 raise IOError(self.__shortError(err))
338 return ast.literal_eval(out.decode("utf-8"))
271 339
272 def mkdir(self, dirname): 340 def mkdir(self, dirname):
273 """ 341 """
274 Public method to create a new directory. 342 Public method to create a new directory.
275 343
374 "f = open('{0}', 'rb')".format(deviceFileName), 442 "f = open('{0}', 'rb')".format(deviceFileName),
375 "r = f.read", 443 "r = f.read",
376 "result = True", 444 "result = True",
377 "\n".join([ 445 "\n".join([
378 "while result:", 446 "while result:",
379 " result = r(32)" 447 " result = r(32)",
380 " if result:", 448 " if result:",
381 " u.write(result)", 449 " u.write(result)",
382 ]), 450 ]),
383 "f.close()", 451 "f.close()",
384 ] 452 ]
388 456
389 # write the received bytes to the local file 457 # write the received bytes to the local file
390 with open(hostFileName, "wb") as hostFile: 458 with open(hostFileName, "wb") as hostFile:
391 hostFile.write(out) 459 hostFile.write(out)
392 return True 460 return True
393
394 # TODO: add rsync function
395 461
396 def version(self): 462 def version(self):
397 """ 463 """
398 Public method to get the MicroPython version information of the 464 Public method to get the MicroPython version information of the
399 connected device. 465 connected device.
462 now.tm_sec, 0)) 528 now.tm_sec, 0))
463 ] 529 ]
464 out, err = self.__execute(commands) 530 out, err = self.__execute(commands)
465 if err: 531 if err:
466 raise IOError(self.__shortError(err)) 532 raise IOError(self.__shortError(err))
533
534 def showTime(self):
535 """
536 Public method to get the current time of the device.
537
538 @return time of the device
539 @rtype str
540 @exception IOError raised to indicate an issue with the device
541 """
542 commands = [
543 "import time",
544 "print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))",
545 # __IGNORE_WARNING_M601__
546 ]
547 out, err = self.__execute(commands)
548 if err:
549 raise IOError(self.__shortError(err))
550 return out.decode("utf-8").strip()
467 551
468 552
469 class MicroPythonFileManager(QObject): 553 class MicroPythonFileManager(QObject):
470 """ 554 """
471 Class implementing an interface to the device file system commands with 555 Class implementing an interface to the device file system commands with
473 557
474 @signal longListFiles(result) emitted with a tuple of tuples containing the 558 @signal longListFiles(result) emitted with a tuple of tuples containing the
475 name, mode, size and time for each directory entry 559 name, mode, size and time for each directory entry
476 @signal currentDir(dirname) emitted to report the current directory of the 560 @signal currentDir(dirname) emitted to report the current directory of the
477 device 561 device
562 @signal currentDirChanged(dirname) emitted to report back a change of the
563 current directory
478 @signal getFileDone(deviceFile, localFile) emitted after the file was 564 @signal getFileDone(deviceFile, localFile) emitted after the file was
479 fetched from the connected device and written to the local file system 565 fetched from the connected device and written to the local file system
480 @signal putFileDone(localFile, deviceFile) emitted after the file was 566 @signal putFileDone(localFile, deviceFile) emitted after the file was
481 copied to the connected device 567 copied to the connected device
482 @signal deleteFileDone(deviceFile) emitted after the file has been deleted 568 @signal deleteFileDone(deviceFile) emitted after the file has been deleted
483 on the connected device 569 on the connected device
570 @signal rsyncDone(localName, deviceName) emitted after the rsync operation
571 has been completed
572 @signal rsyncMessages(list) emitted with a list of messages
484 573
485 @signal longListFilesFailed(exc) emitted with a failure message to indicate 574 @signal longListFilesFailed(exc) emitted with a failure message to indicate
486 a failed long listing operation 575 a failed long listing operation
487 @signal currentDirFailed(exc) emitted with a failure message to indicate 576 @signal currentDirFailed(exc) emitted with a failure message to indicate
488 that the current directory is not available 577 that the current directory is not available
578 @signal currentDirChangeFailed(exc) emitted with a failure message to
579 indicate that the current directory could not be changed
489 @signal getFileFailed(exc) emitted with a failure message to indicate that 580 @signal getFileFailed(exc) emitted with a failure message to indicate that
490 the file could not be fetched 581 the file could not be fetched
491 @signal putFileFailed(exc) emitted with a failure message to indicate that 582 @signal putFileFailed(exc) emitted with a failure message to indicate that
492 the file could not be copied 583 the file could not be copied
493 @signal deleteFileFailed(exc) emitted with a failure message to indicate 584 @signal deleteFileFailed(exc) emitted with a failure message to indicate
494 that the file could not be deleted on the device 585 that the file could not be deleted on the device
586 @signal rsyncFailed(exc) emitted with a failure message to indicate that
587 the rsync operation could not be completed
495 """ 588 """
496 longListFiles = pyqtSignal(tuple) 589 longListFiles = pyqtSignal(tuple)
497 currentDir = pyqtSignal(str) 590 currentDir = pyqtSignal(str)
591 currentDirChanged = pyqtSignal(str)
498 getFileDone = pyqtSignal(str, str) 592 getFileDone = pyqtSignal(str, str)
499 putFileDone = pyqtSignal(str, str) 593 putFileDone = pyqtSignal(str, str)
500 deleteFileDone = pyqtSignal(str) 594 deleteFileDone = pyqtSignal(str)
595 rsyncDone = pyqtSignal(str, str)
596 rsyncMessages = pyqtSignal(list)
501 597
502 longListFilesFailed = pyqtSignal(str) 598 longListFilesFailed = pyqtSignal(str)
503 currentDirFailed = pyqtSignal(str) 599 currentDirFailed = pyqtSignal(str)
600 currentDirChangeFailed = pyqtSignal(str)
504 getFileFailed = pyqtSignal(str) 601 getFileFailed = pyqtSignal(str)
505 putFileFailed = pyqtSignal(str) 602 putFileFailed = pyqtSignal(str)
506 deleteFileFailed = pyqtSignal(str) 603 deleteFileFailed = pyqtSignal(str)
604 rsyncFailed = pyqtSignal(str)
507 605
508 def __init__(self, port, parent=None): 606 def __init__(self, port, parent=None):
509 """ 607 """
510 Constructor 608 Constructor
511 609
564 self.currentDir.emit(pwd) 662 self.currentDir.emit(pwd)
565 except Exception as exc: 663 except Exception as exc:
566 self.currentDirFailed.emit(str(exc)) 664 self.currentDirFailed.emit(str(exc))
567 665
568 @pyqtSlot(str) 666 @pyqtSlot(str)
667 def cd(self, dirname):
668 """
669 Public slot to change the current directory of the device.
670
671 @param dirname name of the desired current directory
672 @type str
673 """
674 try:
675 self.__fs.cd(dirname)
676 self.currentDirChanged.emit(dirname)
677 except Exception as exc:
678 self.currentDirChangeFailed.emit(str(exc))
679
680 @pyqtSlot(str)
569 @pyqtSlot(str, str) 681 @pyqtSlot(str, str)
570 def get(self, deviceFileName, hostFileName=""): 682 def get(self, deviceFileName, hostFileName=""):
571 """ 683 """
572 Public slot to get a file from the connected device. 684 Public slot to get a file from the connected device.
573 685
597 @param deviceFileName name of the file on the connected device 709 @param deviceFileName name of the file on the connected device
598 @type str 710 @type str
599 """ 711 """
600 try: 712 try:
601 self.__fs.put(hostFileName, deviceFileName) 713 self.__fs.put(hostFileName, deviceFileName)
602 self.putFileDone(hostFileName, deviceFileName) 714 self.putFileDone.emit(hostFileName, deviceFileName)
603 except Exception as exc: 715 except Exception as exc:
604 self.putFileFailed(str(exc)) 716 self.putFileFailed.emit(str(exc))
605 717
606 @pyqtSlot(str) 718 @pyqtSlot(str)
607 def delete(self, deviceFileName): 719 def delete(self, deviceFileName):
608 """ 720 """
609 Public slot to delete a file on the device. 721 Public slot to delete a file on the device.
613 """ 725 """
614 try: 726 try:
615 self.__fs.rm(deviceFileName) 727 self.__fs.rm(deviceFileName)
616 self.deleteFileDone.emit(deviceFileName) 728 self.deleteFileDone.emit(deviceFileName)
617 except Exception as exc: 729 except Exception as exc:
618 self.deleteFileFailed(str(exc)) 730 self.deleteFileFailed.emit(str(exc))
619 731
620 ################################################################## 732 def __rsync(self, hostDirectory, deviceDirectory, mirror=True):
621 ## Utility methods below 733 """
622 ################################################################## 734 Private method to synchronize a local directory to the device.
623 735
624 736 @param hostDirectory name of the local directory
625 def mtime2string(mtime): 737 @type str
626 """ 738 @param deviceDirectory name of the directory on the device
627 Function to convert a time value to a string representation. 739 @type str
628 740 @param mirror flag indicating to mirror the local directory to
629 @param mtime time value 741 the device directory
630 @type int 742 @type bool
631 @return string representation of the given time 743 @return tuple containing a list of messages and list of errors
632 @rtype str 744 @rtype tuple of (list of str, list of str)
633 """ 745 """
634 return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime)) 746 messages = []
635 747 errors = []
636 748
637 def mode2string(mode): 749 if not os.isdir(hostDirectory):
638 """ 750 return ([], [self.tr(
639 Function to convert a mode value to a string representation. 751 "The given name '{0}' is not a directory or does not exist.")
640 752 .format(hostDirectory)
641 @param mode mode value 753 ])
642 @type int 754
643 @return string representation of the given mode value 755 sourceDict = {}
644 @rtype str 756 sourceFiles = listdirStat(hostDirectory)
645 """ 757 for name, nstat in sourceFiles:
646 return stat.filemode(mode) 758 sourceDict[name] = nstat
647 759
648 760 destinationDict = {}
649 def decoratedName(name, mode, isDir=False): 761 try:
650 """ 762 destinationFiles = self.__fs.lls(deviceDirectory, fullstat=True)
651 Function to decorate the given name according to the given mode. 763 except Exception as exc:
652 764 return ([], [str(exc)])
653 @param name file or directory name 765 if destinationFiles is None:
654 @type str 766 # the destination directory does not exist
655 @param mode mode value 767 try:
656 @type int 768 self.__fs.mkdir(deviceDirectory)
657 @param isDir flag indicating that name is a directory 769 except Exception as exc:
658 @type bool 770 return ([], [str(exc)])
659 @return decorated file or directory name 771 else:
660 @rtype str 772 for name, nstat in destinationFiles:
661 """ 773 destinationDict[name] = nstat
662 if stat.S_ISDIR(mode) or isDir: 774
663 # append a '/' for directories 775 destinationSet = set(destinationDict.keys())
664 return name + "/" 776 sourceSet = set(sourceDict.keys())
665 else: 777 toAdd = sourceSet - destinationSet # add to dev
666 # no change 778 toDelete = destinationSet - sourceSet # delete from dev
667 return name 779 toUpdate = destinationSet.intersection(sourceSet) # update files
780
781 for sourceBasename in toAdd:
782 # name exists in source but not in device
783 sourceFilename = os.path.join(hostDirectory, sourceBasename)
784 destFilename = deviceDirectory + "/" + sourceBasename
785 if os.path.isfile(sourceFilename):
786 try:
787 self.__fs.put(sourceFilename, destFilename)
788 except Exception as exc:
789 messages.append(str(exc))
790 if os.path.isdir(sourceFilename):
791 # recurse
792 msg, err = self.__rsync(sourceFilename, destFilename,
793 mirror=mirror)
794 messages.extend(msg)
795 errors.extend(err)
796
797 if mirror:
798 for destBasename in toDelete:
799 # name exists in device but not local, delete
800 destFilename = deviceDirectory + "/" + destBasename
801 try:
802 self.__fs.rmrf(destFilename, recursive=True, force=True)
803 except Exception as exc:
804 # ignore errors here
805 messages.append(str(exc))
806
807 for sourceBasename in toUpdate:
808 # names exist in both; do an update
809 sourceStat = sourceDict[sourceBasename]
810 destStat = destinationDict[sourceBasename]
811 sourceFilename = os.path.join(hostDirectory, sourceBasename)
812 destFilename = deviceDirectory + "/" + sourceBasename
813 destMode = destStat[0]
814 if os.path.isdir(sourceFilename):
815 if stat.S_ISDIR(destMode):
816 # both are directories => recurse
817 msg, err = self.__rsync(sourceFilename, destFilename,
818 mirror=mirror)
819 messages.extend(msg)
820 errors.extend(err)
821 else:
822 messages.append(self.tr(
823 "Source '{0}' is a directory and destination '{1}'"
824 " is a file. Ignoring it."
825 ).format(sourceFilename, destFilename))
826 else:
827 if stat.S_ISDIR(destMode):
828 messages.append(self.tr(
829 "Source '{0}' is a file and destination '{1}' is"
830 " a directory. Ignoring it."
831 ).format(sourceFilename, destFilename))
832 else:
833 if sourceStat[8] > destStat[8]: # mtime
834 messages.append(self.tr(
835 "'{0}' is newer than '{1}' - copying"
836 ).format(sourceFilename, destFilename))
837 try:
838 self.__fs.put(sourceFilename, destFilename)
839 except Exception as exc:
840 messages.append(str(exc))
841
842 return messages, errors
843
844 def rsync(self, hostDirectory, deviceDirectory, mirror=True):
845 """
846 Public method to synchronize a local directory to the device.
847
848 @param hostDirectory name of the local directory
849 @type str
850 @param deviceDirectory name of the directory on the device
851 @type str
852 @param mirror flag indicating to mirror the local directory to
853 the device directory
854 @type bool
855 """
856 messages, errors = self.__rsync(hostDirectory, deviceDirectory,
857 mirror=mirror)
858 if errors:
859 self.rsyncFailed.emit("\n".join(errors))
860
861 if messages:
862 self.rsyncMessages.emit(messages)
863
864 self.rsyncDone.emit(hostDirectory, deviceDirectory)

eric ide

mercurial