riot_coap_handler_demos/vfs.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
//! A fileserver backed by the RIOT VFS
//!
//! # INCOMPLETE
//!
//! This currently works, but is quite rought around the edges:
//!
//! * Error handling is sloppy, and while most of the file system operations should not fail, a
//! panic can be forced over the network by requesting a block outside a file's size.
//!
//! * Files do not send ETags along.
//!
//! * Directory listings are in plain text (RFC6690 would need knowledge of the current path), and
//! do not indicate whether an entry is a directory.
use coap_handler::Handler;
use coap_handler_implementations::wkc;
use coap_message::{
Code, MinimalWritableMessage, MutableWritableMessage, OptionNumber, ReadableMessage,
};
use coap_message_utils::option_value::Block2RequestData;
use coap_message_utils::Error;
use coap_message_utils::OptionsExt;
use coap_numbers::{code, option};
use heapless;
use riot_wrappers::vfs::{Dir, File, SeekFrom};
const MAX_PATH: usize = 64;
struct FileServerRoot(&'static str);
type Path = heapless::String<{ MAX_PATH }>;
// It's kind of pointless to make all the distinctions in the extractor when later sending a Path
// along; a later optimization might look at whether there are any moint points available at
// extraction time and store a static str to the mount point path slice if we can make such an
// assumption.
enum GetSuccess {
File(File),
Directory(Path),
}
impl Handler for FileServerRoot {
type RequestData = (Block2RequestData, GetSuccess);
type ExtractRequestError = Error;
type BuildResponseError<M: MinimalWritableMessage> = M::UnionError;
fn extract_request_data<M: ReadableMessage>(
&mut self,
req: &M,
) -> Result<Self::RequestData, Error> {
use core::fmt::Write;
let mut path: Result<Path, _> = Ok(self.0.into());
if req.code().into() != code::GET {
return Err(Error::method_not_allowed());
}
let mut block2 = None;
req.options()
.take_block2(&mut block2)
.take_uri_path(|segment| {
let err = if let Ok(ref mut path) = &mut path {
// FIXME: This is a manual try! block
(|| {
if segment.contains("/") {
// It's not like we have any policy to apply, but let's still not slip this
// through.
return Err(Error::bad_option(option::URI_PATH));
}
// possibly with some "out of space / path too long" extra datum
path.write_str("/")
.map_err(|_| Error::bad_option(option::URI_PATH))?;
path.write_str(segment)
.map_err(|_| Error::bad_option(option::URI_PATH))?;
Ok(())
})()
.err()
} else {
None
};
if let Some(err) = err {
path = Err(err);
}
})
.ignore_elective_others()?;
let path = path?;
if path == "" {
// Could be nice and tell to append slash...
return Err(Error::not_found());
}
Ok((
block2.unwrap_or_default(),
if path.as_bytes()[path.len() - 1] == b'/' {
Ok(GetSuccess::Directory(path))
} else {
File::open(&path).map(GetSuccess::File)
}
.map_err(|e| match -e.number() as _ {
riot_sys::EACCES => Error::not_found(), // FIXME: forbidden()
riot_sys::ENOENT => Error::not_found(),
riot_sys::EISDIR => Error::not_found(), // but we could be nice and tell to add a a slash
_ => Error::internal_server_error(),
})?,
))
}
fn estimate_length(&mut self, _: &Self::RequestData) -> usize {
// TBD estimate
1050
}
fn build_response<M: MutableWritableMessage>(
&mut self,
out: &mut M,
reqdat: Self::RequestData,
) -> Result<(), M::UnionError> {
match reqdat {
(mut b, GetSuccess::File(mut f)) => {
out.set_code(Code::new(code::CONTENT)?);
// 1: payload marker; 5: block option plus intro
let available_len = out.available_space() - 1;
b = b
.shrink(available_len as _)
.expect("Buffer can't even keep minimal block");
let len = if let Ok(stat) = f.stat() {
stat.size()
} else {
// FIXME: What level of error handling of opened files is appropriate?
f.seek(SeekFrom::End(0))
.expect("File system supports neither stat not seek")
};
let more = len > b.start() as usize + b.size() as usize;
out.add_option_uint(OptionNumber::new(option::BLOCK2)?, b.to_option_value(more))?;
// FIXME: Set an ETag, possibly from stat data
f.seek(SeekFrom::Start(b.start() as _)).unwrap(); // FIXME At least *this* should be caught
let mut payload = out.payload_mut_with_len(b.size() as _)?;
let mut read_len = 0;
while !payload.is_empty() {
let r = f.read(&mut payload).unwrap();
read_len += r;
payload = &mut payload[r..];
if r == 0 {
break;
}
}
// Not checking whether that came to exactly the seeked length -- if the file
// length does change between the stat and now, well, we produce an invalid block
// response.
out.truncate(read_len)?;
}
(b, GetSuccess::Directory(path)) => {
let dirslot = core::pin::pin!(Default::default());
let dir = Dir::open(&path, dirslot);
let mut mountpoints = riot_wrappers::vfs::Mount::all();
// .filter(|m| m.mount_point().starts_with(&*path))
// .peekable()
// ;
// If mountpoints were an iterator, we could just apply the commented-out stuff
// above and do `mountpoints.peek().is_some()` here; not having that, doing the
// iterator stuff above manually (and again below) with possible raciness (which,
// at worst, makes the headline show but not list any actual mount points)...
let mut have_some_mountpoints = false;
while let Some(m) = mountpoints.next() {
have_some_mountpoints |= m.mount_point().starts_with(&*path);
}
let mut mountpoints = riot_wrappers::vfs::Mount::all();
// ... and all up to here would be a single line.
if dir.is_err() && !have_some_mountpoints {
out.set_code(Code::new(code::NOT_FOUND)?);
return Ok(());
}
out.set_code(Code::new(code::CONTENT)?);
coap_handler_implementations::helpers::block2_write(b, out, |w| {
use core::fmt::Write;
if let Ok(dir) = dir {
writeln!(w, "Index:").unwrap();
for e in dir {
writeln!(w, "{}", e.name()).unwrap();
}
}
if have_some_mountpoints {
writeln!(w, "Relevant mount points:").unwrap();
while let Some(m) = mountpoints.next() {
// `if` wrapper would be avoided if the filter further up worked
if m.mount_point().starts_with(&*path) {
writeln!(w, "{}", &m.mount_point()[path.len()..]).unwrap();
}
}
}
});
}
};
Ok(())
}
}
/// Build a handler that will serve a subtree of the file system
///
/// Note that this handles a whole subtree, so it's better placed as `.below(&["vfs"],
/// riot_coap_handler_demos::vfs::vfs(""))` (or `"/sda1"`) rather than using `.at()` in a
/// [coap_handler_implementations::HandlerBuilder].
pub fn vfs(root: &'static str) -> impl coap_handler::Handler + coap_handler::Reporting {
wkc::ConstantSingleRecordReport::new_with_path(
FileServerRoot(root),
// Directory listings are currently in plain text
&[coap_handler::Attribute::Ct(0)],
// The best we can report right now -- enumerating everything is no good, so gradual reveal
// it is. If root is not mounted, https://github.com/RIOT-OS/RIOT/issues/15291 strikes.
&[""],
)
}