2018-10-17 12:13:59 +00:00
|
|
|
local DocumentRegistry = require("document/documentregistry")
|
|
|
|
local FFIUtil = require("ffi/util")
|
2021-03-15 00:25:10 +00:00
|
|
|
local http = require("socket.http")
|
|
|
|
local ltn12 = require("ltn12")
|
|
|
|
local socket = require("socket")
|
|
|
|
local socketutil = require("socketutil")
|
2018-10-17 12:13:59 +00:00
|
|
|
local util = require("util")
|
|
|
|
local _ = require("gettext")
|
2021-03-21 13:00:13 +00:00
|
|
|
local logger = require("logger")
|
2023-07-01 05:29:38 +00:00
|
|
|
local lfs = require("libs/libkoreader-lfs")
|
2018-10-17 12:13:59 +00:00
|
|
|
|
|
|
|
local WebDavApi = {
|
|
|
|
}
|
|
|
|
|
2024-06-20 17:46:03 +00:00
|
|
|
-- Trim leading & trailing slashes from string `s` (based on util.trim)
|
|
|
|
function WebDavApi.trim_slashes(s)
|
|
|
|
local from = s:match"^/*()"
|
|
|
|
return from > #s and "" or s:match(".*[^/]", from)
|
2022-10-29 15:45:52 +00:00
|
|
|
end
|
|
|
|
|
2024-06-20 17:46:03 +00:00
|
|
|
-- Trim trailing slashes from string `s` (based on util.rtrim)
|
|
|
|
function WebDavApi.rtrim_slashes(s)
|
|
|
|
local n = #s
|
|
|
|
while n > 0 and s:find("^/", n) do
|
|
|
|
n = n - 1
|
2018-10-17 12:13:59 +00:00
|
|
|
end
|
2024-06-20 17:46:03 +00:00
|
|
|
return s:sub(1, n)
|
2018-10-17 12:13:59 +00:00
|
|
|
end
|
|
|
|
|
2024-06-20 17:46:03 +00:00
|
|
|
-- Variant of util.urlEncode that doesn't encode the /
|
|
|
|
function WebDavApi.urlEncode(url_data)
|
2018-10-17 12:13:59 +00:00
|
|
|
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
|
|
|
|
|
2024-06-20 17:46:03 +00:00
|
|
|
-- Append path to address with a slash separator, trimming any unwanted slashes in the process.
|
|
|
|
function WebDavApi:getJoinedPath(address, path)
|
|
|
|
local path_encoded = self.urlEncode(path) or ""
|
|
|
|
-- Strip leading & trailing slashes from `path`
|
|
|
|
local sane_path = self.trim_slashes(path_encoded)
|
|
|
|
-- Strip trailing slashes from `address` for now
|
|
|
|
local sane_address = self.rtrim_slashes(address)
|
|
|
|
-- Join our final URL
|
|
|
|
return sane_address .. "/" .. sane_path
|
|
|
|
end
|
|
|
|
|
2022-11-11 14:53:06 +00:00
|
|
|
function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
|
2024-06-20 17:46:03 +00:00
|
|
|
local path = folder_path or ""
|
2018-10-17 12:13:59 +00:00
|
|
|
local webdav_list = {}
|
|
|
|
local webdav_file = {}
|
|
|
|
|
2024-06-20 17:46:03 +00:00
|
|
|
-- Strip leading & trailing slashes from `path`
|
|
|
|
path = self.trim_slashes(path)
|
|
|
|
-- Strip trailing slashes from `address` for now
|
|
|
|
address = self.rtrim_slashes(address)
|
|
|
|
-- Join our final URL, which *must* have a trailing / (it's a URL)
|
|
|
|
-- This is where we deviate from getJoinedPath ;).
|
|
|
|
local webdav_url = address .. "/" .. self.urlEncode(path)
|
|
|
|
if webdav_url:sub(-1) ~= "/" then
|
2020-08-17 07:54:16 +00:00
|
|
|
webdav_url = webdav_url .. "/"
|
|
|
|
end
|
2018-10-17 12:13:59 +00:00
|
|
|
|
2021-03-15 00:25:10 +00:00
|
|
|
local sink = {}
|
2018-10-17 12:13:59 +00:00
|
|
|
local data = [[<?xml version="1.0"?><a:propfind xmlns:a="DAV:"><a:prop><a:resourcetype/></a:prop></a:propfind>]]
|
2021-03-15 00:25:10 +00:00
|
|
|
socketutil:set_timeout()
|
|
|
|
local request = {
|
|
|
|
url = webdav_url,
|
|
|
|
method = "PROPFIND",
|
|
|
|
headers = {
|
|
|
|
["Content-Type"] = "application/xml",
|
|
|
|
["Depth"] = "1",
|
|
|
|
["Content-Length"] = #data,
|
|
|
|
},
|
2021-03-21 13:00:13 +00:00
|
|
|
user = user,
|
2021-03-15 00:25:10 +00:00
|
|
|
password = pass,
|
|
|
|
source = ltn12.source.string(data),
|
|
|
|
sink = ltn12.sink.table(sink),
|
|
|
|
}
|
2022-09-16 22:08:00 +00:00
|
|
|
local code, headers, status = socket.skip(1, http.request(request))
|
2021-03-15 00:25:10 +00:00
|
|
|
socketutil:reset_timeout()
|
2022-09-16 22:08:00 +00:00
|
|
|
if headers == nil then
|
|
|
|
logger.dbg("WebDavApi:listFolder: No response:", status or code)
|
2018-10-17 12:13:59 +00:00
|
|
|
return nil
|
2022-09-16 22:08:00 +00:00
|
|
|
elseif not code or code < 200 or code > 299 then
|
2021-03-21 13:00:13 +00:00
|
|
|
-- got a response, but it wasn't a success (e.g. auth failure)
|
2022-09-16 22:08:00 +00:00
|
|
|
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))
|
2021-03-21 13:00:13 +00:00
|
|
|
return nil
|
2018-10-17 12:13:59 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
local res_data = table.concat(sink)
|
2020-08-17 07:54:16 +00:00
|
|
|
|
2018-10-17 12:13:59 +00:00
|
|
|
if res_data ~= "" then
|
|
|
|
-- iterate through the <d:response> tags, each containing an entry
|
2020-08-17 07:54:16 +00:00
|
|
|
for item in res_data:gmatch("<[^:]*:response[^>]*>(.-)</[^:]*:response>") do
|
2018-10-17 12:13:59 +00:00
|
|
|
--logger.dbg("WebDav catalog item=", item)
|
|
|
|
-- <d:href> is the path and filename of the entry.
|
2020-08-17 07:54:16 +00:00
|
|
|
local item_fullpath = item:match("<[^:]*:href[^>]*>(.*)</[^:]*:href>")
|
2024-06-20 17:46:03 +00:00
|
|
|
local item_name = FFIUtil.basename(util.htmlEntitiesToUtf8(util.urlDecode(item_fullpath)))
|
|
|
|
local is_current_dir = item_name == string.sub(folder_path, -#item_name)
|
2024-08-31 07:48:58 +00:00
|
|
|
local is_not_collection = item:find("<[^:]*:resourcetype%s*/>") or
|
2024-06-20 17:46:03 +00:00
|
|
|
item:find("<[^:]*:resourcetype></[^:]*:resourcetype>")
|
|
|
|
local item_path = path .. "/" .. item_name
|
2021-08-23 06:51:33 +00:00
|
|
|
|
2022-12-02 22:47:16 +00:00
|
|
|
if item:find("<[^:]*:collection[^<]*/>") then
|
2018-10-17 12:13:59 +00:00
|
|
|
item_name = item_name .. "/"
|
|
|
|
if not is_current_dir then
|
|
|
|
table.insert(webdav_list, {
|
|
|
|
text = item_name,
|
2024-06-20 17:46:03 +00:00
|
|
|
url = item_path,
|
2018-10-17 12:13:59 +00:00
|
|
|
type = "folder",
|
|
|
|
})
|
|
|
|
end
|
2021-08-23 06:51:33 +00:00
|
|
|
elseif is_not_collection and (DocumentRegistry:hasProvider(item_name)
|
2019-07-21 19:45:02 +00:00
|
|
|
or G_reader_settings:isTrue("show_unsupported")) then
|
2018-10-17 12:13:59 +00:00
|
|
|
table.insert(webdav_file, {
|
|
|
|
text = item_name,
|
2024-06-20 17:46:03 +00:00
|
|
|
url = item_path,
|
2018-10-17 12:13:59 +00:00
|
|
|
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
|
2022-11-11 14:53:06 +00:00
|
|
|
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
|
2018-10-17 12:13:59 +00:00
|
|
|
return webdav_list
|
|
|
|
end
|
|
|
|
|
|
|
|
function WebDavApi:downloadFile(file_url, user, pass, local_path)
|
2021-03-15 00:25:10 +00:00
|
|
|
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
|
2021-08-27 07:40:14 +00:00
|
|
|
logger.dbg("WebDavApi: downloading file: ", file_url)
|
2022-09-16 22:08:00 +00:00
|
|
|
local code, headers, status = socket.skip(1, http.request{
|
2021-03-15 00:25:10 +00:00
|
|
|
url = file_url,
|
|
|
|
method = "GET",
|
|
|
|
sink = ltn12.sink.file(io.open(local_path, "w")),
|
2021-03-21 13:00:13 +00:00
|
|
|
user = user,
|
2021-03-15 00:25:10 +00:00
|
|
|
password = pass,
|
|
|
|
})
|
|
|
|
socketutil:reset_timeout()
|
2021-06-12 19:11:53 +00:00
|
|
|
if code ~= 200 then
|
|
|
|
logger.warn("WebDavApi: Download failure:", status or code or "network unreachable")
|
2022-09-16 22:08:00 +00:00
|
|
|
logger.dbg("WebDavApi: Response headers:", headers)
|
2021-06-12 19:11:53 +00:00
|
|
|
end
|
2022-11-11 14:53:06 +00:00
|
|
|
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 = {
|
2023-07-01 05:29:38 +00:00
|
|
|
["Content-Length"] = lfs.attributes(local_path, "size"),
|
|
|
|
["If-Match"] = etag,
|
2022-11-11 14:53:06 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
socketutil:reset_timeout()
|
2024-06-09 18:52:56 +00:00
|
|
|
if type(code) ~= "number" or code < 200 or code > 299 then
|
2022-11-11 14:53:06 +00:00
|
|
|
logger.warn("WebDavApi: upload failure:", status or code or "network unreachable")
|
|
|
|
end
|
2021-06-12 19:11:53 +00:00
|
|
|
return code
|
2018-10-17 12:13:59 +00:00
|
|
|
end
|
|
|
|
|
2022-11-11 14:53:06 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2018-10-17 12:13:59 +00:00
|
|
|
return WebDavApi
|