chore: init project

This commit is contained in:
Slany 2023-08-02 14:36:47 +02:00
commit 89f4dcc837
15 changed files with 7295 additions and 0 deletions

95
.gitignore vendored Normal file
View file

@ -0,0 +1,95 @@
# ---> C
# Prerequisites
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.map
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
# ---> C++
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Switch build
build/
*.nacp
*.nro
# IDE
.vscode/

24
LICENSE.txt Normal file
View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

222
Makefile Normal file
View file

@ -0,0 +1,222 @@
#---------------------------------------------------------------------------------
.SUFFIXES:
#---------------------------------------------------------------------------------
ifeq ($(strip $(DEVKITPRO)),)
$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>/devkitpro")
endif
TOPDIR ?= $(CURDIR)
include $(DEVKITPRO)/libnx/switch_rules
#---------------------------------------------------------------------------------
# TARGET is the name of the output
# BUILD is the directory where object files & intermediate files will be placed
# SOURCES is a list of directories containing source code
# DATA is a list of directories containing data files
# INCLUDES is a list of directories containing header files
# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional)
#
# NO_ICON: if set to anything, do not use icon.
# NO_NACP: if set to anything, no .nacp file is generated.
# APP_TITLE is the name of the app stored in the .nacp file (Optional)
# APP_AUTHOR is the author of the app stored in the .nacp file (Optional)
# APP_VERSION is the version of the app stored in the .nacp file (Optional)
# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional)
# ICON is the filename of the icon (.jpg), relative to the project folder.
# If not set, it attempts to use one of the following (in this order):
# - <Project name>.jpg
# - icon.jpg
# - <libnx folder>/default_icon.jpg
#
# CONFIG_JSON is the filename of the NPDM config file (.json), relative to the project folder.
# If not set, it attempts to use one of the following (in this order):
# - <Project name>.json
# - config.json
# If a JSON file is provided or autodetected, an ExeFS PFS0 (.nsp) is built instead
# of a homebrew executable (.nro). This is intended to be used for sysmodules.
# NACP building is skipped as well.
#---------------------------------------------------------------------------------
TARGET := $(notdir $(CURDIR))
BUILD := build
SOURCES := source include/inih include/inih/cpp include/tinyxml2
DATA := data
INCLUDES := include
#ROMFS := romfs
#---------------------------------------------------------------------------------
# options for code generation
#---------------------------------------------------------------------------------
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
CFLAGS := -g -Wall -O2 -ffunction-sections \
$(ARCH) $(DEFINES)
CFLAGS += $(INCLUDE) -D__SWITCH__
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions
ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LIBS := `curl-config --libs`
#---------------------------------------------------------------------------------
# list of directories containing libraries, this must be the top level containing
# include and lib
#---------------------------------------------------------------------------------
LIBDIRS := $(PORTLIBS) $(LIBNX)
#---------------------------------------------------------------------------------
# no real need to edit anything past this point unless you need to add additional
# rules for different file extensions
#---------------------------------------------------------------------------------
ifneq ($(BUILD),$(notdir $(CURDIR)))
#---------------------------------------------------------------------------------
export OUTPUT := $(CURDIR)/$(TARGET)
export TOPDIR := $(CURDIR)
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
$(foreach dir,$(DATA),$(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
#---------------------------------------------------------------------------------
# use CXX for linking C++ projects, CC for standard C
#---------------------------------------------------------------------------------
ifeq ($(strip $(CPPFILES)),)
#---------------------------------------------------------------------------------
export LD := $(CC)
#---------------------------------------------------------------------------------
else
#---------------------------------------------------------------------------------
export LD := $(CXX)
#---------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------
export OFILES_BIN := $(addsuffix .o,$(BINFILES))
export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
export OFILES := $(OFILES_BIN) $(OFILES_SRC)
export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES)))
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD)
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
ifeq ($(strip $(CONFIG_JSON)),)
jsons := $(wildcard *.json)
ifneq (,$(findstring $(TARGET).json,$(jsons)))
export APP_JSON := $(TOPDIR)/$(TARGET).json
else
ifneq (,$(findstring config.json,$(jsons)))
export APP_JSON := $(TOPDIR)/config.json
endif
endif
else
export APP_JSON := $(TOPDIR)/$(CONFIG_JSON)
endif
ifeq ($(strip $(ICON)),)
icons := $(wildcard *.jpg)
ifneq (,$(findstring $(TARGET).jpg,$(icons)))
export APP_ICON := $(TOPDIR)/$(TARGET).jpg
else
ifneq (,$(findstring icon.jpg,$(icons)))
export APP_ICON := $(TOPDIR)/icon.jpg
endif
endif
else
export APP_ICON := $(TOPDIR)/$(ICON)
endif
ifeq ($(strip $(NO_ICON)),)
export NROFLAGS += --icon=$(APP_ICON)
endif
ifeq ($(strip $(NO_NACP)),)
export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp
endif
ifneq ($(APP_TITLEID),)
export NACPFLAGS += --titleid=$(APP_TITLEID)
endif
ifneq ($(ROMFS),)
export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS)
endif
.PHONY: $(BUILD) clean all
#---------------------------------------------------------------------------------
all: $(BUILD)
$(BUILD):
@[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
#---------------------------------------------------------------------------------
clean:
@echo clean ...
ifeq ($(strip $(APP_JSON)),)
@rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf
else
@rm -fr $(BUILD) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf
endif
#---------------------------------------------------------------------------------
else
.PHONY: all
DEPENDS := $(OFILES:.o=.d)
#---------------------------------------------------------------------------------
# main targets
#---------------------------------------------------------------------------------
ifeq ($(strip $(APP_JSON)),)
all : $(OUTPUT).nro
ifeq ($(strip $(NO_NACP)),)
$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp
else
$(OUTPUT).nro : $(OUTPUT).elf
endif
else
all : $(OUTPUT).nsp
$(OUTPUT).nsp : $(OUTPUT).nso $(OUTPUT).npdm
$(OUTPUT).nso : $(OUTPUT).elf
endif
$(OUTPUT).elf : $(OFILES)
$(OFILES_SRC) : $(HFILES_BIN)
#---------------------------------------------------------------------------------
# you need a rule like this for each extension you use as binary data
#---------------------------------------------------------------------------------
%.bin.o %_bin.h : %.bin
#---------------------------------------------------------------------------------
@echo $(notdir $<)
@$(bin2o)
-include $(DEPENDS)
#---------------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------------

36
README.md Normal file
View file

@ -0,0 +1,36 @@
# NXDavSync: Sync local folders with WebDAV
Nintendo Switch port of [`3DavSync`](https://github.com/szclsya/3DavSync).
`NXDavSync` allows you to sync folders on the NSW SD card to a remote WebDAV server.
## Build
```
docker pull devkitpro/devkita64:latest
docker run --rm -v "$PWD":/app -w /app devkitpro/devkita64:latest make
```
## Configuration
NXDavSync accepts an ini-formatted config file at `/switch/NXDavSync.ini`. This file should look like this:
```ini
[General]
# List webdav configs that will be synced
Enabled=saves roms
# Example: Sync Checkpoint save folder with Nextcloud/ownCloud
[saves]
Url=https://example.org/remote.php/dav/files/username/saves
LocalPath=/switch/Checkpoint/saves
# Specify credential here
Username=REDACTED
Password=REDACTED
# Example: Sync roms
[roms]
Url=https://example.org/whatever/webdav
LocalPath=/roms
Username=REDACTED
Password=REDACTED
```

27
include/inih/LICENSE.txt Normal file
View file

@ -0,0 +1,27 @@
The "inih" library is distributed under the New BSD license:
Copyright (c) 2009, Ben Hoyt
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Ben Hoyt nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY BEN HOYT ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL BEN HOYT BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,116 @@
// Read an INI file into easy-to-access name/value pairs.
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (C) 2009-2020, Ben Hoyt
// inih and INIReader are released under the New BSD license (see LICENSE.txt).
// Go to the project home page for more info:
//
// https://github.com/benhoyt/inih
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include "../ini.h"
#include "INIReader.h"
using std::string;
INIReader::INIReader(const string& filename)
{
_error = ini_parse(filename.c_str(), ValueHandler, this);
}
INIReader::INIReader(const char *buffer, size_t buffer_size)
{
string content(buffer, buffer_size);
_error = ini_parse_string(content.c_str(), ValueHandler, this);
}
int INIReader::ParseError() const
{
return _error;
}
string INIReader::Get(const string& section, const string& name, const string& default_value) const
{
string key = MakeKey(section, name);
// Use _values.find() here instead of _values.at() to support pre C++11 compilers
return _values.count(key) ? _values.find(key)->second : default_value;
}
string INIReader::GetString(const string& section, const string& name, const string& default_value) const
{
const string str = Get(section, name, "");
return str.empty() ? default_value : str;
}
long INIReader::GetInteger(const string& section, const string& name, long default_value) const
{
string valstr = Get(section, name, "");
const char* value = valstr.c_str();
char* end;
// This parses "1234" (decimal) and also "0x4D2" (hex)
long n = strtol(value, &end, 0);
return end > value ? n : default_value;
}
double INIReader::GetReal(const string& section, const string& name, double default_value) const
{
string valstr = Get(section, name, "");
const char* value = valstr.c_str();
char* end;
double n = strtod(value, &end);
return end > value ? n : default_value;
}
bool INIReader::GetBoolean(const string& section, const string& name, bool default_value) const
{
string valstr = Get(section, name, "");
// Convert to lower case to make string comparisons case-insensitive
std::transform(valstr.begin(), valstr.end(), valstr.begin(), ::tolower);
if (valstr == "true" || valstr == "yes" || valstr == "on" || valstr == "1")
return true;
else if (valstr == "false" || valstr == "no" || valstr == "off" || valstr == "0")
return false;
else
return default_value;
}
bool INIReader::HasSection(const string& section) const
{
const string key = MakeKey(section, "");
std::map<string, string>::const_iterator pos = _values.lower_bound(key);
if (pos == _values.end())
return false;
// Does the key at the lower_bound pos start with "section"?
return pos->first.compare(0, key.length(), key) == 0;
}
bool INIReader::HasValue(const string& section, const string& name) const
{
string key = MakeKey(section, name);
return _values.count(key);
}
string INIReader::MakeKey(const string& section, const string& name)
{
string key = section + "=" + name;
// Convert to lower case to make section/name lookups case-insensitive
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
return key;
}
int INIReader::ValueHandler(void* user, const char* section, const char* name,
const char* value)
{
if (!name) // Happens when INI_CALL_HANDLER_ON_NEW_SECTION enabled
return 1;
INIReader* reader = static_cast<INIReader*>(user);
string key = MakeKey(section, name);
if (reader->_values[key].size() > 0)
reader->_values[key] += "\n";
reader->_values[key] += value ? value : "";
return 1;
}

View file

@ -0,0 +1,94 @@
// Read an INI file into easy-to-access name/value pairs.
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (C) 2009-2020, Ben Hoyt
// inih and INIReader are released under the New BSD license (see LICENSE.txt).
// Go to the project home page for more info:
//
// https://github.com/benhoyt/inih
#ifndef INIREADER_H
#define INIREADER_H
#include <map>
#include <string>
// Visibility symbols, required for Windows DLLs
#ifndef INI_API
#if defined _WIN32 || defined __CYGWIN__
# ifdef INI_SHARED_LIB
# ifdef INI_SHARED_LIB_BUILDING
# define INI_API __declspec(dllexport)
# else
# define INI_API __declspec(dllimport)
# endif
# else
# define INI_API
# endif
#else
# if defined(__GNUC__) && __GNUC__ >= 4
# define INI_API __attribute__ ((visibility ("default")))
# else
# define INI_API
# endif
#endif
#endif
// Read an INI file into easy-to-access name/value pairs. (Note that I've gone
// for simplicity here rather than speed, but it should be pretty decent.)
class INIReader
{
public:
// Construct INIReader and parse given filename. See ini.h for more info
// about the parsing.
INI_API explicit INIReader(const std::string& filename);
// Construct INIReader and parse given buffer. See ini.h for more info
// about the parsing.
INI_API explicit INIReader(const char *buffer, size_t buffer_size);
// Return the result of ini_parse(), i.e., 0 on success, line number of
// first error on parse error, or -1 on file open error.
INI_API int ParseError() const;
// Get a string value from INI file, returning default_value if not found.
INI_API std::string Get(const std::string& section, const std::string& name,
const std::string& default_value) const;
// Get a string value from INI file, returning default_value if not found,
// empty, or contains only whitespace.
INI_API std::string GetString(const std::string& section, const std::string& name,
const std::string& default_value) const;
// Get an integer (long) value from INI file, returning default_value if
// not found or not a valid integer (decimal "1234", "-1234", or hex "0x4d2").
INI_API long GetInteger(const std::string& section, const std::string& name, long default_value) const;
// Get a real (floating point double) value from INI file, returning
// default_value if not found or not a valid floating point value
// according to strtod().
INI_API double GetReal(const std::string& section, const std::string& name, double default_value) const;
// Get a boolean value from INI file, returning default_value if not found or if
// not a valid true/false value. Valid true values are "true", "yes", "on", "1",
// and valid false values are "false", "no", "off", "0" (not case sensitive).
INI_API bool GetBoolean(const std::string& section, const std::string& name, bool default_value) const;
// Return true if the given section exists (section must contain at least
// one name=value pair).
INI_API bool HasSection(const std::string& section) const;
// Return true if a value exists with the given section and field names.
INI_API bool HasValue(const std::string& section, const std::string& name) const;
private:
int _error;
std::map<std::string, std::string> _values;
static std::string MakeKey(const std::string& section, const std::string& name);
static int ValueHandler(void* user, const char* section, const char* name,
const char* value);
};
#endif // INIREADER_H

304
include/inih/ini.c Normal file
View file

@ -0,0 +1,304 @@
/* inih -- simple .INI file parser
SPDX-License-Identifier: BSD-3-Clause
Copyright (C) 2009-2020, Ben Hoyt
inih is released under the New BSD license (see LICENSE.txt). Go to the project
home page for more info:
https://github.com/benhoyt/inih
*/
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include "ini.h"
#if !INI_USE_STACK
#if INI_CUSTOM_ALLOCATOR
#include <stddef.h>
void* ini_malloc(size_t size);
void ini_free(void* ptr);
void* ini_realloc(void* ptr, size_t size);
#else
#include <stdlib.h>
#define ini_malloc malloc
#define ini_free free
#define ini_realloc realloc
#endif
#endif
#define MAX_SECTION 50
#define MAX_NAME 50
/* Used by ini_parse_string() to keep track of string parsing state. */
typedef struct {
const char* ptr;
size_t num_left;
} ini_parse_string_ctx;
/* Strip whitespace chars off end of given string, in place. Return s. */
static char* rstrip(char* s)
{
char* p = s + strlen(s);
while (p > s && isspace((unsigned char)(*--p)))
*p = '\0';
return s;
}
/* Return pointer to first non-whitespace char in given string. */
static char* lskip(const char* s)
{
while (*s && isspace((unsigned char)(*s)))
s++;
return (char*)s;
}
/* Return pointer to first char (of chars) or inline comment in given string,
or pointer to NUL at end of string if neither found. Inline comment must
be prefixed by a whitespace character to register as a comment. */
static char* find_chars_or_comment(const char* s, const char* chars)
{
#if INI_ALLOW_INLINE_COMMENTS
int was_space = 0;
while (*s && (!chars || !strchr(chars, *s)) &&
!(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
was_space = isspace((unsigned char)(*s));
s++;
}
#else
while (*s && (!chars || !strchr(chars, *s))) {
s++;
}
#endif
return (char*)s;
}
/* Similar to strncpy, but ensures dest (size bytes) is
NUL-terminated, and doesn't pad with NULs. */
static char* strncpy0(char* dest, const char* src, size_t size)
{
/* Could use strncpy internally, but it causes gcc warnings (see issue #91) */
size_t i;
for (i = 0; i < size - 1 && src[i]; i++)
dest[i] = src[i];
dest[i] = '\0';
return dest;
}
/* See documentation in header file. */
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
void* user)
{
/* Uses a fair bit of stack (use heap instead if you need to) */
#if INI_USE_STACK
char line[INI_MAX_LINE];
size_t max_line = INI_MAX_LINE;
#else
char* line;
size_t max_line = INI_INITIAL_ALLOC;
#endif
#if INI_ALLOW_REALLOC && !INI_USE_STACK
char* new_line;
size_t offset;
#endif
char section[MAX_SECTION] = "";
char prev_name[MAX_NAME] = "";
char* start;
char* end;
char* name;
char* value;
int lineno = 0;
int error = 0;
#if !INI_USE_STACK
line = (char*)ini_malloc(INI_INITIAL_ALLOC);
if (!line) {
return -2;
}
#endif
#if INI_HANDLER_LINENO
#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno)
#else
#define HANDLER(u, s, n, v) handler(u, s, n, v)
#endif
/* Scan through stream line by line */
while (reader(line, (int)max_line, stream) != NULL) {
#if INI_ALLOW_REALLOC && !INI_USE_STACK
offset = strlen(line);
while (offset == max_line - 1 && line[offset - 1] != '\n') {
max_line *= 2;
if (max_line > INI_MAX_LINE)
max_line = INI_MAX_LINE;
new_line = ini_realloc(line, max_line);
if (!new_line) {
ini_free(line);
return -2;
}
line = new_line;
if (reader(line + offset, (int)(max_line - offset), stream) == NULL)
break;
if (max_line >= INI_MAX_LINE)
break;
offset += strlen(line + offset);
}
#endif
lineno++;
start = line;
#if INI_ALLOW_BOM
if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
(unsigned char)start[1] == 0xBB &&
(unsigned char)start[2] == 0xBF) {
start += 3;
}
#endif
start = lskip(rstrip(start));
if (strchr(INI_START_COMMENT_PREFIXES, *start)) {
/* Start-of-line comment */
}
#if INI_ALLOW_MULTILINE
else if (*prev_name && *start && start > line) {
#if INI_ALLOW_INLINE_COMMENTS
end = find_chars_or_comment(start, NULL);
if (*end)
*end = '\0';
rstrip(start);
#endif
/* Non-blank line with leading whitespace, treat as continuation
of previous name's value (as per Python configparser). */
if (!HANDLER(user, section, prev_name, start) && !error)
error = lineno;
}
#endif
else if (*start == '[') {
/* A "[section]" line */
end = find_chars_or_comment(start + 1, "]");
if (*end == ']') {
*end = '\0';
strncpy0(section, start + 1, sizeof(section));
*prev_name = '\0';
#if INI_CALL_HANDLER_ON_NEW_SECTION
if (!HANDLER(user, section, NULL, NULL) && !error)
error = lineno;
#endif
}
else if (!error) {
/* No ']' found on section line */
error = lineno;
}
}
else if (*start) {
/* Not a comment, must be a name[=:]value pair */
end = find_chars_or_comment(start, "=:");
if (*end == '=' || *end == ':') {
*end = '\0';
name = rstrip(start);
value = end + 1;
#if INI_ALLOW_INLINE_COMMENTS
end = find_chars_or_comment(value, NULL);
if (*end)
*end = '\0';
#endif
value = lskip(value);
rstrip(value);
/* Valid name[=:]value pair found, call handler */
strncpy0(prev_name, name, sizeof(prev_name));
if (!HANDLER(user, section, name, value) && !error)
error = lineno;
}
else if (!error) {
/* No '=' or ':' found on name[=:]value line */
#if INI_ALLOW_NO_VALUE
*end = '\0';
name = rstrip(start);
if (!HANDLER(user, section, name, NULL) && !error)
error = lineno;
#else
error = lineno;
#endif
}
}
#if INI_STOP_ON_FIRST_ERROR
if (error)
break;
#endif
}
#if !INI_USE_STACK
ini_free(line);
#endif
return error;
}
/* See documentation in header file. */
int ini_parse_file(FILE* file, ini_handler handler, void* user)
{
return ini_parse_stream((ini_reader)fgets, file, handler, user);
}
/* See documentation in header file. */
int ini_parse(const char* filename, ini_handler handler, void* user)
{
FILE* file;
int error;
file = fopen(filename, "r");
if (!file)
return -1;
error = ini_parse_file(file, handler, user);
fclose(file);
return error;
}
/* An ini_reader function to read the next line from a string buffer. This
is the fgets() equivalent used by ini_parse_string(). */
static char* ini_reader_string(char* str, int num, void* stream) {
ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream;
const char* ctx_ptr = ctx->ptr;
size_t ctx_num_left = ctx->num_left;
char* strp = str;
char c;
if (ctx_num_left == 0 || num < 2)
return NULL;
while (num > 1 && ctx_num_left != 0) {
c = *ctx_ptr++;
ctx_num_left--;
*strp++ = c;
if (c == '\n')
break;
num--;
}
*strp = '\0';
ctx->ptr = ctx_ptr;
ctx->num_left = ctx_num_left;
return str;
}
/* See documentation in header file. */
int ini_parse_string(const char* string, ini_handler handler, void* user) {
ini_parse_string_ctx ctx;
ctx.ptr = string;
ctx.num_left = strlen(string);
return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler,
user);
}

