From 04741d8cfd76cf66d6a0167f1aa55a8e8827b027 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 18 Nov 2019 18:39:45 +0100 Subject: [PATCH] [feat] Synchronize local folder with dropbox (#5591) Option to synchronize files in local koreader folder with folder on Dropbox. All files that aren't on the local disk will be downloaded. Files with different sizes will also be downloaded and overwritten. The download process can be paused or stopped at any time (similar to downloading images in Wikipedia). Synchronize and settings are available after long press in dropbox account in Cloud storage. Limitations: Only one folder (without subfolders) can be synchronize with one folder on local storage. --- frontend/apps/cloudstorage/cloudstorage.lua | 300 ++++++++++++++++++-- frontend/apps/cloudstorage/dropbox.lua | 17 +- frontend/apps/cloudstorage/dropboxapi.lua | 34 ++- frontend/ui/cloudmgr.lua | 29 ++ 4 files changed, 349 insertions(+), 31 deletions(-) create mode 100644 frontend/ui/cloudmgr.lua diff --git a/frontend/apps/cloudstorage/cloudstorage.lua b/frontend/apps/cloudstorage/cloudstorage.lua index 6d66b53d6..b17e96e5e 100644 --- a/frontend/apps/cloudstorage/cloudstorage.lua +++ b/frontend/apps/cloudstorage/cloudstorage.lua @@ -10,6 +10,7 @@ local Menu = require("ui/widget/menu") local UIManager = require("ui/uimanager") local WebDav = require("apps/cloudstorage/webdav") local lfs = require("libs/libkoreader-lfs") +local T = require("ffi/util").template local _ = require("gettext") local Screen = require("device").screen @@ -26,17 +27,24 @@ local CloudStorage = Menu:extend{ show_parent = nil, is_popout = false, is_borderless = true, + title = _("Cloud storage") } 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() + if self.item then + self.item_table = self:genItemTable(self.item) + self.choose_folder_mode = true + else + self.item_table = self:genItemTableFromRoot() + end self.width = Screen:getWidth() self.height = Screen:getHeight() Menu.init(self) + if self.item then + self.item_table[1].callback() + end end function CloudStorage:genItemTableFromRoot() @@ -57,6 +65,8 @@ function CloudStorage:genItemTableFromRoot() type = server.type, editable = true, url = server.url, + sync_source_folder = server.sync_source_folder, + sync_dest_folder = server.sync_dest_folder, callback = function() self.type = server.type self.password = server.password @@ -69,6 +79,31 @@ function CloudStorage:genItemTableFromRoot() return item_table end +function CloudStorage:genItemTable(item) + local item_table = {} + local added_servers = self.cs_settings:readSetting("cs_servers") or {} + for _, server in ipairs(added_servers) do + if server.name == item.text and server.password == item.password and server.type == item.type then + table.insert(item_table, { + text = server.name, + address = server.address, + username = server.username, + password = server.password, + type = server.type, + 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 + end + return item_table +end + function CloudStorage:selectCloudType() local buttons = { { @@ -117,7 +152,7 @@ function CloudStorage:openCloudServer(url) NetworkMgr:promptWifiOn() return end - tbl = DropBox:run(url, self.password) + tbl = DropBox:run(url, self.password, self.choose_folder_mode) elseif self.type == "ftp" then if not NetworkMgr:isConnected() then NetworkMgr:promptWifiOn() @@ -157,6 +192,8 @@ function CloudStorage:onMenuSelect(item) item.callback() elseif item.type == "file" then self:downloadFile(item) + elseif item.type == "other" then + return true else table.insert(self.paths, { url = item.url, @@ -255,45 +292,254 @@ function CloudStorage:cloudFile(item, path) UIManager:show(self.download_dialog) end +function CloudStorage:updateSyncFolder(item, source, dest) + local cs_settings = self:readSettings() + local cs_servers = cs_settings:readSetting("cs_servers") or {} + for _, server in ipairs(cs_servers) do + if server.name == item.text and server.password == item.password and server.type == item.type then + if source then + server.sync_source_folder = source + end + if dest then + server.sync_dest_folder = dest + end + break + end + end + cs_settings:saveSetting("cs_servers", cs_servers) + cs_settings:flush() +end + function CloudStorage:onMenuHold(item) - if item.editable then - local cs_server_dialog - cs_server_dialog = ButtonDialog:new{ + if item.type == "folder_long_press" then + local title = T(_("Select this directory?\n\n%1"), item.url) + local onConfirm = self.onConfirm + local button_dialog + button_dialog = ButtonDialogTitle:new{ + title = title, buttons = { { { - text = _("Info"), - enabled = true, - callback = function() - UIManager:close(cs_server_dialog) - self:infoServer(item) - end - }, - { - text = _("Edit"), - enabled = true, + text = _("Cancel"), callback = function() - UIManager:close(cs_server_dialog) - self:editCloudServer(item) - - end + UIManager:close(button_dialog) + end, }, { - text = _("Delete"), - enabled = true, + text = _("Select"), callback = function() - UIManager:close(cs_server_dialog) - self:deleteCloudServer(item) - end + if onConfirm then + onConfirm(item.url) + end + UIManager:close(button_dialog) + UIManager:close(self) + end, }, }, - } + }, + } + UIManager:show(button_dialog) + end + if item.editable then + local cs_server_dialog + local 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 + }, + }, + } + if item.type == "dropbox" then + table.insert(buttons, { + { + text = _("Synchronize now"), + enabled = item.sync_source_folder ~= nil and item.sync_dest_folder ~= nil, + callback = function() + UIManager:close(cs_server_dialog) + self:synchronizeCloud(item) + end + }, + { + text = _("Synchronize settings"), + enabled = true, + callback = function() + UIManager:close(cs_server_dialog) + self:synchronizeSettings(item) + end + }, + }) + end + cs_server_dialog = ButtonDialog:new{ + buttons = buttons } UIManager:show(cs_server_dialog) return true end end +function CloudStorage:synchronizeCloud(item) + local Trapper = require("ui/trapper") + Trapper:wrap(function() + Trapper:setPausedText("Download paused.\nDo you want to continue or abort downloading files?") + local ok, downloaded_files, failed_files = pcall(self.downloadListFiles, self, item) + if ok and downloaded_files then + if not failed_files then failed_files = 0 end + local text + if downloaded_files == 0 and failed_files == 0 then + text = _("No files to download from dropbox.") + elseif downloaded_files > 0 and failed_files == 0 then + text = T(_("Successfuly downloaded %1 files from Dropbox to local storage."), downloaded_files) + else + text = T(_("Successfuly downloaded %1 files from Dropbox to local storage.\nFailed downloaded %2 files."), + downloaded_files, failed_files) + end + UIManager:show(InfoMessage:new{ + text = text, + timeout = 3, + }) + else + Trapper:reset() -- close any last widget not cleaned if error + UIManager:show(InfoMessage:new{ + text = _("No files to download from Dropbox.\nPlease check your configuration and connection."), + timeout = 3, + }) + end + end) +end + +function CloudStorage:downloadListFiles(item) + local local_files = {} + local path = item.sync_dest_folder + local UI = require("ui/trapper") + UI:info(_("Retrieving files…")) + + local ok, iter, dir_obj = pcall(lfs.dir, path) + if ok then + for f in iter, dir_obj do + local filename = path .."/" .. f + local attributes = lfs.attributes(filename) + if attributes.mode == "file" then + local_files[f] = attributes.size + end + end + end + local remote_files = DropBox:showFiles(item.sync_source_folder, item.password) + if #remote_files == 0 then + UI:clear() + return false + end + local files_to_download = 0 + for i, file in ipairs(remote_files) do + if not local_files[file.text] or local_files[file.text] ~= file.size then + files_to_download = files_to_download + 1 + remote_files[i].download = true + end + end + + if files_to_download == 0 then + UI:clear() + return 0 + end + + local response, go_on + local proccessed_files = 0 + local success_files = 0 + local unsuccess_files = 0 + for _, file in ipairs(remote_files) do + if file.download then + proccessed_files = proccessed_files + 1 + print(file.url) + local text = string.format("Downloading file (%d/%d):\n%s", proccessed_files, files_to_download, file.text) + go_on = UI:info(text) + if not go_on then + break + end + response = DropBox:downloadFileNoUI(file.url, item.password, item.sync_dest_folder .. "/" .. file.text) + if response then + success_files = success_files + 1 + else + unsuccess_files = unsuccess_files + 1 + end + end + end + UI:clear() + return success_files, unsuccess_files +end + +function CloudStorage:synchronizeSettings(item) + local syn_dialog + local dropbox_sync_folder = item.sync_source_folder or "not set" + local local_sync_folder = item.sync_dest_folder or "not set" + syn_dialog = ButtonDialogTitle:new { + title = T(_("Dropbox folder:\n%1\nLocal folder:\n%2"), dropbox_sync_folder, local_sync_folder), + title_align = "center", + buttons = { + { + { + text = _("Choose dropbox folder"), + callback = function() + UIManager:close(syn_dialog) + require("ui/cloudmgr"):new{ + item = item, + onConfirm = function(path) + self:updateSyncFolder(item, path) + item.sync_source_folder = path + self:synchronizeSettings(item) + end, + }:chooseDir() + end, + }, + }, + { + { + text = _("Choose local folder"), + callback = function() + UIManager:close(syn_dialog) + require("ui/downloadmgr"):new{ + onConfirm = function(path) + self:updateSyncFolder(item, nil, path) + item.sync_dest_folder = path + self:synchronizeSettings(item) + end, + }:chooseDir() + end, + }, + }, + { + { + text = _("Close"), + callback = function() + UIManager:close(syn_dialog) + end, + }, + }, + } + } + UIManager:show(syn_dialog) +end + function CloudStorage:configCloud(type) local callbackAdd = function(fields) local cs_settings = self:readSettings() diff --git a/frontend/apps/cloudstorage/dropbox.lua b/frontend/apps/cloudstorage/dropbox.lua index 60e5ed4b4..2bc304d62 100644 --- a/frontend/apps/cloudstorage/dropbox.lua +++ b/frontend/apps/cloudstorage/dropbox.lua @@ -10,8 +10,12 @@ local _ = require("gettext") local DropBox = {} -function DropBox:run(url, password) - return DropBoxApi:listFolder(url, password) +function DropBox:run(url, password, choose_folder_mode) + return DropBoxApi:listFolder(url, password, choose_folder_mode) +end + +function DropBox:showFiles(url, password) + return DropBoxApi:showFiles(url, password) end function DropBox:downloadFile(item, password, path, close) @@ -39,6 +43,15 @@ function DropBox:downloadFile(item, password, path, close) end end +function DropBox:downloadFileNoUI(url, password, path) + local code_response = DropBoxApi:downloadFile(url, password, path) + if code_response == 200 then + return true + else + return false + 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".. diff --git a/frontend/apps/cloudstorage/dropboxapi.lua b/frontend/apps/cloudstorage/dropboxapi.lua index d7bcfc382..480584044 100644 --- a/frontend/apps/cloudstorage/dropboxapi.lua +++ b/frontend/apps/cloudstorage/dropboxapi.lua @@ -90,7 +90,9 @@ function DropBoxApi:downloadFile(path, token, local_path) return code_return end -function DropBoxApi:listFolder(path, token) +-- folder_mode - set to true when we want to see only folder. +-- We see also extra folder "Long-press to select current directory" at the beginning. +function DropBoxApi:listFolder(path, token, folder_mode) local dropbox_list = {} local dropbox_file = {} local tag, text @@ -101,6 +103,7 @@ function DropBoxApi:listFolder(path, token) tag = files[".tag"] if tag == "folder" then text = text .. "/" + if folder_mode then tag = "folder_long_press" end table.insert(dropbox_list, { text = text, url = files.path_display, @@ -108,7 +111,7 @@ function DropBoxApi:listFolder(path, token) }) --show only file with supported formats elseif tag == "file" and (DocumentRegistry:hasProvider(text) - or G_reader_settings:isTrue("show_unsupported")) then + or G_reader_settings:isTrue("show_unsupported")) and not folder_mode then table.insert(dropbox_file, { text = text, url = files.path_display, @@ -123,6 +126,14 @@ function DropBoxApi:listFolder(path, token) table.sort(dropbox_file, function(v1,v2) return v1.text < v2.text end) + -- Add special folder. + if folder_mode then + table.insert(dropbox_list, 1, { + text = _("Long-press to select current directory"), + url = path, + type = "folder_long_press", + }) + end for _, files in ipairs(dropbox_file) do table.insert(dropbox_list, { text = files.text, @@ -133,4 +144,23 @@ function DropBoxApi:listFolder(path, token) return dropbox_list end +function DropBoxApi:showFiles(path, token) + local dropbox_files = {} + local tag, text + local ls_dropbox = self:fetchListFolders(path, token) + if ls_dropbox == nil or ls_dropbox.entries == nil then return false end + for _, files in ipairs(ls_dropbox.entries) do + text = files.name + tag = files[".tag"] + if tag == "file" and (DocumentRegistry:hasProvider(text) or G_reader_settings:isTrue("show_unsupported")) then + table.insert(dropbox_files, { + text = text, + url = files.path_display, + size = files.size, + }) + end + end + return dropbox_files +end + return DropBoxApi diff --git a/frontend/ui/cloudmgr.lua b/frontend/ui/cloudmgr.lua new file mode 100644 index 000000000..5269762cd --- /dev/null +++ b/frontend/ui/cloudmgr.lua @@ -0,0 +1,29 @@ +local CloudStorage = require("apps/cloudstorage/cloudstorage") +local UIManager = require("ui/uimanager") +local _ = require("gettext") + +local CloudMgr = { + onConfirm = function() end, +} + +function CloudMgr:new(from_o) + local o = from_o or {} + setmetatable(o, self) + self.__index = self + return o +end + +--- Displays a PathChooser for cloud drive for picking a (source) directory. +-- @treturn string path chosen by the user +function CloudMgr:chooseDir() + local cloud_storage = CloudStorage:new{ + title = _("Long-press to select directory"), + item = self.item, + onConfirm = function(dir_path) + self.onConfirm(dir_path) + end, + } + UIManager:show(cloud_storage) +end + +return CloudMgr