[CMD] Автообновление файлов из эталонной папки

barret

Пользователь
Сообщения
5
Реакции
0
Баллы
41
Доброго дня всем!

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

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

Скрипт должен работать на XP, поэтому robocopy не применим. Приводимый вариант, как будто, работает, но есть два вопроса:

1. Подскажите, пожалуйста, как можно, не слишком раздувая код, избежать вывода в консоли сообщений от вложенных в циклы FOR команд DIR, в случае, если они не находят в искомой папке файлов (echo off на них не влияет);

2. Каким образом лучше организовать автоматическое закрытие файлов exe, которым требуется обновление, в случае, если они открыты у пользователя (вопрос утерянных данных не актуален).

CMD/BATCH:
@Echo off
SetLocal EnableDelayedExpansion

SET WorkDir=D:\Test
SET SourceDir=%WorkDir%\Source\
SET TargetDir=%WorkDir%\Target\

Call :perebor_target_dirs

goto :end

:perebor_target_dirs

FOR /R %TargetDir% %%t IN (.) DO (

   SET TempTargetDir=%%t
   SET TempTargetDir=!TempTargetDir:~0,-1!

   Call SET TempSourceDir=%SourceDir%%%TempTargetDir:%TargetDir%=%%

   Call :perebor_target_files
   )
EXIT /B

:perebor_target_files

IF not exist !TempSourceDir! (
   Echo SourceDir !TempSourceDir! is not exist
   Goto :next_target_dir
   ) ELSE (
      CD /D !TempTargetDir!
      FOR /F "delims=" %%z IN ('dir /O-N /A-D !TempTargetDir!\*.* /b') DO (
         SET TargetFileDate=%%~tz
         SET TargetFileName=%%~nxz
         SET SourceFileName=!TempSourceDir!!TargetFileName!

         Call :find_in_source

         IF !SourceFileDate! NEQ !TargetFileDate! (

            Echo !DATE! !Time:~0,5! File !TargetFileName! is up to date!
            REM COPY !SourceFileName! !TempTargetDir!
            Echo COPY !SourceFileName! to !TempTargetDir!
         ) ELSE (
            Echo !DATE! !Time:~0,5! File !TargetFileName! is actually.
      )
    )
)
:next_target_dir
EXIT /B

:find_in_source
Echo !SourceFileName!
IF not exist !SourceFileName! (
   Echo SourceFile !SourceFileName! is not exist
   SET SourceFileDate=!TargetFileDate!
   Goto :next_target_file
   ) ELSE (
      CD /D !TempSourceDir!
      FOR /F "delims=" %%a IN ('dir /O-N /A-D !SourceFileName! /b') DO (
         SET SourceFileDate=%%~ta
       )
       CD /D !TempTargetDir!
    )
:next_target_file
EXIT /B

:end
 
Последнее редактирование модератором:

Dragokas

Very kind Developer
Команда форума
Супер-Модератор
Разработчик
Клуб переводчиков
Сообщения
6,403
Реакции
5,907
Баллы
718
Здравствуйте, barret !
Добро пожаловать на SafeZone!

1. Подскажите, пожалуйста, как можно, не слишком раздувая код, избежать вывода в консоли сообщений от вложенных в циклы FOR команд DIR, в случае, если они не находят в искомой папке файлов (echo off на них не влияет);

Вначале или в конце команды dir дописать 2^>NUL

2. Каким образом лучше организовать автоматическое закрытие файлов exe, которым требуется обновление, в случае, если они открыты у пользователя (вопрос утерянных данных не актуален).

Можно и без потери данных.
Windows позволяет переименовывать файл запущенного процесса. Так, вы можете прежде, чем заменять файл, выполнить последовательность таких операций (универсально для любых типов файлов):
1. Произвести попытку удалить файл (.exe или любой другой заменяемый)
2. При неудаче, переименовать его в *.tmp
3. Выполнить замену.
Псевдокодом:
Код:
del /f /a "%file%" || move /y "%file%" "%file%.tmp"
move /y "%source%" "%file%"
Проблемы могут возникнуть только, если вы попытаетесь заменить какой-то другой файл, занятый в эксклюзивном доступе процессом.
Разумеется для файлов *.tmp через какое-то время придётся вызвать "чистильщика" (просто ради того, чтобы почистить мусор). Допустим перед началом каждого обновления выполнить команду:
Код:
del /F /A /Q /S "%directory%\*.tmp"
Это удалит файлы *.tmp рекурсивно, начиная с каталога %directory%

