Копирование больших файлов при помощи PowerShell (с использованием службы BITS)

Приходилось ли вам копировать большие файлы по сети? А много больших файлов? А в условиях ненадежного/нестабильного канала? А, если компьютер надо перезагрузить, а файл еще не скопировался? Что делать? Тяжело жить, дядь Мить. Но, оказывается все уже украдено  придумано до нас. MS давно позаботилась о тех, кому приходится отвечать на эти вопросы, и придумала ответ. Ответ этот звучит так: BITS (“Background Intelligent Transfer Service”) или, если по-русски, то “Фоновая интеллектуальная служба передачи”. Все мы пользуемся результатами работы этой службы (даже те, кто об этом и не догадывается), ведь, именно эта служба каждый второй вторник месяца скачивает на наши компьютеры очередную порцию обновлений. Замечательная особенность этой службы состоит в том, что она может работать совершено незаметно для нас (в фоновом режиме) и использовать только неактивную часть ресурсов сети, не приводя к задержкам при интерактивной работе пользователя с другими приложениями, позволяет приоритизировать  свою работу, а так же возобновлять ее после восстановления канала и/или перезагрузки компьютера.

Несмотря на то, что BITS появилась на свет еще 2001 году, использовать ее для интерактивной работы (или в скриптах) до недавнего времени было не очень удобно. Но были энтузиасты, которые облегчали коллегам жизнь. Так, например, например Александр Суховей (AKA 4u3u) написал огромный batch-файл, который позволял использовать службу BITS для копирования/загрузки файлов (у меня, наверное, не хватило бы терпения для написания и отладки такого сценария). Однако, сейчас, во времена победного шествия PowerShell, можно забыть про это, как про страшный сон. Ведь, для того, чтобы использовать BITS,  достаточно подгрузить командлеты из модуля BitsTransfer. Вот, собственно, я и решил этим воспользоваться для того, чтобы копировать большие файлы (читай файлы бэкапов) в своей сети. И так, импортируем модуль BitsTransfer (Import-Module BitsTransfer), если этого еще не сделано ранее (например, в профиле), читаем описание (help about_BITS_Cmdlets, help Start-BitsTransfer -full) и понимаем, что все достаточно просто: можно подготовить csv-файл, содержащий попарно путь к файлу источнику и папку места назначения,

Source, Destination
\\srv-02\e$\_Backup\srv-02_disk_c_20110201.rar, G:\_Backup\srv-02\disk_c\
\\srv-03\g$\_Backup\srv-03_disk_c_asr_20101209.rar, G:\_Backup\srv-03\disk_c\
\\srv-03\g$\_Backup\srv-03_disk_c_20110127.rar, G:\_Backup\srv-03\disk_c\
\\srv-04\f$\_Backup\srv-04_disk_c_20110210.rar, G:\_Backup\srv-04\disk_c\
\\srv-05\e$\!Backup\srv-05_disk_c_20110125.rar, G:\_Backup\srv-05\disk_c\
\\srv-06\i$\_Backup\disk_c\srv-06_disk_c_20110127.rar, G:\_Backup\srv-06\disk_c\
\\srv-07\g$\_Backup\disk_c\srv-07_disk_c_20110131.rar, G:\_Backup\srv-07\disk_c\
\\srv-09\h$\Backup\srv-09_disk_c_20110128.rar, G:\_Backup\srv-09\disk_c\
\\srv-dc02\e$\Backup\srv-dc02_disk_c_asr_20110207.rar, G:\_Backup\srv-dc02\disk_c\
\\srv-dc02\e$\Backup\srv-dc02_disk_c_20110126.rar, G:\_Backup\srv-dc02\disk_c\

а затем подать его по конвейеру на вход комадлета

Start-BitsTransfer: Import-CSV filelist.txt | Start-BitsTransfer

Хочу обратить ваше внимание, что по умолчанию командлет Start-BitsTransfer работает с приоритетом Foreground (наивысший из возможных). Запущенная в этом режиме закачка будет состязаться  с другими процессами за полосу пропускания канала. Чтобы этого не происходило, нужно явно указать любой другой приоритет в качестве параметра запуска этого командлета. Так же стоит заметь, что по умолчанию командлет работает в синнхронном режиме, т.е. не возвращает управления в командную строку до завершения передачи всех файлов. Это поведение можно изменить при помощи параметра Asynchronous.

