使用Python通过SFTP上传文件,但如果路径不存在则创建目录

38

我希望使用Python在远程服务器上上传文件。我想要先检查远程路径是否真实存在,如果不存在,则创建该路径。伪代码如下:

if(remote_path not exist):
    create_path(remote_path)
upload_file(local_file, remote_path)

我在考虑使用Paramiko执行一个命令来创建路径(例如 mkdir -p remote_path)。我想到了以下代码:

# I didn't test this code

import paramiko, sys

ssh = paramiko.SSHClient()
ssh.connect(myhost, 22, myusername, mypassword)
ssh.exec_command('mkdir -p ' + remote_path)
ssh.close

transport = paramiko.Transport((myhost, 22))
transport.connect(username = myusername, password = mypassword)

sftp = paramiko.SFTPClient.from_transport(transport)
sftp.put(local_path, remote_path)
sftp.close()

transport.close()

但是这种解决方案对我来说听起来不太好,因为我需要关闭连接,然后再次打开连接。有更好的方法吗?


7个回答

63

SFTP支持通常的FTP命令(如chdir,mkdir等),因此请使用这些命令:

sftp = paramiko.SFTPClient.from_transport(transport)
try:
    sftp.chdir(remote_path)  # Test if remote_path exists
except IOError:
    sftp.mkdir(remote_path)  # Create remote_path
    sftp.chdir(remote_path)
sftp.put(local_path, '.')    # At this point, you are in remote_path in either case
sftp.close()
为了完全模拟`mkdir -p`的功能,您可以通过递归处理`remote_path`来实现:
import os.path

def mkdir_p(sftp, remote_directory):
    """Change to this directory, recursively making new folders if needed.
    Returns True if any folders were created."""
    if remote_directory == '/':
        # absolute path so change directory to root
        sftp.chdir('/')
        return
    if remote_directory == '':
        # top-level relative directory must exist
        return
    try:
        sftp.chdir(remote_directory) # sub-directory exists
    except IOError:
        dirname, basename = os.path.split(remote_directory.rstrip('/'))
        mkdir_p(sftp, dirname) # make parent directories
        sftp.mkdir(basename) # sub-directory missing, so created it
        sftp.chdir(basename)
        return True

sftp = paramiko.SFTPClient.from_transport(transport)
mkdir_p(sftp, remote_path) 
sftp.put(local_path, '.')    # At this point, you are in remote_path
sftp.close()
当然,如果remote_path还包含一个远程文件名,则需要将其拆分,将目录传递给mkdir_p,将文件名用作sftp.put中的“.”的替代品。

它不处理不存在的父目录(-p)。比较os.mkdir()和os.makedirs()。拆分路径并进行递归调用以创建必要的父目录。 - jfs
在函数 mkdir_p 中没有 sftp 的句柄。 - franzlorenzon
1
好的观点。已相应更新(不过更Pythonic的陈述还不是100%确定;P) - isedev
2
你应该在FTP路径中使用posixpath而不是os.path。通过将递归调用移动到异常处理程序中,可以避免访问所有路径段。 - jfs
@J.F.Sebastian -- posixpath?给我讲解一下吧! - isedev
显示剩余2条评论

12

更简单和稍微更易读一点的东西

def mkdir_p(sftp, remote, is_dir=False):
    """
    emulates mkdir_p if required. 
    sftp - is a valid sftp object
    remote - remote path to create. 
    """
    dirs_ = []
    if is_dir:
        dir_ = remote
    else:
        dir_, basename = os.path.split(remote)
    while len(dir_) > 1:
        dirs_.append(dir_)
        dir_, _  = os.path.split(dir_)

    if len(dir_) == 1 and not dir_.startswith("/"): 
        dirs_.append(dir_) # For a remote path like y/x.txt 

    while len(dirs_):
        dir_ = dirs_.pop()
        try:
            sftp.stat(dir_)
        except:
            print "making ... dir",  dir_
            sftp.mkdir(dir_)