P.S. Ваш скрипт пока не смотрел.
 
Последнее редактирование:

barret

Пользователь
Сообщения
5
Реакции
0
Баллы
41
Dragokas, здравствуйте!

Спасибо за подсказки, и за все те сообщения на этом и других форумах, из которых я узнавал многие не всегда очевидные возможности командного интерпретатора.

В скрипте я, однако, не предусмотрел, насколько по-разному ведут себя команды в плане обработки длинных имен файлов (и папок).
То, что корректно обрабатывало мою тестовую подборку, на реальном наборе файлов уперлось в невозможность для команды dir обработать путь, содержащий пробелы (такой как 'D:\Test\Target\Profiles\Иркутский выпуск (приложение)').

Попытка использовать только короткие имена привела к другой проблеме, упомянутый путь в формате 8.3 превращается в нечто, содержащее тильду (~). Скрипт вылетает с сообщением о внезапной встрече этой самой тильды. Так что вопрос пока повис в воздухе.
 

Dragokas

Very kind Developer
Команда форума
Супер-Модератор
Разработчик
Клуб переводчиков
Сообщения
6,403
Реакции
5,907
Баллы
718
Скрипт вылетает с сообщением о внезапной встрече этой самой тильды.
Первый раз такое слышу. Покажите скриншот, чисто из академического интереса.

невозможность для команды dir обработать путь, содержащий пробелы (такой как 'D:\Test\Target\Profiles\Иркутский выпуск (приложение)').
Это из-за знаков пробела. Все пути нужно обрамлять кавычками.
А ещё там могут возникнуть проблемы в путях со знаками "!", т.к. EnableDelayedExpansion включена.
А ещё со знаками % из-за Call SET.
Ещё с такими знаками как украинские "і", "І", "і".

И по мелочи:
Код:
Goto :next_target_dir
пишите без двоеточия. Иначе, когда-нибудь попадётесь на Undefined behavior. Где-то на соседнем форуме даже выкладывал такую демку с "багом".

Code-style у вас относительно красивый, а вот на счёт скрипта в целом, я бы написал иначе:
CMD/BATCH:
@echo off
SetLocal EnableExtensions
chcp 866 >NUL

SET "WorkDir=c:\temp"
SET "SourceDir=%WorkDir%\Source"
SET "TargetDir=%WorkDir%\Target"

:: считаем кол-во \
set tok=0& for %%a in ("%TargetDir:\=" "%") do set /a tok+=1

:: добавляем поддержку всех символов из кодовой таблицы 1251
set CP=&>NUL chcp 1251>nul

:: перечисляем все файлы в папке TargetDir
for /f "delims=" %%t in ('dir /b /s /a-d "%TargetDir%\*"') do (
  rem служебная
  if not defined CP (set CP=+&>NUL chcp 866)

  rem извлекаем r (относительный путь)
  for /f "delims=\ tokens=%tok%*" %%q in ("%%t") do (

    rem построение пути к источнику
    for /f "delims=" %%s in ("%SourceDir%\%%r") do (
   
      rem проверка условий
      rem 1. Источник существует ?
      if exist "%%s" (
     
        rem 2. Сравнение дат модификации
        if "%%~ts" neq "%%~tt" (
       
          rem копируем из s (источника) в t (целевой). XCOPY - потому что поддерживает копирование скрытых / системных файлов.
          echo Copying "%%s" --^> "%%t"
          echo F|>NUL xcopy /c /h /r /y /k "%%s" "%%t" && echo SUCCESS: "%%t" is up to date! || echo FAILED: to copy "%%s" --^> "%%t"
         
        ) else (
          call echo %%DATE%% %%Time:~0,5%% File "%%t" is actual.
        )
      ) else (
        echo SOURCE: "%%s" is not exists in source!
      )
    )
  )
)
pause
goto :eof
 

barret

