Blog. Just Blog

Автогенерация имени файла при наличии дублей

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Образ мышления: Assembler | Автор: ManHunter
При разработке одной из своих программ передо мной встала задача сохранения файлов, но не просто так, а чтобы имя файла автоматически изменялось, если такой файл уже существует. С подобной задачей очень хорошо справляются менеджеры закачек и браузеры, в случае совпадения дополняя имя нового файла числовым индексом. В результате у меня получилась вот такая функция:
  1. ;---------------------------------------------------------------
  2. ; Функция автогенерации имени файла
  3. ;---------------------------------------------------------------
  4. ; lpOldName - указатель на проверяемое имя файла
  5. ; lpNewName - указатель на доступное имя файла
  6. ;---------------------------------------------------------------
  7. proc generate_filename lpOldName:DWORD, lpNewName:DWORD
  8.         locals
  9.             fname    dd ?
  10.             fext     dd ?
  11.             filename rb MAX_PATH
  12.             buff     rb MAX_PATH
  13.             idx      dd ?
  14.         endl
  15.  
  16.         pusha
  17.  
  18.         ; Получить полный путь к файла и указатель на имя
  19.         lea     edi,[filename]
  20.         lea     eax,[fname]
  21.         invoke  GetFullPathName,[lpOldName],MAX_PATH,edi,eax
  22.  
  23.         ; Проверить наличие файла или каталога
  24.         invoke  PathFileExists,edi
  25.         or      eax,eax
  26.         ; Файла нет, сразу вернуться
  27.         jz      .loc_use_filename
  28.  
  29.         ; Индекс для автогенерации имени файла
  30.         mov     [idx],1
  31.  
  32.         ; Поискать расширение в названии файла
  33.         invoke  lstrlen,edi
  34.         add     edi,eax
  35.         mov     [fext],edi
  36.         std
  37.         mov     ecx,edi
  38.         sub     ecx,[fname]
  39.         mov     al,'.'
  40.         repne   scasb
  41.         cld
  42.         or      ecx,ecx
  43.         jz      @f
  44.         ; Расширение найдено
  45.         inc     edi
  46.         mov     byte [edi],0
  47.         inc     edi
  48.         mov     [fext],edi
  49. @@:
  50.         ; Сгенерировать имя файла с индексом
  51.         lea     edi,[buff]
  52.         lea     ebx,[filename]
  53.         invoke  wsprintf,edi,.mask,ebx,[idx],[fext]
  54.         add     esp,20
  55.  
  56.         ; Проверить наличие файла или каталога
  57.         invoke  PathFileExists,edi
  58.         or      eax,eax
  59.         ; Файла нет, использовать это имя
  60.         jz      .loc_use_filename
  61.  
  62.         ; Следуюший номер счетчика
  63.         inc     [idx]
  64.         jmp     @b
  65.  
  66. .loc_use_filename:
  67.         ; Скопировать имя файла в буфер-приемник
  68.         invoke  lstrcpy,[lpNewName],edi
  69.  
  70.         popa
  71.         ret
  72.  
  73.         ; Маска для автогенерации имени файла
  74.         .mask   db '%s (%i).%s',0
  75. endp
Функции передаются два параметра: lpOldName - указатель на проверяемое имя файла и lpNewName - указатель на строку, куда будет записано первое из доступных имен файлов. Если дублей нет, то это будет оригинальное имя, если дубли есть, то имя файла будет дополнено числовым индексом в скобках. Например, если проверяется файл "filename.txt", а такой файл уже есть, то первое доступное имя файла будет "filename (1).txt". Если и такой файл существует, то имя будет "filename (2).txt", и так далее до первого свободного индекса в счетчике. Использование функции PathFileExists позволяет проверять наличие как файлов, так и каталогов. Формат добавляемого числового индекса задается маской, можете менять ее по своему усмотрению.

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

