install and cp fixes (#2398)

testing completed
This commit is contained in:
payonel 2017-05-22 23:10:59 -07:00 committed by GitHub
parent 08bb90faa3
commit f2b5e01730
8 changed files with 245 additions and 272 deletions

View File

@ -4,15 +4,17 @@ local transfer = require("tools/transfer")
local args, options = shell.parse(...)
options.h = options.h or options.help
if #args < 2 or options.h then
io.write("Usage: cp [-inrv] <from...> <to>\n")
io.write(" -i: prompt before overwrite (overrides -n option).\n")
io.write(" -n: do not overwrite an existing file.\n")
io.write(" -r: copy directories recursively.\n")
io.write(" -u: copy only when the SOURCE file differs from the destination\n")
io.write(" file or when the destination file is missing.\n")
io.write(" -P: preserve attributes, e.g. symbolic links.\n")
io.write(" -v: verbose output.\n")
io.write(" -x: stay on original source file system.\n")
io.write([[Usage: cp [OPTIONS] <from...> <to>
-i: prompt before overwrite (overrides -n option).
-n: do not overwrite an existing file.
-r: copy directories recursively.
-u: copy only when the SOURCE file differs from the destination
file or when the destination file is missing.
-P: preserve attributes, e.g. symbolic links.
-v: verbose output.
-x: stay on original source file system.
--skip=P: skip files matching lua regex P
]])
return not not options.h
end
@ -27,6 +29,7 @@ options =
P = options.P,
v = options.v,
x = options.x,
skip = options.skip,
}
return transfer.batch(args, options)

View File

@ -13,21 +13,23 @@ do
end
if not options then return end
local write = io.write
if computer.freeMemory() < 50000 then
write("Low memory, collecting garbage\n")
print("Low memory, collecting garbage")
for i=1,20 do os.sleep(0) end
end
local cp, reason = loadfile(shell.resolve("cp", "lua"), "bt", _G)
assert(cp, reason)
local ok, ec = pcall(cp, table.unpack(options.cp_args))
assert(ok, ec)
local ec = cp(table.unpack(options.cp_args))
if ec ~= nil and ec ~= 0 then
return ec
end
write("Installation complete!\n")
print("Installation complete!")
if options.setlabel then
pcall(options.target.dev.setLabel, options.label)
@ -36,16 +38,16 @@ end
if options.setboot then
local address = options.target.dev.address
if computer.setBootAddress(address) then
write("Boot address set to " .. address)
print("Boot address set to " .. address)
end
end
if options.reboot then
write("Reboot now? [Y/n] ")
io.write("Reboot now? [Y/n] ")
if ((io.read() or "n").."y"):match("^%s*[Yy]") then
write("\nRebooting now!\n")
print("\nRebooting now!\n")
computer.shutdown(true)
end
end
write("Returning to shell.\n")
print("Returning to shell.\n")

View File

