diff --git a/Makefile b/Makefile index a258699f..e6f56e4c 100644 --- a/Makefile +++ b/Makefile @@ -43,8 +43,9 @@ image: $(img) cargo bootimage --no-default-features --features $(output) --release --bin moros dd conv=notrunc if=$(bin) of=$(img) -opts = -m 32 -nic model=$(nic) -drive file=$(img),format=raw \ - -audiodev driver=sdl,id=a0 -machine pcspk-audiodev=a0 +opts = -m 32 -drive file=$(img),format=raw \ + -audiodev driver=sdl,id=a0 -machine pcspk-audiodev=a0 \ + -netdev user,id=e0,hostfwd=tcp::8080-:80 -device $(nic),netdev=e0 ifeq ($(kvm),true) opts += -cpu host -accel kvm else diff --git a/src/api/clock.rs b/src/api/clock.rs index 66e06a22..a4386487 100644 --- a/src/api/clock.rs +++ b/src/api/clock.rs @@ -1,7 +1,10 @@ use crate::api::fs; - use core::convert::TryInto; +pub const DATE_TIME_ZONE: &str = "%Y-%m-%d %H:%M:%S %z"; +pub const DATE_TIME: &str = "%Y-%m-%d %H:%M:%S"; +pub const DATE: &str = "%Y-%m-%d"; + fn read_float(path: &str) -> f64 { if let Ok(bytes) = fs::read_to_bytes(path) { if bytes.len() == 8 { diff --git a/src/api/time.rs b/src/api/time.rs index 1acaabe1..1559d444 100644 --- a/src/api/time.rs +++ b/src/api/time.rs @@ -4,9 +4,13 @@ use crate::api::clock; use time::{OffsetDateTime, Duration, UtcOffset}; pub fn now() -> OffsetDateTime { + now_utc().to_offset(offset()) +} + +pub fn now_utc() -> OffsetDateTime { let s = clock::realtime(); // Since Unix Epoch let ns = Duration::nanoseconds(libm::floor(1e9 * (s - libm::floor(s))) as i64); - OffsetDateTime::from_unix_timestamp(s as i64).to_offset(offset()) + ns + OffsetDateTime::from_unix_timestamp(s as i64) + ns } pub fn from_timestamp(ts: i64) -> OffsetDateTime { @@ -19,5 +23,5 @@ fn offset() -> UtcOffset { return UtcOffset::seconds(offset); } } - UtcOffset::seconds(0) + UtcOffset::UTC } diff --git a/src/sys/clock.rs b/src/sys/clock.rs index 4dc14332..e6dfc064 100644 --- a/src/sys/clock.rs +++ b/src/sys/clock.rs @@ -1,3 +1,4 @@ +use crate::api::clock::DATE_TIME_ZONE; use crate::sys; use crate::sys::cmos::CMOS; use crate::sys::fs::FileIO; @@ -105,7 +106,7 @@ pub fn init() { let s = realtime(); let ns = Duration::nanoseconds(libm::floor(1e9 * (s - libm::floor(s))) as i64); let dt = OffsetDateTime::from_unix_timestamp(s as i64) + ns; - let rtc = dt.format("%F %H:%M:%S UTC"); + let rtc = dt.format(DATE_TIME_ZONE); log!("RTC {}\n", rtc); } diff --git a/src/usr/date.rs b/src/usr/date.rs index 63c4f80f..1310695e 100644 --- a/src/usr/date.rs +++ b/src/usr/date.rs @@ -1,9 +1,10 @@ use crate::api; +use crate::api::clock::DATE_TIME_ZONE; use crate::api::process::ExitCode; use time::validate_format_string; pub fn main(args: &[&str]) -> Result<(), ExitCode> { - let format = if args.len() > 1 { args[1] } else { "%F %H:%M:%S" }; + let format = if args.len() > 1 { args[1] } else { DATE_TIME_ZONE }; match validate_format_string(format) { Ok(()) => { println!("{}", api::time::now().format(format)); diff --git a/src/usr/find.rs b/src/usr/find.rs index 5dfeea93..b27e28b5 100644 --- a/src/usr/find.rs +++ b/src/usr/find.rs @@ -44,7 +44,7 @@ pub fn main(args: &[&str]) -> Result<(), ExitCode> { error!("Missing name"); return Err(ExitCode::UsageError); } - }, + } "-l" | "--line" => { if i + 1 < n { line = Some(args[i + 1]); @@ -53,7 +53,7 @@ pub fn main(args: &[&str]) -> Result<(), ExitCode> { error!("Missing line"); return Err(ExitCode::UsageError); } - }, + } _ => path = args[i], } i += 1; diff --git a/src/usr/httpd.rs b/src/usr/httpd.rs index 4985aa89..0e7daa48 100644 --- a/src/usr/httpd.rs +++ b/src/usr/httpd.rs @@ -1,37 +1,211 @@ use crate::sys; use crate::api::clock; +use crate::api::clock::DATE_TIME_ZONE; use crate::api::console::Style; use crate::api::fs; use crate::api::process::ExitCode; use crate::api::syscall; +use crate::api::time; + +use alloc::collections::btree_map::BTreeMap; use alloc::collections::vec_deque::VecDeque; use alloc::format; use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; +use core::fmt; use smoltcp::socket::{TcpSocket, TcpSocketBuffer}; use smoltcp::time::Instant; use smoltcp::phy::Device; -use time::OffsetDateTime; +use smoltcp::wire::IpAddress; + +const MAX_CONNECTIONS: usize = 32; +const POLL_DELAY_DIV: usize = 128; + +#[derive(Clone)] +struct Request { + addr: IpAddress, + verb: String, + path: String, + body: Vec, +} + +impl Request { + pub fn new() -> Self { + Self { + addr: IpAddress::default(), + verb: String::new(), + path: String::new(), + body: Vec::new(), + } + } + + pub fn from(addr: IpAddress, buf: &[u8]) -> Option { + let mut req = Request::new(); + let msg = String::from_utf8_lossy(buf); + if !msg.is_empty() { + req.addr = addr; + let mut is_body = false; + for (i, line) in msg.lines().enumerate() { + if i == 0 { + let fields: Vec<_> = line.split(' ').collect(); + if fields.len() >= 2 { + req.verb = fields[0].to_string(); + req.path = fields[1].to_string(); + } + } else if !is_body && line.is_empty() { + is_body = true; + } else if is_body { + req.body.extend_from_slice(&format!("{}\n", line).as_bytes()); + } + } + Some(req) + } else { + None + } + } +} -pub fn main(_args: &[&str]) -> Result<(), ExitCode> { +#[derive(Clone)] +struct Response { + req: Request, + buf: Vec, + mime: String, + time: String, + code: usize, + size: usize, + body: Vec, + headers: BTreeMap, +} + +impl Response { + pub fn new() -> Self { + let mut headers = BTreeMap::new(); + headers.insert("Date".to_string(), time::now_utc().format("%a, %d %b %Y %H:%M:%S GMT")); + headers.insert("Server".to_string(), format!("MOROS/{}", env!("CARGO_PKG_VERSION"))); + Self { + req: Request::new(), + buf: Vec::new(), + mime: String::new(), + time: time::now().format(DATE_TIME_ZONE), + code: 0, + size: 0, + body: Vec::new(), + headers, + } + } + + pub fn end(&mut self) { + self.size = self.body.len(); + self.headers.insert("Content-Length".to_string(), self.size.to_string()); + self.headers.insert("Connection".to_string(), "close".to_string()); + self.headers.insert("Content-Type".to_string(), if self.mime.starts_with("text/") { + format!("{}; charset=utf-8", self.mime) + } else { + format!("{}", self.mime) + }); + self.write(); + } + + fn write(&mut self) { + self.buf.clear(); + self.buf.extend_from_slice(&format!("{}\r\n", self.status()).as_bytes()); + for (key, val) in &self.headers { + self.buf.extend_from_slice(&format!("{}: {}\r\n", key, val).as_bytes()); + } + self.buf.extend_from_slice(b"\r\n"); + self.buf.extend_from_slice(&self.body); + } + + fn status(&self) -> String { + let msg = match self.code { + 200 => "OK", + 301 => "Moved Permanently", + 400 => "Bad Request", + 403 => "Forbidden", + 404 => "Not Found", + 500 => "Internal Server Error", + _ => "Unknown Error", + }; + format!("HTTP/1.0 {} {}", self.code, msg) + } +} + +impl fmt::Display for Response { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let csi_blue = Style::color("LightBlue"); + let csi_cyan = Style::color("LightCyan"); + let csi_pink = Style::color("Pink"); + let csi_reset = Style::reset(); + write!( + f, "{}{} - -{} [{}] {}\"{} {}\"{} {} {}", + csi_cyan, self.req.addr, + csi_pink, self.time, + csi_blue, self.req.verb, self.req.path, + csi_reset, self.code, self.size + ) + } +} + +pub fn main(args: &[&str]) -> Result<(), ExitCode> { let csi_color = Style::color("Yellow"); let csi_reset = Style::reset(); - let port = 80; + let mut read_only = false; + let mut port = 80; + let mut dir = sys::process::dir(); + let mut i = 1; + let n = args.len(); + while i < n { + match args[i] { + "-h" | "--help" => { + usage(); + return Ok(()); + } + "-r" | "--read-only" => { + read_only = true; + } + "-p" | "--port" => { + if i + 1 < n { + port = args[i + 1].parse().unwrap_or(port); + i += 1; + } else { + error!("Missing port number"); + return Err(ExitCode::UsageError); + } + } + "-d" | "--dir" => { + if i + 1 < n { + dir = args[i + 1].to_string(); + i += 1; + } else { + error!("Missing directory"); + return Err(ExitCode::UsageError); + } + } + _ => {} + } + i += 1; + } if let Some(ref mut iface) = *sys::net::IFACE.lock() { println!("{}HTTP Server listening on 0.0.0.0:{}{}", csi_color, port, csi_reset); let mtu = iface.device().capabilities().max_transmission_unit; - let tcp_rx_buffer = TcpSocketBuffer::new(vec![0; mtu]); - let tcp_tx_buffer = TcpSocketBuffer::new(vec![0; mtu]); - let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer); - let tcp_handle = iface.add_socket(tcp_socket); + let mut connections = Vec::new(); + for _ in 0..MAX_CONNECTIONS { + let tcp_rx_buffer = TcpSocketBuffer::new(vec![0; mtu]); + let tcp_tx_buffer = TcpSocketBuffer::new(vec![0; mtu]); + let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer); + let tcp_handle = iface.add_socket(tcp_socket); + let send_queue: VecDeque> = VecDeque::new(); + connections.push((tcp_handle, send_queue)); + } - let mut send_queue: VecDeque> = VecDeque::new(); loop { if sys::console::end_of_text() || sys::console::end_of_transmission() { - iface.remove_socket(tcp_handle); + for (tcp_handle, _) in &connections { + iface.remove_socket(*tcp_handle); + } println!(); return Ok(()); } @@ -41,144 +215,129 @@ pub fn main(_args: &[&str]) -> Result<(), ExitCode> { error!("Network Error: {}", e); } - let socket = iface.get_socket::(tcp_handle); + for (tcp_handle, send_queue) in &mut connections { + let socket = iface.get_socket::(*tcp_handle); - if !socket.is_open() { - socket.listen(port).unwrap(); - } - let addr = socket.remote_endpoint().addr; - if socket.may_recv() { - let res = socket.recv(|buffer| { - let mut res = String::new(); - let req = String::from_utf8_lossy(buffer); - if !req.is_empty() { - let mut verb = ""; - let mut path = ""; - let mut header = true; - let mut contents = String::new(); - for (i, line) in req.lines().enumerate() { - if i == 0 { - let fields: Vec<_> = line.split(' ').collect(); - if fields.len() >= 2 { - verb = fields[0]; - path = fields[1]; - } - } else if header && line.is_empty() { - header = false; - } else if !header { - contents.push_str(&format!("{}\n", line)); - } - } - let date = strftime("%d/%b/%Y:%H:%M:%S %z"); - let code; - let mime; - let mut body; - match verb { - "GET" => { - if path.len() > 1 && path.ends_with('/') { - code = 301; - res.push_str("HTTP/1.0 301 Moved Permanently\r\n"); - res.push_str(&format!("Location: {}\r\n", path.trim_end_matches('/'))); - body = "

Moved Permanently

\r\n".to_string(); - mime = "text/html"; - } else if let Ok(contents) = fs::read_to_string(path) { - code = 200; - res.push_str("HTTP/1.0 200 OK\r\n"); - body = contents.replace("\n", "\r\n"); - mime = "text/plain"; - } else if let Ok(mut files) = fs::read_dir(path) { - code = 200; - res.push_str("HTTP/1.0 200 OK\r\n"); - body = format!("

Index of {}

\r\n", path); - files.sort_by_key(|f| f.name()); - for file in files { - let sep = if path == "/" { "" } else { "/" }; - let path = format!("{}{}{}", path, sep, file.name()); - body.push_str(&format!("
  • {}
  • \n", path, file.name())); - } - mime = "text/html"; - } else { - code = 404; - res.push_str("HTTP/1.0 404 Not Found\r\n"); - body = "

    Not Found

    \r\n".to_string(); - mime = "text/plain"; - } - }, - "PUT" => { - if path.ends_with('/') { // Write directory - let path = path.trim_end_matches('/'); - if fs::exists(path) { - code = 403; - res.push_str("HTTP/1.0 403 Forbidden\r\n"); - } else if let Some(handle) = fs::create_dir(path) { - syscall::close(handle); - code = 200; - res.push_str("HTTP/1.0 200 OK\r\n"); + if !socket.is_open() { + socket.listen(port).unwrap(); + } + let addr = socket.remote_endpoint().addr; + if socket.may_recv() { + let res = socket.recv(|buffer| { + let mut res = Response::new(); + if let Some(req) = Request::from(addr, buffer) { + res.req = req.clone(); + let sep = if req.path == "/" { "" } else { "/" }; + let real_path = format!("{}{}{}", dir, sep, req.path.strip_suffix('/').unwrap_or(&req.path)).replace("//", "/"); + + match req.verb.as_str() { + "GET" => { + if fs::is_dir(&real_path) && !req.path.ends_with('/') { + res.code = 301; + res.mime = "text/html".to_string(); + res.headers.insert("Location".to_string(), format!("{}/", req.path)); + res.body.extend_from_slice(b"

    Moved Permanently

    \r\n"); } else { - code = 500; - res.push_str("HTTP/1.0 500 Internal Server Error\r\n"); + let mut not_found = true; + for autocomplete in vec!["", "/index.html", "/index.htm", "/index.txt"] { + let real_path = format!("{}{}", real_path, autocomplete); + if fs::is_dir(&real_path) { + continue; + } + if let Ok(buf) = fs::read_to_bytes(&real_path) { + res.code = 200; + res.mime = content_type(&real_path); + let tmp; + res.body.extend_from_slice(if res.mime.starts_with("text/") { + tmp = String::from_utf8_lossy(&buf).to_string().replace("\n", "\r\n"); + tmp.as_bytes() + } else { + &buf + }); + not_found = false; + break; + } + } + if not_found { + if let Ok(mut files) = fs::read_dir(&real_path) { + res.code = 200; + res.mime = "text/html".to_string(); + res.body.extend_from_slice(&format!("

    Index of {}

    \r\n", req.path).as_bytes()); + files.sort_by_key(|f| f.name()); + for file in files { + let path = format!("{}{}", req.path, file.name()); + let link = format!("
  • {}
  • \n", path, file.name()); + res.body.extend_from_slice(&link.as_bytes()); + } + } else { + res.code = 404; + res.mime = "text/html".to_string(); + res.body.extend_from_slice(b"

    Not Found

    \r\n"); + } + } } - } else { // Write file - if fs::write(path, contents.as_bytes()).is_ok() { - code = 200; - res.push_str("HTTP/1.0 200 OK\r\n"); - } else { - code = 500; - res.push_str("HTTP/1.0 500 Internal Server Error\r\n"); + }, + "PUT" if !read_only => { + if real_path.ends_with('/') { // Write directory + let real_path = real_path.trim_end_matches('/'); + if fs::exists(&real_path) { + res.code = 403; + } else if let Some(handle) = fs::create_dir(&real_path) { + syscall::close(handle); + res.code = 200; + } else { + res.code = 500; + } + } else { // Write file + if fs::write(&real_path, &req.body).is_ok() { + res.code = 200; + } else { + res.code = 500; + } } - } - body = "".to_string(); - mime = "text/plain"; - }, - "DELETE" => { - if fs::exists(path) { - if fs::delete(path).is_ok() { - code = 200; - res.push_str("HTTP/1.0 200 OK\r\n"); + res.mime = "text/plain".to_string(); + }, + "DELETE" if !read_only => { + if fs::exists(&real_path) { + if fs::delete(&real_path).is_ok() { + res.code = 200; + } else { + res.code = 500; + } } else { - code = 500; - res.push_str("HTTP/1.0 500 Internal Server Error\r\n"); + res.code = 404; } - } else { - code = 404; - res.push_str("HTTP/1.0 404 Not Found\r\n"); - } - body = "".to_string(); - mime = "text/plain"; - }, - _ => { - res.push_str("HTTP/1.0 400 Bad Request\r\n"); - code = 400; - body = "

    Bad Request

    \r\n".to_string(); - mime = "text/plain"; - }, + res.mime = "text/plain".to_string(); + }, + _ => { + res.code = 400; + res.mime = "text/html".to_string(); + res.body.extend_from_slice(b"

    Bad Request

    \r\n"); + }, + } + res.end(); + println!("{}", res); } - let size = body.len(); - res.push_str(&format!("Server: MOROS/{}\r\n", env!("CARGO_PKG_VERSION"))); - res.push_str(&format!("Date: {}\r\n", strftime("%a, %d %b %Y %H:%M:%S GMT"))); - res.push_str(&format!("Content-Type: {}; charset=utf-8\r\n", mime)); - res.push_str(&format!("Content-Length: {}\r\n", size)); - res.push_str("Connection: close\r\n"); - res.push_str("\r\n"); - res.push_str(&body); - println!("{} - - [{}] \"{} {}\" {} {}", addr, date, verb, path, code, size); + (buffer.len(), res) + }).unwrap(); + for chunk in res.buf.chunks(mtu) { + send_queue.push_back(chunk.to_vec()); } - (buffer.len(), res) - }).unwrap(); - for chunk in res.as_bytes().chunks(mtu) { - send_queue.push_back(chunk.to_vec()); - } - if socket.can_send() { - if let Some(chunk) = send_queue.pop_front() { - socket.send_slice(&chunk).unwrap(); + if socket.can_send() { + if let Some(chunk) = send_queue.pop_front() { + socket.send_slice(&chunk).unwrap(); + } } + } else if socket.may_send() { + socket.close(); + send_queue.clear(); } - } else if socket.may_send() { - socket.close(); - send_queue.clear(); } if let Some(wait_duration) = iface.poll_delay(timestamp) { - syscall::sleep((wait_duration.total_micros() as f64) / 1000000.0); + let t = wait_duration.total_micros() / POLL_DELAY_DIV as u64; + if t > 0 { + syscall::sleep((t as f64) / 1000000.0); + } } } } else { @@ -187,7 +346,32 @@ pub fn main(_args: &[&str]) -> Result<(), ExitCode> { } } -fn strftime(format: &str) -> String { - let timestamp = clock::realtime(); - OffsetDateTime::from_unix_timestamp(timestamp as i64).format(format) +fn content_type(path: &str) -> String { + let ext = path.rsplit_once('.').unwrap_or(("", "")).1; + match ext { + "css" => "text/css", + "csv" => "text/csv", + "gif" => "text/gif", + "htm" | "html" => "text/html", + "jpg" | "jpeg" => "image/jpeg", + "js" => "text/javascript", + "json" => "application/json", + "lsp" | "lisp" => "text/plain", + "png" => "image/png", + "sh" => "application/x-sh", + "txt" => "text/plain", + _ => "application/octet-stream", + }.to_string() +} + +fn usage() { + let csi_option = Style::color("LightCyan"); + let csi_title = Style::color("Yellow"); + let csi_reset = Style::reset(); + println!("{}Usage:{} httpd {}{1}", csi_title, csi_reset, csi_option); + println!(); + println!("{}Options:{}", csi_title, csi_reset); + println!(" {0}-d{1},{0} --dir {1} Set directory to {0}{1}", csi_option, csi_reset); + println!(" {0}-p{1},{0} --port {1} Listen to port {0}{1}", csi_option, csi_reset); + println!(" {0}-r{1},{0} --read-only{1} Set read-only mode", csi_option, csi_reset); } diff --git a/src/usr/install.rs b/src/usr/install.rs index 3deb2750..b43192e2 100644 --- a/src/usr/install.rs +++ b/src/usr/install.rs @@ -60,18 +60,32 @@ pub fn copy_files(verbose: bool) { copy_file("/tmp/beep/tetris.sh", include_bytes!("../../dsk/tmp/beep/tetris.sh"), verbose); copy_file("/tmp/beep/starwars.sh", include_bytes!("../../dsk/tmp/beep/starwars.sh"), verbose); copy_file("/tmp/beep/mario.sh", include_bytes!("../../dsk/tmp/beep/mario.sh"), verbose); + + create_dir("/var/www", verbose); + copy_file("/var/www/index.html", include_bytes!("../../www/index.html"), verbose); + copy_file("/var/www/moros.png", include_bytes!("../../www/moros.png"), verbose); } -pub fn main(_args: &[&str]) -> Result<(), ExitCode> { +pub fn main(args: &[&str]) -> Result<(), ExitCode> { let csi_color = Style::color("Yellow"); let csi_reset = Style::reset(); println!("{}Welcome to MOROS v{} installation program!{}", csi_color, env!("CARGO_PKG_VERSION"), csi_reset); println!(); - print!("Proceed? [y/N] "); - if io::stdin().read_line().trim() == "y" { + let mut has_confirmed = false; + for &arg in args { + match arg { + "-y" | "--yes" => has_confirmed = true, + _ => continue + } + } + if !has_confirmed { + print!("Proceed? [y/N] "); + has_confirmed = io::stdin().read_line().trim() == "y"; println!(); + } + if has_confirmed { if !sys::fs::is_mounted() { println!("{}Listing disks ...{}", csi_color, csi_reset); usr::shell::exec("disk list").ok(); diff --git a/src/usr/lisp.rs b/src/usr/lisp.rs index 6de806bb..13ff1657 100644 --- a/src/usr/lisp.rs +++ b/src/usr/lisp.rs @@ -77,7 +77,7 @@ impl PartialEq for Exp { impl fmt::Display for Exp { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let str = match self { + let out = match self { Exp::Lambda(_) => "Lambda {}".to_string(), Exp::Func(_) => "Function {}".to_string(), Exp::Bool(a) => a.to_string(), @@ -89,7 +89,7 @@ impl fmt::Display for Exp { format!("({})", xs.join(" ")) }, }; - write!(f, "{}", str) + write!(f, "{}", out) } } diff --git a/src/usr/list.rs b/src/usr/list.rs index 11821d03..55e99f8e 100644 --- a/src/usr/list.rs +++ b/src/usr/list.rs @@ -1,4 +1,5 @@ use crate::sys; +use crate::api::clock::DATE_TIME; use crate::api::console::Style; use crate::api::time; use crate::api::fs; @@ -79,7 +80,7 @@ fn print_file(file: &FileInfo, width: usize) { let csi_dev_color = Style::color("Yellow"); let csi_reset = Style::reset(); - let date = time::from_timestamp(file.time() as i64); + let time = time::from_timestamp(file.time() as i64); let color = if file.is_dir() { csi_dir_color } else if file.is_device() { @@ -87,7 +88,7 @@ fn print_file(file: &FileInfo, width: usize) { } else { csi_reset }; - println!("{:width$} {} {}{}{}", file.size(), date.format("%F %H:%M:%S"), color, file.name(), csi_reset, width = width); + println!("{:width$} {} {}{}{}", file.size(), time.format(DATE_TIME), color, file.name(), csi_reset, width = width); } fn help() -> Result<(), ExitCode> { diff --git a/src/usr/read.rs b/src/usr/read.rs index 5c3d6e5a..536cd4ec 100644 --- a/src/usr/read.rs +++ b/src/usr/read.rs @@ -27,7 +27,7 @@ pub fn main(args: &[&str]) -> Result<(), ExitCode> { "/dev/rtc" => { let rtc = CMOS::new().rtc(); println!( - "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}", rtc.year, rtc.month, rtc.day, rtc.hour, rtc.minute, rtc.second );