Недавно, работая в VBA, я попытался переименовать группу файлов, расположенных в длинных, вложенных директориях. Неожиданно возникли ошибки, которые не позволяли это осуществить. Оказалось, что в Windows 10 (тем более в более ранних версиях) существуют ограничения на длину путей (см., к примеру https://learn.microsoft.com/ru-ru/windows/win32/fileio/maximum-file-path-limitation?tabs=registry). Решения, найденные в результате поиска не принесли результата. Да, для манипуляции с длинными путями необходимо разрешить их в реестре ( раздел Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled (Type: REG_DWORD)
реестра должен существовать и иметь значение 1), но даже если они будут разрешены, манипулировать вы ими не можете, т.к. сам проводник Windows не позволяет работать с длинными путями. Возможно, скрипт VBA, при манипуляции с файлами использует проводник Windows. С другой стороны, с длинными путями хорошо работает проводник 7-Zip File Manager, при этом он имеется практически на каждом компьютере. Если это не так - его легко установить.
Возникла идея обойти ограничения Windows и использовать для манипуляции с файлами именно проводник 7-Zip File Manager. В результате получился рабочий скрипт, который позволяет производить перемещение и переименование файлов с длинными путями.
'Пример использования функции перемещения файлов с длинными путями
Public Sub Per_Files()
Dim sDir As String, dDir As String, old_name As String, new_name As String
sDir = "C:\1\" 'Исходная папка"
dDir = "C:\2\" 'Целевая папка
old_name = "1.pdf" 'Копируемый файл: старое имя
new_name = "1-1.pdf" 'Копируемый файл: новое имя
Call cp7z(sDir, dDir, old_name, new_name)
End Sub
'Подпрограмма перемещения файлов с длинными путями с помощью архиватора 7z
Public Sub cp7z(ByVal sDir As String, ByVal dDir As String, ByVal oldFile As String, ByVal nFile As String)
Dim PrDir As String, tDir As String, comstr As String
PrDir = """C:\Program Files\7-Zip\7z.exe""" 'Расположение исполняемого файла 7z
tDir = "C:\tmp\tmp.7z" 'Вспомогательная папка и файл
'Проверка существования файла tmp.7z. Если такой файл есть - подбирается новое свободное имя
Do While Dir(tDir) <> ""
tDir = "C:\tmp\" & WorksheetFunction.RandBetween(1, 1000) & "tmp.7z"
' MsgBox tDir
Loop
'Создание архива C:\tmp\*tmp.7z из исходного файла с длинным путём (без компрессии, т.е. копирование в файл *tmp.7z)
comstr = PrDir & " a -mx0 " & tDir & " " & Chr(34) & sDir & oldFile & Chr(34)
Debug.Print comstr
ShellAndWait comstr
'Переименование файла в архиве C:\tmp\*tmp.7z старое имя -> новое имя
comstr = PrDir & " rn " & tDir & " " & oldFile & " " & nFile
Debug.Print comstr
ShellAndWait comstr
'Копирование файла из архива C:\tmp\*tmp.7z в целевую папку
comstr = PrDir & " e -y " & tDir & " -o" & Chr(34) & dDir & Chr(34)
Debug.Print comstr
ShellAndWait comstr
'Удаление вспомогательного файла
Kill tDir
End Sub
'Подпрограмма запуска процесса 7z с ожиданием завершения процесса
Sub ShellAndWait(pathFile As String)
Dim WshShell As Object
Set WshShell = CreateObject("Wscript.Shell")
WshShell.Run pathFile, 0, True 'Обязательно True, процесс должен завершиться,
'иначе команды скрипта начнут выполняться раньше срока и скрипт не будет работать
End Sub
Как видно из скрипта, необходимый файл архивируется архиватором 7z во вспомогательную папку во временный файл (без сжатия), а затем распаковывается в файл по новому пути. Временный файл удаляется. При этом файл может быть переименован. В данном случае, в скрипте, в качестве исходных и целевых папок используются папки "C:\1\" и "C:\2\", но в вашем скрипте вы можете задавать пути (в виде переменных string) любой длины и вложенности.
Данный скрипт осуществляет копирование + переименование файлов. Вы можете реализовать функции перемещения, слегка модифицировав скрипт (добавив удаление исходного файла).
И не забывайте, что для функционирования данного скрипта, у вас на компьютере должен быть установлен архиватор 7z и в скрипте необходимо прописать правильный путь к исполняемому файлу архиватора (переменная PrDir).
Комментарии (13)
aamonster
24.10.2023 17:35+2Так вы пути-то длинные в своём скрипте записывали в виде \?\...? Или 7zip только для этого и использовали (типа в скрипте обычная запись, а в эту извращённую пусть 7zip переводит)?
Kutush Автор
24.10.2023 17:35+1Пробовал, и много различных способов записи длинных путей - не получается при использовании стандартных функций манипуляции файлов VBA. Не позволяет.
mesvobodnye
24.10.2023 17:35Для перемещения таких файлов используем Тотал Коммандер
Kutush Автор
24.10.2023 17:35Допустимо, если это 2-3 файла. У меня была задача переименовать тысячи файлов в длинных вложенных папках с именами в которых содержалось полное название госта + допинфа (очень очень длинное) - то есть в ручную это сделать нереально.
Может быть вы знаете как использовать тотал в VBA подобно вышеописанному способу 7z? Подскажите, можно ли из строки шела выполнять команды Тотала на копирование, перемещение и тп.
Kutush Автор
24.10.2023 17:35Поискал сегодня - на сайте разработчика есть раздел по работе с шелом: https://www.ghisler.ch/wiki/index.php?title=Command_line_parameters
К сожалению, не имеется команд для манипуляции с файлами.
B13nerd
24.10.2023 17:35+41) Если VBA (не VBS), почему не использовать WinAPI CopyFile() с добавлением к пути "\\?\" ?
2) Dim sDir, dDir, old_name, new_name As String - типичная ошибка (так сказать). sDir, dDir, old_name будут описаны как Variant.
Kutush Автор
24.10.2023 17:35sDir, dDir, old_name будут описаны как Variant
Действительно, спасибо, поправил скрипт.
belch84
24.10.2023 17:35+1Когда-то очень давно решал в чем-то похожую задачу для FoxPro for DOS. Необходимо было обеспечить работу с файлами, имеющими длинные имена для приложения, которое понимало только короткие имена файлов в формате 8.3 (максимум 8 символов на имя и 3 на расширение имени, для имен папок тоже). Для совместимости в файловых системах тех времен (FAT32 или NTFS) поддерживались одновременно длинное и короткое имена файла (не знаю, как обстоит дело сейчас). Идея состояла в том, чтобы определять короткое имя по длинному и наоборот при помощи MS-DOS команды DIR. При соответствующих параметрах эта команда для заданного файла выдает его длинное и короткое имена, которые можно записать во временный файл, а затем извлечь из него
Код программы на языке FoxPro (очень похожем на BASIC)
* set talk off * S = fnm_cnv('H:\Накладные_вход', .T.) * S = fnm_cnv('H:\_1905~1') * ?S * fnm_cnv('C:\USER1\EXP_DPA\temp3dasjdajsd\',.T.) FUNCTION fnm_cnv PARAMETER m.fnm, m.src_long * преобразование длинных имен файлов в короткие и наоборот PRIVATE S, is_long is_long = type('m.src_long') = 'L' and m.src_long IF getenv('OS') = 'Windows_NT' S = getnm_NT(m.fnm, m.is_long) ELSE S = getnm_98(m.fnm, m.is_long) ENDIF RETURN S FUNCTION getnm_NT PARAMETER m.fnm, m.src_long * преобразование длинного имени файла в короткое или наоборот * fnm - исходное имя файла * src_long - ИСТИНА для преобразования длинного в короткое, * не задано или ЛОЖЬ для обратного пр-ния * Исходное имя должно быть задано с полным путем! IF type('UDir') # 'C' PRIVATE UDir UDir = '' ENDIF PRIVATE src_nm, tmp_nm, bat_nm, h, I, S, p1, ; p2, OS_NT, cur_dir, cur_nm, long_nm, short_nm, drv, ; is_dir, success, long2short, result tmp_nm = UDir + '__tmp__.txt' bat_nm = UDir + 'longdir.bat' result = '' success = .F. OS_NT = (getenv('OS') = 'Windows_NT') long2short = type('m.src_long') = 'L' and m.src_long DIMENSION tmp_arr[1] IF not m.OS_NT WAIT WINDOW 'GETNM_NT: неверная версия операционной системы' RETURN m.result ENDIF src_nm = alltrim(m.fnm) IF at(':\', m.src_nm) = 0 WAIT WINDOW 'GETNM_NT: неверный полный путь для файла' + chr(13) + ; m.src_nm RETURN m.result ENDIF drv = left(m.src_nm,3) IF not m.long2short and not file(m.src_nm) result = '' RETURN m.result ENDIF * обрабатываем полный путь к файлу последовательно FOR I = 1 TO 999 * определяем, является ли текущий элемент именем * оглавления или файла p1 = at('\',m.src_nm, I+1) is_dir = (p1 > 0) * выделяем для текущего элемента его имя и имя оглавления, * в котором он содержится p2 = at('\', m.src_nm, I) cur_dir = left(m.src_nm, p2) IF m.is_dir cur_nm = substr(m.src_nm,p2+1,p1-p2-1) ELSE cur_nm = substr(m.src_nm, p2 + 1) ENDIF * с помощью команды DIR формируем текстовый файл, * в котором содержатся длинное и короткое имена тек.элемента * создаем временный BAT-файл для запуска ERASE (bat_nm) h = fcreate(bat_nm) = fputs(h, '@echo off') IF m.is_dir = fputs(h, 'DIR ' + m.cur_dir + '*.*' + ' /X /D >' + m.tmp_nm) ELSE = fputs(h, 'DIR ' + m.cur_dir + ' /X >' + m.tmp_nm) ENDIF = fclose(h) * выполняем BAT-файл и формирем текстовый файл с результатом DIR ERASE (m.tmp_nm) RUN cmd /c &bat_nm IF not file(m.tmp_nm) WAIT WINDOW 'Ошибка при создании файла, содержащего длинные имена' result = '' RETURN m.result ENDIF * удаляем BAT-файл ERASE (bat_nm) * открываем файл с результатми для анализа h = fopen(m.tmp_nm) * если первая строка пустая - пропускаем ее S = fgets(h,512) IF empty(S) S = fgets(h,512) ENDIF * если в текущей строке инф. о томе - пропускаем ее IF not feof(h) and ; ('том в устройстве' $ lower(S) or 'volume in drive' $ lower(S)) S = fgets(h,512) ENDIF * если в текущей строке серийный номер тома - пропускаем ее IF not feof(h) and ; ('серийный номер тома' $ lower(S) or 'volume serial number' $ lower(S)) S = fgets(h,512) ENDIF * если текущая строка пустая - пропускаем ее IF empty(S) S = fgets(h,512) ENDIF * пропускаем имя текущего оглавления IF not feof(h) S = fgets(h,512) ENDIF * отыскиваем текущий элемент в файле с результатами DIR и * преобразуем его нужным способом success = .F. DO WHILE not feof(h) and not success S = fgets(h, 512) IF empty(left(S,12)) or empty(substr(S,50)) LOOP ENDIF short_nm = trim(substr(S,37,12)) long_nm = trim(substr(S, 50)) IF not m.long2short success = (upper(m.cur_nm) == upper(m.long_nm)) or ; (upper(m.cur_nm) == upper(m.short_nm)) ELSE * длинное имя в короткое success = (upper(m.cur_nm) == upper(m.long_nm)) ENDIF ENDDO = fclose(h) ERASE (m.tmp_nm) IF not m.success EXIT ENDIF IF not m.long2short result = m.result + m.long_nm ELSE IF not empty(m.short_nm) result = m.result + m.short_nm ELSE result = m.result + m.long_nm ENDIF ENDIF IF m.is_dir result = m.result + '\' ENDIF IF not m.is_dir EXIT ENDIF ENDFOR IF m.success and not empty(m.result) result = m.drv + m.result ENDIF RETURN m.result FUNCTION getnm_98 PARAMETER m.fnm, m.src_long * преобразование длинного имени файла в короткое или наоборот * fnm - исходное имя файла * src_long - ИСТИНА для преобразования длинного в короткое, * не задано или ЛОЖЬ для обратного пр-ния * Исходное имя должно быть задано с полным путем! IF type('UDir') # 'C' PRIVATE UDir UDir = '' ENDIF PRIVATE src_nm, tmp_nm, bat_nm, h, I, S, p1, ; p2, OS_NT, cur_dir, cur_nm, long_nm, short_nm, drv, ; is_dir, success, long2short, result tmp_nm = UDir + '__tmp__.txt' bat_nm = UDir + 'longdir.bat' result = '' success = .F. OS_NT = (getenv('OS') = 'Windows_NT') long2short = type('m.src_long') = 'L' and m.src_long DIMENSION tmp_arr[1] IF m.OS_NT WAIT WINDOW 'GETNM_98: неверная версия операционной системы' RETURN m.result ENDIF src_nm = alltrim(m.fnm) IF at(':\', m.src_nm) = 0 WAIT WINDOW 'GETNM_98: неверный полный путь для файла' + chr(13) + ; m.src_nm RETURN m.result ENDIF drv = left(m.src_nm,3) IF not m.long2short and not file(m.src_nm) result = '' RETURN m.result ENDIF * обрабатываем полный путь к файлу последовательно FOR I = 1 TO 999 * определяем, является ли текущий элемент именем * оглавления или файла p1 = at('\',m.src_nm, I+1) is_dir = (p1 > 0) * выделяем для текущего элемента его имя и имя оглавления, * в котором он содержится p2 = at('\', m.src_nm, I) cur_dir = left(m.src_nm, p2) IF m.is_dir cur_nm = substr(m.src_nm,p2+1,p1-p2-1) ELSE cur_nm = substr(m.src_nm, p2 + 1) ENDIF * с помощью команды DIR формируем текстовый файл, * в котором содержатся длинное и короткое имена тек.элемента * создаем временный BAT-файл для запуска ERASE (bat_nm) h = fcreate(bat_nm) = fputs(h, '@echo off') IF m.is_dir = fputs(h, 'DIR "' + m.cur_dir + '*.*' + '" >' + m.tmp_nm) ELSE = fputs(h, 'DIR "' + m.cur_dir + '" >' + m.tmp_nm) ENDIF = fclose(h) * выполняем BAT-файл и формирем текстовый файл с результатом DIR RUN &bat_nm IF not file(m.tmp_nm) WAIT WINDOW 'Ошибка при создании файла, содержащего длинные имена' RETURN 0 ENDIF * удаляем BAT-файл ERASE (bat_nm) * открываем файл с результатми для анализа h = fopen(m.tmp_nm) * если первая строка пустая - пропускаем ее S = fgets(h,512) IF empty(S) S = fgets(h,512) ENDIF * если в текущей строке инф. о томе - пропускаем ее IF not feof(h) and ; ('том в устройстве' $ lower(S) or 'volume in drive' $ lower(S)) S = fgets(h,512) ENDIF * если в текущей строке серийный номер тома - пропускаем ее IF not feof(h) and ; ('серийный номер тома' $ lower(S) or 'volume serial number' $ lower(S)) S = fgets(h,512) ENDIF * если текущая строка пустая - пропускаем ее IF empty(S) S = fgets(h,512) ENDIF * пропускаем имя текущего оглавления IF not feof(h) S = fgets(h,512) ENDIF * если текущая строка пустая - пропускаем ее IF empty(S) S = fgets(h,512) ENDIF * отыскиваем текущий элемент в файле с результатами DIR и * преобразуем его нужным способом success = .F. DO WHILE not feof(h) and not success S = fgets(h, 512) IF empty(left(S,12)) or empty(substr(S,45)) LOOP ENDIF short_nm = left(S, 12) IF not empty(right(m.short_nm,3)) short_nm = trim(left(m.short_nm,8)) + '.' + right(m.short_nm,3) ELSE short_nm = trim(left(m.short_nm,8)) ENDIF short_nm = trim(m.short_nm) long_nm = trim(substr(S, 45)) IF not m.long2short success = (upper(m.cur_nm) == upper(m.long_nm)) or ; (upper(m.cur_nm) == upper(m.short_nm)) ELSE * длинное имя в короткое IF empty(m.short_nm) success = (upper(m.cur_nm) == upper(m.long_nm)) ELSE success = (upper(m.cur_nm) == upper(m.short_nm)) ENDIF ENDIF ENDDO = fclose(h) ERASE (m.tmp_nm) IF not m.success EXIT ENDIF IF not m.long2short result = m.result + m.long_nm ELSE IF not empty(m.short_nm) result = m.result + m.short_nm ELSE result = m.result + m.long_nm ENDIF ENDIF IF m.is_dir result = m.result + '\' ENDIF IF not m.is_dir EXIT ENDIF ENDFOR IF m.success and not empty(m.result) result = m.drv + m.result ENDIF RETURN m.result
Программа работала под Windows 98 и Windows XP, для них способ конвертации имен несколько отличался.
Точно так же можно было бы организовать переименование файлов с длинными именами, используя команду RENAME, или копирование файлов (команда COPY)
IDDQDesnik
24.10.2023 17:35Создание имен 8.3 сейчас много где выключено по соображения производительности. Проверить можно выполнив от Администратора:
fsutil 8dot3name query C:
fido_max
А еще 8192 символ в cmd консольке пропадает. Причем строка не обрезается, а просто символ пропадает. https://stackoverflow.com/questions/2916865/how-to-get-around-the-command-line-length-limit
Kutush Автор
Но это по крайней мере не ограничение в 260 символов ). Я не думаю, что в практической деятельности встречаются пути длиной более 8000 символов...