Ну, вот, теперь, когда теоретическая часть закончена можно и сам скрипт опубликовать.

cls
#Имя динамически формируемого файла-CSV, содержащего попарные наименования
#файлов бэкапов, подежащик копированию, и папок, в которые их надо будет скопировать
#В результате мы должны будем сформировать файл с нижеследующей структурой:
#Source, Destination
#\\server1\share\backup1.rar, \\server3\share\folder1\
#\\server2\share\backup.rar, \\server3\share\folder2\
$CSVListName="Files2Process.txt"
#Имя файла с перечнем "шар"
$SourcesFileName="Shares.txt"
#Файл с перечнем "шар" находится в той же папке, что и сам этот скрипт
#получаем полный путь к этой папке
$ScriptPath=Split-Path $MyInvocation.MyCommand.Path
#если файл с "шарами" существует, то...
if (Test-Path "$ScriptPath\$SourcesFileName") {
    #...подгружаем его содержимое в переменную $BackupDirs
    $BackupDirs=gc "$ScriptPath\$SourcesFileName"
    #и начинаем формирование - файла CSV 
    "Source, Destination" > "$ScriptPath\$CSVListName"
    #для каждой "шары..."
    $BackupDirs|foreach{
        $BackupDir=$_
        #...выделяем имя сервера из UNC-пути
        $ServerName=[regex]::match($BackupDir,'[^\\]+').value
        Write-Verbose "----`nИмя сервера: $ServerName"
        #Создадим массив Фильтров, для отбора файлов backup'а на каждой из "шар" для текущего сервера
        $BackupFilesFilter="*$ServerName*asr*.rar","*$ServerName*.rar"        
        #для каждого фильтра...
        foreach ($filter in $BackupFilesFilter) {
            #...получаем последний созданный файл с backup'ом...
            if ($Source=dir $BackupDir -filter $filter -ErrorAction SilentlyContinue| sort CreationTime -Descending | select -First 1) {
                #
                "G:\_Backup\$ServerName\disk_c\$($Source.Name)"
                if (!(Test-Path "G:\_Backup\$ServerName\disk_c\$($Source.Name)")) {
                    #... и дописываем его полное имя + имя целевой папки в CSV-файл
                    "$($Source.FullName), G:\_Backup\$ServerName\disk_c\" >> "$ScriptPath\$CSVListName"
                    Write-Verbose "$($Source.FullName), G:\_Backup\$ServerName\disk_c\"
                 }
            }
        }
    }
#
#К этому моменту CSV-файл сформирова и готов для скармливания командлету, осуществляющему закачку файлов
#Приступим-с...
$BitJobs=Import-Csv "$ScriptPath\$CSVListName" | Start-BitsTransfer -Asynchronous -Priority Normal -DisplayName CopyMyBackups #-WhatIf
#Сохраним (на всякий случай) в лог перечень созданных заданий
$BitJobs>"$ScriptPath\BitJobs.log"
#Завершаем задачи, которые закончили передачу файлов
while ($CopyMyBackups=Get-BitsTransfer| ?{$_.DisplayName -eq "CopyMyBackups"}) {
    $CopyMyBackups|?{$_.JobState -eq "Transferred"}|Complete-BitsTransfer
    #Впадаем в спячку на заданный промежуток времени, затем идем на новый заход проверки состояния заданий закачки
    Start-Sleep 300
}
}

В первой части скрипта (с троки с 1 по 42) динамически формируется csv-файл, который затем отдается на обработку комадлету Start-BitsTransfer. Эта часть скрипта заточена под конкретную исторически сложившуюся структуру (если это можно так назвать) каталогов, в которых хранятся файлы-бэкапов. На самом деле суть этой части скрипта очень проста:

  • считываем файл, содержащий перечень “шар”, на которых лежат файлы бэкапов
  • на каждой “шаре”, отфильтровываем файлы (согласно заданным фильтрам) и вычисляем по одному самому позднему (по дате создания) файлу для каждого из фильтров.
  • полученные (в предыдущем пункте) полные имена файлов загоняем в csv-файл (попарно с папкой назначения, в которую требуется скопировать отобранные файлы)

