Compare commits
18
Commits
75069f52d6
...
master
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
95216f2e62
|
||
|
|
63bc705669
|
||
|
|
4eca2c45df
|
||
|
|
2568c98bbc
|
||
|
|
3f053cb41c
|
||
|
|
311608e37c
|
||
|
|
f267bdf8f1
|
||
|
|
49dfc9b4fc
|
||
|
|
ce65bc7739
|
||
|
|
791c125290
|
||
|
|
ddf8014561
|
||
|
|
3be41f1733
|
||
|
|
22153cd19a
|
||
|
|
7c7a078f96
|
||
|
|
c84d8cd28c
|
||
|
|
2367de77ca
|
||
|
|
e98da77871
|
||
|
|
cdc38eb9f3
|
Generated
+357
-252
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -9,14 +9,15 @@ members = [
|
||||
ldp = { path = "ldp" }
|
||||
|
||||
async-trait = "0.1"
|
||||
base64 = "0.22"
|
||||
bytes = "1.11"
|
||||
base64 = "0.23"
|
||||
bytes = "1.12"
|
||||
futures = "0.3"
|
||||
http = "1.4"
|
||||
http = "1.5"
|
||||
color-eyre = "0.6"
|
||||
oxigraph = "0.5"
|
||||
parse_link_header = "0.4"
|
||||
reqwest-middleware = { version = "0.5", features = ["stream"] }
|
||||
sfv = "0.15"
|
||||
slotmap = "1.1"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
+4
-1
@@ -4,9 +4,12 @@ version = "0.0.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "ldctl"
|
||||
doc = false
|
||||
|
||||
[dependencies]
|
||||
ldp.workspace = true
|
||||
futures.workspace = true
|
||||
color-eyre.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
+5
-3
@@ -1,14 +1,15 @@
|
||||
[package]
|
||||
name = "ldp"
|
||||
version = "0.0.0"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "A library to assist with the creation and maintenance of remote RDF data via LDP"
|
||||
documentation = "https://docs.rs/ldp"
|
||||
readme = "README.md"
|
||||
license = "GPL-3.0-only"
|
||||
keywords = ["ldp", "rdf", "sparql"]
|
||||
categories = ["database", "web-programming::http-client"]
|
||||
homepage = "https://graphofliberty.org"
|
||||
repository = "https://code.graphofliberty.org/graphofliberty/ldp"
|
||||
repository = "https://code.graphofliberty.org/gl/ldp"
|
||||
|
||||
[features]
|
||||
keyed = ["dep:slotmap"]
|
||||
@@ -22,6 +23,7 @@ http.workspace = true
|
||||
oxigraph.workspace = true
|
||||
parse_link_header.workspace = true
|
||||
reqwest-middleware.workspace = true
|
||||
sfv.workspace = true
|
||||
slotmap = { optional = true, workspace = true }
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
use crate::vocab;
|
||||
use oxigraph::model::NamedNodeRef;
|
||||
|
||||
/// The type of [Container](https://www.w3.org/TR/ldp/#ldpc).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ContainerType {
|
||||
Basic,
|
||||
Direct,
|
||||
Indirect,
|
||||
}
|
||||
|
||||
impl ContainerType {
|
||||
pub fn as_named_node_ref(&self) -> NamedNodeRef<'_> {
|
||||
match self {
|
||||
ContainerType::Basic => vocab::ldp::BASIC_CONTAINER,
|
||||
ContainerType::Direct => vocab::ldp::DIRECT_CONTAINER,
|
||||
ContainerType::Indirect => vocab::ldp::INDIRECT_CONTAINER,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_named_node_ref(node: NamedNodeRef<'_>) -> Option<Self> {
|
||||
match node {
|
||||
vocab::ldp::BASIC_CONTAINER => Some(ContainerType::Basic),
|
||||
vocab::ldp::DIRECT_CONTAINER => Some(ContainerType::Direct),
|
||||
vocab::ldp::INDIRECT_CONTAINER => Some(ContainerType::Indirect),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,7 @@ pub enum Error {
|
||||
/// The RDF data failed to parse.
|
||||
#[error(transparent)]
|
||||
InvalidRdfSyntax(#[from] oxigraph::io::RdfSyntaxError),
|
||||
|
||||
#[error(transparent)]
|
||||
ToStr(#[from] http::header::ToStrError),
|
||||
}
|
||||
|
||||
@@ -3,4 +3,3 @@ use reqwest_middleware::reqwest::header::HeaderName;
|
||||
pub const X_STATE_TOKEN: HeaderName = HeaderName::from_static("x-state-token");
|
||||
pub const X_IF_STATE_TOKEN: HeaderName = HeaderName::from_static("x-if-state-token");
|
||||
pub const PREFER: HeaderName = HeaderName::from_static("prefer");
|
||||
pub const PREFERENCE_APPLIED: HeaderName = HeaderName::from_static("preference-applied");
|
||||
|
||||
+11
-7
@@ -1,23 +1,27 @@
|
||||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
|
||||
mod container;
|
||||
mod error;
|
||||
pub mod header;
|
||||
mod header;
|
||||
pub mod middleware;
|
||||
pub mod model;
|
||||
mod prefer;
|
||||
mod rdf_source;
|
||||
mod resource;
|
||||
pub mod traverse;
|
||||
mod traverse;
|
||||
pub mod vocab;
|
||||
|
||||
#[cfg(feature = "keyed")]
|
||||
pub mod keyed;
|
||||
|
||||
pub use http;
|
||||
pub use oxigraph;
|
||||
pub use reqwest_middleware;
|
||||
pub use reqwest_middleware::reqwest;
|
||||
|
||||
pub use container::ContainerType;
|
||||
pub use error::{Error, Result};
|
||||
pub use prefer::{Preference, header_for_preferences};
|
||||
pub use rdf_source::{
|
||||
RdfSource, RdfSourceUpdateRequest, RdfSourceUpdateResponse, SerializationOptions,
|
||||
};
|
||||
pub use resource::{Resource, ResourceRequest, ResourceRequestBuilder, ResponseFormat};
|
||||
pub use resource::{
|
||||
Resource, ResourceRequest, ResourceRequestBuilder, ResourceType, ResponseFormat,
|
||||
};
|
||||
pub use traverse::Traverse;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use crate::RdfSource;
|
||||
use oxigraph::model::vocab::rdf;
|
||||
use oxigraph::model::{Dataset, NamedNodeRef, NamedOrBlankNodeRef, TermRef};
|
||||
|
||||
impl RdfSource<Dataset> {
|
||||
/// Return all the classes associated with this document.
|
||||
pub fn classes(&self) -> impl Iterator<Item = NamedNodeRef<'_>> {
|
||||
let subject =
|
||||
NamedOrBlankNodeRef::NamedNode(NamedNodeRef::new_unchecked(self.origin.as_str()));
|
||||
self.dataset
|
||||
.quads_for_pattern(Some(subject), Some(rdf::TYPE), None, None)
|
||||
.filter_map(|quad| match quad.object {
|
||||
TermRef::NamedNode(node) => Some(node),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,21 @@
|
||||
use oxigraph::model::{Quad, QuadRef};
|
||||
use slotmap::{SecondaryMap, SlotMap, new_key_type};
|
||||
|
||||
new_key_type! { pub struct QuadKey; }
|
||||
new_key_type! {
|
||||
/// A type which uniquely identifies a quad.
|
||||
pub struct QuadKey;
|
||||
}
|
||||
|
||||
/// A set of quads stored in a [`SlotMap`].
|
||||
///
|
||||
/// This exists as a convenience for those who want to uniquely identify a quad and associate
|
||||
/// application-specific data with it.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct KeyedDataset<T> {
|
||||
/// The underlying SlotMap.
|
||||
pub quads: SlotMap<QuadKey, Quad>,
|
||||
|
||||
/// Application-specific information to be associated with the quads.
|
||||
pub associated_data: SecondaryMap<QuadKey, T>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
//! This module contains functionality related to the content of RDF documents.
|
||||
|
||||
mod dataset;
|
||||
#[cfg(feature = "keyed")]
|
||||
mod keyed_dataset;
|
||||
|
||||
#[cfg(feature = "keyed")]
|
||||
pub use keyed_dataset::{KeyedDataset, QuadKey};
|
||||
@@ -0,0 +1,59 @@
|
||||
use crate::vocab;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use oxigraph::model::{NamedNode, NamedNodeRef};
|
||||
|
||||
/// [Request preferences](https://www.w3.org/TR/ldp/#prefer-parameters)
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Preference {
|
||||
Containment,
|
||||
Membership,
|
||||
MinimalContainer,
|
||||
Other(NamedNode),
|
||||
}
|
||||
|
||||
impl Preference {
|
||||
pub fn as_named_node_ref(&self) -> NamedNodeRef<'_> {
|
||||
match self {
|
||||
Preference::Containment => vocab::ldp::PREFER_CONTAINMENT,
|
||||
Preference::Membership => vocab::ldp::PREFER_MEMBERSHIP,
|
||||
Preference::MinimalContainer => vocab::ldp::PREFER_MINIMAL_CONTAINER,
|
||||
Preference::Other(preference) => preference.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header_for_preferences<'a, I, J>(include: I, omit: J) -> crate::Result<HeaderMap>
|
||||
where
|
||||
I: IntoIterator<Item = &'a Preference>,
|
||||
J: IntoIterator<Item = &'a Preference>,
|
||||
{
|
||||
let include_str = include
|
||||
.into_iter()
|
||||
.map(|preference| preference.as_named_node_ref().as_str().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
let omit_str = omit
|
||||
.into_iter()
|
||||
.map(|preference| preference.as_named_node_ref().as_str().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
let mut value = String::from("return=representation");
|
||||
|
||||
if !include_str.is_empty() {
|
||||
value = format!("{value}; include=\"{include_str}\"");
|
||||
}
|
||||
|
||||
if !omit_str.is_empty() {
|
||||
value = format!("{value}; omit=\"{omit_str}\"");
|
||||
}
|
||||
|
||||
let header_value =
|
||||
HeaderValue::from_str(&value).expect("Failed to produce Prefer header value");
|
||||
|
||||
Ok(HeaderMap::from_iter([(
|
||||
crate::header::PREFER,
|
||||
header_value,
|
||||
)]))
|
||||
}
|
||||
+36
-14
@@ -1,3 +1,4 @@
|
||||
use crate::resource::ResourceType;
|
||||
use bytes::BufMut;
|
||||
use http::{StatusCode, header};
|
||||
use oxigraph::io::{RdfFormat, RdfSerializer};
|
||||
@@ -11,8 +12,11 @@ use reqwest_middleware::reqwest::Url;
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RdfSource<D> {
|
||||
pub(crate) origin: Url,
|
||||
pub(crate) described_by: Option<Url>,
|
||||
pub(crate) origin_type: Option<ResourceType>,
|
||||
pub(crate) state_token: Option<String>,
|
||||
pub(crate) described_by: Option<Url>,
|
||||
pub(crate) file_name: Option<String>,
|
||||
pub(crate) size: Option<usize>,
|
||||
pub(crate) dataset: D,
|
||||
}
|
||||
|
||||
@@ -23,8 +27,11 @@ impl<D: Default> RdfSource<D> {
|
||||
pub fn new(origin: Url) -> Self {
|
||||
Self {
|
||||
origin,
|
||||
described_by: None,
|
||||
origin_type: None,
|
||||
state_token: None,
|
||||
described_by: None,
|
||||
file_name: None,
|
||||
size: None,
|
||||
dataset: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -36,11 +43,9 @@ impl<D> RdfSource<D> {
|
||||
&self.origin
|
||||
}
|
||||
|
||||
/// The URL at which the RDF description of the resource is located.
|
||||
///
|
||||
/// This is the URL at which updates are submitted.
|
||||
pub fn described_by(&self) -> Option<&Url> {
|
||||
self.described_by.as_ref()
|
||||
/// The type of Resource that was originally fetched.
|
||||
pub fn origin_type(&self) -> Option<&ResourceType> {
|
||||
self.origin_type.as_ref()
|
||||
}
|
||||
|
||||
/// The state token of the resource.
|
||||
@@ -50,6 +55,23 @@ impl<D> RdfSource<D> {
|
||||
self.state_token.as_deref()
|
||||
}
|
||||
|
||||
/// The URL at which the RDF description of the resource is located.
|
||||
///
|
||||
/// This is the URL at which updates are submitted.
|
||||
pub fn described_by(&self) -> Option<&Url> {
|
||||
self.described_by.as_ref()
|
||||
}
|
||||
|
||||
/// The name of the file that was originally fetched.
|
||||
pub fn origin_file_name(&self) -> Option<&str> {
|
||||
self.file_name.as_deref()
|
||||
}
|
||||
|
||||
/// The size of the file that was originally fetched.
|
||||
pub fn origin_size(&self) -> Option<usize> {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// The underlying dataset.
|
||||
pub fn dataset(&self) -> &D {
|
||||
&self.dataset
|
||||
@@ -72,7 +94,7 @@ impl<D> RdfSource<D> {
|
||||
.unwrap_or(self.origin().as_str());
|
||||
Quad::new(
|
||||
NamedOrBlankNode::NamedNode(NamedNode::new_unchecked(self.origin.clone())),
|
||||
vocab::rdf::VALUE,
|
||||
vocab::rdf::TYPE,
|
||||
Term::Literal(Literal::new_simple_literal("")),
|
||||
GraphName::NamedNode(NamedNode::new_unchecked(graph_name)),
|
||||
)
|
||||
@@ -100,12 +122,12 @@ impl<D> RdfSource<D> {
|
||||
}
|
||||
|
||||
/// Holds options related to serialization.
|
||||
pub struct SerializationOptions {
|
||||
pub struct SerializationOptions<'a> {
|
||||
format: RdfFormat,
|
||||
filter: Box<dyn Fn(TripleRef<'_>) -> bool>,
|
||||
filter: Box<dyn Fn(TripleRef<'a>) -> bool>,
|
||||
}
|
||||
|
||||
impl SerializationOptions {
|
||||
impl<'a> SerializationOptions<'a> {
|
||||
/// Create a new set of serialization options with the provided format.
|
||||
pub fn from_format(format: RdfFormat) -> Self {
|
||||
Self {
|
||||
@@ -120,7 +142,7 @@ impl SerializationOptions {
|
||||
#[must_use]
|
||||
pub fn with_filter<F>(self, filter: F) -> Self
|
||||
where
|
||||
F: Fn(TripleRef<'_>) -> bool + 'static,
|
||||
F: Fn(TripleRef<'a>) -> bool + 'static,
|
||||
{
|
||||
Self {
|
||||
format: self.format,
|
||||
@@ -134,7 +156,7 @@ where
|
||||
&'a D: IntoIterator<Item = QuadRef<'a>>,
|
||||
{
|
||||
/// Serializes the dataset in to the provided format.
|
||||
pub fn serialize(&'a self, options: SerializationOptions) -> crate::Result<bytes::Bytes> {
|
||||
pub fn serialize(&'a self, options: SerializationOptions<'a>) -> crate::Result<bytes::Bytes> {
|
||||
let writer = bytes::BytesMut::new().writer();
|
||||
let mut serializer = RdfSerializer::from_format(options.format).for_writer(writer);
|
||||
|
||||
@@ -159,7 +181,7 @@ where
|
||||
/// Prepare an update request.
|
||||
pub fn to_update(
|
||||
&'a self,
|
||||
options: SerializationOptions,
|
||||
options: SerializationOptions<'a>,
|
||||
) -> crate::Result<RdfSourceUpdateRequest> {
|
||||
let url = self.described_by.clone().unwrap_or(self.origin.clone());
|
||||
let media_type = options.format.media_type().to_string();
|
||||
|
||||
+234
-17
@@ -1,11 +1,16 @@
|
||||
use crate::container::ContainerType;
|
||||
use crate::rdf_source::RdfSource;
|
||||
use crate::vocab;
|
||||
use crate::{Preference, prefer, vocab};
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use http::Method;
|
||||
use oxigraph::io::{RdfFormat, RdfParser};
|
||||
use oxigraph::model::{GraphNameRef, NamedNodeRef, Quad};
|
||||
use reqwest_middleware::reqwest::{Client, Response, StatusCode, Url, header};
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
|
||||
use sfv::{Item, TokenRef};
|
||||
use std::collections::BTreeSet;
|
||||
use std::str::FromStr;
|
||||
use tracing::error;
|
||||
|
||||
/// Builds an HTTP request for a [Resource](https://www.w3.org/TR/ldp/#ldpr).
|
||||
@@ -34,7 +39,10 @@ pub struct ResourceRequestBuilder {
|
||||
client: ClientWithMiddleware,
|
||||
url: Url,
|
||||
follow_described_by: bool,
|
||||
validate_support: bool,
|
||||
formats: Vec<RdfFormat>,
|
||||
include_preferences: Vec<Preference>,
|
||||
omit_preferences: Vec<Preference>,
|
||||
}
|
||||
|
||||
impl ResourceRequestBuilder {
|
||||
@@ -52,17 +60,37 @@ impl ResourceRequestBuilder {
|
||||
client,
|
||||
url,
|
||||
follow_described_by: true,
|
||||
validate_support: true,
|
||||
formats: Vec::new(),
|
||||
include_preferences: Vec::new(),
|
||||
omit_preferences: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// If the HTTP response includes a [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby) link rel, then it will be followed.
|
||||
/// If the HTTP response includes a [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby)
|
||||
/// link rel, then it will be followed. The original URL may be accessed using [`Resource::origin`],
|
||||
/// and the link rel may be accessed using [`Resource::described_by`].
|
||||
///
|
||||
/// Note: If your intention is to fetch a NonRDFSource (e.g. a PDF, video, etc.), this value
|
||||
/// must be `false`.
|
||||
///
|
||||
/// Default value: `true`.
|
||||
#[must_use]
|
||||
pub fn follow_described_by(mut self, value: bool) -> Self {
|
||||
self.follow_described_by = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Require the presence of a `Link: <http://www.w3.org/ns/ldp#Resource>; rel="type"` response header.
|
||||
/// This is described in [section 4.2.1.4](https://www.w3.org/TR/ldp/#ldpr-resource) of the spec.
|
||||
///
|
||||
/// Default value: `true`.
|
||||
#[must_use]
|
||||
pub fn validate_support(mut self, value: bool) -> Self {
|
||||
self.validate_support = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Restrict the request to the given RDF format.
|
||||
///
|
||||
/// Repeated calls are additive. By default, all formats — even non-RDF formats — are accepted.
|
||||
@@ -87,6 +115,18 @@ impl ResourceRequestBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Include a representation preference according to [section 7.2](https://www.w3.org/TR/ldp/#prefer-parameters).
|
||||
pub fn include_preference(mut self, preference: Preference) -> Self {
|
||||
self.include_preferences.push(preference);
|
||||
self
|
||||
}
|
||||
|
||||
/// Omit a representation preference according to [section 7.2](https://www.w3.org/TR/ldp/#prefer-parameters).
|
||||
pub fn omit_preference(mut self, preference: Preference) -> Self {
|
||||
self.omit_preferences.push(preference);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the request.
|
||||
pub fn build(self) -> ResourceRequest {
|
||||
ResourceRequest { builder: self }
|
||||
@@ -125,7 +165,7 @@ impl ResourceRequest {
|
||||
if response.status() == StatusCode::OK {
|
||||
let headers = response.headers();
|
||||
for link in headers.get_all(header::LINK) {
|
||||
let link = link.to_str().unwrap_or("");
|
||||
let link = link.to_str().unwrap_or_default();
|
||||
match parse_link_header::parse(link) {
|
||||
Ok(link_map) => {
|
||||
if let Some(metadata_url) = link_map.get(&Some("type".to_string())) {
|
||||
@@ -142,6 +182,34 @@ impl ResourceRequest {
|
||||
Err(crate::Error::LDPUnsupported)
|
||||
}
|
||||
|
||||
fn resource_type(response: &Response) -> Option<ResourceType> {
|
||||
let headers = response.headers();
|
||||
let links = headers
|
||||
.get_all(header::LINK)
|
||||
.iter()
|
||||
.map(|hv| hv.to_str().unwrap_or_default())
|
||||
.filter_map(|link| parse_link_header::parse_with_rel(link).ok())
|
||||
.filter_map(|link_map| link_map.get("type").map(|item| item.raw_uri.clone()))
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
// NB: The comparisons below must be done in this order, because multiple Link headers may
|
||||
// be present with differing degrees of specificity. E.g. BasicContainer is more specific
|
||||
// than RDFSource, but both are valid and may be present concomitantly.
|
||||
if links.contains(vocab::ldp::BASIC_CONTAINER.as_str()) {
|
||||
Some(ResourceType::RdfSource(Some(ContainerType::Basic)))
|
||||
} else if links.contains(vocab::ldp::DIRECT_CONTAINER.as_str()) {
|
||||
Some(ResourceType::RdfSource(Some(ContainerType::Direct)))
|
||||
} else if links.contains(vocab::ldp::INDIRECT_CONTAINER.as_str()) {
|
||||
Some(ResourceType::RdfSource(Some(ContainerType::Indirect)))
|
||||
} else if links.contains(vocab::ldp::RDF_SOURCE.as_str()) {
|
||||
Some(ResourceType::RdfSource(None))
|
||||
} else if links.contains(vocab::ldp::NON_RDF_SOURCE.as_str()) {
|
||||
Some(ResourceType::NonRdfSource)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn add_media_types(&self, mut request_builder: RequestBuilder) -> RequestBuilder {
|
||||
let media_types = self.builder.formats.iter().map(|f| f.media_type());
|
||||
for media_type in media_types {
|
||||
@@ -150,19 +218,80 @@ impl ResourceRequest {
|
||||
request_builder
|
||||
}
|
||||
|
||||
fn is_method_allowed(response: &Response, method: Method) -> crate::Result<bool> {
|
||||
for header in response.headers().get_all(header::ALLOW) {
|
||||
for value in header.to_str()?.replace(" ", "").split(',') {
|
||||
if value == method {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Send the request.
|
||||
///
|
||||
/// There are two stages to this process. First, a HEAD request is made to determine whether
|
||||
/// There are two stages to this process. First, a `HEAD` request is made to determine whether
|
||||
/// the requested resource is described by an RDF graph at another location. If it is, and if
|
||||
/// the user permits it, then the URL in the
|
||||
/// the user [permits it](`ResourceRequestBuilder::follow_described_by`), then the URL in the
|
||||
/// [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby) header is used.
|
||||
/// Otherwise, the original URL is used.
|
||||
///
|
||||
/// During the second stage, a GET request is made. No parsing occurs at this time.
|
||||
/// During the second stage, a `GET` request is made. **The response body is not consumed yet.**
|
||||
/// To consume the response body, you must call either [`Resource::into_stream`] to receive the
|
||||
/// raw bytes or [`Resource::into_rdf_source`] to parse the content and receive a [`RdfSource`].
|
||||
///
|
||||
/// If the `HEAD` method is not allowed on this URL (a violation of the LDP spec), the `Allow`
|
||||
/// header is inspected for `GET`. If absent, this method will return an error.
|
||||
pub async fn send(&self) -> crate::Result<Resource> {
|
||||
let request_builder = self.builder.client.head(self.builder.url.clone());
|
||||
let mut response = request_builder.send().await?.error_for_status()?;
|
||||
Self::ensure_ldp_support(&response)?;
|
||||
let mut response = request_builder.send().await?;
|
||||
|
||||
match response.error_for_status_ref() {
|
||||
Ok(response) => {
|
||||
if self.builder.validate_support {
|
||||
Self::ensure_ldp_support(response)?;
|
||||
}
|
||||
}
|
||||
Err(err) if err.status() == Some(StatusCode::METHOD_NOT_ALLOWED) => {
|
||||
if !Self::is_method_allowed(&response, Method::GET)? {
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
err => {
|
||||
err?;
|
||||
}
|
||||
}
|
||||
|
||||
let size = response
|
||||
.headers()
|
||||
.get(header::CONTENT_LENGTH)
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.and_then(|et| usize::from_str(et).ok());
|
||||
|
||||
let content_disposition = response
|
||||
.headers()
|
||||
.get(header::CONTENT_DISPOSITION)
|
||||
.and_then(|hv| hv.to_str().ok());
|
||||
|
||||
let file_name = if let Some(content_disposition) = content_disposition {
|
||||
sfv::Parser::new(content_disposition)
|
||||
.parse::<Item>()
|
||||
.ok()
|
||||
.and_then(|item| {
|
||||
if item.bare_item.as_token() == Some(TokenRef::constant("attachment")) {
|
||||
item.params
|
||||
.get("filename")
|
||||
.and_then(|item| item.as_string().map(|s| s.to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let resource_type = Self::resource_type(&response);
|
||||
|
||||
let url_to_get;
|
||||
let described_by = Self::extract_described_by(&response);
|
||||
@@ -177,8 +306,20 @@ impl ResourceRequest {
|
||||
let mut request_builder = self.builder.client.get(url_to_get);
|
||||
request_builder = self.add_media_types(request_builder);
|
||||
|
||||
if !self.builder.include_preferences.is_empty() || !self.builder.omit_preferences.is_empty()
|
||||
{
|
||||
let header = prefer::header_for_preferences(
|
||||
&self.builder.include_preferences,
|
||||
&self.builder.omit_preferences,
|
||||
)?;
|
||||
request_builder = request_builder.headers(header);
|
||||
}
|
||||
|
||||
response = request_builder.send().await?.error_for_status()?;
|
||||
Self::ensure_ldp_support(&response)?;
|
||||
|
||||
if self.builder.validate_support {
|
||||
Self::ensure_ldp_support(&response)?;
|
||||
}
|
||||
|
||||
let state_token = response
|
||||
.headers()
|
||||
@@ -202,12 +343,47 @@ impl ResourceRequest {
|
||||
origin: self.builder.url.clone(),
|
||||
described_by,
|
||||
state_token,
|
||||
resource_type,
|
||||
format,
|
||||
file_name,
|
||||
size,
|
||||
response,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of [Resource](https://www.w3.org/TR/ldp/#ldpr-resource).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ResourceType {
|
||||
NonRdfSource,
|
||||
RdfSource(Option<ContainerType>),
|
||||
}
|
||||
|
||||
impl ResourceType {
|
||||
pub fn as_named_node_ref(&self) -> NamedNodeRef<'_> {
|
||||
match self {
|
||||
ResourceType::NonRdfSource => vocab::ldp::NON_RDF_SOURCE,
|
||||
ResourceType::RdfSource(None) => vocab::ldp::RDF_SOURCE,
|
||||
ResourceType::RdfSource(Some(container_type)) => container_type.as_named_node_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_named_node_ref(node: NamedNodeRef<'_>) -> Option<Self> {
|
||||
match node {
|
||||
vocab::ldp::NON_RDF_SOURCE => Some(ResourceType::NonRdfSource),
|
||||
vocab::ldp::RDF_SOURCE => Some(ResourceType::RdfSource(None)),
|
||||
_ => {
|
||||
let container_type = ContainerType::from_named_node_ref(node);
|
||||
if container_type.is_some() {
|
||||
Some(ResourceType::RdfSource(container_type))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The format of the response, as determined by the `Content-Type` HTTP header.
|
||||
pub enum ResponseFormat {
|
||||
RdfFormat(RdfFormat),
|
||||
@@ -217,17 +393,20 @@ pub enum ResponseFormat {
|
||||
|
||||
/// A LDP [Resource](https://www.w3.org/TR/ldp/#ldpr).
|
||||
///
|
||||
/// A Resource on its own is not very useful, as it is a very abstract concept. If the intention is
|
||||
/// to treat the response as opaque data (such as a media file), call [`Resource::into_stream`]. In
|
||||
/// the spec, this is equivalent to a [NonRDFSource](https://www.w3.org/TR/ldp/#ldpnr).
|
||||
/// A Resource on its own is not very useful, as it is a very abstract concept. If your intention is
|
||||
/// to treat the response as opaque data (such as a PDF or video file), call [`Resource::into_stream`].
|
||||
/// In the spec, this is equivalent to a [NonRDFSource](https://www.w3.org/TR/ldp/#ldpnr).
|
||||
///
|
||||
/// If the intention is to treat the response as RDF data to be parsed, call
|
||||
/// If your intention is to treat the response as RDF data to be parsed, call
|
||||
/// [`Resource::into_rdf_source`].
|
||||
pub struct Resource {
|
||||
origin: Url,
|
||||
described_by: Option<Url>,
|
||||
state_token: Option<String>,
|
||||
resource_type: Option<ResourceType>,
|
||||
format: ResponseFormat,
|
||||
file_name: Option<String>,
|
||||
size: Option<usize>,
|
||||
response: Response,
|
||||
}
|
||||
|
||||
@@ -244,24 +423,59 @@ impl Resource {
|
||||
self.described_by.as_ref()
|
||||
}
|
||||
|
||||
/// The format of the response.
|
||||
/// The format of the response, as reported by the `Content-Type` HTTP header.
|
||||
pub fn format(&self) -> &ResponseFormat {
|
||||
&self.format
|
||||
}
|
||||
|
||||
/// The size of the content, as reported by the `Content-Length` HTTP header.
|
||||
///
|
||||
/// Note: This value is extracted from the response headers of the HEAD request for the origin
|
||||
/// URL.
|
||||
pub fn size(&self) -> Option<usize> {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// The file name of the content, as reported by the `Content-Disposition` HTTP header.
|
||||
/// Example:
|
||||
/// ```
|
||||
/// Content-Disposition: attachment; filename="new-king-james-version-en.pdf"
|
||||
/// ```
|
||||
///
|
||||
/// Note: This value is extracted from the response headers of the HEAD request for the origin
|
||||
/// URL.
|
||||
pub fn file_name(&self) -> Option<&str> {
|
||||
self.file_name.as_deref()
|
||||
}
|
||||
|
||||
/// The type of Resource, as reported by the `Link: <...> rel="type"` HTTP header.
|
||||
///
|
||||
/// Note: This value is extracted from the response headers of the HEAD request for the origin
|
||||
/// URL.
|
||||
pub fn resource_type(&self) -> Option<&ResourceType> {
|
||||
self.resource_type.as_ref()
|
||||
}
|
||||
|
||||
/// The state token, as extracted from the `X-State-Token` header.
|
||||
///
|
||||
/// Note that this is a feature [specific to Fedora](https://fedora.info/2021/05/01/spec/#state-tokens).
|
||||
/// <div class="warning">
|
||||
/// This is a feature specific to <a href="https://fedora.info/2021/05/01/spec/#state-tokens">Fedora</a>.
|
||||
/// </div>
|
||||
pub fn state_token(&self) -> Option<&String> {
|
||||
self.state_token.as_ref()
|
||||
}
|
||||
|
||||
/// Extract the response as a stream of unparsed bytes.
|
||||
/// Provide the response body as a stream of bytes.
|
||||
pub fn into_stream(self) -> impl Stream<Item = reqwest_middleware::reqwest::Result<Bytes>> {
|
||||
self.response.bytes_stream()
|
||||
}
|
||||
|
||||
/// Parse the response.
|
||||
///
|
||||
/// The `D` type parameter can be any type capable of storing [`Quad`]s. If you intend to query
|
||||
/// the dataset, use [`Dataset`](`oxigraph::model::Dataset`). If you want a stable ordering of the
|
||||
/// quads, a [`KeyedDataset`](`crate::model::KeyedDataset`), [`Vec`], or similar data structure
|
||||
/// may be used.
|
||||
pub async fn into_rdf_source<D>(self) -> crate::Result<RdfSource<D>>
|
||||
where
|
||||
D: FromIterator<Quad>,
|
||||
@@ -275,8 +489,11 @@ impl Resource {
|
||||
let dataset = quads.collect::<Result<_, _>>()?;
|
||||
Ok(RdfSource {
|
||||
origin: self.origin,
|
||||
described_by: self.described_by,
|
||||
origin_type: self.resource_type,
|
||||
state_token: self.state_token,
|
||||
described_by: self.described_by,
|
||||
file_name: self.file_name,
|
||||
size: self.size,
|
||||
dataset,
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -7,6 +7,7 @@ use reqwest_middleware::reqwest::Url;
|
||||
use std::collections::HashSet;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll, ready};
|
||||
use tracing::debug_span;
|
||||
|
||||
/// Recursively traverse an entire repository.
|
||||
///
|
||||
@@ -21,6 +22,7 @@ pub struct Traverse<'a> {
|
||||
visited_urls: HashSet<Url>,
|
||||
pending_urls: HashSet<Url>,
|
||||
parallelism: Option<usize>,
|
||||
span: tracing::Span,
|
||||
}
|
||||
|
||||
impl<'a> Traverse<'a> {
|
||||
@@ -35,6 +37,7 @@ impl<'a> Traverse<'a> {
|
||||
visited_urls: HashSet::new(),
|
||||
pending_urls: HashSet::new(),
|
||||
parallelism,
|
||||
span: debug_span!("Traverse Repository", %root),
|
||||
};
|
||||
this.visited_urls.insert(root.clone());
|
||||
this.stream
|
||||
@@ -89,6 +92,7 @@ impl<'a> Stream for Traverse<'a> {
|
||||
type Item = crate::Result<RdfSource<Dataset>>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let _enter = self.span.clone().entered();
|
||||
let result = match ready!(self.stream.poll_next_unpin(cx)) {
|
||||
Some(Ok(document)) => {
|
||||
self.visited_urls.insert(document.origin().clone());
|
||||
|
||||
@@ -5,4 +5,23 @@ pub mod ldp {
|
||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#contains");
|
||||
pub const RESOURCE: NamedNodeRef<'_> =
|
||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#Resource");
|
||||
pub const NON_RDF_SOURCE: NamedNodeRef<'_> =
|
||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#NonRDFSource");
|
||||
pub const RDF_SOURCE: NamedNodeRef<'_> =
|
||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#RDFSource");
|
||||
pub const CONTAINER: NamedNodeRef<'_> =
|
||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#Container");
|
||||
pub const BASIC_CONTAINER: NamedNodeRef<'_> =
|
||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#BasicContainer");
|
||||
pub const DIRECT_CONTAINER: NamedNodeRef<'_> =
|
||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#DirectContainer");
|
||||
pub const INDIRECT_CONTAINER: NamedNodeRef<'_> =
|
||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#IndirectContainer");
|
||||
|
||||
pub const PREFER_CONTAINMENT: NamedNodeRef<'_> =
|
||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#PreferContainment");
|
||||
pub const PREFER_MEMBERSHIP: NamedNodeRef<'_> =
|
||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#PreferMembership");
|
||||
pub const PREFER_MINIMAL_CONTAINER: NamedNodeRef<'_> =
|
||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#PreferMinimalContainer");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user