Начал изучать волшебный язык Python3 и решил испробовать его в действие на своем маленьком
На сервере стоит 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х дней удаляются.
Автор: xelaxela