diff --git a/CHANGELOG.md b/CHANGELOG.md index 37bf49a..aa04da7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ In Development - Log errors that cause 404 and 500 responses - Added breadcrumbs to HTML views of collections - `FAST_NOT_EXIST` components are now checked for case-insensitively +- Add links to version & asset metadata to the web view v0.2.0 (2024-02-07) ------------------- diff --git a/src/dandi/mod.rs b/src/dandi/mod.rs index 1c1dfaf..93440d1 100644 --- a/src/dandi/mod.rs +++ b/src/dandi/mod.rs @@ -111,12 +111,22 @@ impl DandiClient { pub(crate) fn get_all_dandisets( &self, ) -> impl Stream> + '_ { - self.paginate(self.get_url(["dandisets"])) + self.paginate::(self.get_url(["dandisets"])) + .map_ok(|ds| ds.with_metadata_urls(self)) } pub(crate) fn dandiset(&self, dandiset_id: DandisetId) -> DandisetEndpoint<'_> { DandisetEndpoint::new(self, dandiset_id) } + + fn version_metadata_url(&self, dandiset_id: &DandisetId, version_id: &VersionId) -> Url { + self.get_url([ + "dandisets", + dandiset_id.as_ref(), + "versions", + version_id.as_ref(), + ]) + } } #[derive(Clone, Debug)] @@ -139,21 +149,29 @@ impl<'a> DandisetEndpoint<'a> { pub(crate) async fn get(&self) -> Result { self.client - .get( + .get::( self.client .get_url(["dandisets", self.dandiset_id.as_ref()]), ) .await + .map(|ds| ds.with_metadata_urls(self.client)) } pub(crate) fn get_all_versions( &self, ) -> impl Stream> + '_ { - self.client.paginate(self.client.get_url([ - "dandisets", - self.dandiset_id.as_ref(), - "versions", - ])) + self.client + .paginate::(self.client.get_url([ + "dandisets", + self.dandiset_id.as_ref(), + "versions", + ])) + .map_ok(|v| { + let url = self + .client + .version_metadata_url(&self.dandiset_id, &v.version); + v.with_metadata_url(url) + }) } } @@ -175,7 +193,7 @@ impl<'a> VersionEndpoint<'a> { pub(crate) async fn get(&self) -> Result { self.client - .get(self.client.get_url([ + .get::(self.client.get_url([ "dandisets", self.dandiset_id.as_ref(), "versions", @@ -183,24 +201,37 @@ impl<'a> VersionEndpoint<'a> { "info", ])) .await + .map(|v| v.with_metadata_url(self.metadata_url())) + } + + fn metadata_url(&self) -> Url { + self.client + .version_metadata_url(&self.dandiset_id, &self.version_id) + } + + fn asset_metadata_url(&self, asset_id: &str) -> Url { + self.client.get_url([ + "dandisets", + self.dandiset_id.as_ref(), + "versions", + self.version_id.as_ref(), + "assets", + asset_id, + ]) } pub(crate) async fn get_metadata(&self) -> Result { let data = self .client - .get::(self.client.get_url([ - "dandisets", - self.dandiset_id.as_ref(), - "versions", - self.version_id.as_ref(), - ])) + .get::(self.metadata_url()) .await?; Ok(VersionMetadata(dump_json_as_yaml(data).into_bytes())) } async fn get_asset_by_id(&self, id: &str) -> Result { - self.client - .get(self.client.get_url([ + let raw_asset = self + .client + .get::(self.client.get_url([ "dandisets", self.dandiset_id.as_ref(), "versions", @@ -209,7 +240,8 @@ impl<'a> VersionEndpoint<'a> { id, "info", ])) - .await + .await?; + raw_asset.try_into_asset(self).map_err(Into::into) } pub(crate) fn get_root_children( @@ -272,14 +304,14 @@ impl<'a> VersionEndpoint<'a> { .append_pair("metadata", "1") .append_pair("order", "path"); let dirpath = path.to_dir_path(); - let stream = self.client.paginate::(url.clone()); + let stream = self.client.paginate::(url.clone()); tokio::pin!(stream); while let Some(asset) = stream.try_next().await? { - if asset.path() == path { - return Ok(AtAssetPath::Asset(asset)); - } else if asset.path().is_strictly_under(&dirpath) { + if &asset.path == path { + return Ok(AtAssetPath::Asset(asset.try_into_asset(self)?)); + } else if asset.path.is_strictly_under(&dirpath) { return Ok(AtAssetPath::Folder(AssetFolder { path: dirpath })); - } else if asset.path().as_ref() > dirpath.as_ref() { + } else if asset.path.as_ref() > dirpath.as_ref() { break; } } @@ -308,18 +340,10 @@ impl<'a> VersionEndpoint<'a> { match self.get_path(&zarr_path).await? { AtAssetPath::Folder(_) => continue, AtAssetPath::Asset(Asset::Blob(_)) => { - let mut url = self.client.get_url([ - "dandisets", - self.dandiset_id.as_ref(), - "versions", - self.version_id.as_ref(), - "assets", - ]); - url.query_pairs_mut().append_pair("path", path.as_ref()); return Err(DandiError::PathUnderBlob { path: path.clone(), blob_path: zarr_path, - }); + }) } AtAssetPath::Asset(Asset::Zarr(zarr)) => { let s3 = self.client.get_s3client_for_zarr(&zarr).await?; @@ -414,6 +438,8 @@ pub(crate) enum DandiError { source: ZarrToS3Error, }, #[error(transparent)] + AssetType(#[from] AssetTypeError), + #[error(transparent)] S3(#[from] S3Error), } diff --git a/src/dandi/types.rs b/src/dandi/types.rs index 485f71b..78f3a70 100644 --- a/src/dandi/types.rs +++ b/src/dandi/types.rs @@ -13,29 +13,78 @@ pub(super) struct Page { } #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] -pub(crate) struct Dandiset { - pub(crate) identifier: DandisetId, +pub(crate) struct RawDandiset { + identifier: DandisetId, #[serde(with = "time::serde::rfc3339")] - pub(crate) created: OffsetDateTime, + created: OffsetDateTime, #[serde(with = "time::serde::rfc3339")] - pub(crate) modified: OffsetDateTime, + modified: OffsetDateTime, //contact_person: String, //embargo_status: ..., + draft_version: RawDandisetVersion, + most_recent_published_version: Option, +} + +impl RawDandiset { + pub(super) fn with_metadata_urls(self, client: &super::DandiClient) -> Dandiset { + let draft_version = self + .draft_version + .with_metadata_url(client.version_metadata_url(&self.identifier, &VersionId::Draft)); + let most_recent_published_version = self.most_recent_published_version.map(|v| { + let url = client.version_metadata_url(&self.identifier, &v.version); + v.with_metadata_url(url) + }); + Dandiset { + identifier: self.identifier, + created: self.created, + modified: self.modified, + draft_version, + most_recent_published_version, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct Dandiset { + pub(crate) identifier: DandisetId, + pub(crate) created: OffsetDateTime, + pub(crate) modified: OffsetDateTime, pub(crate) draft_version: DandisetVersion, pub(crate) most_recent_published_version: Option, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] -pub(crate) struct DandisetVersion { - pub(crate) version: VersionId, +pub(crate) struct RawDandisetVersion { + pub(super) version: VersionId, //name: String, //asset_count: u64, - pub(crate) size: i64, + size: i64, //status: ..., #[serde(with = "time::serde::rfc3339")] - pub(crate) created: OffsetDateTime, + created: OffsetDateTime, #[serde(with = "time::serde::rfc3339")] + modified: OffsetDateTime, +} + +impl RawDandisetVersion { + pub(super) fn with_metadata_url(self, metadata_url: Url) -> DandisetVersion { + DandisetVersion { + version: self.version, + size: self.size, + created: self.created, + modified: self.modified, + metadata_url, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct DandisetVersion { + pub(crate) version: VersionId, + pub(crate) size: i64, + pub(crate) created: OffsetDateTime, pub(crate) modified: OffsetDateTime, + pub(crate) metadata_url: Url, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -95,28 +144,19 @@ struct RawFolderEntryAsset { asset_id: String, } +#[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum AtAssetPath { Folder(AssetFolder), Asset(Asset), } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] -#[serde(try_from = "RawAsset")] +#[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum Asset { Blob(BlobAsset), Zarr(ZarrAsset), } -impl Asset { - pub(crate) fn path(&self) -> &PurePath { - match self { - Asset::Blob(a) => &a.path, - Asset::Zarr(a) => &a.path, - } - } -} - #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct BlobAsset { pub(crate) asset_id: String, @@ -126,6 +166,7 @@ pub(crate) struct BlobAsset { pub(crate) created: OffsetDateTime, pub(crate) modified: OffsetDateTime, pub(crate) metadata: AssetMetadata, + pub(crate) metadata_url: Url, } impl BlobAsset { @@ -160,6 +201,7 @@ pub(crate) struct ZarrAsset { pub(crate) created: OffsetDateTime, pub(crate) modified: OffsetDateTime, pub(crate) metadata: AssetMetadata, + pub(crate) metadata_url: Url, } impl ZarrAsset { @@ -211,11 +253,11 @@ pub(crate) struct AssetDigests { } #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] -struct RawAsset { +pub(super) struct RawAsset { asset_id: String, blob: Option, zarr: Option, - path: PurePath, + pub(super) path: PurePath, size: i64, #[serde(with = "time::serde::rfc3339")] created: OffsetDateTime, @@ -224,34 +266,38 @@ struct RawAsset { metadata: AssetMetadata, } -impl TryFrom for Asset { - type Error = AssetTypeError; - - fn try_from(value: RawAsset) -> Result { - match (value.blob, value.zarr) { +impl RawAsset { + pub(super) fn try_into_asset( + self, + endpoint: &super::VersionEndpoint<'_>, + ) -> Result { + let metadata_url = endpoint.asset_metadata_url(&self.asset_id); + match (self.blob, self.zarr) { (Some(blob_id), None) => Ok(Asset::Blob(BlobAsset { - asset_id: value.asset_id, + asset_id: self.asset_id, blob_id, - path: value.path, - size: value.size, - created: value.created, - modified: value.modified, - metadata: value.metadata, + path: self.path, + size: self.size, + created: self.created, + modified: self.modified, + metadata: self.metadata, + metadata_url, })), (None, Some(zarr_id)) => Ok(Asset::Zarr(ZarrAsset { - asset_id: value.asset_id, + asset_id: self.asset_id, zarr_id, - path: value.path, - size: value.size, - created: value.created, - modified: value.modified, - metadata: value.metadata, + path: self.path, + size: self.size, + created: self.created, + modified: self.modified, + metadata: self.metadata, + metadata_url, })), (None, None) => Err(AssetTypeError::Neither { - asset_id: value.asset_id, + asset_id: self.asset_id, }), (Some(_), Some(_)) => Err(AssetTypeError::Both { - asset_id: value.asset_id, + asset_id: self.asset_id, }), } } diff --git a/src/dav/html.rs b/src/dav/html.rs index dab85fa..4c4dc9b 100644 --- a/src/dav/html.rs +++ b/src/dav/html.rs @@ -75,6 +75,8 @@ pub(super) struct ColRow { serialize_with = "maybe_timestamp" )] modified: Option, + #[serde(skip_serializing_if = "Option::is_none")] + metadata_url: Option, } impl ColRow { @@ -87,6 +89,7 @@ impl ColRow { size: None, created: None, modified: None, + metadata_url: None, } } } @@ -110,6 +113,7 @@ impl From for ColRow { size: col.size, created: col.created, modified: col.modified, + metadata_url: col.metadata_url.map(Into::into), } } } @@ -124,6 +128,7 @@ impl From for ColRow { size: item.size, created: item.created, modified: item.modified, + metadata_url: item.metadata_url.map(Into::into), } } } diff --git a/src/dav/mod.rs b/src/dav/mod.rs index d3e5369..b83223a 100644 --- a/src/dav/mod.rs +++ b/src/dav/mod.rs @@ -270,15 +270,15 @@ impl DandiDav { Ok(DavResourceWithChildren::Collection { col, children }) } DavPath::Dandiset { dandiset_id } => { - let ds = self.dandi.dandiset(dandiset_id.clone()).get().await?; + let mut ds = self.dandi.dandiset(dandiset_id.clone()).get().await?; let draft = DavResource::Collection(DavCollection::dandiset_version( ds.draft_version.clone(), version_path(dandiset_id, &VersionSpec::Draft), )); - let children = match ds.most_recent_published_version { - Some(ref v) => { + let children = match ds.most_recent_published_version.take() { + Some(v) => { let latest = DavCollection::dandiset_version( - v.clone(), + v, version_path(dandiset_id, &VersionSpec::Latest), ); let latest = DavResource::Collection(latest); diff --git a/src/dav/static/styles.css b/src/dav/static/styles.css index 5d64eb0..5278d80 100644 --- a/src/dav/static/styles.css +++ b/src/dav/static/styles.css @@ -1,24 +1,21 @@ +body { + font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; +} + div.breadcrumbs { margin-bottom: 16px; - font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; } table { border-collapse: collapse; border-spacing: 0; - border-style: solid; - border-width: 1px; - margin-top: 0; margin-bottom: 16px; - /*display: block;*/ - /*width: 100%;*/ + margin-top: 0; overflow: auto; - font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; } table tr { background-color: #FFFFFF; - border-top: 1px solid #C6CBD1; } table tr:nth-child(2n) { @@ -30,18 +27,32 @@ table th { } table td, table th { - padding: 6px 13px; border: 1px solid #DFE2E5; + padding: 6px 13px; } -table td.dir { +table.collection td.name.dir span.item-link a { font-weight: bold; } -table td.null { +div.link-with-metadata { + align-items: stretch; + display: flex; + justify-content: space-between; +} + +div.link-with-metadata span.fill { + min-width: 1em; +} + +div.link-with-metadata span.metadata-link a { + text-decoration: underline; +} + +table.collection td.null { text-align: center; } footer { - font: 9px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; + font-size: 9px; } diff --git a/src/dav/templates/collection.html.tera b/src/dav/templates/collection.html.tera index 283e36f..e284193 100644 --- a/src/dav/templates/collection.html.tera +++ b/src/dav/templates/collection.html.tera @@ -6,11 +6,11 @@ - +
@@ -20,7 +20,15 @@ {% for r in rows %} - + diff --git a/src/dav/types.rs b/src/dav/types.rs index 870a11f..c99e848 100644 --- a/src/dav/types.rs +++ b/src/dav/types.rs @@ -197,6 +197,7 @@ pub(super) struct DavCollection { pub(super) modified: Option, pub(super) size: Option, pub(super) kind: ResourceKind, + pub(super) metadata_url: Option, } impl DavCollection { @@ -239,6 +240,7 @@ impl DavCollection { modified: None, size: None, kind: ResourceKind::Root, + metadata_url: None, } } @@ -253,6 +255,7 @@ impl DavCollection { modified: None, size: None, kind: ResourceKind::DandisetIndex, + metadata_url: None, } } @@ -266,6 +269,7 @@ impl DavCollection { modified: None, size: None, kind: ResourceKind::DandisetReleases, + metadata_url: None, } } @@ -276,6 +280,7 @@ impl DavCollection { modified: Some(v.modified), size: Some(v.size), kind: ResourceKind::Version, + metadata_url: Some(v.metadata_url), } } @@ -290,6 +295,7 @@ impl DavCollection { modified: None, size: None, kind: ResourceKind::ZarrIndex, + metadata_url: None, } } } @@ -339,6 +345,7 @@ impl From for DavCollection { modified: Some(ds.modified), size: None, kind: ResourceKind::Dandiset, + metadata_url: None, } } } @@ -351,6 +358,7 @@ impl From for DavCollection { modified: None, size: None, kind: ResourceKind::Directory, + metadata_url: None, } } } @@ -363,6 +371,7 @@ impl From for DavCollection { modified: Some(zarr.modified), size: Some(zarr.size), kind: ResourceKind::Zarr, + metadata_url: Some(zarr.metadata_url), } } } @@ -375,6 +384,7 @@ impl From for DavCollection { modified: None, size: None, kind: ResourceKind::Directory, + metadata_url: None, } } } @@ -387,6 +397,7 @@ impl From for DavCollection { modified: None, size: None, kind: ResourceKind::Directory, + metadata_url: None, } } } @@ -399,6 +410,7 @@ impl From for DavCollection { modified: None, size: None, kind: ResourceKind::Zarr, + metadata_url: None, } } } @@ -411,6 +423,7 @@ impl From for DavCollection { modified: None, size: None, kind: ResourceKind::Directory, + metadata_url: None, } } } @@ -425,6 +438,7 @@ pub(super) struct DavItem { pub(super) etag: Option, pub(super) kind: ResourceKind, pub(super) content: DavContent, + pub(super) metadata_url: Option, } impl DavItem { @@ -502,6 +516,7 @@ impl From for DavItem { etag: None, kind: ResourceKind::VersionMetadata, content: DavContent::Blob(blob), + metadata_url: None, } } } @@ -528,6 +543,7 @@ impl From for DavItem { etag, kind: ResourceKind::Blob, content, + metadata_url: Some(blob.metadata_url), } } } @@ -543,6 +559,7 @@ impl From for DavItem { etag: Some(entry.etag), kind: ResourceKind::ZarrEntry, content: DavContent::Redirect(entry.url), + metadata_url: None, } } } @@ -558,6 +575,7 @@ impl From for DavItem { etag: Some(entry.etag), kind: ResourceKind::ZarrEntry, content: DavContent::Redirect(entry.url), + metadata_url: None, } } }
Name Type
{{r.name}}{% if r.is_dir %}/{% endif %} + + {{r.kind}}