Пользователь
Сообщения
5
Реакции
0
Баллы
41
Dragokas, это называется, одномоментность. Только собирался привести свой итоговый вариант, где догадался в том числе закавычить пути, как получил письмо о новом ответе. Теперь, видимо, не стоит захламлять тему.

Ваш вариант явно компактнее и, судя по всему, быстрее. Возьму на вооружение. Ещё раз большая благодарность!

Скриншот ошибки, увы, сейчас не приведу, всё действо у меня происходит в закрытой от внешней сети WinXP, так что и сам скрипт переносил туда-сюда в распечатках, а на домашней системе, Win10, проблема не воспроизводится. Можно было бы снять экран телефоном, но в принципе проблема явно была в отсутствии экранирования служебных символов, и сегодня уже отпала. Там имя папки "ИБ_Иркутский выпуск (приложение)" в коротком виде представлялось как "_()~1", и в командах с использованием этого пути воспринималось неадекватно.

Однако, первая лет за 20 программа, хоть и комом, но всё-таки вышла )
 

Dragokas

Very kind Developer
Команда форума
Супер-Модератор
Разработчик
Клуб переводчиков
Сообщения
6,403
Реакции
5,907
Баллы
718
в коротком виде представлялось как "_()~1"
~ спецсимволом не считается. Проблема была из-за "(". В некоторых случаях ")". В не-"закавыченном" виде воспринимаются как часть синтаксиса, а не пути.
Падало скорее всего на строке с Call Set из-за того, что там используется раскрытие переменных через "%", а не "!", как в остальных местах.
С % переменная сперва раскрывается, а затем происходит синтаксический разбор.
С ! переменная раскрывается уже после синтакс. разбора. По этой же причине все остальные строки у Вас, где !, работали нормально с путями с пробелом, будучи без обрамляющих кавычек.
Это если грубо объяснить.
 
Последнее редактирование:

Dragokas

Very kind Developer
Команда форума
Супер-Модератор
Разработчик
Клуб переводчиков
Сообщения
6,403
Реакции
5,907
Баллы
718
Ваш вариант явно компактнее и, судя по всему, быстрее.
У меня через xcopy (но это внешня утилита). Быстрее будет через copy, но чуть длиннее скрипт (это если захочется с защитой на случай скрытых / системных файлов в источнике).
Да, и я упустил, что вы просили переменовать файл, если он открыт процессом. Добавил это (переименование в *.tmp) и + ещё зачистку старых *.tmp (см. комментарий):

CMD/BATCH:
@echo off
SetLocal EnableExtensions
chcp 866 >NUL

SET "WorkDir=c:\temp"
SET "SourceDir=%WorkDir%\Source"
SET "TargetDir=%WorkDir%\Target"

:: считаем кол-во \
set tok=0& for %%a in ("%TargetDir:\=" "%") do set /a tok+=1

:: добавляем поддержку всех символов из кодовой таблицы 1251
set CP=&>NUL chcp 1251>nul

:: перечисляем все файлы в папке TargetDir
for /f "delims=" %%t in ('dir /b /s /a-d "%TargetDir%\*"') do (
  rem служебная
  if not defined CP (set CP=+&>NUL chcp 866)

  rem извлекаем r (относительный путь)
  for /f "delims=\ tokens=%tok%*" %%q in ("%%t") do (

    rem построение пути к источнику
    for /f "delims=" %%s in ("%SourceDir%\%%r") do (
   
      rem проверка условий
      rem 1. Источник существует ?
      if exist "%%s" (
     
        rem 2. Сравнение дат модификации
        if "%%~ts" neq "%%~tt" (
       
          rem копируем из s (источника) в t (целевой).
          echo Copying "%%s" --^> "%%t"
          2>NUL >&2 copy /y "%%s" "%%t" && echo SUCCESS: "%%t" is up to date! || (
         
            rem если ошибка из-за того, что файл загружен как образ процесса
            move /y "%%t" "%%t.tmp"
           
            rem XCOPY, если ошибка из-за того, что файл в источнике скрытый и/или системный.
            echo F|>NUL xcopy /c /h /r /y /k "%%s" "%%t" && echo SUCCESS: "%%t" is up to date! || (
         
              echo FAILED: to copy "%%s" --^> "%%t"
          ))
        ) else (
          call echo %%DATE%% %%Time:~0,5%% File "%%t" is actual.
        )
      ) else (
        rem Если файла нет в исходной папке, проверим причину:
        rem Если в целевой папке прошлый раз файл был переименован в *.tmp потому, что был открыт процессом, удаляем его как мусор
        if "%%~xt"==".tmp" (del /f /a "%%t") else (echo SOURCE: "%%s" is not exists in source!)
      )
    )
  )
)
pause
goto :eof
 

