From a3a17dbbeb7bcd94702c2f12acf1e09809340b50 Mon Sep 17 00:00:00 2001 From: Yann Muller Date: Wed, 17 Oct 2018 13:13:59 +0100 Subject: [PATCH] WebDav CloudStorage (#4272) Addition of WebDav to the CloudStorage. The functionality is the same as with Dropbox and FTP, it is possible to browse the files on the server and download a copy. Tested with: NextCloud HubZilla --- frontend/apps/cloudstorage/cloudstorage.lua | 62 +++++++- frontend/apps/cloudstorage/ftp.lua | 2 +- frontend/apps/cloudstorage/webdav.lua | 146 ++++++++++++++++++ frontend/apps/cloudstorage/webdavapi.lua | 162 ++++++++++++++++++++ 4 files changed, 369 insertions(+), 3 deletions(-) create mode 100644 frontend/apps/cloudstorage/webdav.lua create mode 100644 frontend/apps/cloudstorage/webdavapi.lua diff --git a/frontend/apps/cloudstorage/cloudstorage.lua b/frontend/apps/cloudstorage/cloudstorage.lua index 334598f30..1eb24d16e 100644 --- a/frontend/apps/cloudstorage/cloudstorage.lua +++ b/frontend/apps/cloudstorage/cloudstorage.lua @@ -8,6 +8,7 @@ local InfoMessage = require("ui/widget/infomessage") local LuaSettings = require("luasettings") local Menu = require("ui/widget/menu") local UIManager = require("ui/uimanager") +local WebDav = require("apps/cloudstorage/webdav") local lfs = require("libs/libkoreader-lfs") local _ = require("gettext") local Screen = require("device").screen @@ -88,6 +89,15 @@ function CloudStorage:selectCloudType() end, }, }, + { + { + text = _("WebDAV"), + callback = function() + UIManager:close(self.cloud_dialog) + self:configCloud("webdav") + end, + }, + }, } self.cloud_dialog = ButtonDialogTitle:new{ title = _("Choose cloud storage type"), @@ -114,6 +124,12 @@ function CloudStorage:openCloudServer(url) return end tbl = Ftp:run(self.address, self.username, self.password, url) + elseif self.type == "webdav" then + if not NetworkMgr:isConnected() then + NetworkMgr:promptWifiOn() + return + end + tbl = WebDav:run(self.address, self.username, self.password, url) end if tbl and #tbl > 0 then self:switchItemTable(url, tbl) @@ -171,6 +187,7 @@ end function CloudStorage:cloudFile(item, path) local path_dir = path + local download_text = _("Downloading. This might take a moment.") local buttons = { { { @@ -185,7 +202,7 @@ function CloudStorage:cloudFile(item, path) end) UIManager:close(self.download_dialog) UIManager:show(InfoMessage:new{ - text = _("Downloading may take several minutes…"), + text = download_text, timeout = 1, }) elseif self.type == "ftp" then @@ -197,7 +214,19 @@ function CloudStorage:cloudFile(item, path) end) UIManager:close(self.download_dialog) UIManager:show(InfoMessage:new{ - text = _("Downloading may take several minutes…"), + text = download_text, + timeout = 1, + }) + elseif self.type == "webdav" then + local callback_close = function() + self:onClose() + end + UIManager:scheduleIn(1, function() + WebDav:downloadFile(item, self.address, self.username, self.password, path_dir, callback_close) + end) + UIManager:close(self.download_dialog) + UIManager:show(InfoMessage:new{ + text = download_text, timeout = 1, }) end @@ -286,6 +315,16 @@ function CloudStorage:configCloud(type) type = "ftp", url = "/" }) + elseif type == "webdav" then + table.insert(cs_servers,{ + name = fields[1], + address = fields[2], + username = fields[3], + password = fields[4], + url = fields[5], + type = "webdav", + --url = "/" + }) end cs_settings:saveSetting("cs_servers", cs_servers) cs_settings:flush() @@ -297,6 +336,9 @@ function CloudStorage:configCloud(type) if type == "ftp" then Ftp:config(nil, callbackAdd) end + if type == "webdav" then + WebDav:config(nil, callbackAdd) + end end function CloudStorage:editCloudServer(item) @@ -324,6 +366,18 @@ function CloudStorage:editCloudServer(item) break end end + elseif item.type == "webdav" 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] + server.url = fields[5] + cs_servers[i] = server + break + end + end end cs_settings:saveSetting("cs_servers", cs_servers) cs_settings:flush() @@ -333,6 +387,8 @@ function CloudStorage:editCloudServer(item) DropBox:config(item, callbackEdit) elseif item.type == "ftp" then Ftp:config(item, callbackEdit) + elseif item.type == "webdav" then + WebDav:config(item, callbackEdit) end end @@ -355,6 +411,8 @@ function CloudStorage:infoServer(item) DropBox:info(item.password) elseif item.type == "ftp" then Ftp:info(item) + elseif item.type == "webdav" then + WebDav:info(item) end end diff --git a/frontend/apps/cloudstorage/ftp.lua b/frontend/apps/cloudstorage/ftp.lua index bc1e9e5f3..04a2195ed 100644 --- a/frontend/apps/cloudstorage/ftp.lua +++ b/frontend/apps/cloudstorage/ftp.lua @@ -43,7 +43,7 @@ function Ftp:downloadFile(item, address, user, pass, path, close) end function Ftp:config(item, callback) - local text_info = "FTP address must be in the format ftp://example.domian.com\n".. + local text_info = "FTP address must be in the format ftp://example.domain.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") diff --git a/frontend/apps/cloudstorage/webdav.lua b/frontend/apps/cloudstorage/webdav.lua new file mode 100644 index 000000000..8388ed1b2 --- /dev/null +++ b/frontend/apps/cloudstorage/webdav.lua @@ -0,0 +1,146 @@ +local ConfirmBox = require("ui/widget/confirmbox") +local InfoMessage = require("ui/widget/infomessage") +local MultiInputDialog = require("ui/widget/multiinputdialog") +local UIManager = require("ui/uimanager") +local ReaderUI = require("apps/reader/readerui") +local WebDavApi = require("apps/cloudstorage/webdavapi") +local _ = require("gettext") +local Screen = require("device").screen +local T = require("ffi/util").template + +local WebDav = {} + +function WebDav:run(address, user, pass, path) + return WebDavApi:listFolder(address, user, pass, path) +end + +function WebDav:downloadFile(item, address, username, password, local_path, close) + local code_response = WebDavApi:downloadFile(address .. WebDavApi:urlEncode( item.url ), username, password, local_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?"), + local_path), + ok_callback = function() + close() + ReaderUI:showReader(local_path) + end + }) + else + UIManager:show(InfoMessage:new{ + text = T(_("Could not save file to:\n%1"), local_path), + timeout = 3, + }) + end +end + +function WebDav:config(item, callback) + local text_info = _([[Server address must be of the form http(s)://domain.name/path +This can point to a sub-directory of the WebDAV server. +The start folder is appended to the server path.]]) + + local hint_name = _("Server display name") + local text_name = "" + local hint_address = _("WebDAV address eg https://example.com/dav") + local text_address = "" + local hint_username = _("Username") + local text_username = "" + local hint_password = _("Password") + local text_password = "" + local hint_folder = _("Start folder") + local text_folder = "" + local title + local text_button_ok = _("Add") + if item then + title = _("Edit WebDAV account") + text_button_ok = _("Apply") + text_name = item.text + text_address = item.address + text_username = item.username + text_password = item.password + text_folder = item.url + else + title = _("Add WebDAV 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", + text_type = "password", + hint = hint_password, + }, + { + text = text_folder, + input_type = "string", + hint = hint_folder, + }, + }, + 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_ok, + 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", + } + UIManager:show(self.settings_dialog) + self.settings_dialog:onShowKeyboard() + +end + +function WebDav:info(item) + local info_text = T(_"Type: %1\nName: %2\nAddress: %3", "WebDAV", item.text, item.address) + UIManager:show(InfoMessage:new{text = info_text}) +end + +return WebDav diff --git a/frontend/apps/cloudstorage/webdavapi.lua b/frontend/apps/cloudstorage/webdavapi.lua new file mode 100644 index 000000000..41c45f041 --- /dev/null +++ b/frontend/apps/cloudstorage/webdavapi.lua @@ -0,0 +1,162 @@ +local DocumentRegistry = require("document/documentregistry") +local FFIUtil = require("ffi/util") +local http = require('socket.http') +local https = require('ssl.https') +local ltn12 = require('ltn12') +local mime = require('mime') +local socket = require('socket') +local url = require('socket.url') +local util = require("util") +local _ = require("gettext") + +local WebDavApi = { +} + +function WebDavApi:isCurrentDirectory( current_item, address, path ) + local is_home, is_parent + local home_path + -- find first occurence of / after http(s):// + local start = string.find( address, "/", 9 ) + if not start then + home_path = "/" + else + home_path = string.sub( address, start ) + end + local item + if string.sub( current_item, -1 ) == "/" then + item = string.sub( current_item, 1, -2 ) + else + item = current_item + end + + if item == home_path then + is_home = true + else + local temp_path = string.sub( item, string.len(home_path) + 1 ) + if temp_path == path then + is_parent = true + end + end + return is_home or is_parent +end + +-- version of urlEncode that doesn't encode the / +function WebDavApi:urlEncode(url_data) + local char_to_hex = function(c) + return string.format("%%%02X", string.byte(c)) + end + if url_data == nil then + return + end + url_data = url_data:gsub("([^%w%/%-%.%_%~%!%*%'%(%)])", char_to_hex) + return url_data +end + +function WebDavApi:listFolder(address, user, pass, folder_path) + local path = self:urlEncode( folder_path ) + local webdav_list = {} + local webdav_file = {} + + local has_trailing_slash = false + local has_leading_slash = false + if string.sub( address, -1 ) ~= "/" then has_trailing_slash = true end + if path == nil or path == "/" then + path = "" + elseif string.sub( path, 1, 2 ) == "/" then + if has_trailing_slash then + -- too many slashes, remove one + path = string.sub( path, 1 ) + end + has_leading_slash = true + end + if not has_trailing_slash and not has_leading_slash then + address = address .. "/" + end + local webdav_url = address .. path + + local request, sink = {}, {} + local parsed = url.parse(webdav_url) + local data = [[]] + local auth = string.format("%s:%s", user, pass) + local headers = { ["Authorization"] = "Basic " .. mime.b64( auth ), + ["Content-Type"] = "application/xml", + ["Depth"] = "1", + ["Content-Length"] = #data} + request["url"] = webdav_url + request["method"] = "PROPFIND" + 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 res_data = table.concat(sink) + if res_data ~= "" then + -- iterate through the tags, each containing an entry + for item in res_data:gmatch("(.-)") do + --logger.dbg("WebDav catalog item=", item) + -- is the path and filename of the entry. + local item_fullpath = item:match("(.*)") + local is_current_dir = self:isCurrentDirectory( item_fullpath, address, path ) + local item_name = util.urlDecode( FFIUtil.basename( item_fullpath ) ) + local item_path = path .. "/" .. item_name + if item:find("") then + item_name = item_name .. "/" + if not is_current_dir then + table.insert(webdav_list, { + text = item_name, + url = util.urlDecode( item_path ), + type = "folder", + }) + end + elseif item:find("") and DocumentRegistry:hasProvider(item_name) then + table.insert(webdav_file, { + text = item_name, + url = util.urlDecode( item_path ), + type = "file", + }) + end + end + else + return nil + end + + --sort + table.sort(webdav_list, function(v1,v2) + return v1.text < v2.text + end) + table.sort(webdav_file, function(v1,v2) + return v1.text < v2.text + end) + for _, files in ipairs(webdav_file) do + table.insert(webdav_list, { + text = files.text, + url = files.url, + type = files.type, + }) + end + return webdav_list +end + +function WebDavApi:downloadFile(file_url, user, pass, local_path) + local parsed = url.parse(file_url) + local auth = string.format("%s:%s", user, pass) + local headers = { ["Authorization"] = "Basic " .. mime.b64( auth ) } + http.TIMEOUT = 5 + https.TIMEOUT = 5 + local httpRequest = parsed.scheme == "http" and http.request or https.request + local _, code_return, _ = httpRequest{ + url = file_url, + method = "GET", + headers = headers, + sink = ltn12.sink.file(io.open(local_path, "w")) + } + return code_return +end + +return WebDavApi