src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py

branch
server
changeset 10631
00f5aae565a3
parent 10610
bb0149571d94
child 10633
dda7e43934dc
equal deleted inserted replaced
10630:552a790fd9bc 10631:00f5aae565a3
11 import contextlib 11 import contextlib
12 import os 12 import os
13 import re 13 import re
14 import stat 14 import stat
15 15
16 from PyQt6.QtCore import QEventLoop, QObject, pyqtSlot 16 from PyQt6.QtCore import QByteArray, QEventLoop, QObject, pyqtSlot
17 17
18 from eric7 import Utilities 18 from eric7 import Utilities
19 from eric7.RemoteServer.EricRequestCategory import EricRequestCategory 19 from eric7.RemoteServer.EricRequestCategory import EricRequestCategory
20 from eric7.SystemUtilities import FileSystemUtilities 20 from eric7.SystemUtilities import FileSystemUtilities
21
22
23 _RemoteFsCache = {}
24 # dictionary containing cached remote file system data keyed by remote path
21 25
22 26
23 class EricServerNotConnectedError(OSError): 27 class EricServerNotConnectedError(OSError):
24 """ 28 """
25 Class defining a special OSError indicating a missing server connection. 29 Class defining a special OSError indicating a missing server connection.
27 31
28 def __init__(self): 32 def __init__(self):
29 """ 33 """
30 Constructor 34 Constructor
31 """ 35 """
32 super().__init("Not connected to an 'eric-ide' server.") 36 super().__init__("Not connected to an 'eric-ide' server.")
33 37
34 38
35 class EricServerFileSystemInterface(QObject): 39 class EricServerFileSystemInterface(QObject):
36 """ 40 """
37 Class implementing the file system interface to the eric-ide server. 41 Class implementing the file system interface to the eric-ide server.
80 @type bool 84 @type bool
81 """ 85 """
82 if connected: 86 if connected:
83 if not bool(self.__serverPathSep): 87 if not bool(self.__serverPathSep):
84 self.__serverPathSep = self.__getPathSep() 88 self.__serverPathSep = self.__getPathSep()
85 else:
86 self.__serverPathSep = ""
87 89
88 def __getPathSep(self): 90 def __getPathSep(self):
89 """ 91 """
90 Private method to get the path separator of the connected server. 92 Private method to get the path separator of the connected server.
91 93
201 return ok, error 203 return ok, error
202 204
203 else: 205 else:
204 return False, EricServerFileSystemInterface.NotConnectedMessage 206 return False, EricServerFileSystemInterface.NotConnectedMessage
205 207
206 def listdir(self, directory=""): 208 def listdir(self, directory="", recursive=False):
207 """ 209 """
208 Public method to get a directory listing. 210 Public method to get a directory listing.
209 211
210 @param directory directory to be listed. An empty directory means to list 212 @param directory directory to be listed. An empty directory means to list
211 the eric-ide server current directory. (defaults to "") 213 the eric-ide server current directory. (defaults to "")
212 @type str (optional) 214 @type str (optional)
215 @param recursive flag indicating a recursive listing (defaults to False)
216 @type bool (optional)
213 @return tuple containing the listed directory, the path separator and the 217 @return tuple containing the listed directory, the path separator and the
214 directory listing. Each directory listing entry contains a dictionary 218 directory listing. Each directory listing entry contains a dictionary
215 with the relevant data. 219 with the relevant data.
216 @rtype tuple of (str, str, dict) 220 @rtype tuple of (str, str, dict)
217 @exception OSError raised in case the server reported an issue 221 @exception OSError raised in case the server reported an issue
250 254
251 if self.__serverInterface.isServerConnected(): 255 if self.__serverInterface.isServerConnected():
252 self.__serverInterface.sendJson( 256 self.__serverInterface.sendJson(
253 category=EricRequestCategory.FileSystem, 257 category=EricRequestCategory.FileSystem,
254 request="Listdir", 258 request="Listdir",
255 params={"directory": FileSystemUtilities.plainFileName(directory)}, 259 params={
260 "directory": FileSystemUtilities.plainFileName(directory),
261 "recursive": recursive,
262 },
256 callback=callback, 263 callback=callback,
257 ) 264 )
258 265
259 loop.exec() 266 loop.exec()
260 if not ok: 267 if not ok:
291 @param ignore list of entries to be ignored (defaults to None) 298 @param ignore list of entries to be ignored (defaults to None)
292 @type list of str (optional) 299 @type list of str (optional)
293 @param recursive flag indicating a recursive search (defaults to True) 300 @param recursive flag indicating a recursive search (defaults to True)
294 @type bool (optional) 301 @type bool (optional)
295 @param dirsonly flag indicating to return only directories. When True it has 302 @param dirsonly flag indicating to return only directories. When True it has
296 precedence over the 'filesonly' parameter ((defaults to False) 303 precedence over the 'filesonly' parameter (defaults to False)
297 @type bool 304 @type bool
298 @return list of all files and directories in the tree rooted at path. 305 @return list of all files and directories in the tree rooted at path.
299 The names are expanded to start with the given directory name. 306 The names are expanded to start with the given directory name.
300 @rtype list of str 307 @rtype list of str
301 @exception OSError raised in case the server reported an issue 308 @exception OSError raised in case the server reported an issue
441 @param name name to be checked 448 @param name name to be checked
442 @type str 449 @type str
443 @return flag indicating a directory 450 @return flag indicating a directory
444 @rtype bool 451 @rtype bool
445 """ 452 """
446 with contextlib.suppress(KeyError, OSError): 453 try:
447 result = self.stat(name, ["st_mode"]) 454 return stat.S_ISDIR(_RemoteFsCache[name]["mode"])
448 return stat.S_ISDIR(result["st_mode"]) 455 except KeyError:
456 with contextlib.suppress(KeyError, OSError):
457 result = self.stat(name, ["st_mode"])
458 return stat.S_ISDIR(result["st_mode"])
449 459
450 return False 460 return False
451 461
452 def isfile(self, name): 462 def isfile(self, name):
453 """ 463 """
456 @param name name to be checked 466 @param name name to be checked
457 @type str 467 @type str
458 @return flag indicating a regular file 468 @return flag indicating a regular file
459 @rtype bool 469 @rtype bool
460 """ 470 """
461 with contextlib.suppress(KeyError, OSError): 471 try:
462 result = self.stat(name, ["st_mode"]) 472 return stat.S_ISREG(_RemoteFsCache[name]["mode"])
463 return stat.S_ISREG(result["st_mode"]) 473 except KeyError:
474 with contextlib.suppress(KeyError, OSError):
475 result = self.stat(name, ["st_mode"])
476 return stat.S_ISREG(result["st_mode"])
464 477
465 return False 478 return False
466 479
467 def exists(self, name): 480 def exists(self, name):
468 """ 481 """
488 nonlocal nameExists 501 nonlocal nameExists
489 502
490 if reply == "Exists": 503 if reply == "Exists":
491 nameExists = params["exists"] 504 nameExists = params["exists"]
492 loop.quit() 505 loop.quit()
506
507 if name in _RemoteFsCache:
508 return True
493 509
494 if self.__serverInterface.isServerConnected(): 510 if self.__serverInterface.isServerConnected():
495 self.__serverInterface.sendJson( 511 self.__serverInterface.sendJson(
496 category=EricRequestCategory.FileSystem, 512 category=EricRequestCategory.FileSystem,
497 request="Exists", 513 request="Exists",
596 params={"directory": FileSystemUtilities.plainFileName(directory)}, 612 params={"directory": FileSystemUtilities.plainFileName(directory)},
597 callback=callback, 613 callback=callback,
598 ) 614 )
599 615
600 loop.exec() 616 loop.exec()
617 if ok:
618 self.populateFsCache(directory)
601 return ok, error 619 return ok, error
602 620
603 else: 621 else:
604 return False, EricServerFileSystemInterface.NotConnectedMessage 622 return False, EricServerFileSystemInterface.NotConnectedMessage
605 623
647 }, 665 },
648 callback=callback, 666 callback=callback,
649 ) 667 )
650 668
651 loop.exec() 669 loop.exec()
670 if ok:
671 self.populateFsCache(directory)
652 return ok, error 672 return ok, error
653 673
654 else: 674 else:
655 return False, EricServerFileSystemInterface.NotConnectedMessage 675 return False, EricServerFileSystemInterface.NotConnectedMessage
656 676
691 params={"directory": FileSystemUtilities.plainFileName(directory)}, 711 params={"directory": FileSystemUtilities.plainFileName(directory)},
692 callback=callback, 712 callback=callback,
693 ) 713 )
694 714
695 loop.exec() 715 loop.exec()
716 if ok:
717 self.removeFromFsCache(directory)
696 return ok, error 718 return ok, error
697 719
698 else: 720 else:
699 return False, EricServerFileSystemInterface.NotConnectedMessage 721 return False, EricServerFileSystemInterface.NotConnectedMessage
700 722
740 }, 762 },
741 callback=callback, 763 callback=callback,
742 ) 764 )
743 765
744 loop.exec() 766 loop.exec()
767 if ok:
768 with contextlib.suppress(KeyError):
769 entry = _RemoteFsCache.pop(oldName)
770 entry["path"] = newName
771 entry["name"] = self.basename(newName)
772 _RemoteFsCache[newName] = entry
745 return ok, error 773 return ok, error
746 774
747 else: 775 else:
748 return False, EricServerFileSystemInterface.NotConnectedMessage 776 return False, EricServerFileSystemInterface.NotConnectedMessage
749 777
784 params={"filename": FileSystemUtilities.plainFileName(filename)}, 812 params={"filename": FileSystemUtilities.plainFileName(filename)},
785 callback=callback, 813 callback=callback,
786 ) 814 )
787 815
788 loop.exec() 816 loop.exec()
817 if ok:
818 with contextlib.suppress(KeyError):
819 del _RemoteFsCache[filename]
789 return ok, error 820 return ok, error
790 821
791 else: 822 else:
792 return False, EricServerFileSystemInterface.NotConnectedMessage 823 return False, EricServerFileSystemInterface.NotConnectedMessage
793 824
857 @param p path to be checked 888 @param p path to be checked
858 @type str 889 @type str
859 @return flag indicating an absolute path 890 @return flag indicating an absolute path
860 @rtype bool 891 @rtype bool
861 """ 892 """
862 if self.__serverInterface.isServerConnected(): 893 if self.__serverPathSep == "\\":
863 if self.__serverPathSep == "\\": 894 s = FileSystemUtilities.plainFileName(p)[:3].replace("/", "\\")
864 s = FileSystemUtilities.plainFileName(p)[:3].replace("/", "\\") 895 return s.startswith("\\)") or s.startswith(":\\", 1)
865 return s.startswith("\\)") or s.startswith(":\\", 1)
866 else:
867 return FileSystemUtilities.plainFileName(p).startswith("/")
868 else: 896 else:
869 return os.path.isabs(p) 897 return FileSystemUtilities.plainFileName(p).startswith("/")
870 898
871 def abspath(self, p): 899 def abspath(self, p):
872 """ 900 """
873 Public method to convert the given path to an absolute path. 901 Public method to convert the given path to an absolute path.
874 902
875 @param p path to be converted 903 @param p path to be converted
876 @type str 904 @type str
877 @return absolute path 905 @return absolute path
878 @rtype str 906 @rtype str
879 """ 907 """
880 if self.__serverInterface.isServerConnected(): 908 p = FileSystemUtilities.plainFileName(p)
881 p = FileSystemUtilities.plainFileName(p) 909 if not self.isabs(p):
882 if not self.isabs(p): 910 p = self.join(self.getcwd(), p)
883 p = self.join(self.getcwd(), p) 911 return FileSystemUtilities.remoteFileName(p)
884 return FileSystemUtilities.remoteFileName(p)
885 else:
886 return os.path.abspath(p)
887 912
888 def join(self, a, *p): 913 def join(self, a, *p):
889 """ 914 """
890 Public method to join two or more path name components using the path separator 915 Public method to join two or more path name components using the path separator
891 of the server side. 916 of the server side.
895 @param *p list of additional path components 920 @param *p list of additional path components
896 @type list of str 921 @type list of str
897 @return joined path name 922 @return joined path name
898 @rtype str 923 @rtype str
899 """ 924 """
900 if self.__serverInterface.isServerConnected(): 925 path = a
901 path = a 926 for b in p:
902 for b in p: 927 if b.startswith(self.__serverPathSep):
903 if b.startswith(self.__serverPathSep): 928 path = b
904 path = b 929 elif not path or path.endswith(self.__serverPathSep):
905 elif not path or path.endswith(self.__serverPathSep): 930 path += b
906 path += b 931 else:
907 else: 932 path += self.__serverPathSep + b
908 path += self.__serverPathSep + b 933 return path
909 return path
910
911 else:
912 return os.path.join(a, *p)
913 934
914 def split(self, p): 935 def split(self, p):
915 """ 936 """
916 Public method to split a path name. 937 Public method to split a path name.
917 938
919 @type str 940 @type str
920 @return tuple containing head and tail, where tail is everything after the last 941 @return tuple containing head and tail, where tail is everything after the last
921 path separator. 942 path separator.
922 @rtype tuple of (str, str) 943 @rtype tuple of (str, str)
923 """ 944 """
924 if self.__serverInterface.isServerConnected(): 945 if self.__serverPathSep == "\\":
925 if self.__serverPathSep == "\\": 946 # remote is a Windows system
926 # remote is a Windows system 947 normp = p.replace("/", "\\")
927 normp = p.replace("/", "\\")
928 else:
929 # remote is a Posix system
930 normp = p.replace("\\", "/")
931
932 i = normp.rfind(self.__serverPathSep) + 1
933 head, tail = normp[:i], normp[i:]
934 if head and head != self.__serverPathSep * len(head):
935 head = head.rstrip(self.__serverPathSep)
936 return head, tail
937
938 else: 948 else:
939 return os.path.split(p) 949 # remote is a Posix system
950 normp = p.replace("\\", "/")
951
952 i = normp.rfind(self.__serverPathSep) + 1
953 head, tail = normp[:i], normp[i:]
954 if head and head != self.__serverPathSep * len(head):
955 head = head.rstrip(self.__serverPathSep)
956 return head, tail
940 957
941 def splitext(self, p): 958 def splitext(self, p):
942 """ 959 """
943 Public method to split a path name into a root part and an extension. 960 Public method to split a path name into a root part and an extension.
944 961
956 @param p path name to be split 973 @param p path name to be split
957 @type str 974 @type str
958 @return tuple containing the drive letter (incl. colon) and the path 975 @return tuple containing the drive letter (incl. colon) and the path
959 @rtype tuple of (str, str) 976 @rtype tuple of (str, str)
960 """ 977 """
961 if self.__serverInterface.isServerConnected(): 978 plainp = FileSystemUtilities.plainFileName(p)
962 plainp = FileSystemUtilities.plainFileName(p) 979
963 980 if self.__serverPathSep == "\\":
964 if self.__serverPathSep == "\\": 981 # remote is a Windows system
965 # remote is a Windows system 982 normp = plainp.replace("/", "\\")
966 normp = plainp.replace("/", "\\") 983 if normp[1:2] == ":":
967 if normp[1:2] == ":": 984 return normp[:2], normp[2:]
968 return normp[:2], normp[2:]
969 else:
970 return "", normp
971 else: 985 else:
972 # remote is a Posix system
973 normp = plainp.replace("\\", "/")
974 return "", normp 986 return "", normp
975
976 else: 987 else:
977 return os.path.splitdrive(p) 988 # remote is a Posix system
989 normp = plainp.replace("\\", "/")
990 return "", normp
978 991
979 def dirname(self, p): 992 def dirname(self, p):
980 """ 993 """
981 Public method to extract the directory component of a path name. 994 Public method to extract the directory component of a path name.
982 995
1026 1039
1027 ####################################################################### 1040 #######################################################################
1028 ## Methods for reading and writing files 1041 ## Methods for reading and writing files
1029 ####################################################################### 1042 #######################################################################
1030 1043
1031 def readFile(self, filename, create=False): 1044 def readFile(self, filename, create=False, newline=None):
1032 """ 1045 """
1033 Public method to read a file from the eric-ide server. 1046 Public method to read a file from the eric-ide server.
1034 1047
1035 @param filename name of the file to read 1048 @param filename name of the file to read
1036 @type str 1049 @type str
1037 @param create flag indicating to create an empty file, if it does not exist 1050 @param create flag indicating to create an empty file, if it does not exist
1038 (defaults to False) 1051 (defaults to False)
1039 @type bool (optional) 1052 @type bool (optional)
1053 @param newline determines how to parse newline characters from the stream
1054 (defaults to None)
1055 @type str (optional)
1040 @return bytes data read from the eric-ide server 1056 @return bytes data read from the eric-ide server
1041 @rtype bytes 1057 @rtype bytes
1042 @exception EricServerNotConnectedError raised to indicate a missing server 1058 @exception EricServerNotConnectedError raised to indicate a missing server
1043 connection 1059 connection
1044 @exception OSError raised in case the server reported an issue 1060 @exception OSError raised in case the server reported an issue
1077 category=EricRequestCategory.FileSystem, 1093 category=EricRequestCategory.FileSystem,
1078 request="ReadFile", 1094 request="ReadFile",
1079 params={ 1095 params={
1080 "filename": FileSystemUtilities.plainFileName(filename), 1096 "filename": FileSystemUtilities.plainFileName(filename),
1081 "create": create, 1097 "create": create,
1098 "newline": "<<none>>" if newline is None else newline,
1082 }, 1099 },
1083 callback=callback, 1100 callback=callback,
1084 ) 1101 )
1085 1102
1086 loop.exec() 1103 loop.exec()
1087 if not ok: 1104 if not ok:
1088 raise OSError(error) 1105 raise OSError(error)
1089 1106
1090 return bText 1107 return bText
1091 1108
1092 def writeFile(self, filename, data, withBackup=False): 1109 def writeFile(self, filename, data, withBackup=False, newline=None):
1093 """ 1110 """
1094 Public method to write the data to a file on the eric-ide server. 1111 Public method to write the data to a file on the eric-ide server.
1095 1112
1096 @param filename name of the file to write 1113 @param filename name of the file to write
1097 @type str 1114 @type str
1098 @param data data to be written 1115 @param data data to be written
1099 @type bytes 1116 @type bytes or QByteArray
1100 @param withBackup flag indicating to create a backup file first 1117 @param withBackup flag indicating to create a backup file first
1101 (defaults to False) 1118 (defaults to False)
1102 @type bool (optional) 1119 @type bool (optional)
1120 @param newline determines how to parse newline characters from the stream
1121 (defaults to None)
1122 @type str (optional)
1103 @exception EricServerNotConnectedError raised to indicate a missing server 1123 @exception EricServerNotConnectedError raised to indicate a missing server
1104 connection 1124 connection
1105 @exception OSError raised in case the server reported an issue 1125 @exception OSError raised in case the server reported an issue
1106 """ 1126 """
1107 loop = QEventLoop() 1127 loop = QEventLoop()
1127 1147
1128 if not self.__serverInterface.isServerConnected(): 1148 if not self.__serverInterface.isServerConnected():
1129 raise EricServerNotConnectedError 1149 raise EricServerNotConnectedError
1130 1150
1131 else: 1151 else:
1152 if isinstance(data, QByteArray):
1153 data = bytes(data)
1132 self.__serverInterface.sendJson( 1154 self.__serverInterface.sendJson(
1133 category=EricRequestCategory.FileSystem, 1155 category=EricRequestCategory.FileSystem,
1134 request="WriteFile", 1156 request="WriteFile",
1135 params={ 1157 params={
1136 "filename": FileSystemUtilities.plainFileName(filename), 1158 "filename": FileSystemUtilities.plainFileName(filename),
1137 "filedata": str(base64.b85encode(data), encoding="ascii"), 1159 "filedata": str(base64.b85encode(data), encoding="ascii"),
1138 "with_backup": withBackup, 1160 "with_backup": withBackup,
1161 "newline": "<<none>>" if newline is None else newline,
1139 }, 1162 },
1140 callback=callback, 1163 callback=callback,
1141 ) 1164 )
1142 1165
1143 loop.exec() 1166 loop.exec()
1313 }, 1336 },
1314 callback=callback, 1337 callback=callback,
1315 ) 1338 )
1316 1339
1317 loop.exec() 1340 loop.exec()
1341 if ok:
1342 self.removeFromFsCache(pathname)
1343
1318 if not ok: 1344 if not ok:
1319 raise OSError(error) 1345 raise OSError(error)
1320 1346
1321 ####################################################################### 1347 #######################################################################
1322 ## Utility methods. 1348 ## Utility methods.
1362 cpath = f"{remoteMarker}{ellipsis}{tail}" 1388 cpath = f"{remoteMarker}{ellipsis}{tail}"
1363 if measure(cpath) <= width: 1389 if measure(cpath) <= width:
1364 return cpath 1390 return cpath
1365 tail = tail[1:] 1391 tail = tail[1:]
1366 return "" 1392 return ""
1393
1394 #######################################################################
1395 ## Remote file system cache methods.
1396 #######################################################################
1397
1398 # TODO: change cache when file renamed/moved/deleted/...
1399 def populateFsCache(self, directory):
1400 """
1401 Public method to populate the remote file system cache for a given directory.
1402
1403 @param directory remote directory to be cached
1404 @type str
1405 @exception ValueError raised to indicate an empty directory
1406 """
1407 if not directory:
1408 raise ValueError("The directory to be cached must not be empty.")
1409
1410 try:
1411 listing = self.listdir(directory=directory, recursive=True)[2]
1412 for entry in listing:
1413 _RemoteFsCache[
1414 FileSystemUtilities.remoteFileName(entry["path"])
1415 ] = entry
1416 print(f"Remote Cache Size: {len(_RemoteFsCache)} entries")
1417 except OSError as err:
1418 print("error in 'populateFsCache()':", str(err))
1419
1420 def removeFromFsCache(self, directory):
1421 """
1422 Public method to remove a given directory from the remote file system cache.
1423
1424 @param directory remote directory to be removed
1425 @type str
1426 """
1427 for entryPath in list(_RemoteFsCache.keys()):
1428 if entryPath.startswith(directory):
1429 del _RemoteFsCache[entryPath]
1430 print(f"Remote Cache Size: {len(_RemoteFsCache)} entries")

eric ide

mercurial