2022-03-06 06:59:22 +01:00
|
|
|
local common = require "core.common"
|
|
|
|
local config = require "core.config"
|
|
|
|
local dirwatch = {}
|
|
|
|
|
2022-10-11 02:40:41 +02:00
|
|
|
function dirwatch:__index(idx)
|
2022-03-06 06:59:22 +01:00
|
|
|
local value = rawget(self, idx)
|
|
|
|
if value ~= nil then return value end
|
|
|
|
return dirwatch[idx]
|
|
|
|
end
|
|
|
|
|
|
|
|
function dirwatch.new()
|
|
|
|
local t = {
|
|
|
|
scanned = {},
|
|
|
|
watched = {},
|
|
|
|
reverse_watched = {},
|
|
|
|
monitor = dirmonitor.new(),
|
2022-10-11 02:40:41 +02:00
|
|
|
single_watch_top = nil,
|
|
|
|
single_watch_count = 0
|
2022-03-06 06:59:22 +01:00
|
|
|
}
|
|
|
|
setmetatable(t, dirwatch)
|
|
|
|
return t
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function dirwatch:scan(directory, bool)
|
|
|
|
if bool == false then return self:unwatch(directory) end
|
|
|
|
self.scanned[directory] = system.get_file_info(directory).modified
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Should be called on every directory in a subdirectory.
|
|
|
|
-- In windows, this is a no-op for anything underneath a top-level directory,
|
|
|
|
-- but code should be called anyway, so we can ensure that we have a proper
|
2022-03-09 01:30:25 +01:00
|
|
|
-- experience across all platforms. Should be an absolute path.
|
2022-05-15 21:21:26 +02:00
|
|
|
-- Can also be called on individual files, though this should be used sparingly,
|
|
|
|
-- so as not to run into system limits (like in the autoreload plugin).
|
2022-03-06 06:59:22 +01:00
|
|
|
function dirwatch:watch(directory, bool)
|
|
|
|
if bool == false then return self:unwatch(directory) end
|
2022-05-15 21:21:26 +02:00
|
|
|
local info = system.get_file_info(directory)
|
|
|
|
if not info then return end
|
2022-03-06 06:59:22 +01:00
|
|
|
if not self.watched[directory] and not self.scanned[directory] then
|
2022-10-11 02:40:41 +02:00
|
|
|
if self.monitor:mode() == "single" then
|
2022-05-15 21:21:26 +02:00
|
|
|
if info.type ~= "dir" then return self:scan(directory) end
|
2022-10-11 02:40:41 +02:00
|
|
|
if not self.single_watch_top or directory:find(self.single_watch_top, 1, true) ~= 1 then
|
2022-04-02 23:03:29 +02:00
|
|
|
-- Get the highest level of directory that is common to this directory, and the original.
|
|
|
|
local target = directory
|
2022-10-11 02:40:41 +02:00
|
|
|
while self.single_watch_top and self.single_watch_top:find(target, 1, true) ~= 1 do
|
2022-04-02 23:03:29 +02:00
|
|
|
target = common.dirname(target)
|
|
|
|
end
|
2022-10-11 02:40:41 +02:00
|
|
|
if target ~= self.single_watch_top then
|
2022-04-02 23:03:29 +02:00
|
|
|
local value = self.monitor:watch(target)
|
|
|
|
if value and value < 0 then
|
|
|
|
return self:scan(directory)
|
|
|
|
end
|
2022-10-11 02:40:41 +02:00
|
|
|
self.single_watch_top = target
|
2022-04-02 23:03:29 +02:00
|
|
|
end
|
2022-03-06 06:59:22 +01:00
|
|
|
end
|
2022-10-11 02:40:41 +02:00
|
|
|
self.single_watch_count = self.single_watch_count + 1
|
2022-03-06 06:59:22 +01:00
|
|
|
self.watched[directory] = true
|
|
|
|
else
|
|
|
|
local value = self.monitor:watch(directory)
|
|
|
|
-- If for whatever reason, we can't watch this directory, revert back to scanning.
|
|
|
|
-- Don't bother trying to find out why, for now.
|
|
|
|
if value and value < 0 then
|
|
|
|
return self:scan(directory)
|
|
|
|
end
|
|
|
|
self.watched[directory] = value
|
|
|
|
self.reverse_watched[value] = directory
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-03-09 01:30:25 +01:00
|
|
|
-- this should be an absolute path
|
2022-03-06 06:59:22 +01:00
|
|
|
function dirwatch:unwatch(directory)
|
|
|
|
if self.watched[directory] then
|
2022-10-11 02:40:41 +02:00
|
|
|
if self.monitor:mode() == "multiple" then
|
2022-03-09 01:30:25 +01:00
|
|
|
self.monitor:unwatch(self.watched[directory])
|
|
|
|
self.reverse_watched[directory] = nil
|
2022-03-06 06:59:22 +01:00
|
|
|
else
|
2022-10-11 02:40:41 +02:00
|
|
|
self.single_watch_count = self.single_watch_count - 1
|
|
|
|
if self.single_watch_count == 0 then
|
|
|
|
self.single_watch_top = nil
|
2022-03-09 01:30:25 +01:00
|
|
|
self.monitor:unwatch(directory)
|
2022-03-06 06:59:22 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
self.watched[directory] = nil
|
|
|
|
elseif self.scanned[directory] then
|
|
|
|
self.scanned[directory] = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- designed to be run inside a coroutine.
|
|
|
|
function dirwatch:check(change_callback, scan_time, wait_time)
|
2022-04-24 19:40:58 +02:00
|
|
|
local had_change = false
|
2023-10-21 19:17:49 +02:00
|
|
|
local last_error
|
2022-03-06 06:59:22 +01:00
|
|
|
self.monitor:check(function(id)
|
2022-04-24 19:40:58 +02:00
|
|
|
had_change = true
|
2022-10-11 02:40:41 +02:00
|
|
|
if self.monitor:mode() == "single" then
|
|
|
|
local path = common.dirname(id)
|
|
|
|
if not string.match(id, "^/") and not string.match(id, "^%a:[/\\]") then
|
2024-02-18 22:31:24 +01:00
|
|
|
path = common.dirname(self.single_watch_top .. PATHSEP .. id)
|
2022-10-11 02:40:41 +02:00
|
|
|
end
|
|
|
|
change_callback(path)
|
2022-03-06 06:59:22 +01:00
|
|
|
elseif self.reverse_watched[id] then
|
|
|
|
change_callback(self.reverse_watched[id])
|
|
|
|
end
|
2023-10-21 19:17:49 +02:00
|
|
|
end, function(err)
|
|
|
|
last_error = err
|
2022-03-06 06:59:22 +01:00
|
|
|
end)
|
2023-10-21 19:17:49 +02:00
|
|
|
if last_error ~= nil then error(last_error) end
|
2022-03-06 06:59:22 +01:00
|
|
|
local start_time = system.get_time()
|
|
|
|
for directory, old_modified in pairs(self.scanned) do
|
|
|
|
if old_modified then
|
2022-05-10 03:30:13 +02:00
|
|
|
local info = system.get_file_info(directory)
|
|
|
|
local new_modified = info and info.modified
|
|
|
|
if old_modified ~= new_modified then
|
2022-03-06 06:59:22 +01:00
|
|
|
change_callback(directory)
|
2022-04-24 19:40:58 +02:00
|
|
|
had_change = true
|
2022-03-06 06:59:22 +01:00
|
|
|
self.scanned[directory] = new_modified
|
|
|
|
end
|
|
|
|
end
|
2022-06-02 00:36:00 +02:00
|
|
|
if system.get_time() - start_time > (scan_time or 0.01) then
|
|
|
|
coroutine.yield(wait_time or 0.01)
|
2022-03-06 06:59:22 +01:00
|
|
|
start_time = system.get_time()
|
|
|
|
end
|
|
|
|
end
|
2022-04-24 19:40:58 +02:00
|
|
|
return had_change
|
2022-03-06 06:59:22 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
-- inspect config.ignore_files patterns and prepare ready to use entries.
|
|
|
|
local function compile_ignore_files()
|
|
|
|
local ipatterns = config.ignore_files
|
|
|
|
local compiled = {}
|
|
|
|
-- config.ignore_files could be a simple string...
|
|
|
|
if type(ipatterns) ~= "table" then ipatterns = {ipatterns} end
|
|
|
|
for i, pattern in ipairs(ipatterns) do
|
|
|
|
-- we ignore malformed pattern that raise an error
|
|
|
|
if pcall(string.match, "a", pattern) then
|
|
|
|
table.insert(compiled, {
|
|
|
|
use_path = pattern:match("/[^/$]"), -- contains a slash but not at the end
|
|
|
|
-- An '/' or '/$' at the end means we want to match a directory.
|
|
|
|
match_dir = pattern:match(".+/%$?$"), -- to be used as a boolen value
|
|
|
|
pattern = pattern -- get the actual pattern
|
|
|
|
})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return compiled
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
local function fileinfo_pass_filter(info, ignore_compiled)
|
|
|
|
if info.size >= config.file_size_limit * 1e6 then return false end
|
|
|
|
local basename = common.basename(info.filename)
|
|
|
|
-- replace '\' with '/' for Windows where PATHSEP = '\'
|
|
|
|
local fullname = "/" .. info.filename:gsub("\\", "/")
|
|
|
|
for _, compiled in ipairs(ignore_compiled) do
|
|
|
|
local test = compiled.use_path and fullname or basename
|
|
|
|
if compiled.match_dir then
|
|
|
|
if info.type == "dir" and string.match(test .. "/", compiled.pattern) then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
else
|
|
|
|
if string.match(test, compiled.pattern) then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
local function compare_file(a, b)
|
2023-01-05 21:59:15 +01:00
|
|
|
return system.path_compare(a.filename, a.type, b.filename, b.type)
|
2022-03-06 06:59:22 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
-- compute a file's info entry completed with "filename" to be used
|
|
|
|
-- in project scan or falsy if it shouldn't appear in the list.
|
|
|
|
local function get_project_file_info(root, file, ignore_compiled)
|
2024-02-17 18:47:15 +01:00
|
|
|
local info = system.get_file_info(common.basepath(root) .. file)
|
2024-02-11 23:11:30 +01:00
|
|
|
|
2022-03-06 06:59:22 +01:00
|
|
|
-- info can be not nil but info.type may be nil if is neither a file neither
|
|
|
|
-- a directory, for example for /dev/* entries on linux.
|
|
|
|
if info and info.type then
|
2022-03-09 01:30:25 +01:00
|
|
|
info.filename = file
|
2022-03-06 06:59:22 +01:00
|
|
|
return fileinfo_pass_filter(info, ignore_compiled) and info
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
-- "root" will by an absolute path without trailing '/'
|
2022-03-09 01:30:25 +01:00
|
|
|
-- "path" will be a path starting without '/' and without trailing '/'
|
2022-03-06 06:59:22 +01:00
|
|
|
-- or the empty string.
|
2023-11-29 15:55:38 +01:00
|
|
|
-- It identifies a sub-path within "root".
|
2022-03-06 06:59:22 +01:00
|
|
|
-- The current path location will therefore always be: root .. path.
|
2023-11-29 15:55:38 +01:00
|
|
|
-- When recursing, "root" will always be the same, only "path" will change.
|
2022-03-09 01:30:25 +01:00
|
|
|
-- Returns a list of file "items". In each item the "filename" will be the
|
|
|
|
-- complete file path relative to "root" *without* the trailing '/', and without the starting '/'.
|
2023-11-29 15:55:38 +01:00
|
|
|
function dirwatch.get_directory_files(dir, root, path, entries_count, recurse_pred)
|
|
|
|
local t = {}
|
2022-03-06 06:59:22 +01:00
|
|
|
local t0 = system.get_time()
|
|
|
|
local ignore_compiled = compile_ignore_files()
|
|
|
|
|
2024-02-17 18:47:15 +01:00
|
|
|
local all = system.list_dir(common.basepath(root) .. path)
|
2024-02-11 23:11:30 +01:00
|
|
|
|
2022-03-09 01:30:25 +01:00
|
|
|
if not all then return nil end
|
2023-11-29 15:55:38 +01:00
|
|
|
local entries = { }
|
|
|
|
for _, file in ipairs(all) do
|
2022-03-09 01:30:25 +01:00
|
|
|
local info = get_project_file_info(root, (path ~= "" and (path .. PATHSEP) or "") .. file, ignore_compiled)
|
2022-03-06 06:59:22 +01:00
|
|
|
if info then
|
2023-11-29 15:55:38 +01:00
|
|
|
table.insert(entries, info)
|
2022-03-06 06:59:22 +01:00
|
|
|
end
|
|
|
|
end
|
2023-11-29 15:55:38 +01:00
|
|
|
table.sort(entries, compare_file)
|
2022-03-06 06:59:22 +01:00
|
|
|
|
|
|
|
local recurse_complete = true
|
2023-11-29 15:55:38 +01:00
|
|
|
for _, info in ipairs(entries) do
|
|
|
|
table.insert(t, info)
|
|
|
|
entries_count = entries_count + 1
|
|
|
|
if info.type == "dir" then
|
|
|
|
if recurse_pred(dir, info.filename, entries_count, system.get_time() - t0) then
|
|
|
|
local t_rec, complete, n = dirwatch.get_directory_files(dir, root, info.filename, entries_count, recurse_pred)
|
|
|
|
recurse_complete = recurse_complete and complete
|
|
|
|
if n ~= nil then
|
|
|
|
entries_count = n
|
|
|
|
for _, info_rec in ipairs(t_rec) do
|
|
|
|
table.insert(t, info_rec)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
recurse_complete = false
|
2022-04-03 22:20:48 +02:00
|
|
|
end
|
2022-03-06 06:59:22 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return t, recurse_complete, entries_count
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
return dirwatch
|