barret

Пользователь
Сообщения
5
Реакции
0
Баллы
41
Добрый день!

Сегодня в процессе боевого тестирования обнаружилась проблема, которая заставила задуматься о необходимости очередной доработки.
Судя по всему, представление дат в операторе сравнения IF (или во всём интерпретаторе) специфическое. Во всяком случае, часть явно устаревших файлов алгоритм не посчитал таковыми.

Чтобы проиллюстрировать ситуацию, сделал скрин:

DateQuest.jpg

Из него видно, что скрипт верно определяет даты файлов, но для некоторых случаев (подчеркнул красным), не считает их достаточно разными :)

Причем речь во всех замеченных случаях идёт о разнице в несколько лет. Хотя и большую разницу скрипт иногда "замечает". Есть ли у кого-нибудь опыт столкновения с такой хитростью?

Игра с переводом дат в строковые значения и последующим разбиением на годы, месяцы, часы представляется избыточной.
 
Последнее редактирование:

Dragokas

Very kind Developer
Команда форума
Супер-Модератор
Разработчик
Клуб переводчиков
Сообщения
6,403
Реакции
5,907
Баллы
718
barret написал(а):
Так нельзя делать!
Сравнение данных, не являющихся числом, с помощью операторов логики является всегда текстовым. Вы могли бы сравнивать таким образом даты ТОЛЬКО, если бы они были написаные в американском формате (YYYY/MM/DD).

Возможен ли у Вас сценарий, когда данные в источнике старее, чем в целевом каталоге?
 

barret

Пользователь
Сообщения
5
Реакции
0
Баллы
41
Dragokas, приветствую!

Возможен ли у Вас сценарий, когда данные в источнике старее, чем в целевом каталоге?

Да, такой вариант как раз возможен, и я пытался его предусмотреть. Несколько самонадеянно, как выясняется. Посоветуете, как сравнить даты именно на больше/меньше?
 

Dragokas

Very kind Developer
Команда форума
Супер-Модератор
Разработчик
Клуб переводчиков
Сообщения
6,403
Реакции
5,907
Баллы
718
Для копирования файлов только в том случае, если их дата модификации новее, чем целевой файл, можно воспользоваться XCOPY с ключем /d без параметров.
При этом один условный блок if нужно будет удалить.
Но усложнится обработчик ошибок, т.к. Вам придётся каким-то образом отличать ошибку при копировании и отказ в копировании по причине более старой даты модификации в источнике.
Итак, если в источнике старая дата - Errorlevel вернёт 0, как при успешном копировании, а на экране вы увидите:
Скопировано файлов: 0.
Во всех остальных случаях Errorlevel будет ненулевой.
Либо просто повторно сравнить даты.

Итоговый вариант я переделал, но у меня возникла одна проблемка:
что делать с файлами запущенных процессов?
Дело в том, что их нельзя заранее переименовать в *.bak, потому что команда XCOPY не сможет понять какой из файлов новее, т.к. целевого не будет существовать.

Вот вариант А, который мне видится немного кривоватым, но это единственное что могу предложить на базе XCOPY (если не переходить к плану Б.: отказ от XCOPY и переворот даты с помощью подпрограммы, дабы получить американский формат).

1. Ничего не трогаем, запускаем XCOPY. Пусть сама определит, какой файл новее.
Если ошибка:
2. перемещаем file.exe в file.exe.bak
3. копируем file.exe.bak в file.exe
4. повторяем XCOPY.

Ну а если посмотреть на Ваш вариант, там Вы вообще все файлы, я смотрю, всегда копируете в bak ?
Так, в таком случае можно сразу пропустить пункт 1, и делать 2,3,4.