Как правильно подсказали в комментариях, при очень большом количестве файлов перебор имен по единичке может сильно тормозить. Для решения этой проблемы алгоритм поиска свободного имени файла изменяется. Стадия 1: индекс увеличивается с шагом 1000 до первого свободного имени. Стадия 2: индекс уменьшается с шагом 100 до первого занятого имени. Стадия 3: индекс увеличивается с шагом 10 до первого свободного имени. Финальная стадия: индекс уменьшается на единицу до первого занятого имени. Даже при нескольких тысячах файлов количество итераций не будет превышать 20-30.
  1. ;---------------------------------------------------------------
  2. ; Функция автогенерации имени файла
  3. ;---------------------------------------------------------------
  4. ; lpOldName - указатель на проверяемое имя файла
  5. ; lpNewName - указатель на доступное имя файла
  6. ;---------------------------------------------------------------
  7. proc generate_filename lpOldName:DWORD, lpNewName:DWORD
  8.         locals
  9.             fname    dd ?
  10.             fext     dd ?
  11.             filename rb MAX_PATH
  12.             buff     rb MAX_PATH
  13.             idx      dd ?
  14.         endl
  15.  
  16.         pusha
  17.  
  18.         ; Получить полный путь к файла и указатель на имя
  19.         lea     edi,[filename]
  20.         lea     eax,[fname]
  21.         invoke  GetFullPathName,[lpOldName],MAX_PATH,edi,eax
  22.  
  23.         ; Проверить наличие файла или каталога
  24.         invoke  PathFileExists,edi
  25.         or      eax,eax
  26.         ; Файла нет, сразу вернуться
  27.         jz      .loc_use_filename
  28.  
  29.         ; Поискать расширение в названии файла
  30.         invoke  lstrlen,edi
  31.         add     edi,eax
  32.         mov     [fext],edi
  33.         std
  34.         mov     ecx,edi
  35.         sub     ecx,[fname]
  36.         mov     al,'.'
  37.         repne   scasb
  38.         cld
  39.         or      ecx,ecx
  40.         jz      @f
  41.         ; Расширение найдено
  42.         inc     edi
  43.         mov     byte [edi],0
  44.         inc     edi
  45.         mov     [fext],edi
  46. @@:
  47.         ; Индекс для автогенерации имени файла
  48.         mov     [idx],1
  49.  
  50.         stdcall .generate_filename
  51.         or      eax,eax
  52.         ; Сразу использовать это имя
  53.         jz      .loc_use_filename
  54.  
  55. ;------------------------------------------------------
  56. ; Стадия 1 - инкрементальный поиск с шагом 1000
  57. ;------------------------------------------------------
  58. .stage_1:
  59.         add     [idx],1000
  60.         stdcall .generate_filename
  61.         or      eax,eax
  62.         jnz     .stage_1
  63.  
  64. ;------------------------------------------------------
  65. ; Стадия 2 - декрементальный поиск с шагом 100
  66. ;------------------------------------------------------
  67. .stage_2:
  68.         sub     [idx],100
  69.         stdcall .generate_filename
  70.         or      eax,eax
  71.         jz      .stage_2
  72.  
  73. ;------------------------------------------------------
  74. ; Стадия 3 - инкрементальный поиск с шагом 10
  75. ;------------------------------------------------------
  76. .stage_3:
  77.         add     [idx],10
  78.         stdcall .generate_filename
  79.         or      eax,eax
  80.         jnz     .stage_3
  81.  
  82. ;------------------------------------------------------
  83. ; Стадия 4 - декрементальный поиск с шагом 1
  84. ;------------------------------------------------------
  85. .stage_4:
  86.         dec     [idx]
  87.         stdcall .generate_filename
  88.         or      eax,eax
  89.         jz      .stage_4
  90.  
  91.         ; Окончательный индекс
  92.         inc     [idx]
  93.         stdcall .generate_filename
  94.  
  95. .loc_use_filename:
  96.         ; Скопировать имя файла в буфер-приемник
  97.         invoke  lstrcpy,[lpNewName],edi
  98.  
  99.         jmp     .loc_ret
  100.  
  101. .generate_filename:
  102.         ; Сгенерировать имя файла с индексом
  103.         lea     edi,[buff]
  104.         lea     ebx,[filename]
  105.         invoke  wsprintf,edi,.mask,ebx,[idx],[fext]
  106.         add     esp,20
  107.         ; Проверить наличие файла или каталога
  108.         invoke  PathFileExists,edi
  109.         retn
  110.  
  111. .loc_ret:
  112.         popa
  113.         ret
  114.  
  115.         ; Маска для автогенерации имени файла
  116.         .mask   db '%s (%i).%s',0
  117. endp
