diff --git a/frontend/apps/cloudstorage/cloudstorage.lua b/frontend/apps/cloudstorage/cloudstorage.lua new file mode 100755 index 000000000..0f0142cb7 --- /dev/null +++ b/frontend/apps/cloudstorage/cloudstorage.lua @@ -0,0 +1,370 @@ +local UIManager = require("ui/uimanager") +local Screen = require("device").screen +local _ = require("gettext") +local Menu = require("ui/widget/menu") +local InfoMessage = require("ui/widget/infomessage") +local ButtonDialog = require("ui/widget/buttondialog") +local DropBox = require("frontend/apps/cloudstorage/dropbox") +local LuaSettings = require("luasettings") +local DataStorage = require("datastorage") +local Ftp = require("frontend/apps/cloudstorage/ftp") +local ConfirmBox = require("ui/widget/confirmbox") +local lfs = require("libs/libkoreader-lfs") +local ButtonDialogTitle = require("ui/widget/buttondialogtitle") + +local CloudStorage = Menu:extend{ + cloud_servers = { + { + text = "Add new cloud storage", + title = "Choose type of cloud", + url = "add", + editable = false, + }, + }, + width = Screen:getWidth(), + height = Screen:getHeight(), + no_title = false, + show_parent = nil, + is_popout = false, + is_borderless = true, +} + +function CloudStorage:init() + self.cs_settings = self:readSettings() + self.menu_select = nil + self.title = "Cloud Storage" + self.show_parent = self + self.item_table = self:genItemTableFromRoot() + Menu.init(self) +end + +function CloudStorage:genItemTableFromRoot() + local item_table = {} + table.insert(item_table, { + text = _("Add new cloud storage"), + callback = function() + self:selectCloudType() + end, + }) + local added_servers = self.cs_settings:readSetting("cs_servers") or {} + for _, server in ipairs(added_servers) do + table.insert(item_table, { + text = server.name, + address = server.address, + username = server.username, + password = server.password, + type = server.type, + editable = true, + url = server.url, + callback = function() + self.type = server.type + self.password = server.password + self.address = server.address + self.username = server.username + self:openCloudServer(server.url) + end, + }) + end + return item_table +end + +function CloudStorage:selectCloudType() + local buttons = { + { + { + text = _("Dropbox"), + callback = function() + UIManager:close(self.cloud_dialog) + self:configCloud("dropbox") + end, + }, + }, + { + { + text = _("FTP"), + callback = function() + UIManager:close(self.cloud_dialog) + self:configCloud("ftp") + end, + }, + }, + } + self.cloud_dialog = ButtonDialogTitle:new{ + title = _("Choose cloud storage type"), + title_align = "center", + buttons = buttons, + } + + UIManager:show(self.cloud_dialog) + return true +end + +function CloudStorage:openCloudServer(url) + local tbl + if self.type == "dropbox" then + tbl = DropBox:run(url, self.password) + elseif self.type == "ftp" then + tbl = Ftp:run(self.address, self.username, self.password, url) + end + if tbl and #tbl > 0 then + self:swithItemTable(url, tbl) + return true + elseif not tbl then + UIManager:show(InfoMessage:new{ + text = _("Cannot fetch list folder!\nCheck configuration or network connection."), + timeout = 3, + }) + table.remove(self.paths) + return false + else + UIManager:show(InfoMessage:new{text = _("Empty folder") }) + return false + end +end + +function CloudStorage:onMenuSelect(item) + if item.callback then + if item.url ~= nil then + table.insert(self.paths, { + url = item.url, + }) + end + item.callback() + elseif item.type == "file" then + self:downloadFile(item) + else + table.insert(self.paths, { + url = item.url, + }) + if not self:openCloudServer(item.url) then + table.remove(self.paths) + end + end + return true +end + +function CloudStorage:downloadFile(item) + local lastdir = G_reader_settings:readSetting("lastdir") + local cs_settings = self:readSettings() + local download_dir = cs_settings:readSetting("download_dir") or lastdir + local path = download_dir .. '/' .. item.text + if lfs.attributes(path) then + UIManager:show(ConfirmBox:new{ + text = _("File exist! Would you like to override it?"), + ok_callback = function() + self:cloudFile(item, path) + end + }) + else + self:cloudFile(item, path) + end +end + +function CloudStorage:cloudFile(item, path) + local path_dir = path + local buttons = { + { + { + text = _("Download file"), + callback = function() + if self.type == "dropbox" then + local callback_close = function() + self:onClose() + end + UIManager:scheduleIn(1, function() + DropBox:downloadFile(item, self.password, path_dir, callback_close) + end) + UIManager:close(self.download_dialog) + UIManager:show(InfoMessage:new{ + text = _("Downloading may take several minutes..."), + timeout = 1, + }) + elseif self.type == "ftp" then + local callback_close = function() + self:onClose() + end + UIManager:scheduleIn(1, function() + Ftp:downloadFile(item, self.address, self.username, self.password, path_dir, callback_close) + end) + UIManager:close(self.download_dialog) + UIManager:show(InfoMessage:new{ + text = _("Downloading may take several minutes..."), + timeout = 1, + }) + end + end, + }, + }, + { + { + text = _("Set download directory"), + callback = function() + require("ui/downloadmgr"):new{ + title = _("Choose download directory"), + onConfirm = function(path_download) + self.cs_settings:saveSetting("download_dir", path_download) + self.cs_settings:flush() + path_dir = path_download .. '/' .. item.text + end, + }:chooseDir() + end, + }, + }, + } + self.download_dialog = ButtonDialog:new{ + buttons = buttons + } + UIManager:show(self.download_dialog) +end + +function CloudStorage:onMenuHold(item) + if item.editable then + local cs_server_dialog + cs_server_dialog = ButtonDialog:new{ + buttons = { + { + { + text = _("Info"), + enabled = true, + callback = function() + UIManager:close(cs_server_dialog) + self:infoServer(item) + end + }, + { + text = _("Edit"), + enabled = true, + callback = function() + UIManager:close(cs_server_dialog) + self:editCloudServer(item) + + end + }, + { + text = _("Delete"), + enabled = true, + callback = function() + UIManager:close(cs_server_dialog) + self:deleteCloudServer(item) + end + }, + }, + } + } + UIManager:show(cs_server_dialog) + return true + end +end + +function CloudStorage:configCloud(type) + local callbackAdd = function(fields) + local cs_settings = self:readSettings() + local cs_servers = cs_settings:readSetting("cs_servers") or {} + if type == "dropbox" then + table.insert(cs_servers,{ + name = fields[1], + password = fields[2], + type = "dropbox", + url = "/" + }) + elseif type == "ftp" then + table.insert(cs_servers,{ + name = fields[1], + address = fields[2], + username = fields[3], + password = fields[4], + type = "ftp", + url = "/" + }) + end + cs_settings:saveSetting("cs_servers", cs_servers) + cs_settings:flush() + self:init() + end + if type == "dropbox" then + DropBox:config(nil, callbackAdd) + end + if type == "ftp" then + Ftp:config(nil, callbackAdd) + end +end + +function CloudStorage:editCloudServer(item) + local callbackEdit = function(updated_config, fields) + local cs_settings = self:readSettings() + local cs_servers = cs_settings:readSetting("cs_servers") or {} + if item.type == "dropbox" then + for i, server in ipairs(cs_servers) do + if server.name == updated_config.text and server.password == updated_config.password then + server.name = fields[1] + server.password = fields[2] + cs_servers[i] = server + break + end + end + elseif item.type == "ftp" then + for i, server in ipairs(cs_servers) do + if server.name == updated_config.text and server.address == updated_config.address then + server.name = fields[1] + server.address = fields[2] + server.username = fields[3] + server.password = fields[4] + cs_servers[i] = server + break + end + end + end + cs_settings:saveSetting("cs_servers", cs_servers) + cs_settings:flush() + self:init() + end + if item.type == "dropbox" then + DropBox:config(item, callbackEdit) + elseif item.type == "ftp" then + Ftp:config(item, callbackEdit) + end +end + +function CloudStorage:deleteCloudServer(item) + local cs_settings = self:readSettings() + local cs_servers = cs_settings:readSetting("cs_servers") or {} + for i, server in ipairs(cs_servers) do + if server.name == item.text and server.password == item.password and server.type == item.type then + table.remove(cs_servers, i) + break + end + end + cs_settings:saveSetting("cs_servers", cs_servers) + cs_settings:flush() + self:init() +end + +function CloudStorage:infoServer(item) + if item.type == "dropbox" then + DropBox:info(item.password) + elseif item.type == "ftp" then + Ftp:info(item) + end +end + +function CloudStorage:readSettings() + self.cs_settings = LuaSettings:open(DataStorage:getSettingsDir().."/cloudstorage.lua") + return self.cs_settings +end + +function CloudStorage:onReturn() + if #self.paths > 0 then + table.remove(self.paths) + local path = self.paths[#self.paths] + if path then + -- return to last path + self:openCloudServer(path.url) + else + -- return to root path + self:init() + end + end + return true +end + +return CloudStorage diff --git a/frontend/apps/cloudstorage/dropbox.lua b/frontend/apps/cloudstorage/dropbox.lua new file mode 100755 index 000000000..8a28c7f08 --- /dev/null +++ b/frontend/apps/cloudstorage/dropbox.lua @@ -0,0 +1,129 @@ +local DropBoxApi = require("frontend/apps/cloudstorage/dropboxapi") +local ConfirmBox = require("ui/widget/confirmbox") +local InfoMessage = require("ui/widget/infomessage") +local MultiInputDialog = require("ui/widget/multiinputdialog") +local UIManager = require("ui/uimanager") +local _ = require("gettext") +local T = require("ffi/util").template +local ReaderUI = require("apps/reader/readerui") +local Screen = require("device").screen + +local DropBox = { +} + +function DropBox:run(url, password) + return DropBoxApi:listFolder(url, password) +end + +function DropBox:downloadFile(item, password, path, close) + local code_response = DropBoxApi:downloadFile(item.url, password, path) + if code_response == 200 then + UIManager:show(ConfirmBox:new{ + text = T(_("File saved to:\n %1\nWould you like to read the downloaded book now?"), + path), + ok_callback = function() + close() + ReaderUI:showReader(path) + end + }) + else + UIManager:show(InfoMessage:new{ + text = _("Could not save file to:\n") .. path, + timeout = 3, + }) + end +end + +function DropBox:config(item, callback) + local text_info = "How to generate Access Token:\n".. + "1. Open the following URL in your Browser, and log in using your account: https://www.dropbox.com/developers/apps.\n".. + "2. Click on >>Create App<<, then select >>Dropbox API app<<.\n".. + "3. Now go on with the configuration, choosing the app permissions and access restrictions to your DropBox folder.\n".. + "4. Enter the >>App Name<< that you prefer (e.g. KOReader).\n".. + "5. Now, click on the >>Create App<< button.\n" .. + "6. When your new App is successfully created, please click on the Generate button.\n".. + "7. Under the 'Generated access token' section, then enter code in Dropbox token field." + local hint_top = _("Your Dropbox name") + local text_top = "" + local hint_bottom = _("Dropbox token\n\n\n\n ") + local text_bottom = "" + local title + local text_button_right = _("Add") + if item then + title = _("Edit Dropbox account") + text_button_right = _("Apply") + text_top = item.text + text_bottom = item.password + else + title = _("Add Dropbox account") + end + self.settings_dialog = MultiInputDialog:new { + title = title, + fields = { + { + text = text_top, + hint = hint_top , + }, + { + text = text_bottom, + hint = hint_bottom, + scroll = false, + }, + }, + buttons = { + { + { + text = _("Cancel"), + callback = function() + self.settings_dialog:onClose() + UIManager:close(self.settings_dialog) + end + }, + { + text = _("Info"), + callback = function() + UIManager:show(InfoMessage:new{text = text_info }) + end + }, + { + text = text_button_right, + callback = function() + local fields = MultiInputDialog:getFields() + if fields[1] ~= "" and fields[2] ~= "" then + if item then + --edit + callback(item, fields) + else + -- add new + callback(fields) + end + self.settings_dialog:onClose() + UIManager:close(self.settings_dialog) + else + UIManager:show(InfoMessage:new{text = "Please fill in all fields." }) + end + end + }, + }, + }, + width = Screen:getWidth() * 0.95, + height = Screen:getHeight() * 0.2, + input_type = "text", + } + self.settings_dialog:onShowKeyboard() + UIManager:show(self.settings_dialog) +end + +function DropBox:info(token) + local info = DropBoxApi:fetchInfo(token) + local info_text + if info and info.name then + info_text = T(_"Type: %1\nName: %2\nEmail: %3\nCounty: %4", + "Dropbox",info.name.display_name, info.email, info.country) + else + info_text = _("No information available") + end + UIManager:show(InfoMessage:new{text = info_text}) +end + +return DropBox diff --git a/frontend/apps/cloudstorage/dropboxapi.lua b/frontend/apps/cloudstorage/dropboxapi.lua new file mode 100755 index 000000000..1f70642d6 --- /dev/null +++ b/frontend/apps/cloudstorage/dropboxapi.lua @@ -0,0 +1,135 @@ +local url = require('socket.url') +local socket = require('socket') +local http = require('socket.http') +local https = require('ssl.https') +local ltn12 = require('ltn12') +local _ = require("gettext") +local JSON = require("json") +local DocumentRegistry = require("document/documentregistry") + +local DropBoxApi = { +} + +local API_URL_INFO = "https://api.dropboxapi.com/2/users/get_current_account" +local API_LIST_FOLDER = "https://api.dropboxapi.com/2/files/list_folder" +local API_DOWNLOAD_FILE = "https://content.dropboxapi.com/2/files/download" + +function DropBoxApi:fetchInfo(token) + local request, sink = {}, {} + local parsed = url.parse(API_URL_INFO) + request['url'] = API_URL_INFO + request['method'] = 'POST' + local headers = { ["Authorization"] = "Bearer ".. token } + request['headers'] = headers + request['sink'] = ltn12.sink.table(sink) + http.TIMEOUT = 5 + https.TIMEOUT = 5 + local httpRequest = parsed.scheme == 'http' and http.request or https.request + local headers_request = socket.skip(1, httpRequest(request)) + local result_response = table.concat(sink) + if headers_request == nil then + return nil + end + if result_response ~= "" then + local _, result = pcall(JSON.decode, result_response) + return result + else + return nil + end +end + +function DropBoxApi:fetchListFolders(path, token) + local request, sink = {}, {} + if path == nil or path == "/" then path = "" end + local parsed = url.parse(API_LIST_FOLDER) + request['url'] = API_LIST_FOLDER + request['method'] = 'POST' + local data = "{\"path\": \"" .. path .. "\",\"recursive\": false,\"include_media_info\": false,".. + "\"include_deleted\": false,\"include_has_explicit_shared_members\": false}" + local headers = { ["Authorization"] = "Bearer ".. token, + ["Content-Type"] = "application/json" , + ["Content-Length"] = #data} + request['headers'] = headers + request['source'] = ltn12.source.string(data) + request['sink'] = ltn12.sink.table(sink) + http.TIMEOUT = 5 + https.TIMEOUT = 5 + local httpRequest = parsed.scheme == 'http' and http.request or https.request + local headers_request = socket.skip(1, httpRequest(request)) + if headers_request == nil then + return nil + end + local result_response = table.concat(sink) + if result_response ~= "" then + local ret, result = pcall(JSON.decode, result_response) + if ret then + return result + else + return nil + end + else + return nil + end +end + +function DropBoxApi:downloadFile(path, token, local_path) + local parsed = url.parse(API_DOWNLOAD_FILE) + local url_api = API_DOWNLOAD_FILE + local data1 = "{\"path\": \"" .. path .. "\"}" + local headers = { ["Authorization"] = "Bearer ".. token, + ["Dropbox-API-Arg"] = data1} + http.TIMEOUT = 5 + https.TIMEOUT = 5 + local httpRequest = parsed.scheme == 'http' and http.request or https.request + local _, code_return, _ = httpRequest{ + url = url_api, + method = 'GET', + headers = headers, + sink = ltn12.sink.file(io.open(local_path, "w")) + } + return code_return +end + +function DropBoxApi:listFolder(path, token) + local dropbox_list = {} + local dropbox_file = {} + local tag, text + local ls_dropbox = self:fetchListFolders(path, token) + if ls_dropbox == nil then return false end + for _, files in ipairs(ls_dropbox.entries) do + text = files.name + tag = files[".tag"] + if tag == "folder" then + text = text .. "/" + table.insert(dropbox_list, { + text = text, + url = files.path_display, + type = tag + }) + --show only file with supported formats + elseif tag == "file" and DocumentRegistry:getProvider(text) then + table.insert(dropbox_file, { + text = text, + url = files.path_display, + type = tag + }) + end + end + --sort + table.sort(dropbox_list, function(v1,v2) + return v1.text < v2.text + end) + table.sort(dropbox_file, function(v1,v2) + return v1.text < v2.text + end) + for _, files in ipairs(dropbox_file) do + table.insert(dropbox_list, { + text = files.text, + url = files.url, + type = files.type + }) + end + return dropbox_list +end + +return DropBoxApi diff --git a/frontend/apps/cloudstorage/ftp.lua b/frontend/apps/cloudstorage/ftp.lua new file mode 100755 index 000000000..e6d8043bd --- /dev/null +++ b/frontend/apps/cloudstorage/ftp.lua @@ -0,0 +1,152 @@ +local FtpApi = require("frontend/apps/cloudstorage/ftpapi") +local ConfirmBox = require("ui/widget/confirmbox") +local InfoMessage = require("ui/widget/infomessage") +local MultiInputDialog = require("ui/widget/multiinputdialog") +local UIManager = require("ui/uimanager") +local _ = require("gettext") +local T = require("ffi/util").template +local ReaderUI = require("apps/reader/readerui") +local Screen = require("device").screen + +local Ftp = { +} +local function generateUrl(address, user, pass) + local colon_sign = "" + local at_sign = "" + if user ~= "" then + at_sign = "@" + end + if pass ~= "" then + colon_sign = ":" + end + local replace = "://" .. user .. colon_sign .. pass .. at_sign + local url = string.gsub(address, "://", replace) + return url +end + +function Ftp:run(address, user, pass, path) + local url = generateUrl(address, user, pass) .. path + return FtpApi:listFolder(url) +end + +function Ftp:downloadFile(item, address, user, pass, path, close) + local url = generateUrl(address, user, pass) .. item.url + local response = FtpApi:downloadFile(url) + if response ~= nil then + local file = io.open(path, "w") + file:write(response) + file:close() + UIManager:show(ConfirmBox:new{ + text = T(_("File saved to:\n %1\nWould you like to read the downloaded book now?"), + path), + ok_callback = function() + close() + ReaderUI:showReader(path) + end + }) + else + UIManager:show(InfoMessage:new{ + text = _("Could not save file to:\n") .. path, + timeout = 3, + }) + end +end + +function Ftp:config(item, callback) + local text_info = "FTP address must be in the format ftp://example.domian.com\n".. + "Also supported is format with IP e.g: ftp://10.10.10.1\n".. + "Username and password are optional." + local hint_name = _("Your FTP name") + local text_name = "" + local hint_address = _("FTP address eg ftp://example.com") + local text_address = "" + local hint_username = _("FTP username") + local text_username = "" + local hint_password = _("FTP password") + local text_password = "" + local title + local text_button_right = _("Add") + if item then + title = _("Edit FTP account") + text_button_right = _("Apply") + text_name = item.text + text_address = item.address + text_username = item.username + text_password = item.password + else + title = _("Add FTP account") + end + self.settings_dialog = MultiInputDialog:new { + title = title, + fields = { + { + text = text_name, + input_type = "string", + hint = hint_name , + }, + { + text = text_address, + input_type = "string", + hint = hint_address , + }, + { + text = text_username, + input_type = "string", + hint = hint_username, + }, + { + text = text_password, + input_type = "string", + hint = hint_password, + }, + }, + buttons = { + { + { + text = _("Cancel"), + callback = function() + self.settings_dialog:onClose() + UIManager:close(self.settings_dialog) + end + }, + { + text = _("Info"), + callback = function() + UIManager:show(InfoMessage:new{text = text_info }) + end + }, + { + text = text_button_right, + callback = function() + local fields = MultiInputDialog:getFields() + if fields[1] ~= "" and fields[2] ~= "" then + if item then + -- edit + callback(item, fields) + else + -- add new + callback(fields) + end + self.settings_dialog:onClose() + UIManager:close(self.settings_dialog) + else + UIManager:show(InfoMessage:new{text = "Please fill in all fields." }) + end + end + }, + }, + }, + width = Screen:getWidth() * 0.95, + height = Screen:getHeight() * 0.2, + input_type = "text", + } + self.settings_dialog:onShowKeyboard() + UIManager:show(self.settings_dialog) +end + +function Ftp:info(item) + local info_text = T(_"Type: %1\nName: %2\nAddress: %3", "FTP", item.text, item.address) + UIManager:show(InfoMessage:new{text = info_text}) +end + +return Ftp diff --git a/frontend/apps/cloudstorage/ftpapi.lua b/frontend/apps/cloudstorage/ftpapi.lua new file mode 100755 index 000000000..2ad8cdc4e --- /dev/null +++ b/frontend/apps/cloudstorage/ftpapi.lua @@ -0,0 +1,70 @@ +local ftp = require("socket.ftp") +local ltn12 = require("ltn12") +local url = require("socket.url") +local DocumentRegistry = require("document/documentregistry") + +local FtpApi = { +} + +function FtpApi:nlst(u) + local t = {} + local p = url.parse(u) + p.command = "nlst" + p.sink = ltn12.sink.table(t) + local r, e = ftp.get(p) + return r and table.concat(t), e +end + +function FtpApi:listFolder(address_path) + local ftp_list = {} + local ftp_file = {} + local type + local extension + local file_name + local ls_ftp = self:nlst(address_path) + if ls_ftp == nil then return false end + for item in (ls_ftp..'\n'):gmatch'(.-)\r?\n' do + if item ~= '' then + file_name = item:match("([^/]+)$") + extension = item:match("^.+(%..+)$") + item = "/" .. item + if not extension then + type = "folder" + table.insert(ftp_list, { + text = file_name .. "/", + url = item, + type = type, + }) + --show only file with supported formats + elseif extension and DocumentRegistry:getProvider(item) then + type = "file" + table.insert(ftp_file, { + text = file_name, + url = item, + type = type, + }) + end + end + end + --sort + table.sort(ftp_list, function(v1,v2) + return v1.text < v2.text + end) + table.sort(ftp_file, function(v1,v2) + return v1.text < v2.text + end) + for _, files in ipairs(ftp_file) do + table.insert(ftp_list, { + text = files.text, + url = files.url, + type = files.type + }) + end + return ftp_list +end + +function FtpApi:downloadFile(file_path) + return ftp.get(file_path ..";type=i") +end + +return FtpApi diff --git a/frontend/apps/filemanager/filemanagermenu.lua b/frontend/apps/filemanager/filemanagermenu.lua index d3da4db42..33ae8467e 100644 --- a/frontend/apps/filemanager/filemanagermenu.lua +++ b/frontend/apps/filemanager/filemanagermenu.lua @@ -13,6 +13,7 @@ local _ = require("gettext") local FileSearcher = require("apps/filemanager/filemanagerfilesearcher") local Search = require("apps/filemanager/filemanagersearch") local SetDefaults = require("apps/filemanager/filemanagersetdefaults") +local CloudStorage = require("apps/cloudstorage/cloudstorage") local FileManagerMenu = InputContainer:extend{ tab_item_table = nil, @@ -230,6 +231,18 @@ function FileManagerMenu:setUpdateItemTable() }, } }) + table.insert(self.tab_item_table.tools, { + text = _("Cloud storage"), + callback = function() + local cloud_storage = CloudStorage:new{} + UIManager:show(cloud_storage) + local filemanagerRefresh = function() self.ui:onRefresh() end + function cloud_storage:onClose() + filemanagerRefresh() + UIManager:close(cloud_storage) + end + end, + }) -- search tab table.insert(self.tab_item_table.search, {