178
include/inih/ini.h Normal file
View file

@ -0,0 +1,178 @@
/* inih -- simple .INI file parser
SPDX-License-Identifier: BSD-3-Clause
Copyright (C) 2009-2020, Ben Hoyt
inih is released under the New BSD license (see LICENSE.txt). Go to the project
home page for more info:
https://github.com/benhoyt/inih
*/
#ifndef INI_H
#define INI_H
/* Make this header file easier to include in C++ code */
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
/* Nonzero if ini_handler callback should accept lineno parameter. */
#ifndef INI_HANDLER_LINENO
#define INI_HANDLER_LINENO 0
#endif
/* Visibility symbols, required for Windows DLLs */
#ifndef INI_API
#if defined _WIN32 || defined __CYGWIN__
# ifdef INI_SHARED_LIB
# ifdef INI_SHARED_LIB_BUILDING
# define INI_API __declspec(dllexport)
# else
# define INI_API __declspec(dllimport)
# endif
# else
# define INI_API
# endif
#else
# if defined(__GNUC__) && __GNUC__ >= 4
# define INI_API __attribute__ ((visibility ("default")))
# else
# define INI_API
# endif
#endif
#endif
/* Typedef for prototype of handler function. */
#if INI_HANDLER_LINENO
typedef int (*ini_handler)(void* user, const char* section,
const char* name, const char* value,
int lineno);
#else
typedef int (*ini_handler)(void* user, const char* section,
const char* name, const char* value);
#endif
/* Typedef for prototype of fgets-style reader function. */
typedef char* (*ini_reader)(char* str, int num, void* stream);
/* Parse given INI-style file. May have [section]s, name=value pairs
(whitespace stripped), and comments starting with ';' (semicolon). Section
is "" if name=value pair parsed before any section heading. name:value
pairs are also supported as a concession to Python's configparser.
For each name=value pair parsed, call handler function with given user
pointer as well as section, name, and value (data only valid for duration
of handler call). Handler should return nonzero on success, zero on error.
Returns 0 on success, line number of first error on parse error (doesn't
stop on first error), -1 on file open error, or -2 on memory allocation
error (only when INI_USE_STACK is zero).
*/
INI_API int ini_parse(const char* filename, ini_handler handler, void* user);
/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
close the file when it's finished -- the caller must do that. */
INI_API int ini_parse_file(FILE* file, ini_handler handler, void* user);
/* Same as ini_parse(), but takes an ini_reader function pointer instead of
filename. Used for implementing custom or string-based I/O (see also
ini_parse_string). */
INI_API int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
void* user);
/* Same as ini_parse(), but takes a zero-terminated string with the INI data
instead of a file. Useful for parsing INI data from a network socket or
already in memory. */
INI_API int ini_parse_string(const char* string, ini_handler handler, void* user);
/* Nonzero to allow multi-line value parsing, in the style of Python's
configparser. If allowed, ini_parse() will call the handler with the same
name for each subsequent line parsed. */
#ifndef INI_ALLOW_MULTILINE
#define INI_ALLOW_MULTILINE 1
#endif
/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
the file. See https://github.com/benhoyt/inih/issues/21 */
#ifndef INI_ALLOW_BOM
#define INI_ALLOW_BOM 1
#endif
/* Chars that begin a start-of-line comment. Per Python configparser, allow
both ; and # comments at the start of a line by default. */
#ifndef INI_START_COMMENT_PREFIXES
#define INI_START_COMMENT_PREFIXES ";#"
#endif
/* Nonzero to allow inline comments (with valid inline comment characters
specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
Python 3.2+ configparser behaviour. */
#ifndef INI_ALLOW_INLINE_COMMENTS
#define INI_ALLOW_INLINE_COMMENTS 1
#endif
#ifndef INI_INLINE_COMMENT_PREFIXES
#define INI_INLINE_COMMENT_PREFIXES ";"
#endif
/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */
#ifndef INI_USE_STACK
#define INI_USE_STACK 1
#endif
/* Maximum line length for any line in INI file (stack or heap). Note that
this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */
#ifndef INI_MAX_LINE
#define INI_MAX_LINE 200
#endif
/* Nonzero to allow heap line buffer to grow via realloc(), zero for a
fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is
zero. */
#ifndef INI_ALLOW_REALLOC
#define INI_ALLOW_REALLOC 0
#endif
/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK
is zero. */
#ifndef INI_INITIAL_ALLOC
#define INI_INITIAL_ALLOC 200
#endif
/* Stop parsing on first error (default is to keep parsing). */
#ifndef INI_STOP_ON_FIRST_ERROR
#define INI_STOP_ON_FIRST_ERROR 0
#endif
/* Nonzero to call the handler at the start of each new section (with
name and value NULL). Default is to only call the handler on
each name=value pair. */
#ifndef INI_CALL_HANDLER_ON_NEW_SECTION
#define INI_CALL_HANDLER_ON_NEW_SECTION 0
#endif
/* Nonzero to allow a name without a value (no '=' or ':' on the line) and
call the handler with value NULL in this case. Default is to treat
no-value lines as an error. */
#ifndef INI_ALLOW_NO_VALUE
#define INI_ALLOW_NO_VALUE 0
#endif
/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory
allocation functions (INI_USE_STACK must also be 0). These functions must
have the same signatures as malloc/free/realloc and behave in a similar
way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */
#ifndef INI_CUSTOM_ALLOCATOR
#define INI_CUSTOM_ALLOCATOR 0
#endif
#ifdef __cplusplus
}
#endif
#endif /* INI_H */