Такой вариант поиска отлично срабатывает при инкременте максимального значения индекса, но с заполнением "дырок" в нумерации может не сработать. Но, как мне кажется, если возникает ситуация с подобным массовым размножением файлов, то правильнее как раз использовать инкремент максимального индекса. Кроме того, этот алгоритм обладает значительным "запасом прочности", первую стадию можно запустить с шагом 10000 или даже 100000, шаг итерации в остальных стадиях изменится на соответствующий и надо будет добавить одну или несколько дополнительных стадий до единичного шага. Таким образом, даже на сотнях тысяч файлов общее количество шагов поиска не будет превышать lg(шаг первой стадии)*10.

В приложении примеры программ с исходным кодом, создающие при каждом запуске пустой файл filename.txt. Имя файла меняется в зависимости от того, какие имена файлов уже заняты.

Примеры программ с исходными текстами (FASM)Примеры программ с исходными текстами (FASM)

Autogenerate.Filename.Demo.zip (4,160 bytes)


Поделиться ссылкой ВКонтакте
Просмотров: 2658 | Комментариев: 13

Внимание! Статья опубликована больше года назад, информация могла устареть!

Комментарии

Отзывы посетителей сайта о статье
ManHunter (08.02.2016 в 14:38):
Добавил второй вариант алгоритма. Аттач тоже обновил.
ManHunter (08.02.2016 в 07:57):
Ценное уточнение, спасибо! Но я бы сделал не так.
1. Инкремент индекса с шагом 1000 до первого НЕ найденного файла.
2. Декремент индекса с шагом 100 до первого найденного файла.
3. Инкремент индекса с шагом 10 до первого НЕ найденного файла.
4. Декремент индекса с шагом 1  до первого найденного файла.
5. Профит!
Даже при нескольких десятках тысяч файлов суммарно будет выполнено всего 20-30 итераций. Допишу и добавлю такой вариант тоже.
Сергей Качалов (08.02.2016 в 01:51):
Если одноименных файлов наберется достаточно большое количество, алгоритм начинает нехило тормозить. Был прецедент в банке. Алгоритм изменил следующим образом: когда счетчик переваливает десяток, номер генерится случайным образом, тоже с проверкой на дублирование. Это если кто в промышленных масштабах собирается использовать.
ManHunter (28.09.2015 в 16:31):
Было б за что. Обычная разминка для ума. А под xor eax,eax я имел в виду вот это:

; EAX=1 - файл есть
; EAX=0 - файла нет
proc existA lpszFileName:DWORD
  local wfd:WIN32_FIND_DATA
  lea eax,[wfd]
  invoke FindFirstFileA,[lpszFileName],eax
  cmp eax,-1
  je  @f
  invoke FindClose,eax
  xor eax,eax   ; <--- в этом контексте совпадает с dec eax, но на 1 байт больше
@@:
  inc eax
  ret
endp
addhaloka (28.09.2015 в 15:05):
Цитатаxor eax,eax после invoke FindClose,eax решает проблему.

Это вроде бы лишнее - проверил в одном патче - не работает (хотя, может я что-то не так делаю). А вот
  invoke FindClose,eax
  dec eax
@@:
  inc eax
  ret
срабатывает, как нужно, и при
  stdcall existW,targetFile
  cmp eax,1
  je  метка