Как делаем?
Вообщем, вот этот вариант:
CMD/BATCH:
@echo off
SetLocal EnableExtensions
chcp 866 >NUL

SET "WorkDir=c:\temp"
SET "SourceDir=%WorkDir%\Source"
SET "TargetDir=%WorkDir%\Target"

:: считаем кол-во \
set tok=0& for %%a in ("%TargetDir:\=" "%") do set /a tok+=1

:: добавляем поддержку всех символов из кодовой таблицы 1251
set CP=&>NUL chcp 1251>nul

:: перечисляем все файлы в папке TargetDir
for /f "delims=" %%t in ('dir /b /s /a-d "%TargetDir%\*"') do (
  rem служебная
  if not defined CP (set CP=+&>NUL chcp 866)

  rem извлекаем r (относительный путь)
  for /f "delims=\ tokens=%tok%*" %%q in ("%%t") do (

    rem построение пути к источнику
    for /f "delims=" %%s in ("%SourceDir%\%%r") do (
 
      rem проверка условий
      rem 1. Источник существует ?
      if exist "%%s" (
   
        rem резервное копирование, а также переименование файла, если он является образом запущенного процесса
        attrib -s -h "%%t"
        move /y "%%t" "%%t.bak" >NUL
        copy "%%t.bak" "%%t" >NUL
       
        rem копируем из s (источника) в t (целевой).
        echo Copying "%%s" --^> "%%t"
       
        echo F|>NUL xcopy /c /h /r /y /k /d "%%s" "%%t" && (

          rem o - old target
          for %%o in ("%%t.bak") do (
            rem выполнено ли копирование?
            if "%%~ts"=="%%~tt" (
              rem у старого файла дата отличалась?
              if "%%~ts" neq "%%~to" (
                echo SUCCESS: "%%t" is up to date!
              ) else (
                call echo %%DATE%% %%Time:~0,5%% File "%%t" is actual.
              )
            ) else (
              call echo %%DATE%% %%Time:~0,5%% File "%%t" is actual.
            )
          )
        ) || echo FAILED: to copy "%%s" --^> "%%t"
      ) else (
        if "%%~xt" neq ".bak" echo SOURCE: "%%s" is not exists in source!
      )
    )
  )
)
pause
goto :eof
 

PinkZebra

Пользователь
Сообщения
4
Реакции
0
Баллы
41
а если сравнивать файлы по Хэш?
я использую
Код:
gpg --print-md md5 имя файл
для получения хэш
результат получается в виде
Код:
имя файл: F1 7B BE 5C 77 A8 EA B7  36 9D CF 4C 7C FE 20 28
можно сделать что то типа?
Код:
 if "gpg --print-md md5 %%s" neq "gpg --print-md md5 %%е" (
Всем доброго дня.
это получается нужно делать через временные файлы?

Код:
gpg --print-md md5 1.cmd > 0.tmp
gpg --print-md md5 1.cmd > 1.tmp
set /p AA="" <0.tmp
set /p BB="" <1.tmp
if "%AA%"=="%BB%" (
echo yes
) else (
echo no
)
del 0.tmp
del 1.tmp
 

PinkZebra

Пользователь
Сообщения
4
Реакции
0
Баллы
41
Товарищи объяснтие мне что происходит в вашем скрипте начиная с это строчки до нее я еще понимаю что происходит)
Код:
 rem извлекаем r (относительный путь)
  for /f "delims=\ tokens=%tok%*" %%q in ("%%t") do
 

PinkZebra

Пользователь
Сообщения
4
Реакции
0
Баллы
41
В общем плане пытаюсь решить подобную задачу. Вижу это так "Рабочая папка", "Суточная копия", "Архивная", скрипт сверяет(по имени и хэш) папки "суточная" и "рабочая" если в "рабочей" файл обновился, то в "суточной" папке файл с таким же именем архивируется в папку "архивную" и далее в папку названную по имени файла. Далее файл который сравнивали копируется из "рабочей" в "суточную". Если файл не найден следует действие в папке "архивная" папка с именем файла меняет название по "шаблону".
 

PinkZebra

Пользователь
Сообщения
4
Реакции
0
Баллы
41
Сверху Снизу