View file

@ -0,0 +1,18 @@
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any
damages arising from the use of this software.
Permission is granted to anyone to use this software for any
purpose, including commercial applications, and to alter it and
redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must
not claim that you wrote the original software. If you use this
software in a product, an acknowledgment in the product documentation
would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and
must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.

File diff suppressed because it is too large Load diff

2380
include/tinyxml2/tinyxml2.h Normal file

File diff suppressed because it is too large Load diff

144
source/main.cpp Normal file
View file

@ -0,0 +1,144 @@
// Include the most common headers from the C standard library
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <sstream>
#include <iostream>
#include <unistd.h>
// Include the main libnx system header, for Switch development
#include <switch.h>
// Include custom webdav libs
#include "webdav.hpp"
#include <inih/cpp/INIReader.h>
using namespace std;
// Main program entrypoint
int main(int argc, char *argv[])
{
// This example uses a text console, as a simple way to output text to the screen.
// If you want to write a software-rendered graphics application,
// take a look at the graphics/simplegfx example, which uses the libnx Framebuffer API instead.
// If on the other hand you want to write an OpenGL based application,
// take a look at the graphics/opengl set of examples, which uses EGL instead.
consoleInit(NULL);
// Configure our supported input layout: a single player with standard controller styles
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
// Initialize the default gamepad (which reads handheld mode inputs as well as the first connected controller)
PadState pad;
padInitializeDefault(&pad);
socketInitializeDefault();
printf(CONSOLE_BLUE "Switch Nexcloud\n\nPress A to start.\n" CONSOLE_RESET);
while (appletMainLoop())
{
padUpdate(&pad);
u64 kDown = padGetButtonsDown(&pad);
if (kDown & HidNpadButton_A)
{
break;
}
consoleUpdate(NULL);
}
INIReader reader("/switch/NXDavSync.ini");
vector<string> bad_config;
vector<pair<string, WebDavClient *>> clients;
if (reader.ParseError() < 0)
{
printf("Error loading configuration file at /switch/NXDavSync.ini");
consoleUpdate(NULL);
}
else
{
string enabled = reader.Get("General", "Enabled", "");
string buf;
stringstream ss(enabled);
while (ss >> buf)
{
if (buf == "")
{
break;
}
// Load server config
string url = reader.Get(buf, "Url", "");
string local_path = reader.Get(buf, "LocalPath", "");
string username = reader.Get(buf, "Username", "");
string password = reader.Get(buf, "Password", "");
if (url.size() == 0 || local_path.size() == 0)
{
bad_config.push_back(buf);
}
else
{
WebDavClient *c = new WebDavClient(url, local_path);
if (username.size() != 0)
{
c->set_basic_auth(username, password);
}
clients.push_back(make_pair(buf, c));
}
}
}
if (!bad_config.empty())
{
for (auto name : bad_config)
{
printf("Malformed config for server %s", name.c_str());
consoleUpdate(NULL);
}
}
else
{
vector<pair<string, bool>> results;
for (auto [name, client] : clients)
{
printf(CONSOLE_BLUE "\nSyncing %s\n", name.c_str());
printf(CONSOLE_RESET);
consoleUpdate(NULL);
bool this_result = client->compareAndUpdate();
results.push_back(make_pair(name, this_result));
}
// Print summary
printf(CONSOLE_BLUE "\n\n===== Sync Summary =====\n\n" CONSOLE_RESET);
consoleUpdate(NULL);
for (auto [name, result] : results)
{
if (result)
{
printf(CONSOLE_GREEN "SUCCESS " CONSOLE_RESET);
}
else
{
printf(CONSOLE_RED "FAILED " CONSOLE_RESET);
}
printf("%s\n", name.c_str());
consoleUpdate(NULL);
}
}
printf(CONSOLE_GREEN "\n Press A to exit.\n" CONSOLE_RESET);
consoleUpdate(NULL);
while (appletMainLoop())
{
padUpdate(&pad);
u64 kDown = padGetButtonsDown(&pad);
if (kDown & HidNpadButton_A)
{
break;
}
consoleUpdate(NULL);
}
socketExit();
// Deinitialize and clean up resources used by the console (important!)
consoleExit(NULL);
return 0;
}

