python实现ftp上传下载

背景:
通过jenkins出了linux部署包后需要手动从jenkins下载,并scp到测试环境服务器,再解压替换重启服务等,手动操作太low,又碎片化时间,我打算做成自动化。只需要跑一下jenkins job,几分钟后打开页面验证功能就好了。

  1. 方案1:通过ftp进行上传下载,目前是使用的这种方式

  2. 方案2:通过SimpleHTTPServerWithUpload和SimpleHTTPServer在包所在的机器(jenkins
    node)上起2个服务,通过端口请求进行上传和下载(未写代码实现

*稍麻烦的一点是:需要轮询去判断是否有新的文件产生
搭建ftp服务器,以及踩的一些坑,我下面写。*

方案1:
python实现ftp上传和下载:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
import parser
from ftplib import FTP
import os
import sys
import time
import socket
import datetime
import errno
from dateutil import parser


class MyFTP:
    """
        ftp自动下载、自动上传脚本,可以递归目录操作
    """

    def __init__(self, host, port=21):
        """ 初始化 FTP 客户端
        参数:
                 host:ip地址

                 port:端口号
        """
        # print("__init__()---> host = %s ,port = %s" % (host, port))

        self.host = host
        self.port = port
        self.ftp = FTP()
        # 重新设置下编码方式
        self.ftp.encoding = 'gbk'
        self.log_file = open("log.txt", "a")
        self.file_list = []

    def login(self, username, password):
        """ 初始化 FTP 客户端
            参数:
                  username: uftp2

                 password: uftp2
            """
        try:
            timeout = 600
            socket.setdefaulttimeout(timeout)
            # 0主动模式 1 #被动模式
            self.ftp.set_pasv(True)
            # 打开调试级别2,显示详细信息
            # self.ftp.set_debuglevel(2)

            self.debug_print('开始尝试连接到 %s' % self.host)
            self.ftp.connect(self.host, self.port)
            self.debug_print('成功连接到 %s' % self.host)

            self.debug_print('开始尝试登录到 %s' % self.host)
            self.ftp.login(username, password)
            self.debug_print('成功登录到 %s' % self.host)

            self.debug_print(self.ftp.welcome)
        except Exception as err:
            self.deal_error("FTP 连接或登录失败 ,错误描述为:%s" % err)
            pass

    def formatSize(self, bytes):
        """
         # 字节bytes转化kb\m\g
        :return: str
        """
        try:
            bytes = float(bytes)
            kb = bytes / 1024
        except:
            print("传入的字节格式不对")
            return "Error"

        if kb >= 1024:
            M = kb / 1024
            if M >= 1024:
                G = M / 1024
                return "%fG" % (G)
            else:
                return "%fM" % (M)
        else:
            return "%fkb" % (kb)

    def is_same_size(self, local_file, remote_file):
        """判断远程文件和本地文件大小是否一致

           参数:
             local_file: 本地文件

             remote_file: 远程文件
        """
        try:
            ssize = self.ftp.size(remote_file)
            remote_file_size = self.formatSize(ssize)
        except Exception as err:
            # self.debug_print("is_same_size() 错误描述为:%s" % err)
            remote_file_size = -1

        try:
            local_file_size = self.formatSize(os.path.getsize(local_file))
        except Exception as err:
            # self.debug_print("is_same_size() 错误描述为:%s" % err)
            local_file_size = -1

        self.debug_print('local_file_size:%s  , remote_file_size:%s' % (local_file_size, remote_file_size))
        if remote_file_size == local_file_size:
            return 1
        else:
            return 0

    def download_file(self, local_file, remote_file):
        """从ftp下载文件
            参数:
                local_file: 本地文件

                remote_file: 远程文件
        """
        self.debug_print("download_file()---> local_path = %s ,remote_path = %s" % (local_file, remote_file))

        if self.is_same_size(local_file, remote_file):
            self.debug_print('%s 文件大小相同,无需下载' % local_file)
            return
        else:
            try:
                self.debug_print('>>>>>>>>>>>>下载文件 %s 到 %s ... ...' % (remote_file, local_file))
                # buf_size = 1024
                file_handler = open(local_file, 'wb')
                self.ftp.retrbinary('RETR %s' % remote_file, file_handler.write)
                file_handler.close()
                self.debug_print('>>>>>>>>>>>>下载完成  ... ...')
            except Exception as err:
                self.debug_print('下载文件出错,出现异常:%s ' % err)
                return

    def download_file_tree(self, local_path, remote_path):
        """从远程目录下载多个文件到本地目录
                       参数:
                         local_path: 本地路径

                         remote_path: 远程路径
                """
        print("download_file_tree()--->  local_path = %s ,remote_path = %s" % (local_path, remote_path))
        try:
            self.ftp.cwd(remote_path)
        except Exception as err:
            self.debug_print('远程目录%s不存在,继续...' % remote_path + " ,具体错误描述为:%s" % err)
            return

        if not os.path.isdir(local_path):
            self.debug_print('本地目录%s不存在,先创建本地目录' % local_path)
            os.makedirs(local_path)

        self.debug_print('切换至目录: %s' % self.ftp.pwd())

        self.file_list = []
        # 方法回调
        self.ftp.dir(self.get_file_list)

        remote_names = self.file_list
        self.debug_print('远程目录 列表: %s' % remote_names)
        for item in remote_names:
            file_type = item[0]
            file_name = item[1]
            local = os.path.join(local_path, file_name)
            if file_type == 'd':
                print("download_file_tree()---> 下载目录: %s" % file_name)
                self.download_file_tree(local, file_name)
            elif file_type == '-':
                print("download_file()---> 下载文件: %s" % file_name)
                self.download_file(local, file_name)
            self.ftp.cwd("..")
            self.debug_print('返回上层目录 %s' % self.ftp.pwd())
        return True

    def upload_file(self, local_file, remote_file):
        """从本地上传文件到ftp

           参数:
             local_path: 本地文件

             remote_path: 远程文件
        """
        if not os.path.isfile(local_file):
            self.debug_print('%s 不存在' % local_file)
            return

        if self.is_same_size(local_file, remote_file):
            self.debug_print('跳过相等的文件: %s' % local_file)
            return

        buf_size = 2048
        file_handler = open(local_file, 'rb')
        try:
            self.ftp.storbinary('STOR %s' % remote_file, file_handler, buf_size)
            file_handler.close()
        except IOError as e:
            if e.errno == errno.EPIPE:
                print(e.errno)
        self.debug_print('上传: %s' % local_file + "成功!")

    def upload_file_tree(self, local_path, remote_path):
        """从本地上传目录下多个文件到ftp
           参数:

             local_path: 本地路径

             remote_path: 远程路径
        """
        if not os.path.isdir(local_path):
            self.debug_print('本地目录 %s 不存在' % local_path)
            return

        self.ftp.cwd(remote_path)
        self.debug_print('切换至远程目录: %s' % self.ftp.pwd())

        local_name_list = os.listdir(local_path)
        for local_name in local_name_list:
            src = os.path.join(local_path, local_name)
            if os.path.isdir(src):
                try:
                    self.ftp.mkd(local_name)
                except Exception as err:
                    self.debug_print("目录已存在 %s ,具体错误描述为:%s" % (local_name, err))
                self.debug_print("upload_file_tree()---> 上传目录: %s" % local_name)
                self.upload_file_tree(src, local_name)
            else:
                self.debug_print("upload_file_tree()---> 上传文件: %s" % local_name)
                self.upload_file(src, local_name)
        self.ftp.cwd("..")

    def close(self):
        """ 退出ftp
        """
        self.debug_print("close()---> FTP退出")
        self.ftp.quit()
        self.log_file.close()

    def debug_print(self, s):
        """ 打印日志
        """
        self.write_log(s)

    def deal_error(self, e):
        """ 处理错误异常
            参数:
                e:异常
        """
        log_str = '发生错误: %s' % e
        self.write_log(log_str)
        sys.exit()

    def write_log(self, log_str):
        """ 记录日志
            参数:
                log_str:日志
        """
        time_now = time.localtime()
        date_now = time.strftime('%Y-%m-%d %H:%M:%S', time_now)
        format_log_str = "%s ---> %s \n " % (date_now, log_str)
        print(format_log_str)
        self.log_file.write(format_log_str)

    def get_file_list(self, line):
        """ 获取文件列表
            参数:
                line:
        """
        file_arr = self.get_file_name(line)
        # 去除  . 和  ..
        if file_arr[1] not in ['.', '..']:
            self.file_list.append(file_arr)

    def get_file_name(self, line):
        """ 获取文件名
            参数:
                line:
        """
        pos = line.rfind(':')
        while (line[pos] != ' '):
            pos += 1
        while (line[pos] == ' '):
            pos += 1
        file_arr = [line[0], line[pos:]]
        return file_arr

    def get_upload_file_exist(self):
        # path_current = "/disk1/jenkins-home/workspace/workspace/xxxx-staging-build2/current"
        files = os.listdir(path_current)  # 得到文件夹下的所有文件名称
        for file in files:
            if file.__contains__("linux"):
                path_file = path_current + "/" + file
                file_create_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getctime(path_file)))

                m2_tmp = str(datetime.datetime.now() + datetime.timedelta(minutes=-2000))
                index = m2_tmp.find('.')  # 第一次出现的位置
                m2 = m2_tmp[:index]

                if file_create_time >= m2:
                    print("%s 创建时间: %s" % (file, file_create_time))
                    return path_file, file

        print(str(datetime.datetime.now()) + " 没有新文件产生")
        return "", ""

    def get_file_form_ftp(self):
        self.ftp.cwd("/package")  # 设置FTP当前操作的路径
        files = self.ftp.nlst()
        for file in files:
            if file.__contains__("linux"):
                timestamp = self.ftp.voidcmd("MDTM /package/" + file)[4:].strip()
                time = parser.parse(timestamp)

                m2_tmp = str(datetime.datetime.now() + datetime.timedelta(minutes=-3))
                index = m2_tmp.find('.')  # 第一次出现的位置
                m2 = m2_tmp[:index]

                print("%s 修改时间: %s" % (file, time))
                if str(time) >= m2:
                    return file
        return ""


