python

Резервное копирование базы mysql и файлов на удаленный FTP — Python 3

  • суббота, 12 августа 2017 г. в 03:12:35
https://habrahabr.ru/post/335440/
  • Разработка под Linux
  • Python


Начал изучать волшебный язык Python3 и решил испробовать его в действие на своем маленьком VPS.

На сервере стоит Mysql, Apache, nginx… во общем простой стандартный набор, там же хостятся с два десятка клиентских сайтов.

Каждый день делается резервная копия всех баз и файлов доменнов средствами приметного скрипта #!bin/bash

Я решил использовать Python 3… Вот непосредственно и сам код:

#!/usr/bin/env python3

import subprocess
import datetime
import optparse
import zipfile
import os
import ftplib

class ReturnCode(Exception):
    pass


class NotExist(Exception):
    pass


class RequiredOpts(Exception):
    pass


class BackupUtils:
    __current_date = str(datetime.datetime.now().strftime('%d_%m_%Y'))

    def __init__(self):
        self.ftp = None

    def to_zip(self, file, filename=__current_date + '.zip', append_to_file=False):
        """
        :param file: file or folder for added to archive
        :param filename: output archive filename
        :param append_to_file: if False, will be create new file, True for append in exist file
        :type append_to_file: False
        :type filename: str
        :type file: str
        :return True
        """
        param_zip = 'a' if append_to_file else 'w'
        try:
            with zipfile.ZipFile(filename, param_zip) as zip_file:
                if os.path.isfile(file):
                    zip_file.write(file)
                else:
                    self.add_folder_to_zip(zip_file, file)
            return True
        except IOError as error:
            print('Cannot create zip file, error: {}'.format(error))
            return False

    def add_folder_to_zip(self, zip_file, folder):
        """
        :type folder: str
        :type zip_file: file
        """
        for file in os.listdir(folder):
            full_path = os.path.join(folder, file)
            if os.path.isfile(full_path):
                zip_file.write(full_path)
            elif os.path.isdir(full_path):
                self.add_folder_to_zip(zip_file, full_path)

    def run_backup(self, mysql_user, mysql_pw, db):
        """
        :type db: str
        :type mysql_pw: str
        :type mysql_user: str
        :return string - dump filename
        """
        try:
            dump = 'dump_' + db + '_' + self.__current_date + '.sql'
            # return dump
            p = subprocess.Popen(
                'mysqldump -u' + mysql_user + ' -p' + mysql_pw + ' --databases ' + db + ' > ' + dump,
                shell=True)
            # Wait for completion
            p.communicate()
            # Check for errors
            if p.returncode != 0:
                raise ReturnCode
            print('Backup done for', db)
            return dump
        except:
            print('Backup failed for ', db)

    def parse_options(self):
        parser = optparse.OptionParser(usage="""\
        %prog -u USERNAME -p PASSWORD -d DATABASE -D /path/for/domain/ -f BACKUP_FILE_NAME
        Required Username, Password, Database name and path for Domain folder
        If you want copy backup to remote ftp, use options:
        %prog -u USERNAME -p PASSWORD -d DATABASE -D /path/for/domain/ -f BACKUP_FILE_NAME --ftp-host HOST --ftp-user USERNAME --ftp-password PASSWORD --ftp-folder FOLDER
        If you want delete archives from ftp, add options: --ftp-delete-old --ftp-delete-day N (not required, 3 days default) 
        """, conflict_handler="resolve")
        parser.add_option("-u", "--username", dest="username",
                          help=("Username of database "
                                "[default: %default]"))
        parser.add_option("-p", "--password", dest="password",
                          help=("Password of database "
                                "[default: %default]"))
        parser.add_option("-d", "--database", dest="database",
                          help=("Database name "
                                "[default: %default]"))
        parser.add_option("-D", "--domain", dest="domain",
                          help=("Domain folder for backup "
                                "[default: %default]"))
        parser.add_option("-f", "--filename", dest="filename",
                          help=("Backup file name "
                                "[default: %default]"))
        parser.add_option("--ftp-host", dest="host",
                          help=("Ftp host "
                                "[default: %default]"))
        parser.add_option("--ftp-user", dest="ftpuser",
                          help=("Ftp username "
                                "[default: %default]"))
        parser.add_option("--ftp-password", dest="ftppassword",
                          help=("Ftp password "
                                "[default: %default]"))
        parser.add_option("--ftp-folder", dest="folder",
                          help=("Ftp upload folder "
                                "[default: %default]"))
        parser.add_option("--ftp-delete-old", dest="ftpdelete", action='store_true',
                          help=("Delete files from ftp older 3 days "
                                "[default: %default]"))
        parser.add_option("--ftp-delete-day", dest="ftpdeleteday", type='int',
                          help=("Delete files from ftp older N days "
                                "[default: %default]"))
        parser.set_defaults(username='root', filename=self.__current_date + '.zip', folder='.', ftpdelete=False,
                            ftpdeleteday=3)
        return parser.parse_args()

    def ftp_connect(self, host, username, password):
        """
                :param host: remote host name
                :param username: username for remote host
                :param password: password for remote host
                :type host: str
                :type username: str
                :type password: str
                :return object self.ftp
                """
        try:
            self.ftp = ftplib.FTP(host=host, user=username, passwd=password)
            return self.ftp
        except ftplib.error_perm as error:
            print('Is there something wrong: {}'.format(error))
        except:
            print('Cannot connected to ftp: ', host)
            return False

    def ftp_disconnect(self):
        """
        :return: True
        """
        try:
            self.ftp.close()
            self.ftp = None
            return True
        except:
            return False

    def upload_file_to_ftp(self, filename, folder='.'):
        """
        :param filename: upload file name
        :param folder: special folder - / default
        :type filename: str
        :type folder: str
        :return True
        """
        try:
            self.ftp.cwd(folder)
            self.ftp.dir()
            with open(filename, 'rb') as f:
                self.ftp.storbinary('STOR %s' % filename, f)
            return True
        except ftplib.all_errors as error:
            print('Is there something wrong: {}'.format(error))
            return False

    def remove_old_files_from_ftp(self, folder='.', day=3):
        """
                :param folder: special folder - / default
                :param day: count of day
                :type folder: str
                :type day: int
                :return True
                """
        try:
            self.ftp.cwd(folder)
            facts = self.ftp.mlsd()
            i = 0
            for fact in facts:
                modify = fact[1]['modify'][:8]
                if (int(datetime.datetime.now().strftime('%Y%m%d')) - int(modify)) > int(day):
                    # if we cannot change directory - is file
                    try:
                        self.ftp.cwd(fact[0])
                    except:
                        self.ftp.delete(fact[0])
                        i += 1
            print('Deleted {} files'.format(str(i)))
            return True
        except ftplib.all_errors as error:
            print('Is there something wrong: {}'.format(error))
            return False
        except TypeError:
            print('Day is not number, use 1 or 2,3,n')
            return False

