628 lines
20 KiB
C++
628 lines
20 KiB
C++
#include "webdav.hpp"
|
|
|
|
#include <sys/stat.h>
|
|
#include <dirent.h>
|
|
#include <regex>
|
|
#include "curl/curl.h"
|
|
#include "curl/easy.h"
|
|
#include <tinyxml2.h>
|
|
|
|
using namespace std;
|
|
using namespace tinyxml2;
|
|
|
|
WebDavClient::WebDavClient(string w, string l) : web_root(w), local_root(l)
|
|
{
|
|
curl = curl_easy_init();
|
|
reset();
|
|
}
|
|
|
|
WebDavClient::~WebDavClient()
|
|
{
|
|
curl_easy_cleanup(this->curl);
|
|
}
|
|
|
|
void WebDavClient::reset()
|
|
{
|
|
curl_easy_reset(this->curl);
|
|
curl_easy_setopt(this->curl, CURLOPT_USERAGENT, "3DavSync 0.1.0");
|
|
curl_easy_setopt(this->curl, CURLOPT_CONNECTTIMEOUT, 50L);
|
|
curl_easy_setopt(this->curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
curl_easy_setopt(this->curl, CURLOPT_FAILONERROR, 1L);
|
|
curl_easy_setopt(this->curl, CURLOPT_NOPROGRESS, 1L);
|
|
curl_easy_setopt(this->curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
|
curl_easy_setopt(this->curl, CURLOPT_PIPEWAIT, 1L);
|
|
// curl_easy_setopt(this->curl, CURLOPT_VERBOSE, 1L);
|
|
if (this->use_basic_auth)
|
|
{
|
|
curl_easy_setopt(this->curl, CURLOPT_USERNAME, this->username.c_str());
|
|
curl_easy_setopt(this->curl, CURLOPT_PASSWORD, this->password.c_str());
|
|
curl_easy_setopt(this->curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
|
}
|
|
}
|
|
|
|
void WebDavClient::set_basic_auth(std::string username, std::string password)
|
|
{
|
|
this->username = username;
|
|
this->password = password;
|
|
this->use_basic_auth = true;
|
|
this->reset();
|
|
}
|
|
|
|
void WebDavClient::set_pad_state(PadState *pad)
|
|
{
|
|
this->pad = pad;
|
|
}
|
|
|
|
string formulate_actual_url(string &root, string &rel_path)
|
|
{
|
|
if (!rel_path.empty())
|
|
{
|
|
string escaped_string = curl_easy_escape(NULL, rel_path.c_str(), 0);
|
|
string escaped_string_with_slash = regex_replace(escaped_string, regex("%2F"), "/");
|
|
string res = root + escaped_string_with_slash;
|
|
return res;
|
|
}
|
|
else
|
|
{
|
|
return root;
|
|
}
|
|
}
|
|
|
|
bool WebDavClient::mkcol(string web_rel_path, optional<u64> mtime)
|
|
{
|
|
string actual_url = formulate_actual_url(this->web_root, web_rel_path);
|
|
// Test if directory existed already
|
|
curl_easy_setopt(this->curl, CURLOPT_URL, actual_url.c_str());
|
|
curl_easy_setopt(this->curl, CURLOPT_NOBODY, 1L);
|
|
CURLcode head_res = curl_easy_perform(this->curl);
|
|
if (head_res == CURLE_OK)
|
|
{
|
|
this->reset();
|
|
return true;
|
|
}
|
|
|
|
this->reset();
|
|
curl_easy_setopt(this->curl, CURLOPT_CUSTOMREQUEST, "MKCOL");
|
|
curl_easy_setopt(this->curl, CURLOPT_URL, actual_url.c_str());
|
|
if (mtime)
|
|
{
|
|
u64 actual_mtime = mtime.value();
|
|
struct curl_slist *list = NULL;
|
|
string oc_mtime = string("X-OC-Mtime: " + to_string(actual_mtime)).c_str();
|
|
list = curl_slist_append(list, oc_mtime.c_str());
|
|
curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, list);
|
|
}
|
|
|
|
CURLcode curl_res = curl_easy_perform(this->curl);
|
|
if (curl_res != CURLE_OK)
|
|
{
|
|
long response_code;
|
|
curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code);
|
|
printf("curl MKCOL failed: %s (%d)\n", curl_easy_strerror(curl_res), curl_res);
|
|
printf("Location: %s\n", actual_url.c_str());
|
|
printf("Response code: %ld\n", response_code);
|
|
consoleUpdate(NULL);
|
|
this->reset();
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
this->reset();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool WebDavClient::pull(string path, string web_rel_path)
|
|
{
|
|
const char *c_path = path.c_str();
|
|
if (this->curl)
|
|
{
|
|
// Formulate and fill actual url
|
|
string actual_url = formulate_actual_url(this->web_root, web_rel_path);
|
|
curl_easy_setopt(curl, CURLOPT_URL, actual_url.c_str());
|
|
|
|
// Open a file at that path, overwrite if exists
|
|
FILE *fp = fopen(c_path, "w");
|
|
if (fp)
|
|
{
|
|
curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, fp);
|
|
CURLcode res = curl_easy_perform(curl);
|
|
fclose(fp);
|
|
if (res != 0)
|
|
{
|
|
long response_code;
|
|
curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code);
|
|
printf("error getting file %s: %s\n", c_path, curl_easy_strerror(res));
|
|
printf("Location: %s\n", actual_url.c_str());
|
|
printf("Response code: %ld\n", response_code);
|
|
consoleUpdate(NULL);
|
|
this->reset();
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
printf("can't open %s for writing: %s\n", c_path, strerror(errno));
|
|
consoleUpdate(NULL);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
printf("can't initalize curl!\n");
|
|
consoleUpdate(NULL);
|
|
}
|
|
this->reset();
|
|
return true;
|
|
}
|
|
|
|
static int seek_helper(void *userp, curl_off_t offset, int origin)
|
|
{
|
|
FILE *fp = (FILE *)userp;
|
|
|
|
if (-1 == fseek(fp, (long)offset, origin))
|
|
/* couldn't seek */
|
|
return CURL_SEEKFUNC_CANTSEEK;
|
|
|
|
return CURL_SEEKFUNC_OK; /* success! */
|
|
}
|
|
|
|
bool WebDavClient::push(string path, string web_rel_path)
|
|
{
|
|
const char *c_path = path.c_str();
|
|
if (this->curl)
|
|
{
|
|
// Formulate and fill actual url
|
|
string actual_url = formulate_actual_url(this->web_root, web_rel_path);
|
|
curl_easy_setopt(curl, CURLOPT_URL, actual_url.c_str());
|
|
|
|
// Open a file at that path to read
|
|
FILE *fp = fopen(c_path, "r");
|
|
if (fp)
|
|
{
|
|
// Read file metadata
|
|
struct stat file_info;
|
|
fstat(fileno(fp), &file_info);
|
|
// Set curl reading stuff
|
|
curl_easy_setopt(this->curl, CURLOPT_READDATA, fp);
|
|
curl_easy_setopt(this->curl, CURLOPT_SEEKFUNCTION, seek_helper);
|
|
// Prepare the headers
|
|
// For Nextcloud/ownCloud, we can ask the server to use our mtime
|
|
u64 mtime;
|
|
struct stat attr;
|
|
stat(path.c_str(), &attr);
|
|
mtime = attr.st_mtime;
|
|
|
|
struct curl_slist *list = NULL;
|
|
string t = string("X-OC-Mtime: " + to_string(mtime)).c_str();
|
|
list = curl_slist_append(list, t.c_str());
|
|
curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, list);
|
|
// We are uploading!
|
|
curl_easy_setopt(this->curl, CURLOPT_UPLOAD, 1L);
|
|
curl_easy_setopt(this->curl, CURLOPT_INFILESIZE, (curl_off_t)file_info.st_size);
|
|
CURLcode res = curl_easy_perform(curl);
|
|
fclose(fp);
|
|
if (res != CURLE_OK)
|
|
{
|
|
long response_code;
|
|
curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code);
|
|
printf("error pushing file %s: %s\n", c_path, curl_easy_strerror(res));
|
|
printf("Location: %s\n", actual_url.c_str());
|
|
printf("Response code: %ld\n", response_code);
|
|
consoleUpdate(NULL);
|
|
this->reset();
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
printf("can't open %s for writing: %s\n", c_path, strerror(errno));
|
|
consoleUpdate(NULL);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
printf("can't initalize curl!\n");
|
|
consoleUpdate(NULL);
|
|
}
|
|
|
|
this->reset();
|
|
return true;
|
|
}
|
|
|
|
/// Write the curl response to a std::string
|
|
size_t curl_write_to_string(void *ptr, size_t size, size_t nmemb,
|
|
string *s)
|
|
{
|
|
s->append(static_cast<char *>(ptr), size * nmemb);
|
|
return size * nmemb;
|
|
}
|
|
|
|
optional<vector<FileEntry>> normalize_filelist(optional<vector<FileEntry>> i)
|
|
{
|
|
if (i)
|
|
{
|
|
auto files = i.value();
|
|
if (files.empty())
|
|
{
|
|
return nullopt;
|
|
}
|
|
else if (files.size() == 1)
|
|
{
|
|
// Only contains the root directory, we don't care about that
|
|
files.erase(files.begin());
|
|
}
|
|
else if (files.size() > 1)
|
|
{
|
|
// Seems to be a query on collection
|
|
// Get root first
|
|
if (files[0].path.back() == '/')
|
|
{
|
|
string root = files[0].path.c_str();
|
|
root.pop_back();
|
|
for (FileEntry &file : files)
|
|
{
|
|
file.path.erase(0, root.length());
|
|
}
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
else
|
|
{
|
|
return nullopt;
|
|
}
|
|
}
|
|
|
|
const char *query = R"(<?xml version="1.0"?>
|
|
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
|
|
<d:prop>
|
|
<d:getlastmodified />
|
|
<d:getcontentlength />
|
|
<d:resourcetype />
|
|
<oc:checksums />
|
|
</d:prop>
|
|
</d:propfind>)";
|
|
|
|
/// Read the timestamp from WebDAV server
|
|
optional<vector<FileEntry>> WebDavClient::get_remote_files()
|
|
{
|
|
// Use PROPFIND to fetch file metadata
|
|
if (this->curl)
|
|
{
|
|
string response = "";
|
|
// Formulate and fill url
|
|
curl_easy_setopt(curl, CURLOPT_URL, this->web_root.c_str());
|
|
// Read XML content
|
|
curl_easy_setopt(this->curl, CURLOPT_CUSTOMREQUEST, "PROPFIND");
|
|
curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, curl_write_to_string);
|
|
curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, &response);
|
|
curl_easy_setopt(this->curl, CURLOPT_POSTFIELDS, query);
|
|
struct curl_slist *list = NULL;
|
|
list = curl_slist_append(list, "Depth: infinity");
|
|
curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, list);
|
|
|
|
CURLcode curl_res = curl_easy_perform(this->curl);
|
|
if (curl_res != CURLE_OK)
|
|
{
|
|
long response_code;
|
|
curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code);
|
|
printf("curl PROPFIND failed: %s (%d)\n", curl_easy_strerror(curl_res), curl_res);
|
|
printf("Response code: %ld\n", response_code);
|
|
consoleUpdate(NULL);
|
|
this->reset();
|
|
return nullopt;
|
|
}
|
|
this->reset();
|
|
|
|
// Now read it!
|
|
XMLDocument doc;
|
|
XMLError parse_res = doc.Parse(response.c_str());
|
|
|
|
vector<FileEntry> res;
|
|
if (parse_res == tinyxml2::XML_SUCCESS)
|
|
{
|
|
XMLElement *root = doc.RootElement();
|
|
for (XMLElement *e = root->FirstChildElement(); e != NULL; e = e->NextSiblingElement())
|
|
{
|
|
string path;
|
|
time_t time;
|
|
bool folder = false;
|
|
int size;
|
|
// Get file path
|
|
if (e->FirstChildElement("d:href"))
|
|
{
|
|
const char *text = e->FirstChildElement("d:href")->GetText();
|
|
// The path here is escaped. Convert them back to unescaped form
|
|
char *decoded = curl_easy_unescape(this->curl, text, 0, NULL);
|
|
path = string(decoded);
|
|
}
|
|
else
|
|
{
|
|
printf("malformed WebDAV response: missing d:href in PROPFIND\n");
|
|
consoleUpdate(NULL);
|
|
return nullopt;
|
|
}
|
|
if (e->FirstChildElement("d:propstat"))
|
|
{
|
|
if (e->FirstChildElement("d:propstat")->FirstChildElement("d:prop"))
|
|
{
|
|
XMLElement *prop = e->FirstChildElement("d:propstat")->FirstChildElement("d:prop");
|
|
if (prop->FirstChildElement("d:getlastmodified"))
|
|
{
|
|
string time_str = prop->FirstChildElement("d:getlastmodified")->GetText();
|
|
time = curl_getdate(time_str.c_str(), NULL);
|
|
}
|
|
else
|
|
{
|
|
printf("malformed WebDAV response: missing d:getlastmodified in PROPFIND\n");
|
|
consoleUpdate(NULL);
|
|
return nullopt;
|
|
}
|
|
if (prop->FirstChildElement("d:getcontentlength"))
|
|
{
|
|
string size_str = prop->FirstChildElement("d:getcontentlength")->GetText();
|
|
size = stoi(size_str);
|
|
}
|
|
else
|
|
{
|
|
size = 0;
|
|
}
|
|
if (prop->FirstChildElement("d:resourcetype") && prop->FirstChildElement("d:resourcetype")->FirstChildElement("d:collection"))
|
|
{
|
|
folder = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
printf("malformed WebDAV response: missing d:prop in PROPFIND\n");
|
|
consoleUpdate(NULL);
|
|
return nullopt;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
printf("malformed WebDAV response: missing d:propstat in PROPFIND\n");
|
|
consoleUpdate(NULL);
|
|
return nullopt;
|
|
}
|
|
|
|
res.push_back(FileEntry{path, time, folder, size});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
printf("malformed XML response: %d\n", parse_res);
|
|
consoleUpdate(NULL);
|
|
return nullopt;
|
|
}
|
|
// Turn file paths into canonical form
|
|
auto out = normalize_filelist(res);
|
|
return out;
|
|
}
|
|
else
|
|
{
|
|
printf("can't initalize curl!\n");
|
|
consoleUpdate(NULL);
|
|
return nullopt;
|
|
}
|
|
}
|
|
|
|
optional<FileEntry> get_file(vector<FileEntry> *files, string path)
|
|
{
|
|
for (unsigned i = 0; i < files->size(); i++)
|
|
{
|
|
if ((*files)[i].path == path)
|
|
{
|
|
FileEntry f = (*files)[i];
|
|
files->erase(files->begin() + i);
|
|
return f;
|
|
}
|
|
}
|
|
return nullopt;
|
|
}
|
|
|
|
vector<pair<string, bool>> recursively_get_dir(string base_path, string ext_path = "")
|
|
{
|
|
vector<pair<string, bool>> paths;
|
|
|
|
DIR *dir;
|
|
struct dirent *ent;
|
|
string path = base_path + ext_path;
|
|
if ((dir = opendir(path.c_str())) != NULL)
|
|
{
|
|
paths.push_back(pair(ext_path + "/", true));
|
|
while ((ent = readdir(dir)) != NULL)
|
|
{
|
|
vector<pair<string, bool>> subdir = recursively_get_dir(base_path, ext_path + "/" + ent->d_name);
|
|
paths.insert(paths.end(), subdir.begin(), subdir.end());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (ext_path != "")
|
|
{
|
|
paths.push_back(make_pair(ext_path, false));
|
|
}
|
|
else
|
|
{
|
|
printf("directory %s not found\n", path.c_str());
|
|
consoleUpdate(NULL);
|
|
}
|
|
}
|
|
closedir(dir);
|
|
return paths;
|
|
}
|
|
|
|
bool WebDavClient::user_confirm()
|
|
{
|
|
while (appletMainLoop())
|
|
{
|
|
padUpdate(this->pad);
|
|
u32 kDown = padGetButtonsDown(this->pad);
|
|
if (kDown & HidNpadButton_A)
|
|
{
|
|
return true;
|
|
}
|
|
else if (kDown & HidNpadButton_B)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool WebDavClient::compareAndUpdate()
|
|
{
|
|
bool success = true;
|
|
|
|
struct stat rootstat;
|
|
if (!stat(this->local_root.c_str(), &rootstat))
|
|
{
|
|
// Create directory
|
|
mkdir(this->local_root.c_str(), 0777);
|
|
}
|
|
else if (!S_ISDIR(rootstat.st_mode))
|
|
{
|
|
printf(CONSOLE_RED "specified local dir is not a dir!" CONSOLE_RESET);
|
|
consoleUpdate(NULL);
|
|
return false;
|
|
}
|
|
|
|
this->mkcol("", nullopt);
|
|
|
|
optional<vector<FileEntry>> remote_files_optional = this->get_remote_files();
|
|
if (!remote_files_optional)
|
|
{
|
|
printf("failed to fetch remote file list\n");
|
|
consoleUpdate(NULL);
|
|
return false;
|
|
}
|
|
|
|
vector<FileEntry> remote_files = remote_files_optional.value();
|
|
vector<pair<string, bool>> local_files = recursively_get_dir(this->local_root);
|
|
for (auto [path, is_dir] : local_files)
|
|
{
|
|
string local_real_path = this->local_root + path;
|
|
time_t local_mtime;
|
|
struct stat attr;
|
|
stat(local_real_path.c_str(), &attr);
|
|
local_mtime = attr.st_mtime;
|
|
// fsdev_getmtime(local_real_path.c_str(), &local_mtime);
|
|
|
|
optional<FileEntry> remote_file_optional = get_file(&remote_files, path);
|
|
if (remote_file_optional)
|
|
{
|
|
FileEntry remote_file = remote_file_optional.value();
|
|
|
|
if (is_dir)
|
|
{
|
|
// Local && Remote directory exists, we are fine
|
|
}
|
|
else if (local_mtime > remote_file.last_modified)
|
|
{
|
|
// Ask if we should do an upload
|
|
printf("\n%s", path.c_str());
|
|
printf(CONSOLE_YELLOW "Local version newer on above file.\n" CONSOLE_RESET);
|
|
printf(CONSOLE_YELLOW "Upload (A) or Not (B)?\n" CONSOLE_RESET);
|
|
consoleUpdate(NULL);
|
|
if (this->user_confirm())
|
|
{
|
|
// Upload local version
|
|
printf("%s: local modified, uploading...\n\n", path.c_str());
|
|
consoleUpdate(NULL);
|
|
if (!this->push(local_real_path, path))
|
|
{
|
|
printf(CONSOLE_RED "%s: local modified, upload failed.\n", path.c_str());
|
|
printf(CONSOLE_RESET);
|
|
consoleUpdate(NULL);
|
|
success = false;
|
|
}
|
|
}
|
|
}
|
|
else if (local_mtime < remote_file.last_modified)
|
|
{
|
|
// Ask if we should do an upload
|
|
printf("\n%s", path.c_str());
|
|
printf(CONSOLE_YELLOW "Local version older on above file.\n" CONSOLE_RESET);
|
|
printf(CONSOLE_YELLOW "Download (A) or Not (B)?\n" CONSOLE_RESET);
|
|
consoleUpdate(NULL);
|
|
// Pull remote version
|
|
if (this->user_confirm())
|
|
{
|
|
printf("%s: remote modified, downloading...\n\n", remote_file.path.c_str());
|
|
consoleUpdate(NULL);
|
|
if (!this->pull(local_real_path, remote_file.path))
|
|
{
|
|
printf(CONSOLE_RED "%s: remote modified, download failed.\n", path.c_str());
|
|
printf(CONSOLE_RESET);
|
|
consoleUpdate(NULL);
|
|
success = false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Identical, don't do anything
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Remote file DNE, upload local version
|
|
if (is_dir)
|
|
{
|
|
this->mkcol(path, local_mtime);
|
|
}
|
|
else
|
|
{
|
|
printf("%s: new local file, uploading...\n\n", path.c_str());
|
|
consoleUpdate(NULL);
|
|
if (!this->push(local_real_path, path))
|
|
{
|
|
printf(CONSOLE_RED "%s: upload failed.\n\n", path.c_str());
|
|
printf(CONSOLE_RESET);
|
|
consoleUpdate(NULL);
|
|
success = false;
|
|
}
|
|
else
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Pull the remaining remote files
|
|
for (FileEntry remote_file : remote_files)
|
|
{
|
|
string real_local_path = this->local_root + remote_file.path;
|
|
if (remote_file.path == "/")
|
|
{
|
|
continue;
|
|
}
|
|
else if (remote_file.folder)
|
|
{
|
|
int status = mkdir(real_local_path.c_str(), 0777);
|
|
if (status != 0)
|
|
{
|
|
printf("can't create local dir %s: %s\n", real_local_path.c_str(), strerror(errno));
|
|
consoleUpdate(NULL);
|
|
success = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// It's a file
|
|
|
|
printf("%s: new remote file, downloading...\n\n", remote_file.path.c_str());
|
|
consoleUpdate(NULL);
|
|
if (!this->pull(real_local_path, remote_file.path))
|
|
{
|
|
printf("can't download remote file %s\n", remote_file.path.c_str());
|
|
consoleUpdate(NULL);
|
|
success = false;
|
|
}
|
|
else
|
|
{
|
|
}
|
|
}
|
|
}
|
|
return success;
|
|
} |