Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,11 +347,11 @@ Top-level fields:

Fields of `media` containing media-specific metadata:

- `type: {audio,image,video,web}`: The media type.
- `type: {audio,document,image,video,web}`: The media type.

If the media type is `audio`, `image`, or `video`, the media object contains a
field named `items`, which is a list of objects containing metadata for
individual items in the package.
If the media type is `audio`, `document`, `image`, or `video`, the media object
contains a field named `items`, which is a list of objects containing metadata
for individual items in the package.

When authoring metadata YAML, each item is an object with a `path` field
containing the path to the item. For example, for an `audio` package:
Expand Down
87 changes: 87 additions & 0 deletions src/document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use super::*;

#[derive(Clone, Debug, Decode, Encode, PartialEq, Serialize)]
pub(crate) struct Document {
#[n(0)]
pub(crate) path: RelativePath,
#[n(1)]
#[serde(rename = "type")]
pub(crate) ty: DocumentType,
}

impl Content for Document {
const LABEL: &'static str = "Document";

type Type = DocumentType;

fn info(&self, builder: InfoBuilder) -> InfoBuilder {
builder.value("type", self.ty)
}

fn load(_root: &Utf8Path, path: RelativePath) -> Result<Item<Self>> {
let ty = DocumentType::from_path(&path).context(error::Path { path: &path })?;

Ok(Item {
content: Self { path, ty },
title: None,
})
}

fn path(&self) -> &RelativePath {
&self.path
}

#[cfg(test)]
fn test(path: &str) -> Self {
let path = path.parse::<RelativePath>().unwrap();
let ty = DocumentType::from_path(&path).unwrap();
Self { path, ty }
}

fn ty(&self) -> Self::Type {
self.ty
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn load() {
let (_tempdir, root) = tempdir();

std::fs::write(root.join("foo.pdf"), b"%PDF-1.7\n").unwrap();

assert_eq!(
Document::load(&root, "foo.pdf".parse().unwrap()).unwrap(),
Item {
content: Document {
path: "foo.pdf".parse().unwrap(),
ty: DocumentType::Pdf,
},
title: None,
},
);
}

#[test]
fn load_rejects_invalid_extension() {
let (_tempdir, root) = tempdir();

assert_eq!(
Document::load(&root, "foo.txt".parse().unwrap())
.unwrap_err()
.to_string(),
"invalid path `foo.txt`",
);
}

#[test]
fn serialize() {
assert_eq!(
serde_json::to_string(&Document::test("foo.pdf")).unwrap(),
r#"{"path":"foo.pdf","type":"pdf"}"#,
);
}
}
58 changes: 58 additions & 0 deletions src/document_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use super::*;

#[derive(Clone, Copy, Debug, Decode, Display, Encode, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "UPPERCASE")]
pub(crate) enum DocumentType {
#[n(0)]
Pdf,
}

impl ContentType for DocumentType {
const EXTENSIONS: &[&str] = &["pdf"];

fn from_extension(extension: &str) -> Option<Self> {
match extension {
"pdf" => Some(Self::Pdf),
_ => None,
}
}

fn resource_type(self) -> ResourceType {
match self {
Self::Pdf => ResourceType::Pdf,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn display() {
assert_eq!(DocumentType::Pdf.to_string(), "PDF");
}

#[test]
fn from_path() {
#[track_caller]
fn case(path: &str, expected: Result<DocumentType, PathError>) {
assert_eq!(DocumentType::from_path(&path.parse().unwrap()), expected);
}

case("foo.pdf", Ok(DocumentType::Pdf));
case(
"foo.txt",
Err(PathError::Extension {
extensions: &["pdf"],
}),
);
case(
"foo",
Err(PathError::Extension {
extensions: &["pdf"],
}),
);
}
}
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ use {
display_path::DisplayPath,
display_sample_rate::DisplaySampleRate,
display_secret::DisplaySecret,
document::Document,
document_type::DocumentType,
entries::Entries,
envelope::Envelope,
exif_decoder::ExifDecoder,
Expand Down Expand Up @@ -329,6 +331,8 @@ mod display_millis;
mod display_path;
mod display_sample_rate;
mod display_secret;
mod document;
mod document_type;
mod encode;
mod encoder;
mod entries;
Expand Down
15 changes: 12 additions & 3 deletions src/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,29 @@ pub(crate) enum Media {
items: Vec<Item<Audio>>,
},
#[n(1)]
Document {
#[n(0)]
items: Vec<Item<Document>>,
},
#[n(2)]
Image {
#[n(0)]
items: Vec<Item<Image>>,
},
#[n(2)]
#[n(3)]
Video {
#[n(0)]
items: Vec<Item<Video>>,
},
#[n(3)]
#[n(4)]
Web,
}

impl Media {
pub(crate) fn item(&self, i: usize) -> Option<&dyn MediaItem> {
match self {
Self::Audio { items } => items.get(i).map(|item| item as &dyn MediaItem),
Self::Document { items } => items.get(i).map(|item| item as &dyn MediaItem),
Self::Image { items } => items.get(i).map(|item| item as &dyn MediaItem),
Self::Video { items } => items.get(i).map(|item| item as &dyn MediaItem),
Self::Web => unreachable!(),
Expand All @@ -42,6 +48,7 @@ impl Media {
pub(crate) fn item_count(&self) -> usize {
match self {
Self::Audio { items } => items.len(),
Self::Document { items } => items.len(),
Self::Image { items } => items.len(),
Self::Video { items } => items.len(),
Self::Web => unreachable!(),
Expand All @@ -55,6 +62,7 @@ impl Media {
pub(crate) fn items<'a>(&'a self) -> Box<dyn Iterator<Item = &dyn MediaItem> + 'a> {
match self {
Self::Audio { items } => Box::new(items.iter().map(|item| item as &dyn MediaItem)),
Self::Document { items } => Box::new(items.iter().map(|item| item as &dyn MediaItem)),
Self::Image { items } => Box::new(items.iter().map(|item| item as &dyn MediaItem)),
Self::Video { items } => Box::new(items.iter().map(|item| item as &dyn MediaItem)),
Self::Web => unreachable!(),
Expand All @@ -81,14 +89,15 @@ impl Media {
impl MediaType {
pub(crate) fn has_items(self) -> bool {
match self {
Self::Audio | Self::Image | Self::Video => true,
Self::Audio | Self::Document | Self::Image | Self::Video => true,
Self::Web => false,
}
}

pub(crate) fn item_noun(self) -> &'static str {
match self {
Self::Audio => "track",
Self::Document => "document",
Self::Image => "image",
Self::Video => "video",
Self::Web => unreachable!(),
Expand Down
3 changes: 3 additions & 0 deletions src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ impl Metadata {
Media::Audio { items } => {
files.extend(items.iter().map(|audio| audio.path().into()));
}
Media::Document { items } => {
files.extend(items.iter().map(|document| document.path().into()));
}
Media::Image { items } => {
files.extend(items.iter().map(|image| image.path().into()));
}
Expand Down
17 changes: 13 additions & 4 deletions src/resource_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub(crate) enum ResourceType {
Markdown,
Mp3,
Mp4,
Pdf,
Png,
Webm,
}
Expand All @@ -16,9 +17,14 @@ impl ResourceType {
pub(crate) fn content_disposition(self) -> Option<HeaderValue> {
match self {
Self::Binary => Some(HeaderValue::from_static("attachment")),
Self::Flac | Self::Jpeg | Self::Markdown | Self::Mp3 | Self::Mp4 | Self::Png | Self::Webm => {
None
}
Self::Flac
| Self::Jpeg
| Self::Markdown
| Self::Mp3
| Self::Mp4
| Self::Pdf
| Self::Png
| Self::Webm => None,
}
}

Expand All @@ -30,6 +36,7 @@ impl ResourceType {
Self::Markdown => mime::TEXT_PLAIN_UTF_8,
Self::Mp3 => "audio/mpeg".parse().unwrap(),
Self::Mp4 => "video/mp4".parse().unwrap(),
Self::Pdf => mime::APPLICATION_PDF,
Self::Png => mime::IMAGE_PNG,
Self::Webm => "video/webm".parse().unwrap(),
}
Expand All @@ -42,6 +49,7 @@ impl ResourceType {
"md" => Some(Self::Markdown),
"mp3" => Some(Self::Mp3),
"mp4" => Some(Self::Mp4),
"pdf" => Some(Self::Pdf),
"png" => Some(Self::Png),
"webm" => Some(Self::Webm),
_ => None,
Expand All @@ -50,7 +58,7 @@ impl ResourceType {

pub(crate) fn sandbox(self) -> bool {
match self {
Self::Binary | Self::Jpeg | Self::Markdown | Self::Png => true,
Self::Binary | Self::Jpeg | Self::Markdown | Self::Pdf | Self::Png => true,
Self::Flac | Self::Mp3 | Self::Mp4 | Self::Webm => false,
}
}
Expand All @@ -77,6 +85,7 @@ mod tests {
case("foo.md", Some(ResourceType::Markdown));
case("foo.mp3", Some(ResourceType::Mp3));
case("foo.mp4", Some(ResourceType::Mp4));
case("foo.pdf", Some(ResourceType::Pdf));
case("foo.png", Some(ResourceType::Png));
case("foo.webm", Some(ResourceType::Webm));

Expand Down
7 changes: 6 additions & 1 deletion src/subcommand/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ use {
},
std::net::TcpStream,
templates::{
AudioHtml, DirectoryHtml, FilesHtml, ImageHtml, MediaHtml, PackageHtml, PackagesHtml, VideoHtml,
AudioHtml, DirectoryHtml, DocumentHtml, FilesHtml, ImageHtml, MediaHtml, PackageHtml,
PackagesHtml, VideoHtml,
},
tokio::{net::TcpListener, runtime, task::block_in_place},
tower_http::set_header::SetResponseHeaderLayer,
Expand Down Expand Up @@ -324,6 +325,10 @@ impl Serve {
"/media/audio/{fingerprint}/item/{item}",
get(route::media_audio_item),
)
.route(
"/media/document/{fingerprint}/item/{item}",
get(route::media_document_item),
)
.route(
"/media/image/{fingerprint}/item/{item}",
get(route::media_image_item),
Expand Down
28 changes: 28 additions & 0 deletions src/subcommand/serve/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,25 @@ pub(crate) async fn media_audio_item(
})
}

pub(crate) async fn media_document_item(
server: ServerExtension,
Path((fingerprint, Ordinal(item))): Path<(Fingerprint, Ordinal)>,
range: Option<TypedHeader<headers::Range>>,
) -> ServerResult<Resource> {
block_in_place(|| {
Ok(
server
.media_item(
fingerprint,
item,
MediaType::Document,
MediaItemResource::Original,
)?
.range(range),
)
})
}

pub(crate) async fn media_image_item(
server: ServerExtension,
Path((fingerprint, Ordinal(item))): Path<(Fingerprint, Ordinal)>,
Expand Down Expand Up @@ -354,6 +373,15 @@ pub(crate) async fn package_item(
.page(server_config.url.clone())
.into_response(),
),
Media::Document { .. } => Ok(
DocumentHtml {
document: index,
fingerprint,
metadata,
}
.page(server_config.url.clone())
.into_response(),
),
Media::Image { .. } => Ok(
ImageHtml {
fingerprint,
Expand Down
Loading