Add a module for recursively scanning directories

This adds a new module that recursively scans a directory and (weakly)
watches for changes to that directory.

Signed-off-by: Uli Schlachter <psychon@znc.in>
This commit is contained in:
Uli Schlachter 2017-11-16 15:26:08 +01:00
parent e20068cb4a
commit 13eef46748
3 changed files with 231 additions and 1 deletions

View File

@ -9,7 +9,9 @@ local Gio = require("lgi").Gio
local gstring = require("gears.string")
local gtable = require("gears.table")
local filesystem = {}
local filesystem = {
subdirectory_cache = require("gears.filesystem.subdirectory_cache")
}
local function make_directory(gfile)
local success, err = gfile:make_directory_with_parents()
@ -152,3 +154,5 @@ function filesystem.get_dir(d)
end
return filesystem
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

View File

@ -0,0 +1,125 @@
---------------------------------------------------------------------------
-- Check if some file exists inside a directory.
--
-- For performance reasons this module once recursively scans the directory to
-- collect information about all files and then uses this cache to answer
-- queries. If the directory is modified, it is rescanned. However,
-- subdirectories are not checked for modification.
--
-- Note that this is not re-entrant in the sense that you may not use instances
-- of this class from multiple async contexts at once.
--
-- @author Uli Schlachter
-- @copyright 2017 Uli Schlachter
-- @classmod gears.filesystem.subdirectory_cache
---------------------------------------------------------------------------
local gio = require("lgi").Gio
local gdebug = require("gears.debug")
local cache = {}
local function async_get_mtime(file)
local info = file:async_query_info("time::modified", gio.FileQueryInfoFlags.NONE)
return info and info:get_attribute_uint64("time::modified")
end
local function get_readable_path(gfile)
return gfile:get_path() or gfile:get_uri()
end
local function scan(self, directory)
local query = "standard::name,standard::type"
local enum, err = directory:async_enumerate_children(query, gio.FileQueryInfoFlags.NONE)
if not enum then
gdebug.print_warning(get_readable_path(directory) .. ": " .. tostring(err))
return
end
local per_call = 1000 -- Random value
local children = {}
while true do
local list, enum_err = enum:async_next_files(per_call)
if enum_err then
gdebug.print_warning(get_readable_path(directory) .. ": " .. tostring(enum_err))
return
end
for _, info in ipairs(list) do
local file_type = info:get_file_type()
local child = enum:get_child(info)
if file_type == 'REGULAR' then
local path = child:get_path()
if path then
self.known_paths[path] = true
end
elseif file_type == 'DIRECTORY' then
table.insert(children, child)
end
end
if #list == 0 then
break
end
end
enum:async_close()
for _, child in ipairs(children) do
scan(self, child)
end
end
--- Asynchronously ensure that this cache object is up to date. You do not have
-- to call this yourself unless you want to inspect the `.known_paths` table
-- directly.
function cache:async_update()
local now = os.time()
-- Can we still just assume that we are up to date?
if self.last_check + self.timeout >= now then
return
end
self.last_check = now
-- Did the directory change?
local mtime = async_get_mtime(self.directory)
if self.directory_mtime == mtime then
return
end
self.directory_mtime = mtime
-- We need to rebuild the cache
self.known_paths = {}
scan(self, self.directory)
end
--- Asynchronously check if a file exists.
-- @tparam string path relative path of the file.
-- @treturn boolean True if the file exists
function cache:async_check_exists(path)
local sane_path = self.directory:get_child(path):get_path()
if not sane_path then
return false
end
self:async_update()
return self.known_paths[sane_path] or false
end
--- Asynchronously creates a new cache object.
-- @tparam string directory The directory to check.
-- @tparam[opt=5] int timeout Number of seconds that cache might be out of date.
-- @return A new cache object.
function cache.async_new(directory, timeout)
local result = setmetatable({
directory = gio.File.new_for_path(directory),
timeout = timeout or 5,
last_check = 0,
directory_mtime = 0,
known_paths = {},
}, { __index = cache })
result:async_update()
return result
end
--- Contains all known subfiles
-- @table cache.known_paths
return cache
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

View File

@ -0,0 +1,101 @@
---------------------------------------------------------------------------
-- @author Uli Schlachter
-- @copyright 2017 Uli Schlachter
---------------------------------------------------------------------------
-- Fake news?
local fake_time = os.time()
_G.os.time = function()
return fake_time
end
local dir_cache = require("gears.filesystem.subdirectory_cache")
local protected_call = require("gears.protected_call")
local gio = require("lgi").Gio
describe("gears.filesystem.subdirectory_cache", function()
describe("finds files", function()
-- This assumes that we are run from awesome's source directory
gio.Async.call(protected_call)(function()
local cache = dir_cache.async_new("spec/gears")
assert.is_true(cache:async_check_exists("filesystem/subdirectory_cache_spec.lua"))
assert.is_true(cache:async_check_exists("cache_spec.lua"))
assert.is_false(cache:async_check_exists(""))
assert.is_false(cache:async_check_exists("something weird"))
end)
end)
describe("non-existing directory", function()
gio.Async.call(protected_call)(function()
local gdebug = require("gears.debug")
local print_warning = gdebug.print_warning
local called
function gdebug.print_warning(message)
assert(message:find("/this/does not/exist/please"), message)
called = true
end
local cache = dir_cache.async_new("/this/does not/exist/please")
assert(called)
gdebug.print_warning = print_warning
assert.is_false(cache:async_check_exists(""))
assert.is_false(cache:async_check_exists("something weird"))
assert.is.same({}, cache.known_paths)
end)
end)
describe("cache expiry", function()
local test_path = gio.File.new_tmp("awesome-tests-path-XXXXXX")
local dir = test_path:get_child("dir")
local file1 = test_path:get_child("file1")
local file2 = dir:get_child("file2")
test_path:delete()
assert(test_path:make_directory())
assert(gio.Async.call(protected_call)(function()
-- At this point the directory is empty
local cache = dir_cache.async_new(test_path:get_path())
assert.is.same({}, cache.known_paths)
-- Create some files
assert(dir:make_directory())
assert(file1:create(gio.FileCreateFlags.NONE):async_close())
assert(file2:create(gio.FileCreateFlags.NONE):async_close())
-- The cache still doesn't know about these files
cache:async_update()
assert.is.same({}, cache.known_paths)
-- Let the cache think some time has passed so that it updates.
fake_time = fake_time + 6
cache:async_update()
assert.is.same({}, cache.known_paths)
-- Plus, mess with the mtime of the file.
fake_time = fake_time + 6
assert(test_path:set_attribute_uint64("time::modified",
fake_time, gio.FileQueryInfoFlags.NONE))
cache:async_update()
assert(cache.known_paths[file1:get_path()])
assert(cache.known_paths[file2:get_path()])
local count = 0
for _ in pairs(cache.known_paths) do
count = count + 1
end
assert.is.equal(count, 2)
return true
end))
file1:delete()
file2:delete()
dir:delete()
test_path:delete()
end)
end)
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80