-
Notifications
You must be signed in to change notification settings - Fork 1
/
FileInfo.hs
executable file
·451 lines (379 loc) · 22 KB
/
FileInfo.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
{-# OPTIONS_GHC -cpp #-}
----------------------------------------------------------------------------------------------------
---- Получение и хранение информации о файлах, поиск файлов на диске. ----
----------------------------------------------------------------------------------------------------
module FileInfo where
import Prelude hiding (catch)
import Control.OldException
import Control.Monad
import Data.Bits
import Data.Char
import Data.HashTable as Hash
import Data.Int
import Data.IORef
import Data.List
import Data.Maybe
import Data.Word
import Foreign.C
import System.IO.Unsafe
import System.Posix.Internals
import Utils
import Process
import Files
import Errors
#ifdef FREEARC_PACKED_STRINGS
import UTF8Z
#endif
#if defined(FREEARC_WIN)
import Win32Files
import System.Win32.File
#endif
----------------------------------------------------------------------------------------------------
---- Компактное представление имени файла ----------------------------------------------------------
----------------------------------------------------------------------------------------------------
-- |Хранение имён файлов в компактном виде с представлением быстрого доступа
-- к имени каталога, имени файла без каталога и расширению файла
data PackedFilePath = PackedFilePath
{ fpPackedDirectory :: !MyPackedString -- Имя каталога
, fpPackedBasename :: !MyPackedString -- Имя файла без каталога, но с расширением
, fpLCExtension :: !String -- Расширение, переведённое в нижний регистр
, fpHash :: {-# UNPACK #-} !Int32 -- Хеш от имени файла
, fpParent :: !PackedFilePath -- Структура PackedFilePath родительского каталога
}
| RootDir
instance Eq PackedFilePath where
(==) = map2eq$ map3 (fpHash,fpPackedBasename,fpPackedDirectory)
#ifdef FREEARC_PACKED_STRINGS
-- Использование упакованных строк уменьшает расход памяти в 2 раза
type MyPackedString = PackedString
myPackStr = packString
myUnpackStr = unpackPS
-- |Заменяет повторения одинаковых расширений одной и той же строкой
packext ext = unsafePerformIO$ do
found <- Hash.lookup extsHash ext
case found of
Nothing -> do Hash.insert extsHash ext ext
return ext
Just oldext -> return oldext
extsHash = unsafePerformIO$ Hash.new (==) (filenameHash 0)
#else
type MyPackedString = String
myPackStr = id
myUnpackStr = id
packext = id
#endif
fpDirectory = myUnpackStr.fpPackedDirectory
fpBasename = myUnpackStr.fpPackedBasename
-- |Виртуальное поле: полное имя файла, включая каталог и расширение
fpFullname fp = fpDirectory fp </> fpBasename fp
-- |Ускоренное вычисление упакованного полного имени
fpPackedFullname fp = if fpPackedDirectory fp == myPackStr ""
then fpPackedBasename fp
else myPackStr (fpFullname fp)
-- |Создание упакованного представления из имени файла
packFilePath parent fullname = packFilePath2 parent dir name
where (dir,name) = splitDirFilename fullname
-- |Создание упакованного представления из имени каталога и имени файла без каталога
packFilePath2 parent dir name = packFilePathPacked2 parent (myPackStr dir) name
packFilePathPacked2 parent packed_dir name = packFilePathPacked3 parent packed_dir name (packext$ filenameLower$ getFileSuffix name)
-- |Создание упакованного представления из имени каталога, имени файла без каталога и расширения.
packFilePath3 parent dir name lcext = packFilePathPacked3 parent (myPackStr dir) name lcext
packFilePathPacked3 parent packed_dir name lcext =
PackedFilePath { fpPackedDirectory = packed_dir
, fpPackedBasename = myPackStr name
, fpLCExtension = lcext
, fpHash = filenameHash (fpHash parent) name
, fpParent = parent
}
-- |Создать структуру для базового каталога при поиске файлов
packParentDirPath dir =
PackedFilePath { fpPackedDirectory = myPackStr "" -- Чтобы не тратить зря время,
, fpPackedBasename = myPackStr dir -- помещаем имя каталога целиком в Basename
, fpLCExtension = ""
, fpHash = filenameHash 0 (filter (not.isPathSeparator) dir)
, fpParent = RootDir
}
-- |Хеш по полному имени файла (без разделителей каталога!).
-- Для ускорения его вычисления используется `dirhash` - хеш имени каталога, содержащего файл,
-- и `basename` - имя файла без имени каталога
filenameHash {-dirhash basename-} = foldl (\h c -> h*37+i(ord c))
{-# INLINE filenameHash #-}
----------------------------------------------------------------------------------------------------
---- Сопоставление имён файлов с регулярными выражениями -------------------------------------------
----------------------------------------------------------------------------------------------------
-- |Сопоставить имя файла с маской `filespec`.
-- Маски "*", "*.ext" или имя файла без каталога - обрабатываются особо
match_FP getName filespec =
if filespec==reANY_FILE then const True else
case (splitFilename3 filespec) of
("", "*", ext) -> match (filenameLower ext) . fpLCExtension
("", _, _ ) -> match (filenameLower filespec) . filenameLower . getName
_ -> match (filenameLower filespec) . filenameLower . fpFullname
-- |Соответствует ли путь к файлу `filepath` хоть одной из масок `filespecs`?
match_filespecs getName {-filespecs filepath-} = anyf . map (match_FP getName)
-- |Маска, которой соответствует любое имя файла
reANY_FILE = "*"
----------------------------------------------------------------------------------------------------
---- Информация о файле ----------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
-- Типы данных для..
type FileCount = Int -- количества файлов
type FileSize = Integer -- размера файла или позиции чтения/записи в нём
aFILESIZE_MIN = -(2^63) -- очень маленькое значение типа FileSize
type FileTime = CTime -- времени создания/модификации/чтения файла
aMINIMUM_POSSIBLE_FILETIME = 0 :: FileTime -- ^Минимальное datetime, которое только может быть у файла. Соответствует 1 января 1970 г.
aMAXIMUM_POSSIBLE_FILETIME = (i (maxBound :: Int32)) :: FileTime
type FileAttr = FileAttributes -- досовских атрибутов файла
type FileGroup = Int -- номера группы в arc.groups
-- |Структура, хранящая всю необходимую нам информацию о файле
data FileInfo = FileInfo
{ fiFilteredName :: !PackedFilePath -- Имя файла, сопоставляемое с указанными в командной строке
, fiDiskName :: !PackedFilePath -- "Внешнее" имя файла - для чтения/записи файлов на диске
, fiStoredName :: !PackedFilePath -- "Внутреннее" имя файла - сохраняемое в оглавлении архива
, fiSize :: {-# UNPACK #-} !FileSize -- Размер файла (0 для каталогов)
, fiTime :: {-# UNPACK #-} !FileTime -- Дата/время создания файла
, fiAttr :: {-# UNPACK #-} !FileAttr -- Досовские атрибуты файла
, fiIsDir :: {-# UNPACK #-} !Bool -- Это каталог?
, fiGroup :: {-# UNPACK #-} !FileGroup -- Номер группы в arc.groups
}
-- |Битовые поля в fiAttr
aFI_ATTR_READONLY = 0x01
aFI_ATTR_HIDDEN = 0x02
aFI_ATTR_SYSTEM = 0x04
aFI_ATTR_ARCHIVE = 0x20
-- |Преобразовать FileInfo в имя файла на диске
diskName = fpFullname.fiDiskName
storedName = fpFullname.fiStoredName
filteredName = fpFullname.fiFilteredName
-- |Преобразовать FileInfo в базовое имя файла
baseName = fpBasename.fiStoredName
-- |Специальные файлы (каталоги, симлинки и тому подобное) не требуют упаковки
fiSpecialFile = fiIsDir
-- |Номер группы, проставляемый там, где он не используется.
fiUndefinedGroup = -1
-- |Заменить код ошибки в определении DateTime максимально возможным значением DateTime (поскольку эта ошибка обычно возникает когда время позже 2038-го года)
fiTimeCorrect t | t<aMINIMUM_POSSIBLE_FILETIME = aMAXIMUM_POSSIBLE_FILETIME
| otherwise = t
-- |При выходе даты за поддерживаемые диапазон (до 2038 г.) возвращать последнее значение из этого диапазона
stat_mtime p_stat = raw_stat_mtime p_stat >>== fiTimeCorrect
-- |Строка атрибутов файла, используемая при выводе листинга архива
fiAttrStr fi = '.' : (if fiIsDir fi then 'D' else '.') : zipWith check [aFI_ATTR_READONLY, aFI_ATTR_HIDDEN, aFI_ATTR_SYSTEM, aFI_ATTR_ARCHIVE,0] "RHSA."
where check n c = if fiAttr fi .&. n > 0 then c else '.'
-- |Создать структуру FileInfo для каталога с заданным именем
createParentDirFileInfo fiFilteredName fiDiskName fiStoredName =
FileInfo { fiFilteredName = packParentDirPath fiFilteredName
, fiDiskName = packParentDirPath fiDiskName
, fiStoredName = packParentDirPath fiStoredName
, fiSize = 0
, fiTime = aMINIMUM_POSSIBLE_FILETIME
, fiAttr = 0
, fiIsDir = True
, fiGroup = fiUndefinedGroup
}
-- |Перечитать информацию о файле после его открытия (на случай, если файл успел измениться).
-- Возвращает некорректные fiAttr (под юниксом) и fiGroup
rereadFileInfo fi file = do
getFileInfo (fiFilteredName fi) (fiDiskName fi) (fiStoredName fi)
-- |Создать структуру FileInfo с информацией о заданном файле.
-- Возвращает некорректные fiAttr (под юниксом) и fiGroup
getFileInfo fiFilteredName fiDiskName fiStoredName =
let filename = fpFullname fiDiskName in do
fileWithStatus "getFileInfo" filename $ \p_stat -> do
fiIsDir <- stat_mode p_stat >>== s_isdir
fiTime <- stat_mtime p_stat
fiSize <- if fiIsDir then return 0
else stat_size p_stat
return$ Just$ FileInfo fiFilteredName fiDiskName fiStoredName fiSize fiTime 0 fiIsDir fiUndefinedGroup
`catch`
\e -> do registerWarning$ CANT_GET_FILEINFO filename
return Nothing -- В случае ошибки при выполнении stat возвращаем Nothing
-- |Get file's date/time
getFileDateTime filename = fileWithStatus "getFileInfo" filename stat_mtime
-- |Restore date/time/attrs saved in FileInfo structure
setFileDateTimeAttr filename fileinfo = setFileDateTime filename (fiTime fileinfo)
{-# NOINLINE getFileInfo #-}
----------------------------------------------------------------------------------------------------
---- Процесс поиска файлов на диске ----------------------------------------------------------------
----------------------------------------------------------------------------------------------------
-- |Настройки для процесса поиска файлов на диске
data FindFiles = FindFiles
{ ff_disk_eq_filtered :: Bool
, ff_stored_eq_filtered :: Bool
, ff_recursive :: Bool
, ff_parent_or_root :: FileInfo -> FileInfo
, ff_accept_f :: FileInfo -> Bool
, ff_process_f :: [FileInfo] -> IO ()
}
-- |Вернуть FileInfo файлов и каталогов (исключая "." и ".."), находящихся в каталоге `parent`
getDirectoryContents_FileInfo ff parent{-родительская структура FileInfo-} = do
let -- Полное дисковое имя род. каталога
diskDirName = fpFullname$ fiDiskName parent
-- Упакованные строки с дисковым, фильтруемым и запоминаемым именем род. каталога
-- Эти имена могут совпадать при отсутствии -ap/-dp, что позволяет нам экономить память в этих случаях
packedDisk = myPackStr diskDirName
packedFiltered = if ff.$ff_disk_eq_filtered
then packedDisk
else myPackStr$ fpFullname$ fiFilteredName parent
packedStored = if ff.$ff_stored_eq_filtered
then packedFiltered
else myPackStr$ fpFullname$ fiStoredName parent_or_root
-- Выбрать parent или root в качестве родительской записи (последнее - только при -ep0)
parent_or_root = (ff.$ff_parent_or_root) parent
-- Вызвать функцию f, передав ей объекты фильтруемого, дискового и запомненного имени
make_names f name = f (packFilePathPacked3 (fiFilteredName parent) packedFiltered name lcext)
(packFilePathPacked3 (fiDiskName parent) packedDisk name lcext)
(packFilePathPacked3 (fiStoredName parent_or_root) packedStored name lcext)
where lcext = packext$ filenameLower$ getFileSuffix name
#if !defined(FREEARC_WIN)
(dirList (diskDirName|||".")) .$handleFindErrors diskDirName -- Получим список файлов в каталоге, обрабатывая ошибки чтения каталога,
>>== filter exclude_special_names -- Исключим из списка "." и ".."
>>= (mapMaybeM $! make_names getFileInfo) -- Превратим имена файлов в структуры FileInfo и уберём из списка файлы, на которых споткнулся `stat`
#else
withList $ \list -> do
handleFindErrors diskDirName $ do
wfindfiles (diskDirName </> reANY_FILE) $ \find -> do
name <- w_find_name find
when (exclude_special_names name) $ do
fiAttr <- w_find_attrib find
fiSize <- w_find_size find
fiTime <- w_find_time_write find >>== fiTimeCorrect
fiIsDir <- w_find_isDir find
(list <<=) $! make_names FileInfo name fiSize fiTime fiAttr fiIsDir fiUndefinedGroup
#endif
-- |Добавить exception handler, вызываемый при ошибках получения списка файлов в каталоге
handleFindErrors dir =
handleJust ioErrors $ \e -> do
-- Сообщение об ошибке не печатается для каталогов "/System Volume Information"
d <- myCanonicalizePath dir
unless (stripRoot d `strLowerEq` "System Volume Information") $ do
registerWarning$ CANT_READ_DIRECTORY dir
return defaultValue
-- |Создать список файлов в `dir`, удовлетворяющих `accept_f` и отослать результат в `process_f`.
-- Если recursive==True - повторить эти действия рекурсивно в каждом найденном подкаталоге
findFiles_FileInfo dir ff@FindFiles{ff_accept_f=accept_f, ff_process_f=process_f, ff_recursive=recursive} = do
if recursive then recursiveM processDir dir else do processDir dir; return ()
where processDir dir = do
dirContents <- getDirectoryContents_FileInfo ff dir
process_f `unlessNull` (filter accept_f dirContents) -- Обработать отфильтрованные файлы, если их список непуст
return (filter fiIsDir dirContents) -- Возвратить список подкаталогов для рекурсивной обработки
{-# NOINLINE getDirectoryContents_FileInfo #-}
{-# NOINLINE findFiles_FileInfo #-}
----------------------------------------------------------------------------------------------------
---- Поиск и обработка файлов, удовлетворяющих заданным критериям ----------------------------------
----------------------------------------------------------------------------------------------------
-- |Условия поиска файлов на диске
data FileFind = FileFind
{ ff_ep :: !Int
, ff_scan_subdirs :: !Bool
, ff_include_dirs :: !(Maybe Bool)
, ff_no_nst_filters :: !Bool
, ff_filter_f :: !(FileInfo -> Bool)
, ff_group_f :: !(Maybe (FileInfo -> FileGroup))
, ff_arc_basedir :: !String
, ff_disk_basedir :: !String
}
-- |Найти [рекурсивно] все файлы, удовлетворяющие маске `filespec`, и вернуть их список
find_files scan_subdirs filespec = find_and_filter_files [filespec] doNothing $
FileFind { ff_ep = -1
, ff_scan_subdirs = scan_subdirs
, ff_include_dirs = Just False
, ff_no_nst_filters = True
, ff_filter_f = const True
, ff_group_f = Nothing
, ff_arc_basedir = ""
, ff_disk_basedir = ""
}
-- |Составить список всех файлов и подкаталогов в каталоге
dir_list directory = find_and_filter_files [directory </> reANY_FILE] doNothing $
FileFind { ff_ep = 0
, ff_scan_subdirs = False
, ff_include_dirs = Just True
, ff_no_nst_filters = True
, ff_filter_f = const True
, ff_group_f = Nothing
, ff_arc_basedir = ""
, ff_disk_basedir = ""
}
-- |Найти все файлы, удовлетворяющие критерию отбора `ff`,
-- и вернуть их список
find_and_filter_files filespecs process_f ff = do
concat ==<< withList (\list -> do -- Сконкатенировать списки файлов, найденных в каждом подкаталоге
find_filter_and_process_files filespecs ff $ \files -> do
process_f files
list <<= files)
-- |Найти все файлы, удовлетворяющие критерию отбора `ff`,
-- и послать их список по частям в выходной канал процесса
find_and_filter_files_PROCESS filespecs ff pipe = do
find_filter_and_process_files filespecs ff (sendP pipe)
sendP pipe [] -- сигнал "А кино-то уже кончилось!" :)
-- |Найти [рекурсивно] все файлы, описываемые масками `filespecs` и критерием отбора `filter_f`,
-- и выполнить над каждым списком файлов, найденных в одном каталоге, операцию `process_f`
find_filter_and_process_files filespecs ff@FileFind{ ff_ep=ep, ff_scan_subdirs=scan_subdirs, ff_include_dirs=include_dirs, ff_filter_f=filter_f, ff_group_f=group_f, ff_arc_basedir=arc_basedir, ff_disk_basedir=disk_basedir, ff_no_nst_filters=no_nst_filters} process_f
-- Сгруппировать маски по имени каталога, и обработать каждую из этих групп отдельно
= do curdir <- getCurrentDirectory >>== translatePath
{-
-- Поиск файлов как в RAR
let doit f = do
let re = isRegExp f
isdir <- isDirExists f
if not re && isdir then findRecursively f else do
if not re && -r- then getStat f `catch` "WARNING: file %s not found"
else find (re || !-r-) f
-}
-- Заменить имена каталогов dir на две маски "dir dir/" чтобы охватить сам каталог и все файлы в нём
modified_filespecs <- foreach filespecs $ \filespec -> do
isDir <- if hasTrailingPathSeparator filespec
then return True
else dirExist (disk_basedir </> filespec)
when isDir $ do
find_files_in_one_dir curdir True [dropTrailingPathSeparator filespec]
return$ (isDir &&& addTrailingPathSeparator) filespec
--
mapM_ (find_files_in_one_dir curdir False) $ sort_and_groupOn (filenameLower.takeDirectory) modified_filespecs
where
-- Обработать группу масок, относящихся к одному каталогу
find_files_in_one_dir curdir addDir filespecs = do
findFiles_FileInfo root FindFiles{ff_process_f=process_f.map_group_f, ff_recursive=recursive, ff_disk_eq_filtered=disk_eq_filtered, ff_stored_eq_filtered=stored_eq_filtered, ff_parent_or_root=parent_or_root, ff_accept_f=accept_f}
where dirname = takeDirectory (head filespecs) -- Общий для всех масок каталог
masks = map takeFileName filespecs -- Маски без этого имени каталога
root = createParentDirFileInfo -- Базовый FileInfo для этого поиска:
dirname -- базовый каталог для фильтрации файлов
diskdir -- базовый каталог на диске
arcdir -- базовый каталог в архиве
-- Базовый каталог на диске
diskdir = disk_basedir </> dirname
-- Имена файлов на диске и в ком. строке совпадают?
disk_eq_filtered = diskdir==dirname
-- Полный путь к базовому каталогу на диске для -ep2/-ep3
full_dirname = curdir </> diskdir
-- Базовый каталог в архиве
arcdir = remove_unsafe_dirs (arc_basedir </> arc_subdir)
arc_subdir = case ep of
0 -> "" -- -ep: exclude any paths from names
1 -> "" -- -ep1: exclude base dir from names
2 -> full_dirname.$stripRoot -- -ep2: full absolute path without "d:\"
3 -> full_dirname -- -ep3: full absolute path with "d:\"
_ -> dirname.$stripRoot -- Default: full relative path
-- Выбирает parent или root каталог в зависимости от опции -ep
parent_or_root = if ep==0 then const root else id
-- Имена файлов внутри архива и в ком. строке совпадают?
stored_eq_filtered = arcdir==dirname && ep/=0
-- Одно из имён указано как "dir/"?
dir_slash = dirname>"" && masks `contains` ""
-- Сканировать подкаталоги если указана опция "-r" или одно из имён указано как "dir/"
recursive = scan_subdirs || dir_slash
-- Включить в список все файлы/каталоги, если одно из имён указано как "dir/" или "*" или "dir/*"
include_all = dir_slash || masks `contains` reANY_FILE
-- Предикат, определяющий какие файлы и каталоги будут включены в создаваемый список:
-- для каталогов это зависит от опций --[no]dirs, by default - при условии "[dir/]* -r" || "dir/" и отсутствии фильтров отбора файлов -n/-s../-t..
-- для файлов проверяется соответствие предикату `filter_f` и одной из масок
accept_f fi | fiIsDir fi = include_dirs `defaultVal` (addDir && baseName fi `elem` masks || no_nst_filters && recursive && include_all)
| otherwise = filter_f fi && (include_all || match_filespecs fpBasename masks (fiFilteredName fi))
-- Устанавливает в [FileInfo] номера групп fiGroup функцией, переданной в group_f
map_group_f = case group_f of
Nothing -> id
Just f -> map (\x -> x {fiGroup = f x})
{-# NOINLINE find_files #-}
{-# NOINLINE find_and_filter_files #-}
{-# NOINLINE find_and_filter_files_PROCESS #-}
{-# NOINLINE find_filter_and_process_files #-}