mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-13 07:53:22 -05:00
give downloaded .mp4s a useful filename
This is effective both for Chrome's "Save As" dialog and for curl -OJ. It makes the filename like 20190717135519-driveway-main.mp4 rather than view.mp4 (Chrome) or view.mp4?s=33-36&ts=true (Curl).
This commit is contained in:
parent
dd3c3f2f84
commit
f7da085335
26
src/mp4.rs
26
src/mp4.rs
@ -95,6 +95,7 @@ use reffers::ARefss;
|
||||
use crate::slices::{self, Slices};
|
||||
use smallvec::SmallVec;
|
||||
use std::cell::UnsafeCell;
|
||||
use std::convert::TryFrom;
|
||||
use std::cmp;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
@ -104,7 +105,7 @@ use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// This value should be incremented any time a change is made to this file that causes different
|
||||
/// bytes to be output for a particular set of `Mp4Builder` options. Incrementing this value will
|
||||
/// bytes to be output for a particular set of `FileBuilder` options. Incrementing this value will
|
||||
/// cause the etag to change as well.
|
||||
const FORMAT_VERSION: [u8; 1] = [0x06];
|
||||
|
||||
@ -551,6 +552,7 @@ pub struct FileBuilder {
|
||||
body: BodyState,
|
||||
type_: Type,
|
||||
include_timestamp_subtitle_track: bool,
|
||||
content_disposition: Option<HeaderValue>,
|
||||
}
|
||||
|
||||
/// The portion of `FileBuilder` which is mutated while building the body of the file.
|
||||
@ -711,7 +713,7 @@ macro_rules! write_length {
|
||||
}}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
Normal,
|
||||
InitSegment,
|
||||
@ -734,6 +736,7 @@ impl FileBuilder {
|
||||
},
|
||||
type_: type_,
|
||||
include_timestamp_subtitle_track: false,
|
||||
content_disposition: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -773,6 +776,13 @@ impl FileBuilder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_filename(&mut self, filename: &str) -> Result<(), Error> {
|
||||
self.content_disposition =
|
||||
Some(HeaderValue::try_from(format!("attachment; filename=\"{}\"", filename))
|
||||
.err_kind(ErrorKind::InvalidArgument)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds the `File`, consuming the builder.
|
||||
pub fn build(mut self, db: Arc<db::Database>,
|
||||
dirs_by_stream_id: Arc<::fnv::FnvHashMap<i32, Arc<dir::SampleFileDir>>>)
|
||||
@ -784,6 +794,10 @@ impl FileBuilder {
|
||||
if self.include_timestamp_subtitle_track {
|
||||
etag.update(b":ts:").err_kind(ErrorKind::Internal)?;
|
||||
}
|
||||
if let Some(cd) = self.content_disposition.as_ref() {
|
||||
etag.update(b":cd:").err_kind(ErrorKind::Internal)?;
|
||||
etag.update(cd.as_bytes()).err_kind(ErrorKind::Internal)?;
|
||||
}
|
||||
match self.type_ {
|
||||
Type::Normal => {},
|
||||
Type::InitSegment => etag.update(b":init:").err_kind(ErrorKind::Internal)?,
|
||||
@ -884,8 +898,9 @@ impl FileBuilder {
|
||||
video_sample_entries: self.video_sample_entries,
|
||||
initial_sample_byte_pos,
|
||||
last_modified,
|
||||
etag: HeaderValue::from_str(&format!("\"{}\"", &strutil::hex(&etag)))
|
||||
etag: HeaderValue::try_from(format!("\"{}\"", &strutil::hex(&etag)))
|
||||
.expect("hex string should be valid UTF-8"),
|
||||
content_disposition: self.content_disposition,
|
||||
})))
|
||||
}
|
||||
|
||||
@ -1433,6 +1448,7 @@ struct FileInner {
|
||||
initial_sample_byte_pos: u64,
|
||||
last_modified: SystemTime,
|
||||
etag: HeaderValue,
|
||||
content_disposition: Option<HeaderValue>,
|
||||
}
|
||||
|
||||
impl FileInner {
|
||||
@ -1513,6 +1529,10 @@ impl http_serve::Entity for File {
|
||||
mime.extend_from_slice(b"\"");
|
||||
hdrs.insert(http::header::CONTENT_TYPE,
|
||||
http::header::HeaderValue::from_maybe_shared(mime.freeze()).unwrap());
|
||||
|
||||
if let Some(cd) = self.0.content_disposition.as_ref() {
|
||||
hdrs.insert(http::header::CONTENT_DISPOSITION, cd.clone());
|
||||
}
|
||||
}
|
||||
fn last_modified(&self) -> Option<SystemTime> { Some(self.0.last_modified) }
|
||||
fn etag(&self) -> Option<HeaderValue> { Some(self.0.etag.clone()) }
|
||||
|
24
src/web.rs
24
src/web.rs
@ -399,16 +399,20 @@ impl ServiceInner {
|
||||
if !caller.permissions.view_video {
|
||||
return Err(plain_response(StatusCode::UNAUTHORIZED, "view_video required"));
|
||||
}
|
||||
let stream_id = {
|
||||
let (stream_id, camera_name);
|
||||
{
|
||||
let db = self.db.lock();
|
||||
let camera = db.get_camera(uuid)
|
||||
.ok_or_else(|| plain_response(StatusCode::NOT_FOUND,
|
||||
format!("no such camera {}", uuid)))?;
|
||||
camera.streams[stream_type.index()]
|
||||
camera_name = camera.short_name.clone();
|
||||
stream_id = camera.streams[stream_type.index()]
|
||||
.ok_or_else(|| plain_response(StatusCode::NOT_FOUND,
|
||||
format!("no such stream {}/{}", uuid,
|
||||
stream_type)))?
|
||||
stream_type)))?;
|
||||
|
||||
};
|
||||
let mut start_time_for_filename = None;
|
||||
let mut builder = mp4::FileBuilder::new(mp4_type);
|
||||
if let Some(q) = req.uri().query() {
|
||||
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
||||
@ -465,6 +469,10 @@ impl ServiceInner {
|
||||
let times = start as i32 .. end as i32;
|
||||
debug!("...appending recording {} with times {:?} \
|
||||
(out of dur {})", r.id, times, d);
|
||||
if start_time_for_filename.is_none() {
|
||||
start_time_for_filename =
|
||||
Some(r.start + recording::Duration(start));
|
||||
}
|
||||
builder.append(&db, r, start as i32 .. end as i32)?;
|
||||
} else {
|
||||
debug!("...skipping recording {} dur {}", r.id, d);
|
||||
@ -499,6 +507,14 @@ impl ServiceInner {
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(start) = start_time_for_filename {
|
||||
let tm = time::at(time::Timespec{sec: start.unix_seconds(), nsec: 0});
|
||||
let stream_abbrev = if stream_type == db::StreamType::MAIN { "main" } else { "sub" };
|
||||
let suffix = if mp4_type == mp4::Type::Normal { "mp4" } else { "m4s" };
|
||||
builder.set_filename(&format!("{}-{}-{}.{}", tm.strftime("%Y%m%d%H%M%S").unwrap(),
|
||||
camera_name, stream_abbrev, suffix))
|
||||
.map_err(from_base_error)?;
|
||||
}
|
||||
let mp4 = builder.build(self.db.clone(), self.dirs_by_stream_id.clone())
|
||||
.map_err(from_base_error)?;
|
||||
if debug {
|
||||
@ -572,7 +588,7 @@ impl ServiceInner {
|
||||
let flags = (auth::SessionFlags::HttpOnly as i32) |
|
||||
(auth::SessionFlags::SameSite as i32) |
|
||||
(auth::SessionFlags::SameSiteStrict as i32) |
|
||||
if is_secure { (auth::SessionFlags::Secure as i32) } else { 0 };
|
||||
if is_secure { auth::SessionFlags::Secure as i32 } else { 0 };
|
||||
let (sid, _) = l.login_by_password(authreq, &r.username, r.password, Some(domain),
|
||||
flags)
|
||||
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
|
||||
|
Loading…
Reference in New Issue
Block a user