@ -4,12 +4,13 @@ local transfer = require("tools/transfer")
local args, options = shell.parse(...)
options.h = options.h or options.help
if #args < 2 or options.h then
io.write([[Usage: mv [-fiv] <from> <to>
io.write([[Usage: mv [OPTIONS] <from> <to>
-f overwrite without prompt
-i prompt before overwriting
unless -f
-v verbose
-n do not overwrite an existing file
--skip=P ignore paths matching lua regex P
-h, --help show this help
]])
return not not options.h
@ -23,6 +24,7 @@ options =
i = options.i,
v = options.v,
n = options.n, -- no clobber
skip = options.skip,
P = true, -- move operations always preserve
r = true, -- move is allowed to move entire dirs
x = true, -- cannot move mount points

View File

@ -78,22 +78,21 @@ function require(module)
return loaded[module]
elseif not loading[module] then
loading[module] = true
local loader, value, errorMsg = nil, nil, {"module '" .. module .. "' not found:"}
local loader, errorMsg = nil, {"module '" .. module .. "' not found:"}
for i = 1, #package.searchers do
-- the pcall is mostly for out of memory errors
local ok, f, extra = pcall(package.searchers[i], module)
local ok, f = pcall(package.searchers[i], module)
if not ok then
table.insert(errorMsg, "\t" .. (f or "nil"))
elseif f and type(f) ~= "string" then
loader = f
value = extra
break
elseif f then
table.insert(errorMsg, f)
end
end
if loader then
local success, result = pcall(loader, module, value)
local success, result = pcall(loader, module)
loading[module] = false
if not success then
error(result, 2)

View File

@ -1,5 +1,6 @@
local fs = require("filesystem")
local shell = require("shell")
local text = require("text")
local lib = {}
local function perr(ops, format, ...)
@ -31,10 +32,10 @@ end
local function areEqual(path1, path2)
local f1, f2 = fs.open(path1, "rb")
local result = true
if f1 then
f2 = fs.open(path2, "rb")
if f2 then
local result = true
local chunkSize = 4 * 1024
repeat
local s1, s2 = f1:read(chunkSize), f2:read(chunkSize)
@ -86,10 +87,19 @@ local function stat(path, ops, P)
end
function lib.recurse(fromPath, toPath, options, origin, top)
fromPath = fromPath:gsub("/+", "/")
toPath = toPath:gsub("/+", "/")
local fromPathFull = shell.resolve(fromPath)
local toPathFull = shell.resolve(toPath)
local mv = options.cmd == "mv"
local verbose = options.v and (not mv or top)
if select(2, fromPathFull:find(options.skip)) == #fromPathFull then
status(verbose, string.format("skipping %s", fromPath))
return true
end
local function release(result, reason)
if result and mv and top then
local rm_result = not fs.get(fromPath).isReadOnly() and fs.remove(fromPath)
local rm_result = not fs.get(fromPathFull).isReadOnly() and fs.remove(fromPathFull)
if not rm_result then
perr(options, "cannot remove '%s': filesystem is readonly", fromPath)
result = false
@ -101,22 +111,22 @@ function lib.recurse(fromPath, toPath, options, origin, top)
local
ok,
fromReal,
fromError,
_, --fromError,
fromIsLink,
fromLinkTarget,
fromExists,
fromFs,
fromIsDir = stat(fromPath, options, options.P)
fromIsDir = stat(fromPathFull, options, options.P)
if not ok then return nil end
local
ok,
toReal,
toError,
_,--toError,
toIsLink,
toLinkTarget,
_,--toLinkTarget,
toExists,
toFs,
toIsDir = stat(toPath, options)
toIsDir = stat(toPathFull, options)
if not ok then os.exit(1) end
if toFs.isReadOnly() then
perr(options, "cannot create target '%s': filesystem is readonly", toPath)
@ -124,9 +134,7 @@ function lib.recurse(fromPath, toPath, options, origin, top)
end
local same_path = fromReal == toReal
local same_link = fromIsLink and toIsLink and same_path
local verbose = options.v
local same_fs = fromFs == toFs
local is_mount = origin[fromReal]
@ -138,12 +146,12 @@ function lib.recurse(fromPath, toPath, options, origin, top)
if toExists and options.n then
return true
end
fs.remove(toPath)
fs.remove(toPathFull)
if toExists then
status(verbose, string.format("removed '%s'\n", toPath))
status(verbose, string.format("removed '%s'", toPath))
end
status(verbose, fromPath, toPath)
return release(fs.link(fromLinkTarget, toPath))
return release(fs.link(fromLinkTarget, toPathFull))
elseif fromIsDir then
if not options.r then
status(true, string.format("omitting directory '%s'", fromPath))
@ -160,17 +168,20 @@ function lib.recurse(fromPath, toPath, options, origin, top)
if same_fs then
if (toReal.."/"):find(fromReal.."/",1,true) then
return nil, "cannot write a directory, '" .. fromPath .. "', into itself, '" .. toPath .. "'"
elseif mv then
status(verbose, fromPath, toPath)
return os.rename(fromPath, toPath)
end
end
if mv then
if fs.list(toReal)() then -- to is NOT empty
return nil, "cannot move '" .. fromPath .. "' to '" .. toPath .. "': Directory not empty"
end
status(verbose, fromPath, toPath)
end
if not toExists then
status(verbose, fromPath, toPath)
fs.makeDirectory(toPath)
fs.makeDirectory(toPathFull)
end
for file in fs.list(fromPath) do
local result, reason = lib.recurse(fs.concat(fromPath, file), fs.concat(toPath, file), options, origin, false) -- false, no longer top
for file in fs.list(fromPathFull) do
local result, reason = lib.recurse(fromPath .."/".. file, toPath.."/"..file, options, origin, false) -- false, no longer top
if not result then
return false, reason
end
@ -198,7 +209,7 @@ function lib.recurse(fromPath, toPath, options, origin, top)
fs.remove(toReal)
end
status(verbose, fromPath, toPath)
return release(fs.copy(fromPath, toPath))
return release(fs.copy(fromPathFull, toPathFull))
else
return nil, "'" .. fromPath .. "': No such file or directory"
end
@ -210,6 +221,7 @@ function lib.batch(args, options)
-- standardized options
options.i = options.i and not options.f
options.P = options.P or options.r
options.skip = text.escapeMagic(options.skip or "")
local origin = {}
for dev,path in fs.mounts() do
@ -217,27 +229,31 @@ function lib.batch(args, options)
end
local toArg = table.remove(args)
local _, toPath = contents_check(toArg, options)
if not toPath then
local _, ok = contents_check(toArg, options)
if not ok then
return 1
end
local originalToIsDir = fs.isDirectory(toPath)
local originalToIsDir = fs.isDirectory(ok)
for _,arg in ipairs(args) do
for _, fromArg in ipairs(args) do
-- a "contents of" copy is where src path ends in . or ..
-- a source path ending with . is not sufficient - could be the source filename
local contents_of, fromPath = contents_check(arg, options, true)
if fromPath then
local contents_of
contents_of, ok = contents_check(fromArg, options, true)
if ok then
-- we do not append fromPath name to toPath in case of contents_of copy
local nextPath = toPath
local toPath = toArg
if contents_of and options.cmd == "mv" then
perr(options, "invalid move path '%s'", arg)
perr(options, "invalid move path '%s'", fromArg)
else
if not contents_of and originalToIsDir then
nextPath = fs.concat(nextPath, fs.name(fromPath))
local fromName = fs.name(fromArg)
if fromName then
toPath = toPath .. "/" .. fromName
end
end
local result, reason = lib.recurse(fromPath, nextPath, options, origin, true)
local result, reason = lib.recurse(fromArg, toPath, options, origin, true)
if not result then
perr(options, reason)

View File

@ -1,23 +1,11 @@
local computer = require("computer")
local shell = require("shell")
local component = require("component")
local event = require("event")
local fs = require("filesystem")
local unicode = require("unicode")
local text = require("text")
local write = io.write
local args, options = shell.parse(...)
options.sources = {}
options.targets = {}
options.source_label = args[1]
local root_exception
if options.help then
write([[Usage: install [OPTION]...
io.write([[Usage: install [OPTION]...
--from=ADDR install filesystem at ADDR
default: builds list of
candidates and prompts user
@ -34,176 +22,161 @@ if options.help then
return nil -- exit success
end
local utils_path = "/opt/core/install_utils.lua"
local utils
local rootfs = fs.get("/")
if not rootfs then
io.stderr:write("no root filesystem, aborting\n");
os.exit(1)
end
local function up_deprecate(old_key, new_key)
if options[new_key] == nil then
options[new_key] = options[old_key]
end
options[old_key] = nil
end
local label = args[1]
options.label = label
local function cleanPath(path)
if path then
local rpath = shell.resolve(path)
if fs.isDirectory(rpath) then
return fs.canonical(rpath) .. '/'
end
local source_filter = options.from
local source_filter_dev
if source_filter then
local from_path = shell.resolve(source_filter)
if fs.isDirectory(from_path) then
source_filter_dev = fs.get(from_path)
source_filter = source_filter_dev.address
options.from = from_path
end
end
local rootAddress = rootfs.address
-- if the rootfs is read only, it is probably the loot disk!
root_exception = rootAddress
if rootfs.isReadOnly() then
root_exception = nil
end
-- this may be OpenOS specific, default to "" in case no /dev mount point
local devfsAddress = (fs.get("/dev/") or {}).address or ""
-- tmp is only valid if specified as an option
local tmpAddress = computer.tmpAddress()
----- For now, I am allowing source==target -- cp can handle it if the user prepares conditions correctly
----- in other words, install doesn't need to filter this scenario:
--if #options.targets == 1 and #options.sources == 1 and options.targets[1] == options.sources[1] then
-- io.stderr:write("It is not the intent of install to use the same source and target filesystem.\n")
-- return 1
--end
------ load options
up_deprecate('noboot', 'nosetboot')
up_deprecate('nolabelset', 'nosetlabel')
up_deprecate('name', 'label')
options.source_root = cleanPath(options.from)
options.target_root = cleanPath(options.to)
options.target_dir = fs.canonical(options.root or options.toDir or "")
options.update = options.u or options.update
local function path_to_dev(path)
return path and fs.isDirectory(path) and not fs.isLink(path) and fs.get(path)
end
local source_dev = path_to_dev(options.source_root)
local target_dev = path_to_dev(options.target_root)
-- use a single for loop of all filesystems to build the list of candidates of sources and targets
local function validDevice(candidate, exceptions, specified, existing)
local address = candidate.dev.address
for _,e in ipairs(existing) do
if e.dev.address == address then
return
end
end
if specified then
if address:find(specified, 1, true) == 1 then
return true
end
else
for _,e in ipairs(exceptions) do
if e == address then
return
end
end
return true
local target_filter = options.to
local target_filter_dev
if target_filter then
local to_path = shell.resolve(target_filter)
if fs.isDirectory(target_filter) then
target_filter_dev = fs.get(to_path)
target_filter = target_filter_dev.address
options.to = to_path
end
end
local sources = {}
local targets = {}
-- tmpfs is not a candidate unless it is specified
local devices = {}
for dev, path in fs.mounts() do
local candidate = {dev=dev, path=path:gsub("/+$","")..'/'}
devices[dev] = path
end
if validDevice(candidate, {devfsAddress, tmpAddress, root_exception}, source_dev and source_dev.address or options.from, options.sources) then
-- root path is either the command line path given for this dev or its natural mount point
local root_path = source_dev == dev and options.source_root or path
if (options.from or fs.list(root_path)()) then -- ignore empty sources unless specified
local prop = fs.open(root_path .. '/.prop')
if prop then
local prop_data = prop:read(math.huge)
prop:close()
prop = prop_data
end
candidate.prop = prop and load('return ' .. prop)() or {}
if not candidate.prop.ignore then
if not options.source_label or options.source_label:lower() == (candidate.prop.label or (dev.getLabel() or "")):lower() then
table.insert(options.sources, candidate)
end
end
end
end
devices[fs.get("/dev/") or false] = nil
local tmpAddress = computer.tmpAddress()
-- in case candidate is valid for BOTH, we want a new table
candidate = {dev=candidate.dev, path=candidate.path} -- but not the prop
for dev, path in pairs(devices) do
local address = dev.address
local install_path = dev == target_filter_dev and options.to or path
local specified = target_filter and address:find(target_filter, 1, true) == 1
if validDevice(candidate, {devfsAddress, tmpAddress}, target_dev and target_dev.address or options.to, options.targets) then
if not dev.isReadOnly() then
table.insert(options.targets, candidate)
elseif options.to then
if dev.isReadOnly() then
if specified then
io.stderr:write("Cannot install to " .. options.to .. ", it is read only\n")
os.exit(1)
end
elseif specified or
not target_filter and
address ~= tmpAddress then
table.insert(targets, {dev=dev, path=install_path, specified=specified})
end
end
local source = options.sources[1]
local target = options.targets[1]
local utils_path = "/opt/core/install_utils.lua"
local target = targets[1]
if #targets ~= 1 then
utils = loadfile(utils_path, "bt", _G)
target = utils("select", "targets", options, targets)
end
if not target then return end
devices[target.dev] = nil
if #options.sources ~= 1 or #options.targets ~= 1 then
source, target = loadfile(utils_path, "bt", _G)('select', options)
for dev, path in pairs(devices) do
local address = dev.address
local install_path = dev == source_filter_dev and options.from or path
local specified = source_filter and address:find(source_filter, 1, true) == 1
if fs.list(install_path)()
and (specified or
not source_filter and
address ~= tmpAddress and
not (address == rootfs.address and not rootfs.isReadOnly())) then
local prop = {}
local prop_path = path .. "/.prop"
local prop_file = fs.open(prop_path)
if prop_file then
local prop_data = prop_file:read(math.huge)
prop_file:close()
local prop_load = load("return " .. prop_data)
prop = prop_load and prop_load()
if not prop then
io.stderr:write("Ignoring " .. path .. " due to malformed prop file\n")
prop = {ignore = true}
end
end
if not prop.ignore then
if not label or label:lower() == (prop.label or dev.getLabel() or ""):lower() then
table.insert(sources, {dev=dev, path=install_path, prop=prop, specified=specified})
end
end
end
end
local source = sources[1]
if #sources ~= 1 then
utils = utils or loadfile(utils_path, "bt", _G)
source = utils("select", "sources", options, sources)
end
if not source then return end
options.source_root = options.source_root or source.path
if not target then return end
options.target_root = options.target_root or target.path
-- now that source is selected, we can update options
options.label = options.label or source.prop.label
options.setlabel = source.prop.setlabel and not options.nosetlabel
options.setboot = source.prop.setboot and not options.nosetboot
options.reboot = source.prop.reboot and not options.noreboot
options.source_dir = fs.canonical(source.prop.fromDir or options.fromDir or "") .. '/.'
options =
{
from = source.path .. '/',
to = target.path .. '/',
fromDir = fs.canonical(options.fromDir or source.prop.fromDir or ""),
root = fs.canonical(options.root or options.toDir or source.prop.root or ""),
update = options.update or options.u,
label = source.prop.label or label,
setlabel = not (options.nosetlabel or options.nolabelset) and source.prop.setlabel,
setboot = not (options.nosetboot or options.noboot) and source.prop.setboot,
reboot = not options.noreboot and source.prop.reboot,
}
local cp_args =
{
"-vrx" .. (options.update and "ui" or ""),
options.source_root .. options.source_dir,
options.target_root:gsub("//","/") .. options.target_dir
"--skip=.prop",
fs.concat(options.from, options.fromDir) .. "/.",
fs.concat(options.to , options.root)
}
local source_display = (source.prop or {}).label or source.dev.getLabel() or source.path
local source_display = options.label or source.dev.getLabel() or source.path
local special_target = ""
if #options.targets > 1 or options.to then
if #targets > 1 or target_filter then
special_target = " to " .. cp_args[3]
end
io.write("Install " .. source_display .. special_target .. "? [Y/n] ")
if not ((io.read() or "n").."y"):match("^%s*[Yy]") then
write("Installation cancelled\n")
io.write("Installation cancelled\n")
os.exit()
end
local installer_path = options.source_root .. "/.install"
local installer_path = options.from .. "/.install"
if fs.exists(installer_path) then
os.exit(loadfile(utils_path, "bt", _G)('install', options))
local installer, reason = loadfile(installer_path, "bt", setmetatable({install=options}, {__index = _G}))
if not installer then
io.stderr:write("installer failed to load: " .. tostring(reason) .. '\n')
os.exit(1)
end
os.exit(installer())
end
return
{
setlabel = options.setlabel,
label = options.label,
setboot = options.setboot,
reboot = options.reboot,
target = target,
cp_args = cp_args,
}
options.cp_args = cp_args
options.target = target
return options

View File

@ -1,48 +1,51 @@
local cmd, options = ...
local cmd, arg, options, devices = ...
local function select_prompt(devs, prompt)
table.sort(devs, function(a, b) return a.path<b.path end)
local num_devs = #devs
if num_devs < 2 then
return devs[1]
end
local choice = devs[1]
if #devs > 1 then
io.write(prompt,'\n')
for i = 1, #devs do
for i = 1, num_devs do
local src = devs[i]
local label = src.dev.getLabel()
if label then
label = label .. " (" .. src.dev.address:sub(1, 8) .. "...)"
local dev = src.dev
local selection_label = src.prop.label or dev.getLabel()
if selection_label then
selection_label = string.format("%s (%s...)", selection_label, dev.address:sub(1, 8))
else
label = src.dev.address
selection_label = dev.address
end
io.write(i .. ") " .. label .. " at " .. src.path .. '\n')
io.write(string.format("%d) %s at %s [r%s]\n", i, selection_label, src.path, dev.isReadOnly() and 'o' or 'w'))
end
io.write("Please enter a number between 1 and " .. #devs .. '\n')
io.write("Please enter a number between 1 and " .. num_devs .. '\n')
io.write("Enter 'q' to cancel the installation: ")
choice = nil
while not choice do
result = io.read() or "q"
for _=1,5 do
local result = io.read() or "q"
if result == "q" then
os.exit()
end
local number = tonumber(result)
if number and number > 0 and number <= #devs then
choice = devs[number]
if number and number > 0 and number <= num_devs then
return devs[number]
else
io.write("Invalid input, please try again: ")
os.sleep(0)
end
end
end
return choice
print("\ntoo many bad inputs, aborting")
os.exit(1)
end
if cmd == 'select' then
if #options.sources == 0 then
if options.source_label then
io.stderr:write("Nothing to install labeled: " .. options.source_label .. '\n')
if cmd == "select" then
if arg == "sources" then
if #devices == 0 then
if options.label then
io.stderr:write("Nothing to install labeled: " .. options.label .. '\n')
elseif options.from then
io.stderr:write("Nothing to install from: " .. options.from .. '\n')
else
@ -50,8 +53,9 @@ if cmd == 'select' then
end
os.exit(1)
end
if #options.targets == 0 then
return select_prompt(devices, "What do you want to install?")
elseif arg == "targets" then
if #devices == 0 then
if options.to then
io.stderr:write("No such target to install to: " .. options.to .. '\n')
else
@ -60,32 +64,6 @@ if cmd == 'select' then
os.exit(1)
end
local source = select_prompt(options.sources, "What do you want to install?")
if #options.sources > 1 and #options.targets > 1 then
io.write('\n')
end
local target = select_prompt(options.targets, "Where do you want to install to?")
return source, target
elseif cmd == 'install' then
local installer_path = options.source_root .. "/.install"
local installer, reason = loadfile(installer_path, "bt", setmetatable({install=
{
from=options.source_root,
to=options.target_root,
fromDir=options.source_dir,
root=options.target_dir,
update=options.update,
label=options.label,
setlabel=options.setlabel,
setboot=options.setboot,
reboot=options.reboot,
}}, {__index=_G}))
if not installer then
io.stderr:write("installer failed to load: " .. tostring(reason) .. '\n')
os.exit(1)
else
return installer()
return select_prompt(devices, "Where do you want to install to?")
end
end

View File

@ -65,7 +65,7 @@ The following can override settings defined in .prop in the source filesystem.
.install ENVIRONMENT
A loot disc can optionally provide a custom installation script at the root of the source filesytem selected for installation. The script must be named ".install"
When provided, the default install action is replaced by executation of this script. The default action is to copy all source files to the destination
An _ENV.install table is set in the environment of '.install' when loaded
A table of configuration options, named `install`, is provided in _ENV
These are the keys and their descriptions of that table
_ENV.install.from:
@ -76,7 +76,7 @@ The following can override settings defined in .prop in the source filesystem.
This is the path of the selected target filesystem to install to.
example: "/"
_ENV.install.fromdir
_ENV.install.fromDir
This is the relative path to use in the source filesystem as passed by command line to install. If unspecified to install it defaults to "."
example: Perhaps the user executed `install --fromDir="bin"` with the intention that only files under /mnt/ABC/bin would be copied to their rootfs