mirror of
https://github.com/koreader/koreader.git
synced 2025-12-13 20:36:53 +01:00
Allow running shell scripts from the FileManager/Favorites (#5804)
* Allow running Shell/Python scripts from the FM * Show an InfoMessage before/after running the script Since we're blocking the UI ;). * Allow running scripts from the favorites menu, too.
This commit is contained in:
@@ -40,7 +40,8 @@ local UIManager = require("ui/uimanager")
|
||||
local filemanagerutil = require("apps/filemanager/filemanagerutil")
|
||||
local lfs = require("libs/libkoreader-lfs")
|
||||
local logger = require("logger")
|
||||
local util = require("ffi/util")
|
||||
local BaseUtil = require("ffi/util")
|
||||
local util = require("util")
|
||||
local _ = require("gettext")
|
||||
local C_ = _.pgettext
|
||||
local Screen = Device.screen
|
||||
@@ -226,10 +227,10 @@ function FileManager:init()
|
||||
},
|
||||
{
|
||||
text = _("Purge .sdr"),
|
||||
enabled = DocSettings:hasSidecarFile(util.realpath(file)),
|
||||
enabled = DocSettings:hasSidecarFile(BaseUtil.realpath(file)),
|
||||
callback = function()
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = util.template(_("Purge .sdr to reset settings for this document?\n\n%1"), BD.filename(self.file_dialog.title)),
|
||||
text = T(_("Purge .sdr to reset settings for this document?\n\n%1"), BD.filename(self.file_dialog.title)),
|
||||
ok_text = _("Purge"),
|
||||
ok_callback = function()
|
||||
filemanagerutil.purgeSettings(file)
|
||||
@@ -271,7 +272,7 @@ function FileManager:init()
|
||||
UIManager:close(self.file_dialog)
|
||||
fileManager.rename_dialog = InputDialog:new{
|
||||
title = _("Rename file"),
|
||||
input = util.basename(file),
|
||||
input = BaseUtil.basename(file),
|
||||
buttons = {{
|
||||
{
|
||||
text = _("Cancel"),
|
||||
@@ -297,8 +298,44 @@ function FileManager:init()
|
||||
}
|
||||
},
|
||||
-- a little hack to get visual functionality grouping
|
||||
{},
|
||||
{
|
||||
},
|
||||
}
|
||||
|
||||
if not Device:isAndroid() and lfs.attributes(file, "mode") == "file" and util.isAllowedScript(file) then
|
||||
-- NOTE: We populate the empty separator, in order not to mess with the button reordering code in CoverMenu
|
||||
table.insert(buttons[3],
|
||||
{
|
||||
-- @translators This is the script's programming language (e.g., shell or python)
|
||||
text = T(_("Execute %1 script"), util.getScriptType(file)),
|
||||
enabled = true,
|
||||
callback = function()
|
||||
UIManager:close(self.file_dialog)
|
||||
local script_is_running_msg = InfoMessage:new{
|
||||
-- @translators %1 is the script's programming language (e.g., shell or python), %2 is the filename
|
||||
text = T(_("Running %1 script %2 ..."), util.getScriptType(file), BD.filename(BaseUtil.basename(file))),
|
||||
}
|
||||
UIManager:show(script_is_running_msg)
|
||||
UIManager:scheduleIn(0.5, function()
|
||||
local rv = os.execute(BaseUtil.realpath(file))
|
||||
UIManager:close(script_is_running_msg)
|
||||
if rv == 0 then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("The script exited successfully."),
|
||||
})
|
||||
else
|
||||
--- @note: Lua 5.1 returns the raw return value from the os's system call. Counteract this madness.
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = T(_("The script returned a non-zero status code: %1!"), bit.rshift(rv, 8)),
|
||||
icon_file = "resources/info-warn.png",
|
||||
})
|
||||
end
|
||||
end)
|
||||
end,
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
if lfs.attributes(file, "mode") == "file" then
|
||||
table.insert(buttons, {
|
||||
{
|
||||
@@ -352,7 +389,7 @@ function FileManager:init()
|
||||
end
|
||||
end
|
||||
if lfs.attributes(file, "mode") == "directory" then
|
||||
local realpath = util.realpath(file)
|
||||
local realpath = BaseUtil.realpath(file)
|
||||
table.insert(buttons, {
|
||||
{
|
||||
text = _("Set as HOME directory"),
|
||||
@@ -679,7 +716,7 @@ end
|
||||
function FileManager:setHome(path)
|
||||
path = path or self.file_chooser.path
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = util.template(_("Set '%1' as HOME directory?"), BD.dirpath(path)),
|
||||
text = T(_("Set '%1' as HOME directory?"), BD.dirpath(path)),
|
||||
ok_text = _("Set as HOME"),
|
||||
ok_callback = function()
|
||||
G_reader_settings:saveSetting("home_dir", path)
|
||||
@@ -692,7 +729,7 @@ function FileManager:openRandomFile(dir)
|
||||
local random_file = DocumentRegistry:getRandomFile(dir, false)
|
||||
if random_file then
|
||||
UIManager:show(MultiConfirmBox:new {
|
||||
text = T(_("Do you want to open %1?"), BD.filename(util.basename(random_file))),
|
||||
text = T(_("Do you want to open %1?"), BD.filename(BaseUtil.basename(random_file))),
|
||||
choice1_text = _("Open"),
|
||||
choice1_callback = function()
|
||||
FileManager.instance:onClose()
|
||||
@@ -725,17 +762,17 @@ end
|
||||
|
||||
function FileManager:pasteHere(file)
|
||||
if self.clipboard then
|
||||
file = util.realpath(file)
|
||||
local orig = util.realpath(self.clipboard)
|
||||
file = BaseUtil.realpath(file)
|
||||
local orig = BaseUtil.realpath(self.clipboard)
|
||||
local dest = lfs.attributes(file, "mode") == "directory" and
|
||||
file or file:match("(.*/)")
|
||||
|
||||
local function infoCopyFile()
|
||||
-- if we copy a file, also copy its sidecar directory
|
||||
if DocSettings:hasSidecarFile(orig) then
|
||||
util.execute(self.cp_bin, "-r", DocSettings:getSidecarDir(orig), dest)
|
||||
BaseUtil.execute(self.cp_bin, "-r", DocSettings:getSidecarDir(orig), dest)
|
||||
end
|
||||
if util.execute(self.cp_bin, "-r", orig, dest) == 0 then
|
||||
if BaseUtil.execute(self.cp_bin, "-r", orig, dest) == 0 then
|
||||
UIManager:show(InfoMessage:new {
|
||||
text = T(_("Copied to: %1"), BD.dirpath(dest)),
|
||||
timeout = 2,
|
||||
@@ -755,7 +792,7 @@ function FileManager:pasteHere(file)
|
||||
end
|
||||
if self:moveFile(orig, dest) then
|
||||
-- Update history and collections.
|
||||
local dest_file = string.format("%s/%s", dest, util.basename(orig))
|
||||
local dest_file = string.format("%s/%s", dest, BaseUtil.basename(orig))
|
||||
require("readhistory"):updateItemByPath(orig, dest_file)
|
||||
ReadCollection:updateItemByPath(orig, dest_file)
|
||||
-- Update last open file.
|
||||
@@ -780,7 +817,7 @@ function FileManager:pasteHere(file)
|
||||
else
|
||||
info_file = infoCopyFile
|
||||
end
|
||||
local basename = util.basename(self.clipboard)
|
||||
local basename = BaseUtil.basename(self.clipboard)
|
||||
local mode = lfs.attributes(string.format("%s/%s", dest, basename), "mode")
|
||||
if mode == "file" or mode == "directory" then
|
||||
local text
|
||||
@@ -809,7 +846,7 @@ end
|
||||
|
||||
function FileManager:createFolder(curr_folder, new_folder)
|
||||
local folder = string.format("%s/%s", curr_folder, new_folder)
|
||||
local code = util.execute(self.mkdir_bin, folder)
|
||||
local code = BaseUtil.execute(self.mkdir_bin, folder)
|
||||
local text
|
||||
if code == 0 then
|
||||
self:onRefresh()
|
||||
@@ -825,10 +862,10 @@ end
|
||||
|
||||
function FileManager:deleteFile(file)
|
||||
local ok, err, is_dir
|
||||
local file_abs_path = util.realpath(file)
|
||||
local file_abs_path = BaseUtil.realpath(file)
|
||||
if file_abs_path == nil then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = util.template(_("File %1 not found"), BD.filepath(file)),
|
||||
text = T(_("File %1 not found"), BD.filepath(file)),
|
||||
})
|
||||
return
|
||||
end
|
||||
@@ -837,7 +874,7 @@ function FileManager:deleteFile(file)
|
||||
if lfs.attributes(file_abs_path, "mode") == "file" then
|
||||
ok, err = os.remove(file_abs_path)
|
||||
else
|
||||
ok, err = util.purgeDir(file_abs_path)
|
||||
ok, err = BaseUtil.purgeDir(file_abs_path)
|
||||
is_dir = true
|
||||
end
|
||||
if ok and not err then
|
||||
@@ -852,19 +889,19 @@ function FileManager:deleteFile(file)
|
||||
end
|
||||
ReadCollection:removeItemByPath(file, is_dir)
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = util.template(_("Deleted %1"), BD.filepath(file)),
|
||||
text = T(_("Deleted %1"), BD.filepath(file)),
|
||||
timeout = 2,
|
||||
})
|
||||
else
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = util.template(_("An error occurred while trying to delete %1"), BD.filepath(file)),
|
||||
text = T(_("An error occurred while trying to delete %1"), BD.filepath(file)),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function FileManager:renameFile(file)
|
||||
if util.basename(file) ~= self.rename_dialog:getInputText() then
|
||||
local dest = util.joinPath(util.dirname(file), self.rename_dialog:getInputText())
|
||||
if BaseUtil.basename(file) ~= self.rename_dialog:getInputText() then
|
||||
local dest = BaseUtil.joinPath(BaseUtil.dirname(file), self.rename_dialog:getInputText())
|
||||
if self:moveFile(file, dest) then
|
||||
ReadCollection:updateItemByPath(file, dest)
|
||||
if lfs.attributes(dest, "mode") == "file" then
|
||||
@@ -880,12 +917,12 @@ function FileManager:renameFile(file)
|
||||
end
|
||||
if move_history then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = util.template(_("Renamed from %1 to %2"), BD.filepath(file), BD.filepath(dest)),
|
||||
text = T(_("Renamed from %1 to %2"), BD.filepath(file), BD.filepath(dest)),
|
||||
timeout = 2,
|
||||
})
|
||||
else
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = util.template(
|
||||
text = T(
|
||||
_("Failed to move history data of %1 to %2.\nThe reading history may be lost."),
|
||||
BD.filepath(file), BD.filepath(dest)),
|
||||
})
|
||||
@@ -893,7 +930,7 @@ function FileManager:renameFile(file)
|
||||
end
|
||||
else
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = util.template(
|
||||
text = T(
|
||||
_("Failed to rename from %1 to %2"), BD.filepath(file), BD.filepath(dest)),
|
||||
})
|
||||
end
|
||||
@@ -932,7 +969,7 @@ function FileManager:getSortingMenuTable()
|
||||
end
|
||||
return {
|
||||
text_func = function()
|
||||
return util.template(
|
||||
return T(
|
||||
_("Sort by: %1"),
|
||||
collates[fm.file_chooser.collate][1]
|
||||
)
|
||||
@@ -982,7 +1019,7 @@ function FileManager:getStartWithMenuTable()
|
||||
end
|
||||
return {
|
||||
text_func = function()
|
||||
return util.template(
|
||||
return T(
|
||||
_("Start with: %1"),
|
||||
start_withs[start_with_setting][1]
|
||||
)
|
||||
@@ -1018,7 +1055,7 @@ A shortcut to execute mv command (self.mv_bin) with from and to as parameters.
|
||||
Returns a boolean value to indicate the result of mv command.
|
||||
--]]
|
||||
function FileManager:moveFile(from, to)
|
||||
return util.execute(self.mv_bin, from, to) == 0
|
||||
return BaseUtil.execute(self.mv_bin, from, to) == 0
|
||||
end
|
||||
|
||||
function FileManager:onHome()
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
local BD = require("ui/bidi")
|
||||
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
|
||||
local Device = require("device")
|
||||
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local Menu = require("ui/widget/menu")
|
||||
local ReadCollection = require("readcollection")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local Screen = require("device").screen
|
||||
local BaseUtil = require("ffi/util")
|
||||
local util = require("util")
|
||||
local _ = require("gettext")
|
||||
local T = require("ffi/util").template
|
||||
|
||||
local FileManagerCollection = InputContainer:extend{
|
||||
coll_menu_title = _("Favorites"),
|
||||
@@ -87,6 +93,39 @@ function FileManagerCollection:onMenuHold(item)
|
||||
},
|
||||
},
|
||||
}
|
||||
-- NOTE: Duplicated from frontend/apps/filemanager/filemanager.lua
|
||||
if not Device:isAndroid() and util.isAllowedScript(item.file) then
|
||||
table.insert(buttons, {
|
||||
{
|
||||
-- @translators This is the script's programming language (e.g., shell or python)
|
||||
text = T(_("Execute %1 script"), util.getScriptType(item.file)),
|
||||
enabled = true,
|
||||
callback = function()
|
||||
UIManager:close(self.collfile_dialog)
|
||||
local script_is_running_msg = InfoMessage:new{
|
||||
-- @translators %1 is the script's programming language (e.g., shell or python), %2 is the filename
|
||||
text = T(_("Running %1 script %2 ..."), util.getScriptType(item.file), BD.filename(BaseUtil.basename(item.file))),
|
||||
}
|
||||
UIManager:show(script_is_running_msg)
|
||||
UIManager:scheduleIn(0.5, function()
|
||||
local rv = os.execute(BaseUtil.realpath(item.file))
|
||||
UIManager:close(script_is_running_msg)
|
||||
if rv == 0 then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("The script exited successfully."),
|
||||
})
|
||||
else
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = T(_("The script returned a non-zero status code: %1!"), bit.rshift(rv, 8)),
|
||||
icon_file = "resources/info-warn.png",
|
||||
})
|
||||
end
|
||||
end)
|
||||
end,
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
self.collfile_dialog = ButtonDialogTitle:new{
|
||||
title = item.text:match("([^/]+)$"),
|
||||
title_align = "center",
|
||||
|
||||
@@ -142,7 +142,7 @@ function Kindle:usbPlugIn()
|
||||
-- NOTE: We cannot support running in USBMS mode (we cannot, we live on the partition being exported!).
|
||||
-- But since that's the default state of the Kindle system, we have to try to make nice...
|
||||
-- To that end, we're currently SIGSTOPping volumd to inhibit the system's USBMS mode handling.
|
||||
-- It's not perfect (f.g., if the system is setup for USBMS and not USBNet,
|
||||
-- It's not perfect (e.g., if the system is setup for USBMS and not USBNet,
|
||||
-- the frontlight will be turned off when plugged in), but it at least prevents users from completely
|
||||
-- shooting themselves in the foot (c.f., https://github.com/koreader/koreader/issues/3220)!
|
||||
-- On the upside, we don't have to bother waking up the WM to show us the USBMS screen :D.
|
||||
|
||||
@@ -67,7 +67,7 @@ function SysfsLight:setNaturalBrightness(brightness, warmth)
|
||||
|
||||
-- Newer devices use a mixer instead of writting values per color.
|
||||
if self.frontlight_mixer then
|
||||
-- Honor the device's scale, which may not be [0...100] (f.g., it's [0...10] on the Forma) ;).
|
||||
-- Honor the device's scale, which may not be [0...100] (e.g., it's [0...10] on the Forma) ;).
|
||||
warmth = math.floor(warmth / self.nl_max)
|
||||
if set_brightness then
|
||||
-- Prefer the ioctl, as it's much lower latency.
|
||||
|
||||
@@ -882,6 +882,9 @@ function CreDocument:register(registry)
|
||||
registry:addProvider("rtf", "application/rtf", self, 90)
|
||||
registry:addProvider("xhtml", "application/xhtml+xml", self, 90)
|
||||
registry:addProvider("zip", "application/zip", self, 10)
|
||||
-- Scripts that we allow running in the FM (c.f., util.isAllowedScript)
|
||||
registry:addProvider("sh", "application/x-shellscript", self, 90)
|
||||
registry:addProvider("py", "text/x-python", self, 90)
|
||||
end
|
||||
|
||||
-- Optimise usage of some of the above methods by caching their results,
|
||||
|
||||
@@ -491,15 +491,15 @@ the second parameter (refreshtype) can either specify a refreshtype
|
||||
or a function that returns refreshtype AND refreshregion and is called
|
||||
after painting the widget.
|
||||
Here's a quick rundown of what each refreshtype should be used for:
|
||||
full: high-fidelity flashing refresh (f.g., large images).
|
||||
full: high-fidelity flashing refresh (e.g., large images).
|
||||
Highest quality, but highest latency.
|
||||
Don't abuse if you only want a flash (in this case, prefer flashpartial or flashui).
|
||||
partial: medium fidelity refresh (f.g., text on a white background).
|
||||
partial: medium fidelity refresh (e.g., text on a white background).
|
||||
Can be promoted to flashing after FULL_REFRESH_COUNT refreshes.
|
||||
Don't abuse to avoid spurious flashes.
|
||||
ui: medium fidelity refresh (f.g., mixed content).
|
||||
ui: medium fidelity refresh (e.g., mixed content).
|
||||
Should apply to most UI elements.
|
||||
fast: low fidelity refresh (f.g., monochrome content).
|
||||
fast: low fidelity refresh (e.g., monochrome content).
|
||||
Should apply to most highlighting effects achieved through inversion.
|
||||
Note that if your highlighted element contains text,
|
||||
you might want to keep the unhighlight refresh as "ui" instead, for crisper text.
|
||||
@@ -877,9 +877,9 @@ function UIManager:_refresh(mode, region, dither)
|
||||
|
||||
-- NOTE: While, ideally, we shouldn't merge refreshes w/ different waveform modes,
|
||||
-- this allows us to optimize away a number of quirks of our rendering stack
|
||||
-- (f.g., multiple setDirty calls queued when showing/closing a widget because of update mechanisms),
|
||||
-- (e.g., multiple setDirty calls queued when showing/closing a widget because of update mechanisms),
|
||||
-- as well as a few actually effective merges
|
||||
-- (f.g., the disappearance of a selection HL with the following menu update).
|
||||
-- (e.g., the disappearance of a selection HL with the following menu update).
|
||||
for i = 1, #self._refresh_stack do
|
||||
-- check for collision with refreshes that are already enqueued
|
||||
if region:intersectWith(self._refresh_stack[i].region) then
|
||||
|
||||
@@ -363,7 +363,7 @@ function ImageViewer:update()
|
||||
file = self.file,
|
||||
image = self.image,
|
||||
image_disposable = false, -- we may re-use self.image
|
||||
alpha = true, -- we might be showing images with an alpha channel (f.g., from Wikipedia)
|
||||
alpha = true, -- we might be showing images with an alpha channel (e.g., from Wikipedia)
|
||||
width = max_image_w,
|
||||
height = max_image_h,
|
||||
rotation_angle = rotation_angle,
|
||||
|
||||
@@ -661,6 +661,33 @@ function util.getFileNameSuffix(file)
|
||||
return suffix
|
||||
end
|
||||
|
||||
--- Returns true if the file is a script we allow running
|
||||
--- Basically a helper method to check a specific list of file extensions.
|
||||
---- @string filename
|
||||
---- @treturn boolean
|
||||
function util.isAllowedScript(file)
|
||||
local file_ext = string.lower(util.getFileNameSuffix(file))
|
||||
if file_ext == "sh"
|
||||
or file_ext == "py" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--- Companion helper function that returns the script's language,
|
||||
--- based on the filme extension.
|
||||
---- @string filename
|
||||
---- @treturn string (lowercase) (or nil if !isAllowedScript)
|
||||
function util.getScriptType(file)
|
||||
local file_ext = string.lower(util.getFileNameSuffix(file))
|
||||
if file_ext == "sh" then
|
||||
return "shell"
|
||||
elseif file_ext == "py" then
|
||||
return "python"
|
||||
end
|
||||
end
|
||||
|
||||
--- Gets human friendly size as string
|
||||
---- @int size (bytes)
|
||||
---- @bool right_align (by padding with spaces on the left)
|
||||
|
||||
@@ -41,7 +41,7 @@ describe("FileManager module", function()
|
||||
root_path = "../../test",
|
||||
}
|
||||
|
||||
local tmp_fn = "../../test/2col.test.tmp.sh"
|
||||
local tmp_fn = "../../test/2col.test.tmp.foo"
|
||||
util.copyFile("../../test/2col.pdf", tmp_fn)
|
||||
|
||||
local tmp_sidecar = docsettings:getSidecarDir(util.realpath(tmp_fn))
|
||||
|
||||
Reference in New Issue
Block a user