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. |
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", |
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() |
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") |