data-tre.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['data-tre'] = {
    version   = 1.001,
    comment   = "companion to luat-lib.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

-- A tree search is rather dumb ... there is some basic caching of searched trees
-- but nothing is cached over runs ... it's also a wildcard one so we cannot use
-- the normal scanner.

-- tree://e:/temporary/mb-mp/**/drawing.jpg
-- tree://e:/temporary/mb-mp/**/Drawing.jpg
-- tree://t:./**/tufte.tex
-- tree://t:/./**/tufte.tex
-- tree://t:/**/tufte.tex
-- dirlist://e:/temporary/mb-mp/**/drawing.jpg
-- dirlist://e:/temporary/mb-mp/**/Drawing.jpg
-- dirlist://e:/temporary/mb-mp/**/just/some/place/drawing.jpg
-- dirlist://e:/temporary/mb-mp/**/images/drawing.jpg
-- dirlist://e:/temporary/mb-mp/**/images/drawing.jpg?option=fileonly
-- dirlist://///storage-2/resources/mb-mp/**/drawing.jpg
-- dirlist://e:/**/drawing.jpg

local type = type
local find, gsub, lower = string.find, string.gsub, string.lower
local basename, dirname, joinname = file.basename, file.dirname, file.join
local globdir, isdir, isfile = dir.glob, lfs.isdir, lfs.isfile
local P, lpegmatch = lpeg.P, lpeg.match

local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)

local report_trees   = logs.reporter("resolvers","trees")

local resolvers  = resolvers
local finders    = resolvers.finders
local openers    = resolvers.openers
local loaders    = resolvers.loaders
local locators   = resolvers.locators
local hashers    = resolvers.hashers
local generators = resolvers.generators

do

    local collectors = { }
    local found      = { }
    local notfound   = finders.notfound

    function finders.tree(specification) -- to be adapted to new formats
        local spec = specification.filename
        local okay = found[spec]
        if okay == nil then
            if spec ~= "" then
                local path = dirname(spec)
                local name = basename(spec)
                if path == "" then
                    path = "."
                end
                local names = collectors[path]
                if not names then
                    local pattern = find(path,"/%*+$") and path or (path .. "/*")
                    names = globdir(pattern)
                    collectors[path] = names
                end
                local pattern = "/" .. gsub(name,"([%.%-%+])", "%%%1") .. "$"
                for i=1,#names do
                    local fullname = names[i]
                    if find(fullname,pattern) then
                        found[spec] = fullname
                        return fullname
                    end
                end
                -- let's be nice:
                local pattern = lower(pattern)
                for i=1,#names do
                    local fullname = lower(names[i])
                    if find(fullname,pattern) then
                        if isfile(fullname) then
                            found[spec] = fullname
                            return fullname
                        else
                            -- no os name mapping
                            break
                        end
                    end
                end
            end
            okay = notfound() -- false
            found[spec] = okay
        end
        return okay
    end

end

do

    local resolveprefix = resolvers.resolve
    local appendhash    = resolvers.appendhash

    local function dolocate(specification)
        local name     = specification.filename
        local realname = resolveprefix(name) -- no shortcut
        if realname and realname ~= '' and isdir(realname) then
            if trace_locating then
                report_trees("locator %a found",realname)
            end
            appendhash('tree',name,false) -- don't cache
        elseif trace_locating then
            report_trees("locator %a not found",name)
        end
    end

    locators.tree    = dolocate
    locators.dirlist = dolocate
    locators.dirfile = dolocate

end


do

    local filegenerator = generators.file

    generators.dirlist = filegenerator
    generators.dirfile = filegenerator

end

do

    local filegenerator = generators.file
    local methodhandler = resolvers.methodhandler

    local function dohash(specification)
        local name = specification.filename
        if trace_locating then
            report_trees("analyzing %a",name)
        end
        methodhandler("hashers",name)
        filegenerator(specification)
    end

    hashers.tree    = dohash
    hashers.dirlist = dohash
    hashers.dirfile = dohash

end

-- This is a variation on tree lookups but this time we do cache in the given
-- root. We use a similar hasher as the resolvers because we have to deal with
-- for instance trees with 50K xml files plus a similar amount of resources to
-- deal and we don't want too much overhead.