и при
  stdcall existW,targetFile
  cmp eax,0
  jne метка
Ещё раз спасибо.
ManHunter (28.09.2015 в 13:20):
Даже не так. Можно сделать еще на байт короче, если посмотреть в отладчике, что FindClose в случае успеха возвращает EAX=1. Финальная версия:

; EAX=1 - файл есть
; EAX=0 - файла нет
proc existA lpszFileName:DWORD
  local wfd:WIN32_FIND_DATA
  lea eax,[wfd]
  invoke FindFirstFileA,[lpszFileName],eax
  cmp eax,-1
  je  @f
  invoke FindClose,eax
  dec eax
@@:
  inc eax
  ret
endp
ManHunter (28.09.2015 в 13:04):
Цитатаp.s. Хотя в оптимизированном виде несколько ограничен простор, т. е. так уже нельзя:

xor eax,eax после invoke FindClose,eax решает проблему. Все равно получается красивее :)
addhaloka (28.09.2015 в 12:56):
ЦитатаДа и процедуру можно немного оптимизировать :)

Спасибо, так красивей. Я раньше эту функцию вообще в дизассемблированном виде юзал (пока не узнал, что исходники в m32lib лежат)), типа так:
proc existA
  push ebp
  mov ebp,esp и т. д.
Но в таком виде, в этом примере она не работала, поэтому переписал по нормальному.

p.s. Хотя в оптимизированном виде несколько ограничен простор, т. е. так уже нельзя:
stdcall existW,targetFile
cmp eax,1
je  .skipopen
а только так:
stdcall existW,targetFile
cmp eax,0
jne .skipopen
А у меня в большинстве проектов именно первый вариант. Но всё-равно, большое спасибо - этот и много других примеров из блога оказались очень полезны, как в изучении FASM и вообще ассемблера, так и в использовании в своих поделиях. :)
ManHunter (28.09.2015 в 11:29):
Тут еще можно поспорить, что займет меньше места. Добавление двух функций в импорт kernel32 + отдельная процедура проверки, или импорт PathFileExists.

Да и процедуру можно немного оптимизировать :)

; EAX=0 - файла нет
; EAX!=0 - файл есть
proc existA lpszFileName:DWORD
  local wfd:WIN32_FIND_DATA
  lea eax,[wfd]
  invoke FindFirstFileA,[lpszFileName],eax
  cmp eax,-1
  je  @f
  invoke FindClose,eax
@@:
  inc eax
  ret
endp
addhaloka (28.09.2015 в 10:45):
Вместо PathFileExists (чтобы shlwapi.dll не подключать) можно использовать exist из masm32 (давно уже в своих поделках юзаю), как-то так:
...
        ; Проверить наличие файла или каталога
        stdcall existA,edi
...
proc existA lpszFileName:DWORD

  local wfd:WIN32_FIND_DATA

  lea eax,[wfd]
  invoke FindFirstFileA,[lpszFileName],eax
  cmp eax,0FFFFFFFFh
  jne @F
  mov eax,0
  jmp .endexist
  @@:
  invoke FindClose,eax
  mov eax,1

  .endexist:
  ret
endp
SMaSm-94 (25.09.2015 в 19:22):
Понятно. Я тоже быстро считаю, но привык наоборот макросами пользоваться. Они немного упрощают процесс написания программ.
А за статейку спасибо. У Вас на сайте очень много эксклюзивного контента для FASM'а)
ManHunter (25.09.2015 в 08:08):
Да как-то привык уже. И количество считаю чуть ли не на подсознании :)
SMaSm-94 (25.09.2015 в 02:26):
Уважаемый ManHunter, почему вы не юзаете макрос cinvoke с wsprintf? Код же можно немного уменьшить с его помощью + не надо считать сколько байт нужно выталкивать из стека))

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2024
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.07 сек. / MySQL: 2 (0.0054 сек.) / Память: 4.5 Mb
Наверх