if __name__ == "__main__":
    my_ftp = MyFTP("10.200.20.xx")
    my_ftp.login("uftp", "uftp")

    # 上传
    while True:
        path_file, file = my_ftp.get_upload_file_exist()
        if path_file:
            print(path_file)
            my_ftp.upload_file(path_file, "/package/" + file)
        time.sleep(120)  # 每2分钟轮询一次

    # 下载
    # while True:
    #     file = my_ftp.get_file_form_ftp()
    #     if file:
    #         # 下载单个文件
    #         my_ftp.download_file("/home/qboxserver/juncheng/" + file, "/package/" + file, )
    #         # os.system('bash update_pandora.sh ' + file)
    #     time.sleep(120)  # 每2分钟轮询一次

参考了这篇文章,做了一些适配修改,感谢大神!https://blog.csdn.net/ouyang_peng/article/details/79271113

方案2:

# 起个服务,可以通过这个端口访问和下载wget该服务器上的内容
python -m SimpleHTTPServer <port>

#把下面的py脚本放到机器上,启动,通过该接口进行上传文件。
# 可以通过浏览器打开进行手动upload文件,python发请求还没试,抽空我试一下
python SimpleHTTPServerWithUpload.py <port>

SimpleHTTPServerWithUpload.py

#!/usr/bin/env python  