local resolve  do

    local collectors  = { }
    local splitter    = lpeg.splitat("/**/")
    local stripper    = lpeg.replacer { [P("/") * P("*")^1 * P(-1)] = "" }

    local loadcontent = caches.loadcontent
    local savecontent = caches.savecontent

    local notfound    = finders.notfound

    local scanfiles   = resolvers.scanfiles
    local lookup      = resolvers.get_from_content

    table.setmetatableindex(collectors, function(t,k)
        local rootname = lpegmatch(stripper,k)
        local dataname = joinname(rootname,"dirlist")
        local content  = loadcontent(dataname,"files",dataname)
        if not content then
            -- path branch usecache onlyonce tolerant
            content = scanfiles(rootname,nil,nil,false,true) -- so we accept crap
            savecontent(dataname,"files",content,dataname)
        end
        t[k] = content
        return content
    end)

    local function checked(root,p,n)
        if p then
            if type(p) == "table" then
                for i=1,#p do
                    local fullname = joinname(root,p[i],n)
                    if isfile(fullname) then -- safeguard
                        return fullname
                    end
                end
            else
                local fullname = joinname(root,p,n)
                if isfile(fullname) then -- safeguard
                    return fullname
                end
            end
        end
        return notfound()
    end

    -- no funny characters in path but in filename permitted .. sigh

    resolve = function(specification) -- can be called directly too
        local filename = specification.filename
     -- inspect(specification)
        if filename ~= "" then
            local root, rest = lpegmatch(splitter,filename)
            if root and rest then
                local path, name = dirname(rest), basename(rest)
                if name ~= rest then
                    local content = collectors[root]
                    local p, n = lookup(content,name)
                    if not p then
                        return notfound()
                    end
                    local pattern = ".*/" .. path .. "$"
                    local istable = type(p) == "table"
                    if istable then
                        for i=1,#p do
                            local pi = p[i]
                            if pi == path or find(pi,pattern) then
                                local fullname = joinname(root,pi,n)
                                if isfile(fullname) then -- safeguard
                                    return fullname
                                end
                            end
                        end
                    elseif p == path or find(p,pattern) then
                        local fullname = joinname(root,p,n)
                        if isfile(fullname) then -- safeguard
                            return fullname
                        end
                    end
                    local queries = specification.queries
                    if queries and queries.option == "fileonly" then
                        return checked(root,p,n)
                    else
                        return notfound()
                    end
                end
            end
            local path    = dirname(filename)
            local name    = basename(filename)
            local root    = lpegmatch(stripper,path)
            local content = collectors[path]
            local p, n = lookup(content,name)
            if p then
                return checked(root,p,n)
            end
        end
        return notfound()
    end

    finders.dirlist = resolve

    function finders.dirfile(specification)
        local queries = specification.queries
        if queries then
            queries.option = "fileonly"
        else
            specification.queries = { option = "fileonly" }
        end
        return resolve(specification)
    end

end

do

    local fileopener = openers.file
    local fileloader = loaders.file

    openers.dirlist = fileopener
    loaders.dirlist = fileloader

    openers.dirfile = fileopener
    loaders.dirfile = fileloader

end

-- print(resolvers.findtexfile("tree://e:/temporary/mb-mp/**/VB_wmf_03_vw_01d_ant.jpg"))
-- print(resolvers.findtexfile("tree://t:/**/tufte.tex"))
-- print(resolvers.findtexfile("dirlist://e:/temporary/mb-mp/**/VB_wmf_03_vw_01d_ant.jpg"))


do

    local hashfile = "dirhash.lua"
    local kind     = "HASH256"
    local version  = 1.0

    local loadtable = table.load
    local savetable = table.save
    local loaddata  = io.loaddata

    function resolvers.dirstatus(patterns)
        local t = type(patterns)
        if t == "string" then
            patterns = { patterns }
        elseif t ~= "table" then
            return false
        end
        local status = loadtable(hashfile)
        if not status or status.version ~= version or status.kind ~= kind then
            status = {
                version = 1.0,
                kind    = kind,
                hashes  = { },
            }
        end
        local hashes  = status.hashes
        local changed = { }
        local action  = sha2[kind]
        local update  = { }
        for i=1,#patterns do
            local pattern = patterns[i]
            local files   = globdir(pattern)
            for i=1,#files do
                local name = files[i]
                local hash = action(loaddata(name))
                if hashes[name] ~= hash then
                    changed[#changed+1] = name
                end
                update[name] = hash
            end
        end
        status.hashes = update
        savetable(hashfile,status)
        return #changed > 0 and changed or false
    end

end