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 |
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 |
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) |