Комплексная автоматизация резервного копирования баз данных Firebird/InterBase на Windows-серверах

image
Приведенный ниже материал может быть полезен начинающим администраторам баз данных, которые осознали важность создания системы резервного копирования, но пока не разжились собственными скриптами или утилитами. Ниже я приведу скрипт в виде командного bat-файла, используемого в нашей организации для автоматизации снятия бекапов с баз данных Firebird, разберу его ключевые фрагменты и приведу примеры использования. Он также подойдет для баз данных Interbase или Yaffil, но полагаю, что с минимальной переделкой вызова утилиты-бекапера, его можно адаптировать практически для любой СУБД.

Прежде всего, я привожу пару ссылок, по которым можно и нужно ознакомиться с теорией вопроса:
firebirdsql.org: Firebird's gbak Backup and Restore tool (англ.)
ibase.ru: Утилита GBAK, Firebird и InterBase

Немного о терминах:
  • Бекап (b) — процесс снятия резервной копии с БД. На выходе имеем файл специального формата, который не является БД, но который можно развернуть в новую БД. Обычно он много легче, чем исходный файл БД за счет отсутствия тел индексов, не использования резервирования пространства на страницах данных, отсутствия «мусорных» записей и прочих факторов, играющих на уплотнение.
  • Восстановление, или рестор (r) — процесс создания новой БД из файла резервной копии. Полученная БД будет эквивалентна исходной на момент старта бекапа. На этапе восстановления дополнительно можно сменить владельца БД, изменить размер страницы БД, задать новый размер кеша. В скриптах рестор зачастую играет вспомогательную роль — проверка того, что файл бекапа получен корректный и пригодный для восстановления БД.
  • b/r — совокупность процессов бекапа и восстановления, в результате которого мы получаем свежую, незамусореную базу, со сброшенными счетчиками транзакций, и занимающую минимальное пространство на диске. Если кто не знал: БД Firebird/InterBase могут лишь расти в размере, но никогда не возвращают в файловую систему «лишние» страницы. Поэтому b/r — единственный способ «похудеть» для БД, если вас это беспокоит. Также b/r — крайне желательный вариант при переносе БД между серверами, в особенности если речь идет о разных ОС, и тем более о разных архитектурах железа.

Итак, какие мероприятия составляют процесс резервного копирования БД, если подойти к вопросу комплексно, а не ограничиваться лишь запуском gbak.exe с нужными ключами? Я бы выделил следующие основные:
  1. Проверка, что каталог назначения существует, и при необходимости создание его (не всякие утилиты создадут структуру каталогов, если окажется, что выходной файл нужно разместить в пока несуществующем каталоге).
  2. Снятие резервной копии с БД средствами штатной утилиты из комплекта поставки сервера.
  3. Опциональное тестовое восстановление резервной копии (увы, но изредка бывают случаи, когда сервер создал файл резервной копии, но из-за ошибок в метаданных не смог её восстановить в новую БД).
  4. Опциональное сжатие файла резервной копии архиватором.
  5. Опциональный контроль хранимых резервных копий по количеству файлов и/или по занимаемому ими пространству.
  6. Уведомление администратора о любого рода сбоях, произошедших на предыдущих шагах, посредством электронной почты, транспорта NET SEND или другими способами. Желательно также ведение накопительного журнала сбоев.

Наш скрипт конечно же умеет делать все перечисленное, но также может и кое-что еще:
  • Работа с любыми БД: локальными или удаленными; с полностью специфицированным путем, или через алиас; с опциональным указанием порта, если используется нестандартный.
  • В случае локальной БД используется бекап через сервисы, что позволяет ощутимо сократить время операции. Тестовый рестор всегда выполняется на локальной машине, и происходит через сервисы.
  • По умолчанию отключена сборка мусора в исходной БД на этапе бекапа (есть опция включения), что позволяет сократить время операции.
  • В качестве архиватора резервной копии используется RAR, причем задается степень сжатия (а следовательно и скорость операции) от 1 (малое сжатие, лучшая скорость) до 5 (лучшее сжатие, долгое время). Значение сжатия 0 означает «не архивировать, сохранить файл бекапа в неизменном виде». Вы можете легко заменить вызовы rar на другой архиватор, например, свободный 7zip. Но rar пока заметно быстрее в наших тестах.
  • Скрипту можно передать неопределенное кол-во файлов (возможно через маску), которые следует добавить в конечный архив. Мы пользуемся этой возможностью, чтобы с каждым бекапом базы лежал исполняемый файл прикладной программы, которая обслуживает эту базу. Полезная штука, когда приходится подымать архив 6-12 месячной давности: сразу под рукой оказывается актуальная для старой БД программа, которая умеет работать с той версией метаданных.
  • Есть опциональная возможность складывать логи b/r внутрь архива с бекапом. На случай «разбора полетов» постфактум.
  • Количество хранимых бекапов, если не переопределено, — 30 шт. При достижении заданного количества копий в папке назначения, самые старые бекапы будут удаляться, освобождая место новым. Выставив значение параметра в 0, можно отключить контроль количества бекапов.
  • Объем хранимых бекапов не контролируется по умолчанию, но может быть задан в байтах, Кб, Мб или Гб.
  • b/r по умолчанию выполняется от имени встроенного суперпользователя SYSDBA. Пароль SYSDBA локального сервера хранится в исходнике. А для выполнения бекапа по сети, скрипту можно передать параметр с паролем SYSDBA удаленного сервера. В особо пароноидальных случаях пароль SYSDBA локального сервера можно стереть из исходника и также передавать параметром при вызове.
  • В качестве мейлера используется небезызвестная среди сисадминов утилита Blat.
  • Выходной файл (архив или обычный файл бекапа, если сжатие отключено) содержит метку даты и времени своего создания, что позволяет зрительно ориентироваться в каталоге с бекапами.

Вступление затянулось; под спойлером привожу полный исходник скрипта.
исходник fb_backup.bat
@CLS
@ECHO OFF
ECHO #=============================================================================#
ECHO #                                                                             #
ECHO # Firebird/InterBase database backup, test restore, zip and rotate script     #
ECHO # Ver 3.2.8 (26.01.2013)                                                      #
ECHO #                                                                             #
ECHO # Author: arni (email:arnisoft at rambler dot ru)                             #
ECHO #                                                                             #
ECHO # Format:                                                                     #
ECHO # FB_BACKUP [host[/port]:][path]db_file_or_alias  result_dir                  #
ECHO #           [/count:backup_count]  [/space:backup_space_limit]  [/gc]         #
ECHO #           [/restore]  [/compress:level]  [/password:SYSDBA_password]        #
ECHO #           [other_files_to_compress [...]]                                   #
ECHO #                                                                             #
ECHO # Input params:                                                               #
ECHO # [host[/port]:][path]      : local or network, full-specified path or alias  #
ECHO #   db_file_or_alias            to the source database                        #
ECHO # result_dir                : result backup collecting directory              #
ECHO # /count:backup_count       : backup file number to keep (30 by default)      #
ECHO # /space:backup_space_limit : total backup size in bytes (not use by default) #
ECHO #                               you can use suffixes K, M or G.               #
ECHO # /gc                       : need to collect garbage in DB (OFF by default)  #
ECHO # /restore                  : need to do test restore (OFF by default)        #
ECHO # /compress:level           : compress ratio for RAR (2 by default):          #
ECHO #                               0: not compress, 1: fastest, 2: fast,         #
ECHO #                               3: normal, 4: good, 5: best                   #
ECHO # /password:SYSDBA_password : optional SYSDBA password for remote server      #
ECHO #                               (by default uses one from the source code)    #
ECHO # other_files_to_compress   : list of files that must be add to archive       #
ECHO #                                                                             #
ECHO #=============================================================================#


REM ==== Server ====================================================================
SET gbak="C:\Programs\FB25\bin\gbak.exe"
SET ISC_USER=SYSDBA
SET ISC_PASSWORD_LOCAL=masterkey
SET ISC_PASSWORD_REMOTE=


REM ==== Backup/restore preferences ================================================
SET temp_backup_dir=%TEMP%
SET temp_restore_dir=%TEMP%
SET backup_count=30
SET backup_space_limit=0
SET backup_ext=fbk
SET garbage_collection=-g
SET restore=0


REM ==== RAR =======================================================================
SET rar="C:\Program Files\WinRAR\rar.exe"
SET rar_options=a -y -ep -idcd
SET rar_password=
SET rar_compress_ratio=2


