-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhardlink_visualiser.lua
More file actions
255 lines (204 loc) · 5.63 KB
/
hardlink_visualiser.lua
File metadata and controls
255 lines (204 loc) · 5.63 KB
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
-- Double Commander WDX content plugin for highlighting hardlinked files.
-- Linux/Unix only: it uses find(1) and stat(1) to obtain link counts.
local FT_NOMOREFIELDS = 0
local FT_NUMERIC_64 = 2
local FT_BOOLEAN = 6
local FA_DIRECTORY = 0x00000010
local FA_SYMLINK = 0x00000400
local FIELD_HARDLINK_COUNT = 0
local FIELD_HAS_HARDLINKS = 1
local directory_cache = {}
local ENABLE_POLLING_INVALIDATION = true
local DIRECTORY_RECHECK_MS = 5000
local function has_flag(value, flag)
return math.floor(value / flag) % 2 ~= 0
end
local function shell_quote(value)
return "'" .. string.gsub(value, "'", [['"'"']]) .. "'"
end
local function read_all(path)
local handle = io.open(path, "rb")
if not handle then
return nil
end
local content = handle:read("*a")
handle:close()
return content
end
local function run_to_temp(command)
local temp_name = SysUtils.GetTempName()
if temp_name == nil or temp_name == "" then
return nil
end
local status = os.execute(command .. " > " .. shell_quote(temp_name) .. " 2>/dev/null")
if status ~= true and status ~= 0 then
os.remove(temp_name)
return nil
end
local content = read_all(temp_name)
os.remove(temp_name)
return content
end
local function is_supported_file(file_name)
local attr = SysUtils.FileGetAttr(file_name)
if attr == nil or attr < 0 then
return false
end
if has_flag(attr, FA_DIRECTORY) or has_flag(attr, FA_SYMLINK) then
return false
end
return true
end
local function parse_nul_records(blob, fields_per_record)
local records = {}
local fields = {}
local start_pos = 1
while true do
local end_pos = string.find(blob, "\0", start_pos, true)
if end_pos == nil then
break
end
fields[#fields + 1] = string.sub(blob, start_pos, end_pos - 1)
if #fields == fields_per_record then
records[#records + 1] = fields
fields = {}
end
start_pos = end_pos + 1
end
return records
end
local function get_directory_mtime(directory_name)
local command = "stat -c '%Y\\0' -- " .. shell_quote(directory_name)
local blob = run_to_temp(command)
if blob == nil then
return nil
end
local records = parse_nul_records(blob, 1)
if #records < 1 then
return nil
end
return tonumber(records[1][1])
end
local function scan_directory(directory_name, directory_mtime)
local command = "find "
.. shell_quote(directory_name)
.. " -mindepth 1 -maxdepth 1 -type f -printf '%f\\0%n\\0%C@\\0'"
local blob = run_to_temp(command)
local tick = SysUtils.GetTickCount()
local entries = {}
if blob ~= nil then
local records = parse_nul_records(blob, 3)
local index
for index = 1, #records do
local record = records[index]
local name = record[1]
local count = tonumber(record[2]) or 1
local ctime = tonumber(record[3]) or 0
entries[name] = {
count = count,
ctime = math.floor(ctime),
}
end
end
local cache = {
tick = tick,
dir_mtime = directory_mtime,
entries = entries,
}
directory_cache[directory_name] = cache
return cache
end
local function stat_single_file(file_name)
local command = "stat -c '%h\\0%Z\\0' -- " .. shell_quote(file_name)
local blob = run_to_temp(command)
if blob == nil then
return nil
end
local records = parse_nul_records(blob, 2)
if #records < 1 then
return nil
end
return {
count = tonumber(records[1][1]) or 1,
ctime = tonumber(records[1][2]) or 0,
}
end
local function get_hardlink_info(file_name)
if not is_supported_file(file_name) then
return {
count = 1,
has_hardlinks = false,
}
end
local directory_name = SysUtils.ExtractFileDir(file_name)
local base_name = SysUtils.ExtractFileName(file_name)
local current_ctime = SysUtils.GetFileProperty(file_name, 7)
local now_tick = SysUtils.GetTickCount()
local directory_mtime = nil
local cache = directory_cache[directory_name]
local entry = nil
if ENABLE_POLLING_INVALIDATION and cache ~= nil and (now_tick - cache.tick >= DIRECTORY_RECHECK_MS) then
directory_mtime = get_directory_mtime(directory_name)
if directory_mtime == nil or directory_mtime ~= cache.dir_mtime then
cache = nil
directory_cache[directory_name] = nil
else
cache.tick = now_tick
end
end
if cache ~= nil then
entry = cache.entries[base_name]
end
if entry == nil or current_ctime == nil or entry.ctime ~= current_ctime then
if directory_mtime == nil then
directory_mtime = get_directory_mtime(directory_name)
end
cache = scan_directory(directory_name, directory_mtime)
entry = cache.entries[base_name]
end
if entry == nil then
entry = stat_single_file(file_name)
end
if entry == nil then
return {
count = 1,
has_hardlinks = false,
}
end
return {
count = entry.count,
has_hardlinks = entry.count > 1,
}
end
function ContentGetSupportedField(Index)
if Index == FIELD_HARDLINK_COUNT then
return "HardlinkCount", "", FT_NUMERIC_64
elseif Index == FIELD_HAS_HARDLINKS then
return "HasHardlinks", "", FT_BOOLEAN
end
return "", "", FT_NOMOREFIELDS
end
function ContentGetDefaultSortOrder(FieldIndex)
if FieldIndex == FIELD_HARDLINK_COUNT then
return -1
end
return 1
end
function ContentGetDetectString()
return 'EXT="*"'
end
function ContentGetValue(FileName, FieldIndex, UnitIndex, flags)
if flags == 1 then
return nil
end
local info = get_hardlink_info(FileName)
if FieldIndex == FIELD_HARDLINK_COUNT then
return info.count
elseif FieldIndex == FIELD_HAS_HARDLINKS then
return info.has_hardlinks
end
return nil
end
function ContentStopGetValue(FileName)
return true
end