"""Simple HTTP Server With Upload. 

This module builds on BaseHTTPServer by implementing the standard GET 
and HEAD requests in a fairly straightforward manner. 

"""  


__version__ = "0.1"  
__all__ = ["SimpleHTTPRequestHandler"]  
__author__ = "bones7456"  
__home_page__ = "http://luy.li/"  

import os  
import posixpath  
import BaseHTTPServer  
import urllib  
import cgi  
import shutil  
import mimetypes  
import re  
try:  
    from cStringIO import StringIO  
except ImportError:  
    from StringIO import StringIO  


class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):  

    """Simple HTTP request handler with GET/HEAD/POST commands. 

    This serves files from the current directory and any of its 
    subdirectories.  The MIME type for files is determined by 
    calling the .guess_type() method. And can reveive file uploaded 
    by client. 

    The GET/HEAD/POST requests are identical except that the HEAD 
    request omits the actual contents of the file. 

    """  

    server_version = "SimpleHTTPWithUpload/" + __version__  

    def do_GET(self):  
        """Serve a GET request."""  
        f = self.send_head()  
        if f:  
            self.copyfile(f, self.wfile)  
            f.close()  

    def do_HEAD(self):  
        """Serve a HEAD request."""  
        f = self.send_head()  
        if f:  
            f.close()  

    def do_POST(self):  
        """Serve a POST request."""  
        r, info = self.deal_post_data()  
        print r, info, "by: ", self.client_address  
        f = StringIO()  
        f.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">')  
        f.write("<html>\n<title>Upload Result Page</title>\n")  
        f.write("<body>\n<h2>Upload Result Page</h2>\n")  
        f.write("<hr>\n")  
        if r:  
            f.write("<strong>Success:</strong>")  
        else:  
            f.write("<strong>Failed:</strong>")  
        f.write(info)  
        f.write("<br><a href=\"%s\">back</a>" % self.headers['referer'])  
        f.write("<hr><small>Powered By: bones7456, check new version at ")  
        f.write("<a href=\"http://luy.li/?s=SimpleHTTPServerWithUpload\">")  
        f.write("here</a>.</small></body>\n</html>\n")  
        length = f.tell()  
        f.seek(0)  
        self.send_response(200)  
        self.send_header("Content-type", "text/html")  
        self.send_header("Content-Length", str(length))  
        self.end_headers()  
        if f:  
            self.copyfile(f, self.wfile)  
            f.close()  

    def deal_post_data(self):  
        boundary = self.headers.plisttext.split("=")[1]  
        remainbytes = int(self.headers['content-length'])  
        line = self.rfile.readline()  
        remainbytes -= len(line)  
        if not boundary in line:  
            return (False, "Content NOT begin with boundary")  
        line = self.rfile.readline()  
        remainbytes -= len(line)  
        fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line)  
        if not fn:  
            return (False, "Can't find out file name...")  
        path = self.translate_path(self.path)  
        fn = os.path.join(path, fn[0])  
        while os.path.exists(fn):  
            fn += "_"  
        line = self.rfile.readline()  
        remainbytes -= len(line)  
        line = self.rfile.readline()  
        remainbytes -= len(line)  
        try:  
            out = open(fn, 'wb')  
        except IOError:  
            return (False, "Can't create file to write, do you have permission to write?")  

        preline = self.rfile.readline()  
        remainbytes -= len(preline)  
        while remainbytes > 0:  
            line = self.rfile.readline()  
            remainbytes -= len(line)  
            if boundary in line:  
                preline = preline[0:-1]  
                if preline.endswith('\r'):  
                    preline = preline[0:-1]  
                out.write(preline)  
                out.close()  
                return (True, "File '%s' upload success!" % fn)  
            else:  
                out.write(preline)  
                preline = line  
        return (False, "Unexpect Ends of data.")  

    def send_head(self):  
        """Common code for GET and HEAD commands. 

        This sends the response code and MIME headers. 

        Return value is either a file object (which has to be copied 
        to the outputfile by the caller unless the command was HEAD, 
        and must be closed by the caller under all circumstances), or 
        None, in which case the caller has nothing further to do. 

        """  
        path = self.translate_path(self.path)  
        f = None  
        if os.path.isdir(path):  
            if not self.path.endswith('/'):  
                # redirect browser - doing basically what apache does  
                self.send_response(301)  
                self.send_header("Location", self.path + "/")  
                self.end_headers()  
                return None  
            for index in "index.html", "index.htm":  
                index = os.path.join(path, index)  
                if os.path.exists(index):  
                    path = index  
                    break  
            else:  
                return self.list_directory(path)  
        ctype = self.guess_type(path)  
        try:  
            # Always read in binary mode. Opening files in text mode may cause  
            # newline translations, making the actual size of the content  
            # transmitted *less* than the content-length!  
            f = open(path, 'rb')  
        except IOError:  
            self.send_error(404, "File not found")  
            return None  
        self.send_response(200)  
        self.send_header("Content-type", ctype)  
        fs = os.fstat(f.fileno())  
        self.send_header("Content-Length", str(fs[6]))  
        self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))  
        self.end_headers()  
        return f  

    def list_directory(self, path):  
        """Helper to produce a directory listing (absent index.html). 

        Return value is either a file object, or None (indicating an 
        error).  In either case, the headers are sent, making the 
        interface the same as for send_head(). 

        """  
        try:  
            list = os.listdir(path)  
        except os.error:  
            self.send_error(404, "No permission to list directory")  
            return None  
        list.sort(key=lambda a: a.lower())  
        f = StringIO()  
        displaypath = cgi.escape(urllib.unquote(self.path))  
        f.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">')  
        f.write("<html>\n<title>Directory listing for %s</title>\n" % displaypath)  
        f.write("<body>\n<h2>Directory listing for %s</h2>\n" % displaypath)  
        f.write("<hr>\n")  
        f.write("<form ENCTYPE=\"multipart/form-data\" method=\"post\">")  
        f.write("<input name=\"file\" type=\"file\"/>")  
        f.write("<input type=\"submit\" value=\"upload\"/></form>\n")  
        f.write("<hr>\n<ul>\n")  
        for name in list:  
            fullname = os.path.join(path, name)  
            displayname = linkname = name  
            # Append / for directories or @ for symbolic links  
            if os.path.isdir(fullname):  
                displayname = name + "/"  
                linkname = name + "/"  
            if os.path.islink(fullname):  
                displayname = name + "@"  
                # Note: a link to a directory displays with @ and links with /  
            f.write('<li><a href="%s">%s</a>\n'  
                    % (urllib.quote(linkname), cgi.escape(displayname)))  
        f.write("</ul>\n<hr>\n</body>\n</html>\n")  
        length = f.tell()  
        f.seek(0)  
        self.send_response(200)  
        self.send_header("Content-type", "text/html")  
        self.send_header("Content-Length", str(length))  
        self.end_headers()  
        return f  

    def translate_path(self, path):  
        """Translate a /-separated PATH to the local filename syntax. 

        Components that mean special things to the local file system 
        (e.g. drive or directory names) are ignored.  (XXX They should 
        probably be diagnosed.) 

        """  
        # abandon query parameters  
        path = path.split('?',1)[0]  
        path = path.split('#',1)[0]  
        path = posixpath.normpath(urllib.unquote(path))  
        words = path.split('/')  
        words = filter(None, words)  
        path = os.getcwd()  
        for word in words:  
            drive, word = os.path.splitdrive(word)  
            head, word = os.path.split(word)  
            if word in (os.curdir, os.pardir): continue  
            path = os.path.join(path, word)  
        return path  

    def copyfile(self, source, outputfile):  
        """Copy all data between two file objects. 

        The SOURCE argument is a file object open for reading 
        (or anything with a read() method) and the DESTINATION 
        argument is a file object open for writing (or 
        anything with a write() method). 

        The only reason for overriding this would be to change 
        the block size or perhaps to replace newlines by CRLF 
        -- note however that this the default server uses this 
        to copy binary data as well. 

        """  
        shutil.copyfileobj(source, outputfile)  

    def guess_type(self, path):  
        """Guess the type of a file. 

        Argument is a PATH (a filename). 

        Return value is a string of the form type/subtype, 
        usable for a MIME Content-type header. 

        The default implementation looks the file's extension 
        up in the table self.extensions_map, using application/octet-stream 
        as a default; however it would be permissible (if 
        slow) to look inside the data to make a better guess. 

        """  

        base, ext = posixpath.splitext(path)  
        if ext in self.extensions_map:  
            return self.extensions_map[ext]  
        ext = ext.lower()  
        if ext in self.extensions_map:  
            return self.extensions_map[ext]  
        else:  
            return self.extensions_map['']  

    if not mimetypes.inited:  
        mimetypes.init() # try to read system mime.types  
    extensions_map = mimetypes.types_map.copy()  
    extensions_map.update({  
        '': 'application/octet-stream', # Default  
        '.py': 'text/plain',  
        '.c': 'text/plain',  
        '.h': 'text/plain',  
        })  


def test(HandlerClass = SimpleHTTPRequestHandler,  
         ServerClass = BaseHTTPServer.HTTPServer):  
    BaseHTTPServer.test(HandlerClass, ServerClass)  

if __name__ == '__main__':  
    test()  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值