path_ops.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. """Path operations."""
  2. from __future__ import annotations
  3. import json
  4. import os
  5. import re
  6. import shutil
  7. import stat
  8. from pathlib import Path
  9. from reflex import constants
  10. from reflex.config import environment, get_config
  11. # Shorthand for join.
  12. join = os.linesep.join
  13. def chmod_rm(path: Path):
  14. """Remove a file or directory with chmod.
  15. Args:
  16. path: The path to the file or directory.
  17. """
  18. path.chmod(stat.S_IWRITE)
  19. if path.is_dir():
  20. shutil.rmtree(path)
  21. elif path.is_file():
  22. path.unlink()
  23. def rm(path: str | Path):
  24. """Remove a file or directory.
  25. Args:
  26. path: The path to the file or directory.
  27. """
  28. path = Path(path)
  29. if path.is_dir():
  30. # In Python 3.12, onerror is deprecated in favor of onexc
  31. shutil.rmtree(path, onerror=lambda _func, _path, _info: chmod_rm(path))
  32. elif path.is_file():
  33. path.unlink()
  34. def cp(src: str | Path, dest: str | Path, overwrite: bool = True) -> bool:
  35. """Copy a file or directory.
  36. Args:
  37. src: The path to the file or directory.
  38. dest: The path to the destination.
  39. overwrite: Whether to overwrite the destination.
  40. Returns:
  41. Whether the copy was successful.
  42. """
  43. src, dest = Path(src), Path(dest)
  44. if src == dest:
  45. return False
  46. if not overwrite and dest.exists():
  47. return False
  48. if src.is_dir():
  49. rm(dest)
  50. shutil.copytree(src, dest)
  51. else:
  52. shutil.copyfile(src, dest)
  53. return True
  54. def mv(src: str | Path, dest: str | Path, overwrite: bool = True) -> bool:
  55. """Move a file or directory.
  56. Args:
  57. src: The path to the file or directory.
  58. dest: The path to the destination.
  59. overwrite: Whether to overwrite the destination.
  60. Returns:
  61. Whether the move was successful.
  62. """
  63. src, dest = Path(src), Path(dest)
  64. if src == dest:
  65. return False
  66. if not overwrite and dest.exists():
  67. return False
  68. rm(dest)
  69. shutil.move(src, dest)
  70. return True
  71. def mkdir(path: str | Path):
  72. """Create a directory.
  73. Args:
  74. path: The path to the directory.
  75. """
  76. Path(path).mkdir(parents=True, exist_ok=True)
  77. def ls(path: str | Path) -> list[Path]:
  78. """List the contents of a directory.
  79. Args:
  80. path: The path to the directory.
  81. Returns:
  82. A list of paths to the contents of the directory.
  83. """
  84. return list(Path(path).iterdir())
  85. def ln(src: str | Path, dest: str | Path, overwrite: bool = False) -> bool:
  86. """Create a symbolic link.
  87. Args:
  88. src: The path to the file or directory.
  89. dest: The path to the destination.
  90. overwrite: Whether to overwrite the destination.
  91. Returns:
  92. Whether the link was successful.
  93. """
  94. src, dest = Path(src), Path(dest)
  95. if src == dest:
  96. return False
  97. if not overwrite and (dest.exists() or dest.is_symlink()):
  98. return False
  99. if src.is_dir():
  100. rm(dest)
  101. src.symlink_to(dest, target_is_directory=True)
  102. else:
  103. src.symlink_to(dest)
  104. return True
  105. def which(program: str | Path) -> Path | None:
  106. """Find the path to an executable.
  107. Args:
  108. program: The name of the executable.
  109. Returns:
  110. The path to the executable.
  111. """
  112. which_result = shutil.which(program)
  113. return Path(which_result) if which_result else None
  114. def use_system_node() -> bool:
  115. """Check if the system node should be used.
  116. Returns:
  117. Whether the system node should be used.
  118. """
  119. return environment.REFLEX_USE_SYSTEM_NODE.get()
  120. def use_system_bun() -> bool:
  121. """Check if the system bun should be used.
  122. Returns:
  123. Whether the system bun should be used.
  124. """
  125. return environment.REFLEX_USE_SYSTEM_BUN.get()
  126. def get_node_bin_path() -> Path | None:
  127. """Get the node binary dir path.
  128. Returns:
  129. The path to the node bin folder.
  130. """
  131. bin_path = Path(constants.Node.BIN_PATH)
  132. if not bin_path.exists():
  133. path = which("node")
  134. return path.parent.absolute() if path else None
  135. return bin_path.absolute()
  136. def get_node_path() -> Path | None:
  137. """Get the node binary path.
  138. Returns:
  139. The path to the node binary file.
  140. """
  141. node_path = Path(constants.Node.PATH)
  142. if use_system_node() or not node_path.exists():
  143. node_path = which("node")
  144. return node_path
  145. def get_npm_path() -> Path | None:
  146. """Get npm binary path.
  147. Returns:
  148. The path to the npm binary file.
  149. """
  150. npm_path = Path(constants.Node.NPM_PATH)
  151. if use_system_node() or not npm_path.exists():
  152. npm_path = which("npm")
  153. return npm_path.absolute() if npm_path else None
  154. def get_bun_path() -> Path | None:
  155. """Get bun binary path.
  156. Returns:
  157. The path to the bun binary file.
  158. """
  159. bun_path = get_config().bun_path
  160. if use_system_bun() or not bun_path.exists():
  161. bun_path = which("bun")
  162. return bun_path.absolute() if bun_path else None
  163. def update_json_file(file_path: str | Path, update_dict: dict[str, int | str]):
  164. """Update the contents of a json file.
  165. Args:
  166. file_path: the path to the JSON file.
  167. update_dict: object to update json.
  168. """
  169. fp = Path(file_path)
  170. # Create the parent directory if it doesn't exist.
  171. fp.parent.mkdir(parents=True, exist_ok=True)
  172. # Create the file if it doesn't exist.
  173. fp.touch(exist_ok=True)
  174. # Create an empty json object if file is empty
  175. fp.write_text("{}") if fp.stat().st_size == 0 else None
  176. # Read the existing json object from the file.
  177. json_object = {}
  178. if fp.stat().st_size:
  179. with fp.open() as f:
  180. json_object = json.load(f)
  181. # Update the json object with the new data.
  182. json_object.update(update_dict)
  183. # Write the updated json object to the file
  184. with fp.open("w") as f:
  185. json.dump(json_object, f, ensure_ascii=False)
  186. def find_replace(directory: str | Path, find: str, replace: str):
  187. """Recursively find and replace text in files in a directory.
  188. Args:
  189. directory: The directory to search.
  190. find: The text to find.
  191. replace: The text to replace.
  192. """
  193. directory = Path(directory)
  194. for root, _dirs, files in os.walk(directory):
  195. for file in files:
  196. filepath = Path(root, file)
  197. text = filepath.read_text(encoding="utf-8")
  198. text = re.sub(find, replace, text)
  199. filepath.write_text(text, encoding="utf-8")
  200. def samefile(file1: Path, file2: Path) -> bool:
  201. """Check if two files are the same.
  202. Args:
  203. file1: The first file.
  204. file2: The second file.
  205. Returns:
  206. Whether the files are the same. If either file does not exist, returns False.
  207. """
  208. if file1.exists() and file2.exists():
  209. return file1.samefile(file2)
  210. return False
  211. def update_directory_tree(src: Path, dest: Path):
  212. """Recursively copies a directory tree from src to dest.
  213. Only copies files if the destination file is missing or modified earlier than the source file.
  214. Args:
  215. src: Source directory
  216. dest: Destination directory
  217. Raises:
  218. ValueError: If the source is not a directory
  219. """
  220. if not src.is_dir():
  221. raise ValueError(f"Source {src} is not a directory")
  222. # Ensure the destination directory exists
  223. dest.mkdir(parents=True, exist_ok=True)
  224. for item in src.iterdir():
  225. dest_item = dest / item.name
  226. if item.is_dir():
  227. # Recursively copy subdirectories
  228. update_directory_tree(item, dest_item)
  229. elif item.is_file() and (
  230. not dest_item.exists() or item.stat().st_mtime > dest_item.stat().st_mtime
  231. ):
  232. # Copy file if it doesn't exist in the destination or is older than the source
  233. shutil.copy2(item, dest_item)