460dc2c5c8
Many upstream plugins publish no tags at all, so a tag-only installer cannot manage them. - Add a branch spec field, mutually exclusive with tag and validated the same way - Build archive/refs/heads/<branch> URLs for branch specs - Refetch a branch only on :ArtiPackUpdate or a forced sync, never from cache - Record the branch, archive digest and fetch time in the lockfile - Report a branch plugin as tracked and fetched, never as up to date
109 lines
2.5 KiB
Lua
109 lines
2.5 KiB
Lua
local uv = vim.uv or vim.loop
|
|
|
|
local M = {}
|
|
|
|
local ZIP_MAGIC = 'PK\3\4'
|
|
local GZIP_MAGIC = '\31\139'
|
|
|
|
function M.join(...)
|
|
return table.concat({ ... }, '/')
|
|
end
|
|
|
|
function M.mkdirp(path)
|
|
vim.fn.mkdir(path, 'p')
|
|
return path
|
|
end
|
|
|
|
function M.rm_rf(path)
|
|
if uv.fs_stat(path) then
|
|
vim.fn.delete(path, 'rf')
|
|
end
|
|
end
|
|
|
|
function M.exists(path)
|
|
return uv.fs_stat(path) ~= nil
|
|
end
|
|
|
|
function M.entries(dir)
|
|
local handle = uv.fs_scandir(dir)
|
|
if not handle then
|
|
return {}
|
|
end
|
|
local out = {}
|
|
while true do
|
|
local name, kind = uv.fs_scandir_next(handle)
|
|
if not name then
|
|
break
|
|
end
|
|
table.insert(out, { name = name, kind = kind })
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Tag archives wrap everything in a single "<repo>-<tag>" directory. Detect it
|
|
-- rather than reconstructing the name, since the leading "v" is dropped.
|
|
function M.archive_root(dir)
|
|
local entries = M.entries(dir)
|
|
if #entries == 1 and entries[1].kind == 'directory' then
|
|
return M.join(dir, entries[1].name)
|
|
end
|
|
return dir
|
|
end
|
|
|
|
function M.read_head(path, bytes)
|
|
local fd = io.open(path, 'rb')
|
|
if not fd then
|
|
return nil
|
|
end
|
|
local head = fd:read(bytes)
|
|
fd:close()
|
|
return head
|
|
end
|
|
|
|
-- Identifies what was actually fetched. A branch name says nothing about
|
|
-- content, so the digest is what makes a branch install reportable. Archive
|
|
-- bytes reach sha256() as a Blob, which it refuses, so they are hashed in their
|
|
-- base64 form: an identity for the content, not the file's sha256sum.
|
|
function M.digest(path)
|
|
local fd = io.open(path, 'rb')
|
|
if not fd then
|
|
return nil
|
|
end
|
|
local data = fd:read('*a')
|
|
fd:close()
|
|
if not data then
|
|
return nil
|
|
end
|
|
return vim.fn.sha256(vim.base64.encode(data))
|
|
end
|
|
|
|
function M.verify_archive(path, ext)
|
|
local stat = uv.fs_stat(path)
|
|
if not stat or stat.size == 0 then
|
|
return false, 'downloaded archive is empty'
|
|
end
|
|
|
|
local magic = ext == 'zip' and ZIP_MAGIC or GZIP_MAGIC
|
|
local head = M.read_head(path, #magic)
|
|
if head ~= magic then
|
|
return false, string.format('downloaded archive is not a %s', ext)
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Extraction is pure Lua: no unzip or tar on PATH, and entry paths are vetted
|
|
-- here rather than trusted to an external tool.
|
|
function M.extract(archive, dest, ext)
|
|
local ok, res, err = pcall(require('arti-pack.archive').extract, archive, dest, ext)
|
|
if not ok then
|
|
return nil, tostring(res)
|
|
end
|
|
return res, err
|
|
end
|
|
|
|
function M.rename(from, to)
|
|
return uv.fs_rename(from, to)
|
|
end
|
|
|
|
return M
|