1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2025-12-15 17:43:51 +01:00

Merge pull request #18133 from fabian18/gcoap_fileserver_file_and_directory_creation

gcoap/fileserver: add file and directory creation and deletion
This commit is contained in:
benpicco 2022-08-04 16:14:29 +02:00 committed by GitHub
commit c125e3d98a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 646 additions and 150 deletions

View File

@ -14281,6 +14281,8 @@ sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_BLOCK1 \(macro definiti
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_BLOCK2 \(macro definition\) of group net_coap is not documented\. sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_BLOCK2 \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_CONTENT_FORMAT \(macro definition\) of group net_coap is not documented\. sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_CONTENT_FORMAT \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_ETAG \(macro definition\) of group net_coap is not documented\. sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_ETAG \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_IF_MATCH \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_IF_NONE_MATCH \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_MAX_AGE \(macro definition\) of group net_coap is not documented\. sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_MAX_AGE \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_LOCATION_PATH \(macro definition\) of group net_coap is not documented\. sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_LOCATION_PATH \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_LOCATION_QUERY \(macro definition\) of group net_coap is not documented\. sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_LOCATION_QUERY \(macro definition\) of group net_coap is not documented\.

View File

@ -21,6 +21,8 @@ USEMODULE += shell_commands
# enable the fileserver module # enable the fileserver module
USEMODULE += gcoap_fileserver USEMODULE += gcoap_fileserver
USEMODULE += gcoap_fileserver_delete
USEMODULE += gcoap_fileserver_put
# select network modules # select network modules
USEMODULE += gnrc_ipv6_default USEMODULE += gnrc_ipv6_default

View File

@ -17,6 +17,7 @@
* @} * @}
*/ */
#include "kernel_defines.h"
#include "net/gcoap.h" #include "net/gcoap.h"
#include "net/gcoap/fileserver.h" #include "net/gcoap/fileserver.h"
#include "shell.h" #include "shell.h"
@ -27,7 +28,16 @@ static msg_t _main_msg_queue[MAIN_QUEUE_SIZE];
/* CoAP resources. Must be sorted by path (ASCII order). */ /* CoAP resources. Must be sorted by path (ASCII order). */
static const coap_resource_t _resources[] = { static const coap_resource_t _resources[] = {
{ "/vfs", COAP_GET | COAP_MATCH_SUBTREE, gcoap_fileserver_handler, VFS_DEFAULT_DATA }, { "/vfs",
COAP_GET |
#if IS_USED(MODULE_GCOAP_FILESERVER_PUT)
COAP_PUT |
#endif
#if IS_USED(MODULE_GCOAP_FILESERVER_DELETE)
COAP_DELETE |
#endif
COAP_MATCH_SUBTREE,
gcoap_fileserver_handler, VFS_DEFAULT_DATA },
}; };
static gcoap_listener_t _listener = { static gcoap_listener_t _listener = {

View File

@ -67,6 +67,8 @@ PSEUDOMODULES += fatfs_vfs_format
PSEUDOMODULES += fmt_% PSEUDOMODULES += fmt_%
PSEUDOMODULES += gcoap_forward_proxy PSEUDOMODULES += gcoap_forward_proxy
PSEUDOMODULES += gcoap_fileserver PSEUDOMODULES += gcoap_fileserver
PSEUDOMODULES += gcoap_fileserver_delete
PSEUDOMODULES += gcoap_fileserver_put
PSEUDOMODULES += gcoap_dtls PSEUDOMODULES += gcoap_dtls
## Enable @ref net_gcoap_dns ## Enable @ref net_gcoap_dns
PSEUDOMODULES += gcoap_dns PSEUDOMODULES += gcoap_dns

View File

@ -656,6 +656,15 @@ ifneq (,$(filter gcoap_fileserver,$(USEMODULE)))
USEMODULE += vfs USEMODULE += vfs
endif endif
ifneq (,$(filter gcoap_fileserver_delete,$(USEMODULE)))
USEMODULE += gcoap_fileserver
USEMODULE += vfs_util
endif
ifneq (,$(filter gcoap_fileserver_put,$(USEMODULE)))
USEMODULE += gcoap_fileserver
endif
ifneq (,$(filter gcoap_forward_proxy,$(USEMODULE))) ifneq (,$(filter gcoap_forward_proxy,$(USEMODULE)))
USEMODULE += gcoap USEMODULE += gcoap
USEMODULE += uri_parser USEMODULE += uri_parser

View File

@ -35,8 +35,10 @@ extern "C" {
* @name CoAP option numbers * @name CoAP option numbers
* @{ * @{
*/ */
#define COAP_OPT_IF_MATCH (1)
#define COAP_OPT_URI_HOST (3) #define COAP_OPT_URI_HOST (3)
#define COAP_OPT_ETAG (4) #define COAP_OPT_ETAG (4)
#define COAP_OPT_IF_NONE_MATCH (5)
#define COAP_OPT_OBSERVE (6) #define COAP_OPT_OBSERVE (6)
#define COAP_OPT_LOCATION_PATH (8) #define COAP_OPT_LOCATION_PATH (8)
#define COAP_OPT_URI_PATH (11) #define COAP_OPT_URI_PATH (11)

View File

@ -16,13 +16,20 @@
* This maps files in the local file system onto a resources in CoAP. In that, * This maps files in the local file system onto a resources in CoAP. In that,
* it is what is called a static web server in the unconstrained web. * it is what is called a static web server in the unconstrained web.
* *
* As usual, GET operations are used to read files<!-- WRITESUPPORT, and PUT writes to files. * As usual, GET operations are used to read files and PUT writes to files.
* In the current implementation, PUTs are expressed as random-access, meaning * In the current implementation, PUTs are expressed as random-access, meaning
* that files are not updated atomically -->. * that files are not updated atomically, although files are created atomically.
* The Content-Format option is not checked in the current implementation.
* Conditional file modification and deletion is supported using the If-Match
* option. The If-Match option carries a previously received Etag or in case of
* zero length, requires a request to be processed only if the resource exists.
* In opposite, the If-None-Match option requires a request to be processed,
* only if the resource does not yet exist, and is most useful for file creation.
* *
* Directories are expressed to URIs with trailing slashes<!-- WRITESUPPORT, and are always * Directories are expressed to URIs with trailing slashes. Directories and
* created implicitly when files are PUT under them; they can be DELETEd when * their content are deleted as if one would do an `$rm -r`. If you only would
* empty -->. * like to delete a directory if it is empty, you must supply an If-Match option
* with the special value @ref COAPFILESERVER_DIR_DELETE_ETAG.
* *
* @note The file server uses ETag for cache validation. The ETags are built * @note The file server uses ETag for cache validation. The ETags are built
* from the file system stat values. As clients rely on the ETag to differ when * from the file system stat values. As clients rely on the ETag to differ when
@ -55,8 +62,10 @@
* The path argument specifies under which path the folder is served via CoAP while * The path argument specifies under which path the folder is served via CoAP while
* the context argument contains the path on the local filesystem that will be served. * the context argument contains the path on the local filesystem that will be served.
* *
* The allowed methods dictate whether it's read-only (``COAP_GET``) or (in the * The allowed methods dictate whether it's read-only (``COAP_GET``) or
* future<!-- WRITESUPPORT -->) read-write (``COAP_GET | COAP_PUT | COAP_DELETE``). * read-write (``COAP_GET | COAP_PUT | COAP_DELETE``).
* If you want to support ``PUT`` and `DELETE`, you need to enable the modules
* ``gcoap_fileserver_put`` and ``gcoap_fileserver_delete``.
* *
* @{ * @{
* *
@ -75,6 +84,12 @@ extern "C" {
#include "net/nanocoap.h" #include "net/nanocoap.h"
/**
* @brief Randomly generated Etag, used by a client when a directory should only be
* deleted, if it is empty
*/
#define COAPFILESERVER_DIR_DELETE_ETAG (0x6ce88b56u)
/** /**
* @brief File server handler * @brief File server handler
* *

View File

@ -101,6 +101,33 @@ int vfs_file_sha256(const char* file, void *digest,
void *work_buf, size_t work_buf_len); void *work_buf, size_t work_buf_len);
#endif #endif
/**
* @brief Checks if @p path is a file or a directory.
*
* This function uses @ref vfs_stat(), so if you need @ref vfs_stat() anyway,
* you should not do double work and check it yourself.
*
* @param[in] path Path to check
*
* @return < 0 on FS error
* @return 0 if @p path is a file
* @return > 0 if @p path is a directory
*/
int vfs_is_dir(const char *path);
/**
* @brief Behaves like `rm -r @p root`.
*
* @param[in] root FS root directory to be deleted
* @param[in] path_buf Buffer that must be able to store the longest path
* of the file or directory being deleted
* @param[in] max_size Size of @p path_buf
*
* @return < 0 on error
* @return 0
*/
int vfs_unlink_recursive(const char *root, char *path_buf, size_t max_size);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -21,10 +21,12 @@
#include <unistd.h> #include <unistd.h>
#include <fcntl.h> #include <fcntl.h>
#include "kernel_defines.h"
#include "checksum/fletcher32.h" #include "checksum/fletcher32.h"
#include "net/gcoap/fileserver.h" #include "net/gcoap/fileserver.h"
#include "net/gcoap.h" #include "net/gcoap.h"
#include "vfs.h" #include "vfs.h"
#include "vfs_util.h"
#define ENABLE_DEBUG 0 #define ENABLE_DEBUG 0
#include "debug.h" #include "debug.h"
@ -32,14 +34,27 @@
/** Maximum length of an expressible path, including the trailing 0 character. */ /** Maximum length of an expressible path, including the trailing 0 character. */
#define COAPFILESERVER_PATH_MAX (64) #define COAPFILESERVER_PATH_MAX (64)
/**
* @brief Structure holding information about present options in a request
*/
struct requestoptions {
uint32_t etag; /**< Etag value in an Etag option */
uint32_t if_match; /**< Etag value in an If-Match option */
char if_match_len; /**< Length of the Etag in an If-Match option */
struct {
bool if_match :1; /**< Request carries an If-Match option */
bool if_none_match :1; /**< Request carries an If-None-Match option */
bool etag :1; /**< Request carries an Etag option */
bool block2 :1; /**< Request carries a Block2 option */
bool block1 :1; /**< Request carries a Block1 option */
} exists; /**< Structure holding flags of present request options */
};
/** Data extracted from a request on a file */ /** Data extracted from a request on a file */
struct requestdata { struct requestdata {
/** 0-terminated expanded file name in the VFS */ /** 0-terminated expanded file name in the VFS */
char namebuf[COAPFILESERVER_PATH_MAX]; char namebuf[COAPFILESERVER_PATH_MAX];
uint32_t blocknum2; struct requestoptions options;
uint32_t etag;
bool etag_sent;
uint8_t szx2;
}; };
/** /**
@ -84,69 +99,75 @@ static unsigned _count_char(const char *s, char c)
/** Build an ETag based on the given file's VFS stat. If the stat fails, /** Build an ETag based on the given file's VFS stat. If the stat fails,
* returns the error and leaves etag in any state; otherwise there's an etag * returns the error and leaves etag in any state; otherwise there's an etag
* in the stattag's field */ * in the stattag's field */
static int stat_etag(const char *filename, uint32_t *etag) static void stat_etag(struct stat *stat, uint32_t *etag)
{ {
struct stat stat;
int err = vfs_stat(filename, &stat);
if (err < 0) {
return err;
}
/* Normalizing fields whose value can change without affecting the ETag */ /* Normalizing fields whose value can change without affecting the ETag */
stat.st_nlink = 0; stat->st_nlink = 0;
#if defined(CPU_ESP32) || defined(CPU_ESP8266) || defined(CPU_MIPS_PIC32MX) || defined(CPU_MIPS_PIC32MZ) #if defined(CPU_ESP32) || defined(CPU_ESP8266) || defined(CPU_MIPS_PIC32MX) || defined(CPU_MIPS_PIC32MZ)
memset(&stat.st_atime, 0, sizeof(stat.st_atime)); memset(&stat->st_atime, 0, sizeof(stat->st_atime));
#else #else
memset(&stat.st_atim, 0, sizeof(stat.st_atim)); memset(&stat->st_atim, 0, sizeof(stat->st_atim));
#endif #endif
*etag = fletcher32((void *)stat, sizeof(*stat) / 2);
*etag = fletcher32((void *)&stat, sizeof(stat) / 2);
return 0;
} }
/** Create a CoAP response for a given errno (eg. EACCESS -> 4.03 Forbidden /** Create a CoAP response for a given errno (eg. EACCESS -> 4.03 Forbidden
* etc., defaulting to 5.03 Internal Server Error) */ * etc., defaulting to 5.03 Internal Server Error), or interpret a positive
static size_t gcoap_fileserver_errno_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len, int err) * value for err as a CoAP response code */
static size_t gcoap_fileserver_error_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len, int err)
{ {
uint8_t code; uint8_t code = err;
switch (err) { if (err < 0) {
case -EACCES: switch (err) {
code = COAP_CODE_FORBIDDEN; case -EACCES:
break; code = COAP_CODE_FORBIDDEN;
case -ENOENT: break;
code = COAP_CODE_PATH_NOT_FOUND; case -ENOENT:
break; code = COAP_CODE_PATH_NOT_FOUND;
default: break;
code = COAP_CODE_INTERNAL_SERVER_ERROR; default:
}; code = COAP_CODE_INTERNAL_SERVER_ERROR;
DEBUG("gcoap_fileserver: Rejecting error %d (%s) as %d.%02d\n", err, strerror(err), };
code >> 5, code & 0x1f); DEBUG("gcoap_fileserver: Rejecting error %d (%s) as %d.%02d\n", err, strerror(err),
code >> 5, code & 0x1f);
}
return gcoap_response(pdu, buf, len, code); return gcoap_response(pdu, buf, len, code);
} }
static void _calc_szx2(coap_pkt_t *pdu, size_t reserve, struct requestdata *request) static void _calc_szx2(coap_pkt_t *pdu, size_t reserve, coap_block1_t *block2)
{ {
assert(pdu->payload_len > reserve); assert(pdu->payload_len > reserve);
size_t remaining_length = pdu->payload_len - reserve; size_t remaining_length = pdu->payload_len - reserve;
/* > 0: To not wrap around; if that still won't fit that's later caught in /* > 0: To not wrap around; if that still won't fit that's later caught in
* an assertion */ * an assertion */
while ((coap_szx2size(request->szx2) > remaining_length) && (request->szx2 > 0)) { while ((coap_szx2size(block2->szx) > remaining_length) && (block2->szx > 0)) {
request->szx2--; block2->szx--;
request->blocknum2 <<= 1; block2->blknum <<= 1;
} }
} }
static ssize_t gcoap_fileserver_file_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len, static ssize_t _get_file(coap_pkt_t *pdu, uint8_t *buf, size_t len,
struct requestdata *request) struct requestdata *request)
{ {
int err;
uint32_t etag; uint32_t etag;
int err = stat_etag(request->namebuf, &etag); coap_block1_t block2 = { .szx = CONFIG_NANOCOAP_BLOCK_SIZE_EXP_MAX };
if (err < 0) { {
return gcoap_fileserver_errno_handler(pdu, buf, len, err); struct stat stat;
if ((err = vfs_stat(request->namebuf, &stat)) < 0) {
return gcoap_fileserver_error_handler(pdu, buf, len, err);
}
stat_etag(&stat, &etag);
} }
if (request->options.exists.block2 && !coap_get_block2(pdu, &block2)) {
if (request->etag_sent && etag == request->etag) { return gcoap_fileserver_error_handler(pdu, buf, len, COAP_CODE_BAD_OPTION);
}
if (request->options.exists.if_match &&
memcmp(&etag, &request->options.if_match, request->options.if_match_len)) {
return gcoap_fileserver_error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
}
if (request->options.exists.etag &&
!memcmp(&etag, &request->options.etag, sizeof(etag))) {
gcoap_resp_init(pdu, buf, len, COAP_CODE_VALID); gcoap_resp_init(pdu, buf, len, COAP_CODE_VALID);
coap_opt_add_opaque(pdu, COAP_OPT_ETAG, &etag, sizeof(etag)); coap_opt_add_opaque(pdu, COAP_OPT_ETAG, &etag, sizeof(etag));
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE); return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
@ -154,7 +175,7 @@ static ssize_t gcoap_fileserver_file_handler(coap_pkt_t *pdu, uint8_t *buf, size
int fd = vfs_open(request->namebuf, O_RDONLY, 0); int fd = vfs_open(request->namebuf, O_RDONLY, 0);
if (fd < 0) { if (fd < 0) {
return gcoap_fileserver_errno_handler(pdu, buf, len, fd); return gcoap_fileserver_error_handler(pdu, buf, len, fd);
} }
gcoap_resp_init(pdu, buf, len, COAP_CODE_CONTENT); gcoap_resp_init(pdu, buf, len, COAP_CODE_CONTENT);
@ -162,8 +183,8 @@ static ssize_t gcoap_fileserver_file_handler(coap_pkt_t *pdu, uint8_t *buf, size
coap_block_slicer_t slicer; coap_block_slicer_t slicer;
_calc_szx2(pdu, _calc_szx2(pdu,
5 + 1 + 1 /* reserve BLOCK2 size + payload marker + more */, 5 + 1 + 1 /* reserve BLOCK2 size + payload marker + more */,
request); &block2);
coap_block_slicer_init(&slicer, request->blocknum2, coap_szx2size(request->szx2)); coap_block_slicer_init(&slicer, block2.blknum, coap_szx2size(block2.szx));
coap_opt_add_block2(pdu, &slicer, true); coap_opt_add_block2(pdu, &slicer, true);
size_t resp_len = coap_opt_finish(pdu, COAP_OPT_FINISH_PAYLOAD); size_t resp_len = coap_opt_finish(pdu, COAP_OPT_FINISH_PAYLOAD);
@ -205,16 +226,170 @@ late_err:
return coap_get_total_hdr_len(pdu); return coap_get_total_hdr_len(pdu);
} }
static ssize_t gcoap_fileserver_directory_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len, #if IS_USED(MODULE_GCOAP_FILESERVER_PUT)
struct requestdata *request, static ssize_t _put_file(coap_pkt_t *pdu, uint8_t *buf, size_t len,
const char *root, const char* resource_dir) struct requestdata *request)
{ {
int ret, fd;
uint32_t etag;
struct stat stat;
coap_block1_t block1 = {0};
bool create = (vfs_stat(request->namebuf, &stat) == -ENOENT);
if (create) {
/* While a file 'f' is initially being created,
we save the partial content in '.f' and rename it afterwards */
if (!(ret = strlen(request->namebuf)) || (unsigned)ret >= sizeof(request->namebuf) - 1) {
/* need one more char '.' */
return gcoap_fileserver_error_handler(pdu, buf, len, -ENOBUFS);
}
char *file = strrchr(request->namebuf, '/');
memmove(file + 2, file + 1, strlen(file + 1));
*(file + 1) = '.';
}
if (request->options.exists.block1 && !coap_get_block1(pdu, &block1)) {
ret = COAP_CODE_BAD_OPTION;
goto unlink_on_error;
}
if (block1.blknum == 0) {
/* "If-None-Match only works correctly on Block1 requests with (NUM=0)
and MUST NOT be used on Block1 requests with NUM != 0." */
if (request->options.exists.if_none_match && !create) {
ret = COAP_CODE_PRECONDITION_FAILED;
goto unlink_on_error;
}
}
if (request->options.exists.if_match) {
stat_etag(&stat, &etag); /* Etag before write */
if (create || memcmp(&etag, &request->options.if_match, request->options.if_match_len)) {
ret = COAP_CODE_PRECONDITION_FAILED;
goto unlink_on_error;
}
}
ret = O_WRONLY;
ret |= (create ? O_CREAT | O_APPEND : 0);
if ((fd = vfs_open(request->namebuf, ret, 0)) < 0) {
ret = fd;
goto unlink_on_error;
}
if (create) {
if ((ret = vfs_lseek(fd, 0, SEEK_END)) < 0) {
goto close_on_error;
}
if (block1.offset != (unsigned)ret) {
/* expect block to be in the correct order during initial creation */
ret = COAP_CODE_REQUEST_ENTITY_INCOMPLETE;
goto close_on_error;
}
}
else {
if (block1.offset > (unsigned)stat.st_size) {
/* after initial file creation, random access is fine,
as long as it does not result in holes in the file */
ret = COAP_CODE_REQUEST_ENTITY_INCOMPLETE;
goto close_on_error;
}
if ((ret = vfs_lseek(fd, block1.offset, SEEK_SET)) < 0) {
goto close_on_error;
}
}
if ((ret = vfs_write(fd, pdu->payload, pdu->payload_len)) < 0 ||
(unsigned)ret != pdu->payload_len) {
goto close_on_error;
}
vfs_close(fd);
if (!block1.more) {
if ((ret = vfs_stat(request->namebuf, &stat)) < 0) {
goto unlink_on_error;
}
if (create) {
char path[COAPFILESERVER_PATH_MAX];
strcpy(path, request->namebuf);
char *file = strrchr(path, '/');
memmove(file + 1, file + 2, strlen(file + 2) + 1);
if ((ret = vfs_rename(request->namebuf, path)) < 0) {
goto unlink_on_error;
}
}
stat_etag(&stat, &etag); /* Etag after write */
gcoap_resp_init(pdu, buf, len, create ? COAP_CODE_CREATED : COAP_CODE_CHANGED);
coap_opt_add_opaque(pdu, COAP_OPT_ETAG, &etag, sizeof(etag));
}
else {
gcoap_resp_init(pdu, buf, len, COAP_CODE_CONTINUE);
block1.more = true; /* resource is created atomically */
coap_opt_add_block1_control(pdu, &block1);
}
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
close_on_error:
vfs_close(fd);
unlink_on_error:
if (create) {
vfs_unlink(request->namebuf);
}
return gcoap_fileserver_error_handler(pdu, buf, len, ret);
}
#endif
#if IS_USED(MODULE_GCOAP_FILESERVER_DELETE)
static ssize_t _delete_file(coap_pkt_t *pdu, uint8_t *buf, size_t len,
struct requestdata *request)
{
int ret;
uint32_t etag;
struct stat stat;
if ((ret = vfs_stat(request->namebuf, &stat)) < 0) {
return gcoap_fileserver_error_handler(pdu, buf, len, ret);
}
if (request->options.exists.if_match && request->options.if_match_len) {
stat_etag(&stat, &etag);
if (memcmp(&etag, &request->options.if_match, request->options.if_match_len)) {
return gcoap_fileserver_error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
}
}
if ((ret = vfs_unlink(request->namebuf)) < 0) {
return gcoap_fileserver_error_handler(pdu, buf, len, ret);
}
gcoap_resp_init(pdu, buf, len, COAP_CODE_DELETED);
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
}
#endif
static ssize_t gcoap_fileserver_file_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len,
struct requestdata *request)
{
switch (coap_get_code(pdu)) {
case COAP_METHOD_GET:
return _get_file(pdu, buf, len, request);
#if IS_USED(MODULE_GCOAP_FILESERVER_PUT)
case COAP_METHOD_PUT:
return _put_file(pdu, buf, len, request);
#endif
#if IS_USED(MODULE_GCOAP_FILESERVER_DELETE)
case COAP_METHOD_DELETE:
return _delete_file(pdu, buf, len, request);
#endif
default:
return gcoap_fileserver_error_handler(pdu, buf, len, COAP_CODE_METHOD_NOT_ALLOWED);
}
}
static ssize_t _get_directory(coap_pkt_t *pdu, uint8_t *buf, size_t len,
struct requestdata *request,
const char *root, const char* resource_dir)
{
int err;
vfs_DIR dir; vfs_DIR dir;
coap_block_slicer_t slicer; coap_block_slicer_t slicer;
coap_block1_t block2 = { .szx = CONFIG_NANOCOAP_BLOCK_SIZE_EXP_MAX };
int err = vfs_opendir(&dir, request->namebuf); if (request->options.exists.block2 && !coap_get_block2(pdu, &block2)) {
if (err != 0) { return gcoap_fileserver_error_handler(pdu, buf, len, COAP_OPT_FINISH_NONE);
return gcoap_fileserver_errno_handler(pdu, buf, len, err); }
if ((err = vfs_opendir(&dir, request->namebuf)) < 0) {
return gcoap_fileserver_error_handler(pdu, buf, len, err);
}
if (request->options.exists.if_match && request->options.if_match_len) {
return gcoap_fileserver_error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
} }
DEBUG("gcoap_fileserver: Serving directory listing\n"); DEBUG("gcoap_fileserver: Serving directory listing\n");
@ -222,8 +397,8 @@ static ssize_t gcoap_fileserver_directory_handler(coap_pkt_t *pdu, uint8_t *buf,
coap_opt_add_format(pdu, COAP_FORMAT_LINK); coap_opt_add_format(pdu, COAP_FORMAT_LINK);
_calc_szx2(pdu, _calc_szx2(pdu,
5 + 1 /* reserve BLOCK2 size + payload marker */, 5 + 1 /* reserve BLOCK2 size + payload marker */,
request); &block2);
coap_block_slicer_init(&slicer, request->blocknum2, coap_szx2size(request->szx2)); coap_block_slicer_init(&slicer, block2.blknum, coap_szx2size(block2.szx));
coap_opt_add_block2(pdu, &slicer, true); coap_opt_add_block2(pdu, &slicer, true);
buf += coap_opt_finish(pdu, COAP_OPT_FINISH_PAYLOAD); buf += coap_opt_finish(pdu, COAP_OPT_FINISH_PAYLOAD);
@ -236,8 +411,8 @@ static ssize_t gcoap_fileserver_directory_handler(coap_pkt_t *pdu, uint8_t *buf,
while (vfs_readdir(&dir, &entry) > 0) { while (vfs_readdir(&dir, &entry) > 0) {
const char *entry_name = entry.d_name; const char *entry_name = entry.d_name;
size_t entry_len = strlen(entry_name); size_t entry_len = strlen(entry_name);
if (entry_len <= 2 && memcmp(entry_name, "..", entry_len) == 0) { if (*entry_name == '.') {
/* Up pointers don't work the same way in URI semantics */ /* Exclude everything that starts with '.' */
continue; continue;
} }
bool is_dir = entry_is_dir(request->namebuf, entry_name); bool is_dir = entry_is_dir(request->namebuf, entry_name);
@ -246,7 +421,6 @@ static ssize_t gcoap_fileserver_directory_handler(coap_pkt_t *pdu, uint8_t *buf,
buf += coap_blockwise_put_char(&slicer, buf, ','); buf += coap_blockwise_put_char(&slicer, buf, ',');
} }
buf += coap_blockwise_put_char(&slicer, buf, '<'); buf += coap_blockwise_put_char(&slicer, buf, '<');
buf += coap_blockwise_put_bytes(&slicer, buf, resource_dir, resource_dir_len); buf += coap_blockwise_put_bytes(&slicer, buf, resource_dir, resource_dir_len);
buf += coap_blockwise_put_bytes(&slicer, buf, root_dir, root_dir_len); buf += coap_blockwise_put_bytes(&slicer, buf, root_dir, root_dir_len);
buf += coap_blockwise_put_char(&slicer, buf, '/'); buf += coap_blockwise_put_char(&slicer, buf, '/');
@ -263,15 +437,82 @@ static ssize_t gcoap_fileserver_directory_handler(coap_pkt_t *pdu, uint8_t *buf,
return (uintptr_t)buf - (uintptr_t)pdu->hdr; return (uintptr_t)buf - (uintptr_t)pdu->hdr;
} }
#if IS_USED(MODULE_GCOAP_FILESERVER_PUT)
static ssize_t _put_directory(coap_pkt_t *pdu, uint8_t *buf, size_t len,
struct requestdata *request)
{
int err;
vfs_DIR dir;
if ((err = vfs_opendir(&dir, request->namebuf)) == 0) {
vfs_closedir(&dir);
if (request->options.exists.if_match && request->options.if_match_len) {
return gcoap_fileserver_error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
}
gcoap_resp_init(pdu, buf, len, COAP_CODE_CHANGED);
}
else {
if (request->options.exists.if_match) {
return gcoap_fileserver_error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
}
if ((err = vfs_mkdir(request->namebuf, 0)) < 0) {
return gcoap_fileserver_error_handler(pdu, buf, len, err);
}
gcoap_resp_init(pdu, buf, len, COAP_CODE_CREATED);
}
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
}
#endif
#if IS_USED(MODULE_GCOAP_FILESERVER_DELETE)
static ssize_t _delete_directory(coap_pkt_t *pdu, uint8_t *buf, size_t len,
struct requestdata *request)
{
int err;
if (request->options.exists.if_match && request->options.if_match_len) {
if (request->options.if_match != byteorder_htonl(COAPFILESERVER_DIR_DELETE_ETAG).u32) {
return gcoap_fileserver_error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
}
if ((err = vfs_rmdir(request->namebuf)) < 0) {
return gcoap_fileserver_error_handler(pdu, buf, len, err);
}
}
else if (IS_USED(MODULE_VFS_UTIL)) {
if ((err = vfs_unlink_recursive(request->namebuf,
request->namebuf,
sizeof(request->namebuf))) < 0) {
gcoap_fileserver_error_handler(pdu, buf, len, err);
}
}
gcoap_resp_init(pdu, buf, len, COAP_CODE_DELETED);
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
}
#endif
static ssize_t gcoap_fileserver_directory_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len,
struct requestdata *request,
const char *root, const char* resource_dir)
{
switch (coap_get_code(pdu)) {
case COAP_METHOD_GET:
return _get_directory(pdu, buf, len, request, root, resource_dir);
#if IS_USED(MODULE_GCOAP_FILESERVER_PUT)
case COAP_METHOD_PUT:
return _put_directory(pdu, buf, len, request);
#endif
#if IS_USED(MODULE_GCOAP_FILESERVER_DELETE)
case COAP_METHOD_DELETE:
return _delete_directory(pdu, buf, len, request);
#endif
default:
return gcoap_fileserver_error_handler(pdu, buf, len, COAP_CODE_METHOD_NOT_ALLOWED);
}
}
ssize_t gcoap_fileserver_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len, ssize_t gcoap_fileserver_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len,
coap_request_ctx_t *ctx) { coap_request_ctx_t *ctx) {
const char *root = coap_request_ctx_get_context(ctx); const char *root = coap_request_ctx_get_context(ctx);
const char *resource = coap_request_ctx_get_path(ctx); const char *resource = coap_request_ctx_get_path(ctx);
struct requestdata request = { struct requestdata request = {0};
.etag_sent = false,
.blocknum2 = 0,
.szx2 = CONFIG_NANOCOAP_BLOCK_SIZE_EXP_MAX,
};
/** Index in request.namebuf. Must not point at the last entry as that will be /** Index in request.namebuf. Must not point at the last entry as that will be
* zeroed to get a 0-terminated string. */ * zeroed to get a 0-terminated string. */
@ -284,75 +525,111 @@ ssize_t gcoap_fileserver_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len,
strncpy(request.namebuf, root, sizeof(request.namebuf)); strncpy(request.namebuf, root, sizeof(request.namebuf));
namelength = strlen(root); namelength = strlen(root);
} }
bool is_directory = true; /* either no path component at all or trailing '/' */ bool is_directory = true; /* either no path component at all or trailing '/' */
coap_optpos_t opt = { {
.offset = coap_get_total_hdr_len(pdu), coap_optpos_t opt = { .offset = coap_get_total_hdr_len(pdu) };
}; uint8_t *value;
uint8_t *value; ssize_t optlen;
ssize_t optlen; while ((optlen = coap_opt_get_next(pdu, &opt, &value, 0)) != -ENOENT) {
while ((optlen = coap_opt_get_next(pdu, &opt, &value, 0)) != -ENOENT) { if (optlen < 0) {
errorcode = COAP_CODE_BAD_REQUEST;
if (optlen < 0) { goto error;
errorcode = COAP_CODE_BAD_REQUEST; }
switch (opt.opt_num) {
case COAP_OPT_IF_MATCH:
/* support only one if-match option, although it is repeatable */
if (request.options.exists.if_match) {
errorcode = COAP_CODE_NOT_IMPLEMENTED;
goto error;
}
if (optlen > 8) {
errorcode = COAP_CODE_BAD_OPTION;
goto error;
}
if (optlen && optlen != sizeof(request.options.if_match)) {
errorcode = COAP_CODE_PRECONDITION_FAILED;
goto error;
}
memcpy(&request.options.if_match, value,
request.options.if_match_len = optlen);
request.options.exists.if_match = true;
break;
case COAP_OPT_IF_NONE_MATCH:
if (optlen || request.options.exists.if_none_match) {
errorcode = COAP_CODE_BAD_OPTION;
goto error;
}
request.options.exists.if_none_match = true;
break;
case COAP_OPT_URI_HOST:
/* ignore host name option */
continue;
case COAP_OPT_URI_PATH:
if (strip_remaining != 0) {
strip_remaining -= 1;
continue;
}
if ((is_directory = (optlen == 0))) { /* '/' */
continue;
}
if (memchr(value, '\0', optlen) != NULL ||
memchr(value, '/', optlen) != NULL) {
/* Path can not be expressed in the file system */
errorcode = COAP_CODE_PATH_NOT_FOUND;
goto error;
}
size_t newlength = namelength + 1 + optlen;
if (newlength > sizeof(request.namebuf) - 1) {
/* Path too long, therefore can't exist in this mapping */
errorcode = COAP_CODE_PATH_NOT_FOUND;
goto error;
}
request.namebuf[namelength] = '/';
memcpy(&request.namebuf[namelength] + 1, value, optlen);
namelength = newlength;
break;
case COAP_OPT_ETAG:
if (optlen != sizeof(request.options.etag)) {
/* Can't be a matching tag, no use in carrying that */
continue;
}
if (request.options.exists.etag) {
/* We can reasonably only check for a limited sized set,
* and it size is 1 here (sending multiple ETags is
* possible but rare) */
continue;
}
request.options.exists.etag = true;
memcpy(&request.options.etag, value, sizeof(request.options.etag));
break;
case COAP_OPT_BLOCK2:
if (optlen > 4 || request.options.exists.block2) {
errorcode = COAP_CODE_BAD_OPTION;
goto error;
}
request.options.exists.block2 = true;
break;
case COAP_OPT_BLOCK1:
if (optlen > 4 || request.options.exists.block1) {
errorcode = COAP_CODE_BAD_OPTION;
goto error;
}
request.options.exists.block1 = true;
break;
default:
if (opt.opt_num & 1) {
errorcode = COAP_CODE_BAD_OPTION;
goto error;
}
break;
}
}
if (request.options.exists.if_match &&
request.options.exists.if_none_match) {
errorcode = COAP_CODE_PRECONDITION_FAILED;
goto error; goto error;
} }
switch (opt.opt_num) {
case COAP_OPT_URI_PATH:
if (strip_remaining != 0) {
strip_remaining -= 1;
continue;
}
if ((is_directory = (optlen == 0))) { /* '/' */
continue;
}
if (memchr(value, '\0', optlen) != NULL ||
memchr(value, '/', optlen) != NULL) {
/* Path can not be expressed in the file system */
errorcode = COAP_CODE_PATH_NOT_FOUND;
goto error;
}
size_t newlength = namelength + 1 + optlen;
if (newlength > sizeof(request.namebuf) - 1) {
/* Path too long, therefore can't exist in this mapping */
errorcode = COAP_CODE_PATH_NOT_FOUND;
goto error;
}
request.namebuf[namelength] = '/';
memcpy(&request.namebuf[namelength] + 1, value, optlen);
namelength = newlength;
break;
case COAP_OPT_ETAG:
if (optlen != sizeof(request.etag)) {
/* Can't be a matching tag, no use in carrying that */
continue;
}
if (request.etag_sent) {
/* We can reasonably only check for a limited sized set,
* and it size is 1 here (sending multiple ETags is
* possible but rare) */
continue;
}
request.etag_sent = true;
memcpy(&request.etag, value, sizeof(request.etag));
break;
case COAP_OPT_BLOCK2:
/* Could be more efficient now that we already know where it
* is, but meh */
coap_get_blockopt(pdu, COAP_OPT_BLOCK2, &request.blocknum2, &request.szx2);
break;
default:
if (opt.opt_num & 1) {
errorcode = COAP_CODE_BAD_REQUEST;
goto error;
}
else {
/* Ignoring elective option */
}
}
} }
request.namebuf[namelength] = '\0'; request.namebuf[namelength] = '\0';
DEBUG("request: '%s'\n", request.namebuf); DEBUG("request: '%s'\n", request.namebuf);
@ -360,13 +637,9 @@ ssize_t gcoap_fileserver_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len,
/* Note to self: As we parse more options than just Uri-Path, we'll likely /* Note to self: As we parse more options than just Uri-Path, we'll likely
* pass a struct pointer later. So far, those could even be hooked into the * pass a struct pointer later. So far, those could even be hooked into the
* resource list, but that'll go away once we parse more options */ * resource list, but that'll go away once we parse more options */
if (is_directory) { return is_directory
return gcoap_fileserver_directory_handler(pdu, buf, len, &request, root, resource); ? gcoap_fileserver_directory_handler(pdu, buf, len, &request, root, resource)
} : gcoap_fileserver_file_handler(pdu, buf, len, &request);
else {
return gcoap_fileserver_file_handler(pdu, buf, len, &request);
}
error: error:
return gcoap_response(pdu, buf, len, errorcode); return gcoap_response(pdu, buf, len, errorcode);
} }

View File

@ -31,8 +31,10 @@
#include "shell.h" #include "shell.h"
#include "vfs.h" #include "vfs.h"
#if MODULE_VFS_UTIL
#include "vfs_util.h" #include "vfs_util.h"
#ifndef SHELL_VFS_PATH_SIZE_MAX
#define SHELL_VFS_PATH_SIZE_MAX 256
#endif #endif
#define SHELL_VFS_BUFSIZE 256 #define SHELL_VFS_BUFSIZE 256
@ -62,7 +64,11 @@ static void _vfs_usage(char **argv)
printf("%s cp <src> <dest>\n", argv[0]); printf("%s cp <src> <dest>\n", argv[0]);
printf("%s mv <src> <dest>\n", argv[0]); printf("%s mv <src> <dest>\n", argv[0]);
printf("%s mkdir <path> \n", argv[0]); printf("%s mkdir <path> \n", argv[0]);
printf("%s rm <file>\n", argv[0]); printf("%s rm"
#if IS_USED(MODULE_VFS_UTIL)
" [-r]"
#endif
" <path>\n", argv[0]);
printf("%s df [path]\n", argv[0]); printf("%s df [path]\n", argv[0]);
if (MOUNTPOINTS_NUMOF > 0) { if (MOUNTPOINTS_NUMOF > 0) {
printf("%s mount [path]\n", argv[0]); printf("%s mount [path]\n", argv[0]);
@ -79,7 +85,7 @@ static void _vfs_usage(char **argv)
puts("mv: Move <src> file to <dest>"); puts("mv: Move <src> file to <dest>");
puts("mkdir: Create directory <path> "); puts("mkdir: Create directory <path> ");
puts("cp: Copy <src> file to <dest>"); puts("cp: Copy <src> file to <dest>");
puts("rm: Unlink (delete) <file>"); puts("rm: Unlink (delete) a file or a directory at <path>");
puts("df: Show file system space utilization stats"); puts("df: Show file system space utilization stats");
} }
@ -558,10 +564,21 @@ static int _rm_handler(int argc, char **argv)
_vfs_usage(argv); _vfs_usage(argv);
return 1; return 1;
} }
char *rm_name = argv[1]; bool recursive = !strcmp(argv[1], "-r");
if (recursive && (argc < 3 || !IS_USED(MODULE_VFS_UTIL))) {
_vfs_usage(argv);
return 1;
}
char *rm_name = recursive ? argv[2] : argv[1];
printf("%s: unlink: %s\n", argv[0], rm_name); printf("%s: unlink: %s\n", argv[0], rm_name);
int res;
int res = vfs_unlink(rm_name); if (IS_USED(MODULE_VFS_UTIL) && recursive) {
char pbuf[SHELL_VFS_PATH_SIZE_MAX];
res = vfs_unlink_recursive(rm_name, pbuf, sizeof(pbuf));
}
else {
res = vfs_unlink(rm_name);
}
if (res < 0) { if (res < 0) {
char errbuf[16]; char errbuf[16];
_errno_string(res, (char *)errbuf, sizeof(errbuf)); _errno_string(res, (char *)errbuf, sizeof(errbuf));

View File

@ -154,3 +154,140 @@ int vfs_file_sha256(const char* file, void *digest,
} }
#endif /* MODULE_HASHES */ #endif /* MODULE_HASHES */
int vfs_is_dir(const char *path)
{
assert(path);
int err;
struct stat stat;
if (*path != '/') {
/* only accept absolute paths */
return -EINVAL;
}
if ((err = vfs_stat(path, &stat)) < 0) {
return err;
}
return ((stat.st_mode & S_IFMT) == S_IFDIR);
}
/**
* @brief Removes additional "/" slashes from @p path
*
* @param[in] path Path to be prepared
*/
static void _vfs_prepare_path(char *path)
{
assert(path);
assert(*path == '/');
int path_len = strlen(path);
char *p_write = path; /* end of so far constructed path */
int len = 0;
const char *p_read = p_write; /* segment to be appended to the path */
while (p_read < path + path_len) {
len = 0;
while (*p_read && *p_read == '/') {
p_read++; /* skip slashes */
}
while (p_read[len] && p_read[len] != '/') {
len++; /* length of segment to be copied */
}
if (*p_read && p_write + len + 1 <= path + path_len) {
memmove(p_write + 1, p_read, len);
p_write = p_write + len + 1; /* advance write pointer by segment length + 1 */
*p_write = p_read[len]; /* either '\0' or '/' */
}
p_read += len; /* advance read pointer by segment length */
}
if (*p_write) {
*++p_write = '\0';
}
}
int vfs_unlink_recursive(const char *root, char *path_buf, size_t max_size)
{
assert(root);
assert(path_buf);
/* This function works like a Depth-first search (DFS).
First, we go as deep as we can into a directory and delete contained files.
Then we delete the now empty directory and go to the parent directory
and repeat the process. */
int err;
if (*root != '/' || !strcmp(root, "/")) {
/* only accept absolute paths and not the FS root */
return -EINVAL;
}
if (strlen(root) >= max_size) {
return -ENOBUFS;
}
strcpy(path_buf, root);
_vfs_prepare_path(path_buf);
if (path_buf[strlen(path_buf) - 1] != '/') {
if ((err = vfs_is_dir(path_buf)) < 0) {
return err; /* early unexpected error */
}
else if (!err) {
/* just a file */
return vfs_unlink(path_buf);
}
strcat(path_buf, "/");
}
vfs_DIR dir;
vfs_dirent_t entry;
char seg[VFS_NAME_MAX + 1] = {0}; /* + 1 to append a '/' */
size_t seg_len, root_len, fin = strlen(path_buf);
while ((root_len = strlen(path_buf)) >= fin) {
strcat(path_buf, seg);
*seg = '\0';
if ((err = vfs_opendir(&dir, path_buf)) < 0) { /* this works with a trailing '/' */
return err;
}
while (vfs_readdir(&dir, &entry) > 0) {
if (!strcmp(entry.d_name, "..")) {
continue;
}
seg_len = strlen(entry.d_name);
root_len = strlen(path_buf);
if (root_len + seg_len >= max_size) {
vfs_closedir(&dir);
return -ENOBUFS;
}
strcat(path_buf, entry.d_name);
if ((err = vfs_is_dir(path_buf)) < 0) {
/* error */
vfs_closedir(&dir);
return err;
}
else if (err) {
/* is dir */
if (*seg == '\0') {
strcat(seg, entry.d_name);
strcat(seg, "/");
}
}
else {
/* is file */
if ((err = vfs_unlink(path_buf)) < 0) {
vfs_closedir(&dir);
return err;
}
}
path_buf[root_len] = '\0';
}
vfs_closedir(&dir);
if (*seg == '\0') {
/* no files and no subdirectory */
if ((err = vfs_rmdir(path_buf)) < 0) {
return err;
}
/* go one segment up */
char *end = &path_buf[strlen(path_buf) - 1];
assert(*end == '/');
while (*--end != '/') { }
*++end = '\0';
}
}
return 0;
}