Skip to content

Commit

Permalink
Save metadata to metadata.json (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Oct 9, 2024
1 parent e30e596 commit e446814
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 53 deletions.
25 changes: 9 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,7 @@ slash.
Filepack has no way of tracking empty directories, the presence of which are an
error when creating or verifying a manifest.

Manifests contain an object with one mandatory key, `files`, and one optional
key, `metadata`.
Manifests contain an object with one mandatory key, `files`.

### `files`

Expand All @@ -182,26 +181,20 @@ An example manifest for a directory containing the files `README.md` and
}
```

### `metadata`
Metadata
--------

The value of the optional `metadata` key is an object containing metadata
describing the package, with keys and values as follows:
`filepack create` can optionally write metadata describing the contents of the
package to a file named `metadata.json`, containing a JSON object with the
following keys:

- `title`: A string containing the package's human-readable title.

An example manifest with metadata:
An example `metadata.json`:

```json
```js
{
"files": {
"tobins-spirit-guide.md": {
"hash": "5a9a6d96244ec398545fc0c98c2cb7ed52511b025c19e9ad1e3c1ef4ac8575ad",
"size": 175934
}
},
"metadata": {
"title": "Tobin's Spirit Guide"
}
"title": "Tobin's Spirit Guide"
}
```

Expand Down
11 changes: 11 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ pub(crate) enum Error {
},
#[snafu(display("failed to deserialize metadata at `{path}`"))]
DeserializeMetadata {
backtrace: Option<Backtrace>,
path: DisplayPath,
source: serde_json::Error,
},
#[snafu(display("failed to deserialize metadata template at `{path}`"))]
DeserializeMetadataTemplate {
backtrace: Option<Backtrace>,
path: DisplayPath,
source: serde_yaml::Error,
Expand Down Expand Up @@ -59,6 +65,11 @@ pub(crate) enum Error {
backtrace: Option<Backtrace>,
path: DisplayPath,
},
#[snafu(display("metadata `{path}` already exists"))]
MetadataAlreadyExists {
backtrace: Option<Backtrace>,
path: DisplayPath,
},
#[snafu(display("manifest hash mismatch"))]
ManifestHashMismatch { backtrace: Option<Backtrace> },
#[snafu(display("manifest `{path}` not found"))]
Expand Down
39 changes: 39 additions & 0 deletions src/io_result_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use super::*;

pub(crate) trait IoResultExt<T> {
fn into_option(self) -> io::Result<Option<T>>;
}

impl<T> IoResultExt<T> for io::Result<T> {
fn into_option(self) -> io::Result<Option<T>> {
match self {
Err(err) => {
if err.kind() == io::ErrorKind::NotFound {
Ok(None)
} else {
Err(err)
}
}
Ok(value) => Ok(Some(value)),
}
}
}

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

#[test]
fn into_option() {
Err::<(), io::Error>(io::Error::new(io::ErrorKind::InvalidInput, "foo"))
.into_option()
.unwrap_err();

assert_eq!(
Err::<(), io::Error>(io::Error::new(io::ErrorKind::NotFound, "foo"))
.into_option()
.unwrap(),
None,
);
}
}
11 changes: 6 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use {
self::{
arguments::Arguments, display_path::DisplayPath, display_secret::DisplaySecret, entry::Entry,
error::Error, hash::Hash, lint::Lint, lint_group::LintGroup, list::List, manifest::Manifest,
metadata::Metadata, options::Options, owo_colorize_ext::OwoColorizeExt,
private_key::PrivateKey, public_key::PublicKey, relative_path::RelativePath,
signature::Signature, signature_error::SignatureError, style::Style, subcommand::Subcommand,
template::Template, utf8_path_ext::Utf8PathExt,
error::Error, hash::Hash, io_result_ext::IoResultExt, lint::Lint, lint_group::LintGroup,
list::List, manifest::Manifest, metadata::Metadata, options::Options,
owo_colorize_ext::OwoColorizeExt, private_key::PrivateKey, public_key::PublicKey,
relative_path::RelativePath, signature::Signature, signature_error::SignatureError,
style::Style, subcommand::Subcommand, template::Template, utf8_path_ext::Utf8PathExt,
},
blake3::Hasher,
camino::{Utf8Component, Utf8Path, Utf8PathBuf},
Expand Down Expand Up @@ -40,6 +40,7 @@ mod display_secret;
mod entry;
mod error;
mod hash;
mod io_result_ext;
mod lint;
mod lint_group;
mod list;
Expand Down
2 changes: 0 additions & 2 deletions src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ use super::*;
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub(crate) struct Manifest {
pub(crate) files: BTreeMap<RelativePath, Entry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) metadata: Option<Metadata>,
}

