delayed-rm 2.4.1__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/PKG-INFO +3 -2
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/README.md +1 -1
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/delayed_rm/delayed_rm.py +71 -47
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/delayed_rm.egg-info/PKG-INFO +4 -3
- delayed_rm-2.7.0/pyproject.toml +47 -0
- delayed_rm-2.4.1/pyproject.toml +0 -3
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/LICENSE +0 -0
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/delayed_rm/__init__.py +0 -0
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/delayed_rm/py.typed +0 -0
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/delayed_rm.egg-info/SOURCES.txt +0 -0
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/delayed_rm.egg-info/dependency_links.txt +0 -0
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/delayed_rm.egg-info/entry_points.txt +0 -0
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/delayed_rm.egg-info/top_level.txt +0 -0
- {delayed_rm-2.4.1 → delayed_rm-2.7.0}/setup.cfg +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: delayed_rm
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Ever wish you had a few minutes to undo an rm? Now you do!
|
|
5
5
|
Home-page: https://github.com/zwimer/delayed_rm
|
|
6
6
|
License: GPL
|
|
7
|
+
Project-URL: Homepage, https://github.com/zwimer/delayed_rm
|
|
7
8
|
Classifier: Programming Language :: Python :: 3
|
|
8
9
|
Classifier: Programming Language :: Python :: 3.8
|
|
9
10
|
Requires-Python: >=3.8
|
|
@@ -11,7 +12,7 @@ Description-Content-Type: text/markdown
|
|
|
11
12
|
License-File: LICENSE
|
|
12
13
|
|
|
13
14
|
# delayed\_rm
|
|
14
|
-
Ever wish you had a few minutes to undo an rm? Now you do!
|
|
15
|
+
Ever wish you had a few minutes to undo an rm? Now you do!
|
|
15
16
|
|
|
16
17
|
# Usage
|
|
17
18
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from collections import defaultdict
|
|
3
|
-
from typing import Callable, Set
|
|
4
3
|
from tempfile import gettempdir
|
|
5
4
|
from datetime import datetime
|
|
5
|
+
from typing import Callable
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
import subprocess
|
|
8
8
|
import argparse
|
|
@@ -13,7 +13,7 @@ import sys
|
|
|
13
13
|
import os
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
__version__ = "2.
|
|
16
|
+
__version__ = "2.7.0"
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
#
|
|
@@ -42,6 +42,7 @@ class _Secret:
|
|
|
42
42
|
Contains a string needed to activate the secret CLI
|
|
43
43
|
Users should *not* use this, it is an internal class!
|
|
44
44
|
"""
|
|
45
|
+
|
|
45
46
|
key: str = "DELAYED_RM_SECRET_CLI"
|
|
46
47
|
value: str = "cL5r0!L4hmWmonW7k^RZM*4nq7mR&yfF"
|
|
47
48
|
|
|
@@ -58,8 +59,11 @@ def _eprint(e: str | BaseException) -> None:
|
|
|
58
59
|
e2: str | BaseException = e
|
|
59
60
|
if isinstance(e, RMError) and isinstance(e.__cause__, BaseException):
|
|
60
61
|
e2 = e.__cause__
|
|
61
|
-
err: str =
|
|
62
|
-
|
|
62
|
+
err: str = (
|
|
63
|
+
"Error"
|
|
64
|
+
if isinstance(e2, RMError) or not isinstance(e, BaseException)
|
|
65
|
+
else str(type(e2)).split("'")[1].split("delayed_rm.")[-1]
|
|
66
|
+
)
|
|
63
67
|
print(f"{err}: {e}", file=sys.stderr)
|
|
64
68
|
|
|
65
69
|
|
|
@@ -79,7 +83,7 @@ def _prep(paths: list[Path], rf: bool) -> list[Path]:
|
|
|
79
83
|
"""
|
|
80
84
|
# Normalize paths and error checking
|
|
81
85
|
try:
|
|
82
|
-
paths = [
|
|
86
|
+
paths = [i.parent.resolve(strict=True) / i.name for i in paths]
|
|
83
87
|
# pathlib.stat does not support follow_symlinks until 3.10
|
|
84
88
|
if len(paths) != len({os.stat(i, follow_symlinks=False).st_ino for i in paths}):
|
|
85
89
|
raise RMError("duplicate items passed")
|
|
@@ -120,64 +124,82 @@ def delayed_rm(paths: list[Path], delay: int, rf: bool) -> bool:
|
|
|
120
124
|
# Init data structures
|
|
121
125
|
success: list[Path] = []
|
|
122
126
|
failed: list[Path] = []
|
|
123
|
-
out_dirs:
|
|
124
|
-
where: dict[str,
|
|
127
|
+
out_dirs: set[Path] = {_mkdir(base / "0")}
|
|
128
|
+
where: dict[str, set[Path]] = defaultdict(set)
|
|
125
129
|
full_where: dict[Path, Path] = {}
|
|
126
130
|
# Delete files
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
ctrlc = False
|
|
132
|
+
edited = False
|
|
133
|
+
try:
|
|
134
|
+
for p in paths:
|
|
135
|
+
# Select an output directory that an item of name p.name does not exist
|
|
136
|
+
outd: Path
|
|
137
|
+
if len(out_dirs) != len(where[p.name]):
|
|
138
|
+
outd = next(iter(out_dirs - where[p.name]))
|
|
139
|
+
else:
|
|
140
|
+
outd = _mkdir(base / str(len(out_dirs)))
|
|
141
|
+
out_dirs.add(outd)
|
|
142
|
+
# Move file into the temp directory
|
|
138
143
|
try:
|
|
139
|
-
p.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
shutil.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
144
|
+
new: Path = outd / p.name
|
|
145
|
+
try:
|
|
146
|
+
p.rename(new)
|
|
147
|
+
except OSError:
|
|
148
|
+
copyf = lambda src, dst: shutil.copy2(src, dst, follow_symlinks=False)
|
|
149
|
+
if p.is_dir():
|
|
150
|
+
shutil.copytree(p, new, copy_function=copyf, symlinks=False)
|
|
151
|
+
edited = True
|
|
152
|
+
shutil.rmtree(p)
|
|
153
|
+
else:
|
|
154
|
+
copyf(p, new)
|
|
155
|
+
edited = True
|
|
156
|
+
p.unlink()
|
|
157
|
+
success.append(p)
|
|
158
|
+
full_where[p] = new
|
|
159
|
+
where[p.name].add(outd)
|
|
160
|
+
except OSError as e:
|
|
161
|
+
failed.append(p)
|
|
162
|
+
_eprint(e)
|
|
163
|
+
except KeyboardInterrupt:
|
|
164
|
+
ctrlc = True
|
|
154
165
|
# Inform user of failures
|
|
155
166
|
failed_plus: list[str] = [str(i) for i in failed]
|
|
156
|
-
if len(failed) > 0:
|
|
167
|
+
if len(failed) > 0 and not ctrlc:
|
|
157
168
|
_eprint("failed to rm:\n " + "\n ".join(failed_plus))
|
|
158
169
|
# Log result
|
|
159
170
|
success_plus: list[str] = [f"{i} ---> {full_where[i]}" for i in success]
|
|
160
171
|
fmt: Callable[[list[str]], str] = lambda l: ("\n " + "\n ".join(l)) if l else " None"
|
|
161
|
-
msg: str =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
msg: str = (
|
|
173
|
+
str(datetime.now())
|
|
174
|
+
+ "\n "
|
|
175
|
+
+ "\n".join(
|
|
176
|
+
(
|
|
177
|
+
("Interrupted by: SIGINT\n" if ctrlc else "") + f"Delay: {delay}",
|
|
178
|
+
f"rf: {rf}",
|
|
179
|
+
f"Storage Directory: {base}",
|
|
180
|
+
f"Succeeded:{fmt(success_plus)}",
|
|
181
|
+
f"Failed:{fmt(failed_plus)}",
|
|
182
|
+
)
|
|
183
|
+
).replace("\n", "\n ")
|
|
184
|
+
+ "\n\n"
|
|
185
|
+
)
|
|
186
|
+
try:
|
|
187
|
+
with log_f.open("a") as f:
|
|
188
|
+
f.write(msg)
|
|
189
|
+
except OSError:
|
|
190
|
+
print(msg)
|
|
191
|
+
raise
|
|
170
192
|
# Delay rm and die
|
|
171
|
-
if not
|
|
193
|
+
if not edited:
|
|
172
194
|
shutil.rmtree(base)
|
|
173
195
|
else:
|
|
174
196
|
subprocess.Popen( # pylint: disable=consider-using-with
|
|
175
197
|
(sys.executable, __file__, _Secret.value, str(delay), base),
|
|
176
|
-
env
|
|
198
|
+
env={_Secret.key: _Secret.value},
|
|
177
199
|
stdout=subprocess.DEVNULL,
|
|
178
200
|
stderr=subprocess.DEVNULL,
|
|
179
201
|
)
|
|
180
|
-
return not failed
|
|
202
|
+
return not failed and not ctrlc
|
|
181
203
|
|
|
182
204
|
|
|
183
205
|
def delayed_rm_raw(delay: int, log: bool, r: bool, f: bool, paths: list[Path]) -> bool:
|
|
@@ -216,7 +238,9 @@ def main(prog: str, *args: str) -> bool:
|
|
|
216
238
|
parser = argparse.ArgumentParser(prog=base)
|
|
217
239
|
parser.add_argument("--version", action="version", version=f"{base} {__version__}")
|
|
218
240
|
parser.add_argument("-d", "--delay", type=int, default=900, help="The deletion delay in seconds")
|
|
219
|
-
parser.add_argument(
|
|
241
|
+
parser.add_argument(
|
|
242
|
+
"--log", action="store_true", help=f"Show {base}'s log files; may not be used with other arguments"
|
|
243
|
+
)
|
|
220
244
|
parser.add_argument("-r", action="store_true", help="rm -r; must use -f with this")
|
|
221
245
|
parser.add_argument("-f", action="store_true", help="rm -f; must use -r with this")
|
|
222
246
|
parser.add_argument("paths", type=Path, nargs="*", help="The items to delete")
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
|
-
Name:
|
|
3
|
-
Version: 2.
|
|
2
|
+
Name: delayed_rm
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Ever wish you had a few minutes to undo an rm? Now you do!
|
|
5
5
|
Home-page: https://github.com/zwimer/delayed_rm
|
|
6
6
|
License: GPL
|
|
7
|
+
Project-URL: Homepage, https://github.com/zwimer/delayed_rm
|
|
7
8
|
Classifier: Programming Language :: Python :: 3
|
|
8
9
|
Classifier: Programming Language :: Python :: 3.8
|
|
9
10
|
Requires-Python: >=3.8
|
|
@@ -11,7 +12,7 @@ Description-Content-Type: text/markdown
|
|
|
11
12
|
License-File: LICENSE
|
|
12
13
|
|
|
13
14
|
# delayed\_rm
|
|
14
|
-
Ever wish you had a few minutes to undo an rm? Now you do!
|
|
15
|
+
Ever wish you had a few minutes to undo an rm? Now you do!
|
|
15
16
|
|
|
16
17
|
# Usage
|
|
17
18
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.2"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "delayed_rm"
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Programming Language :: Python :: 3",
|
|
9
|
+
"Programming Language :: Python :: 3.8",
|
|
10
|
+
]
|
|
11
|
+
license = {text = "GPL"}
|
|
12
|
+
description = "Ever wish you had a few minutes to undo an rm? Now you do!"
|
|
13
|
+
urls = {Homepage = "https://github.com/zwimer/delayed_rm"}
|
|
14
|
+
requires-python = ">= 3.8"
|
|
15
|
+
dynamic = ["version"]
|
|
16
|
+
|
|
17
|
+
[project.readme]
|
|
18
|
+
file = "README.md"
|
|
19
|
+
content-type = "text/markdown"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
delayed_rm = "delayed_rm:cli"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
license-files = ["LICENSE"]
|
|
26
|
+
include-package-data = false
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages]
|
|
29
|
+
find = {namespaces = false}
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
delayed_rm = ["py.typed"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.dynamic]
|
|
35
|
+
version = {attr = "delayed_rm.__version__"}
|
|
36
|
+
|
|
37
|
+
# Tools
|
|
38
|
+
|
|
39
|
+
[tool.black]
|
|
40
|
+
line-length = 120
|
|
41
|
+
target-version = ["py38"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
ignore=["E731","E741"]
|
|
45
|
+
line-length = 120
|
|
46
|
+
[tool.ruff.per-file-ignores]
|
|
47
|
+
"__init__.py" = ["F401", "F403"]
|
delayed_rm-2.4.1/pyproject.toml
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|