Есть сайт, который хранится в Subversion репозитории. Есть хостинг с ssh или ftp доступом. Задача: безопасно заливать на хостинг новые и измененные файлы сайта.
Для работы с SVN используется модуль pysvn
(пакет python-svn).
Если есть доступ через ssh, будем заливать файлы с помощью
rsync
,
который запускается скриптом как отдельный процесс.
Для работы с FTP используется стандартный модуль ftplib
.
Файл конфигурации скрипта
(~/.svn-rsync/config
)
содержит набор заданий,
при каждом запуске выполняется одно из них:
# Каждая секция - отдельное задание. # Имя задания указывается в качестве аргумента при запуске скрипта. [gelin.ru] # URL SVN репозитория или каталога в репозитории, # должен завершаться слешем. svn_url = file:///home/gelin/svn/web/gelin.ru/trunk/ # Месторасположение файла, где будет хранится # номер последней обработанной ревизии. # Таким образом, скрипт обрабатывает только файлы, # измененные с момента последнего запуска. # Если файл отсутствует, будет автоматически определена ревизия, # в которой был создан данный путь в репозитории. revision_file = ~/.svn-rsync/gelin.ru.rev # Цель синхронизации для rsync. # Содержит имя пользователя, адрес удаленного сервера, каталог. # Подробности смотрите в man rsync. rsync_dest = feshost.ru:public_html/ [test] svn_url = file:///home/gelin/svn/web/test/trunk/ revision_file = ~/.svn-rsync/test.rev # URL FTP сервера. # Содержит имя хоста (или IP адрес) сервера, каталог. # Логин и пароль берется из ~/.netrc, см. man netrc, man ftp. ftp_url = ftp://localhost/public_html/ # Можно указать логин и пароль в URL. #ftp_url = ftp://test:123@localhost/public_html/
Вот сам скрипт:
#!/usr/bin/python # (c) 2008 Denis Nelubin aka Gelin # This script exports last changed files from Subversion repository # and uploads the files to remote server using rsync or ftp import sys import pysvn import os import os.path import tempfile import urlparse import ConfigParser import ftplib import netrc CONFIG = "~/.svn-rsync/config" class Config: def __init__(self, target='default'): self._config = ConfigParser.ConfigParser() self._section = target self._config.read(os.path.expanduser(CONFIG)) def __getattr__(self, name): if name == 'svn_revision': try: return self._read_revision except: try: self._read_revision = int(open( os.path.expanduser(self.revision_file)).read()) except: self._read_revision = 0 return self._read_revision return self._config.get(self._section, name) def save_revision(self, revision): open(os.path.expanduser(self.revision_file), 'wt').write(str(revision)) def targets(self): return self._config.sections() class SVN: def __init__(self, config): self._config = config self._client = pysvn.Client() def export(self, todir): from_rev = self._first_revision() to_rev = self._last_revision() summary = self._summary(from_rev, to_rev) for item in summary: if item['node_kind'] == pysvn.node_kind.file and \ item['summarize_kind'] != pysvn.diff_summarize_kind.delete: print '%(summarize_kind)s\t%(path)s' % item path = item['path'] file = os.path.join(todir, path) try: os.makedirs(os.path.dirname(file)) except: pass #dirs can be created multiple times self._client.export(self._svnurl(path), file, revision=to_rev, recurse=False) def _first_revision(self): if self._config.svn_revision > 0: return pysvn.Revision(pysvn.opt_revision_kind.number, self._config.svn_revision) log = self._client.log(self._config.svn_url, pysvn.Revision(pysvn.opt_revision_kind.number, 0), pysvn.Revision(pysvn.opt_revision_kind.head), limit=1) return log[0]['revision'] def _last_revision(self): info = self._client.info2(self._config.svn_url, recurse=False)[0][1] self.last_revision = info['last_changed_rev'].number return info['last_changed_rev'] def _summary(self, from_rev, to_rev): summary = self._client.diff_summarize(self._config.svn_url, revision1=from_rev, revision2=to_rev) return summary def _svnurl(self, file): return urlparse.urljoin(self._config.svn_url, file) class Uploader: def __init__(self, config): self._config = config def upload(self, fromdir): if hasattr(self._config, 'rsync_dest'): self._rsync(fromdir) elif hasattr(self._config, 'ftp_url'): self._ftp(fromdir) def _rsync(self, fromdir): print 'Syncing to %s' % self._config.rsync_dest status = os.spawnlp(os.P_WAIT, 'rsync', 'rsync', '-rv', os.path.join(fromdir, ''), self._config.rsync_dest) if not status == os.EX_OK: raise OSError def _ftp(self, fromdir): print 'Uploading to %s' % self._config.ftp_url client = self._createFTP(self._config.ftp_url) self._uploadFTP(client, fromdir) def _createFTP(self, url): ftp = ftplib.FTP() urlparts = urlparse.urlparse(url) ftp.connect(urlparts.hostname, urlparts.port) if urlparts.username: ftp.login(urlparts.username, urlparts.password) else: auths = netrc.netrc().authenticators(urlparts.hostname) if auths: ftp.login(auths[0], auths[2], auths[1]) else: ftp.login() ftp.cwd(urlparts.path.strip('/')) return ftp def _uploadFTP(self, client, fromdir): pwd = client.pwd() for root, dirs, files in os.walk(fromdir): relroot = self._relative_path(fromdir, root) client.cwd(relroot) ftpfiles = [] client.dir(lambda(line): ftpfiles.append(self._parse_ftp_dir(line))) for name in files: print os.path.join(relroot, name) client.storbinary('STOR %s' % name, open(os.path.join(root, name))) for name in dirs: if not name in ftpfiles: client.mkd(name) client.cwd(pwd) def _relative_path(self, base, path): (head, tail) = (path, '') result = [] while not os.path.samefile(head, base) and head: (head, tail) = os.path.split(head) result.insert(0, tail) result.append('') return os.path.join(*result) def _parse_ftp_dir(self, line): #only UNIX listing is supported #copied from ftpmirror.py words = line.split(None, 8) if len(words) >= 6: filename = words[-1].lstrip() i = filename.find(" -> ") #symlink support? if i >= 0: filename = filename[:i] return filename def rmdir(directory): for root, dirs, files in os.walk(directory, topdown=False): for name in files: os.remove(os.path.join(root, name)) for name in dirs: os.rmdir(os.path.join(root, name)) os.rmdir(directory) if __name__ == '__main__': if len(sys.argv) < 2: print "Usage: svn-rsync target" print "Available targets:" print "\n".join(Config().targets()) sys.exit(2) config = Config(sys.argv[1]) svn = SVN(config) uploader = Uploader(config) tmpdir = tempfile.mkdtemp('svn-rsync') print 'Exporting from %s -r %i:HEAD' % (config.svn_url, config.svn_revision) svn.export(tmpdir) uploader.upload(tmpdir) config.save_revision(svn.last_revision) rmdir(tmpdir)
Запускаем svn-rsync gelin.ru
и автоматически получаем
обновление нашего сайта.