624
source/webdav.cpp Normal file
View file

@ -0,0 +1,624 @@
#include "webdav.hpp"
#include <sys/stat.h>
#include <dirent.h>
#include <regex>
#include "curl/curl.h"
#include "curl/easy.h"
#include <tinyxml2/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();
}
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 user_confirm()
{
PadState pad;
while (appletMainLoop())
{
padUpdate(&pad);
u32 kDown = padGetButtonsDown(&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 (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 (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;
}

47
source/webdav.hpp Normal file
View file

@ -0,0 +1,47 @@
#pragma once
#include <string>
#include <optional>
#include <ctime>
#include <vector>
#include <switch.h>
#include <curl/curl.h>
struct FileEntry
{
std::string path;
time_t last_modified;
bool folder;
int size;
};
class WebDavClient
{
public:
WebDavClient(std::string web_root, std::string local_root);
~WebDavClient();
/// Configure this instance to use HTTP simple auth
void set_basic_auth(std::string username, std::string password);
/// Make a directory on the remote server
bool mkcol(std::string web_path_rel, std::optional<u64> mtime);
/// Push a file to the remote WebDAV collection
bool push(std::string path, std::string web_path_rel);
/// Pull a file from remote WebDAV collection
bool pull(std::string path, std::string web_path_rel);
/// Get a list of remote files
std::optional<std::vector<FileEntry>> get_remote_files();
/// compare the local file with the remote file specified by web_path_rel
/// if local is newer, upload. if remote newer, pull and overwrite
/// the remote path will be appended to web_root
bool compareAndUpdate();
private:
CURL *curl;
std::string web_root; // Base URL
std::string local_root;
bool use_basic_auth;
std::string username;
std::string password;
void reset();
};