impl Manifest {
Expand Down
4 changes: 4 additions & 0 deletions src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ pub(crate) struct Metadata {
title: String,
}

impl Metadata {
pub(crate) const FILENAME: &'static str = "metadata.json";
}

impl From<Template> for Metadata {
fn from(Template { title }: Template) -> Self {
Self { title }
Expand Down
23 changes: 13 additions & 10 deletions src/subcommand/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,19 @@ impl Create {
root.join(Manifest::FILENAME)
};

let metadata = if let Some(path) = &self.metadata {
if let Some(path) = &self.metadata {
let yaml = fs::read_to_string(path).context(error::Io { path })?;
Some(
serde_yaml::from_str::<Template>(&yaml)
.context(error::DeserializeMetadata { path })?
.into(),
)
} else {
None
};
let template = serde_yaml::from_str::<Template>(&yaml)
.context(error::DeserializeMetadataTemplate { path })?;
let path = root.join(Metadata::FILENAME);
ensure! {
self.force || !path.try_exists().context(error::Io { path: &path })?,
error::MetadataAlreadyExists { path: &path },
}
let metadata = Metadata::from(template);
let json = serde_json::to_string(&metadata).unwrap();
fs::write(&path, json).context(error::Io { path: &path })?;
}

let cleaned_manifest = current_dir.join(&manifest).lexiclean();

Expand Down Expand Up @@ -170,7 +173,7 @@ impl Create {
bar.inc(entry.size);
}

let json = serde_json::to_string(&Manifest { files, metadata }).unwrap();
let json = serde_json::to_string(&Manifest { files }).unwrap();

fs::write(&manifest, &json).context(error::Io { path: manifest })?;

Expand Down
12 changes: 12 additions & 0 deletions src/subcommand/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,18 @@ mismatched file: `{path}`
}
}

{
let path = root.join(Metadata::FILENAME);

if let Some(json) = fs::read_to_string(&path)
.into_option()
.context(error::Io { path: &path })?
{
serde_json::from_str::<Metadata>(&json)
.context(error::DeserializeMetadata { path: &path })?;
}
}

if let Some(key) = self.key {
ensure! {
signatures.contains(&key),
Expand Down
34 changes: 33 additions & 1 deletion tests/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,9 +462,11 @@ fn with_metadata() {
.success();

dir.child("foo/filepack.json").assert(
r#"{"files":{"bar":{"hash":"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262","size":0}},"metadata":{"title":"Foo"}}"#,
r#"{"files":{"bar":{"hash":"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262","size":0},"metadata.json":{"hash":"bf15e6d4fc37be38eb02255ad98c52ac3b54acd1ff7b8de56c343f022eb770de","size":15}}}"#,
);

dir.child("foo/metadata.json").assert(r#"{"title":"Foo"}"#);

Command::cargo_bin("filepack")
.unwrap()
.args(["verify", "foo"])
Expand Down Expand Up @@ -631,3 +633,33 @@ fn existing_signature_will_not_be_overwritten() {
))
.failure();
}

#[test]
fn metadata_already_exists() {
let dir = TempDir::new().unwrap();

dir.child("foo/bar").touch().unwrap();

dir.child("foo/metadata.json").touch().unwrap();

dir.child("metadata.yaml").write_str("title: Foo").unwrap();

Command::cargo_bin("filepack")
.unwrap()
.args(["create", "foo", "--metadata", "metadata.yaml"])
.current_dir(&dir)
.assert()
.stderr(format!(
"error: metadata `foo{SEPARATOR}metadata.json` already exists\n"
))
.failure();

Command::cargo_bin("filepack")
.unwrap()
.args(["create", "foo", "--metadata", "metadata.yaml", "--force"])
.current_dir(&dir)
.assert()
.success();

dir.child("foo/metadata.json").assert(r#"{"title":"Foo"}"#);
}
67 changes: 48 additions & 19 deletions tests/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ fn extra_fields_are_not_allowed() {
.stderr(
"\
error: failed to deserialize manifest at `filepack.json`
└─ unknown field `foo`, expected `files` or `metadata` at line 1 column 17\n",
└─ unknown field `foo`, expected `files` at line 1 column 17\n",
)
.failure();
}
Expand Down Expand Up @@ -453,24 +453,6 @@ fn with_manifest_path() {
.success();
}

#[test]
fn manifest_metadata_allows_unknown_keys() {
let dir = TempDir::new().unwrap();

dir.child("foo/bar").touch().unwrap();

dir.child("foo/filepack.json").write_str(
r#"{"files":{"bar":{"hash":"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262","size":0}},"metadata":{"title":"Foo","bar":100}}"#,
).unwrap();

Command::cargo_bin("filepack")
.unwrap()
.args(["verify", "foo"])
.current_dir(&dir)
.assert()
.success();
}

#[test]
fn missing_signature_file_extension() {
let dir = TempDir::new().unwrap();
Expand Down Expand Up @@ -806,3 +788,50 @@ fn ignore_missing() {
.assert()
.success();
}

#[test]
fn metadata_allows_unknown_keys() {
let dir = TempDir::new().unwrap();

dir
.child("filepack.json")
.write_str(r#"{"files":{"metadata.json":{"hash":"1845a2ea1b86a250cb1c24115032cc0fdc064001f59af4a5e9a17be5cd7efbbc","size":25}}}"#)
.unwrap();

dir
.child("metadata.json")
.write_str(r#"{"title":"Foo","bar":100}"#)
.unwrap();

Command::cargo_bin("filepack")
.unwrap()
.args(["verify"])
.current_dir(&dir)
.assert()
.success();
}

#[test]
fn metadata_may_not_be_invalid() {
let dir = TempDir::new().unwrap();

dir
.child("filepack.json")
.write_str(r#"{"files":{"metadata.json":{"hash":"f113b1430243e68a2976426b0e13f21e5795cc107a914816fbf6c2f511092f4b","size":13}}}"#)
.unwrap();

dir
.child("metadata.json")
.write_str(r#"{"title":100}"#)
.unwrap();

Command::cargo_bin("filepack")
.unwrap()
.args(["verify"])
.current_dir(&dir)
.assert()
.stderr(is_match(
"error: failed to deserialize metadata at `.*metadata.json`\n.*",
))
.failure();
}

0 comments on commit e446814

Please sign in to comment.