mirror of
https://github.com/ArduPilot/ardupilot
synced 2025-01-03 06:28:27 -04:00
6538e8c9ae
Delete unneeded orphan comment replace get_last_txbuf() with a predicate Make txbuf flow control threashold consistent between Parameter download and FTP and keep it in range where we are also slowing down normal streams Delay sending text banner until after first FTP response to reduce latency on slow links Don't let flow control delay setting ftp.last_send_ms so as to slow down normal streams as soon as possible to improve FTP response time
705 lines
28 KiB
C++
705 lines
28 KiB
C++
/*
|
|
GCS MAVLink functions related to FTP
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "GCS_config.h"
|
|
|
|
#if AP_MAVLINK_FTP_ENABLED
|
|
|
|
#include <AP_HAL/AP_HAL.h>
|
|
|
|
#include "GCS.h"
|
|
|
|
#include <AP_Filesystem/AP_Filesystem.h>
|
|
#include <AP_HAL/utility/sparse-endian.h>
|
|
#include <AP_BoardConfig/AP_BoardConfig.h>
|
|
|
|
extern const AP_HAL::HAL& hal;
|
|
|
|
struct GCS_MAVLINK::ftp_state GCS_MAVLINK::ftp;
|
|
|
|
// timeout for session inactivity
|
|
#define FTP_SESSION_TIMEOUT 3000
|
|
|
|
bool GCS_MAVLINK::ftp_init(void) {
|
|
|
|
// check if ftp is disabled for memory savings
|
|
#if !defined(HAL_BUILD_AP_PERIPH)
|
|
if (AP_BoardConfig::ftp_disabled()) {
|
|
goto failed;
|
|
}
|
|
#endif
|
|
// we can simply check if we allocated everything we need
|
|
|
|
if (ftp.requests != nullptr) {
|
|
return true;
|
|
}
|
|
|
|
ftp.requests = new ObjectBuffer<pending_ftp>(5);
|
|
if (ftp.requests == nullptr || ftp.requests->get_size() == 0) {
|
|
goto failed;
|
|
}
|
|
|
|
if (!hal.scheduler->thread_create(FUNCTOR_BIND_MEMBER(&GCS_MAVLINK::ftp_worker, void),
|
|
"FTP", 2560, AP_HAL::Scheduler::PRIORITY_IO, 0)) {
|
|
goto failed;
|
|
}
|
|
|
|
return true;
|
|
|
|
failed:
|
|
delete ftp.requests;
|
|
ftp.requests = nullptr;
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "failed to initialize MAVFTP");
|
|
|
|
return false;
|
|
}
|
|
|
|
void GCS_MAVLINK::handle_file_transfer_protocol(const mavlink_message_t &msg) {
|
|
if (ftp_init()) {
|
|
mavlink_file_transfer_protocol_t packet;
|
|
mavlink_msg_file_transfer_protocol_decode(&msg, &packet);
|
|
|
|
struct pending_ftp request;
|
|
|
|
request.chan = chan;
|
|
request.seq_number = le16toh_ptr(packet.payload);
|
|
|
|
request.session = packet.payload[2];
|
|
request.opcode = static_cast<FTP_OP>(packet.payload[3]);
|
|
request.size = packet.payload[4];
|
|
request.req_opcode = static_cast<FTP_OP>(packet.payload[5]);
|
|
request.burst_complete = packet.payload[6];
|
|
request.offset = le32toh_ptr(&packet.payload[8]);
|
|
request.sysid = msg.sysid;
|
|
request.compid = msg.compid;
|
|
memcpy(request.data, &packet.payload[12], sizeof(packet.payload) - 12);
|
|
|
|
if (!ftp.requests->push(request)) {
|
|
// dropping the message, no buffer space to queue it in
|
|
// we could NACK it, but that can lead to GCS confusion, so we're treating it like lost data
|
|
}
|
|
}
|
|
}
|
|
|
|
bool GCS_MAVLINK::send_ftp_reply(const pending_ftp &reply)
|
|
{
|
|
if (!last_txbuf_is_greater(33)) { // It helps avoid GCS timeout if this is less than the threshold where we slow down normal streams (<=49)
|
|
return false;
|
|
}
|
|
WITH_SEMAPHORE(comm_chan_lock(reply.chan));
|
|
if (!HAVE_PAYLOAD_SPACE(chan, FILE_TRANSFER_PROTOCOL)) {
|
|
return false;
|
|
}
|
|
uint8_t payload[251] = {};
|
|
put_le16_ptr(payload, reply.seq_number);
|
|
payload[2] = reply.session;
|
|
payload[3] = static_cast<uint8_t>(reply.opcode);
|
|
payload[4] = reply.size;
|
|
payload[5] = static_cast<uint8_t>(reply.req_opcode);
|
|
payload[6] = reply.burst_complete ? 1 : 0;
|
|
put_le32_ptr(&payload[8], reply.offset);
|
|
memcpy(&payload[12], reply.data, sizeof(reply.data));
|
|
mavlink_msg_file_transfer_protocol_send(
|
|
reply.chan,
|
|
0, reply.sysid, reply.compid,
|
|
payload);
|
|
return true;
|
|
}
|
|
|
|
void GCS_MAVLINK::ftp_error(struct pending_ftp &response, FTP_ERROR error) {
|
|
response.opcode = FTP_OP::Nack;
|
|
response.data[0] = static_cast<uint8_t>(error);
|
|
response.size = 1;
|
|
|
|
// FIXME: errno's are not thread-local as they should be on ChibiOS
|
|
if (error == FTP_ERROR::FailErrno) {
|
|
// translate the errno's that we have useful messages for
|
|
switch (errno) {
|
|
case EEXIST:
|
|
response.data[0] = static_cast<uint8_t>(FTP_ERROR::FileExists);
|
|
break;
|
|
case ENOENT:
|
|
response.data[0] = static_cast<uint8_t>(FTP_ERROR::FileNotFound);
|
|
break;
|
|
default:
|
|
response.data[1] = static_cast<uint8_t>(errno);
|
|
response.size = 2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// send our response back out to the system
|
|
void GCS_MAVLINK::ftp_push_replies(pending_ftp &reply)
|
|
{
|
|
ftp.last_send_ms = AP_HAL::millis(); // Used to detect active FTP session
|
|
|
|
while (!send_ftp_reply(reply)) {
|
|
hal.scheduler->delay(2);
|
|
}
|
|
|
|
if (reply.req_opcode == FTP_OP::TerminateSession) {
|
|
ftp.last_send_ms = 0;
|
|
}
|
|
/*
|
|
provide same banner we would give with old param download
|
|
Do this after send_ftp_reply() to get the first FTP response out sooner
|
|
on slow links to avoid GCS timeout. The slowdown of normal streams in
|
|
get_reschedule_interval_ms() should help for subsequent responses.
|
|
*/
|
|
if (ftp.need_banner_send_mask & (1U<<reply.chan)) {
|
|
ftp.need_banner_send_mask &= ~(1U<<reply.chan);
|
|
send_banner();
|
|
}
|
|
}
|
|
|
|
void GCS_MAVLINK::ftp_worker(void) {
|
|
pending_ftp request;
|
|
pending_ftp reply = {};
|
|
reply.session = -1; // flag the reply as invalid for any reuse
|
|
|
|
while (true) {
|
|
bool skip_push_reply = false;
|
|
|
|
while (ftp.requests == nullptr || !ftp.requests->pop(request)) {
|
|
// nothing to handle, delay ourselves a bit then check again. Ideally we'd use conditional waits here
|
|
hal.scheduler->delay(2);
|
|
}
|
|
|
|
// if it's a rerequest and we still have the last response then send it
|
|
if ((request.sysid == reply.sysid) && (request.compid == reply.compid) &&
|
|
(request.session == reply.session) && (request.seq_number + 1 == reply.seq_number)) {
|
|
ftp_push_replies(reply);
|
|
continue;
|
|
}
|
|
|
|
// setup the response
|
|
memset(&reply, 0, sizeof(reply));
|
|
reply.req_opcode = request.opcode;
|
|
reply.session = request.session;
|
|
reply.seq_number = request.seq_number + 1;
|
|
reply.chan = request.chan;
|
|
reply.sysid = request.sysid;
|
|
reply.compid = request.compid;
|
|
|
|
// sanity check the request size
|
|
if (request.size > sizeof(request.data)) {
|
|
ftp_error(reply, FTP_ERROR::InvalidDataSize);
|
|
ftp_push_replies(reply);
|
|
continue;
|
|
}
|
|
|
|
uint32_t now = AP_HAL::millis();
|
|
|
|
// check for session termination
|
|
if (request.session != ftp.current_session &&
|
|
(request.opcode == FTP_OP::TerminateSession || request.opcode == FTP_OP::ResetSessions)) {
|
|
// terminating a different session, just ack
|
|
reply.opcode = FTP_OP::Ack;
|
|
} else if (ftp.fd != -1 && request.session != ftp.current_session &&
|
|
now - ftp.last_send_ms < FTP_SESSION_TIMEOUT) {
|
|
// if we have an open file and the session isn't right
|
|
// then reject. This prevents IO on the wrong file
|
|
ftp_error(reply, FTP_ERROR::InvalidSession);
|
|
} else {
|
|
if (ftp.fd != -1 &&
|
|
request.session != ftp.current_session &&
|
|
now - ftp.last_send_ms >= FTP_SESSION_TIMEOUT) {
|
|
// if a new session appears and the old session has
|
|
// been idle for more than the timeout then force
|
|
// close the old session
|
|
AP::FS().close(ftp.fd);
|
|
ftp.fd = -1;
|
|
ftp.current_session = -1;
|
|
}
|
|
// dispatch the command as needed
|
|
switch (request.opcode) {
|
|
case FTP_OP::None:
|
|
reply.opcode = FTP_OP::Ack;
|
|
break;
|
|
case FTP_OP::TerminateSession:
|
|
case FTP_OP::ResetSessions:
|
|
// we already handled this, just listed for completeness
|
|
if (ftp.fd != -1) {
|
|
AP::FS().close(ftp.fd);
|
|
ftp.fd = -1;
|
|
}
|
|
ftp.current_session = -1;
|
|
reply.opcode = FTP_OP::Ack;
|
|
break;
|
|
case FTP_OP::ListDirectory:
|
|
ftp_list_dir(request, reply);
|
|
break;
|
|
case FTP_OP::OpenFileRO:
|
|
{
|
|
// only allow one file to be open per session
|
|
if (ftp.fd != -1 && now - ftp.last_send_ms > FTP_SESSION_TIMEOUT) {
|
|
// no activity for 3s, assume client has
|
|
// timed out receiving open reply, close
|
|
// the file
|
|
AP::FS().close(ftp.fd);
|
|
ftp.fd = -1;
|
|
ftp.current_session = -1;
|
|
}
|
|
if (ftp.fd != -1) {
|
|
ftp_error(reply, FTP_ERROR::Fail);
|
|
break;
|
|
}
|
|
|
|
// sanity check that our the request looks well formed
|
|
const size_t file_name_len = strnlen((char *)request.data, sizeof(request.data));
|
|
if ((file_name_len != request.size) || (request.size == 0)) {
|
|
ftp_error(reply, FTP_ERROR::InvalidDataSize);
|
|
break;
|
|
}
|
|
|
|
request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
|
|
|
|
// get the file size
|
|
struct stat st;
|
|
if (AP::FS().stat((char *)request.data, &st)) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
const size_t file_size = st.st_size;
|
|
|
|
// actually open the file
|
|
ftp.fd = AP::FS().open((char *)request.data, O_RDONLY);
|
|
if (ftp.fd == -1) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
ftp.mode = FTP_FILE_MODE::Read;
|
|
ftp.current_session = request.session;
|
|
|
|
reply.opcode = FTP_OP::Ack;
|
|
reply.size = sizeof(uint32_t);
|
|
put_le32_ptr(reply.data, (uint32_t)file_size);
|
|
|
|
// provide compatibility with old protocol banner download
|
|
if (strncmp((const char *)request.data, "@PARAM/param.pck", 16) == 0) {
|
|
ftp.need_banner_send_mask |= 1U<<reply.chan;
|
|
}
|
|
break;
|
|
}
|
|
case FTP_OP::ReadFile:
|
|
{
|
|
// must actually be working on a file
|
|
if (ftp.fd == -1) {
|
|
ftp_error(reply, FTP_ERROR::FileNotFound);
|
|
break;
|
|
}
|
|
|
|
// must have the file in read mode
|
|
if ((ftp.mode != FTP_FILE_MODE::Read)) {
|
|
ftp_error(reply, FTP_ERROR::Fail);
|
|
break;
|
|
}
|
|
|
|
// seek to requested offset
|
|
if (AP::FS().lseek(ftp.fd, request.offset, SEEK_SET) == -1) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
|
|
// fill the buffer
|
|
const ssize_t read_bytes = AP::FS().read(ftp.fd, reply.data, MIN(sizeof(reply.data),request.size));
|
|
if (read_bytes == -1) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
if (read_bytes == 0) {
|
|
ftp_error(reply, FTP_ERROR::EndOfFile);
|
|
break;
|
|
}
|
|
|
|
reply.opcode = FTP_OP::Ack;
|
|
reply.offset = request.offset;
|
|
reply.size = (uint8_t)read_bytes;
|
|
break;
|
|
}
|
|
case FTP_OP::Ack:
|
|
case FTP_OP::Nack:
|
|
// eat these, we just didn't expect them
|
|
continue;
|
|
break;
|
|
case FTP_OP::OpenFileWO:
|
|
case FTP_OP::CreateFile:
|
|
{
|
|
// only allow one file to be open per session
|
|
if (ftp.fd != -1) {
|
|
ftp_error(reply, FTP_ERROR::Fail);
|
|
break;
|
|
}
|
|
|
|
// sanity check that our the request looks well formed
|
|
const size_t file_name_len = strnlen((char *)request.data, sizeof(request.data));
|
|
if ((file_name_len != request.size) || (request.size == 0)) {
|
|
ftp_error(reply, FTP_ERROR::InvalidDataSize);
|
|
break;
|
|
}
|
|
|
|
request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
|
|
|
|
// actually open the file
|
|
ftp.fd = AP::FS().open((char *)request.data,
|
|
(request.opcode == FTP_OP::CreateFile) ? O_WRONLY|O_CREAT|O_TRUNC : O_WRONLY);
|
|
if (ftp.fd == -1) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
ftp.mode = FTP_FILE_MODE::Write;
|
|
ftp.current_session = request.session;
|
|
|
|
reply.opcode = FTP_OP::Ack;
|
|
break;
|
|
}
|
|
case FTP_OP::WriteFile:
|
|
{
|
|
// must actually be working on a file
|
|
if (ftp.fd == -1) {
|
|
ftp_error(reply, FTP_ERROR::FileNotFound);
|
|
break;
|
|
}
|
|
|
|
// must have the file in write mode
|
|
if ((ftp.mode != FTP_FILE_MODE::Write)) {
|
|
ftp_error(reply, FTP_ERROR::Fail);
|
|
break;
|
|
}
|
|
|
|
// seek to requested offset
|
|
if (AP::FS().lseek(ftp.fd, request.offset, SEEK_SET) == -1) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
|
|
// fill the buffer
|
|
const ssize_t write_bytes = AP::FS().write(ftp.fd, request.data, request.size);
|
|
if (write_bytes == -1) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
|
|
reply.opcode = FTP_OP::Ack;
|
|
reply.offset = request.offset;
|
|
break;
|
|
}
|
|
case FTP_OP::CreateDirectory:
|
|
{
|
|
// sanity check that our the request looks well formed
|
|
const size_t file_name_len = strnlen((char *)request.data, sizeof(request.data));
|
|
if ((file_name_len != request.size) || (request.size == 0)) {
|
|
ftp_error(reply, FTP_ERROR::InvalidDataSize);
|
|
break;
|
|
}
|
|
|
|
request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
|
|
|
|
// actually make the directory
|
|
if (AP::FS().mkdir((char *)request.data) == -1) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
|
|
reply.opcode = FTP_OP::Ack;
|
|
break;
|
|
}
|
|
case FTP_OP::RemoveDirectory:
|
|
case FTP_OP::RemoveFile:
|
|
{
|
|
// sanity check that our the request looks well formed
|
|
const size_t file_name_len = strnlen((char *)request.data, sizeof(request.data));
|
|
if ((file_name_len != request.size) || (request.size == 0)) {
|
|
ftp_error(reply, FTP_ERROR::InvalidDataSize);
|
|
break;
|
|
}
|
|
|
|
request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
|
|
|
|
// remove the file/dir
|
|
if (AP::FS().unlink((char *)request.data) == -1) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
|
|
reply.opcode = FTP_OP::Ack;
|
|
break;
|
|
}
|
|
case FTP_OP::CalcFileCRC32:
|
|
{
|
|
// sanity check that our the request looks well formed
|
|
const size_t file_name_len = strnlen((char *)request.data, sizeof(request.data));
|
|
if ((file_name_len != request.size) || (request.size == 0)) {
|
|
ftp_error(reply, FTP_ERROR::InvalidDataSize);
|
|
break;
|
|
}
|
|
|
|
request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
|
|
|
|
uint32_t checksum = 0;
|
|
if (!AP::FS().crc32((char *)request.data, checksum)) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
|
|
// reset our scratch area so we don't leak data, and can leverage trimming
|
|
memset(reply.data, 0, sizeof(reply.data));
|
|
reply.size = sizeof(uint32_t);
|
|
put_le32_ptr(reply.data, checksum);
|
|
reply.opcode = FTP_OP::Ack;
|
|
break;
|
|
}
|
|
case FTP_OP::BurstReadFile:
|
|
{
|
|
const uint16_t max_read = (request.size == 0?sizeof(reply.data):request.size);
|
|
// must actually be working on a file
|
|
if (ftp.fd == -1) {
|
|
ftp_error(reply, FTP_ERROR::FileNotFound);
|
|
break;
|
|
}
|
|
|
|
// must have the file in read mode
|
|
if ((ftp.mode != FTP_FILE_MODE::Read)) {
|
|
ftp_error(reply, FTP_ERROR::Fail);
|
|
break;
|
|
}
|
|
|
|
// seek to requested offset
|
|
if (AP::FS().lseek(ftp.fd, request.offset, SEEK_SET) == -1) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
|
|
/*
|
|
calculate a burst delay so that FTP burst
|
|
transfer doesn't use more than 1/3 of
|
|
available bandwidth on links that don't have
|
|
flow control. This reduces the chance of
|
|
lost packets a lot, which results in overall
|
|
faster transfers
|
|
*/
|
|
uint32_t burst_delay_ms = 0;
|
|
if (valid_channel(request.chan)) {
|
|
auto *port = mavlink_comm_port[request.chan];
|
|
if (port != nullptr && port->get_flow_control() != AP_HAL::UARTDriver::FLOW_CONTROL_ENABLE) {
|
|
const uint32_t bw = port->bw_in_bytes_per_second();
|
|
const uint16_t pkt_size = PAYLOAD_SIZE(request.chan, FILE_TRANSFER_PROTOCOL) - (sizeof(reply.data) - max_read);
|
|
burst_delay_ms = 3000 * pkt_size / bw;
|
|
}
|
|
}
|
|
|
|
// this transfer size is enough for a full parameter file with max parameters
|
|
const uint32_t transfer_size = 500;
|
|
for (uint32_t i = 0; (i < transfer_size); i++) {
|
|
// fill the buffer
|
|
const ssize_t read_bytes = AP::FS().read(ftp.fd, reply.data, MIN(sizeof(reply.data), max_read));
|
|
if (read_bytes == -1) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
|
|
if (read_bytes != sizeof(reply.data)) {
|
|
// don't send any old data
|
|
memset(reply.data + read_bytes, 0, sizeof(reply.data) - read_bytes);
|
|
}
|
|
|
|
if (read_bytes == 0) {
|
|
ftp_error(reply, FTP_ERROR::EndOfFile);
|
|
break;
|
|
}
|
|
|
|
reply.opcode = FTP_OP::Ack;
|
|
reply.offset = request.offset + i * max_read;
|
|
reply.burst_complete = (i == (transfer_size - 1));
|
|
reply.size = (uint8_t)read_bytes;
|
|
|
|
ftp_push_replies(reply);
|
|
|
|
if (read_bytes < max_read) {
|
|
// ensure the NACK which we send next is at the right offset
|
|
reply.offset += read_bytes;
|
|
}
|
|
|
|
// prep the reply to be used again
|
|
reply.seq_number++;
|
|
|
|
hal.scheduler->delay(burst_delay_ms);
|
|
}
|
|
|
|
if (reply.opcode != FTP_OP::Nack) {
|
|
// prevent a duplicate packet send for
|
|
// normal replies of burst reads
|
|
skip_push_reply = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case FTP_OP::Rename: {
|
|
// sanity check that the request looks well formed
|
|
const char *filename1 = (char*)request.data;
|
|
const size_t len1 = strnlen(filename1, sizeof(request.data)-2);
|
|
const char *filename2 = (char*)&request.data[len1+1];
|
|
const size_t len2 = strnlen(filename2, sizeof(request.data)-(len1+1));
|
|
if (filename1[len1] != 0 || (len1+len2+1 != request.size) || (request.size == 0)) {
|
|
ftp_error(reply, FTP_ERROR::InvalidDataSize);
|
|
break;
|
|
}
|
|
request.data[sizeof(request.data) - 1] = 0; // ensure the 2nd path is null terminated
|
|
// remove the file/dir
|
|
if (AP::FS().rename(filename1, filename2) != 0) {
|
|
ftp_error(reply, FTP_ERROR::FailErrno);
|
|
break;
|
|
}
|
|
reply.opcode = FTP_OP::Ack;
|
|
break;
|
|
}
|
|
|
|
case FTP_OP::TruncateFile:
|
|
default:
|
|
// this was bad data, just nack it
|
|
gcs().send_text(MAV_SEVERITY_DEBUG, "Unsupported FTP: %d", static_cast<int>(request.opcode));
|
|
ftp_error(reply, FTP_ERROR::Fail);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!skip_push_reply) {
|
|
ftp_push_replies(reply);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// calculates how much string length is needed to fit this in a list response
|
|
int GCS_MAVLINK::gen_dir_entry(char *dest, size_t space, const char *path, const struct dirent * entry) {
|
|
const bool is_file = entry->d_type == DT_REG || entry->d_type == DT_LNK;
|
|
|
|
if (space < 3) {
|
|
return -1;
|
|
}
|
|
dest[0] = 0;
|
|
|
|
if (!is_file && entry->d_type != DT_DIR) {
|
|
return -1; // this just forces it so we can't send this back, it's easier then sending skips to a GCS
|
|
}
|
|
|
|
if (is_file) {
|
|
#ifdef MAX_NAME_LEN
|
|
const uint8_t max_name_len = MIN(unsigned(MAX_NAME_LEN), 255U);
|
|
#else
|
|
const uint8_t max_name_len = 255U;
|
|
#endif
|
|
const size_t full_path_len = strlen(path) + strnlen(entry->d_name, max_name_len);
|
|
char full_path[full_path_len + 2];
|
|
hal.util->snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
|
|
struct stat st;
|
|
if (AP::FS().stat(full_path, &st)) {
|
|
return -1;
|
|
}
|
|
return hal.util->snprintf(dest, space, "F%s\t%u%c", entry->d_name, (unsigned)st.st_size, (char)0);
|
|
} else {
|
|
return hal.util->snprintf(dest, space, "D%s%c", entry->d_name, (char)0);
|
|
}
|
|
}
|
|
|
|
// list the contents of a directory, skip the offset number of entries before providing data
|
|
void GCS_MAVLINK::ftp_list_dir(struct pending_ftp &request, struct pending_ftp &response) {
|
|
response.offset = request.offset; // this should be set for any failure condition for debugging
|
|
|
|
const size_t directory_name_size = strnlen((char *)request.data, sizeof(request.data));
|
|
// sanity check that our the request looks well formed
|
|
if ((directory_name_size != request.size) || (request.size == 0)) {
|
|
ftp_error(response, FTP_ERROR::InvalidDataSize);
|
|
return;
|
|
}
|
|
|
|
request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
|
|
|
|
// Strip trailing /
|
|
const size_t dir_len = strlen((char *)request.data);
|
|
if ((dir_len > 1) && (request.data[dir_len - 1] == '/')) {
|
|
request.data[dir_len - 1] = 0;
|
|
}
|
|
|
|
// open the dir
|
|
auto *dir = AP::FS().opendir((char *)request.data);
|
|
if (dir == nullptr) {
|
|
ftp_error(response, FTP_ERROR::FailErrno);
|
|
return;
|
|
}
|
|
|
|
// burn the entries we don't care about
|
|
while (request.offset > 0) {
|
|
const struct dirent *entry = AP::FS().readdir(dir);
|
|
if(entry == nullptr) {
|
|
ftp_error(response, FTP_ERROR::EndOfFile);
|
|
AP::FS().closedir(dir);
|
|
return;
|
|
}
|
|
|
|
// check how much space would be needed to emit the listing
|
|
const int needed_space = gen_dir_entry((char *)response.data, sizeof(request.data), (char *)request.data, entry);
|
|
|
|
if (needed_space < 0 || needed_space > (int)sizeof(request.data)) {
|
|
continue;
|
|
}
|
|
|
|
request.offset--;
|
|
}
|
|
|
|
// start packing in entries that fit
|
|
uint8_t index = 0;
|
|
struct dirent *entry;
|
|
while ((entry = AP::FS().readdir(dir))) {
|
|
// figure out if we can fit the file
|
|
const int required_space = gen_dir_entry((char *)(response.data + index), sizeof(response.data) - index, (char *)request.data, entry);
|
|
|
|
// couldn't ever send this so drop it
|
|
if (required_space < 0) {
|
|
continue;
|
|
}
|
|
|
|
// can't fit it in this one, leave it for the next list to send
|
|
if ((required_space + index) >= (int)sizeof(request.data)) {
|
|
break;
|
|
}
|
|
|
|
// step the index forward and keep going
|
|
index += required_space + 1;
|
|
}
|
|
|
|
if (index == 0) {
|
|
ftp_error(response, FTP_ERROR::EndOfFile);
|
|
AP::FS().closedir(dir);
|
|
return;
|
|
}
|
|
|
|
// strip any bad temp data from our response as it can confuse a GCS, and defeats 0 trimming
|
|
if (index < sizeof(response.data)) {
|
|
memset(response.data + index, 0, MAX(0, (int)(sizeof(response.data)) - index));
|
|
}
|
|
|
|
response.opcode = FTP_OP::Ack;
|
|
response.size = index;
|
|
|
|
AP::FS().closedir(dir);
|
|
}
|
|
|
|
#endif // AP_MAVLINK_FTP_ENABLED
|