Во второй части скрипта (строки с 43 по 54) копируем отобранные файлы с помощью службы BITS

  • стартуем задания закачки в фоновом режиме и с пониженным (относительно дефолтного уровня) приоритетом, присваивая всем заданиям одно и то же имя
  • затем, с интервалом в 5 минут опрашиваем состояние запущенных, именованных заданий закачки и, если задание перешло в состояние “transferred”, завершаем данную закачку

PS Можно подстраховаться и выделить в отдельный скрипт (назовем его “чистельщик”) ту часть скрипта, которая опрашивает состояние закачек и завершает уже отработанные, а, затем,  воткнуть это “чистельщик” в планировщик для регулярного запуска. Если так поступить, то можно спокойно перезагружать компьютер, на котором мы запустили закачки (ведь, после рестарта планировщик будет регулярно запускать наш “чистельщик” для удаления уже отработанных закачек).

11 Comments

  1. я бы добавил еще и расписание работы в котором при условии что наступило время Х выполнялась бы команда Suspend-BitsTransfer

    • А зачем (что-то никак не догоню)? IMHO, в рамках поставленной задачи закачку приостанавливать нет никакой необходимости.

  2. Что касается моего “огромного” bget.cmd, это был скорее хобби-проект по изучению возможностей cmd-скриптига по построению интерактивного пользовательского интерфейса и интерактивного отображения прогресса выполнения задач. Ну а то, что скрипт верно служил мне долгое время и помогал при закачках, особенно больших файлов – приятное дополнение :)

    • О! Александр, рад видеть вас у себя в блоге. На самом деле, очень благодарен вам за идею использования BITS для копирования больших файлов (сам бы я не додумался). А, что касается ваших огромных batch-файлов… То объем работы, который вам пришлось проделать для разработки и отладки, вызывает уважение. Спасибо.

  3. Спасибо за скрипт! Почему то не срабатывает Complete-BitsTransfer
    Все джобы находятся в состоянии Transferred и не завершаются сами.

    PS C:\Users\pldmitry> Get-BitsTransfer -allusers

    JobId DisplayName TransferType JobState OwnerAccount
    —– ———– ———— ——– ————
    b3e4989a-a9ef-4af5-b… CopyMyBackups Download Transferred OS82\pldmitry
    95fe459c-e2b6-4182-a… CopyMyBackups Download Transferred OS82\pldmitry

    Пробовал вручную – результат:

    + Get-BitsTransfer | Complete-BitsTransfer <<<<
    + CategoryInfo : PermissionDenied: (:) [Complete-BitsTransfer], UnauthorizedAccessException
    + FullyQualifiedErrorId : CompleteBitsTransferAuthException,Microsoft.BackgroundIntelligentTransfer.Management.Com
    pleteBitsTransferCommand

    PoSH запускаю As Administrator

    • У меня такой ошибки ни разу не возникало. Посему думаю, что проблема не в скрипте, а в настройках вашего компьютера. Из сообщения об ошибке

      > PermissionDenied UnauthorizedAccessException

      понятно, что где-то каких-то прав не хватает. Вот, только где и каких? ;)

      >Пробовал вручную – результат
      Вы пробовали вручную из-под учетки OS82\pldmitry?

  4. и как осуществить копирование файла, если на другом сервере необходимо логиниться под другим пользователем (логин, пароль) ?!

    • С эти проблем нет. Не помню, есть прараметр Credential у соответсвующего командлета, используемого в скрипте, но даже, если нет, но ничто не помешает вам по старинке подцепиться к \\server_name\share_name или к \\server_name\ipc$ любым известным вам способом (например, при помощи net use)

Leave a Reply

Your email address will not be published. Required fields are marked *

Notify me of followup comments via e-mail. You can also subscribe without commenting.