just-bash 0.1.5__py3-none-any.whl
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.
- just_bash/__init__.py +55 -0
- just_bash/ast/__init__.py +213 -0
- just_bash/ast/factory.py +320 -0
- just_bash/ast/types.py +953 -0
- just_bash/bash.py +220 -0
- just_bash/commands/__init__.py +23 -0
- just_bash/commands/argv/__init__.py +5 -0
- just_bash/commands/argv/argv.py +21 -0
- just_bash/commands/awk/__init__.py +5 -0
- just_bash/commands/awk/awk.py +1168 -0
- just_bash/commands/base64/__init__.py +5 -0
- just_bash/commands/base64/base64.py +138 -0
- just_bash/commands/basename/__init__.py +5 -0
- just_bash/commands/basename/basename.py +72 -0
- just_bash/commands/bash/__init__.py +5 -0
- just_bash/commands/bash/bash.py +188 -0
- just_bash/commands/cat/__init__.py +5 -0
- just_bash/commands/cat/cat.py +173 -0
- just_bash/commands/checksum/__init__.py +5 -0
- just_bash/commands/checksum/checksum.py +179 -0
- just_bash/commands/chmod/__init__.py +5 -0
- just_bash/commands/chmod/chmod.py +216 -0
- just_bash/commands/column/__init__.py +5 -0
- just_bash/commands/column/column.py +180 -0
- just_bash/commands/comm/__init__.py +5 -0
- just_bash/commands/comm/comm.py +150 -0
- just_bash/commands/compression/__init__.py +5 -0
- just_bash/commands/compression/compression.py +298 -0
- just_bash/commands/cp/__init__.py +5 -0
- just_bash/commands/cp/cp.py +149 -0
- just_bash/commands/curl/__init__.py +5 -0
- just_bash/commands/curl/curl.py +801 -0
- just_bash/commands/cut/__init__.py +5 -0
- just_bash/commands/cut/cut.py +327 -0
- just_bash/commands/date/__init__.py +5 -0
- just_bash/commands/date/date.py +258 -0
- just_bash/commands/diff/__init__.py +5 -0
- just_bash/commands/diff/diff.py +118 -0
- just_bash/commands/dirname/__init__.py +5 -0
- just_bash/commands/dirname/dirname.py +56 -0
- just_bash/commands/du/__init__.py +5 -0
- just_bash/commands/du/du.py +150 -0
- just_bash/commands/echo/__init__.py +5 -0
- just_bash/commands/echo/echo.py +125 -0
- just_bash/commands/env/__init__.py +5 -0
- just_bash/commands/env/env.py +163 -0
- just_bash/commands/expand/__init__.py +5 -0
- just_bash/commands/expand/expand.py +299 -0
- just_bash/commands/expr/__init__.py +5 -0
- just_bash/commands/expr/expr.py +273 -0
- just_bash/commands/file/__init__.py +5 -0
- just_bash/commands/file/file.py +274 -0
- just_bash/commands/find/__init__.py +5 -0
- just_bash/commands/find/find.py +623 -0
- just_bash/commands/fold/__init__.py +5 -0
- just_bash/commands/fold/fold.py +160 -0
- just_bash/commands/grep/__init__.py +5 -0
- just_bash/commands/grep/grep.py +418 -0
- just_bash/commands/head/__init__.py +5 -0
- just_bash/commands/head/head.py +167 -0
- just_bash/commands/help/__init__.py +5 -0
- just_bash/commands/help/help.py +67 -0
- just_bash/commands/hostname/__init__.py +5 -0
- just_bash/commands/hostname/hostname.py +21 -0
- just_bash/commands/html_to_markdown/__init__.py +5 -0
- just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
- just_bash/commands/join/__init__.py +5 -0
- just_bash/commands/join/join.py +252 -0
- just_bash/commands/jq/__init__.py +5 -0
- just_bash/commands/jq/jq.py +280 -0
- just_bash/commands/ln/__init__.py +5 -0
- just_bash/commands/ln/ln.py +127 -0
- just_bash/commands/ls/__init__.py +5 -0
- just_bash/commands/ls/ls.py +280 -0
- just_bash/commands/mkdir/__init__.py +5 -0
- just_bash/commands/mkdir/mkdir.py +92 -0
- just_bash/commands/mv/__init__.py +5 -0
- just_bash/commands/mv/mv.py +142 -0
- just_bash/commands/nl/__init__.py +5 -0
- just_bash/commands/nl/nl.py +180 -0
- just_bash/commands/od/__init__.py +5 -0
- just_bash/commands/od/od.py +157 -0
- just_bash/commands/paste/__init__.py +5 -0
- just_bash/commands/paste/paste.py +100 -0
- just_bash/commands/printf/__init__.py +5 -0
- just_bash/commands/printf/printf.py +157 -0
- just_bash/commands/pwd/__init__.py +5 -0
- just_bash/commands/pwd/pwd.py +23 -0
- just_bash/commands/read/__init__.py +5 -0
- just_bash/commands/read/read.py +185 -0
- just_bash/commands/readlink/__init__.py +5 -0
- just_bash/commands/readlink/readlink.py +86 -0
- just_bash/commands/registry.py +844 -0
- just_bash/commands/rev/__init__.py +5 -0
- just_bash/commands/rev/rev.py +74 -0
- just_bash/commands/rg/__init__.py +5 -0
- just_bash/commands/rg/rg.py +1048 -0
- just_bash/commands/rm/__init__.py +5 -0
- just_bash/commands/rm/rm.py +106 -0
- just_bash/commands/search_engine/__init__.py +13 -0
- just_bash/commands/search_engine/matcher.py +170 -0
- just_bash/commands/search_engine/regex.py +159 -0
- just_bash/commands/sed/__init__.py +5 -0
- just_bash/commands/sed/sed.py +863 -0
- just_bash/commands/seq/__init__.py +5 -0
- just_bash/commands/seq/seq.py +190 -0
- just_bash/commands/shell/__init__.py +5 -0
- just_bash/commands/shell/shell.py +206 -0
- just_bash/commands/sleep/__init__.py +5 -0
- just_bash/commands/sleep/sleep.py +62 -0
- just_bash/commands/sort/__init__.py +5 -0
- just_bash/commands/sort/sort.py +411 -0
- just_bash/commands/split/__init__.py +5 -0
- just_bash/commands/split/split.py +237 -0
- just_bash/commands/sqlite3/__init__.py +5 -0
- just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
- just_bash/commands/stat/__init__.py +5 -0
- just_bash/commands/stat/stat.py +150 -0
- just_bash/commands/strings/__init__.py +5 -0
- just_bash/commands/strings/strings.py +150 -0
- just_bash/commands/tac/__init__.py +5 -0
- just_bash/commands/tac/tac.py +158 -0
- just_bash/commands/tail/__init__.py +5 -0
- just_bash/commands/tail/tail.py +180 -0
- just_bash/commands/tar/__init__.py +5 -0
- just_bash/commands/tar/tar.py +1067 -0
- just_bash/commands/tee/__init__.py +5 -0
- just_bash/commands/tee/tee.py +63 -0
- just_bash/commands/timeout/__init__.py +5 -0
- just_bash/commands/timeout/timeout.py +188 -0
- just_bash/commands/touch/__init__.py +5 -0
- just_bash/commands/touch/touch.py +91 -0
- just_bash/commands/tr/__init__.py +5 -0
- just_bash/commands/tr/tr.py +297 -0
- just_bash/commands/tree/__init__.py +5 -0
- just_bash/commands/tree/tree.py +139 -0
- just_bash/commands/true/__init__.py +5 -0
- just_bash/commands/true/true.py +32 -0
- just_bash/commands/uniq/__init__.py +5 -0
- just_bash/commands/uniq/uniq.py +323 -0
- just_bash/commands/wc/__init__.py +5 -0
- just_bash/commands/wc/wc.py +169 -0
- just_bash/commands/which/__init__.py +5 -0
- just_bash/commands/which/which.py +52 -0
- just_bash/commands/xan/__init__.py +5 -0
- just_bash/commands/xan/xan.py +1663 -0
- just_bash/commands/xargs/__init__.py +5 -0
- just_bash/commands/xargs/xargs.py +136 -0
- just_bash/commands/yq/__init__.py +5 -0
- just_bash/commands/yq/yq.py +848 -0
- just_bash/fs/__init__.py +29 -0
- just_bash/fs/in_memory_fs.py +621 -0
- just_bash/fs/mountable_fs.py +504 -0
- just_bash/fs/overlay_fs.py +894 -0
- just_bash/fs/read_write_fs.py +455 -0
- just_bash/interpreter/__init__.py +37 -0
- just_bash/interpreter/builtins/__init__.py +92 -0
- just_bash/interpreter/builtins/alias.py +154 -0
- just_bash/interpreter/builtins/cd.py +76 -0
- just_bash/interpreter/builtins/control.py +127 -0
- just_bash/interpreter/builtins/declare.py +336 -0
- just_bash/interpreter/builtins/export.py +56 -0
- just_bash/interpreter/builtins/let.py +44 -0
- just_bash/interpreter/builtins/local.py +57 -0
- just_bash/interpreter/builtins/mapfile.py +152 -0
- just_bash/interpreter/builtins/misc.py +378 -0
- just_bash/interpreter/builtins/readonly.py +80 -0
- just_bash/interpreter/builtins/set.py +234 -0
- just_bash/interpreter/builtins/shopt.py +201 -0
- just_bash/interpreter/builtins/source.py +136 -0
- just_bash/interpreter/builtins/test.py +290 -0
- just_bash/interpreter/builtins/unset.py +53 -0
- just_bash/interpreter/conditionals.py +387 -0
- just_bash/interpreter/control_flow.py +381 -0
- just_bash/interpreter/errors.py +116 -0
- just_bash/interpreter/expansion.py +1156 -0
- just_bash/interpreter/interpreter.py +813 -0
- just_bash/interpreter/types.py +134 -0
- just_bash/network/__init__.py +1 -0
- just_bash/parser/__init__.py +39 -0
- just_bash/parser/lexer.py +948 -0
- just_bash/parser/parser.py +2162 -0
- just_bash/py.typed +0 -0
- just_bash/query_engine/__init__.py +83 -0
- just_bash/query_engine/builtins/__init__.py +1283 -0
- just_bash/query_engine/evaluator.py +578 -0
- just_bash/query_engine/parser.py +525 -0
- just_bash/query_engine/tokenizer.py +329 -0
- just_bash/query_engine/types.py +373 -0
- just_bash/types.py +180 -0
- just_bash-0.1.5.dist-info/METADATA +410 -0
- just_bash-0.1.5.dist-info/RECORD +193 -0
- just_bash-0.1.5.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
"""Tar command implementation."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import tarfile
|
|
5
|
+
import gzip
|
|
6
|
+
import bz2
|
|
7
|
+
import lzma
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from fnmatch import fnmatch
|
|
10
|
+
|
|
11
|
+
from ...types import CommandContext, ExecResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TarCommand:
|
|
15
|
+
"""The tar command - manipulate tape archives."""
|
|
16
|
+
|
|
17
|
+
name = "tar"
|
|
18
|
+
|
|
19
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
20
|
+
"""Execute the tar command."""
|
|
21
|
+
if "--help" in args:
|
|
22
|
+
return ExecResult(
|
|
23
|
+
stdout=(
|
|
24
|
+
"Usage: tar [options] [file...]\n"
|
|
25
|
+
"Create, extract, or list contents of tar archives.\n\n"
|
|
26
|
+
"Options:\n"
|
|
27
|
+
" -c, --create create a new archive\n"
|
|
28
|
+
" -x, --extract extract files from an archive\n"
|
|
29
|
+
" -t, --list list contents of an archive\n"
|
|
30
|
+
" -f FILE use archive file FILE\n"
|
|
31
|
+
" -z, --gzip filter archive through gzip\n"
|
|
32
|
+
" -v, --verbose verbosely list files processed\n"
|
|
33
|
+
" -C DIR change to directory DIR\n"
|
|
34
|
+
" --help display this help\n"
|
|
35
|
+
),
|
|
36
|
+
stderr="",
|
|
37
|
+
exit_code=0,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Parse options
|
|
41
|
+
create = False
|
|
42
|
+
extract = False
|
|
43
|
+
list_mode = False
|
|
44
|
+
append_mode = False
|
|
45
|
+
update_mode = False
|
|
46
|
+
archive_file = ""
|
|
47
|
+
use_gzip = False
|
|
48
|
+
use_bzip2 = False
|
|
49
|
+
use_xz = False
|
|
50
|
+
auto_compress = False
|
|
51
|
+
verbose = False
|
|
52
|
+
directory = ""
|
|
53
|
+
exclude_patterns: list[str] = []
|
|
54
|
+
strip_components = 0
|
|
55
|
+
to_stdout = False
|
|
56
|
+
keep_old_files = False
|
|
57
|
+
no_mtime = False
|
|
58
|
+
preserve_permissions = False
|
|
59
|
+
files_from: str = ""
|
|
60
|
+
exclude_from: str = ""
|
|
61
|
+
files: list[str] = []
|
|
62
|
+
|
|
63
|
+
i = 0
|
|
64
|
+
while i < len(args):
|
|
65
|
+
arg = args[i]
|
|
66
|
+
|
|
67
|
+
# Handle combined short options (e.g., -cvzf)
|
|
68
|
+
if arg.startswith("-") and not arg.startswith("--") and len(arg) > 2:
|
|
69
|
+
j = 1
|
|
70
|
+
while j < len(arg):
|
|
71
|
+
char = arg[j]
|
|
72
|
+
if char == "c":
|
|
73
|
+
create = True
|
|
74
|
+
elif char == "x":
|
|
75
|
+
extract = True
|
|
76
|
+
elif char == "t":
|
|
77
|
+
list_mode = True
|
|
78
|
+
elif char == "z":
|
|
79
|
+
use_gzip = True
|
|
80
|
+
elif char == "j":
|
|
81
|
+
use_bzip2 = True
|
|
82
|
+
elif char == "J":
|
|
83
|
+
use_xz = True
|
|
84
|
+
elif char == "a":
|
|
85
|
+
auto_compress = True
|
|
86
|
+
elif char == "v":
|
|
87
|
+
verbose = True
|
|
88
|
+
elif char == "O":
|
|
89
|
+
to_stdout = True
|
|
90
|
+
elif char == "k":
|
|
91
|
+
keep_old_files = True
|
|
92
|
+
elif char == "m":
|
|
93
|
+
no_mtime = True
|
|
94
|
+
elif char == "p":
|
|
95
|
+
preserve_permissions = True
|
|
96
|
+
elif char == "r":
|
|
97
|
+
append_mode = True
|
|
98
|
+
elif char == "u":
|
|
99
|
+
update_mode = True
|
|
100
|
+
elif char == "f":
|
|
101
|
+
# -f requires a value
|
|
102
|
+
if j < len(arg) - 1:
|
|
103
|
+
archive_file = arg[j + 1:]
|
|
104
|
+
j = len(arg)
|
|
105
|
+
else:
|
|
106
|
+
i += 1
|
|
107
|
+
if i >= len(args):
|
|
108
|
+
return ExecResult(
|
|
109
|
+
stdout="",
|
|
110
|
+
stderr="tar: option requires an argument -- 'f'\n",
|
|
111
|
+
exit_code=2,
|
|
112
|
+
)
|
|
113
|
+
archive_file = args[i]
|
|
114
|
+
elif char == "C":
|
|
115
|
+
if j < len(arg) - 1:
|
|
116
|
+
directory = arg[j + 1:]
|
|
117
|
+
j = len(arg)
|
|
118
|
+
else:
|
|
119
|
+
i += 1
|
|
120
|
+
if i >= len(args):
|
|
121
|
+
return ExecResult(
|
|
122
|
+
stdout="",
|
|
123
|
+
stderr="tar: option requires an argument -- 'C'\n",
|
|
124
|
+
exit_code=2,
|
|
125
|
+
)
|
|
126
|
+
directory = args[i]
|
|
127
|
+
elif char == "T":
|
|
128
|
+
# -T requires a value
|
|
129
|
+
if j < len(arg) - 1:
|
|
130
|
+
files_from = arg[j + 1:]
|
|
131
|
+
j = len(arg)
|
|
132
|
+
else:
|
|
133
|
+
i += 1
|
|
134
|
+
if i >= len(args):
|
|
135
|
+
return ExecResult(
|
|
136
|
+
stdout="",
|
|
137
|
+
stderr="tar: option requires an argument -- 'T'\n",
|
|
138
|
+
exit_code=2,
|
|
139
|
+
)
|
|
140
|
+
files_from = args[i]
|
|
141
|
+
elif char == "X":
|
|
142
|
+
# -X requires a value
|
|
143
|
+
if j < len(arg) - 1:
|
|
144
|
+
exclude_from = arg[j + 1:]
|
|
145
|
+
j = len(arg)
|
|
146
|
+
else:
|
|
147
|
+
i += 1
|
|
148
|
+
if i >= len(args):
|
|
149
|
+
return ExecResult(
|
|
150
|
+
stdout="",
|
|
151
|
+
stderr="tar: option requires an argument -- 'X'\n",
|
|
152
|
+
exit_code=2,
|
|
153
|
+
)
|
|
154
|
+
exclude_from = args[i]
|
|
155
|
+
else:
|
|
156
|
+
return ExecResult(
|
|
157
|
+
stdout="",
|
|
158
|
+
stderr=f"tar: invalid option -- '{char}'\n",
|
|
159
|
+
exit_code=2,
|
|
160
|
+
)
|
|
161
|
+
j += 1
|
|
162
|
+
i += 1
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Long options and single short options
|
|
166
|
+
if arg in ("-c", "--create"):
|
|
167
|
+
create = True
|
|
168
|
+
elif arg in ("-x", "--extract", "--get"):
|
|
169
|
+
extract = True
|
|
170
|
+
elif arg in ("-t", "--list"):
|
|
171
|
+
list_mode = True
|
|
172
|
+
elif arg in ("-z", "--gzip", "--gunzip"):
|
|
173
|
+
use_gzip = True
|
|
174
|
+
elif arg in ("-j", "--bzip2"):
|
|
175
|
+
use_bzip2 = True
|
|
176
|
+
elif arg in ("-J", "--xz"):
|
|
177
|
+
use_xz = True
|
|
178
|
+
elif arg in ("-a", "--auto-compress"):
|
|
179
|
+
auto_compress = True
|
|
180
|
+
elif arg in ("-v", "--verbose"):
|
|
181
|
+
verbose = True
|
|
182
|
+
elif arg in ("-O", "--to-stdout"):
|
|
183
|
+
to_stdout = True
|
|
184
|
+
elif arg in ("-k", "--keep-old-files"):
|
|
185
|
+
keep_old_files = True
|
|
186
|
+
elif arg in ("-m", "--touch"):
|
|
187
|
+
no_mtime = True
|
|
188
|
+
elif arg in ("-p", "--preserve-permissions", "--same-permissions"):
|
|
189
|
+
preserve_permissions = True
|
|
190
|
+
elif arg in ("-r", "--append"):
|
|
191
|
+
append_mode = True
|
|
192
|
+
elif arg in ("-u", "--update"):
|
|
193
|
+
update_mode = True
|
|
194
|
+
elif arg == "-f" or arg == "--file":
|
|
195
|
+
i += 1
|
|
196
|
+
if i >= len(args):
|
|
197
|
+
return ExecResult(
|
|
198
|
+
stdout="",
|
|
199
|
+
stderr="tar: option requires an argument -- 'f'\n",
|
|
200
|
+
exit_code=2,
|
|
201
|
+
)
|
|
202
|
+
archive_file = args[i]
|
|
203
|
+
elif arg.startswith("--file="):
|
|
204
|
+
archive_file = arg[7:]
|
|
205
|
+
elif arg in ("-C", "--directory"):
|
|
206
|
+
i += 1
|
|
207
|
+
if i >= len(args):
|
|
208
|
+
return ExecResult(
|
|
209
|
+
stdout="",
|
|
210
|
+
stderr="tar: option requires an argument -- 'C'\n",
|
|
211
|
+
exit_code=2,
|
|
212
|
+
)
|
|
213
|
+
directory = args[i]
|
|
214
|
+
elif arg.startswith("--directory="):
|
|
215
|
+
directory = arg[12:]
|
|
216
|
+
elif arg.startswith("--exclude="):
|
|
217
|
+
exclude_patterns.append(arg[10:])
|
|
218
|
+
elif arg == "--exclude":
|
|
219
|
+
i += 1
|
|
220
|
+
if i >= len(args):
|
|
221
|
+
return ExecResult(
|
|
222
|
+
stdout="",
|
|
223
|
+
stderr="tar: option requires an argument -- 'exclude'\n",
|
|
224
|
+
exit_code=2,
|
|
225
|
+
)
|
|
226
|
+
exclude_patterns.append(args[i])
|
|
227
|
+
elif arg.startswith("--strip-components="):
|
|
228
|
+
try:
|
|
229
|
+
strip_components = int(arg[19:])
|
|
230
|
+
except ValueError:
|
|
231
|
+
return ExecResult(
|
|
232
|
+
stdout="",
|
|
233
|
+
stderr=f"tar: {arg}: invalid argument\n",
|
|
234
|
+
exit_code=2,
|
|
235
|
+
)
|
|
236
|
+
elif arg == "--strip-components":
|
|
237
|
+
i += 1
|
|
238
|
+
if i >= len(args):
|
|
239
|
+
return ExecResult(
|
|
240
|
+
stdout="",
|
|
241
|
+
stderr="tar: option requires an argument -- 'strip-components'\n",
|
|
242
|
+
exit_code=2,
|
|
243
|
+
)
|
|
244
|
+
try:
|
|
245
|
+
strip_components = int(args[i])
|
|
246
|
+
except ValueError:
|
|
247
|
+
return ExecResult(
|
|
248
|
+
stdout="",
|
|
249
|
+
stderr=f"tar: {args[i]}: invalid argument\n",
|
|
250
|
+
exit_code=2,
|
|
251
|
+
)
|
|
252
|
+
elif arg == "-T" or arg == "--files-from":
|
|
253
|
+
i += 1
|
|
254
|
+
if i >= len(args):
|
|
255
|
+
return ExecResult(
|
|
256
|
+
stdout="",
|
|
257
|
+
stderr="tar: option requires an argument -- 'T'\n",
|
|
258
|
+
exit_code=2,
|
|
259
|
+
)
|
|
260
|
+
files_from = args[i]
|
|
261
|
+
elif arg.startswith("--files-from="):
|
|
262
|
+
files_from = arg[13:]
|
|
263
|
+
elif arg == "-X" or arg == "--exclude-from":
|
|
264
|
+
i += 1
|
|
265
|
+
if i >= len(args):
|
|
266
|
+
return ExecResult(
|
|
267
|
+
stdout="",
|
|
268
|
+
stderr="tar: option requires an argument -- 'X'\n",
|
|
269
|
+
exit_code=2,
|
|
270
|
+
)
|
|
271
|
+
exclude_from = args[i]
|
|
272
|
+
elif arg.startswith("--exclude-from="):
|
|
273
|
+
exclude_from = arg[15:]
|
|
274
|
+
elif arg == "--":
|
|
275
|
+
files.extend(args[i + 1:])
|
|
276
|
+
break
|
|
277
|
+
elif arg.startswith("-"):
|
|
278
|
+
return ExecResult(
|
|
279
|
+
stdout="",
|
|
280
|
+
stderr=f"tar: invalid option -- '{arg}'\n",
|
|
281
|
+
exit_code=2,
|
|
282
|
+
)
|
|
283
|
+
else:
|
|
284
|
+
files.append(arg)
|
|
285
|
+
i += 1
|
|
286
|
+
|
|
287
|
+
# Validate operation mode
|
|
288
|
+
op_count = sum([create, extract, list_mode, append_mode, update_mode])
|
|
289
|
+
if op_count == 0:
|
|
290
|
+
return ExecResult(
|
|
291
|
+
stdout="",
|
|
292
|
+
stderr="tar: You must specify one of -c, -x, -t, -r, or -u\n",
|
|
293
|
+
exit_code=2,
|
|
294
|
+
)
|
|
295
|
+
if op_count > 1:
|
|
296
|
+
return ExecResult(
|
|
297
|
+
stdout="",
|
|
298
|
+
stderr="tar: You may not specify more than one of -c, -x, -t, -r, -u\n",
|
|
299
|
+
exit_code=2,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Determine work directory
|
|
303
|
+
work_dir = ctx.fs.resolve_path(ctx.cwd, directory) if directory else ctx.cwd
|
|
304
|
+
|
|
305
|
+
# Read files-from if specified
|
|
306
|
+
if files_from:
|
|
307
|
+
files_from_path = ctx.fs.resolve_path(ctx.cwd, files_from)
|
|
308
|
+
try:
|
|
309
|
+
content = await ctx.fs.read_file(files_from_path)
|
|
310
|
+
for line in content.strip().split("\n"):
|
|
311
|
+
line = line.strip()
|
|
312
|
+
if line:
|
|
313
|
+
files.append(line)
|
|
314
|
+
except FileNotFoundError:
|
|
315
|
+
return ExecResult(
|
|
316
|
+
stdout="",
|
|
317
|
+
stderr=f"tar: {files_from}: Cannot open: No such file or directory\n",
|
|
318
|
+
exit_code=2,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Read exclude-from if specified
|
|
322
|
+
if exclude_from:
|
|
323
|
+
exclude_from_path = ctx.fs.resolve_path(ctx.cwd, exclude_from)
|
|
324
|
+
try:
|
|
325
|
+
content = await ctx.fs.read_file(exclude_from_path)
|
|
326
|
+
for line in content.strip().split("\n"):
|
|
327
|
+
line = line.strip()
|
|
328
|
+
if line:
|
|
329
|
+
exclude_patterns.append(line)
|
|
330
|
+
except FileNotFoundError:
|
|
331
|
+
return ExecResult(
|
|
332
|
+
stdout="",
|
|
333
|
+
stderr=f"tar: {exclude_from}: Cannot open: No such file or directory\n",
|
|
334
|
+
exit_code=2,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if create:
|
|
338
|
+
return await self._create_archive(
|
|
339
|
+
ctx, archive_file, files, work_dir,
|
|
340
|
+
use_gzip=use_gzip, use_bzip2=use_bzip2, use_xz=use_xz,
|
|
341
|
+
auto_compress=auto_compress, verbose=verbose,
|
|
342
|
+
exclude_patterns=exclude_patterns
|
|
343
|
+
)
|
|
344
|
+
elif append_mode:
|
|
345
|
+
return await self._append_archive(
|
|
346
|
+
ctx, archive_file, files, work_dir,
|
|
347
|
+
verbose=verbose, exclude_patterns=exclude_patterns
|
|
348
|
+
)
|
|
349
|
+
elif update_mode:
|
|
350
|
+
return await self._update_archive(
|
|
351
|
+
ctx, archive_file, files, work_dir,
|
|
352
|
+
verbose=verbose, exclude_patterns=exclude_patterns
|
|
353
|
+
)
|
|
354
|
+
elif extract:
|
|
355
|
+
return await self._extract_archive(
|
|
356
|
+
ctx, archive_file, files, work_dir,
|
|
357
|
+
use_gzip=use_gzip, use_bzip2=use_bzip2, use_xz=use_xz,
|
|
358
|
+
verbose=verbose, strip_components=strip_components,
|
|
359
|
+
to_stdout=to_stdout, keep_old_files=keep_old_files,
|
|
360
|
+
no_mtime=no_mtime, preserve_permissions=preserve_permissions
|
|
361
|
+
)
|
|
362
|
+
else: # list_mode
|
|
363
|
+
return await self._list_archive(
|
|
364
|
+
ctx, archive_file, files,
|
|
365
|
+
use_gzip=use_gzip, use_bzip2=use_bzip2, use_xz=use_xz,
|
|
366
|
+
verbose=verbose
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def _detect_compression_from_filename(self, filename: str) -> str | None:
|
|
370
|
+
"""Detect compression type from filename extension."""
|
|
371
|
+
if filename.endswith(".tar.gz") or filename.endswith(".tgz"):
|
|
372
|
+
return "gz"
|
|
373
|
+
elif filename.endswith(".tar.bz2") or filename.endswith(".tbz2"):
|
|
374
|
+
return "bz2"
|
|
375
|
+
elif filename.endswith(".tar.xz") or filename.endswith(".txz"):
|
|
376
|
+
return "xz"
|
|
377
|
+
elif filename.endswith(".tar"):
|
|
378
|
+
return None
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
async def _create_archive(
|
|
382
|
+
self,
|
|
383
|
+
ctx: CommandContext,
|
|
384
|
+
archive_file: str,
|
|
385
|
+
files: list[str],
|
|
386
|
+
work_dir: str,
|
|
387
|
+
use_gzip: bool = False,
|
|
388
|
+
use_bzip2: bool = False,
|
|
389
|
+
use_xz: bool = False,
|
|
390
|
+
auto_compress: bool = False,
|
|
391
|
+
verbose: bool = False,
|
|
392
|
+
exclude_patterns: list[str] | None = None,
|
|
393
|
+
) -> ExecResult:
|
|
394
|
+
"""Create a tar archive."""
|
|
395
|
+
if exclude_patterns is None:
|
|
396
|
+
exclude_patterns = []
|
|
397
|
+
|
|
398
|
+
if not files:
|
|
399
|
+
return ExecResult(
|
|
400
|
+
stdout="",
|
|
401
|
+
stderr="tar: Cowardly refusing to create an empty archive\n",
|
|
402
|
+
exit_code=2,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Handle auto-compress: detect compression from filename
|
|
406
|
+
if auto_compress and archive_file and archive_file != "-":
|
|
407
|
+
detected = self._detect_compression_from_filename(archive_file)
|
|
408
|
+
if detected == "gz":
|
|
409
|
+
use_gzip = True
|
|
410
|
+
elif detected == "bz2":
|
|
411
|
+
use_bzip2 = True
|
|
412
|
+
elif detected == "xz":
|
|
413
|
+
use_xz = True
|
|
414
|
+
|
|
415
|
+
# Create archive in memory - first as uncompressed tar
|
|
416
|
+
buffer = io.BytesIO()
|
|
417
|
+
# For bz2 and xz, we create uncompressed tar first then compress
|
|
418
|
+
if use_bzip2 or use_xz:
|
|
419
|
+
mode = "w"
|
|
420
|
+
elif use_gzip:
|
|
421
|
+
mode = "w:gz"
|
|
422
|
+
else:
|
|
423
|
+
mode = "w"
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
tar = tarfile.open(fileobj=buffer, mode=mode)
|
|
427
|
+
except Exception as e:
|
|
428
|
+
return ExecResult(
|
|
429
|
+
stdout="",
|
|
430
|
+
stderr=f"tar: error opening archive: {e}\n",
|
|
431
|
+
exit_code=2,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
verbose_output = ""
|
|
435
|
+
errors: list[str] = []
|
|
436
|
+
|
|
437
|
+
for file_path in files:
|
|
438
|
+
try:
|
|
439
|
+
added = await self._add_to_archive(
|
|
440
|
+
ctx, tar, work_dir, file_path, verbose, errors, exclude_patterns
|
|
441
|
+
)
|
|
442
|
+
if verbose:
|
|
443
|
+
verbose_output += added
|
|
444
|
+
except Exception as e:
|
|
445
|
+
errors.append(f"tar: {file_path}: {e}")
|
|
446
|
+
|
|
447
|
+
tar.close()
|
|
448
|
+
|
|
449
|
+
# Write archive to file or stdout
|
|
450
|
+
archive_data = buffer.getvalue()
|
|
451
|
+
|
|
452
|
+
# Apply bz2 or xz compression if needed
|
|
453
|
+
if use_bzip2:
|
|
454
|
+
archive_data = bz2.compress(archive_data)
|
|
455
|
+
elif use_xz:
|
|
456
|
+
archive_data = lzma.compress(archive_data)
|
|
457
|
+
|
|
458
|
+
if archive_file and archive_file != "-":
|
|
459
|
+
archive_path = ctx.fs.resolve_path(ctx.cwd, archive_file)
|
|
460
|
+
try:
|
|
461
|
+
await ctx.fs.write_file(archive_path, archive_data)
|
|
462
|
+
except Exception as e:
|
|
463
|
+
return ExecResult(
|
|
464
|
+
stdout="",
|
|
465
|
+
stderr=f"tar: {archive_file}: {e}\n",
|
|
466
|
+
exit_code=2,
|
|
467
|
+
)
|
|
468
|
+
stdout = ""
|
|
469
|
+
else:
|
|
470
|
+
# Output binary to stdout
|
|
471
|
+
stdout = archive_data.decode("latin-1")
|
|
472
|
+
|
|
473
|
+
# Verbose output always goes to stderr (matching real tar behavior)
|
|
474
|
+
stderr = verbose_output
|
|
475
|
+
if errors:
|
|
476
|
+
stderr = "\n".join(errors) + "\n"
|
|
477
|
+
return ExecResult(
|
|
478
|
+
stdout=stdout,
|
|
479
|
+
stderr=stderr,
|
|
480
|
+
exit_code=2 if errors else 0,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
async def _add_to_archive(
|
|
484
|
+
self,
|
|
485
|
+
ctx: CommandContext,
|
|
486
|
+
tar: tarfile.TarFile,
|
|
487
|
+
base_path: str,
|
|
488
|
+
relative_path: str,
|
|
489
|
+
verbose: bool,
|
|
490
|
+
errors: list[str],
|
|
491
|
+
exclude_patterns: list[str],
|
|
492
|
+
) -> str:
|
|
493
|
+
"""Add a file or directory to the archive. Returns verbose output."""
|
|
494
|
+
full_path = ctx.fs.resolve_path(base_path, relative_path)
|
|
495
|
+
verbose_output = ""
|
|
496
|
+
|
|
497
|
+
# Check exclusion patterns
|
|
498
|
+
for pattern in exclude_patterns:
|
|
499
|
+
if fnmatch(relative_path, pattern) or fnmatch(relative_path.split("/")[-1], pattern):
|
|
500
|
+
return ""
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
stat = await ctx.fs.stat(full_path)
|
|
504
|
+
except FileNotFoundError:
|
|
505
|
+
errors.append(f"tar: {relative_path}: No such file or directory")
|
|
506
|
+
return ""
|
|
507
|
+
|
|
508
|
+
# Get mtime - handle both float and datetime
|
|
509
|
+
mtime = stat.mtime
|
|
510
|
+
if hasattr(mtime, 'timestamp'):
|
|
511
|
+
mtime = int(mtime.timestamp())
|
|
512
|
+
elif isinstance(mtime, (int, float)):
|
|
513
|
+
mtime = int(mtime)
|
|
514
|
+
else:
|
|
515
|
+
mtime = 0
|
|
516
|
+
|
|
517
|
+
if stat.is_directory:
|
|
518
|
+
# Add directory entry
|
|
519
|
+
info = tarfile.TarInfo(name=relative_path)
|
|
520
|
+
info.type = tarfile.DIRTYPE
|
|
521
|
+
info.mode = stat.mode
|
|
522
|
+
info.mtime = mtime
|
|
523
|
+
tar.addfile(info)
|
|
524
|
+
if verbose:
|
|
525
|
+
verbose_output += f"{relative_path}\n"
|
|
526
|
+
|
|
527
|
+
# Add contents recursively
|
|
528
|
+
items = await ctx.fs.readdir(full_path)
|
|
529
|
+
for item in items:
|
|
530
|
+
child_path = f"{relative_path}/{item}" if relative_path else item
|
|
531
|
+
verbose_output += await self._add_to_archive(
|
|
532
|
+
ctx, tar, base_path, child_path, verbose, errors, exclude_patterns
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
elif stat.is_file:
|
|
536
|
+
content = await ctx.fs.read_file_bytes(full_path)
|
|
537
|
+
info = tarfile.TarInfo(name=relative_path)
|
|
538
|
+
info.size = len(content)
|
|
539
|
+
info.mode = stat.mode
|
|
540
|
+
info.mtime = mtime
|
|
541
|
+
tar.addfile(info, io.BytesIO(content))
|
|
542
|
+
if verbose:
|
|
543
|
+
verbose_output += f"{relative_path}\n"
|
|
544
|
+
|
|
545
|
+
elif stat.is_symlink:
|
|
546
|
+
target = await ctx.fs.readlink(full_path)
|
|
547
|
+
info = tarfile.TarInfo(name=relative_path)
|
|
548
|
+
info.type = tarfile.SYMTYPE
|
|
549
|
+
info.linkname = target
|
|
550
|
+
info.mode = stat.mode
|
|
551
|
+
tar.addfile(info)
|
|
552
|
+
if verbose:
|
|
553
|
+
verbose_output += f"{relative_path}\n"
|
|
554
|
+
|
|
555
|
+
return verbose_output
|
|
556
|
+
|
|
557
|
+
def _is_bzip2(self, data: bytes) -> bool:
|
|
558
|
+
"""Check if data is bzip2 compressed."""
|
|
559
|
+
return len(data) >= 2 and data[0:2] == b"BZ"
|
|
560
|
+
|
|
561
|
+
def _is_xz(self, data: bytes) -> bool:
|
|
562
|
+
"""Check if data is xz compressed."""
|
|
563
|
+
return len(data) >= 6 and data[0:6] == b"\xfd7zXZ\x00"
|
|
564
|
+
|
|
565
|
+
def _decompress_data(
|
|
566
|
+
self, data: bytes, use_gzip: bool, use_bzip2: bool, use_xz: bool
|
|
567
|
+
) -> bytes:
|
|
568
|
+
"""Decompress data based on flags or auto-detection."""
|
|
569
|
+
# Try explicit flags first
|
|
570
|
+
if use_bzip2:
|
|
571
|
+
return bz2.decompress(data)
|
|
572
|
+
if use_xz:
|
|
573
|
+
return lzma.decompress(data)
|
|
574
|
+
if use_gzip:
|
|
575
|
+
return gzip.decompress(data)
|
|
576
|
+
|
|
577
|
+
# Auto-detect compression
|
|
578
|
+
if self._is_bzip2(data):
|
|
579
|
+
return bz2.decompress(data)
|
|
580
|
+
if self._is_xz(data):
|
|
581
|
+
return lzma.decompress(data)
|
|
582
|
+
if self._is_gzip(data):
|
|
583
|
+
return gzip.decompress(data)
|
|
584
|
+
|
|
585
|
+
return data # No compression detected
|
|
586
|
+
|
|
587
|
+
async def _extract_archive(
|
|
588
|
+
self,
|
|
589
|
+
ctx: CommandContext,
|
|
590
|
+
archive_file: str,
|
|
591
|
+
specific_files: list[str],
|
|
592
|
+
work_dir: str,
|
|
593
|
+
use_gzip: bool = False,
|
|
594
|
+
use_bzip2: bool = False,
|
|
595
|
+
use_xz: bool = False,
|
|
596
|
+
verbose: bool = False,
|
|
597
|
+
strip_components: int = 0,
|
|
598
|
+
to_stdout: bool = False,
|
|
599
|
+
keep_old_files: bool = False,
|
|
600
|
+
no_mtime: bool = False,
|
|
601
|
+
preserve_permissions: bool = False,
|
|
602
|
+
) -> ExecResult:
|
|
603
|
+
"""Extract a tar archive."""
|
|
604
|
+
# Read archive
|
|
605
|
+
if archive_file and archive_file != "-":
|
|
606
|
+
archive_path = ctx.fs.resolve_path(ctx.cwd, archive_file)
|
|
607
|
+
try:
|
|
608
|
+
archive_data = await ctx.fs.read_file_bytes(archive_path)
|
|
609
|
+
except FileNotFoundError:
|
|
610
|
+
return ExecResult(
|
|
611
|
+
stdout="",
|
|
612
|
+
stderr=f"tar: {archive_file}: Cannot open: No such file or directory\n",
|
|
613
|
+
exit_code=2,
|
|
614
|
+
)
|
|
615
|
+
else:
|
|
616
|
+
archive_data = ctx.stdin.encode("latin-1")
|
|
617
|
+
|
|
618
|
+
# Decompress if needed
|
|
619
|
+
try:
|
|
620
|
+
decompressed = self._decompress_data(
|
|
621
|
+
archive_data, use_gzip, use_bzip2, use_xz
|
|
622
|
+
)
|
|
623
|
+
except Exception as e:
|
|
624
|
+
return ExecResult(
|
|
625
|
+
stdout="",
|
|
626
|
+
stderr=f"tar: error decompressing archive: {e}\n",
|
|
627
|
+
exit_code=2,
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
# Open archive
|
|
631
|
+
buffer = io.BytesIO(decompressed)
|
|
632
|
+
try:
|
|
633
|
+
tar = tarfile.open(fileobj=buffer, mode="r")
|
|
634
|
+
except Exception as e:
|
|
635
|
+
return ExecResult(
|
|
636
|
+
stdout="",
|
|
637
|
+
stderr=f"tar: error opening archive: {e}\n",
|
|
638
|
+
exit_code=2,
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
verbose_output = ""
|
|
642
|
+
stdout_content = ""
|
|
643
|
+
errors: list[str] = []
|
|
644
|
+
|
|
645
|
+
# Create work directory if needed (unless extracting to stdout)
|
|
646
|
+
if not to_stdout:
|
|
647
|
+
try:
|
|
648
|
+
await ctx.fs.mkdir(work_dir, recursive=True)
|
|
649
|
+
except Exception:
|
|
650
|
+
pass
|
|
651
|
+
|
|
652
|
+
for member in tar.getmembers():
|
|
653
|
+
name = member.name
|
|
654
|
+
|
|
655
|
+
# Apply strip-components
|
|
656
|
+
if strip_components > 0:
|
|
657
|
+
parts = name.split("/")
|
|
658
|
+
if len(parts) <= strip_components:
|
|
659
|
+
continue # Skip if not enough components
|
|
660
|
+
name = "/".join(parts[strip_components:])
|
|
661
|
+
if not name:
|
|
662
|
+
continue
|
|
663
|
+
|
|
664
|
+
# Check if specific files requested
|
|
665
|
+
if specific_files:
|
|
666
|
+
if not any(
|
|
667
|
+
name == f or name.startswith(f"{f}/") or fnmatch(name, f)
|
|
668
|
+
for f in specific_files
|
|
669
|
+
):
|
|
670
|
+
continue
|
|
671
|
+
|
|
672
|
+
target_path = ctx.fs.resolve_path(work_dir, name)
|
|
673
|
+
|
|
674
|
+
try:
|
|
675
|
+
if to_stdout:
|
|
676
|
+
# Extract file contents to stdout
|
|
677
|
+
if member.isfile():
|
|
678
|
+
f = tar.extractfile(member)
|
|
679
|
+
if f:
|
|
680
|
+
content = f.read()
|
|
681
|
+
try:
|
|
682
|
+
stdout_content += content.decode("utf-8")
|
|
683
|
+
except UnicodeDecodeError:
|
|
684
|
+
stdout_content += content.decode("latin-1")
|
|
685
|
+
elif member.isdir():
|
|
686
|
+
if not keep_old_files or not await ctx.fs.exists(target_path):
|
|
687
|
+
await ctx.fs.mkdir(target_path, recursive=True)
|
|
688
|
+
elif member.isfile():
|
|
689
|
+
# Check if file exists and -k flag is set
|
|
690
|
+
if keep_old_files and await ctx.fs.exists(target_path):
|
|
691
|
+
# Skip this file, keep the existing one
|
|
692
|
+
if verbose:
|
|
693
|
+
verbose_output += f"{name}\n"
|
|
694
|
+
continue
|
|
695
|
+
|
|
696
|
+
# Ensure parent directory exists
|
|
697
|
+
parent = target_path.rsplit("/", 1)[0]
|
|
698
|
+
if parent:
|
|
699
|
+
try:
|
|
700
|
+
await ctx.fs.mkdir(parent, recursive=True)
|
|
701
|
+
except Exception:
|
|
702
|
+
pass
|
|
703
|
+
|
|
704
|
+
f = tar.extractfile(member)
|
|
705
|
+
if f:
|
|
706
|
+
content = f.read()
|
|
707
|
+
await ctx.fs.write_file(target_path, content)
|
|
708
|
+
|
|
709
|
+
# Preserve permissions if requested
|
|
710
|
+
if preserve_permissions:
|
|
711
|
+
await ctx.fs.chmod(target_path, member.mode)
|
|
712
|
+
|
|
713
|
+
elif member.issym():
|
|
714
|
+
if keep_old_files and await ctx.fs.exists(target_path):
|
|
715
|
+
if verbose:
|
|
716
|
+
verbose_output += f"{name}\n"
|
|
717
|
+
continue
|
|
718
|
+
|
|
719
|
+
parent = target_path.rsplit("/", 1)[0]
|
|
720
|
+
if parent:
|
|
721
|
+
try:
|
|
722
|
+
await ctx.fs.mkdir(parent, recursive=True)
|
|
723
|
+
except Exception:
|
|
724
|
+
pass
|
|
725
|
+
try:
|
|
726
|
+
await ctx.fs.symlink(member.linkname, target_path)
|
|
727
|
+
except Exception:
|
|
728
|
+
pass
|
|
729
|
+
|
|
730
|
+
if verbose:
|
|
731
|
+
verbose_output += f"{name}\n"
|
|
732
|
+
|
|
733
|
+
except Exception as e:
|
|
734
|
+
errors.append(f"tar: {name}: {e}")
|
|
735
|
+
|
|
736
|
+
tar.close()
|
|
737
|
+
|
|
738
|
+
stderr = verbose_output
|
|
739
|
+
if errors:
|
|
740
|
+
stderr += "\n".join(errors) + "\n"
|
|
741
|
+
return ExecResult(
|
|
742
|
+
stdout=stdout_content,
|
|
743
|
+
stderr=stderr,
|
|
744
|
+
exit_code=2 if errors else 0,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
async def _list_archive(
|
|
748
|
+
self,
|
|
749
|
+
ctx: CommandContext,
|
|
750
|
+
archive_file: str,
|
|
751
|
+
specific_files: list[str],
|
|
752
|
+
use_gzip: bool = False,
|
|
753
|
+
use_bzip2: bool = False,
|
|
754
|
+
use_xz: bool = False,
|
|
755
|
+
verbose: bool = False,
|
|
756
|
+
) -> ExecResult:
|
|
757
|
+
"""List contents of a tar archive."""
|
|
758
|
+
# Read archive
|
|
759
|
+
if archive_file and archive_file != "-":
|
|
760
|
+
archive_path = ctx.fs.resolve_path(ctx.cwd, archive_file)
|
|
761
|
+
try:
|
|
762
|
+
archive_data = await ctx.fs.read_file_bytes(archive_path)
|
|
763
|
+
except FileNotFoundError:
|
|
764
|
+
return ExecResult(
|
|
765
|
+
stdout="",
|
|
766
|
+
stderr=f"tar: {archive_file}: Cannot open: No such file or directory\n",
|
|
767
|
+
exit_code=2,
|
|
768
|
+
)
|
|
769
|
+
else:
|
|
770
|
+
archive_data = ctx.stdin.encode("latin-1")
|
|
771
|
+
|
|
772
|
+
# Decompress if needed
|
|
773
|
+
try:
|
|
774
|
+
decompressed = self._decompress_data(
|
|
775
|
+
archive_data, use_gzip, use_bzip2, use_xz
|
|
776
|
+
)
|
|
777
|
+
except Exception as e:
|
|
778
|
+
return ExecResult(
|
|
779
|
+
stdout="",
|
|
780
|
+
stderr=f"tar: error decompressing archive: {e}\n",
|
|
781
|
+
exit_code=2,
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
# Open archive
|
|
785
|
+
buffer = io.BytesIO(decompressed)
|
|
786
|
+
try:
|
|
787
|
+
tar = tarfile.open(fileobj=buffer, mode="r")
|
|
788
|
+
except Exception as e:
|
|
789
|
+
return ExecResult(
|
|
790
|
+
stdout="",
|
|
791
|
+
stderr=f"tar: error opening archive: {e}\n",
|
|
792
|
+
exit_code=2,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
stdout = ""
|
|
796
|
+
|
|
797
|
+
for member in tar.getmembers():
|
|
798
|
+
name = member.name
|
|
799
|
+
|
|
800
|
+
# Check if specific files requested
|
|
801
|
+
if specific_files:
|
|
802
|
+
if not any(
|
|
803
|
+
name == f or name.startswith(f"{f}/") or fnmatch(name, f)
|
|
804
|
+
for f in specific_files
|
|
805
|
+
):
|
|
806
|
+
continue
|
|
807
|
+
|
|
808
|
+
if verbose:
|
|
809
|
+
# Verbose format
|
|
810
|
+
mode_str = self._format_mode(member.mode, member.isdir())
|
|
811
|
+
owner = f"{member.uid}/{member.gid}"
|
|
812
|
+
size = str(member.size).rjust(8)
|
|
813
|
+
mtime = datetime.fromtimestamp(member.mtime)
|
|
814
|
+
date_str = mtime.strftime("%b %d %H:%M")
|
|
815
|
+
line = f"{mode_str} {owner:<10} {size} {date_str} {name}"
|
|
816
|
+
if member.issym():
|
|
817
|
+
line += f" -> {member.linkname}"
|
|
818
|
+
stdout += f"{line}\n"
|
|
819
|
+
else:
|
|
820
|
+
stdout += f"{name}\n"
|
|
821
|
+
|
|
822
|
+
tar.close()
|
|
823
|
+
return ExecResult(stdout=stdout, stderr="", exit_code=0)
|
|
824
|
+
|
|
825
|
+
async def _append_archive(
|
|
826
|
+
self,
|
|
827
|
+
ctx: CommandContext,
|
|
828
|
+
archive_file: str,
|
|
829
|
+
files: list[str],
|
|
830
|
+
work_dir: str,
|
|
831
|
+
verbose: bool = False,
|
|
832
|
+
exclude_patterns: list[str] | None = None,
|
|
833
|
+
) -> ExecResult:
|
|
834
|
+
"""Append files to an existing tar archive."""
|
|
835
|
+
if exclude_patterns is None:
|
|
836
|
+
exclude_patterns = []
|
|
837
|
+
|
|
838
|
+
if not archive_file:
|
|
839
|
+
return ExecResult(
|
|
840
|
+
stdout="",
|
|
841
|
+
stderr="tar: -r requires an archive file\n",
|
|
842
|
+
exit_code=2,
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
# Read existing archive
|
|
846
|
+
archive_path = ctx.fs.resolve_path(ctx.cwd, archive_file)
|
|
847
|
+
try:
|
|
848
|
+
archive_data = await ctx.fs.read_file_bytes(archive_path)
|
|
849
|
+
except FileNotFoundError:
|
|
850
|
+
return ExecResult(
|
|
851
|
+
stdout="",
|
|
852
|
+
stderr=f"tar: {archive_file}: Cannot open: No such file or directory\n",
|
|
853
|
+
exit_code=2,
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
# Open existing archive
|
|
857
|
+
buffer = io.BytesIO(archive_data)
|
|
858
|
+
try:
|
|
859
|
+
existing_tar = tarfile.open(fileobj=buffer, mode="r")
|
|
860
|
+
existing_members = list(existing_tar.getmembers())
|
|
861
|
+
existing_contents: dict[str, bytes] = {}
|
|
862
|
+
for member in existing_members:
|
|
863
|
+
if member.isfile():
|
|
864
|
+
f = existing_tar.extractfile(member)
|
|
865
|
+
if f:
|
|
866
|
+
existing_contents[member.name] = f.read()
|
|
867
|
+
existing_tar.close()
|
|
868
|
+
except Exception as e:
|
|
869
|
+
return ExecResult(
|
|
870
|
+
stdout="",
|
|
871
|
+
stderr=f"tar: error opening archive: {e}\n",
|
|
872
|
+
exit_code=2,
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
# Create new archive with existing + new files
|
|
876
|
+
new_buffer = io.BytesIO()
|
|
877
|
+
try:
|
|
878
|
+
tar = tarfile.open(fileobj=new_buffer, mode="w")
|
|
879
|
+
except Exception as e:
|
|
880
|
+
return ExecResult(
|
|
881
|
+
stdout="",
|
|
882
|
+
stderr=f"tar: error creating archive: {e}\n",
|
|
883
|
+
exit_code=2,
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
# Add existing members
|
|
887
|
+
for member in existing_members:
|
|
888
|
+
if member.isfile() and member.name in existing_contents:
|
|
889
|
+
tar.addfile(member, io.BytesIO(existing_contents[member.name]))
|
|
890
|
+
else:
|
|
891
|
+
tar.addfile(member)
|
|
892
|
+
|
|
893
|
+
verbose_output = ""
|
|
894
|
+
errors: list[str] = []
|
|
895
|
+
|
|
896
|
+
# Add new files
|
|
897
|
+
for file_path in files:
|
|
898
|
+
try:
|
|
899
|
+
added = await self._add_to_archive(
|
|
900
|
+
ctx, tar, work_dir, file_path, verbose, errors, exclude_patterns
|
|
901
|
+
)
|
|
902
|
+
if verbose:
|
|
903
|
+
verbose_output += added
|
|
904
|
+
except Exception as e:
|
|
905
|
+
errors.append(f"tar: {file_path}: {e}")
|
|
906
|
+
|
|
907
|
+
tar.close()
|
|
908
|
+
|
|
909
|
+
# Write back the archive
|
|
910
|
+
archive_data = new_buffer.getvalue()
|
|
911
|
+
try:
|
|
912
|
+
await ctx.fs.write_file(archive_path, archive_data)
|
|
913
|
+
except Exception as e:
|
|
914
|
+
return ExecResult(
|
|
915
|
+
stdout="",
|
|
916
|
+
stderr=f"tar: {archive_file}: {e}\n",
|
|
917
|
+
exit_code=2,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
stderr = verbose_output
|
|
921
|
+
if errors:
|
|
922
|
+
stderr = "\n".join(errors) + "\n"
|
|
923
|
+
return ExecResult(
|
|
924
|
+
stdout="",
|
|
925
|
+
stderr=stderr,
|
|
926
|
+
exit_code=2 if errors else 0,
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
async def _update_archive(
|
|
930
|
+
self,
|
|
931
|
+
ctx: CommandContext,
|
|
932
|
+
archive_file: str,
|
|
933
|
+
files: list[str],
|
|
934
|
+
work_dir: str,
|
|
935
|
+
verbose: bool = False,
|
|
936
|
+
exclude_patterns: list[str] | None = None,
|
|
937
|
+
) -> ExecResult:
|
|
938
|
+
"""Update archive with files that are newer or don't exist in archive."""
|
|
939
|
+
if exclude_patterns is None:
|
|
940
|
+
exclude_patterns = []
|
|
941
|
+
|
|
942
|
+
if not archive_file:
|
|
943
|
+
return ExecResult(
|
|
944
|
+
stdout="",
|
|
945
|
+
stderr="tar: -u requires an archive file\n",
|
|
946
|
+
exit_code=2,
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
# Read existing archive
|
|
950
|
+
archive_path = ctx.fs.resolve_path(ctx.cwd, archive_file)
|
|
951
|
+
try:
|
|
952
|
+
archive_data = await ctx.fs.read_file_bytes(archive_path)
|
|
953
|
+
except FileNotFoundError:
|
|
954
|
+
return ExecResult(
|
|
955
|
+
stdout="",
|
|
956
|
+
stderr=f"tar: {archive_file}: Cannot open: No such file or directory\n",
|
|
957
|
+
exit_code=2,
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
# Open existing archive and get member info
|
|
961
|
+
buffer = io.BytesIO(archive_data)
|
|
962
|
+
try:
|
|
963
|
+
existing_tar = tarfile.open(fileobj=buffer, mode="r")
|
|
964
|
+
existing_members = list(existing_tar.getmembers())
|
|
965
|
+
existing_contents: dict[str, bytes] = {}
|
|
966
|
+
existing_mtimes: dict[str, float] = {}
|
|
967
|
+
for member in existing_members:
|
|
968
|
+
existing_mtimes[member.name] = member.mtime
|
|
969
|
+
if member.isfile():
|
|
970
|
+
f = existing_tar.extractfile(member)
|
|
971
|
+
if f:
|
|
972
|
+
existing_contents[member.name] = f.read()
|
|
973
|
+
existing_tar.close()
|
|
974
|
+
except Exception as e:
|
|
975
|
+
return ExecResult(
|
|
976
|
+
stdout="",
|
|
977
|
+
stderr=f"tar: error opening archive: {e}\n",
|
|
978
|
+
exit_code=2,
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
# Create new archive
|
|
982
|
+
new_buffer = io.BytesIO()
|
|
983
|
+
try:
|
|
984
|
+
tar = tarfile.open(fileobj=new_buffer, mode="w")
|
|
985
|
+
except Exception as e:
|
|
986
|
+
return ExecResult(
|
|
987
|
+
stdout="",
|
|
988
|
+
stderr=f"tar: error creating archive: {e}\n",
|
|
989
|
+
exit_code=2,
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
# Add existing members first
|
|
993
|
+
for member in existing_members:
|
|
994
|
+
if member.isfile() and member.name in existing_contents:
|
|
995
|
+
tar.addfile(member, io.BytesIO(existing_contents[member.name]))
|
|
996
|
+
else:
|
|
997
|
+
tar.addfile(member)
|
|
998
|
+
|
|
999
|
+
verbose_output = ""
|
|
1000
|
+
errors: list[str] = []
|
|
1001
|
+
|
|
1002
|
+
# Add new/updated files
|
|
1003
|
+
for file_path in files:
|
|
1004
|
+
full_path = ctx.fs.resolve_path(work_dir, file_path)
|
|
1005
|
+
try:
|
|
1006
|
+
stat = await ctx.fs.stat(full_path)
|
|
1007
|
+
mtime = stat.mtime
|
|
1008
|
+
if hasattr(mtime, 'timestamp'):
|
|
1009
|
+
mtime = mtime.timestamp()
|
|
1010
|
+
|
|
1011
|
+
# Check if file needs updating
|
|
1012
|
+
if file_path in existing_mtimes:
|
|
1013
|
+
if mtime <= existing_mtimes[file_path]:
|
|
1014
|
+
continue # Skip, archive version is not older
|
|
1015
|
+
|
|
1016
|
+
# Add the file
|
|
1017
|
+
added = await self._add_to_archive(
|
|
1018
|
+
ctx, tar, work_dir, file_path, verbose, errors, exclude_patterns
|
|
1019
|
+
)
|
|
1020
|
+
if verbose:
|
|
1021
|
+
verbose_output += added
|
|
1022
|
+
except FileNotFoundError:
|
|
1023
|
+
errors.append(f"tar: {file_path}: No such file or directory")
|
|
1024
|
+
except Exception as e:
|
|
1025
|
+
errors.append(f"tar: {file_path}: {e}")
|
|
1026
|
+
|
|
1027
|
+
tar.close()
|
|
1028
|
+
|
|
1029
|
+
# Write back the archive
|
|
1030
|
+
archive_data = new_buffer.getvalue()
|
|
1031
|
+
try:
|
|
1032
|
+
await ctx.fs.write_file(archive_path, archive_data)
|
|
1033
|
+
except Exception as e:
|
|
1034
|
+
return ExecResult(
|
|
1035
|
+
stdout="",
|
|
1036
|
+
stderr=f"tar: {archive_file}: {e}\n",
|
|
1037
|
+
exit_code=2,
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
stderr = verbose_output
|
|
1041
|
+
if errors:
|
|
1042
|
+
stderr = "\n".join(errors) + "\n"
|
|
1043
|
+
return ExecResult(
|
|
1044
|
+
stdout="",
|
|
1045
|
+
stderr=stderr,
|
|
1046
|
+
exit_code=2 if errors else 0,
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
def _is_gzip(self, data: bytes) -> bool:
|
|
1050
|
+
"""Check if data is gzip compressed."""
|
|
1051
|
+
return len(data) >= 2 and data[0] == 0x1F and data[1] == 0x8B
|
|
1052
|
+
|
|
1053
|
+
def _format_mode(self, mode: int, is_dir: bool) -> str:
|
|
1054
|
+
"""Format file mode like ls -l."""
|
|
1055
|
+
chars = "d" if is_dir else "-"
|
|
1056
|
+
perms = [
|
|
1057
|
+
"r" if mode & 0o400 else "-",
|
|
1058
|
+
"w" if mode & 0o200 else "-",
|
|
1059
|
+
"x" if mode & 0o100 else "-",
|
|
1060
|
+
"r" if mode & 0o040 else "-",
|
|
1061
|
+
"w" if mode & 0o020 else "-",
|
|
1062
|
+
"x" if mode & 0o010 else "-",
|
|
1063
|
+
"r" if mode & 0o004 else "-",
|
|
1064
|
+
"w" if mode & 0o002 else "-",
|
|
1065
|
+
"x" if mode & 0o001 else "-",
|
|
1066
|
+
]
|
|
1067
|
+
return chars + "".join(perms)
|