src/eric7/MicroPython/MicroPythonFileManager.py

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

eric ide

mercurial