summaryrefslogtreecommitdiff
path: root/script/fs-utility.lua
diff options
context:
space:
mode:
Diffstat (limited to 'script/fs-utility.lua')
-rw-r--r--script/fs-utility.lua559
1 files changed, 559 insertions, 0 deletions
diff --git a/script/fs-utility.lua b/script/fs-utility.lua
new file mode 100644
index 00000000..42041734
--- /dev/null
+++ b/script/fs-utility.lua
@@ -0,0 +1,559 @@
+local fs = require 'bee.filesystem'
+local platform = require 'bee.platform'
+
+local type = type
+local ioOpen = io.open
+local pcall = pcall
+local pairs = pairs
+local setmetatable = setmetatable
+local next = next
+local ipairs = ipairs
+local tostring = tostring
+local tableSort = table.sort
+
+_ENV = nil
+
+local m = {}
+--- 读取文件
+---@param path string
+function m.loadFile(path)
+ if type(path) ~= 'string' then
+ path = path:string()
+ end
+ local f, e = ioOpen(path, 'rb')
+ if not f then
+ return nil, e
+ end
+ if f:read(3) ~= '\xEF\xBB\xBF' then
+ f:seek("set")
+ end
+ local buf = f:read 'a'
+ f:close()
+ return buf
+end
+
+--- 写入文件
+---@param path string
+---@param content string
+function m.saveFile(path, content)
+ if type(path) ~= 'string' then
+ path = path:string()
+ end
+ local f, e = ioOpen(path, "wb")
+
+ if f then
+ f:write(content)
+ f:close()
+ return true
+ else
+ return false, e
+ end
+end
+
+local function buildOptional(optional)
+ optional = optional or {}
+ optional.add = optional.add or {}
+ optional.del = optional.del or {}
+ optional.mod = optional.mod or {}
+ optional.err = optional.err or {}
+ return optional
+end
+
+local function split(str, sep)
+ local t = {}
+ local current = 1
+ while current <= #str do
+ local s, e = str:find(sep, current)
+ if not s then
+ t[#t+1] = str:sub(current)
+ break
+ end
+ if s > 1 then
+ t[#t+1] = str:sub(current, s - 1)
+ end
+ current = e + 1
+ end
+ return t
+end
+
+local dfs = {}
+dfs.__index = dfs
+dfs.type = 'dummy'
+dfs.path = ''
+
+function m.dummyFS(t)
+ return setmetatable({
+ files = t or {},
+ }, dfs)
+end
+
+function dfs:__tostring()
+ return 'dummy:' .. tostring(self.path)
+end
+
+function dfs:__div(filename)
+ if type(filename) ~= 'string' then
+ filename = filename:string()
+ end
+ local new = m.dummyFS(self.files)
+ if self.path:sub(-1):match '[^/\\]' then
+ new.path = self.path .. '\\' .. filename
+ else
+ new.path = self.path .. filename
+ end
+ return new
+end
+
+function dfs:_open(index)
+ local paths = split(self.path, '[/\\]')
+ local current = self.files
+ if not index then
+ index = #paths
+ elseif index < 0 then
+ index = #paths + index + 1
+ end
+ for i = 1, index do
+ local path = paths[i]
+ if current[path] then
+ current = current[path]
+ else
+ return nil
+ end
+ end
+ return current
+end
+
+function dfs:_filename()
+ return self.path:match '[^/\\]+$'
+end
+
+function dfs:parent_path()
+ local new = m.dummyFS(self.files)
+ if self.path:find('[/\\]') then
+ new.path = self.path:gsub('[/\\]+[^/\\]*$', '')
+ else
+ new.path = ''
+ end
+ return new
+end
+
+function dfs:filename()
+ local new = m.dummyFS(self.files)
+ new.path = self:_filename()
+ return new
+end
+
+function dfs:string()
+ return self.path
+end
+
+function dfs:list_directory()
+ local dir = self:_open()
+ if type(dir) ~= 'table' then
+ return function () end
+ end
+ local keys = {}
+ for k in pairs(dir) do
+ keys[#keys+1] = k
+ end
+ tableSort(keys)
+ local i = 0
+ return function ()
+ i = i + 1
+ local k = keys[i]
+ if not k then
+ return nil
+ end
+ return self / k
+ end
+end
+
+function dfs:isDirectory()
+ local target = self:_open()
+ if type(target) == 'table' then
+ return true
+ end
+ return false
+end
+
+function dfs:remove()
+ local dir = self:_open(-2)
+ local filename = self:_filename()
+ if not filename then
+ return
+ end
+ dir[filename] = nil
+end
+
+function dfs:exists()
+ local target = self:_open()
+ return target ~= nil
+end
+
+function dfs:createDirectories(path)
+ if type(path) ~= 'string' then
+ path = path:string()
+ end
+ local paths = split(path, '[/\\]')
+ local current = self.files
+ for i = 1, #paths do
+ local sub = paths[i]
+ if current[sub] then
+ if type(current[sub]) ~= 'table' then
+ return false
+ end
+ else
+ current[sub] = {}
+ end
+ current = current[sub]
+ end
+ return true
+end
+
+function dfs:saveFile(path, text)
+ if type(path) ~= 'string' then
+ path = path:string()
+ end
+ local temp = m.dummyFS(self.files)
+ temp.path = path
+ local dir = temp:_open(-2)
+ if not dir then
+ return false, '无法打开:' .. path
+ end
+ local filename = temp:_filename()
+ if not filename then
+ return false, '无法打开:' .. path
+ end
+ if type(dir[filename]) == 'table' then
+ return false, '无法打开:' .. path
+ end
+ dir[filename] = text
+end
+
+local function fsAbsolute(path, optional)
+ if type(path) == 'string' then
+ local suc, res = pcall(fs.path, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return nil
+ end
+ path = res
+ elseif type(path) == 'table' then
+ return path
+ end
+ local suc, res = pcall(fs.absolute, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return nil
+ end
+ return res
+end
+
+local function fsIsDirectory(path, optional)
+ if path.type == 'dummy' then
+ return path:isDirectory()
+ end
+ local suc, res = pcall(fs.is_directory, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return false
+ end
+ return res
+end
+
+local function fsRemove(path, optional)
+ if path.type == 'dummy' then
+ return path:remove()
+ end
+ local suc, res = pcall(fs.remove, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ end
+ optional.del[#optional.del+1] = path:string()
+end
+
+local function fsExists(path, optional)
+ if path.type == 'dummy' then
+ return path:exists()
+ end
+ local suc, res = pcall(fs.exists, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return false
+ end
+ return res
+end
+
+local function fsSave(path, text, optional)
+ if path.type == 'dummy' then
+ local dir = path:_open(-2)
+ if not dir then
+ optional.err[#optional.err+1] = '无法打开:' .. path:string()
+ return false
+ end
+ local filename = path:_filename()
+ if not filename then
+ optional.err[#optional.err+1] = '无法打开:' .. path:string()
+ return false
+ end
+ if type(dir[filename]) == 'table' then
+ optional.err[#optional.err+1] = '无法打开:' .. path:string()
+ return false
+ end
+ dir[filename] = text
+ else
+ local suc, err = m.saveFile(path, text)
+ if suc then
+ return true
+ end
+ optional.err[#optional.err+1] = err
+ return false
+ end
+end
+
+local function fsLoad(path, optional)
+ if path.type == 'dummy' then
+ local text = path:_open()
+ if type(text) == 'string' then
+ return text
+ else
+ optional.err[#optional.err+1] = '无法打开:' .. path:string()
+ return nil
+ end
+ else
+ local text, err = m.loadFile(path)
+ if text then
+ return text
+ else
+ optional.err[#optional.err+1] = err
+ return nil
+ end
+ end
+end
+
+local function fsCopy(source, target, optional)
+ if source.type == 'dummy' then
+ local sourceText = source:_open()
+ if not sourceText then
+ optional.err[#optional.err+1] = '无法打开:' .. source:string()
+ return false
+ end
+ return fsSave(target, sourceText, optional)
+ else
+ if target.type == 'dummy' then
+ local sourceText, err = m.loadFile(source)
+ if not sourceText then
+ optional.err[#optional.err+1] = err
+ return false
+ end
+ return fsSave(target, sourceText, optional)
+ else
+ local suc, res = pcall(fs.copy_file, source, target, true)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return false
+ end
+ end
+ end
+ return true
+end
+
+local function fsCreateDirectories(path, optional)
+ if path.type == 'dummy' then
+ return path:createDirectories()
+ end
+ local suc, res = pcall(fs.create_directories, path)
+ if not suc then
+ optional.err[#optional.err+1] = res
+ return false
+ end
+ return true
+end
+
+local function fileRemove(path, optional)
+ if optional.onRemove and optional.onRemove(path) == false then
+ return
+ end
+ if fsIsDirectory(path, optional) then
+ for child in path:list_directory() do
+ fileRemove(child, optional)
+ end
+ end
+ if fsRemove(path, optional) then
+ optional.del[#optional.del+1] = path:string()
+ end
+end
+
+local function fileCopy(source, target, optional)
+ if optional.onCopy and optional.onCopy(source, target) == false then
+ return
+ end
+ local isDir1 = fsIsDirectory(source, optional)
+ local isDir2 = fsIsDirectory(target, optional)
+ local isExists = fsExists(target, optional)
+ if isDir1 then
+ if isDir2 or fsCreateDirectories(target, optional) then
+ for filePath in source:list_directory() do
+ local name = filePath:filename():string()
+ fileCopy(filePath, target / name, optional)
+ end
+ end
+ else
+ if isExists and not isDir2 then
+ local buf1 = fsLoad(source, optional)
+ local buf2 = fsLoad(target, optional)
+ if buf1 and buf2 then
+ if buf1 ~= buf2 then
+ if fsCopy(source, target, optional) then
+ optional.mod[#optional.mod+1] = target:string()
+ end
+ end
+ end
+ else
+ if fsCopy(source, target, optional) then
+ optional.add[#optional.add+1] = target:string()
+ end
+ end
+ end
+end
+
+local function fileSync(source, target, optional)
+ if optional.onSync and optional.onSync(source, target) == false then
+ return
+ end
+ local isDir1 = fsIsDirectory(source, optional)
+ local isDir2 = fsIsDirectory(target, optional)
+ local isExists = fsExists(target, optional)
+ if isDir1 then
+ if isDir2 then
+ local fileList = m.fileList()
+ for filePath in target:list_directory() do
+ fileList[filePath] = true
+ end
+ for filePath in source:list_directory() do
+ local name = filePath:filename():string()
+ local targetPath = target / name
+ fileSync(filePath, targetPath, optional)
+ fileList[targetPath] = nil
+ end
+ for path in pairs(fileList) do
+ fileRemove(path, optional)
+ end
+ else
+ if isExists then
+ fileRemove(target, optional)
+ end
+ if fsCreateDirectories(target) then
+ for filePath in source:list_directory() do
+ local name = filePath:filename():string()
+ fileCopy(filePath, target / name, optional)
+ end
+ end
+ end
+ else
+ if isDir2 then
+ fileRemove(target, optional)
+ end
+ if isExists then
+ local buf1 = fsLoad(source, optional)
+ local buf2 = fsLoad(target, optional)
+ if buf1 and buf2 then
+ if buf1 ~= buf2 then
+ if fsCopy(source, target, optional) then
+ optional.mod[#optional.mod+1] = target:string()
+ end
+ end
+ end
+ else
+ if fsCopy(source, target, optional) then
+ optional.add[#optional.add+1] = target:string()
+ end
+ end
+ end
+end
+
+--- 文件列表
+function m.fileList(optional)
+ optional = optional or buildOptional(optional)
+ local os = platform.OS
+ local keyMap = {}
+ local fileList = {}
+ local function computeKey(path)
+ path = fsAbsolute(path, optional)
+ if not path then
+ return nil
+ end
+ local key
+ if os == 'Windows' then
+ key = path:string():lower()
+ else
+ key = path:string()
+ end
+ return key
+ end
+ return setmetatable({}, {
+ __index = function (_, path)
+ local key = computeKey(path)
+ return fileList[key]
+ end,
+ __newindex = function (_, path, value)
+ local key = computeKey(path)
+ if not key then
+ return
+ end
+ if value == nil then
+ keyMap[key] = nil
+ else
+ keyMap[key] = path
+ fileList[key] = value
+ end
+ end,
+ __pairs = function ()
+ local key, path
+ return function ()
+ key, path = next(keyMap, key)
+ return path, fileList[key]
+ end
+ end,
+ })
+end
+
+--- 删除文件(夹)
+function m.fileRemove(path, optional)
+ optional = buildOptional(optional)
+ path = fsAbsolute(path, optional)
+
+ fileRemove(path, optional)
+
+ return optional
+end
+
+--- 复制文件(夹)
+---@param source string
+---@param target string
+---@return table
+function m.fileCopy(source, target, optional)
+ optional = buildOptional(optional)
+ source = fsAbsolute(source, optional)
+ target = fsAbsolute(target, optional)
+
+ fileCopy(source, target, optional)
+
+ return optional
+end
+
+--- 同步文件(夹)
+---@param source string
+---@param target string
+---@return table
+function m.fileSync(source, target, optional)
+ optional = buildOptional(optional)
+ source = fsAbsolute(source, optional)
+ target = fsAbsolute(target, optional)
+
+ fileSync(source, target, optional)
+
+ return optional
+end
+
+return m