1
提供一种非递归替代方案加1。请注意,这里的“remote”输入参数是远程文件路径。如果您希望此函数具有远程目录路径作为输入,请将“dir_,basename = os.path.split(remote)”替换为“dir_ = remote”。 - Alan Evangelista
@AlanEvangelista 感谢您的评论。更新了传递标志 is_dir 的代码。请查看并进行必要的编辑。 - gabhijit
1
你不应该使用 except: 来检查错误。请参考:https://dev59.com/5mMk5IYBdhLWcg3wxAry#18982771 - Mikhail Gerasimov

7

今天必须要做这件事情。以下是我完成它的方法。

def mkdir_p(sftp, remote_directory):
    dir_path = str()
    for dir_folder in remote_directory.split("/"):
        if dir_folder == "":
            continue
        dir_path += r"/{0}".format(dir_folder)
        try:
            sftp.listdir(dir_path)
        except IOError:
            sftp.mkdir(dir_path)

5

您可以使用pysftp包:

import pysftp as sftp

#used to pypass key login
cnopts = sftp.CnOpts()
cnopts.hostkeys = None

srv = sftp.Connection(host="10.2.2.2",username="ritesh",password="ritesh",cnopts=cnopts)
srv.makedirs("a3/a2/a1", mode=777)  # will happily make all non-existing directories

您可以点击此链接了解更多细节:https://pysftp.readthedocs.io/en/release_0.2.9/cookbook.html#pysftp-connection-mkdir。该链接与it技术有关,涉及pysftp连接的创建和目录创建。

2
我的版本:
def is_sftp_dir_exists(sftp, path):
    try:
        sftp.stat(path)
        return True
    except Exception:
        return False


def create_sftp_dir(sftp, path):
    try:
        sftp.mkdir(path)
    except IOError as exc:
        if not is_sftp_dir_exists(sftp, path):
            raise exc


def create_sftp_dir_recursive(sftp, path):
    parts = deque(Path(path).parts)

    to_create = Path()
    while parts:
        to_create /= parts.popleft()
        create_sftp_dir(sftp, str(to_create))

我们尝试使用mkdir,而不是先尝试listdir/stat,这是由于EAFP原则(进行一个网络请求比进行多个请求更有效率)。

1
但是这种方式,如果目录不存在且无法创建,create_sftp_dir 看起来会成功。 - Martin Prikryl
@MartinPrikryl 没错,谢谢。我修复了代码,它仍然尝试在没有预检查的情况下创建一个目录,并且只有在目录未被创建时才进行检查以确定原因。 - Mikhail Gerasimov
好的,但是现在,如果你用 /foo/bar 调用它且两个存在,你的代码将会执行四次请求,相较于先测试 /foo/bar 是否存在只需要一次。 - Martin Prikryl
顺便问一下,当在Windows上使用Path类时,您确定它会正确处理posix风格的SFTP路径吗? - Martin Prikryl
将执行四个请求,相比之下只有一个。确切地说是4比2(一个用于检查+一个用于创建)。这仅适用于尝试创建已经存在的目录的情况。对于许多目录不存在的情况,我们将获得更多的好处。请求数量还取决于您如何检查事物:从左到右还是从右到左,这是EAFP无关的。关于Windows上的SFTP路径的好处,谢谢,我会进一步考虑! - Mikhail Gerasimov

1

1
我知道。但是这是否处理了不存在路径的情况? - franzlorenzon

1
假设sftp操作很耗费资源, 我会选择:
def sftp_mkdir_p(sftp, remote_directory):
    dirs_exist = remote_directory.split('/')
    dirs_make = []
    # find level where dir doesn't exist
    while len(dirs_exist) > 0:
        try:
            sftp.listdir('/'.join(dirs_exist))
            break
        except IOError:
            value = dirs_exist.pop()
            if value == '':
                continue
            dirs_make.append(value)
        else:
            return False
    # ...and create dirs starting from that level
    for mdir in dirs_make[::-1]:
        dirs_exist.append(mdir)
        sftp.mkdir('/'.join(dirs_exist))```

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接