GCS_MAVLink: report a size and time for directories in FTP listings

The listing format gives every entry a size, and ListDirectoryWithTime
gives every entry a time as well, with the type character the only thing
distinguishing a directory from a file. We emitted a bare "D<name>" for a
directory instead, so a client parsing the documented three fields found
only one.

A directory has no meaningful size, so it is reported as zero. Listing
with times now has to stat a directory as well; one which cannot be
stat'ed is still listed, with its time reported as unknown.

Plain ListDirectory is unchanged: it still emits the bare "D<name>" it
always has, so no existing client sees a different directory entry.

MAVSDK's server already sends these fields and its client parses them.
QGroundControl assumed a directory entry was a bare name; a fix for that
is in hand.
This commit is contained in:
Peter Barker
2026-09-22 11:10:46 +10:00
committed by Andrew Tridgell
parent 3e28e00457
commit 42277bd175
+58 -25
View File
@@ -156,8 +156,50 @@ void GCS_FTP::Session::push_reply(Transaction &reply)
}
}
// return a listing entry's last-modification time, or zero if it is unknown.
// a FAT filesystem with no RTC stamps an entry with its own epoch,
// 1980-01-01, rather than recording that it does not know when the entry was
// written. nothing at or before that is a real modification time, so report
// it as unknown
static uint32_t gen_dir_entry_mtime(const struct stat &st)
{
const time_t fat_epoch = 315532800; // 1980-01-01T00:00:00Z
return st.st_mtime > fat_epoch ? (uint32_t)st.st_mtime : 0;
}
// stat the entry named by entry->d_name in the directory path. returns false
// if it could not be stat'ed
static bool gen_dir_entry_stat(const char *path, const struct dirent *entry, struct stat &st)
{
#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 // MAX_NAME_LEN
const size_t path_len = strlen(path);
const size_t full_path_len = path_len + strnlen(entry->d_name, max_name_len);
char full_path[full_path_len + 2];
// the path already ends in a separator when the directory being listed is
// the root; adding another gives "//name", which is a different place
const char *sep = (path_len > 0 && path[path_len - 1] != '/') ? "/" : "";
hal.util->snprintf(full_path, sizeof(full_path), "%s%s%s", path, sep, entry->d_name);
return AP::FS().stat(full_path, &st) == 0;
}
// emit a directory's entry in a listing. the listing format gives every
// entry a size, and a directory does not have a meaningful one, so it is
// reported as zero
static int gen_dir_entry_dir(char *dest, size_t space, const struct dirent *entry, bool with_time, uint32_t mtime)
{
if (with_time) {
// D<name>\t<size>\t<mtime>\0 - mtime in seconds since the UNIX epoch (UTC), 0 if unknown
return hal.util->snprintf(dest, space, "D%s\t0\t%u%c", entry->d_name, (unsigned)mtime, (char)0);
}
return hal.util->snprintf(dest, space, "D%s%c", entry->d_name, (char)0);
}
// calculates how much string length is needed to fit this in a list response
// when with_time is set, file entries also carry their last-modification time
// when with_time is set, entries also carry their last-modification time
int GCS_FTP::Session::gen_dir_entry(char *dest, size_t space, const char *path, const struct dirent * entry, bool with_time)
{
#if AP_FILESYSTEM_HAVE_DIRENT_DTYPE
@@ -179,43 +221,34 @@ int GCS_FTP::Session::gen_dir_entry(char *dest, size_t space, const char *path,
#endif
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 path_len = strlen(path);
const size_t full_path_len = path_len + strnlen(entry->d_name, max_name_len);
char full_path[full_path_len + 2];
// the path already ends in a separator when the directory being
// listed is the root; adding another gives "//name", which is a
// different place
const char *sep = (path_len > 0 && path[path_len - 1] != '/') ? "/" : "";
hal.util->snprintf(full_path, sizeof(full_path), "%s%s%s", path, sep, entry->d_name);
struct stat st;
if (AP::FS().stat(full_path, &st)) {
if (!gen_dir_entry_stat(path, entry, st)) {
return -1;
}
#if !AP_FILESYSTEM_HAVE_DIRENT_DTYPE
if (S_ISDIR(st.st_mode)) {
return hal.util->snprintf(dest, space, "D%s%c", entry->d_name, (char)0);
return gen_dir_entry_dir(dest, space, entry, with_time, gen_dir_entry_mtime(st));
}
#endif
if (with_time) {
// a FAT filesystem with no RTC stamps a file with its own epoch,
// 1980-01-01, rather than recording that it does not know when
// the file was written. nothing at or before that is a real
// modification time, so report it as unknown
const time_t fat_epoch = 315532800; // 1980-01-01T00:00:00Z
const uint32_t mtime = st.st_mtime > fat_epoch ? (uint32_t)st.st_mtime : 0;
// F<name>\t<size>\t<mtime>\0 - mtime in seconds since the UNIX epoch (UTC), 0 if unknown
return hal.util->snprintf(dest, space, "F%s\t%u\t%u%c", entry->d_name, (unsigned)st.st_size, (unsigned)mtime, (char)0);
return hal.util->snprintf(dest, space, "F%s\t%u\t%u%c", entry->d_name, (unsigned)st.st_size, (unsigned)gen_dir_entry_mtime(st), (char)0);
}
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);
}
uint32_t mtime = 0;
if (with_time) {
// a listing with times has to stat the directory too; one which
// cannot be stat'ed is still listed, with an unknown time
// (ESP32 is known to have problems stat'ing directories)
struct stat st;
if (gen_dir_entry_stat(path, entry, st)) {
mtime = gen_dir_entry_mtime(st);
}
}
return gen_dir_entry_dir(dest, space, entry, with_time, mtime);
}
// list the contents of a directory, skip the offset number of entries before providing data