Problem: files were always created with mode 0644 and directories with 0755, hardcoded in fs.touch and uv.fs_mkdir. Users who need different defaults (e.g. 0600 for security) had no config option. Solution: add new_file_mode (default 420 = 0644) and new_dir_mode (default 493 = 0755) config options, passed through to fs.touch and uv.fs_mkdir in the files and mac trash adapters. The fs.touch signature accepts an optional mode parameter with backwards compatibility (detects function argument to support old callers). Local cache directories (SSH, S3) continue using standard system permissions rather than the user-configured mode. Based on: stevearc/oil.nvim#537
385 lines
9.8 KiB
Lua
385 lines
9.8 KiB
Lua
local log = require("oil.log")
|
|
local M = {}
|
|
|
|
local uv = vim.uv or vim.loop
|
|
|
|
---@type boolean
|
|
M.is_windows = uv.os_uname().version:match("Windows")
|
|
|
|
M.is_mac = uv.os_uname().sysname == "Darwin"
|
|
|
|
M.is_linux = not M.is_windows and not M.is_mac
|
|
|
|
---@type string
|
|
M.sep = M.is_windows and "\\" or "/"
|
|
|
|
---@param ... string
|
|
M.join = function(...)
|
|
return table.concat({ ... }, M.sep)
|
|
end
|
|
|
|
---Check if OS path is absolute
|
|
---@param dir string
|
|
---@return boolean
|
|
M.is_absolute = function(dir)
|
|
if M.is_windows then
|
|
return dir:match("^%a:\\")
|
|
else
|
|
return vim.startswith(dir, "/")
|
|
end
|
|
end
|
|
|
|
M.abspath = function(path)
|
|
if not M.is_absolute(path) then
|
|
path = vim.fn.fnamemodify(path, ":p")
|
|
end
|
|
return path
|
|
end
|
|
|
|
---@param path string
|
|
---@param mode? integer File mode in decimal (default 420 = 0644)
|
|
---@param cb fun(err: nil|string)
|
|
M.touch = function(path, mode, cb)
|
|
if type(mode) == "function" then
|
|
cb = mode
|
|
mode = 420
|
|
end
|
|
uv.fs_open(path, "a", mode or 420, function(err, fd)
|
|
if err then
|
|
cb(err)
|
|
else
|
|
assert(fd)
|
|
uv.fs_close(fd, cb)
|
|
end
|
|
end)
|
|
end
|
|
|
|
--- Returns true if candidate is a subpath of root, or if they are the same path.
|
|
---@param root string
|
|
---@param candidate string
|
|
---@return boolean
|
|
M.is_subpath = function(root, candidate)
|
|
if candidate == "" then
|
|
return false
|
|
end
|
|
root = vim.fs.normalize(M.abspath(root))
|
|
-- Trim trailing "/" from the root
|
|
if root:find("/", -1) then
|
|
root = root:sub(1, -2)
|
|
end
|
|
candidate = vim.fs.normalize(M.abspath(candidate))
|
|
if M.is_windows then
|
|
root = root:lower()
|
|
candidate = candidate:lower()
|
|
end
|
|
if root == candidate then
|
|
return true
|
|
end
|
|
local prefix = candidate:sub(1, root:len())
|
|
if prefix ~= root then
|
|
return false
|
|
end
|
|
|
|
local candidate_starts_with_sep = candidate:find("/", root:len() + 1, true) == root:len() + 1
|
|
local root_ends_with_sep = root:find("/", root:len(), true) == root:len()
|
|
|
|
return candidate_starts_with_sep or root_ends_with_sep
|
|
end
|
|
|
|
---@param path string
|
|
---@return string
|
|
M.posix_to_os_path = function(path)
|
|
if M.is_windows then
|
|
if vim.startswith(path, "/") then
|
|
local drive = path:match("^/(%a+)")
|
|
if not drive then
|
|
return path
|
|
end
|
|
local rem = path:sub(drive:len() + 2)
|
|
return string.format("%s:%s", drive, rem:gsub("/", "\\"))
|
|
else
|
|
local newpath = path:gsub("/", "\\")
|
|
return newpath
|
|
end
|
|
else
|
|
return path
|
|
end
|
|
end
|
|
|
|
---@param path string
|
|
---@return string
|
|
M.os_to_posix_path = function(path)
|
|
if M.is_windows then
|
|
if M.is_absolute(path) then
|
|
local drive, rem = path:match("^([^:]+):\\(.*)$")
|
|
return string.format("/%s/%s", drive:upper(), rem:gsub("\\", "/"))
|
|
else
|
|
local newpath = path:gsub("\\", "/")
|
|
return newpath
|
|
end
|
|
else
|
|
return path
|
|
end
|
|
end
|
|
|
|
local home_dir = assert(uv.os_homedir())
|
|
|
|
---@param path string
|
|
---@param relative_to? string Shorten relative to this path (default cwd)
|
|
---@return string
|
|
M.shorten_path = function(path, relative_to)
|
|
if not relative_to then
|
|
relative_to = vim.fn.getcwd()
|
|
end
|
|
local relpath
|
|
if M.is_subpath(relative_to, path) then
|
|
local idx = relative_to:len() + 1
|
|
-- Trim the dividing slash if it's not included in relative_to
|
|
if not vim.endswith(relative_to, "/") and not vim.endswith(relative_to, "\\") then
|
|
idx = idx + 1
|
|
end
|
|
relpath = path:sub(idx)
|
|
if relpath == "" then
|
|
relpath = "."
|
|
end
|
|
end
|
|
if M.is_subpath(home_dir, path) then
|
|
local homepath = "~" .. path:sub(home_dir:len() + 1)
|
|
if not relpath or homepath:len() < relpath:len() then
|
|
return homepath
|
|
end
|
|
end
|
|
return relpath or path
|
|
end
|
|
|
|
---@param dir string
|
|
---@param mode? integer
|
|
M.mkdirp = function(dir, mode)
|
|
mode = mode or 493
|
|
local mod = ""
|
|
local path = dir
|
|
while vim.fn.isdirectory(path) == 0 do
|
|
mod = mod .. ":h"
|
|
path = vim.fn.fnamemodify(dir, mod)
|
|
end
|
|
while mod ~= "" do
|
|
mod = mod:sub(3)
|
|
path = vim.fn.fnamemodify(dir, mod)
|
|
uv.fs_mkdir(path, mode)
|
|
end
|
|
end
|
|
|
|
---@param dir string
|
|
---@param cb fun(err: nil|string, entries: nil|{type: oil.EntryType, name: string})
|
|
M.listdir = function(dir, cb)
|
|
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
|
|
uv.fs_opendir(dir, function(open_err, fd)
|
|
if open_err then
|
|
return cb(open_err)
|
|
end
|
|
local read_next
|
|
read_next = function()
|
|
uv.fs_readdir(fd, function(err, entries)
|
|
if err then
|
|
uv.fs_closedir(fd, function()
|
|
cb(err)
|
|
end)
|
|
return
|
|
elseif entries then
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
cb(nil, entries)
|
|
read_next()
|
|
else
|
|
uv.fs_closedir(fd, function(close_err)
|
|
if close_err then
|
|
cb(close_err)
|
|
else
|
|
cb()
|
|
end
|
|
end)
|
|
end
|
|
end)
|
|
end
|
|
read_next()
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
end, 10000)
|
|
end
|
|
|
|
---@param entry_type oil.EntryType
|
|
---@param path string
|
|
---@param cb fun(err: nil|string)
|
|
M.recursive_delete = function(entry_type, path, cb)
|
|
if entry_type ~= "directory" then
|
|
return uv.fs_unlink(path, cb)
|
|
end
|
|
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
|
|
uv.fs_opendir(path, function(open_err, fd)
|
|
if open_err then
|
|
return cb(open_err)
|
|
end
|
|
local poll
|
|
poll = function(inner_cb)
|
|
uv.fs_readdir(fd, function(err, entries)
|
|
if err then
|
|
return inner_cb(err)
|
|
elseif entries then
|
|
local waiting = #entries
|
|
local complete
|
|
complete = function(err2)
|
|
if err2 then
|
|
complete = function() end
|
|
return inner_cb(err2)
|
|
end
|
|
waiting = waiting - 1
|
|
if waiting == 0 then
|
|
poll(inner_cb)
|
|
end
|
|
end
|
|
for _, entry in ipairs(entries) do
|
|
M.recursive_delete(entry.type, path .. M.sep .. entry.name, complete)
|
|
end
|
|
else
|
|
inner_cb()
|
|
end
|
|
end)
|
|
end
|
|
poll(function(err)
|
|
uv.fs_closedir(fd)
|
|
if err then
|
|
return cb(err)
|
|
end
|
|
uv.fs_rmdir(path, cb)
|
|
end)
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
end, 10000)
|
|
end
|
|
|
|
---Move the undofile for the file at src_path to dest_path
|
|
---@param src_path string
|
|
---@param dest_path string
|
|
---@param copy boolean
|
|
local move_undofile = vim.schedule_wrap(function(src_path, dest_path, copy)
|
|
local undofile = vim.fn.undofile(src_path)
|
|
uv.fs_stat(
|
|
undofile,
|
|
vim.schedule_wrap(function(stat_err)
|
|
if stat_err then
|
|
-- undofile doesn't exist
|
|
return
|
|
end
|
|
local dest_undofile = vim.fn.undofile(dest_path)
|
|
if copy then
|
|
uv.fs_copyfile(src_path, dest_path, function(err)
|
|
if err then
|
|
log.warn("Error copying undofile %s: %s", undofile, err)
|
|
end
|
|
end)
|
|
else
|
|
uv.fs_rename(undofile, dest_undofile, function(err)
|
|
if err then
|
|
log.warn("Error moving undofile %s: %s", undofile, err)
|
|
end
|
|
end)
|
|
end
|
|
end)
|
|
)
|
|
end)
|
|
|
|
---@param entry_type oil.EntryType
|
|
---@param src_path string
|
|
---@param dest_path string
|
|
---@param cb fun(err: nil|string)
|
|
M.recursive_copy = function(entry_type, src_path, dest_path, cb)
|
|
if entry_type == "link" then
|
|
uv.fs_readlink(src_path, function(link_err, link)
|
|
if link_err then
|
|
return cb(link_err)
|
|
end
|
|
assert(link)
|
|
uv.fs_symlink(link, dest_path, 0, cb)
|
|
end)
|
|
return
|
|
end
|
|
if entry_type ~= "directory" then
|
|
uv.fs_copyfile(src_path, dest_path, { excl = true }, cb)
|
|
move_undofile(src_path, dest_path, true)
|
|
return
|
|
end
|
|
uv.fs_stat(src_path, function(stat_err, src_stat)
|
|
if stat_err then
|
|
return cb(stat_err)
|
|
end
|
|
assert(src_stat)
|
|
uv.fs_mkdir(dest_path, src_stat.mode, function(mkdir_err)
|
|
if mkdir_err then
|
|
return cb(mkdir_err)
|
|
end
|
|
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
|
|
uv.fs_opendir(src_path, function(open_err, fd)
|
|
if open_err then
|
|
return cb(open_err)
|
|
end
|
|
local poll
|
|
poll = function(inner_cb)
|
|
uv.fs_readdir(fd, function(err, entries)
|
|
if err then
|
|
return inner_cb(err)
|
|
elseif entries then
|
|
local waiting = #entries
|
|
local complete
|
|
complete = function(err2)
|
|
if err2 then
|
|
complete = function() end
|
|
return inner_cb(err2)
|
|
end
|
|
waiting = waiting - 1
|
|
if waiting == 0 then
|
|
poll(inner_cb)
|
|
end
|
|
end
|
|
for _, entry in ipairs(entries) do
|
|
M.recursive_copy(
|
|
entry.type,
|
|
src_path .. M.sep .. entry.name,
|
|
dest_path .. M.sep .. entry.name,
|
|
complete
|
|
)
|
|
end
|
|
else
|
|
inner_cb()
|
|
end
|
|
end)
|
|
end
|
|
poll(cb)
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
end, 10000)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
---@param entry_type oil.EntryType
|
|
---@param src_path string
|
|
---@param dest_path string
|
|
---@param cb fun(err: nil|string)
|
|
M.recursive_move = function(entry_type, src_path, dest_path, cb)
|
|
uv.fs_rename(src_path, dest_path, function(err)
|
|
if err then
|
|
-- fs_rename fails for cross-partition or cross-device operations.
|
|
-- We then fall back to a copy + delete
|
|
M.recursive_copy(entry_type, src_path, dest_path, function(err2)
|
|
if err2 then
|
|
cb(err2)
|
|
else
|
|
M.recursive_delete(entry_type, src_path, cb)
|
|
end
|
|
end)
|
|
else
|
|
if entry_type ~= "directory" then
|
|
move_undofile(src_path, dest_path, false)
|
|
end
|
|
cb()
|
|
end
|
|
end)
|
|
end
|
|
|
|
return M
|