Skip to content

Commit 8110d1a

Browse files
committed
feat(Lib/shutil): rmtree
1 parent 2544eea commit 8110d1a

File tree

2 files changed

+244
-1
lines changed

2 files changed

+244
-1
lines changed

src/pylib/Lib/n_shutil.nim

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22

33
import ../private/trans_imp
44
impExp shutil_impl,
5-
terminals, copys
5+
terminals, copys, rmtreeImpl
6+
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import ./sys
2+
import ../os
3+
const use_fd_functions = (
4+
os.supports_dir_fd >= {os.open, os.stat, os.unlink, os.rmdir} and
5+
#(os.open, os.stat, os.unlink, os.rmdir) <= os.supports_dir_fd and
6+
os.scandir in os.supports_fd and
7+
os.stat in os.supports_follow_symlinks)
8+
when not use_fd_functions:
9+
import ../stat
10+
11+
type P = string
12+
type
13+
#Path1Proc = proc (p: string)
14+
Path1ProcKind = enum
15+
os_close
16+
os_rmdir
17+
os_lstat
18+
os_open
19+
os_unlink
20+
os_path_islink
21+
os_scandir
22+
OnExc = proc (
23+
p: Path1ProcKind,
24+
fullpath: P,
25+
exc: ref OSError
26+
)
27+
28+
type Stack[T] = seq[
29+
tuple[fun: Path1ProcKind, dirfd: int, path: T, orig_entry: DirEntry[int]]
30+
]
31+
using onexc: OnExc
32+
template isinstance(e; t): bool = e of t
33+
const
34+
False = false
35+
template pass = discard
36+
template `is`(a, b: Path1ProcKind): bool = a == b
37+
template `is_not`(a, b: Path1ProcKind): bool = a != b
38+
#template `is`(a: DirEntry, b: NoneType): bool = a.isNone
39+
when use_fd_functions:
40+
import ../../pysugar/pywith
41+
import std/sequtils
42+
type List[T] = seq[T]
43+
template append[T](s: seq[T]; e: T) = s.add e
44+
template list(s): untyped = toSeq(s)
45+
const
46+
None = nil
47+
proc rmtree_safe_fd_step(stack: var Stack[P]; onexc) =
48+
# Each stack item has four elements:
49+
# * func: The first operation to perform: os.lstat, os.close or os.rmdir.
50+
# Walking a directory starts with an os.lstat() to detect symlinks; in
51+
# this case, func is updated before subsequent operations and passed to
52+
# onexc() if an error occurs.
53+
# * dirfd: Open file descriptor, or None if we're processing the top-level
54+
# directory given to rmtree() and the user didn't supply dir_fd.
55+
# * path: Path of file to operate upon. This is passed to onexc() if an
56+
# error occurs.
57+
# * orig_entry: os.DirEntry, or None if we're processing the top-level
58+
# directory given to rmtree(). We used the cached stat() of the entry to
59+
# save a call to os.lstat() when walking subdirectories.
60+
var
61+
entries: List[DirEntry[int]]
62+
orig_st: stat_result
63+
topfd: int
64+
name: P
65+
fullname: P
66+
(fun, dirfd, path, orig_entry) = stack.pop()
67+
68+
name = if orig_entry is None: path else: orig_entry.name
69+
try:
70+
if fun is os_close:
71+
os.close(dirfd)
72+
return
73+
if fun is os_rmdir:
74+
os.rmdir(name, dir_fd=dirfd)
75+
return
76+
77+
# Note: To guard against symlink races, we use the standard
78+
# lstat()/open()/fstat() trick.
79+
assert fun is os_lstat
80+
if orig_entry is None:
81+
orig_st = os.lstat(name, dir_fd=dirfd)
82+
else:
83+
orig_st = orig_entry.stat(follow_symlinks=False)
84+
85+
fun = os_open # For error reporting.
86+
topfd = os.open(name, os.O_RDONLY | os.O_NONBLOCK, dir_fd=dirfd)
87+
88+
fun = os_path_islink # For error reporting.
89+
try:
90+
if not os.path.samestat(orig_st, os.fstat(topfd)):
91+
# Symlinks to directories are forbidden, see GH-46010.
92+
raise newPyOSError("Cannot call rmtree on a symbolic link")
93+
stack.append((os_rmdir, dirfd, path, orig_entry))
94+
finally:
95+
stack.append((os_close, topfd, path, orig_entry))
96+
97+
fun = os_scandir # For error reporting.
98+
with os.scandir(topfd) as scandir_it:
99+
entries = list(scandir_it)
100+
for entry in entries:
101+
fullname = os.path.join(path, entry.name)
102+
try:
103+
if entry.is_dir(follow_symlinks=False):
104+
# Traverse into sub-directory.
105+
stack.append((os_lstat, topfd, fullname, entry))
106+
continue
107+
except FileNotFoundError:
108+
continue
109+
except OSError:
110+
pass
111+
try:
112+
unlink(entry.name, dir_fd=topfd)
113+
except FileNotFoundError:
114+
continue
115+
except OSError as err:
116+
onexc(os_unlink, fullname, err)
117+
except FileNotFoundError as err:
118+
if orig_entry is None or fun is os_close:
119+
err.filename = path
120+
onexc(fun, path, err)
121+
except OSError as err:
122+
if isinstance(err, PyOSError):
123+
let e = (ref PyOSError)(err)
124+
e.filename = path
125+
onexc(fun, path, err)
126+
127+
# _rmtree_safe_fd
128+
proc rmtreeImpl[P](path: P, dir_fd: int, onexc) =
129+
## Version using fd-based APIs to protect against races
130+
# While the unsafe rmtree works fine on bytes, the fd based does not.
131+
#if isinstance(path, bytes):
132+
when P is_not string:
133+
let path = os.fsdecode(path)
134+
var stack: Stack[P] = @[(os_lstat, dir_fd, path, DirEntry[int](None))]
135+
try:
136+
while len(stack) != 0:
137+
rmtree_safe_fd_step(stack, onexc)
138+
finally:
139+
# Close any file descriptors still on the stack.
140+
while len(stack) != 0:
141+
let (fun, fd, path, _) = stack.pop()
142+
if fun is_not os_close:
143+
continue
144+
try:
145+
os.close(fd)
146+
except OSError as err:
147+
onexc(os_close, path, err)
148+
else:
149+
when defined(windows): # hasattr(os.stat_result, 'st_file_attributes'):
150+
proc rmtree_islink(st: stat_result): bool =
151+
return (stat.S_ISLNK(st.st_mode) or
152+
(bool(st.st_file_attributes and stat.FILE_ATTRIBUTE_REPARSE_POINT) and
153+
st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT))
154+
else:
155+
proc rmtree_islink(st: stat_result): bool =
156+
return stat.S_ISLNK(st.st_mode)
157+
158+
# _rmtree_unsafe
159+
proc rmtreeImpl[P](path: P, dir_fd: int, onexc) =
160+
when dir_fd is_not NoneType:
161+
static:
162+
raise newException(NotImplementedError, "dir_fd unavailable on this platform")
163+
var st: stat_result
164+
try:
165+
st = os.lstat(path)
166+
except OSError as err:
167+
onexc(os_lstat, path, err)
168+
return
169+
try:
170+
if rmtree_islink(st):
171+
# symlinks to directories are forbidden, see bug #1669
172+
raise newPyOSError("Cannot call rmtree on a symbolic link")
173+
except OSError as err:
174+
onexc(os_path_islink, path, err)
175+
# can't continue even if onexc hook returns
176+
return
177+
proc onerror(err) =
178+
if not isinstance(err, FileNotFoundError):
179+
onexc(os_scandir, err.filename, err)
180+
results = os.walk(path, topdown=False, onerror=onerror,
181+
followlinks=os.walk_symlinks_as_files)
182+
for (dirpath, dirnames, filenames) in results:
183+
for name in dirnames:
184+
fullname = os.path.join(dirpath, name)
185+
try:
186+
os.rmdir(fullname)
187+
except FileNotFoundError:
188+
continue
189+
except OSError as err:
190+
onexc(os.rmdir, fullname, err)
191+
for name in filenames:
192+
fullname = os.path.join(dirpath, name)
193+
try:
194+
os.unlink(fullname)
195+
except FileNotFoundError:
196+
continue
197+
except OSError as err:
198+
onexc(os.unlink, fullname, err)
199+
try:
200+
os.rmdir(path)
201+
except FileNotFoundError:
202+
pass
203+
except OSError as err:
204+
onexc(os_rmdir, path, err)
205+
206+
207+
proc rmtree*(path: string, ignore_errors=false;
208+
onerror: OnExc=nil; # deprecated
209+
onexc: OnExc = nil,
210+
dir_fd = -1
211+
) =
212+
sys.audit("shutil.rmtree", path, dir_fd)
213+
template defonexc(body){.dirty.} =
214+
onexc = proc (
215+
fun: Path1ProcKind,
216+
path: P,
217+
exc: ref OSError
218+
) = body
219+
var onexc = onexc
220+
if ignore_errors:
221+
def_onexc: pass
222+
223+
elif onerror is None and onexc is None:
224+
def_onexc:
225+
raise
226+
elif onexc is None:
227+
if onerror is None:
228+
def_onexc:
229+
raise
230+
else:
231+
# delegate to onerror
232+
def_onexc:
233+
#[
234+
func, path, exc = args
235+
if exc is None:
236+
exc_info = None, None, None
237+
else:
238+
exc_info = type(exc), exc, exc.__traceback__
239+
]#
240+
let exc_info = exc
241+
onerror(fun, path, exc_info)
242+
rmtreeImpl(path, dir_fd, onexc)

0 commit comments

Comments
 (0)