Создал простой класс с несколькими методами:

to_zip(self, file, filename=__current_date + '.zip', append_to_file=False)

Метод принимает файл или папку и создает архив с именем ТЕКУЩАЯДАТА.zip или с вашим именем, если передать append_to_file=True, файлы будут добавлены в существующий архив

run_backup(self, mysql_user, mysql_pw, db)

Делаем резервную копию базы данных, использую линуксовскую утилиту mysqldump, метод принимает ИМЯ ПОЛЬЗОВАТЕЛЯ, ПАРОЛЬ, НАЗВАНИЕ БАЗЫ

parse_options(self)

Парсим переданные опции, об этом в примере ниже…

ftp_connect(self, host, username, password)

Открываем FTP соединение, метод принимает ХОСТ, ИМЯ ПОЛЬЗОВАТЕЛЯ, ПАРОЛЬ от FTP сервера

ftp_disconnect(self)

Не понятный метод с не ясным названием )

upload_file_to_ftp(self, filename, folder='.')

Метод принимает ИМЯ ФАЙЛА и опционально ПАПКУ, как раз в нее и копируется ФАЙЛ

remove_old_files_from_ftp(self, folder='.', day=3)

Удаляет все файлы старше N дней с указанной папки, метод принимает соответственно ПАПКУ и ДНИ

А теперь пример того как этот класс использую я:

def main():
    backup_utils = BackupUtils()
    opts, args = backup_utils.parse_options()
    # required Username, password, database name and path for domain folder
    try:
        if opts.username is None or opts.password is None or opts.database is None or opts.domain is None:
            raise RequiredOpts
    except RequiredOpts:
        print('Use -h or --help option')
        exit()

    # create sql dump
    backup_database = backup_utils.run_backup(opts.username, opts.password, opts.database)
    #  dump archive filename
    dump_archive = 'dump_' + opts.filename if '.zip' in opts.filename else 'dump_' + opts.filename + '.zip'

    if backup_database:
        # add sql dump to zip "dump_filename.zip"
        backup_utils.to_zip(backup_database, dump_archive)
        # remove sql dump
        os.remove(backup_database)

    # find domain name in path - site.com
    try:
        i = opts.domain.index('.')
        if opts.domain[:-1] != '/': opts.domain += '/'
        left = opts.domain.rindex('/', 0, i)
        right = opts.domain.index('/', i)
        domain = opts.domain[left + 1:right]
    except:
        domain = ''

    # backup file name
    backup_archive = 'backup_' + domain + '_' + opts.filename if '.zip' in opts.filename else 'backup_' + domain + '_' + opts.filename + '.zip'

    # check if path exist
    try:
        if not os.path.isdir(opts.domain) and not os.path.isfile(opts.domain):
            raise NotExist
    except NotExist:
        print('{} No such file or directory'.format(opts.domain))
        exit()

    # create domain folder archive
    backup_utils.to_zip(opts.domain, backup_archive)

    if os.path.isfile(dump_archive):
        # add dump archive to domain archive
        backup_utils.to_zip(dump_archive, backup_archive, True)
        # remove dump zip file
        os.remove(dump_archive)

        # upload backup to ftp
        if opts.host and opts.ftpuser and opts.ftppassword and backup_utils.ftp_connect(opts.host, opts.ftpuser,
                                                                                        opts.ftppassword) is not None:
            backup_utils.upload_file_to_ftp(backup_archive, folder=opts.folder)
            backup_utils.ftp_disconnect()
            # remove local backup archive
            os.remove(backup_archive)

        # delete files from ftp older N days
        if opts.ftpdelete and backup_utils.ftp_connect(opts.host, opts.ftpuser,
                                                       opts.ftppassword) is not None:
            backup_utils.remove_old_files_from_ftp(folder=opts.folder, day=opts.ftpdeleteday)
            backup_utils.ftp_disconnect()


if __name__ == "__main__":
    main()
  


И напоследок в cron добавляем команду:

backup.py -p PASSWORD FOR DB -d NAME FO DB -D /PATH/FOR/WEB/SITE.COM/HTML/ --ftp-host FTP HOST NAME --ftp-user FTP USER --ftp-password FTP PASSWORD --ftp-delete-old --ftp-delete-day DAYS --ftp-folder FTP FOLDER

Все! Каждый день создается резервная копия базы и файлов проекта и копируется на ftp, и что бы не переполнять ftp сервер все копии старше 3х дней удаляются.