REM ==== Mailer (see "Blat" at http://sourceforge.net/projects/blat) ===============
SET blat="C:\Programs\Blat307\blat.exe"
SET smtp_server=smtp.mailserver.ru
SET mail_sender=foo@mailserver.ru
SET mail_login=foo
SET mail_password=1234
SET mail_receiver=
SET mail_subject=Fail while database b/r


REM ==== Other preferences =========================================================
SET include_logs_to_archive=1
SET net_send_receiver=
SET error_log=


REM ==== Define database location ==================================================
SET full_db_specification=%~1
ECHO full_db_specification   = %full_db_specification%
REM Devide DB spec to network and local parts
FOR /f "DELIMS=: TOKENS=1*" %%i IN ("%full_db_specification%") DO (
  SET network=%%i
  SET local=%%j
)
REM Test if spec. is alias with no network part
IF "%local%" == "" (
  SET network=
  SET local=%full_db_specification%
)
REM Test if spec. is full specified file with no network part
FOR /f "DELIMS=\ TOKENS=*" %%i IN ("%local%") DO IF "%local%" == "\%%i" (
  SET network=
  SET local=%full_db_specification%
)
ECHO network_specification   = %network%
IF "%network%" GTR "" (
  REM Extract port from network spec (if exists)
  FOR /f "DELIMS=/ TOKENS=1*" %%i IN ("%network%") DO (
    SET host=%%i
    SET port=%%j
    IF "%%j" GTR "" (
      ECHO network_host            = %%i
      ECHO network_port            = %%j
    )
  )
)

REM Test if DB is local or remote
SET service_mgr_host=localhost
SET is_local_db=1
IF "%network%" GTR "" IF "%host%" NEQ "127.0.0.1" IF /i "%host%" NEQ "localhost" SET is_local_db=0
IF %is_local_db% == 1 IF "%network%" GTR "" SET service_mgr_host=%network%

ECHO local_db_specification  = %local%
IF "%local%" == "" (
  SET fail=Param #1 {DB specification} missing!
  GOTO finish
)
REM Extract file (or alias) from local part of spec.
FOR /f %%i IN ("%local%") DO (
  SET local_path=%%~dpi
  SET local_file_or_alias=%%~nxi
)
IF "%local%" NEQ "%local_file_or_alias%" (
  ECHO local_path              = %local_path%
  REM Check DB file exists for local, not aliased specification
  IF %is_local_db% == 1 IF NOT EXIST "%local%" (
    SET fail=Local DB file %local% not found!
    GOTO finish
  )
)
ECHO local_db_file_or_alias  = %local_file_or_alias%


REM ==== Define result directory ===================================================
SET result_dir=%~2
ECHO result_dir              = %result_dir%
IF "%result_dir%" == "" (
  SET fail=Param #2 {backup collecting directory} missing!
  GOTO finish
)
REM Cut the result dir if it is in path-style (ends with separator)
IF "%result_dir:~-1%" == "\" SET result_dir=%result_dir:~0,-1%
REM Try to create the result directory if it is not exists yet
IF NOT EXIST "%result_dir%" (
  MD "%result_dir%"
  IF NOT EXIST "%result_dir%" (
    SET fail=Cannot create backup collecting directory!
    GOTO finish
  )
)
REM Test if it is local or remote directory (elementary, may get wrong answer)
SET is_local_result_dir=1
IF "%result_dir:~0,2%" == "\\" SET is_local_result_dir=0


REM ==== Use other command line options ============================================
:loop_options
SHIFT
SET next_param=%~2
IF "%next_param%" == "" GOTO print_options
SET prefix=%next_param:~0,1%
IF "%prefix%" == "/" SET next_param=%next_param:~1%
IF "%prefix%" == "-" SET next_param=%next_param:~1%
IF "%next_param%" GTR "" (
  FOR /f "DELIMS=: TOKENS=1*" %%i IN ("%next_param%") DO (
    SET value=%%j
    IF /i "%%i" == "count" IF "%%j" GTR "" GOTO count
    IF /i "%%i" == "space" IF "%%j" GTR "" GOTO space
    IF /i "%%i" == "gc" GOTO gc
    IF /i "%%i" == "restore" GOTO restore
    IF /i "%%i" == "compress" IF "%%j" GTR "" GOTO compress
    IF /i "%%i" == "password" IF "%%j" GTR "" GOTO password
    IF EXIST "%next_param%" GOTO add_file_to_compress
    IF "%prefix%" GTR "/" GOTO add_file_to_compress
    ECHO unknown param found: %next_param%
    GOTO loop_options
  )
) ELSE (
  ECHO empty param found!
  GOTO loop_options
)


REM ==== Define file count in the result dir =======================================
:count
SET /a backup_count=0+%value%
GOTO loop_options


REM ==== Define allowed backup space limit =========================================
:space
SET suffix=%value:~-1%
IF "%suffix%" GTR "9" (
  SET value=%value:~0,-1%
  IF /i "%suffix%" == "K" (
    SET file_size_shift=0
    SET /a value*=1000
  )
  IF /i "%suffix%" == "M" (
    SET file_size_shift=3
    SET /a value*=1000
  )
  IF /i "%suffix%" == "G" (
    SET file_size_shift=6
    SET /a value*=1000
  )
)
SET /a backup_space_limit=0+%value%
GOTO loop_options


REM ==== Define need of garbage collection =========================================
:gc
SET garbage_collection=
IF "%value%" == "0" SET garbage_collection=-g
IF /i "%value%" == "N" SET garbage_collection=-g
IF /i "%value%" == "NO" SET garbage_collection=-g
IF /i "%value%" == "OFF" SET garbage_collection=-g
GOTO loop_options


REM ==== Define need of test restore ===============================================
:restore
SET restore=1
IF "%value%" == "0" SET restore=0
IF /i "%value%" == "N" SET restore=0
IF /i "%value%" == "NO" SET restore=0
IF /i "%value%" == "OFF" SET restore=0
GOTO loop_options


REM ==== Define need of backup compression and compress ratio ======================
:compress
IF "%value%" GEQ "0" IF "%value%" LEQ "5" SET rar_compress_ratio=%value%
GOTO loop_options


REM ==== Define SYSDBA password (in addition or for replace source code given) =====
:password
if "%ISC_PASSWORD_LOCAL%" GTR "" if "%ISC_PASSWORD_REMOTE%" GTR "" (
  SET ISC_PASSWORD_LOCAL=%value%
  SET ISC_PASSWORD_REMOTE=%value%
)
if "%ISC_PASSWORD_LOCAL%" == "" SET ISC_PASSWORD_LOCAL=%value%
if "%ISC_PASSWORD_REMOTE%" == "" SET ISC_PASSWORD_REMOTE=%value%
GOTO loop_options


REM ==== Define file list to compress (in addition to backup and maybe logs) =======
:add_file_to_compress
IF "%backup_files%" == "" (
  SET backup_files="%next_param%"
) ELSE (
  SET backup_files=%backup_files% "%next_param%"
)
GOTO loop_options


REM ==== Print predefined or recognized in command line options ====================
:print_options
IF %backup_count% GTR 0 (
  ECHO backup_count            = %backup_count%
) ELSE (
  ECHO backup_count            = OFF
)
IF "%file_size_shift%" == "" SET file_size_shift=0
IF %backup_space_limit% GTR 0 (
  IF %file_size_shift% == 6 (
    ECHO backup_space_limit      = %backup_space_limit% Mb
  ) ELSE IF %file_size_shift% == 3 (
    ECHO backup_space_limit      = %backup_space_limit% Kb
  ) ELSE ECHO backup_space_limit      = %backup_space_limit% bytes
) ELSE (
  ECHO backup_space_limit      = OFF
)
IF "%garbage_collection%" == "-g" (
  ECHO garbage_collection_flag = OFF
) ELSE (
  ECHO garbage_collection_flag = ON
)
IF %restore% == 0 (
  ECHO test_restore_flag       = OFF
) ELSE (
  ECHO test_restore_flag       = ON
)
IF %rar_compress_ratio% == 0 (
  ECHO backup_compressing      = OFF
) ELSE (
  ECHO backup_compressing      = ON, RAR-ratio=%rar_compress_ratio%
)


REM ==== Define backup file and backup log =========================================
SET datetime=%date:~-2%%date:~3,2%%date:~0,2%_%time:~0,2%%time:~3,2%
SET finish_file=%result_dir%\%local_file_or_alias%.%datetime: =0%.%backup_ext%
SET direct_backup=0
IF %rar_compress_ratio% == 0 (
  IF %is_local_result_dir% == 1 SET direct_backup=1
  IF %restore% == 0 SET direct_backup=1
)
IF %direct_backup% == 1 (
  SET backup_file=%finish_file%
) ELSE (
  SET backup_file=%temp_backup_dir%\%local_file_or_alias%.%backup_ext%
  IF NOT EXIST "%temp_backup_dir%" (
    MD "%temp_backup_dir%"
    IF NOT EXIST "%temp_backup_dir%" (
      SET fail=Cannot create backup directory!
      GOTO finish
    )
  )
)
ECHO backup_file             = %backup_file%
SET backup_log=%result_dir%\%local_file_or_alias%.backup.log
ECHO backup_log              = %backup_log%


REM ==== Define restore file and restore log =======================================
SET restore_file=%temp_restore_dir%\%local_file_or_alias%.testrest
IF %restore% GTR 0 (
  IF NOT EXIST "%temp_restore_dir%" (
    MD "%temp_restore_dir%"
    IF NOT EXIST "%temp_restore_dir%" (
      ECHO temp_restore_dir             = %temp_restore_dir%
      SET fail=Cannot create restore directory!
      GOTO finish
    )
  )
  ECHO restore_file            = %restore_file%
  SET restore_log=%result_dir%\%local_file_or_alias%.restore.log
)
IF "%restore_log%" GTR "" (
  ECHO restore_log             = %restore_log%
) else (
  SET restore_log=just_a_stub
)


REM ==== Define compresed file =====================================================
SET compressed_file=%finish_file%.rar
IF %rar_compress_ratio% GTR 0 (
  ECHO compressed_file         = %compressed_file%
  SET finish_file=%compressed_file%
)


REM ==== Delete not actual files (over defined count) ==============================
SET /a over=%backup_count%-1
IF %backup_count% == 1 (
  ECHO deleting_old_files      = %result_dir%\%local_file_or_alias%.*.%backup_ext%*
  DEL "%result_dir%\%local_file_or_alias%.*.%backup_ext%*" /q
) ELSE IF %backup_count% GTR 1 (
  FOR /f "SKIP=%over%" %%f IN ('DIR "%result_dir%\%local_file_or_alias%.*.%backup_ext%*" /a:-D /b /o:-N 2^>NUL') DO (
    IF EXIST "%result_dir%\%%f" (
      ECHO deleting_old_file       = %result_dir%\%%f
      DEL "%result_dir%\%%f" /q
    )
  )
)


REM ==== Perform backup ============================================================
IF EXIST "%backup_log%" DEL "%backup_log%" /q 
ECHO backup_start            = %date% %time:~0,8%
SET is_local_backup=0
IF %is_local_db% == 1 (
  IF %is_local_result_dir% == 1 SET is_local_backup=1
  IF %direct_backup% == 0 SET is_local_backup=1
  SET ISC_PASSWORD=%ISC_PASSWORD_LOCAL%
) ELSE (
  SET ISC_PASSWORD=%ISC_PASSWORD_REMOTE%
)
IF "%rar_password%" == "" SET rar_password=%ISC_PASSWORD%
IF %is_local_backup% == 1 (
  ECHO %gbak% -b %garbage_collection% -se %service_mgr_host%:service_mgr %local% "%backup_file%" -v -y "%backup_log%"
  %gbak% -b %garbage_collection% -se %service_mgr_host%:service_mgr %local% "%backup_file%" -v >"%backup_log%" 2>&1
) ELSE (
  ECHO %gbak% -b %garbage_collection% "%full_db_specification%" "%backup_file%" -v -y "%backup_log%"
  %gbak% -b %garbage_collection% "%full_db_specification%" "%backup_file%" -v >"%backup_log%" 2>&1
)
IF %ERRORLEVEL% GTR 0 (
  IF EXIST "%backup_log%" (
    SET fail=Backup fail! See %backup_log% for details.
  ) ELSE (
    SET fail=Backup fail!
  )
  GOTO finish
)
IF NOT EXIST "%backup_log%" (
  SET fail=Backup fail!
  GOTO finish
)


REM ==== Perform test restore ======================================================
IF %restore% GTR 0 (
  IF EXIST "%restore_log%" DEL "%restore_log%" /q
  IF "%ISC_PASSWORD_LOCAL%" GTR "" (
    SET ISC_PASSWORD=%ISC_PASSWORD_LOCAL%
  ) ELSE (
    SET ISC_PASSWORD=%ISC_PASSWORD_REMOTE%
  )
  ECHO restore_start           = %date% %time:~0,8%
  ECHO %gbak% -rep -se %service_mgr_host%:service_mgr "%backup_file%" "%restore_file%" -v -y "%restore_log%"
  %gbak% -rep -se %service_mgr_host%:service_mgr "%backup_file%" "%restore_file%" -v >"%restore_log%" 2>&1
  IF %ERRORLEVEL% GTR 0 (
    IF EXIST "%restore_log%" (
      SET fail=Test restore fail! See %restore_log% for details.
    ) ELSE (
      SET fail=Test restore fail!
    )
    GOTO finish
  )
  IF NOT EXIST "%restore_log%" (
    SET fail=Test restore fail!
    GOTO finish
  )
)


REM ==== Perform RAR-compression or copy backup into destination dir ===============
IF "%rar_password%" GTR "" SET rar_password=-p%rar_password%
SET rar_options=%rar_options% -m%rar_compress_ratio%
SET backup_files="%backup_file%" %backup_files%
IF %include_logs_to_archive% == 1 (
  IF %restore% GTR 0 (
    SET backup_files=%backup_files% "%backup_log%" "%restore_log%"
  ) ELSE (
    SET backup_files=%backup_files% "%backup_log%"
  )
)
IF %rar_compress_ratio% GTR 0 (
  ECHO compressing_start       = %date% %time:~0,8%
  ECHO %rar% %rar_options% "%compressed_file%" %backup_files%
  %rar% %rar_options% %rar_password% "%compressed_file%" %backup_files%
  IF %ERRORLEVEL% GTR 0 (
    SET fail=Compression fail!
    GOTO finish
  )
) ELSE IF %direct_backup% == 0 (
    ECHO copying_start           = %date% %time:~0,8%
    ECHO COPY "%backup_file%" "%finish_file%"
    COPY "%backup_file%" "%finish_file%"
  )


REM ==== Delete not actual files (over defined space) ==============================
IF %backup_space_limit% GTR 0 (
  SETLOCAL EnableDelayedExpansion
  IF %ERRORLEVEL% GTR 0 (
    ECHO You must enable var delayed expansion by CMD.EXE /V:ON or at registry key 
    ECHO Software\Microsoft\Command Processor\DelayedExpansion: HKLM or HKCU
    GOTO finish
  ) 
  FOR /f %%f IN ('DIR "%result_dir%\%local_file_or_alias%.*.%backup_ext%*" /a:-D /b /o:-N') DO (
    FOR %%i in ("%result_dir%\%%f") DO (
      SET size=%%~zi
      IF %file_size_shift% == 3 SET size=!size:~0,-3!
      IF %file_size_shift% == 6 SET size=!size:~0,-6!
      IF "!size!" == "" SET size=0
      IF "!total_space!" == "" (
        SET /a total_space=!size!
      ) ELSE (
        IF !total_space! LEQ %backup_space_limit% SET /a total_space+=!size!
        IF !total_space! GTR %backup_space_limit% (
          ECHO deleting_overquota_file = %result_dir%\%%f
          DEL "%result_dir%\%%f" /q
        )
      )
    )
  )
)


REM ==== Report when fail or exit ==================================================
:finish
IF "%fail%" == "" (
  ECHO Finish                  = %date% %time:~0,8%
  GOTO exit
)
ECHO #=============================================================================#
ECHO # %fail%
ECHO #=============================================================================#
SET fail=%fail%,  DB: %full_db_specification%,  Dest: %result_dir%
IF "%net_send_receiver%" GTR "" (
  ECHO NET SEND %net_send_receiver% "%fail%"
  NET SEND %net_send_receiver% "%fail%"
)
IF "%blat%" GTR "" IF "%smtp_server%" GTR "" IF "%mail_sender%" GTR "" IF "%mail_login%" GTR "" IF "%mail_receiver%" GTR "" (
  ECHO %blat% -to "%mail_receiver%" -subject "%mail_subject%" -body "%fail%" -server %smtp_server% -f "%mail_sender%" -u "%mail_login%" -pw "*******"
  %blat% -to "%mail_receiver%" -subject "%mail_subject%" -body "%fail%" -server %smtp_server% -f "%mail_sender%" -u "%mail_login%" -pw "%mail_password%"
)
SET time_ex=%time: =0%
IF "%error_log%" GTR "" (
  ECHO %date% %time_ex:~0,8%   %fail% >> "%error_log%"
)
EXIT /b 1

:exit

Приведу также лог, генерируемый запуском скрипта в наиболее простом виде: никаких дополнительных ключей, все параметры по умолчанию, передается только адрес БД и каталог назначения
fb_backup.bat localhost:p:\MSO\DB\MS_ORDERS.FDB \\192.168.1.1\disk_a1\обмен >fb_backup.log
#=============================================================================#
#                                                                             #
# Firebird/InterBase database backup, test restore, zip and rotate script     #
# Ver 3.2.7 (11.11.2012)                                                      #
#                                                                             #
# Author: arni (email:arnisoft at rambler dot ru)                             #
#                                                                             #
# Format:                                                                     #
# FB_BACKUP [host[/port]:][path]db_file_or_alias  result_dir                  #
#           [/count:backup_count]  [/space:backup_space_limit]  [/gc]         #
#           [/restore]  [/compress:level]  [/password:SYSDBA_password]        #
#           [other_files_to_compress [...]]                                   #
#                                                                             #
# Input params:                                                               #
# [host[/port]:][path]      : local or network, full-specified path or alias  #
#   db_file_or_alias            to the source database                        #
# result_dir                : result backup collecting directory              #
# /count:backup_count       : backup file number to keep (30 by default)      #
# /space:backup_space_limit : total backup size in bytes (not use by default) #
#                               you can use suffixes K, M or G.               #
# /gc                       : need to collect garbage in DB (OFF by default)  #
# /restore                  : need to do test restore (OFF by default)        #
# /compress:level           : compress ratio for RAR (2 by default):          #
#                               0: not compress, 1: fastest, 2: fast,         #
#                               3: normal, 4: good, 5: best                   #
# /password:SYSDBA_password : optional SYSDBA password for remote server      #
#                               (by default uses one from the source code)    #
# other_files_to_compress   : list of files that must be add to archive       #
#                                                                             #
#=============================================================================#
full_db_specification   = localhost:p:\MSO\DB\MS_ORDERS.FDB
network_specification   = localhost
local_db_specification  = p:\MSO\DB\MS_ORDERS.FDB
local_path              = p:\MSO\DB\
local_db_file_or_alias  = MS_ORDERS.FDB
result_dir              = \\192.168.1.1\disk_a1\обмен
backup_count            = 30
backup_space_limit      = OFF
garbage_collection_flag = OFF
test_restore_flag       = OFF
backup_compressing      = ON, RAR-ratio=2
backup_file             = C:\WINDOWS\TEMP\MS_ORDERS.FDB.fbk
backup_log              = \\192.168.1.1\disk_a1\обмен\MS_ORDERS.FDB.backup.log
compressed_file         = \\192.168.1.1\disk_a1\обмен\MS_ORDERS.FDB.121111_1621.fbk.rar
backup_start            = 11.11.2012 16:21:48
"C:\Programs\FB25\bin\gbak.exe" -b -g -se localhost:service_mgr p:\MSO\DB\MS_ORDERS.FDB "C:\WINDOWS\TEMP\MS_ORDERS.FDB.fbk" -v -y "\\192.168.1.1\disk_a1\обмен\MS_ORDERS.FDB.backup.log"
compressing_start       = 11.11.2012 16:21:51
"C:\Program Files\WinRAR\rar.exe" a -y -ep -idcd -m2 "\\192.168.1.1\disk_a1\обмен\MS_ORDERS.FDB.121111_1621.fbk.rar" "C:\WINDOWS\TEMP\MS_ORDERS.FDB.fbk"  "\\192.168.1.1\disk_a1\обмен\MS_ORDERS.FDB.backup.log"
 
Создание архива \\192.168.1.1\disk_a1\обмен\MS_ORDERS.FDB.121111_1621.fbk.rar
 
Добавление C:\WINDOWS\TEMP\MS_ORDERS.FDB.fbk                                6% 12% 18% 24% 30% 36% 42% 48% 54% 60% 66% 72% 78% 84% 86%  OK 
Добавление \\192.168.1.1\disk_a1\обмен\MS_ORDERS.FDB.backup.log            92% 98%100%  OK 
Finish                  = 11.11.2012 16:21:51

Разберем фрагменты скрипта:
заголовочная часть
Хидер содержит ссылку на авторство, текущую версию, но главное — описание формата вызова: какие параметры основные, какие — опциональные, какие дефолтные значения принимают опциональные параметры.
Опциональные параметры могут идти в любой последовательности, и их желательно предварять стандартными знаками ключа командной строки: прямым слешем [/], либо тире[-].
@CLS
@ECHO OFF
ECHO #=============================================================================#
ECHO #                                                                             #
ECHO # Firebird/InterBase database backup, test restore, zip and rotate script     #
ECHO # Ver 3.2.8 (26.01.2013)                                                      #
ECHO #                                                                             #
ECHO # Author: arni (email:arnisoft at rambler dot ru)                             #
ECHO #                                                                             #
ECHO # Format:                                                                     #
ECHO # FB_BACKUP [host[/port]:][path]db_file_or_alias  result_dir                  #
ECHO #           [/count:backup_count]  [/space:backup_space_limit]  [/gc]         #
ECHO #           [/restore]  [/compress:level]  [/password:SYSDBA_password]        #
ECHO #           [other_files_to_compress [...]]                                   #
ECHO #                                                                             #
ECHO # Input params:                                                               #
ECHO # [host[/port]:][path]      : local or network, full-specified path or alias  #
ECHO #   db_file_or_alias            to the source database                        #
ECHO # result_dir                : result backup collecting directory              #
ECHO # /count:backup_count       : backup file number to keep (30 by default)      #
ECHO # /space:backup_space_limit : total backup size in bytes (not use by default) #
ECHO #                               you can use suffixes K, M or G.               #
ECHO # /gc                       : need to collect garbage in DB (OFF by default)  #
ECHO # /restore                  : need to do test restore (OFF by default)        #
ECHO # /compress:level           : compress ratio for RAR (2 by default):          #
ECHO #                               0: not compress, 1: fastest, 2: fast,         #
ECHO #                               3: normal, 4: good, 5: best                   #
ECHO # /password:SYSDBA_password : optional SYSDBA password for remote server      #
ECHO #                               (by default uses one from the source code)    #
ECHO # other_files_to_compress   : list of files that must be add to archive       #
ECHO #                                                                             #
ECHO #=============================================================================#

определение дефолтных параметров
тут мы видим 5 блоков, отвечающих за разделы:
  • параметры сервера;
  • параметры b/r;
  • параметры архиватора;
  • параметры мейлера;
  • прочие параметры;
REM ==== Server ====================================================================
SET gbak="C:\Programs\FB25\bin\gbak.exe"
SET ISC_USER=SYSDBA
SET ISC_PASSWORD_LOCAL=masterkey
SET ISC_PASSWORD_REMOTE=
 
 
REM ==== Backup/restore preferences ================================================
SET temp_backup_dir=%TEMP%
SET temp_restore_dir=%TEMP%
SET backup_count=30
SET backup_space_limit=0
SET backup_ext=fbk
SET garbage_collection=-g
SET restore=0
 
 
REM ==== RAR =======================================================================
SET rar="C:\Program Files\WinRAR\rar.exe"
SET rar_options=a -y -ep -idcd
SET rar_password=
SET rar_compress_ratio=2
 
 
REM ==== Mailer (see "Blat" at http://sourceforge.net/projects/blat) ===============
SET blat="C:\Programs\Blat307\blat.exe"
SET smtp_server=smtp.mailserver.ru
SET mail_sender=foo@mailserver.ru
SET mail_login=foo
SET mail_password=1234
SET mail_receiver=
SET mail_subject=Fail while database b/r
 
 
REM ==== Other preferences =========================================================
SET include_logs_to_archive=1
SET net_send_receiver=
SET error_log=
где:
  • gbak — путь к служебной утилите из комплекта сервера;
  • ISC_USER — пользователь, от имени которого будет происходить b/r;
  • ISC_PASSWORD_LOCAL — пароль выбранного пользователя на локальной машине;
  • ISC_PASSWORD_REMOTE — пароль выбранного пользователя на удаленной машине (лучше передавать параметром /password:xxxxxxxx);
  • temp_backup_dir — временный каталог, куда будет производиться бекап до его архивации в каталог назначения; лучше избрать физический диск, не совпадающий с тем, где лежит исходная БД;
  • temp_restore_dir — каталог, куда будет производиться тестовый рестор; лучше избрать физический диск, не совпадающий с тем, где лежит исходная БД, и не совпадающий с тем, где лежит файл бекапа;
  • backup_ext — дефолтное расширение файла с резервной копией;
  • garbage_collection — ключ gbak.exe, отвечающий за дефолтное поведение в отношении сборки мусора во время бекапа (лучше управлять параметром /gc);
  • restore — флаг дефолтного поведения в отношении необходимости выполнить тестовое восстановление (лучше управлять параметром /restore);
  • rar_options — дефолтные ключи архиватора RAR: «a» — сжать; "-y" — не задавать вопросов; "-ep" — исключить пути; "-idcd" — не мусорить вывод копирайтом и отметкой о готовности;
  • rar_password — пароль на архив (если не будет задан, то архиватор использует пароль Firebird-пользователя);
  • rar_compress_ratio — степень сжатия по умолчанию (лучше управлять параметром /compress:level)
  • blat — путь к мейлеру blat.exe;
  • smtp_server — SMTP-сервер, посредством которого будет производиться рассылка;
  • mail_sender — почтовый адрес отправителя;
  • mail_login — логин отправителя на сервере;
  • mail_password — пароль отправителя на сервере;
  • mail_receiver — почтовый адрес получателя рассылки;
  • mail_subject — заголовок письма рассылки;
  • include_logs_to_archive — флаг необходимости поместить логи b/r внутрь архива;
  • net_send_receiver — сетевое имя хоста, на который будут рассылаться оповещения о сбоях транспортом NET SEND;
  • error_log — файл журнала-накопителя произошедших сбоев;

определение исходной БД
Тут мы видим:
  • Чтение спецификации исходной БД из первого переданного параметра;
  • Разделение спецификации на имя хоста, порт, локальный путь и имя файла БД или её алиас;
  • Печать частей спецификации на консоль;
  • Выяснение, локальная эта БД или удаленная (будет важно позднее, при выборе между классическим бекапом или бекапом через сервисы);
REM ==== Define database location ==================================================
SET full_db_specification=%~1
ECHO full_db_specification   = %full_db_specification%
REM Devide DB spec to network and local parts
FOR /f "DELIMS=: TOKENS=1*" %%i IN ("%full_db_specification%") DO (
  SET network=%%i
  SET local=%%j
)
REM Test if spec. is alias with no network part
IF "%local%" == "" (
  SET network=
  SET local=%full_db_specification%
)
REM Test if spec. is full specified file with no network part
FOR /f "DELIMS=\ TOKENS=*" %%i IN ("%local%") DO IF "%local%" == "\%%i" (
  SET network=
  SET local=%full_db_specification%
)
ECHO network_specification   = %network%
IF "%network%" GTR "" (
  REM Extract port from network spec (if exists)
  FOR /f "DELIMS=/ TOKENS=1*" %%i IN ("%network%") DO (
    SET host=%%i
    SET port=%%j
    IF "%%j" GTR "" (
      ECHO network_host            = %%i
      ECHO network_port            = %%j
    )
  )
)
 
REM Test if DB is local or remote
SET service_mgr_host=localhost
SET is_local_db=1
IF "%network%" GTR "" IF "%host%" NEQ "127.0.0.1" IF /i "%host%" NEQ "localhost" SET is_local_db=0
IF %is_local_db% == 1 IF "%network%" GTR "" SET service_mgr_host=%network%
 
ECHO local_db_specification  = %local%
IF "%local%" == "" (
  SET fail=Param #1 {DB specification} missing!
  GOTO finish
)
REM Extract file (or alias) from local part of spec.
FOR /f %%i IN ("%local%") DO (
  SET local_path=%%~dpi
  SET local_file_or_alias=%%~nxi
)
IF "%local%" NEQ "%local_file_or_alias%" (
  ECHO local_path              = %local_path%
  REM Check DB file exists for local, not aliased specification
  IF %is_local_db% == 1 IF NOT EXIST "%local%" (
    SET fail=Local DB file %local% not found!
    GOTO finish
  )
)
ECHO local_db_file_or_alias  = %local_file_or_alias%

определение результирующего каталога (куда складываются резервные копии)
Тут мы видим:
  • Чтение результирующего каталога из второго параметра, переданного скрипту;
  • Если результирующий каталог отсутствует, он создается;
  • Определение расположения каталога: локальный или удаленный (сетевой) — понадобится ниже для выбора оптимальной стратегии бекапа;
REM ==== Define result directory ===================================================
SET result_dir=%~2
ECHO result_dir              = %result_dir%
IF "%result_dir%" == "" (
  SET fail=Param #2 {backup collecting directory} missing!
  GOTO finish
)
REM Cut the result dir if it is in path-style (ends with separator)
IF "%result_dir:~-1%" == "\" SET result_dir=%result_dir:~0,-1%
REM Try to create the result directory if it is not exists yet
IF NOT EXIST "%result_dir%" (
  MD "%result_dir%"
  IF NOT EXIST "%result_dir%" (
    SET fail=Cannot create backup collecting directory!
    GOTO finish
  )
)
REM Test if it is local or remote directory (elementary, may get wrong answer)
SET is_local_result_dir=1
IF "%result_dir:~0,2%" == "\\" SET is_local_result_dir=0
Данный блок имеет код обработки ошибок, при возникновении которых в переменную пишется суть сбоя, после чего управление передается в конец скрипта — в блок обратной связи с админом. Подобные обработчики встречаются во всех последующих блоках, и далее я не буду их специально упоминать.

чтение остальных переданных параметров
Тут мы видим блоки:
  • чтение очередного параметра;
  • разделение ключа на имя и значение;
  • распознание ключа, переход к его обработке;
  • установка количества хранимых копий;
  • установка совокупного объема копий;
  • управление флагом сбора мусора в БД;
  • управление флагом тестового восстановления;
  • определение коэффициента сжатия архиватором;
  • сохранение пароля SYSDBA для удаленного сервера;
  • чтение списка дополнительных файлов для помещения в архив;
REM ==== Use other command line options ============================================
:loop_options
SHIFT
SET next_param=%~2
IF "%next_param%" == "" GOTO print_options
SET prefix=%next_param:~0,1%
IF "%prefix%" == "/" SET next_param=%next_param:~1%
IF "%prefix%" == "-" SET next_param=%next_param:~1%
IF "%next_param%" GTR "" (
  FOR /f "DELIMS=: TOKENS=1*" %%i IN ("%next_param%") DO (
    SET value=%%j
    IF /i "%%i" == "count" IF "%%j" GTR "" GOTO count
    IF /i "%%i" == "space" IF "%%j" GTR "" GOTO space
    IF /i "%%i" == "gc" GOTO gc
    IF /i "%%i" == "restore" GOTO restore
    IF /i "%%i" == "compress" IF "%%j" GTR "" GOTO compress
    IF /i "%%i" == "password" IF "%%j" GTR "" GOTO password
    IF EXIST "%next_param%" GOTO add_file_to_compress
    IF "%prefix%" GTR "/" GOTO add_file_to_compress
    ECHO unknown param found: %next_param%
    GOTO loop_options
  )
) ELSE (
  ECHO empty param found!
  GOTO loop_options
)
 
 
REM ==== Define file count in the result dir =======================================
:count
SET /a backup_count=0+%value%
GOTO loop_options
 
 
REM ==== Define allowed backup space limit =========================================
:space
SET suffix=%value:~-1%
IF "%suffix%" GTR "9" (
  SET value=%value:~0,-1%
  IF /i "%suffix%" == "K" (
    SET file_size_shift=0
    SET /a value*=1000
  )
  IF /i "%suffix%" == "M" (
    SET file_size_shift=3
    SET /a value*=1000
  )
  IF /i "%suffix%" == "G" (
    SET file_size_shift=6
    SET /a value*=1000
  )
)
SET /a backup_space_limit=0+%value%
GOTO loop_options
 
 
REM ==== Define need of garbage collection =========================================
:gc
SET garbage_collection=
IF "%value%" == "0" SET garbage_collection=-g
IF /i "%value%" == "N" SET garbage_collection=-g
IF /i "%value%" == "NO" SET garbage_collection=-g
IF /i "%value%" == "OFF" SET garbage_collection=-g
GOTO loop_options
 
 
REM ==== Define need of test restore ===============================================
:restore
SET restore=1
IF "%value%" == "0" SET restore=0
IF /i "%value%" == "N" SET restore=0
IF /i "%value%" == "NO" SET restore=0
IF /i "%value%" == "OFF" SET restore=0
GOTO loop_options
 
 
REM ==== Define need of backup compression and compress ratio ======================
:compress
IF "%value%" GEQ "0" IF "%value%" LEQ "5" SET rar_compress_ratio=%value%
GOTO loop_options
 
 
REM ==== Define SYSDBA password (in addition or for replace source code given) =====
:password
if "%ISC_PASSWORD_LOCAL%" GTR "" if "%ISC_PASSWORD_REMOTE%" GTR "" (
  SET ISC_PASSWORD_LOCAL=%value%
  SET ISC_PASSWORD_REMOTE=%value%
)
if "%ISC_PASSWORD_LOCAL%" == "" SET ISC_PASSWORD_LOCAL=%value%
if "%ISC_PASSWORD_REMOTE%" == "" SET ISC_PASSWORD_REMOTE=%value%
GOTO loop_options
 
 
REM ==== Define file list to compress (in addition to backup and maybe logs) =======
:add_file_to_compress
IF "%backup_files%" == "" (
  SET backup_files="%next_param%"
) ELSE (
  SET backup_files=%backup_files% "%next_param%"
)
GOTO loop_options
Примечание:
  • Булевы флаги для рестора или сбора мусора принимают следующие значения: Ложь=0,N,NO,OFF; Истина=1,Y,YES,ON.
  • Совокупный объем хранимых данных передается в байтах. Но т.к. числа в командном процессоре ограничены 32-битным целым, то значения свыше 1 Гб лучше передавать, указывая суффикс килобайта (K), мегабайта (M) или гигабайта (G). Например: /space:1200К, /space:280M, /space:12G. В этом блоке также можно наблюдать код, который усекает масштаб единиц хранения, позволяя в дальнейшем оперировать объемами, сверх 32-битной арифметики командного процессора.
  • Переданный пароль трактуется следующим образом: если в исходнике определены значения и переменной ISC_PASSWORD_LOCAL (локальный) и переменной ISC_PASSWORD_REMOTE (удаленный), то переданное значение переопределяет обе эти переменные. В остальных случаях (если хотя бы одна переменная не определена), переданное значение записывается только в пустую переменную, а заполненная сохраняет свое предопределенное значение.
  • Дополнительные файлы для сжатия (или маски файлов) передаются в естественном виде, без символа ключа [/].

печать итоговых параметров запуска скрипта
Тут мы видим:
  • вывод количества хранимых копий;
  • вывод совокупного пространства для хранения копий;
  • вывод флага сбора мусора;
  • вывод флага тестового восстановления;
  • вывод признака архивации и используемого коэффициента компрессии;
REM ==== Print predefined or recognized in command line options ====================
:print_options
IF %backup_count% GTR 0 (
  ECHO backup_count            = %backup_count%
) ELSE (
  ECHO backup_count            = OFF
)
IF "%file_size_shift%" == "" SET file_size_shift=0
IF %backup_space_limit% GTR 0 (
  IF %file_size_shift% == 6 (
    ECHO backup_space_limit      = %backup_space_limit% Mb
  ) ELSE IF %file_size_shift% == 3 (
    ECHO backup_space_limit      = %backup_space_limit% Kb
  ) ELSE ECHO backup_space_limit      = %backup_space_limit% bytes
) ELSE (
  ECHO backup_space_limit      = OFF
)
IF "%garbage_collection%" == "-g" (
  ECHO garbage_collection_flag = OFF
) ELSE (
  ECHO garbage_collection_flag = ON
)
IF %restore% == 0 (
  ECHO test_restore_flag       = OFF
) ELSE (
  ECHO test_restore_flag       = ON
)
IF %rar_compress_ratio% == 0 (
  ECHO backup_compressing      = OFF
) ELSE (
  ECHO backup_compressing      = ON, RAR-ratio=%rar_compress_ratio%
)

определение рабочих файлов
Тут мы видим:
  • снятие временной метки, чтобы использовать её в имени файла бекапа;
  • определение того, будем ли мы делать бекап непосредственно в каталог назначения, или сначала во временное хранилище для последующего восстановления, сжатия и т.д.;
  • определение полной спецификации файла бекапа;
  • определение полной спецификации лога бекапа;
  • вывод рассчитанных параметров на печать;
  • определение и вывод на печать спецификации файла рестора и лога рестора, если флаг рестора поднят;
  • определение и вывод на печать файла архива, если сжатие затребовано;
REM ==== Define backup file and backup log =========================================
SET datetime=%date:~-2%%date:~3,2%%date:~0,2%_%time:~0,2%%time:~3,2%
SET finish_file=%result_dir%\%local_file_or_alias%.%datetime: =0%.%backup_ext%
SET direct_backup=0
IF %rar_compress_ratio% == 0 (
  IF %is_local_result_dir% == 1 SET direct_backup=1
  IF %restore% == 0 SET direct_backup=1
)
IF %direct_backup% == 1 (
  SET backup_file=%finish_file%
) ELSE (
  SET backup_file=%temp_backup_dir%\%local_file_or_alias%.%backup_ext%
  IF NOT EXIST "%temp_backup_dir%" (
    MD "%temp_backup_dir%"
    IF NOT EXIST "%temp_backup_dir%" (
      SET fail=Cannot create backup directory!
      GOTO finish
    )
  )
)
ECHO backup_file             = %backup_file%
SET backup_log=%result_dir%\%local_file_or_alias%.backup.log
ECHO backup_log              = %backup_log%
 
 
REM ==== Define restore file and restore log =======================================
SET restore_file=%temp_restore_dir%\%local_file_or_alias%.testrest
IF %restore% GTR 0 (
  IF NOT EXIST "%temp_restore_dir%" (
    MD "%temp_restore_dir%"
    IF NOT EXIST "%temp_restore_dir%" (
      ECHO temp_restore_dir             = %temp_restore_dir%
      SET fail=Cannot create restore directory!
      GOTO finish
    )
  )
  ECHO restore_file            = %restore_file%
  SET restore_log=%result_dir%\%local_file_or_alias%.restore.log
)
IF "%restore_log%" GTR "" (
  ECHO restore_log             = %restore_log%
) else (
  SET restore_log=just_a_stub
)
 
 
REM ==== Define compresed file =====================================================
SET compressed_file=%finish_file%.rar
IF %rar_compress_ratio% GTR 0 (
  ECHO compressed_file         = %compressed_file%
  SET finish_file=%compressed_file%
)

удаление лишних копий из каталога назначения
REM ==== Delete not actual files (over defined count) ==============================
SET /a over=%backup_count%-1
IF %backup_count% == 1 (
  ECHO deleting_old_files      = %result_dir%\%local_file_or_alias%.*.%backup_ext%*
  DEL "%result_dir%\%local_file_or_alias%.*.%backup_ext%*" /q
) ELSE IF %backup_count% GTR 1 (
  FOR /f "SKIP=%over%" %%f IN ('DIR "%result_dir%\%local_file_or_alias%.*.%backup_ext%*" /a:-D /b /o:-N 2^>NUL') DO (
    IF EXIST "%result_dir%\%%f" (
      ECHO deleting_old_file       = %result_dir%\%%f
      DEL "%result_dir%\%%f" /q
    )
  )
)

выполнение бекапа
Тут мы видим:
  • удаление предыдущего лога бекапа (gbak.exe вернет ошибку, если наткнется на старый лог);
  • в зависимости от того, распознана ли база как локальная или как удаленная, выбирается пароль;
  • в зависимости от расположения базы и расположения каталога для бекапа, определяем возможность использовать максимально быструю стратегию — бекап через сервисы, либо классический бекап в противном случае;
REM ==== Perform backup ============================================================
IF EXIST "%backup_log%" DEL "%backup_log%" /q 
ECHO backup_start            = %date% %time:~0,8%
SET is_local_backup=0
IF %is_local_db% == 1 (
  IF %is_local_result_dir% == 1 SET is_local_backup=1
  IF %direct_backup% == 0 SET is_local_backup=1
  SET ISC_PASSWORD=%ISC_PASSWORD_LOCAL%
) ELSE (
  SET ISC_PASSWORD=%ISC_PASSWORD_REMOTE%
)
IF "%rar_password%" == "" SET rar_password=%ISC_PASSWORD%
IF %is_local_backup% == 1 (
  ECHO %gbak% -b %garbage_collection% -se %service_mgr_host%:service_mgr %local% "%backup_file%" -v -y "%backup_log%"
  %gbak% -b %garbage_collection% -se %service_mgr_host%:service_mgr %local% "%backup_file%" -v >"%backup_log%" 2>&1
) ELSE (
  ECHO %gbak% -b %garbage_collection% "%full_db_specification%" "%backup_file%" -v -y "%backup_log%"
  %gbak% -b %garbage_collection% "%full_db_specification%" "%backup_file%" -v >"%backup_log%" 2>&1
)
IF %ERRORLEVEL% GTR 0 (
  IF EXIST "%backup_log%" (
    SET fail=Backup fail! See %backup_log% for details.
  ) ELSE (
    SET fail=Backup fail!
  )
  GOTO finish
)
IF NOT EXIST "%backup_log%" (
  SET fail=Backup fail!
  GOTO finish
)

выполнение тестового восстановления
Тут мы видим:
  • удаление предыдущего лога рестора (gbak.exe вернет ошибку, если наткнется на старый лог);
  • выполнение восстановления (всегда через сервисы, т.к. и файл бекапа и тестовая БД локальны в этой ветви сценария);
REM ==== Perform test restore ======================================================
IF %restore% GTR 0 (
  IF EXIST "%restore_log%" DEL "%restore_log%" /q
  IF "%ISC_PASSWORD_LOCAL%" GTR "" (
    SET ISC_PASSWORD=%ISC_PASSWORD_LOCAL%
  ) ELSE (
    SET ISC_PASSWORD=%ISC_PASSWORD_REMOTE%
  )
  ECHO restore_start           = %date% %time:~0,8%
  ECHO %gbak% -rep -se %service_mgr_host%:service_mgr "%backup_file%" "%restore_file%" -v -y "%restore_log%"
  %gbak% -rep -se %service_mgr_host%:service_mgr "%backup_file%" "%restore_file%" -v >"%restore_log%" 2>&1
  IF %ERRORLEVEL% GTR 0 (
    IF EXIST "%restore_log%" (
      SET fail=Test restore fail! See %restore_log% for details.
    ) ELSE (
      SET fail=Test restore fail!
    )
    GOTO finish
  )
  IF NOT EXIST "%restore_log%" (
    SET fail=Test restore fail!
    GOTO finish
  )
)
Примечание:
Если у вас InterBase, Yaffil или Firebird младше версии 2.0, флаг восстановления -rep нужно заменить на -r

сжатие копии в архив
Тут мы видим:
  • сбор всех параметров и ключей rar, включая пароль на архив и коэффициент сжатия;
  • сбор всех файлов для сжатия, включая резервную копию, логи (если задано политикой) и прочие файлы (если переданы);
  • если сжатие задано, производим компрессию;
  • если сжатие отключено, то просто копируем файл бекапа в каталог назначения (только если сценарий скрипта не создал его изначально там);
REM ==== Perform RAR-compression or copy backup into destination dir ===============
IF "%rar_password%" GTR "" SET rar_password=-p%rar_password%
SET rar_options=%rar_options% -m%rar_compress_ratio%
SET backup_files="%backup_file%" %backup_files%
IF %include_logs_to_archive% == 1 (
  IF %restore% GTR 0 (
    SET backup_files=%backup_files% "%backup_log%" "%restore_log%"
  ) ELSE (
    SET backup_files=%backup_files% "%backup_log%"
  )
)
IF %rar_compress_ratio% GTR 0 (
  ECHO compressing_start       = %date% %time:~0,8%
  ECHO %rar% %rar_options% "%compressed_file%" %backup_files%
  %rar% %rar_options% %rar_password% "%compressed_file%" %backup_files%
  IF %ERRORLEVEL% GTR 0 (
    SET fail=Compression fail!
    GOTO finish
  )
) ELSE IF %direct_backup% == 0 (
    ECHO copying_start           = %date% %time:~0,8%
    ECHO COPY "%backup_file%" "%finish_file%"
    COPY "%backup_file%" "%finish_file%"
  )
Примечание:
В ряде обсуждений встречаются примеры, когда для ускорения сжатия резервной копии gbak.exe и rar.exe запускаются в связке, когда вывод первого подается непосредственно на вход второго. К сожалению, у этого метода есть существенный недостаток, не позволяющий рекомендовать его к использованию: если gbak.exe завершается ошибкой, то в итоге все равно возвращается нулевой (успешный) код возврата, т.к. rar.exe честно и безошибочно забекапил те крохи, что gbak.exe успел ему передать, прежде чем завалился. Т.е. вы никогда не узнаете, что у вас сбоит процесс изготовления резервных копий, пока не попытаетесь восстановить битый бекап, либо случайно не обратите внимание на копеешный размер итогового архива.

удаление файлов сверх заданной дисковой квоты
Тут мы видим:
  • Попытку активировать режим отложенного расширения переменных в командном процессоре (в случае неудачи, информируем пользователя и выходим из блока), который необходим для подсчета совокупного объема файлов;
  • Перебор накопленных бекапов от самого свежего до самого древнего, и подсчет их суммарного объема. С момента превышения лимита, удаляем все оставшиеся файлы. В этом блоке используется прием, который позволяет преодолеть ограничение 32-битной арифметики при калькуляции десятков и сотен гигабайт дискового пространства (банально отсекаем младшие разряды), если вам приходится иметь дело с такими большими базами.
REM ==== Delete not actual files (over defined space) ==============================
IF %backup_space_limit% GTR 0 (
  SETLOCAL EnableDelayedExpansion
  IF %ERRORLEVEL% GTR 0 (
    ECHO You must enable var delayed expansion by CMD.EXE /V:ON or at registry key 
    ECHO Software\Microsoft\Command Processor\DelayedExpansion: HKLM or HKCU
    GOTO finish
  ) 
  FOR /f %%f IN ('DIR "%result_dir%\%local_file_or_alias%.*.%backup_ext%*" /a:-D /b /o:-N') DO (
    FOR %%i in ("%result_dir%\%%f") DO (
      SET size=%%~zi
      IF %file_size_shift% == 3 SET size=!size:~0,-3!
      IF %file_size_shift% == 6 SET size=!size:~0,-6!
      IF "!size!" == "" SET size=0
      IF "!total_space!" == "" (
        SET /a total_space=!size!
      ) ELSE (
        IF !total_space! LEQ %backup_space_limit% SET /a total_space+=!size!
        IF !total_space! GTR %backup_space_limit% (
          ECHO deleting_overquota_file = %result_dir%\%%f
          DEL "%result_dir%\%%f" /q
        )
      )
    )
  )
)

финальная часть
Тут мы видим:
  • В случае обнаружения ошибки на любом из вышестоящих шагов, составляется краткий отчет: суть ошибки, исходная БД, каталог назначения;
  • Если определен адресат для транспорта NET SEND, то отчет об ошибке отсылается ему;
  • Если определены параметры почтовой рассылки (мейлер, сервер, отправитель, получатель), то отчет об ошибке рассылается по почте;
  • Если определен файл-журнал сбоев, то в него добавляется запись об ошибке;
  • Последний шаг в сценарии ошибки — вернуть управление с положительным кодом завершения, что будет сигнализировать вызывающей стороне (обычно это — планировщик задач), что скрипт окончился сбоем;
REM ==== Report when fail or exit ==================================================
:finish
IF "%fail%" == "" (
  ECHO Finish                  = %date% %time:~0,8%
  GOTO exit
)
ECHO #=============================================================================#
ECHO # %fail%
ECHO #=============================================================================#
SET fail=%fail%,  DB: %full_db_specification%,  Dest: %result_dir%
IF "%net_send_receiver%" GTR "" (
  ECHO NET SEND %net_send_receiver% "%fail%"
  NET SEND %net_send_receiver% "%fail%"
)
IF "%blat%" GTR "" IF "%smtp_server%" GTR "" IF "%mail_sender%" GTR "" IF "%mail_login%" GTR "" IF "%mail_receiver%" GTR "" (
  ECHO %blat% -to "%mail_receiver%" -subject "%mail_subject%" -body "%fail%" -server %smtp_server% -f "%mail_sender%" -u "%mail_login%" -pw "*******"
  %blat% -to "%mail_receiver%" -subject "%mail_subject%" -body "%fail%" -server %smtp_server% -f "%mail_sender%" -u "%mail_login%" -pw "%mail_password%"
)
SET time_ex=%time: =0%
IF "%error_log%" GTR "" (
  ECHO %date% %time_ex:~0,8%   %fail% >> "%error_log%"
)
EXIT /b 1

:exit

Приведу пару сценариев, описывающих применение скрипта в нашей повседневной практике. Условно назову их «ночной бекап» и «дневной бекап».
В ночное время с базой никто не работает, или почти никто. Ресурсы сервера простаивают, а поэтому уместно произвести полноценный цикл b/r, а именно:
— бекап со сборкой мусора;
— тестовое восстановление;
— сжатие резервной копии и текущей версии прикладной программы, причем сжатие наилучшим образом;
Вот пример запуска «ночного бекапа»:
fb_backup.bat localhost:Orma4 d:\Bak /count:99 /space:500G /compress:5 /restore /gc d:\Orma.exe >C:\Orma4.log 2>&1

Напротив, запуск «дневного бекапа» должен отработать как можно быстрее, как можно менее напрягая сервер, а значит и подключенных к БД пользователей. Поэтому исключаем сборку мусора и тестовое восстановление, сжимаем наибыстрейшим методом, и только файл резервной копии:
fb_backup.bat localhost:Orma4 d:\Bak /count:99 /space:500G /compress:1 >C:\Orma4.log 2>&1

Есть еще вариант записи бекапа на удаленный сервер (в целях безопасности согласно принципа «не хранить все яйца в одной корзине»). Он сочетает промежуточный набор ключей: жать нужно наиболее плотно, т.к. архиватор обычно успевает перемолачивать быстрее, чем сеть справляется с перекачкой файла, а остальные операции (сбор мусора, тестовое восстановление, сохранение прикладной программы и т.д.) по возможности исключить, т.к. они уже были выполнены «ночным бекапом» на локальный сервер:
fb_backup.bat localhost:Orma4 \\ifs\E$\backup\FirebirdDB /count:99 /space:300G /compress:5 >C:\Orma4.log 2>&1

Для тех кто не в курсе, концовка команды означает перенаправить вывод скрипта (stdout) в файл, и вывод ошибок (stderr) направить в тот же файл.
 >C:\Orma4.log 2>&1


Немного об ограничениях скрипта.
  • Все наши БД работают под управлением серверных редакций ОС Windows, поэтому скрипт не проверялся на пригодность снятия копий с БД, работающих под Linux. Вероятно, они могут возникнуть на этапе парсинга пути к БД с использованием прямого слеша в качестве разделителя UNIX-систем. Но если использовать адресацию через алиас, тогда проблем, похоже, возникнуть не должно. Я буду благодарен, если кто-то проверит этот аспект и отпишется в комментариях.
  • Мне не особенно нравится, что пароль SYSDBA открытым текстом лежит в скрипте, либо также открытым текстом передается в скрипт через планировщик. Напрашивается доработка скрипта в направлении опционального подключения к БД посредством Windows Trusted Authentication, что позволит выполнять b/r от имени учетной записи пользователя с правами доменного админа, не передавая пароль явно.
  • Некоторые предпочитают сохранять не бекап БД, а отресторенную копию, т.к. первый нужно будет восстанавливать, а вторая уже готова для использования, что важно в случае необходимости экстренной подмены. Однако в последнем случае существенно увеличиваются требования к объему хранилища бекапов, а также этот вариант ограниченно переносим между различными версиями сервера (предположим, в продакшене у вас крутится FB2.5.1, а на девелоперской машине стоит FB2.5.2 или снепшот 2.5.3, между которыми есть несовместимость), т.к. штатную миграцию через b/r тут уже произвести не получится.
  • b/r происходит от имени штатного суперпользователя SYSDBA (либо иного пользователя, которого вы пропишите в инициализирующей части скрипта). Поддержка альтернативного владельца БД у нас не практикуется. Кроме того, процесс рестора в данном скрипте носит контрольный характер (проверка восстановимости бекапа), а не целевой.
  • На этапе восстановления размер страницы БД не декларируется, и тем самым берется предыдущее значение. ИМХО, смена страницы — это редкая разовая операция, которая должна делаться «зряче», и ей не место в скрипте, основное назначение которого — периодический вызов из планировщика. Ну и отсылка на специфическую роль рестора в этом скрипте также будет уместна: нечего мудрить с размером страницы, если мы хотим лишь проверить восстановимость бекапа.
  • Скрипт достаточно обильно снабжен комментариями, но все они англоязычны (укажите в комментариях, где совсем коряво получилась с инглишем). От кириллических вставок пришлось отказаться из соображений практичности: текст обычно набирается/правится в «блокноте» с кодовой страницей 1251, а чтобы русские комментарии были видны на этапе исполнения командного файла, их нужно записывать в кодовой странице 866. Т.к. FAR или другой продвинутый редактор не всегда оказывается под рукой, проще использовать буквы только латинского алфавита. Тут я сильно завидую линуксовой консоли с UTF8.
  • Хотя скрипт разрабатывался с учетом возможности работать с путями, содержащими пробелы (которые по стандарту необходимо обрамлять кавычками), этот аспект у нас толком не тестировался, и есть предположение, что, например, спецификация удаленной БД с указанием имени/адреса сервера и локального пути, содержащего пробелы, может быть распарсена некорректно. Работа через алиас, либо сокращенная запись пути позволят выйти из этого затруднения.
  • +10
  • 25,1k
  • 8
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 8
  • +1
    На больших базах время восстановления gbak-файла неприемлемо большое.
    Почему-то ни слова о стандартной утилите nbackup, позволяющей делать бекап-снепшоты, не требующие restore, а также инкрементальные бэкапы.
    • 0
      Эх, этот скрипт лет 5 назад назад бы :).
      А так могу порекомендовать делать не 30-дневный архив бекапов, а т.н. циклический бекап, известный еще со времен ленточных накопителей:
      — ежедневные бекапы, которые сохраняются 7 дней, далее затираются, кроме недельного и месячного
      — недельные бекапы (например, пятничный) хранятся месяц, далее затираются
      — месячный бекап (например, за 1 число) хранится полгода/год — по выбору
      Таким образом, вы охватываете больший период, при меньшем количестве «носителей». В максимальном случае это 6+4+12 = 22 «носителя».

      Также, если вас интересует безопасность, рекомендуется владельцем баз сделать все-таки не sysdba, а специального пользователя
      • +2
        Циклический бекап лучше подходит при использовании инкрементных бекапов с помощью nbackup, о которых спрашивал qw1. В случае gbak-а придется выбирать между возможностю восстановиться (условно) на любой день за последний месяц, либо же на более отдаленные даты, но с «дырами». Тут уже больше от предметной области зависит.

        Хотя, если учесть, что скрипт все же ориентирован в первую очередь на начинающих администраторов, будем надеятся, что у них относительно маленькие базы и какого-нибудь терабайтного винчестера/рейда хватит для обеспечения приемлимого уровня надежности.

        А вот что касается владельца базы — опишите, пожалуйста, примерный сценарий, при котором владелец не sysdba обеспечит большую безопасность. Я действительно не понимаю, что это дает.
        • 0
          Это, конечно, скорее защита от дурака, но при владельце, отличном от sysdba, в базе можно создать роль sysdba, которая не имеет никаких прав.
          В этом случае sysdba не сможет зайти в базу, а также восстановить украденный бекап. То есть прикрываем того пользователя, который 100% есть на сервере, и на которого скорее всего будет атака.
          Хотя, конечно, при физическом доступе к файлу БД/бекапу задача определения хозяина решаемая.
          Ну и вообще — зачем светить лишний раз паролем администратора сервера, если нужно всего лишь запускать бекап отдельной базы?

          Насчет защиты доступа sysdba — проверено на FB1.5, как на современных версиях не знаю, так как лет 5 уже плотно с fb не работаю.
          • +1
            При наличии утекшего бекапа безопасность firebird-базы равна нулю, т.к. а) база и бекап не шифруются, б) SYSDBA лежит не в базе. Т.е. любой другой сервер, пароль SYSDBA которого вы знаете, любезно пустит вас в неё.
            Ну и роль 'SYSDBA' — это защита только от очень юного хакера.
        • +1
          Описанная вами схема реализуется созданием трех заданий в планировщике, каждый со своим периодом запуска и со своей папкой назначения. Т.е. это не вопрос алгоритма, а лишь регламента.
          • 0
            Ваш скрипт тоже реализуется 30 заданиями, но ведь это так неудобно :). Настоящий ленивый администратор, если ему нужно, и циклический бекап сделает 1 командой
        • 0
          Спасибо за интересную статью! Добавил топик в избранное для последующего использования и/или рекомендации клиентам (бывает, на форуме нашего ПО спрашивают про скрипты для Firebird).

          Скажите, а у вас нет варианта для такого специфического, но, возможно, не столь редкого у разработчиков случая, когда есть большая папка с кучей разной степени вложенности подпапок, в которой хранятся разные базы, и периодически добавляются новые (или удаляются старые), и задача — делать бэкап своих баз или, скажем, прогнать один раз бэкап-рестор со сборкой мусора с целью уменьшения размера занимаемого на жестком диске пространства (ну заодно много других разработческих проблем решается)? Если готового решения нет, подскажите в какую сторону копать, я не очень силен в языке сценариев.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.