diff --git a/kittens/ssh/copy.py b/kittens/ssh/copy.py index 2c2590473..e20927fa8 100644 --- a/kittens/ssh/copy.py +++ b/kittens/ssh/copy.py @@ -5,7 +5,9 @@ import glob import os 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_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 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 -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. + + +--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 -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)) locations: List[str] = [] 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') if len(locations) > 1 and opts.dest: raise CopyCLIError('Specifying a remote location with more than one file is not supported') + home = os.path.expanduser('~') for loc in locations: - yield loc, opts + arcname = get_arcname(loc, opts.dest, home) + yield loc, CopyInstruction(arcname, tuple(opts.exclude)) diff --git a/kittens/ssh/main.py b/kittens/ssh/main.py index 518bc6db9..897086838 100644 --- a/kittens/ssh/main.py +++ b/kittens/ssh/main.py @@ -2,6 +2,7 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal import atexit +import fnmatch import io import json import os @@ -15,7 +16,7 @@ import traceback from base64 import standard_b64decode from contextlib import suppress 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 @@ -66,10 +67,13 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str]) -> bytes: tf.addfile(ans, io.BytesIO(data)) return ans - def filter_files(tarinfo: tarfile.TarInfo) -> Optional[tarfile.TarInfo]: - if tarinfo.name.endswith('ssh/bootstrap.sh'): - return None - return normalize_tarinfo(tarinfo) + def filter_from_globs(*pats: str) -> Callable[[tarfile.TarInfo], Optional[tarfile.TarInfo]]: + def filter(tarinfo: tarfile.TarInfo) -> Optional[tarfile.TarInfo]: + for pat in pats: + if fnmatch.fnmatch(tarinfo.name, pat): + return None + return normalize_tarinfo(tarinfo) + return filter from kitty.shell_integration import get_effective_ksi_env_var 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() with tarfile.open(mode='w:bz2', fileobj=buf, encoding='utf-8') as tf: 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) if ksi: - tf.add(shell_integration_dir, arcname=rd + '/shell-integration', filter=filter_files) - tf.add(terminfo_dir, arcname='.terminfo', filter=filter_files) + arcname = rd + '/shell-integration' + 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() diff --git a/kittens/ssh/options/types.py b/kittens/ssh/options/types.py index d2d472099..03922bafb 100644 --- a/kittens/ssh/options/types.py +++ b/kittens/ssh/options/types.py @@ -1,7 +1,7 @@ # generated by gen-config.py DO NOT edit import typing -import kitty.cli_stub +import kittens.ssh.copy option_names = ( # {{{ @@ -12,7 +12,7 @@ class Options: hostname: str = '*' remote_dir: str = '.local/share/kitty-ssh-kitten' 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] = {} config_paths: typing.Tuple[str, ...] = () config_overrides: typing.Tuple[str, ...] = () diff --git a/kittens/ssh/options/utils.py b/kittens/ssh/options/utils.py index 936aa9533..616eabfc2 100644 --- a/kittens/ssh/options/utils.py +++ b/kittens/ssh/options/utils.py @@ -4,9 +4,7 @@ import posixpath from typing import Any, Dict, Iterable, Optional, Tuple -from kitty.cli_stub import CopyCLIOptions - -from ..copy import parse_copy_instructions +from ..copy import CopyInstruction, parse_copy_instructions 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 -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)