More work on ssh copy

This commit is contained in:
Kovid Goyal 2022-02-28 07:58:49 +05:30
parent fadae42715
commit 4b6bfaffba
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 49 additions and 17 deletions

View File

@ -5,7 +5,9 @@
import glob import glob
import os import os
import shlex import shlex
from typing import Iterable, Iterator, List, Optional, Sequence, Tuple from typing import (
Iterable, Iterator, List, NamedTuple, Optional, Sequence, Tuple
)
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import CopyCLIOptions from kitty.cli_stub import CopyCLIOptions
@ -24,8 +26,15 @@ Interpret file arguments as glob patterns.
The destination on the remote computer to copy to. Relative paths are resolved The destination on the remote computer to copy to. Relative paths are resolved
relative to HOME on the remote machine. When this option is not specified, the relative to HOME on the remote machine. When this option is not specified, the
local file path is used as the remote destination (with the HOME directory local file path is used as the remote destination (with the HOME directory
getting automatically replaced by the remote HOME). Note that enviroment getting automatically replaced by the remote HOME). Note that environment
variables and ~ are not expanded. variables and ~ are not expanded.
--exclude
type=list
A glob pattern. Files whose names would match this pattern after transfer
are excluded from being transferred. Useful when adding directories. Can
be specified multiple times, if any of the patterns match the file will be excluded.
''' '''
@ -58,7 +67,23 @@ class CopyCLIError(ValueError):
pass pass
def parse_copy_instructions(val: str) -> Iterable[Tuple[str, CopyCLIOptions]]: def get_arcname(loc: str, dest: Optional[str], home: str) -> str:
if dest:
arcname = dest
else:
arcname = os.path.normpath(loc)
if arcname.startswith(home):
arcname = os.path.relpath(arcname, home)
arcname = os.path.normpath(arcname).replace(os.sep, '/')
return arcname
class CopyInstruction(NamedTuple):
arcname: str
exclude_patterns: Tuple[str, ...]
def parse_copy_instructions(val: str) -> Iterable[Tuple[str, CopyInstruction]]:
opts, args = parse_copy_args(shlex.split(val)) opts, args = parse_copy_args(shlex.split(val))
locations: List[str] = [] locations: List[str] = []
for a in args: for a in args:
@ -67,5 +92,7 @@ def parse_copy_instructions(val: str) -> Iterable[Tuple[str, CopyCLIOptions]]:
raise CopyCLIError('No files to copy specified') raise CopyCLIError('No files to copy specified')
if len(locations) > 1 and opts.dest: if len(locations) > 1 and opts.dest:
raise CopyCLIError('Specifying a remote location with more than one file is not supported') raise CopyCLIError('Specifying a remote location with more than one file is not supported')
home = os.path.expanduser('~')
for loc in locations: for loc in locations:
yield loc, opts arcname = get_arcname(loc, opts.dest, home)
yield loc, CopyInstruction(arcname, tuple(opts.exclude))

View File

@ -2,6 +2,7 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import atexit import atexit
import fnmatch
import io import io
import json import json
import os import os
@ -15,7 +16,7 @@ import traceback
from base64 import standard_b64decode from base64 import standard_b64decode
from contextlib import suppress from contextlib import suppress
from typing import ( from typing import (
Any, Dict, Iterator, List, NoReturn, Optional, Set, Tuple, Union Any, Callable, Dict, Iterator, List, NoReturn, Optional, Set, Tuple, Union
) )
from kitty.constants import cache_dir, shell_integration_dir, terminfo_dir from kitty.constants import cache_dir, shell_integration_dir, terminfo_dir
@ -66,10 +67,13 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str]) -> bytes:
tf.addfile(ans, io.BytesIO(data)) tf.addfile(ans, io.BytesIO(data))
return ans return ans
def filter_files(tarinfo: tarfile.TarInfo) -> Optional[tarfile.TarInfo]: def filter_from_globs(*pats: str) -> Callable[[tarfile.TarInfo], Optional[tarfile.TarInfo]]:
if tarinfo.name.endswith('ssh/bootstrap.sh'): def filter(tarinfo: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
return None for pat in pats:
return normalize_tarinfo(tarinfo) if fnmatch.fnmatch(tarinfo.name, pat):
return None
return normalize_tarinfo(tarinfo)
return filter
from kitty.shell_integration import get_effective_ksi_env_var from kitty.shell_integration import get_effective_ksi_env_var
if ssh_opts.shell_integration == 'inherit': if ssh_opts.shell_integration == 'inherit':
@ -94,10 +98,13 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str]) -> bytes:
buf = io.BytesIO() buf = io.BytesIO()
with tarfile.open(mode='w:bz2', fileobj=buf, encoding='utf-8') as tf: with tarfile.open(mode='w:bz2', fileobj=buf, encoding='utf-8') as tf:
rd = ssh_opts.remote_dir.rstrip('/') rd = ssh_opts.remote_dir.rstrip('/')
for location, ci in ssh_opts.copy.items():
tf.add(location, arcname=ci.arcname, filter=filter_from_globs(*ci.exclude_patterns))
add_data_as_file(tf, 'kitty-ssh-kitten-data.sh', env_script) add_data_as_file(tf, 'kitty-ssh-kitten-data.sh', env_script)
if ksi: if ksi:
tf.add(shell_integration_dir, arcname=rd + '/shell-integration', filter=filter_files) arcname = rd + '/shell-integration'
tf.add(terminfo_dir, arcname='.terminfo', filter=filter_files) tf.add(shell_integration_dir, arcname=arcname, filter=filter_from_globs(f'{arcname}/ssh/bootstrap.*'))
tf.add(terminfo_dir, arcname='.terminfo', filter=normalize_tarinfo)
return buf.getvalue() return buf.getvalue()

View File

@ -1,7 +1,7 @@
# generated by gen-config.py DO NOT edit # generated by gen-config.py DO NOT edit
import typing import typing
import kitty.cli_stub import kittens.ssh.copy
option_names = ( # {{{ option_names = ( # {{{
@ -12,7 +12,7 @@ class Options:
hostname: str = '*' hostname: str = '*'
remote_dir: str = '.local/share/kitty-ssh-kitten' remote_dir: str = '.local/share/kitty-ssh-kitten'
shell_integration: str = 'inherit' shell_integration: str = 'inherit'
copy: typing.Dict[str, kitty.cli_stub.CLIOptions] = {} copy: typing.Dict[str, kittens.ssh.copy.CopyInstruction] = {}
env: typing.Dict[str, str] = {} env: typing.Dict[str, str] = {}
config_paths: typing.Tuple[str, ...] = () config_paths: typing.Tuple[str, ...] = ()
config_overrides: typing.Tuple[str, ...] = () config_overrides: typing.Tuple[str, ...] = ()

View File

@ -4,9 +4,7 @@
import posixpath import posixpath
from typing import Any, Dict, Iterable, Optional, Tuple from typing import Any, Dict, Iterable, Optional, Tuple
from kitty.cli_stub import CopyCLIOptions from ..copy import CopyInstruction, parse_copy_instructions
from ..copy import parse_copy_instructions
DELETE_ENV_VAR = '_delete_this_env_var_' DELETE_ENV_VAR = '_delete_this_env_var_'
@ -33,7 +31,7 @@ def env(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, str]]:
yield val, DELETE_ENV_VAR yield val, DELETE_ENV_VAR
def copy(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, CopyCLIOptions]]: def copy(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, CopyInstruction]]:
yield from parse_copy_instructions(val) yield from parse_copy_instructions(val)