blob: bcd5976d153433cc00bb0af78edebaa5a95eada8 [file] [log] [blame]
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
'''
Simple wrapper for Paramiko SFTP client (see http://www.paramiko.org/)
'''
import getpass
import json
import os
import signal
import sys
import paramiko
VERSION='1.0'
# -----------------------------------------------------------------------------------------
def die(msg=None,rc=1):
"""
Cleanly exits the program with an error message
"""
if msg:
print(msg)
sys.exit(rc)
# ----------------------------------------------------------------------------
def isEmpty(s):
if (s is None) or (len(s) <= 0):
return True
else:
return False
# ----------------------------------------------------------------------------
def isNumberString(value):
"""
Checks if value is a string that has only digits - possibly with leading '+' or '-'
"""
if not value:
return False
sign = value[0]
if (sign == '+') or (sign == '-'):
if len(value) <= 1:
return False
absValue = value[1:]
return absValue.isdigit()
else:
if len(value) <= 0:
return False
else:
return value.isdigit()
def isNumberValue(value):
return isinstance(value, (int, float))
# ----------------------------------------------------------------------------
def isFloatingPointString(value):
"""
Checks if value is a string that has only digits - possibly with leading '+' or '-' - AND a single dot
"""
if isEmpty(value):
return False
sign = value[0]
if (sign == '+') or (sign == '-'):
if len(value) <= 1:
return False
absValue = value[1:]
else:
absValue = value
dotPos = absValue.find('.')
# Must have a dot and it cannot be the last character
if (dotPos < 0) or (dotPos == (len(absValue) - 1)):
return False
# Must have EXACTLY one dot
dotCount = absValue.count('.')
if dotCount != 1:
return False
# Make sure both sides of the dot are integer numbers
intPart = absValue[0:dotPos]
if not isNumberString(intPart):
return False
facPart = absValue[dotPos + 1:]
# Do not allow 123.-5
sign = facPart[0]
if (sign == '+') or (sign == '-'):
return False
if not isNumberString(facPart):
return False
return True
# ----------------------------------------------------------------------------
def normalizeValue(value):
"""
Checks if value is 'True', 'False' or all numeric and converts it accordingly
Otherwise it just returns it
Args:
value (str) - String value
"""
if not value:
return value
loCase = value.lower()
if loCase == "none":
return None
elif loCase == "true":
return True
elif loCase == "false":
return False
elif isNumberString(loCase):
return int(loCase)
else:
return value
# ----------------------------------------------------------------------------
def parseCommandLineArguments(args):
"""
Parses an array of arguments having the format: --name=value. If
only --name is provided then it is assumed to a TRUE boolean value.
If the value is all digits, then it is assumed to be a number.
If the same key is specified more than once, then a list of
the accumulated values is created. The result is a dictionary
with the names as the keys and value as their mapped values
Args:
args (str[]) - The command line arguments to parse
"""
valsMap = {}
if len(args) <= 0:
return valsMap
for item in args:
if not item.startswith("--"):
raise Exception("Missing option identifier: %s" % item)
propPair = item[2:] # strip the prefix
sepPos = propPair.find('=')
if sepPos == 0:
raise Exception("Missing name: %s" % item)
if sepPos >= (len(propPair) - 1):
raise Exception("Missing value: %s" % item)
propName = propPair
propValue = None
if sepPos < 0:
propValue = True
else:
propName = propPair[0:sepPos]
propValue = normalizeValue(propPair[sepPos + 1:])
if propName in valsMap:
curValue = valsMap[propName]
if not isinstance(curValue, list):
curValue = [ curValue ]
curValue.append(propValue)
valsMap[propName] = curValue
else:
valsMap[propName] = propValue
return valsMap
# ----------------------------------------------------------------------------
def resolvePathVariables(path):
"""
Expands ~/xxx and ${XXX} variables
"""
if isEmpty(path):
return path
path = os.path.expanduser(path)
path = os.path.expandvars(path)
return path
# ----------------------------------------------------------------------------
def _decode_list(data):
# can happen for internal sub-lists of objects
if isinstance(data, dict):
return _decode_dict(data)
rv = []
for item in data:
if isinstance(item, list):
item = _decode_list(item)
elif isinstance(item, dict):
item = _decode_dict(item)
rv.append(item)
return rv
# ----------------------------------------------------------------------------
def _decode_dict(data):
# can happen for internal sub-lists of objects
if isinstance(data, list):
return _decode_list(data)
rv = {}
for key, value in data.items():
if isinstance(value, list):
value = _decode_list(value)
elif isinstance(value, dict):
value = _decode_dict(value)
rv[key] = value
return rv
# ----------------------------------------------------------------------------
def loadJsonFile(configFile):
if isEmpty(configFile):
return {}
with open(configFile) as config_file:
return json.load(config_file, object_hook=_decode_dict);
# ----------------------------------------------------------------------------
def createSftpClient(args):
host = args.get("host", "localhost")
port = args.get("port", 22)
username = args.get("username", None)
if isEmpty(username):
username = getpass.getuser()
password = args.get("password", None)
keyfile = args.get("keyFile", None)
keytype = args.get("keyType", "RSA")
sftp = None
transport = None
try:
key = None
if keyfile is not None:
# Get private key used to authenticate user.
if keytype == 'DSA':
# The private key is a DSA type key.
key = paramiko.DSSKey.from_private_key_file(keyfile)
else:
# The private key is a RSA type key.
key = paramiko.RSAKey.from_private_key(keyfile)
# Create Transport object using supplied method of authentication.
transport = paramiko.Transport((host, port))
transport.connect(None, username, password, key)
sftp = paramiko.SFTPClient.from_transport(transport)
return sftp
except Exception as e:
print('An error occurred creating SFTP client: %s: %s' % (e.__class__, e))
if sftp is not None:
try:
sftp.close()
except Exception as err:
print('Failed to close SFTP client: %s: %s' % (err.__class__, err))
if transport is not None:
try:
transport.close()
except Exception as err:
print('Failed to close transport: %s: %s' % (err.__class__, err))
raise e
# =========================================================================================
def doList(sftp, curdir, argsList):
dirPath = curdir;
if not isEmpty(argsList):
dirPath = argsList.pop(0)
dirPath = dirPath.strip()
dirPath = os.path.join(curdir, dirPath)
# Also available: listdir_attr, listdir
dirlist = sftp.listdir_iter(path=dirPath)
for row in dirlist:
# see https://docs.paramiko.org/en/2.6/api/sftp.html#paramiko.sftp_attr.SFTPAttributes
print(" %s" % str(row))
def doChdir(sftp, homedir, curdir, argsList):
dirPath = homedir
if not isEmpty(argsList):
dirPath = argsList.pop(0)
dirPath = dirPath.strip()
dirPath = os.path.join(curdir, dirPath)
sftp.chdir(dirPath)
# ----------------------------------------------------------------------------
# see https://github.com/paramiko/paramiko/blob/master/demos/demo_sftp.py
# see https://docs.paramiko.org/en/2.6/api/sftp.html
def doSftp(sftp):
homedir = sftp.normalize('.')
sftp.chdir(homedir)
while True:
curdir = sftp.getcwd()
sys.stdout.write("%s > " % curdir)
sys.stdout.flush()
l = sys.stdin.readline()
l = l.strip()
if isEmpty(l):
continue
argsList = l.split(' ')
op = argsList.pop(0)
if (op == "quit") or (op == "exit") or (op == "bye"):
break
elif (op == "ls") or (op == "list"):
doList(sftp, curdir, argsList)
elif (op == "cd"):
doChdir(sftp, homedir, curdir, argsList)
# TODO get_channel()
# show info using get_transport() on it
# get_security_options() on transport
else:
print("Unknown command: %s" % l)
def doMain(args):
sftp = createSftpClient(args)
try:
doSftp(sftp);
except Exception as e:
print('An error occurred using the SFTP client: %s: %s' % (e.__class__, e))
raise e
finally:
sftp.close()
#
# Usage: python3 sftpclient.py --arg1=value1 --arg2=value2 ...
#
# Where available arguments are:
#
# * host - default=localhost
# * port - default=22
# * username - the login user - default=currently logged in user
# * password - the password - can be omitted if key file used
# * keyFile - path to key file
# * keyType - type of key in file (RSA/DSA) - default=RSA
def main(args):
if len(args) > 0:
subArgs = parseCommandLineArguments(args)
else:
subArgs = {}
doMain(subArgs)
sys.exit(0)
# ----------------------------------------------------------------------------
def signal_handler(signal, frame):
die('Exit due to Control+C')
if __name__ == "__main__":
pyVersion = sys.version_info
if pyVersion.major != 3:
die("Major Python version must be 3.x: %s" % str(pyVersion))
if pyVersion.minor < 0:
print("Warning: minor Python version %s should be at least 3.0+" % str(pyVersion))
signal.signal(signal.SIGINT, signal_handler)
if os.name == 'nt':
print("Use Ctrl+Break to stop the script")
else:
print("Use Ctrl+C to stop the script")
main(sys.argv[1:])