local DocumentRegistry = require("document/documentregistry") local FFIUtil = require("ffi/util") local http = require("socket.http") local ltn12 = require("ltn12") local socket = require("socket") local socketutil = require("socketutil") local util = require("util") local _ = require("gettext") local logger = require("logger") local WebDavApi = { } function WebDavApi:getJoinedPath( address, path ) local path_encoded = self:urlEncode( path ) or "" local address_strip = address:sub(-1) == "/" and address:sub(1, -2) or address local path_strip = path_encoded:sub(1, 1) == "/" and path_encoded:sub(2) or path_encoded return address_strip .. "/" .. path_strip end 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 string.sub( path, -1 ) == "/" then path = string.sub( path, 1, -2 ) end 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, folder_mode) 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, 1 ) == "/" then if has_trailing_slash then -- too many slashes, remove one path = string.sub( path, 2 ) 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 if string.sub(webdav_url, -1) ~= "/" then webdav_url = webdav_url .. "/" end local sink = {} local data = [[]] socketutil:set_timeout() local request = { url = webdav_url, method = "PROPFIND", headers = { ["Content-Type"] = "application/xml", ["Depth"] = "1", ["Content-Length"] = #data, }, user = user, password = pass, source = ltn12.source.string(data), sink = ltn12.sink.table(sink), } local code, headers, status = socket.skip(1, http.request(request)) socketutil:reset_timeout() if headers == nil then logger.dbg("WebDavApi:listFolder: No response:", status or code) return nil elseif not code or code < 200 or code > 299 then -- got a response, but it wasn't a success (e.g. auth failure) logger.dbg("WebDavApi:listFolder: Request failed:", status or code) logger.dbg("WebDavApi:listFolder: Response headers:", headers) logger.dbg("WebDavApi:listFolder: Response body:", table.concat(sink)) 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("<[^:]*:response[^>]*>(.-)") do --logger.dbg("WebDav catalog item=", item) -- is the path and filename of the entry. local item_fullpath = item:match("<[^:]*:href[^>]*>(.*)") if string.sub( item_fullpath, -1 ) == "/" then item_fullpath = string.sub( item_fullpath, 1, -2 ) end local is_current_dir = self:isCurrentDirectory( util.urlDecode(item_fullpath), address, folder_path ) local item_name = util.urlDecode( FFIUtil.basename( item_fullpath ) ) item_name = util.htmlEntitiesToUtf8(item_name) local is_not_collection = item:find("<[^:]*:resourcetype/>") or item:find("<[^:]*:resourcetype>") local item_path = (path == "" and has_trailing_slash) and item_name or path .. "/" .. item_name if item:find("<[^:]*:collection[^<]*/>") 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 is_not_collection and (DocumentRegistry:hasProvider(item_name) or G_reader_settings:isTrue("show_unsupported")) 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 if folder_mode then table.insert(webdav_list, 1, { text = _("Long-press to choose current folder"), url = folder_path, type = "folder_long_press", bold = true }) end return webdav_list end function WebDavApi:downloadFile(file_url, user, pass, local_path) socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT) logger.dbg("WebDavApi: downloading file: ", file_url) local code, headers, status = socket.skip(1, http.request{ url = file_url, method = "GET", sink = ltn12.sink.file(io.open(local_path, "w")), user = user, password = pass, }) socketutil:reset_timeout() if code ~= 200 then logger.warn("WebDavApi: Download failure:", status or code or "network unreachable") logger.dbg("WebDavApi: Response headers:", headers) end return code, (headers or {}).etag end function WebDavApi:uploadFile(file_url, user, pass, local_path, etag) socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT) local code, _, status = socket.skip(1, http.request{ url = file_url, method = "PUT", source = ltn12.source.file(io.open(local_path, "r")), user = user, password = pass, headers = { ["If-Match"] = etag } }) socketutil:reset_timeout() if code < 200 or code > 299 then logger.warn("WebDavApi: upload failure:", status or code or "network unreachable") end return code end function WebDavApi:createFolder(folder_url, user, pass, folder_name) socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT) local code, _, status = socket.skip(1, http.request{ url = folder_url, method = "MKCOL", user = user, password = pass, }) socketutil:reset_timeout() if code ~= 201 then logger.warn("WebDavApi: create folder failure:", status or code or "network unreachable") end return code end return WebDavApi